+}
+
+function Row ({ name }) {
+ return (
+ {name}
+ )
+}
+
+function SavedSearchDropdown(props: Props) {
+ return (
+
+ {props.list.map(item => (
+
+ ))}
+
+ );
+}
+
+export default SavedSearchDropdown;
\ No newline at end of file
diff --git a/frontend/app/components/shared/SavedSearch/components/SavedSearchDropdown/index.ts b/frontend/app/components/shared/SavedSearch/components/SavedSearchDropdown/index.ts
new file mode 100644
index 000000000..2fea67949
--- /dev/null
+++ b/frontend/app/components/shared/SavedSearch/components/SavedSearchDropdown/index.ts
@@ -0,0 +1 @@
+export { default } from './SavedSearchDropdown';
\ No newline at end of file
diff --git a/frontend/app/components/shared/SavedSearch/index.ts b/frontend/app/components/shared/SavedSearch/index.ts
new file mode 100644
index 000000000..71c14305d
--- /dev/null
+++ b/frontend/app/components/shared/SavedSearch/index.ts
@@ -0,0 +1 @@
+export { default } from './SavedSearch'
\ No newline at end of file
diff --git a/frontend/app/components/shared/SessionSearchField/SessionSearchField.css b/frontend/app/components/shared/SessionSearchField/SessionSearchField.css
new file mode 100644
index 000000000..6a3a268ba
--- /dev/null
+++ b/frontend/app/components/shared/SessionSearchField/SessionSearchField.css
@@ -0,0 +1,10 @@
+.searchField {
+ box-shadow: none !important;
+ & input {
+ box-shadow: none !important;
+ border-radius: 3 !important;
+ border: solid thin $gray-light !important;
+ height: 34px !important;
+ font-size: 16px;
+ }
+}
\ No newline at end of file
diff --git a/frontend/app/components/shared/SessionSearchField/SessionSearchField.tsx b/frontend/app/components/shared/SessionSearchField/SessionSearchField.tsx
new file mode 100644
index 000000000..76aac5240
--- /dev/null
+++ b/frontend/app/components/shared/SessionSearchField/SessionSearchField.tsx
@@ -0,0 +1,69 @@
+import React, { useRef, useState } from 'react';
+import { connect } from 'react-redux';
+import stl from './SessionSearchField.css';
+import { Input } from 'UI';
+import FilterModal from 'Shared/EventFilter/FilterModal';
+import { fetchList as fetchEventList } from 'Duck/events';
+import { debounce } from 'App/utils';
+import {
+ addEvent, applyFilter, moveEvent, clearEvents,
+ addCustomFilter, addAttribute, setSearchQuery, setActiveFlow, setFilterOption
+} from 'Duck/filters';
+
+interface Props {
+ setSearchQuery: (query: string) => void;
+ fetchEventList: (query: any) => void;
+ searchQuery: string
+}
+function SessionSearchField(props: Props) {
+ const debounceFetchEventList = debounce(props.fetchEventList, 1000)
+ const [showModal, setShowModal] = useState(false)
+
+ const onSearchChange = (e, { value }) => {
+ // props.setSearchQuery(value)
+ debounceFetchEventList({ q: value });
+ }
+
+ return (
+
+ setShowModal(true) }
+ onBlur={ () => setTimeout(setShowModal, 100, false) }
+ // ref={ this.inputRef }
+ onChange={ onSearchChange }
+ // onKeyUp={this.onKeyUp}
+ // value={props.searchQuery}
+ icon="search"
+ iconPosition="left"
+ placeholder={ 'Search sessions using any captured event (click, input, page, error...)'}
+ fluid
+ id="search"
+ type="search"
+ autocomplete="off"
+ />
+
+ setShowModal(false) }
+ displayed={ showModal }
+ // displayed={ true }
+ // loading={ loading }
+ // searchedEvents={ searchedEvents }
+ searchQuery={ props.searchQuery }
+ />
+
+ );
+}
+
+export default connect(state => ({
+ events: state.getIn([ 'filters', 'appliedFilter', 'events' ]),
+ appliedFilter: state.getIn([ 'filters', 'appliedFilter' ]),
+ searchQuery: state.getIn([ 'filters', 'searchQuery' ]),
+ appliedFilterKeys: state.getIn([ 'filters', 'appliedFilter', 'filters' ])
+ .map(({type}) => type).toJS(),
+ searchedEvents: state.getIn([ 'events', 'list' ]),
+ loading: state.getIn([ 'events', 'loading' ]),
+ strict: state.getIn([ 'filters', 'appliedFilter', 'strict' ]),
+ blink: state.getIn([ 'funnels', 'blink' ]),
+}), { setSearchQuery, fetchEventList })(SessionSearchField);
\ No newline at end of file
diff --git a/frontend/app/components/shared/SessionSearchField/index.ts b/frontend/app/components/shared/SessionSearchField/index.ts
new file mode 100644
index 000000000..1f99e0c0b
--- /dev/null
+++ b/frontend/app/components/shared/SessionSearchField/index.ts
@@ -0,0 +1 @@
+export { default } from './SessionSearchField';
\ No newline at end of file
diff --git a/frontend/app/components/ui/SegmentSelection/SegmentSelection.js b/frontend/app/components/ui/SegmentSelection/SegmentSelection.js
index c4d71a93a..d1d3ae6df 100644
--- a/frontend/app/components/ui/SegmentSelection/SegmentSelection.js
+++ b/frontend/app/components/ui/SegmentSelection/SegmentSelection.js
@@ -9,12 +9,13 @@ class SegmentSelection extends React.Component {
}
render() {
- const { className, list, primary = false, size = "normal" } = this.props;
+ const { className, list, small = false, extraSmall = false, primary = false, size = "normal" } = this.props;
return (
{ list.map(item => (
diff --git a/frontend/app/components/ui/SegmentSelection/segmentSelection.css b/frontend/app/components/ui/SegmentSelection/segmentSelection.css
index 543016246..a74d7d77b 100644
--- a/frontend/app/components/ui/SegmentSelection/segmentSelection.css
+++ b/frontend/app/components/ui/SegmentSelection/segmentSelection.css
@@ -61,4 +61,9 @@
.small .item {
padding: 4px 8px;
+}
+
+.extraSmall .item {
+ padding: 2px 4px;
+ font-size: 12px;
}
\ No newline at end of file
diff --git a/frontend/app/duck/customMetrics.js b/frontend/app/duck/customMetrics.js
new file mode 100644
index 000000000..0909ff3b1
--- /dev/null
+++ b/frontend/app/duck/customMetrics.js
@@ -0,0 +1,105 @@
+import { List, Map } from 'immutable';
+import { clean as cleanParams } from 'App/api_client';
+import ErrorInfo, { RESOLVED, UNRESOLVED, IGNORED } from 'Types/errorInfo';
+import CustomMetric, { FilterSeries } from 'Types/customMetric'
+import { createFetch, fetchListType, fetchType, saveType, editType, createEdit } from './funcTools/crud';
+// import { createEdit, createInit } from './funcTools/crud';
+import { createRequestReducer, ROOT_KEY } from './funcTools/request';
+import { array, request, success, failure, createListUpdater, mergeReducers } from './funcTools/tools';
+import Filter from 'Types/filter';
+import NewFilter from 'Types/filter/newFilter';
+import Event from 'Types/filter/event';
+// import CustomFilter from 'Types/filter/customFilter';
+
+const name = "custom_metric";
+const idKey = "metricId";
+
+const FETCH_LIST = fetchListType(name);
+const FETCH = fetchType(name);
+const SAVE = saveType(name);
+const EDIT = editType(name);
+const UPDATE_SERIES = `${name}/UPDATE_SERIES`;
+
+function chartWrapper(chart = []) {
+ return chart.map(point => ({ ...point, count: Math.max(point.count, 0) }));
+}
+
+// const updateItemInList = createListUpdater(idKey);
+// const updateInstance = (state, instance) => state.getIn([ "instance", idKey ]) === instance[ idKey ]
+// ? state.mergeIn([ "instance" ], instance)
+// : state;
+
+const initialState = Map({
+ list: List(),
+ instance: CustomMetric({
+ name: 'New',
+ series: List([
+ {
+ name: 'Session Count',
+ filter: new Filter({ filters: [] }),
+ },
+ ])
+ }),
+});
+
+// Metric - Series - [] - filters
+function reducer(state = initialState, action = {}) {
+ switch (action.type) {
+ case EDIT:
+ return state.mergeIn([ 'instance' ], CustomMetric(action.instance));
+ case UPDATE_SERIES:
+ return state.setIn(['instance', 'series', action.index], FilterSeries(action.series));
+ case success(SAVE):
+ return state.set([ 'instance' ], CustomMetric(action.data));
+ case success(FETCH):
+ return state.set("instance", ErrorInfo(action.data));
+ case success(FETCH_LIST):
+ const { data } = action;
+ return state
+ .set("totalCount", data ? data.total : 0)
+ .set("list", List(data && data.errors).map(CustomMetric)
+ .filter(e => e.parentErrorId == null)
+ .map(e => e.update("chart", chartWrapper)));
+ }
+ return state;
+}
+
+export default mergeReducers(
+ reducer,
+ createRequestReducer({
+ [ ROOT_KEY ]: FETCH_LIST,
+ fetch: FETCH,
+ }),
+);
+
+export const edit = createEdit(name);
+
+export const updateSeries = (index, series) => ({
+ type: UPDATE_SERIES,
+ index,
+ series,
+});
+
+export function fetch(id) {
+ return {
+ id,
+ types: array(FETCH),
+ call: c => c.get(`/errors/${id}`),
+ }
+}
+
+export function save(instance) {
+ return {
+ types: SAVE.array,
+ call: client => client.post( `/${ name }s`, instance.toData()),
+ };
+}
+
+export function fetchList(params = {}, clear = false) {
+ return {
+ types: array(FETCH_LIST),
+ call: client => client.post('/errors/search', params),
+ clear,
+ params: cleanParams(params),
+ };
+}
\ No newline at end of file
diff --git a/frontend/app/duck/filters.js b/frontend/app/duck/filters.js
index a06a0b6bc..290a4f1cb 100644
--- a/frontend/app/duck/filters.js
+++ b/frontend/app/duck/filters.js
@@ -7,8 +7,33 @@ import CustomFilter, { KEYS } from 'Types/filter/customFilter';
import withRequestState, { RequestTypes } from './requestStateCreator';
import { fetchList as fetchSessionList } from './sessions';
import { fetchList as fetchErrorsList } from './errors';
+import { fetchListType, fetchType, saveType, editType, initType, removeType } from './funcTools/crud/types';
import logger from 'App/logger';
+import { newFiltersList } from 'Types/filter'
+import NewFilter, { filtersMap } from 'Types/filter/newFilter';
+
+const filterOptions = {}
+// newFiltersList.forEach(filter => {
+// filterOptions[filter.category] = filter
+// })
+
+Object.keys(filtersMap).forEach(key => {
+ const filter = filtersMap[key];
+ if (filterOptions.hasOwnProperty(filter.category)) {
+ filterOptions[filter.category].push(filter);
+ } else {
+ filterOptions[filter.category] = [filter];
+ }
+})
+
+console.log('filterOptions', filterOptions)
+
+
+// for (var i = 0; i < newFiltersList.length; i++) {
+// filterOptions[newFiltersList[i].category] = newFiltersList.filter(filter => filter.category === newFiltersList[i].category)
+// }
+
const ERRORS_ROUTE = errorsRoute();
const FETCH_LIST = new RequestTypes('filters/FETCH_LIST');
@@ -16,6 +41,7 @@ const FETCH_FILTER_OPTIONS = new RequestTypes('filters/FETCH_FILTER_OPTIONS');
const SET_FILTER_OPTIONS = 'filters/SET_FILTER_OPTIONS';
const SAVE = new RequestTypes('filters/SAVE');
const REMOVE = new RequestTypes('filters/REMOVE');
+const EDIT = editType('funnel/EDIT');
const SET_SEARCH_QUERY = 'filters/SET_SEARCH_QUERY';
const SET_ACTIVE = 'filters/SET_ACTIVE';
@@ -35,7 +61,11 @@ const EDIT_ATTRIBUTE = 'filters/EDIT_ATTRIBUTE';
const REMOVE_ATTRIBUTE = 'filters/REMOVE_ATTRIBUTE';
const SET_ACTIVE_FLOW = 'filters/SET_ACTIVE_FLOW';
+const UPDATE_VALUE = 'filters/UPDATE_VALUE';
+
const initialState = Map({
+ filterList: filterOptions,
+ instance: Filter(),
activeFilter: null,
list: List(),
appliedFilter: Filter(),
@@ -71,6 +101,8 @@ const updateList = (state, instance) => state.update('list', (list) => {
const reducer = (state = initialState, action = {}) => {
let optionsMap = null;
switch (action.type) {
+ case EDIT:
+ return state.mergeIn([ 'appliedFilter' ], action.instance);
case FETCH_FILTER_OPTIONS.SUCCESS:
optionsMap = state.getIn(['filterOptions', action.key]).map(i => i.value).toJS();
return state.mergeIn(['filterOptions', action.key], Set(action.data.filter(i => !optionsMap.includes(i.value))));
@@ -177,6 +209,8 @@ const reducer = (state = initialState, action = {}) => {
return state.removeIn([ 'appliedFilter', 'filters', action.index ]);
case SET_SEARCH_QUERY:
return state.set('searchQuery', action.query);
+ case UPDATE_VALUE:
+ return state.setIn([ 'appliedFilter', action.filterType, action.index, 'value' ], action.value);
default:
return state;
}
@@ -234,7 +268,7 @@ export function removeAttribute(index) {
export function fetchList(range) {
return {
types: FETCH_LIST.toArray(),
- call: client => client.get(`/flows${range ? '?range_value=' + range : ''}`),
+ call: client => client.get(`/saved_search`),
};
}
@@ -257,7 +291,7 @@ export function setFilterOption(key, filterOption) {
export function save(instance) {
return {
types: SAVE.toArray(),
- call: client => client.post('/filters', instance.toData()),
+ call: client => client.post('/saved_search', instance.toData()),
instance,
};
}
@@ -367,4 +401,21 @@ export function setSearchQuery(query) {
type: SET_SEARCH_QUERY,
query
}
+}
+
+export const edit = instance => {
+ return {
+ type: EDIT,
+ instance,
+ }
+};
+
+// filterType: 'events' or 'filters'
+export const updateValue = (filterType, index, value) => {
+ return {
+ type: UPDATE_VALUE,
+ filterType,
+ index,
+ value
+ }
}
\ No newline at end of file
diff --git a/frontend/app/duck/funnels.js b/frontend/app/duck/funnels.js
index 4e591237c..7a611dbca 100644
--- a/frontend/app/duck/funnels.js
+++ b/frontend/app/duck/funnels.js
@@ -117,9 +117,8 @@ const reducer = (state = initialState, action = {}) => {
.set('issueTypesMap', tmpMap);
case FETCH_INSIGHTS_SUCCESS:
let stages = [];
- if (action.isRefresh) {
+ if (action.isRefresh) {
const activeStages = state.get('activeStages');
- console.log('test', activeStages);
const oldInsights = state.get('insights');
const lastStage = action.data.stages[action.data.stages.length - 1]
const lastStageIndex = activeStages.toJS()[1];
diff --git a/frontend/app/duck/index.js b/frontend/app/duck/index.js
index c8d7a7c65..71eb6d0e2 100644
--- a/frontend/app/duck/index.js
+++ b/frontend/app/duck/index.js
@@ -34,6 +34,7 @@ import errors from './errors';
import funnels from './funnels';
import config from './config';
import roles from './roles';
+import customMetrics from './customMetrics';
export default combineReducers({
jwt,
@@ -68,6 +69,7 @@ export default combineReducers({
funnels,
config,
roles,
+ customMetrics,
...integrations,
...sources,
});
diff --git a/frontend/app/styles/main.css b/frontend/app/styles/main.css
index c77533f3c..077ec9904 100644
--- a/frontend/app/styles/main.css
+++ b/frontend/app/styles/main.css
@@ -106,4 +106,9 @@
opacity: .5;
}
-
+.form-group {
+ margin-bottom: 20px;
+ & label {
+ margin-bottom: 5px;
+ }
+}
\ No newline at end of file
diff --git a/frontend/app/types/customMetric.js b/frontend/app/types/customMetric.js
new file mode 100644
index 000000000..32c4386b6
--- /dev/null
+++ b/frontend/app/types/customMetric.js
@@ -0,0 +1,61 @@
+import Record from 'Types/Record';
+import { List } from 'immutable';
+// import { DateTime } from 'luxon';
+// import { validateEmail, validateName } from 'App/validate';
+import Filter from 'Types/filter';
+import NewFilter from 'Types/filter';
+// import { Event } from 'Types/filter';
+// import CustomFilter from 'Types/filter/customFilter';
+
+export const FilterSeries = Record({
+ seriesId: undefined,
+ index: undefined,
+ name: 'Filter Series',
+ filter: new Filter(),
+}, {
+ idKey: 'seriesId',
+ methods: {
+ toData() {
+ const js = this.toJS();
+ // js.filter = js.filter.toData();
+ return js;
+ },
+ },
+ fromJS: ({ filter, ...rest }) => ({
+ ...rest,
+ filter: new Filter(filter),
+ }),
+});
+
+export default Record({
+ metricId: undefined,
+ name: 'Series',
+ type: 'session_count',
+ series: List(),
+ isPublic: false,
+}, {
+ idKey: 'metricId',
+ methods: {
+ validate() {
+ return validateName(this.name, { diacritics: true });
+ },
+
+ toData() {
+ const js = this.toJS();
+ js.series = js.series.map(series => {
+ series.filter.filters = series.filter.filters.map(filter => {
+ delete filter.operatorOptions
+ delete filter.icon
+ return filter;
+ });
+ return series;
+ });
+
+ return js;
+ },
+ },
+ fromJS: ({ series, ...rest }) => ({
+ ...rest,
+ series: List(series).map(FilterSeries),
+ }),
+});
diff --git a/frontend/app/types/filter/customFilter.js b/frontend/app/types/filter/customFilter.js
index ea7d82af2..59e47d985 100644
--- a/frontend/app/types/filter/customFilter.js
+++ b/frontend/app/types/filter/customFilter.js
@@ -30,6 +30,20 @@ const REVID = 'REVID';
const USERANONYMOUSID = 'USERANONYMOUSID';
const USERID = 'USERID';
+const ISSUE = 'ISSUE';
+const EVENTS_COUNT = 'EVENTS_COUNT';
+const UTM_SOURCE = 'UTM_SOURCE';
+const UTM_MEDIUM = 'UTM_MEDIUM';
+const UTM_CAMPAIGN = 'UTM_CAMPAIGN';
+
+
+const DOM_COMPLETE = 'DOM_COMPLETE';
+const LARGEST_CONTENTFUL_PAINT_TIME = 'LARGEST_CONTENTFUL_PAINT_TIME';
+const TIME_BETWEEN_EVENTS = 'TIME_BETWEEN_EVENTS';
+const TTFB = 'TTFB';
+const AVG_CPU_LOAD = 'AVG_CPU_LOAD';
+const AVG_MEMORY_USAGE = 'AVG_MEMORY_USAGE';
+
export const KEYS = {
ERROR,
MISSING_RESOURCE,
@@ -56,7 +70,19 @@ export const KEYS = {
STATEACTION,
REVID,
USERANONYMOUSID,
- USERID
+ USERID,
+ ISSUE,
+ EVENTS_COUNT,
+ UTM_SOURCE,
+ UTM_MEDIUM,
+ UTM_CAMPAIGN,
+
+ DOM_COMPLETE,
+ LARGEST_CONTENTFUL_PAINT_TIME,
+ TIME_BETWEEN_EVENTS,
+ TTFB,
+ AVG_CPU_LOAD,
+ AVG_MEMORY_USAGE,
};
const getOperatorDefault = (type) => {
@@ -85,7 +111,7 @@ export default Record({
label: '',
icon: '',
type: '',
- value: '',
+ value: [""],
custom: '',
target: Target(),
level: '',
diff --git a/frontend/app/types/filter/event.js b/frontend/app/types/filter/event.js
index 2a6f54da6..17d6357ff 100644
--- a/frontend/app/types/filter/event.js
+++ b/frontend/app/types/filter/event.js
@@ -17,6 +17,13 @@ const USERANONYMOUSID = 'USERANONYMOUSID';
const USERID = 'USERID';
const ERROR = 'ERROR';
+const DOM_COMPLETE = 'DOM_COMPLETE';
+const LARGEST_CONTENTFUL_PAINT_TIME = 'LARGEST_CONTENTFUL_PAINT_TIME';
+const TIME_BETWEEN_EVENTS = 'TIME_BETWEEN_EVENTS';
+const TTFB = 'TTFB';
+const AVG_CPU_LOAD = 'AVG_CPU_LOAD';
+const AVG_MEMORY_USAGE = 'AVG_MEMORY_USAGE';
+
export const TYPES = {
CONSOLE,
GRAPHQL,
@@ -31,7 +38,14 @@ export const TYPES = {
METADATA,
USERANONYMOUSID,
USERID,
- ERROR
+ ERROR,
+
+ DOM_COMPLETE,
+ LARGEST_CONTENTFUL_PAINT_TIME,
+ TIME_BETWEEN_EVENTS,
+ TTFB,
+ AVG_CPU_LOAD,
+ AVG_MEMORY_USAGE,
};
function getLabelText(type, source) {
@@ -49,11 +63,19 @@ function getLabelText(type, source) {
if (type === TYPES.USERID) return 'User Id';
if (type === TYPES.USERANONYMOUSID) return 'User Anonymous Id';
if (type === TYPES.REVID) return 'Rev ID';
+
+ if (type === TYPES.DOM_COMPLETE) return 'DOM Complete';
+ if (type === TYPES.LARGEST_CONTENTFUL_PAINT_TIME) return 'Largest Contentful Paint Time';
+ if (type === TYPES.TIME_BETWEEN_EVENTS) return 'Time Between Events';
+ if (type === TYPES.TTFB) return 'TTFB';
+ if (type === TYPES.AVG_CPU_LOAD) return 'Avg CPU Load';
+ if (type === TYPES.AVG_MEMORY_USAGE) return 'Avg Memory Usage';
+
if (type === TYPES.CUSTOM) {
if (!source) return 'Custom';
return source;
}
- return '?';
+ return type;
}
export default Record({
@@ -63,7 +85,7 @@ export default Record({
operator: 'on',
type: '',
searchType: '',
- value: '',
+ value: [],
custom: '',
inputValue: '',
target: Target(),
@@ -73,16 +95,16 @@ export default Record({
}, {
keyKey: "_key",
fromJS: ({ target, type = "" , ...event }) => {
- const viewType = type.split('_')[0];
+ // const viewType = type.split('_')[0];
return {
...event,
- type: viewType,
+ type: type,
searchType: event.searchType || type,
- label: getLabelText(viewType, event.source),
+ label: getLabelText(type, event.source),
target: Target(target),
operator: event.operator || defaultOperator(event),
// value: target ? target.label : event.value,
- icon: event.icon || getEventIcon({...event, type: viewType })
+ icon: event.icon || getEventIcon({...event, type: type })
};
}
})
diff --git a/frontend/app/types/filter/filter.js b/frontend/app/types/filter/filter.js
index 42637a2fa..41c3421f8 100644
--- a/frontend/app/types/filter/filter.js
+++ b/frontend/app/types/filter/filter.js
@@ -9,7 +9,8 @@ import {
getDateRangeFromValue
} from 'App/dateRange';
import Event from './event';
-import CustomFilter from './customFilter';
+// import CustomFilter from './customFilter';
+import NewFilter from './newFilter';
const rangeValue = DATE_RANGE_VALUES.LAST_7_DAYS;
const range = getDateRangeFromValue(rangeValue);
@@ -17,8 +18,8 @@ const startDate = range.start.unix() * 1000;
const endDate = range.end.unix() * 1000;
export default Record({
- name: undefined,
- id: undefined,
+ name: '',
+ searchId: undefined,
referrer: undefined,
userBrowser: undefined,
userOs: undefined,
@@ -33,6 +34,7 @@ export default Record({
rangeValue,
startDate,
endDate,
+ condition: 'then',
sort: undefined,
order: undefined,
@@ -45,6 +47,19 @@ export default Record({
consoleLevel: undefined,
strict: false,
}, {
+ idKey: 'searchId',
+ methods: {
+ toData() {
+ const js = this.toJS();
+ js.filters = js.filters.map(filter => {
+ delete filter.operatorOptions
+ return filter;
+ });
+
+ delete js.createdAt;
+ return js;
+ }
+ },
fromJS({ filters, events, custom, ...filter }) {
let startDate;
let endDate;
@@ -59,7 +74,7 @@ export default Record({
startDate,
endDate,
events: List(events).map(Event),
- filters: List(filters).map(CustomFilter),
+ filters: List(filters).map(NewFilter),
custom: Map(custom),
}
}
@@ -73,6 +88,12 @@ export const defaultFilters = [
type: 'default',
keys: [
{ label: 'Click', key: KEYS.CLICK, type: KEYS.CLICK, filterKey: KEYS.CLICK, icon: 'filters/click', isFilter: false },
+ { label: 'DOM Complete', key: KEYS.DOM_COMPLETE, type: KEYS.DOM_COMPLETE, filterKey: KEYS.DOM_COMPLETE, icon: 'filters/click', isFilter: false },
+ { label: 'Largest Contentful Paint Time', key: KEYS.LARGEST_CONTENTFUL_PAINT_TIME, type: KEYS.LARGEST_CONTENTFUL_PAINT_TIME, filterKey: KEYS.LARGEST_CONTENTFUL_PAINT_TIME, icon: 'filters/click', isFilter: false },
+ { label: 'Time Between Events', key: KEYS.TIME_BETWEEN_EVENTS, type: KEYS.TIME_BETWEEN_EVENTS, filterKey: KEYS.TIME_BETWEEN_EVENTS, icon: 'filters/click', isFilter: false },
+ { label: 'Avg CPU Load', key: KEYS.AVG_CPU_LOAD, type: KEYS.AVG_CPU_LOAD, filterKey: KEYS.AVG_CPU_LOAD, icon: 'filters/click', isFilter: false },
+ { label: 'Memory Usage', key: KEYS.AVG_MEMORY_USAGE, type: KEYS.AVG_MEMORY_USAGE, filterKey: KEYS.AVG_MEMORY_USAGE, icon: 'filters/click', isFilter: false },
+
{ label: 'Input', key: KEYS.INPUT, type: KEYS.INPUT, filterKey: KEYS.INPUT, icon: 'event/input', isFilter: false },
{ label: 'Page', key: KEYS.LOCATION, type: KEYS.LOCATION, filterKey: KEYS.LOCATION, icon: 'event/link', isFilter: false },
// { label: 'View', key: KEYS.VIEW, type: KEYS.VIEW, filterKey: KEYS.VIEW, icon: 'event/view', isFilter: false }
@@ -103,6 +124,11 @@ export const defaultFilters = [
type: 'default',
keys: [
{ label: 'Errors', key: KEYS.ERROR, type: KEYS.ERROR, filterKey: KEYS.ERROR, icon: 'exclamation-circle', isFilter: false },
+ { label: 'Issues', key: KEYS.ISSUES, type: KEYS.ISSUES, filterKey: KEYS.ISSUES, icon: 'exclamation-circle', isFilter: true },
+ { label: 'UTM Source', key: KEYS.UTM_SOURCE, type: KEYS.UTM_SOURCE, filterKey: KEYS.UTM_SOURCE, icon: 'exclamation-circle', isFilter: true },
+ { label: 'UTM Medium', key: KEYS.UTM_MEDIUM, type: KEYS.UTM_MEDIUM, filterKey: KEYS.UTM_MEDIUM, icon: 'exclamation-circle', isFilter: true },
+ { label: 'UTM Campaign', key: KEYS.UTM_CAMPAIGN, type: KEYS.UTM_CAMPAIGN, filterKey: KEYS.UTM_CAMPAIGN, icon: 'exclamation-circle', isFilter: true },
+
{ label: 'Fetch Requests', key: KEYS.FETCH, type: KEYS.FETCH, filterKey: KEYS.FETCH, icon: 'fetch', isFilter: false },
{ label: 'GraphQL Queries', key: KEYS.GRAPHQL, type: KEYS.GRAPHQL, filterKey: KEYS.GRAPHQL, icon: 'vendors/graphql', isFilter: false },
{ label: 'Store Actions', key: KEYS.STATEACTION, type: KEYS.STATEACTION, filterKey: KEYS.STATEACTION, icon: 'store', isFilter: false },
@@ -127,6 +153,13 @@ export const getEventIcon = (filter) => {
if (type === KEYS.USERBROWSER) return 'window';
if (type === KEYS.PLATFORM) return 'window';
+ if (type === TYPES.DOM_COMPLETE) return 'filters/click';
+ if (type === TYPES.LARGEST_CONTENTFUL_PAINT_TIME) return 'filters/click';
+ if (type === TYPES.TIME_BETWEEN_EVENTS) return 'filters/click';
+ if (type === TYPES.TTFB) return 'filters/click';
+ if (type === TYPES.AVG_CPU_LOAD) return 'filters/click';
+ if (type === TYPES.AVG_MEMORY_USAGE) return 'filters/click';
+
if (type === TYPES.CLICK) return 'filters/click';
if (type === TYPES.LOCATION) return 'map-marker-alt';
if (type === TYPES.VIEW) return 'event/view';
diff --git a/frontend/app/types/filter/index.js b/frontend/app/types/filter/index.js
index dba5512db..957e1dfb8 100644
--- a/frontend/app/types/filter/index.js
+++ b/frontend/app/types/filter/index.js
@@ -6,7 +6,7 @@ export * from './filter';
const filterKeys = ['is', 'isNot'];
-const stringFilterKeys = ['is', 'isNot', 'contains'];
+const stringFilterKeys = ['is', 'isNot', 'contains', 'startsWith', 'endsWith'];
const targetFilterKeys = ['on', 'notOn'];
const signUpStatusFilterKeys = ['isSignedUp', 'notSignedUp'];
const rangeFilterKeys = ['before', 'after', 'on', 'inRange', 'notInRange', 'withInLast', 'notWithInLast'];
@@ -209,6 +209,12 @@ export const operatorOptions = (filter) => {
case TYPES.REVID:
case 'metadata':
case KEYS.ERROR:
+ case TYPES.DOM_COMPLETE:
+ case TYPES.LARGEST_CONTENTFUL_PAINT_TIME:
+ case TYPES.TIME_BETWEEN_EVENTS:
+ case TYPES.TTFB:
+ case TYPES.AVG_CPU_LOAD:
+ case TYPES.AVG_MEMORY_USAGE:
return stringFilterOptions;
case TYPES.INPUT:
@@ -230,4 +236,22 @@ export const operatorOptions = (filter) => {
case KEYS.CLICK_RAGE:
return [{ key: 'onAnything', text: 'on anything', value: 'true' }]
}
-}
\ No newline at end of file
+}
+
+const NewFilterType = (key, category, label, icon, isEvent = false) => {
+ return {
+ key: key,
+ category: category,
+ label: label,
+ icon: icon,
+ isEvent: isEvent,
+ operators: operatorOptions({ key }),
+ value: [""]
+ }
+}
+
+export const newFiltersList = [
+ NewFilterType(TYPES.CLICK, 'Gear', 'Click', 'filters/click', true),
+ NewFilterType(TYPES.CLICK, 'Gear', 'Input', 'filters/click', true),
+ NewFilterType(TYPES.CONSOLE, 'Other', 'Console', 'filters/click', true),
+];
\ No newline at end of file
diff --git a/frontend/app/types/filter/newFilter.js b/frontend/app/types/filter/newFilter.js
new file mode 100644
index 000000000..2c1a49f48
--- /dev/null
+++ b/frontend/app/types/filter/newFilter.js
@@ -0,0 +1,308 @@
+import Record from 'Types/Record';
+
+const CLICK = 'CLICK';
+const INPUT = 'INPUT';
+const LOCATION = 'LOCATION';
+const VIEW = 'VIEW_IOS';
+const CONSOLE = 'ERROR';
+const METADATA = 'METADATA';
+const CUSTOM = 'CUSTOM';
+const URL = 'URL';
+const CLICK_RAGE = 'CLICKRAGE';
+const USER_BROWSER = 'USERBROWSER';
+const USER_OS = 'USEROS';
+const USER_COUNTRY = 'USERCOUNTRY';
+const USER_DEVICE = 'USERDEVICE';
+const PLATFORM = 'PLATFORM';
+const DURATION = 'DURATION';
+const REFERRER = 'REFERRER';
+const ERROR = 'ERROR';
+const MISSING_RESOURCE = 'MISSINGRESOURCE';
+const SLOW_SESSION = 'SLOWSESSION';
+const JOURNEY = 'JOUNRNEY';
+const FETCH = 'REQUEST';
+const GRAPHQL = 'GRAPHQL';
+const STATEACTION = 'STATEACTION';
+const REVID = 'REVID';
+const USERANONYMOUSID = 'USERANONYMOUSID';
+const USERID = 'USERID';
+
+const ISSUE = 'ISSUE';
+const EVENTS_COUNT = 'EVENTS_COUNT';
+const UTM_SOURCE = 'UTM_SOURCE';
+const UTM_MEDIUM = 'UTM_MEDIUM';
+const UTM_CAMPAIGN = 'UTM_CAMPAIGN';
+
+
+const DOM_COMPLETE = 'DOM_COMPLETE';
+const LARGEST_CONTENTFUL_PAINT_TIME = 'LARGEST_CONTENTFUL_PAINT_TIME';
+const TIME_BETWEEN_EVENTS = 'TIME_BETWEEN_EVENTS';
+const TTFB = 'TTFB';
+const AVG_CPU_LOAD = 'AVG_CPU_LOAD';
+const AVG_MEMORY_USAGE = 'AVG_MEMORY_USAGE';
+
+export const TYPES = {
+ ERROR,
+ MISSING_RESOURCE,
+ SLOW_SESSION,
+ CLICK_RAGE,
+ CLICK,
+ INPUT,
+ LOCATION,
+ VIEW,
+ CONSOLE,
+ METADATA,
+ CUSTOM,
+ URL,
+ USER_BROWSER,
+ USER_OS,
+ USER_DEVICE,
+ PLATFORM,
+ DURATION,
+ REFERRER,
+ USER_COUNTRY,
+ JOURNEY,
+ FETCH,
+ GRAPHQL,
+ STATEACTION,
+ REVID,
+ USERANONYMOUSID,
+ USERID,
+ ISSUE,
+ EVENTS_COUNT,
+ UTM_SOURCE,
+ UTM_MEDIUM,
+ UTM_CAMPAIGN,
+
+ DOM_COMPLETE,
+ LARGEST_CONTENTFUL_PAINT_TIME,
+ TIME_BETWEEN_EVENTS,
+ TTFB,
+ AVG_CPU_LOAD,
+ AVG_MEMORY_USAGE,
+};
+
+const filterKeys = ['is', 'isNot'];
+const stringFilterKeys = ['is', 'isNot', 'contains', 'startsWith', 'endsWith'];
+const targetFilterKeys = ['on', 'notOn'];
+const signUpStatusFilterKeys = ['isSignedUp', 'notSignedUp'];
+const rangeFilterKeys = ['before', 'after', 'on', 'inRange', 'notInRange', 'withInLast', 'notWithInLast'];
+
+const options = [
+ {
+ key: 'is',
+ text: 'is',
+ value: 'is'
+ }, {
+ key: 'isNot',
+ text: 'is not',
+ value: 'isNot'
+ }, {
+ key: 'startsWith',
+ text: 'starts with',
+ value: 'startsWith'
+ }, {
+ key: 'endsWith',
+ text: 'ends with',
+ value: 'endsWith'
+ }, {
+ key: 'contains',
+ text: 'contains',
+ value: 'contains'
+ }, {
+ key: 'doesNotContain',
+ text: 'does not contain',
+ value: 'doesNotContain'
+ }, {
+ key: 'hasAnyValue',
+ text: 'has any value',
+ value: 'hasAnyValue'
+ }, {
+ key: 'hasNoValue',
+ text: 'has no value',
+ value: 'hasNoValue'
+ },
+
+
+ {
+ key: 'isSignedUp',
+ text: 'is signed up',
+ value: 'isSignedUp'
+ }, {
+ key: 'notSignedUp',
+ text: 'not signed up',
+ value: 'notSignedUp'
+ },
+
+
+ {
+ key: 'before',
+ text: 'before',
+ value: 'before'
+ }, {
+ key: 'after',
+ text: 'after',
+ value: 'after'
+ }, {
+ key: 'on',
+ text: 'on',
+ value: 'on'
+ }, {
+ key: 'notOn',
+ text: 'not on',
+ value: 'notOn'
+ }, {
+ key: 'inRage',
+ text: 'in rage',
+ value: 'inRage'
+ }, {
+ key: 'notInRage',
+ text: 'not in rage',
+ value: 'notInRage'
+ }, {
+ key: 'withinLast',
+ text: 'within last',
+ value: 'withinLast'
+ }, {
+ key: 'notWithinLast',
+ text: 'not within last',
+ value: 'notWithinLast'
+ },
+
+ {
+ key: 'greaterThan',
+ text: 'greater than',
+ value: 'greaterThan'
+ }, {
+ key: 'lessThan',
+ text: 'less than',
+ value: 'lessThan'
+ }, {
+ key: 'equal',
+ text: 'equal',
+ value: 'equal'
+ }, {
+ key: 'not equal',
+ text: 'not equal',
+ value: 'not equal'
+ },
+
+
+ {
+ key: 'onSelector',
+ text: 'on selector',
+ value: 'onSelector'
+ }, {
+ key: 'onText',
+ text: 'on text',
+ value: 'onText'
+ }, {
+ key: 'onComponent',
+ text: 'on component',
+ value: 'onComponent'
+ },
+
+
+ {
+ key: 'onAnything',
+ text: 'on anything',
+ value: 'onAnything'
+ }
+];
+
+export const filterOptions = options.filter(({key}) => filterKeys.includes(key));
+export const stringFilterOptions = options.filter(({key}) => stringFilterKeys.includes(key));
+export const targetFilterOptions = options.filter(({key}) => targetFilterKeys.includes(key));
+export const booleanOptions = [
+ { key: 'true', text: 'true', value: 'true' },
+ { key: 'false', text: 'false', value: 'false' },
+]
+
+export const filtersMap = {
+ [TYPES.CLICK]: { category: 'interactions', label: 'Click', operator: 'on', operatorOptions: targetFilterOptions, icon: 'filters/click' },
+ [TYPES.INPUT]: { category: 'interactions', label: 'Input', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
+ [TYPES.LOCATION]: { category: 'interactions', label: 'Page', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
+
+ [TYPES.USER_OS]: { category: 'gear', label: 'User OS', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
+ [TYPES.USER_BROWSER]: { category: 'gear', label: 'User Browser', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
+ [TYPES.USER_DEVICE]: { category: 'gear', label: 'User Device', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
+ [TYPES.PLATFORM]: { category: 'gear', label: 'Platform', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
+ [TYPES.REVID]: { category: 'gear', label: 'RevId', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
+
+ [TYPES.REFERRER]: { category: 'recording_attributes', label: 'Referrer', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
+ [TYPES.DURATION]: { category: 'recording_attributes', label: 'Duration', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
+ [TYPES.USER_COUNTRY]: { category: 'recording_attributes', label: 'User Country', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
+
+ [TYPES.CONSOLE]: { category: 'javascript', label: 'Console', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
+ [TYPES.ERROR]: { category: 'javascript', label: 'Error', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
+ [TYPES.FETCH]: { category: 'javascript', label: 'Fetch', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
+ [TYPES.GRAPHQL]: { category: 'javascript', label: 'GraphQL', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
+ [TYPES.STATEACTION]: { category: 'javascript', label: 'StateAction', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
+
+ [TYPES.USERID]: { category: 'user', label: 'UserId', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
+ [TYPES.USERANONYMOUSID]: { category: 'user', label: 'UserAnonymousId', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
+
+ [TYPES.DOM_COMPLETE]: { category: 'new', label: 'DOM Complete', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
+ [TYPES.LARGEST_CONTENTFUL_PAINT_TIME]: { category: 'new', label: 'Largest Contentful Paint Time', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
+ [TYPES.TIME_BETWEEN_EVENTS]: { category: 'new', label: 'Time Between Events', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
+ [TYPES.TTFB]: { category: 'new', label: 'TTFB', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
+ [TYPES.AVG_CPU_LOAD]: { category: 'new', label: 'Avg CPU Load', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
+ [TYPES.AVG_MEMORY_USAGE]: { category: 'new', label: 'Avg Memory Usage', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/click' },
+ [TYPES.SLOW_SESSION]: { category: 'new', label: 'Slow Session', operator: 'true', operatorOptions: [{ key: 'true', text: 'true', value: 'true' }], icon: 'filters/click' },
+ [TYPES.MISSING_RESOURCE]: { category: 'new', label: 'Missing Resource', operator: 'true', operatorOptions: [{ key: 'inImages', text: 'in images', value: 'true' }], icon: 'filters/click' },
+ [TYPES.CLICK_RAGE]: { category: 'new', label: 'Click Rage', operator: 'onAnything', operatorOptions: [{ key: 'onAnything', text: 'on anything', value: 'true' }], icon: 'filters/click' },
+ // [TYPES.URL]: { category: 'interactions', label: 'URL', operator: 'is', operatorOptions: stringFilterOptions },
+ // [TYPES.CUSTOM]: { category: 'interactions', label: 'Custom', operator: 'is', operatorOptions: stringFilterOptions },
+ // [TYPES.METADATA]: { category: 'interactions', label: 'Metadata', operator: 'is', operatorOptions: stringFilterOptions },
+}
+
+export default Record({
+ timestamp: 0,
+ key: '',
+ label: '',
+ icon: '',
+ type: '',
+ value: [""],
+
+ custom: '',
+ // target: Target(),
+ level: '',
+ source: null,
+ hasNoValue: false,
+ isFilter: false,
+ actualValue: '',
+
+ operator: 'is',
+ operatorOptions: [],
+}, {
+ keyKey: "_key",
+ fromJS: ({ ...filter }) => ({
+ ...filter,
+ type: filter.type, // camelCased(filter.type.toLowerCase()),
+ // key: filter.type === METADATA ? filter.label : filter.key || filter.type, // || camelCased(filter.type.toLowerCase()),
+ // label: getLabel(filter),
+ // target: Target(target),
+ // operator: getOperatorDefault(filter.type),
+ // value: target ? target.label : filter.value,
+ // value: typeof value === 'string' ? [value] : value,
+ // icon: filter.type ? getfilterIcon(filter.type) : 'filters/metadata'
+ }),
+})
+
+// const NewFilterType = (key, category, icon, isEvent = false) => {
+// return {
+// key: key,
+// category: category,
+// label: filterMap[key].label,
+// icon: icon,
+// isEvent: isEvent,
+// operators: filterMap[key].operatorOptions,
+// value: [""]
+// }
+// }
+
+// export const newFiltersList = [
+// NewFilterType(TYPES.CLICK, 'Click', 'filters/click', true),
+// NewFilterType(TYPES.CLICK, 'Input', 'filters/click', true),
+// NewFilterType(TYPES.CONSOLE, 'Console', 'filters/click', true),
+// ];
\ No newline at end of file
diff --git a/frontend/env.js b/frontend/env.js
index f7c49c874..ad23c710a 100644
--- a/frontend/env.js
+++ b/frontend/env.js
@@ -5,13 +5,13 @@ require('dotenv').config()
const oss = {
name: 'oss',
- PRODUCTION: true,
+ PRODUCTION: false,
SENTRY_ENABLED: false,
SENTRY_URL: "",
CAPTCHA_ENABLED: process.env.CAPTCHA_ENABLED === 'true',
CAPTCHA_SITE_KEY: process.env.CAPTCHA_SITE_KEY,
ORIGIN: () => 'window.location.origin',
- API_EDP: () => 'window.location.origin + "/api"',
+ API_EDP: "https://dol.openreplay.com/api",
ASSETS_HOST: () => 'window.location.origin + "/assets"',
VERSION: '1.3.6',
SOURCEMAP: true,