+
@@ -49,4 +86,4 @@ function OverviewMenu(props: Props) {
export default connect((state: any) => ({
activeTab: state.getIn(['search', 'activeTab', 'type']),
isEnterprise: state.getIn(['user', 'account', 'edition']) === 'ee',
-}), { setActiveTab })(OverviewMenu);
+}), { setActiveTab })(withRouter(OverviewMenu));
diff --git a/frontend/app/components/shared/SessionListContainer/components/Notes/index.ts b/frontend/app/components/shared/SessionListContainer/components/Notes/index.ts
deleted file mode 100644
index 0d46fcee2..000000000
--- a/frontend/app/components/shared/SessionListContainer/components/Notes/index.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-export { default } from './NoteList'
-export { default as TeamBadge } from './TeamBadge'
diff --git a/frontend/app/components/shared/SessionSearch/SessionSearch.tsx b/frontend/app/components/shared/SessionSearch/SessionSearch.tsx
index e617ec9cb..4238fad87 100644
--- a/frontend/app/components/shared/SessionSearch/SessionSearch.tsx
+++ b/frontend/app/components/shared/SessionSearch/SessionSearch.tsx
@@ -72,7 +72,7 @@ function SessionSearch(props: Props) {
debounceFetch()
};
- return !metaLoading && (
+ return !metaLoading ? (
<>
{hasEvents || hasFilters ? (
@@ -107,7 +107,7 @@ function SessionSearch(props: Props) {
<>>
)}
>
- );
+ ) : null;
}
export default connect(
diff --git a/frontend/app/components/shared/SessionListContainer/SessionListContainer.tsx b/frontend/app/components/shared/SessionsTabOverview/SessionsTabOverview.tsx
similarity index 93%
rename from frontend/app/components/shared/SessionListContainer/SessionListContainer.tsx
rename to frontend/app/components/shared/SessionsTabOverview/SessionsTabOverview.tsx
index f5144a9a6..8080bae32 100644
--- a/frontend/app/components/shared/SessionListContainer/SessionListContainer.tsx
+++ b/frontend/app/components/shared/SessionsTabOverview/SessionsTabOverview.tsx
@@ -5,7 +5,7 @@ import NotesList from './components/Notes/NoteList';
import { connect } from 'react-redux';
import LatestSessionsMessage from './components/LatestSessionsMessage';
-function SessionListContainer({
+function SessionsTabOverview({
activeTab,
members,
}: {
@@ -29,4 +29,4 @@ export default connect(
// @ts-ignore
members: state.getIn(['members', 'list']),
}),
-)(SessionListContainer);
+)(SessionsTabOverview);
diff --git a/frontend/app/components/shared/SessionListContainer/components/LatestSessionsMessage/LatestSessionsMessage.tsx b/frontend/app/components/shared/SessionsTabOverview/components/LatestSessionsMessage/LatestSessionsMessage.tsx
similarity index 100%
rename from frontend/app/components/shared/SessionListContainer/components/LatestSessionsMessage/LatestSessionsMessage.tsx
rename to frontend/app/components/shared/SessionsTabOverview/components/LatestSessionsMessage/LatestSessionsMessage.tsx
diff --git a/frontend/app/components/shared/SessionListContainer/components/LatestSessionsMessage/index.ts b/frontend/app/components/shared/SessionsTabOverview/components/LatestSessionsMessage/index.ts
similarity index 100%
rename from frontend/app/components/shared/SessionListContainer/components/LatestSessionsMessage/index.ts
rename to frontend/app/components/shared/SessionsTabOverview/components/LatestSessionsMessage/index.ts
diff --git a/frontend/app/components/shared/SessionListContainer/components/NoContentMessage.tsx b/frontend/app/components/shared/SessionsTabOverview/components/NoContentMessage.tsx
similarity index 100%
rename from frontend/app/components/shared/SessionListContainer/components/NoContentMessage.tsx
rename to frontend/app/components/shared/SessionsTabOverview/components/NoContentMessage.tsx
diff --git a/frontend/app/components/shared/SessionListContainer/components/Notes/NoteItem.tsx b/frontend/app/components/shared/SessionsTabOverview/components/Notes/NoteItem.tsx
similarity index 100%
rename from frontend/app/components/shared/SessionListContainer/components/Notes/NoteItem.tsx
rename to frontend/app/components/shared/SessionsTabOverview/components/Notes/NoteItem.tsx
diff --git a/frontend/app/components/shared/SessionListContainer/components/Notes/NoteList.tsx b/frontend/app/components/shared/SessionsTabOverview/components/Notes/NoteList.tsx
similarity index 100%
rename from frontend/app/components/shared/SessionListContainer/components/Notes/NoteList.tsx
rename to frontend/app/components/shared/SessionsTabOverview/components/Notes/NoteList.tsx
diff --git a/frontend/app/components/shared/SessionListContainer/components/Notes/NoteTags.tsx b/frontend/app/components/shared/SessionsTabOverview/components/Notes/NoteTags.tsx
similarity index 100%
rename from frontend/app/components/shared/SessionListContainer/components/Notes/NoteTags.tsx
rename to frontend/app/components/shared/SessionsTabOverview/components/Notes/NoteTags.tsx
diff --git a/frontend/app/components/shared/SessionListContainer/components/Notes/TeamBadge.tsx b/frontend/app/components/shared/SessionsTabOverview/components/Notes/TeamBadge.tsx
similarity index 99%
rename from frontend/app/components/shared/SessionListContainer/components/Notes/TeamBadge.tsx
rename to frontend/app/components/shared/SessionsTabOverview/components/Notes/TeamBadge.tsx
index 3ddab831e..8f509b5eb 100644
--- a/frontend/app/components/shared/SessionListContainer/components/Notes/TeamBadge.tsx
+++ b/frontend/app/components/shared/SessionsTabOverview/components/Notes/TeamBadge.tsx
@@ -8,4 +8,4 @@ export default function TeamBadge() {
Team
)
-}
+}
\ No newline at end of file
diff --git a/frontend/app/components/shared/SessionsTabOverview/components/Notes/index.ts b/frontend/app/components/shared/SessionsTabOverview/components/Notes/index.ts
new file mode 100644
index 000000000..e9c9e74a4
--- /dev/null
+++ b/frontend/app/components/shared/SessionsTabOverview/components/Notes/index.ts
@@ -0,0 +1,2 @@
+export { default } from './NoteList'
+export { default as TeamBadge } from './TeamBadge'
\ No newline at end of file
diff --git a/frontend/app/components/shared/SessionListContainer/components/SessionHeader/SessionHeader.tsx b/frontend/app/components/shared/SessionsTabOverview/components/SessionHeader/SessionHeader.tsx
similarity index 100%
rename from frontend/app/components/shared/SessionListContainer/components/SessionHeader/SessionHeader.tsx
rename to frontend/app/components/shared/SessionsTabOverview/components/SessionHeader/SessionHeader.tsx
diff --git a/frontend/app/components/shared/SessionListContainer/components/SessionHeader/index.ts b/frontend/app/components/shared/SessionsTabOverview/components/SessionHeader/index.ts
similarity index 100%
rename from frontend/app/components/shared/SessionListContainer/components/SessionHeader/index.ts
rename to frontend/app/components/shared/SessionsTabOverview/components/SessionHeader/index.ts
diff --git a/frontend/app/components/shared/SessionListContainer/components/SessionList/SessionDateRange.tsx b/frontend/app/components/shared/SessionsTabOverview/components/SessionList/SessionDateRange.tsx
similarity index 97%
rename from frontend/app/components/shared/SessionListContainer/components/SessionList/SessionDateRange.tsx
rename to frontend/app/components/shared/SessionsTabOverview/components/SessionList/SessionDateRange.tsx
index 7bc6afc57..700e05e6c 100644
--- a/frontend/app/components/shared/SessionListContainer/components/SessionList/SessionDateRange.tsx
+++ b/frontend/app/components/shared/SessionsTabOverview/components/SessionList/SessionDateRange.tsx
@@ -31,4 +31,4 @@ export default connect(
filter: state.getIn(['search', 'instance']),
}),
{ applyFilter }
-)(SessionDateRange);
+)(SessionDateRange);
\ No newline at end of file
diff --git a/frontend/app/components/shared/SessionListContainer/components/SessionList/SessionList.tsx b/frontend/app/components/shared/SessionsTabOverview/components/SessionList/SessionList.tsx
similarity index 100%
rename from frontend/app/components/shared/SessionListContainer/components/SessionList/SessionList.tsx
rename to frontend/app/components/shared/SessionsTabOverview/components/SessionList/SessionList.tsx
diff --git a/frontend/app/components/shared/SessionListContainer/components/SessionList/index.ts b/frontend/app/components/shared/SessionsTabOverview/components/SessionList/index.ts
similarity index 100%
rename from frontend/app/components/shared/SessionListContainer/components/SessionList/index.ts
rename to frontend/app/components/shared/SessionsTabOverview/components/SessionList/index.ts
diff --git a/frontend/app/components/shared/SessionListContainer/components/SessionSettingButton/SessionSettingButton.tsx b/frontend/app/components/shared/SessionsTabOverview/components/SessionSettingButton/SessionSettingButton.tsx
similarity index 100%
rename from frontend/app/components/shared/SessionListContainer/components/SessionSettingButton/SessionSettingButton.tsx
rename to frontend/app/components/shared/SessionsTabOverview/components/SessionSettingButton/SessionSettingButton.tsx
diff --git a/frontend/app/components/shared/SessionListContainer/components/SessionSettingButton/index.ts b/frontend/app/components/shared/SessionsTabOverview/components/SessionSettingButton/index.ts
similarity index 100%
rename from frontend/app/components/shared/SessionListContainer/components/SessionSettingButton/index.ts
rename to frontend/app/components/shared/SessionsTabOverview/components/SessionSettingButton/index.ts
diff --git a/frontend/app/components/shared/SessionListContainer/components/SessionSort/SessionSort.tsx b/frontend/app/components/shared/SessionsTabOverview/components/SessionSort/SessionSort.tsx
similarity index 100%
rename from frontend/app/components/shared/SessionListContainer/components/SessionSort/SessionSort.tsx
rename to frontend/app/components/shared/SessionsTabOverview/components/SessionSort/SessionSort.tsx
diff --git a/frontend/app/components/shared/SessionListContainer/components/SessionSort/index.ts b/frontend/app/components/shared/SessionsTabOverview/components/SessionSort/index.ts
similarity index 100%
rename from frontend/app/components/shared/SessionListContainer/components/SessionSort/index.ts
rename to frontend/app/components/shared/SessionsTabOverview/components/SessionSort/index.ts
diff --git a/frontend/app/components/shared/SessionListContainer/components/SessionSort/sortDropdown.module.css b/frontend/app/components/shared/SessionsTabOverview/components/SessionSort/sortDropdown.module.css
similarity index 100%
rename from frontend/app/components/shared/SessionListContainer/components/SessionSort/sortDropdown.module.css
rename to frontend/app/components/shared/SessionsTabOverview/components/SessionSort/sortDropdown.module.css
diff --git a/frontend/app/components/shared/SessionListContainer/components/SessionTags/SessionTags.tsx b/frontend/app/components/shared/SessionsTabOverview/components/SessionTags/SessionTags.tsx
similarity index 100%
rename from frontend/app/components/shared/SessionListContainer/components/SessionTags/SessionTags.tsx
rename to frontend/app/components/shared/SessionsTabOverview/components/SessionTags/SessionTags.tsx
diff --git a/frontend/app/components/shared/SessionListContainer/components/SessionTags/index.ts b/frontend/app/components/shared/SessionsTabOverview/components/SessionTags/index.ts
similarity index 100%
rename from frontend/app/components/shared/SessionListContainer/components/SessionTags/index.ts
rename to frontend/app/components/shared/SessionsTabOverview/components/SessionTags/index.ts
diff --git a/frontend/app/components/shared/SessionsTabOverview/index.ts b/frontend/app/components/shared/SessionsTabOverview/index.ts
new file mode 100644
index 000000000..e69de29bb
diff --git a/frontend/app/components/ui/SVG.tsx b/frontend/app/components/ui/SVG.tsx
index 33b19d892..3325527fd 100644
--- a/frontend/app/components/ui/SVG.tsx
+++ b/frontend/app/components/ui/SVG.tsx
@@ -1,7 +1,7 @@
import React from 'react';
-export type IconNames = 'activity' | 'alarm-clock' | 'alarm-plus' | 'all-sessions' | 'analytics' | 'anchor' | 'arrow-alt-square-right' | 'arrow-bar-left' | 'arrow-clockwise' | 'arrow-counterclockwise' | 'arrow-down-short' | 'arrow-down' | 'arrow-repeat' | 'arrow-right-short' | 'arrow-square-left' | 'arrow-square-right' | 'arrow-up-short' | 'arrow-up' | 'arrows-angle-extend' | 'avatar/icn_bear' | 'avatar/icn_beaver' | 'avatar/icn_bird' | 'avatar/icn_bison' | 'avatar/icn_camel' | 'avatar/icn_chameleon' | 'avatar/icn_deer' | 'avatar/icn_dog' | 'avatar/icn_dolphin' | 'avatar/icn_elephant' | 'avatar/icn_fish' | 'avatar/icn_fox' | 'avatar/icn_gorilla' | 'avatar/icn_hippo' | 'avatar/icn_horse' | 'avatar/icn_hyena' | 'avatar/icn_kangaroo' | 'avatar/icn_lemur' | 'avatar/icn_mammel' | 'avatar/icn_monkey' | 'avatar/icn_moose' | 'avatar/icn_panda' | 'avatar/icn_penguin' | 'avatar/icn_porcupine' | 'avatar/icn_quail' | 'avatar/icn_rabbit' | 'avatar/icn_rhino' | 'avatar/icn_sea_horse' | 'avatar/icn_sheep' | 'avatar/icn_snake' | 'avatar/icn_squirrel' | 'avatar/icn_tapir' | 'avatar/icn_turtle' | 'avatar/icn_vulture' | 'avatar/icn_wild1' | 'avatar/icn_wild_bore' | 'ban' | 'bar-chart-line' | 'bar-pencil' | 'bell-fill' | 'bell-plus' | 'bell-slash' | 'bell' | 'binoculars' | 'book-doc' | 'book' | 'browser/browser' | 'browser/chrome' | 'browser/edge' | 'browser/electron' | 'browser/facebook' | 'browser/firefox' | 'browser/ie' | 'browser/opera' | 'browser/safari' | 'buildings' | 'bullhorn' | 'business-time' | 'calendar-alt' | 'calendar-check' | 'calendar-day' | 'calendar' | 'call' | 'camera-alt' | 'camera-video-off' | 'camera-video' | 'camera' | 'card-checklist' | 'card-text' | 'caret-down-fill' | 'caret-left-fill' | 'caret-right-fill' | 'caret-up-fill' | 'chat-dots' | 'chat-right-text' | 'chat-square-quote' | 'check-circle-fill' | 'check-circle' | 'check' | 'chevron-double-left' | 'chevron-double-right' | 'chevron-down' | 'chevron-left' | 'chevron-right' | 'chevron-up' | 'circle-fill' | 'circle' | 'click-hesitation' | 'click-rage' | 'clipboard-list-check' | 'clock' | 'close' | 'cloud-fog2-fill' | 'code' | 'cog' | 'cogs' | 'collection' | 'columns-gap-filled' | 'columns-gap' | 'console/error' | 'console/exception' | 'console/info' | 'console/warning' | 'console' | 'controller' | 'cookies' | 'copy' | 'credit-card-front' | 'cross' | 'cubes' | 'cursor-trash' | 'dash' | 'dashboard-icn' | 'desktop' | 'device' | 'diagram-3' | 'dizzy' | 'door-closed' | 'doublecheck' | 'download' | 'drag' | 'edit' | 'ellipsis-v' | 'enter' | 'envelope-check' | 'envelope-x' | 'envelope' | 'errors-icon' | 'event/click' | 'event/click_hesitation' | 'event/clickrage' | 'event/code' | 'event/i-cursor' | 'event/input' | 'event/input_hesitation' | 'event/link' | 'event/location' | 'event/mouse_thrashing' | 'event/resize' | 'event/view' | 'exclamation-circle-fill' | 'exclamation-circle' | 'expand-wide' | 'explosion' | 'external-link-alt' | 'eye-slash-fill' | 'eye-slash' | 'eye' | 'fetch' | 'file-code' | 'file-medical-alt' | 'file-pdf' | 'file-play' | 'file' | 'files' | 'filter' | 'filters/arrow-return-right' | 'filters/browser' | 'filters/click' | 'filters/clickrage' | 'filters/code' | 'filters/console' | 'filters/country' | 'filters/cpu-load' | 'filters/custom' | 'filters/device' | 'filters/dom-complete' | 'filters/duration' | 'filters/error' | 'filters/fetch-failed' | 'filters/fetch' | 'filters/file-code' | 'filters/graphql' | 'filters/i-cursor' | 'filters/input' | 'filters/lcpt' | 'filters/link' | 'filters/location' | 'filters/memory-load' | 'filters/metadata' | 'filters/os' | 'filters/perfromance-network-request' | 'filters/platform' | 'filters/referrer' | 'filters/resize' | 'filters/rev-id' | 'filters/state-action' | 'filters/ttfb' | 'filters/user-alt' | 'filters/userid' | 'filters/view' | 'flag-na' | 'folder-plus' | 'folder2' | 'fullscreen' | 'funnel/cpu-fill' | 'funnel/cpu' | 'funnel/dizzy' | 'funnel/emoji-angry-fill' | 'funnel/emoji-angry' | 'funnel/emoji-dizzy-fill' | 'funnel/exclamation-circle-fill' | 'funnel/exclamation-circle' | 'funnel/file-earmark-break-fill' | 'funnel/file-earmark-break' | 'funnel/file-earmark-minus-fill' | 'funnel/file-earmark-minus' | 'funnel/file-medical-alt' | 'funnel/file-x' | 'funnel/hdd-fill' | 'funnel/hourglass-top' | 'funnel/image-fill' | 'funnel/image' | 'funnel/microchip' | 'funnel/mouse' | 'funnel/patch-exclamation-fill' | 'funnel/sd-card' | 'funnel-fill' | 'funnel-new' | 'funnel' | 'gear-fill' | 'gear' | 'geo-alt-fill-custom' | 'github' | 'graph-up-arrow' | 'graph-up' | 'grid-1x2' | 'grid-3x3' | 'grid-check' | 'grid-horizontal' | 'grid' | 'grip-horizontal' | 'hash' | 'hdd-stack' | 'headset' | 'heart-rate' | 'high-engagement' | 'history' | 'hourglass-start' | 'ic-errors' | 'ic-network' | 'ic-rage' | 'ic-resources' | 'id-card' | 'image' | 'info-circle-fill' | 'info-circle' | 'info-square' | 'info' | 'input-hesitation' | 'inspect' | 'integrations/assist' | 'integrations/bugsnag-text' | 'integrations/bugsnag' | 'integrations/cloudwatch-text' | 'integrations/cloudwatch' | 'integrations/datadog' | 'integrations/elasticsearch-text' | 'integrations/elasticsearch' | 'integrations/github' | 'integrations/graphql' | 'integrations/jira-text' | 'integrations/jira' | 'integrations/mobx' | 'integrations/newrelic-text' | 'integrations/newrelic' | 'integrations/ngrx' | 'integrations/openreplay-text' | 'integrations/openreplay' | 'integrations/redux' | 'integrations/rollbar-text' | 'integrations/rollbar' | 'integrations/segment' | 'integrations/sentry-text' | 'integrations/sentry' | 'integrations/slack-bw' | 'integrations/slack' | 'integrations/stackdriver' | 'integrations/sumologic-text' | 'integrations/sumologic' | 'integrations/teams-white' | 'integrations/teams' | 'integrations/vuejs' | 'journal-code' | 'key' | 'layer-group' | 'lightbulb-on' | 'lightbulb' | 'link-45deg' | 'list-alt' | 'list-arrow' | 'list-ul' | 'list' | 'lock-alt' | 'magic' | 'map-marker-alt' | 'memory' | 'mic-mute' | 'mic' | 'minus' | 'mobile' | 'mouse-alt' | 'network' | 'next1' | 'no-dashboard' | 'no-metrics-chart' | 'no-metrics' | 'no-recordings' | 'os/android' | 'os/chrome_os' | 'os/fedora' | 'os/ios' | 'os/linux' | 'os/mac_os_x' | 'os/other' | 'os/ubuntu' | 'os/windows' | 'os' | 'pause-fill' | 'pause' | 'pdf-download' | 'pencil-stop' | 'pencil' | 'people' | 'percent' | 'performance-icon' | 'person-border' | 'person-fill' | 'person' | 'pie-chart-fill' | 'pin-fill' | 'play-circle-bold' | 'play-circle-light' | 'play-circle' | 'play-fill-new' | 'play-fill' | 'play-hover' | 'play' | 'plug' | 'plus-circle' | 'plus-lg' | 'plus' | 'pointer-sessions-search' | 'prev1' | 'pulse' | 'puzzle-piece' | 'puzzle' | 'question-circle' | 'question-lg' | 'quote-left' | 'quote-right' | 'quotes' | 'record-btn' | 'record-circle' | 'redo-back' | 'redo' | 'remote-control' | 'replay-10' | 'resources-icon' | 'safe-fill' | 'safe' | 'sandglass' | 'search' | 'search_notification' | 'server' | 'share-alt' | 'shield-lock' | 'signpost-split' | 'signup' | 'skip-forward-fill' | 'skip-forward' | 'slack' | 'slash-circle' | 'sleep' | 'sliders' | 'social/slack' | 'social/trello' | 'speedometer2' | 'spinner' | 'star-solid' | 'star' | 'step-forward' | 'stickies' | 'stop-record-circle' | 'stopwatch' | 'store' | 'sync-alt' | 'table-new' | 'table' | 'tablet-android' | 'tachometer-slow' | 'tachometer-slowest' | 'tags' | 'team-funnel' | 'telephone-fill' | 'telephone' | 'text-paragraph' | 'tools' | 'trash' | 'turtle' | 'user-alt' | 'user-circle' | 'user-friends' | 'users' | 'vendors/graphql' | 'vendors/mobx' | 'vendors/ngrx' | 'vendors/redux' | 'vendors/vuex' | 'web-vitals' | 'wifi' | 'window-alt' | 'window-restore' | 'window-x' | 'window' | 'zoom-in';
+export type IconNames = 'activity' | 'alarm-clock' | 'alarm-plus' | 'all-sessions' | 'analytics' | 'anchor' | 'arrow-alt-square-right' | 'arrow-bar-left' | 'arrow-clockwise' | 'arrow-counterclockwise' | 'arrow-down-short' | 'arrow-down' | 'arrow-repeat' | 'arrow-right-short' | 'arrow-square-left' | 'arrow-square-right' | 'arrow-up-short' | 'arrow-up' | 'arrows-angle-extend' | 'avatar/icn_bear' | 'avatar/icn_beaver' | 'avatar/icn_bird' | 'avatar/icn_bison' | 'avatar/icn_camel' | 'avatar/icn_chameleon' | 'avatar/icn_deer' | 'avatar/icn_dog' | 'avatar/icn_dolphin' | 'avatar/icn_elephant' | 'avatar/icn_fish' | 'avatar/icn_fox' | 'avatar/icn_gorilla' | 'avatar/icn_hippo' | 'avatar/icn_horse' | 'avatar/icn_hyena' | 'avatar/icn_kangaroo' | 'avatar/icn_lemur' | 'avatar/icn_mammel' | 'avatar/icn_monkey' | 'avatar/icn_moose' | 'avatar/icn_panda' | 'avatar/icn_penguin' | 'avatar/icn_porcupine' | 'avatar/icn_quail' | 'avatar/icn_rabbit' | 'avatar/icn_rhino' | 'avatar/icn_sea_horse' | 'avatar/icn_sheep' | 'avatar/icn_snake' | 'avatar/icn_squirrel' | 'avatar/icn_tapir' | 'avatar/icn_turtle' | 'avatar/icn_vulture' | 'avatar/icn_wild1' | 'avatar/icn_wild_bore' | 'ban' | 'bar-chart-line' | 'bar-pencil' | 'bell-fill' | 'bell-plus' | 'bell-slash' | 'bell' | 'binoculars' | 'book-doc' | 'book' | 'browser/browser' | 'browser/chrome' | 'browser/edge' | 'browser/electron' | 'browser/facebook' | 'browser/firefox' | 'browser/ie' | 'browser/opera' | 'browser/safari' | 'buildings' | 'bullhorn' | 'business-time' | 'calendar-alt' | 'calendar-check' | 'calendar-day' | 'calendar' | 'call' | 'camera-alt' | 'camera-video-off' | 'camera-video' | 'camera' | 'card-checklist' | 'card-text' | 'caret-down-fill' | 'caret-left-fill' | 'caret-right-fill' | 'caret-up-fill' | 'chat-dots' | 'chat-right-text' | 'chat-square-quote' | 'check-circle-fill' | 'check-circle' | 'check' | 'chevron-double-left' | 'chevron-double-right' | 'chevron-down' | 'chevron-left' | 'chevron-right' | 'chevron-up' | 'circle-fill' | 'circle' | 'click-hesitation' | 'click-rage' | 'clipboard-list-check' | 'clock' | 'close' | 'cloud-fog2-fill' | 'code' | 'cog' | 'cogs' | 'collection' | 'columns-gap-filled' | 'columns-gap' | 'console/error' | 'console/exception' | 'console/info' | 'console/warning' | 'console' | 'controller' | 'cookies' | 'copy' | 'credit-card-front' | 'cross' | 'cubes' | 'cursor-trash' | 'dash' | 'dashboard-icn' | 'desktop' | 'device' | 'diagram-3' | 'dizzy' | 'door-closed' | 'doublecheck' | 'download' | 'drag' | 'edit' | 'ellipsis-v' | 'enter' | 'envelope-check' | 'envelope-x' | 'envelope' | 'errors-icon' | 'event/click' | 'event/click_hesitation' | 'event/clickrage' | 'event/code' | 'event/i-cursor' | 'event/input' | 'event/input_hesitation' | 'event/link' | 'event/location' | 'event/mouse_thrashing' | 'event/resize' | 'event/view' | 'exclamation-circle-fill' | 'exclamation-circle' | 'expand-wide' | 'explosion' | 'external-link-alt' | 'eye-slash-fill' | 'eye-slash' | 'eye' | 'fetch' | 'fflag-multi' | 'fflag-single' | 'file-code' | 'file-medical-alt' | 'file-pdf' | 'file' | 'files' | 'filter' | 'filters/arrow-return-right' | 'filters/browser' | 'filters/click' | 'filters/clickrage' | 'filters/code' | 'filters/console' | 'filters/country' | 'filters/cpu-load' | 'filters/custom' | 'filters/device' | 'filters/dom-complete' | 'filters/duration' | 'filters/error' | 'filters/fetch-failed' | 'filters/fetch' | 'filters/file-code' | 'filters/graphql' | 'filters/i-cursor' | 'filters/input' | 'filters/lcpt' | 'filters/link' | 'filters/location' | 'filters/memory-load' | 'filters/metadata' | 'filters/os' | 'filters/perfromance-network-request' | 'filters/platform' | 'filters/referrer' | 'filters/resize' | 'filters/rev-id' | 'filters/state-action' | 'filters/ttfb' | 'filters/user-alt' | 'filters/userid' | 'filters/view' | 'flag-na' | 'folder-plus' | 'folder2' | 'fullscreen' | 'funnel/cpu-fill' | 'funnel/cpu' | 'funnel/dizzy' | 'funnel/emoji-angry-fill' | 'funnel/emoji-angry' | 'funnel/emoji-dizzy-fill' | 'funnel/exclamation-circle-fill' | 'funnel/exclamation-circle' | 'funnel/file-earmark-break-fill' | 'funnel/file-earmark-break' | 'funnel/file-earmark-minus-fill' | 'funnel/file-earmark-minus' | 'funnel/file-medical-alt' | 'funnel/file-x' | 'funnel/hdd-fill' | 'funnel/hourglass-top' | 'funnel/image-fill' | 'funnel/image' | 'funnel/microchip' | 'funnel/mouse' | 'funnel/patch-exclamation-fill' | 'funnel/sd-card' | 'funnel-fill' | 'funnel-new' | 'funnel' | 'gear-fill' | 'gear' | 'geo-alt-fill-custom' | 'github' | 'graph-up-arrow' | 'graph-up' | 'grid-1x2' | 'grid-3x3' | 'grid-check' | 'grid-horizontal' | 'grid' | 'grip-horizontal' | 'hash' | 'hdd-stack' | 'headset' | 'heart-rate' | 'high-engagement' | 'history' | 'hourglass-start' | 'ic-errors' | 'ic-network' | 'ic-rage' | 'ic-resources' | 'id-card' | 'image' | 'info-circle-fill' | 'info-circle' | 'info-square' | 'info' | 'input-hesitation' | 'inspect' | 'integrations/assist' | 'integrations/bugsnag-text' | 'integrations/bugsnag' | 'integrations/cloudwatch-text' | 'integrations/cloudwatch' | 'integrations/datadog' | 'integrations/elasticsearch-text' | 'integrations/elasticsearch' | 'integrations/github' | 'integrations/graphql' | 'integrations/jira-text' | 'integrations/jira' | 'integrations/mobx' | 'integrations/newrelic-text' | 'integrations/newrelic' | 'integrations/ngrx' | 'integrations/openreplay-text' | 'integrations/openreplay' | 'integrations/redux' | 'integrations/rollbar-text' | 'integrations/rollbar' | 'integrations/segment' | 'integrations/sentry-text' | 'integrations/sentry' | 'integrations/slack-bw' | 'integrations/slack' | 'integrations/stackdriver' | 'integrations/sumologic-text' | 'integrations/sumologic' | 'integrations/teams-white' | 'integrations/teams' | 'integrations/vuejs' | 'journal-code' | 'key' | 'layer-group' | 'lightbulb-on' | 'lightbulb' | 'link-45deg' | 'list-alt' | 'list-arrow' | 'list-ul' | 'list' | 'lock-alt' | 'magic' | 'map-marker-alt' | 'memory' | 'mic-mute' | 'mic' | 'minus' | 'mobile' | 'mouse-alt' | 'network' | 'next1' | 'no-dashboard' | 'no-metrics-chart' | 'no-metrics' | 'no-recordings' | 'os/android' | 'os/chrome_os' | 'os/fedora' | 'os/ios' | 'os/linux' | 'os/mac_os_x' | 'os/other' | 'os/ubuntu' | 'os/windows' | 'os' | 'pause-fill' | 'pause' | 'pdf-download' | 'pencil-stop' | 'pencil' | 'people' | 'percent' | 'performance-icon' | 'person-border' | 'person-fill' | 'person' | 'pie-chart-fill' | 'pin-fill' | 'play-circle-bold' | 'play-circle-light' | 'play-circle' | 'play-fill-new' | 'play-fill' | 'play-hover' | 'play' | 'plug' | 'plus-circle' | 'plus-lg' | 'plus' | 'pointer-sessions-search' | 'prev1' | 'pulse' | 'puzzle-piece' | 'puzzle' | 'question-circle' | 'question-lg' | 'quote-left' | 'quote-right' | 'quotes' | 'record-btn' | 'record-circle' | 'redo-back' | 'redo' | 'remote-control' | 'replay-10' | 'resources-icon' | 'safe-fill' | 'safe' | 'sandglass' | 'search' | 'search_notification' | 'server' | 'share-alt' | 'shield-lock' | 'signpost-split' | 'signup' | 'skip-forward-fill' | 'skip-forward' | 'slack' | 'slash-circle' | 'sleep' | 'sliders' | 'social/slack' | 'social/trello' | 'speedometer2' | 'spinner' | 'star-solid' | 'star' | 'step-forward' | 'stickies' | 'stop-record-circle' | 'stopwatch' | 'store' | 'sync-alt' | 'table-new' | 'table' | 'tablet-android' | 'tachometer-slow' | 'tachometer-slowest' | 'tags' | 'team-funnel' | 'telephone-fill' | 'telephone' | 'text-paragraph' | 'toggles' | 'tools' | 'trash' | 'turtle' | 'user-alt' | 'user-circle' | 'user-friends' | 'users' | 'vendors/graphql' | 'vendors/mobx' | 'vendors/ngrx' | 'vendors/redux' | 'vendors/vuex' | 'web-vitals' | 'wifi' | 'window-alt' | 'window-restore' | 'window-x' | 'window' | 'zoom-in';
interface Props {
name: IconNames;
@@ -183,10 +183,11 @@ const SVG = (props: Props) => {
case 'eye-slash': return
;
case 'eye': return
;
case 'fetch': return
;
+ case 'fflag-multi': return
;
+ case 'fflag-single': return
;
case 'file-code': return
;
case 'file-medical-alt': return
;
case 'file-pdf': return
;
- case 'file-play': return
;
case 'file': return
;
case 'files': return
;
case 'filter': return
;
@@ -431,6 +432,7 @@ const SVG = (props: Props) => {
case 'telephone-fill': return
;
case 'telephone': return
;
case 'text-paragraph': return
;
+ case 'toggles': return
;
case 'tools': return
;
case 'trash': return
;
case 'turtle': return
;
diff --git a/frontend/app/components/ui/SideMenuitem/SideMenuitem.js b/frontend/app/components/ui/SideMenuitem/SideMenuitem.tsx
similarity index 80%
rename from frontend/app/components/ui/SideMenuitem/SideMenuitem.js
rename to frontend/app/components/ui/SideMenuitem/SideMenuitem.tsx
index c62b84787..27923b713 100644
--- a/frontend/app/components/ui/SideMenuitem/SideMenuitem.js
+++ b/frontend/app/components/ui/SideMenuitem/SideMenuitem.tsx
@@ -2,22 +2,37 @@ import React from 'react';
import { Icon, Tooltip } from 'UI';
import cn from 'classnames';
import stl from './sideMenuItem.module.css';
+import { IconNames } from 'UI/SVG';
function SideMenuitem({
iconBg = false,
iconColor = "gray-dark",
iconSize = 18,
className = '',
- iconName = null,
+ iconName,
title,
active = false,
disabled = false,
tooltipTitle = '',
onClick,
- deleteHandler = null,
+ deleteHandler,
leading = null,
...props
- }) {
+ }: {
+ title: string;
+ iconName?: IconNames;
+ iconBg?: boolean;
+ iconColor?: string;
+ iconSize?: number;
+ className?: string;
+ active?: boolean;
+ disabled?: boolean;
+ tooltipTitle?: string;
+ onClick?: () => void;
+ deleteHandler?: () => void;
+ leading?: React.ReactNode;
+ id?: string;
+}) {
return (
{
action.data.forEach((item) => {
addElementToFiltersMap(FilterCategory.METADATA, item.key);
addElementToLiveFiltersMap(FilterCategory.METADATA, item.key);
+ addElementToFlagConditionsMap(FilterCategory.METADATA, item.key)
});
return state.set('list', List(action.data).map(CustomField))
diff --git a/frontend/app/duck/search.js b/frontend/app/duck/search.js
index a9ffc0f12..d71e41da6 100644
--- a/frontend/app/duck/search.js
+++ b/frontend/app/duck/search.js
@@ -192,7 +192,7 @@ export const reduceThenFetchResource =
(dispatch, getState) => {
dispatch(actionCreator(...args));
const activeTab = getState().getIn(['search', 'activeTab']);
- if (activeTab.type === 'notes') return;
+ if (['notes', 'flags'].includes(activeTab.type)) return;
const filter = getFilters(getState());
filter.limit = PER_PAGE;
diff --git a/frontend/app/mstore/featureFlagsStore.ts b/frontend/app/mstore/featureFlagsStore.ts
new file mode 100644
index 000000000..494c25995
--- /dev/null
+++ b/frontend/app/mstore/featureFlagsStore.ts
@@ -0,0 +1,165 @@
+import { makeAutoObservable } from 'mobx';
+import FeatureFlag from './types/FeatureFlag';
+import { fflagsService } from 'App/services';
+
+type All = '0'
+type Active = '1'
+type Inactive = '2'
+export type Activity = All | Active | Inactive
+
+export default class FeatureFlagsStore {
+ currentFflag: FeatureFlag | null = null;
+ isDescrEditing: boolean = false;
+ isTitleEditing: boolean = false;
+ flags: FeatureFlag[] = [];
+ isLoading: boolean = false;
+ flagsSearch: string = '';
+ activity: Activity = '0';
+ sort = { order: 'DESC', query: '' };
+ page: number = 1;
+ readonly pageSize: number = 10;
+
+ constructor() {
+ makeAutoObservable(this);
+ }
+
+ setFlagsSearch = (search: string) => {
+ this.flagsSearch = search;
+ };
+
+ setPage = (page: number) => {
+ this.page = page;
+ };
+
+ setEditing = ({ isDescrEditing = false, isTitleEditing = false }) => {
+ this.isDescrEditing = isDescrEditing;
+ this.isTitleEditing = isTitleEditing;
+ };
+
+ setList = (flags: FeatureFlag[]) => {
+ this.flags = flags;
+ };
+
+ removeFromList = (id: FeatureFlag['featureFlagId']) => {
+ this.flags = this.flags.filter((f) => f.featureFlagId !== id);
+ };
+
+ addFlag = (flag: FeatureFlag) => {
+ this.flags.push(flag);
+ };
+
+ getFlagById = (id: string) => {
+ return this.flags.find((f) => f.featureFlagId === parseInt(id, 10));
+ };
+
+ setCurrentFlag = (flag: FeatureFlag | null) => {
+ this.currentFflag = flag;
+ };
+
+ initNewFlag = () => {
+ this.currentFflag = new FeatureFlag();
+ };
+
+ setLoading = (isLoading: boolean) => {
+ this.isLoading = isLoading;
+ };
+
+ setActivity = (activity: Activity) => {
+ this.activity = activity;
+ }
+
+ setSort = (sort: { order: string, query: string }) => {
+ this.sort = sort;
+ }
+
+ fetchFlags = async () => {
+ this.setLoading(true);
+ try {
+ const filters = {
+ limit: this.pageSize,
+ page: this.page,
+ order: this.sort.order,
+ query: this.sort.query,
+ isActive: this.activity === '0' ? undefined : this.activity === '1',
+ // userId: 3,
+ }
+ const { list } = await fflagsService.fetchFlags(filters);
+ const flags = list.map((record) => new FeatureFlag(record));
+ this.setList(flags);
+ } catch (e) {
+ console.error(e);
+ } finally {
+ this.setLoading(false);
+ }
+ };
+
+ checkFlagForm = () => {
+ if (!this.currentFflag) return 'Feature flag not initialized'
+ if (this.currentFflag.flagKey === '') {
+ return 'Feature flag must have a key'
+ }
+ if (this.currentFflag?.variants.findIndex((v) => v.value === '') !== -1) {
+ return 'Variants must include key'
+ }
+ return null;
+ }
+
+ createFlag = async () => {
+ if (this.currentFflag) {
+ this.setLoading(true);
+ try {
+ // @ts-ignore
+ const result = await fflagsService.createFlag(this.currentFflag.toJS());
+ this.addFlag(new FeatureFlag(result));
+ } catch (e) {
+ console.error(e);
+ throw e.response;
+ } finally {
+ this.setLoading(false);
+ }
+ }
+ };
+
+ updateFlag = async (flag?: FeatureFlag, skipLoader?: boolean) => {
+ const usedFlag = flag || this.currentFflag;
+ if (usedFlag) {
+ if (!skipLoader) {
+ this.setLoading(true);
+ }
+ try {
+ // @ts-ignore
+ const result = await fflagsService.updateFlag(usedFlag.toJS());
+ if (!flag) this.setCurrentFlag(new FeatureFlag(result));
+ } catch (e) {
+ console.error('getting api error', e);
+ throw e.response;
+ } finally {
+ this.setLoading(false);
+ }
+ }
+ };
+
+ deleteFlag = async (id: FeatureFlag['featureFlagId']) => {
+ this.setLoading(true);
+ try {
+ await fflagsService.deleteFlag(id);
+ this.removeFromList(id);
+ } catch (e) {
+ console.error(e);
+ } finally {
+ this.setLoading(false);
+ }
+ };
+
+ fetchFlag = async (id: FeatureFlag['featureFlagId']) => {
+ this.setLoading(true);
+ try {
+ const result = await fflagsService.getFlag(id);
+ this.setCurrentFlag(new FeatureFlag(result));
+ } catch (e) {
+ console.error(e);
+ } finally {
+ this.setLoading(false);
+ }
+ };
+}
diff --git a/frontend/app/mstore/index.tsx b/frontend/app/mstore/index.tsx
index e177d86d0..25ee1da0d 100644
--- a/frontend/app/mstore/index.tsx
+++ b/frontend/app/mstore/index.tsx
@@ -19,6 +19,7 @@ import RecordingsStore from './recordingsStore'
import AssistMultiviewStore from './assistMultiviewStore';
import WeeklyReportStore from './weeklyReportConfigStore'
import AlertStore from './alertsStore'
+import FeatureFlagsStore from "./featureFlagsStore";
export class RootStore {
dashboardStore: DashboardStore;
@@ -37,6 +38,7 @@ export class RootStore {
assistMultiviewStore: AssistMultiviewStore;
weeklyReportStore: WeeklyReportStore
alertsStore: AlertStore
+ featureFlagsStore: FeatureFlagsStore
constructor() {
this.dashboardStore = new DashboardStore();
@@ -55,6 +57,7 @@ export class RootStore {
this.assistMultiviewStore = new AssistMultiviewStore();
this.weeklyReportStore = new WeeklyReportStore();
this.alertsStore = new AlertStore();
+ this.featureFlagsStore = new FeatureFlagsStore();
}
initClient() {
diff --git a/frontend/app/mstore/types/FeatureFlag.ts b/frontend/app/mstore/types/FeatureFlag.ts
new file mode 100644
index 000000000..ac83b9601
--- /dev/null
+++ b/frontend/app/mstore/types/FeatureFlag.ts
@@ -0,0 +1,179 @@
+import { makeAutoObservable } from "mobx";
+import { SingleFFlag } from 'App/services/FFlagsService';
+import Filter from "App/mstore/types/filter";
+
+export class Conditions {
+ rolloutPercentage = 100;
+ filter = new Filter().fromJson({ name: 'Rollout conditions', filters: [] })
+
+ constructor(data?: Record) {
+ makeAutoObservable(this)
+ if (data) {
+ this.rolloutPercentage = data.rolloutPercentage
+ this.filter = new Filter().fromJson(data)
+ }
+ }
+
+ setRollout = (value: number) => {
+ this.rolloutPercentage = value
+ }
+
+ toJS() {
+ return {
+ name: this.filter.name,
+ rolloutPercentage: this.rolloutPercentage,
+ filters: this.filter.filters.map(f => f.toJson()),
+ }
+ }
+}
+
+const initData = {
+ flagKey: '',
+ isActive: false,
+ isPersist: false,
+ isSingleOption: true,
+ conditions: [],
+ description: '',
+ featureFlagId: 0,
+ createdAt: 0,
+ updatedAt: 0,
+ createdBy: 0,
+ updatedBy: 0,
+}
+
+export class Variant {
+ index: number;
+ value: string = '';
+ description: string = '';
+ payload: string = '';
+ rolloutPercentage: number = 100;
+
+ constructor(index: number, data?: Record) {
+ Object.assign(this, data)
+ this.index = index;
+ makeAutoObservable(this)
+ }
+
+ setIndex = (index: number) => {
+ this.index = index;
+ }
+
+ setKey = (key: string) => {
+ this.value = key.replace(/\s/g, '-');
+ }
+
+ setDescription = (description: string) => {
+ this.description = description;
+ }
+
+ setPayload = (payload: string) => {
+ this.payload = payload;
+ }
+
+ setRollout = (rollout: number) => {
+ if (rollout <= 100) {
+ this.rolloutPercentage = rollout;
+ }
+ }
+}
+
+export default class FeatureFlag {
+ flagKey: SingleFFlag['flagKey']
+ conditions: Conditions[]
+ createdBy?: SingleFFlag['createdBy']
+ createdAt?: SingleFFlag['createdAt']
+ updatedAt?: SingleFFlag['updatedAt']
+ updatedBy?: SingleFFlag['updatedBy']
+ isActive: SingleFFlag['isActive']
+ description: SingleFFlag['description']
+ isPersist: SingleFFlag['isPersist']
+ isSingleOption: boolean
+ featureFlagId: SingleFFlag['featureFlagId']
+ payload: SingleFFlag['payload']
+ flagType: string;
+ variants: Variant[] = [];
+
+ constructor(data?: SingleFFlag) {
+ Object.assign(
+ this,
+ initData,
+ {
+ ...data,
+ isSingleOption: data ? data.flagType === 'single' : true,
+ conditions: data?.conditions?.map(c => new Conditions(c)) || [new Conditions()],
+ variants: data?.flagType === 'multi' ? data?.variants?.map((v, i) => new Variant(i, v)) : [new Variant(1)],
+ });
+
+ makeAutoObservable(this);
+ }
+
+ setPayload = (payload: string) => {
+ this.payload = payload;
+ }
+
+ addVariant = () => {
+ this.variants.push(new Variant(this.variants.length + 1))
+ this.redistributeVariants()
+ }
+
+ removeVariant = (index: number) => {
+ this.variants = this.variants.filter(v => v.index !== index)
+ }
+
+ get isRedDistribution() {
+ const totalRollout = this.variants.reduce((acc, v) => acc + v.rolloutPercentage, 0)
+
+ return Math.floor(
+ totalRollout/this.variants.length) !== Math.floor(100 / this.variants.length)
+ }
+
+ redistributeVariants = () => {
+ const newRolloutDistribution = Math.floor(100 / this.variants.length)
+ this.variants.forEach(v => v.setRollout(newRolloutDistribution))
+ }
+
+ toJS() {
+ return {
+ flagKey: this.flagKey,
+ conditions: this.conditions.map(c => c.toJS()),
+ createdBy: this.createdBy,
+ updatedBy: this.createdBy,
+ createdAt: this.createdAt,
+ updatedAt: this.updatedAt,
+ isActive: this.isActive,
+ description: this.description,
+ isPersist: this.isPersist,
+ flagType: this.isSingleOption ? 'single' as const : 'multi' as const,
+ featureFlagId: this.featureFlagId,
+ variants: this.isSingleOption ? undefined : this.variants.map(v => ({ value: v.value, description: v.description, payload: v.payload, rolloutPercentage: v.rolloutPercentage })),
+ }
+ }
+
+ addCondition = () => {
+ this.conditions.push(new Conditions())
+ }
+
+ removeCondition = (index: number) => {
+ this.conditions.splice(index, 1)
+ }
+
+ setFlagKey = (flagKey: string) => {
+ this.flagKey = flagKey;
+ }
+
+ setDescription = (description: string) => {
+ this.description = description;
+ }
+
+ setIsPersist = (isPersist: boolean) => {
+ this.isPersist = isPersist;
+ }
+
+ setIsSingleOption = (isSingleOption: boolean) => {
+ this.isSingleOption = isSingleOption;
+ }
+
+ setIsEnabled = (isEnabled: boolean) => {
+ this.isActive = isEnabled;
+ }
+}
\ No newline at end of file
diff --git a/frontend/app/mstore/types/filter.ts b/frontend/app/mstore/types/filter.ts
index 9b2874e9f..82d087072 100644
--- a/frontend/app/mstore/types/filter.ts
+++ b/frontend/app/mstore/types/filter.ts
@@ -34,7 +34,7 @@ export default class Filter {
addFilter(filter: any) {
filter.value = [""]
if (Array.isArray(filter.filters)) {
- filter.filters = filter.filters.map(i => {
+ filter.filters = filter.filters.map((i: Record) => {
i.value = [""]
return new FilterItem(i)
})
@@ -47,6 +47,7 @@ export default class Filter {
}
updateKey(key: string, value: any) {
+ // @ts-ignore fix later
this[key] = value
}
@@ -56,7 +57,7 @@ export default class Filter {
fromJson(json: any) {
this.name = json.name
- this.filters = json.filters.map(i => new FilterItem().fromJson(i))
+ this.filters = json.filters.map((i: Record) => new FilterItem().fromJson(i))
this.eventsOrder = json.eventsOrder
return this
}
diff --git a/frontend/app/mstore/types/filterItem.ts b/frontend/app/mstore/types/filterItem.ts
index 65d79e22d..ad714384d 100644
--- a/frontend/app/mstore/types/filterItem.ts
+++ b/frontend/app/mstore/types/filterItem.ts
@@ -37,7 +37,7 @@ export default class FilterItem {
});
if (Array.isArray(data.filters)) {
- data.filters = data.filters.map(function (i) {
+ data.filters = data.filters.map(function (i: Record) {
return new FilterItem(i);
});
}
@@ -46,11 +46,13 @@ export default class FilterItem {
}
updateKey(key: string, value: any) {
+ // @ts-ignore
this[key] = value;
}
merge(data: any) {
Object.keys(data).forEach((key) => {
+ // @ts-ignore
this[key] = data[key];
});
}
@@ -63,8 +65,10 @@ export default class FilterItem {
const mainFilter = filtersMap[mainFilterKey];
const subFilterMap = {};
mainFilter.filters.forEach((option: any) => {
+ // @ts-ignore
subFilterMap[option.key] = option;
});
+ // @ts-ignore
_filter = subFilterMap[json.type];
}
this.type = _filter.type;
@@ -77,7 +81,7 @@ export default class FilterItem {
this.options = _filter.options;
this.isEvent = _filter.isEvent;
- (this.value = json.value.length === 0 || !json.value ? [''] : json.value);
+ (this.value = !json.value || json.value.length === 0 ? [''] : json.value);
(this.operator = json.operator);
this.source = json.source;
this.sourceOperator = json.sourceOperator;
diff --git a/frontend/app/routes.js b/frontend/app/routes.js
index 411b48ba5..1e030f208 100644
--- a/frontend/app/routes.js
+++ b/frontend/app/routes.js
@@ -83,6 +83,10 @@ const routerOBTabString = `:activeTab(${ Object.values(OB_TABS).join('|') })`;
export const onboarding = (tab = routerOBTabString) => `/onboarding/${ tab }`;
export const sessions = params => queried('/sessions', params);
+export const fflags = params => queried('/feature-flags', params);
+export const newFFlag = () => '/feature-flags/create';
+export const fflag = (id = ':fflagId', hash) => hashed(`/feature-flags/${ id }`, hash);
+export const notes = params => queried('/notes', params);
export const assist = params => queried('/assist', params);
export const recordings = params => queried("/recordings", params);
export const multiviewIndex = params => queried('/multiview', params);
@@ -97,13 +101,7 @@ export const funnels = params => queried('/funnels', params)
export const funnelsCreate = () => `/funnels/create`;
export const funnel = (id = ':funnelId', hash) => hashed(`/funnels/${ id }`, hash);
export const funnelIssue = (id = ':funnelId', issueId = ':issueId', hash) => hashed(`/funnels/${ id }/${ issueId}`, hash);
-
export const tests = () => '/tests';
-
-export const testBuilderNew = () => '/test-sessions';
-
-export const testBuilder = (testId = ':testId') => `/test-builder/${ testId }`;
-
export const dashboard = () => '/dashboard';
export const dashboardMetrics = () => '/dashboard/metrics';
export const dashboardSelected = (id = ':dashboardId', hash) => hashed(`/dashboard/${ id }`, hash);
@@ -123,6 +121,10 @@ const REQUIRED_SITE_ID_ROUTES = [
liveSession(''),
session(''),
sessions(),
+ newFFlag(),
+ fflag(),
+ notes(),
+ fflags(),
assist(),
recordings(),
multiview(),
@@ -174,6 +176,8 @@ export function isRoute(route, path){
const SITE_CHANGE_AVALIABLE_ROUTES = [
sessions(),
+ notes(),
+ fflags(),
funnels(),
assist(),
recordings(),
diff --git a/frontend/app/services/FFlagsService.ts b/frontend/app/services/FFlagsService.ts
new file mode 100644
index 000000000..28fe133c4
--- /dev/null
+++ b/frontend/app/services/FFlagsService.ts
@@ -0,0 +1,84 @@
+import BaseService from 'App/services/BaseService';
+
+type FFlagType = 'single' | 'multi';
+type FFlagCondition = {
+ name: string;
+ rolloutPercentage: number;
+ filters: [];
+};
+
+export interface SimpleFlag {
+ name: string;
+ flagKey: string;
+ description: string;
+ flagType: FFlagType;
+ isPersist: boolean;
+ conditions: FFlagCondition[];
+ payload?: string;
+}
+
+type Variant = {
+ variantId?: number;
+ value: string;
+ description?: string;
+ payload: string;
+ rolloutPercentage: number;
+}
+
+export interface FFlag extends SimpleFlag {
+ featureFlagId: number;
+ isActive: boolean;
+ createdAt: number;
+ updatedAt: number;
+ createdBy: number;
+ updatedBy: number;
+ conditions: never;
+ variants: Variant[]
+}
+
+export interface SingleFFlag extends SimpleFlag {
+ createdAt: number;
+ updatedAt: number;
+ createdBy: number;
+ updatedBy: number;
+ featureFlagId: number;
+ isActive: boolean;
+ variants: Variant[]
+}
+
+export default class FFlagsService extends BaseService {
+ fetchFlags(filters: Record): Promise<{ list: FFlag[]; total: number }> {
+ return this.client
+ .post('/feature-flags/search', filters)
+ .then((r) => r.json())
+ .then((j) => j.data || []);
+ }
+
+ createFlag(flag: SimpleFlag): Promise {
+ return this.client
+ .post('/feature-flags', flag)
+ .then((r) => r.json())
+ .then((j) => j.data || {});
+ }
+
+ updateFlag(flag: FFlag): Promise {
+ return this.client
+ .put(`/feature-flags/${flag.featureFlagId}`, flag)
+ .then((r) => r.json())
+ .then((j) => j.data || {});
+ }
+
+ deleteFlag(id: number): Promise {
+ return this.client
+ .delete(`/feature-flags/${id}`)
+ .then((r) => r.json())
+ .then((j) => j.data || {});
+ }
+
+ getFlag(id: number): Promise {
+ return this.client
+ .get(`/feature-flags/${id}`)
+ .then((r) => r.json())
+ .then((j) => j.data || {});
+ }
+}
diff --git a/frontend/app/services/index.ts b/frontend/app/services/index.ts
index 32e216127..bc6e4bff6 100644
--- a/frontend/app/services/index.ts
+++ b/frontend/app/services/index.ts
@@ -11,6 +11,7 @@ import ConfigService from './ConfigService'
import AlertsService from './AlertsService'
import WebhookService from './WebhookService'
import HealthService from "./HealthService";
+import FFlagsService from "App/services/FFlagsService";
export const dashboardService = new DashboardService();
export const metricService = new MetricService();
@@ -27,6 +28,8 @@ export const webhookService = new WebhookService();
export const healthService = new HealthService();
+export const fflagsService = new FFlagsService();
+
export const services = [
dashboardService,
metricService,
@@ -41,4 +44,5 @@ export const services = [
alertsService,
webhookService,
healthService,
+ fflagsService
]
\ No newline at end of file
diff --git a/frontend/app/svg/icons/fflag-multi.svg b/frontend/app/svg/icons/fflag-multi.svg
new file mode 100644
index 000000000..70f79fcaa
--- /dev/null
+++ b/frontend/app/svg/icons/fflag-multi.svg
@@ -0,0 +1,9 @@
+
diff --git a/frontend/app/svg/icons/fflag-single.svg b/frontend/app/svg/icons/fflag-single.svg
new file mode 100644
index 000000000..ae951d2a7
--- /dev/null
+++ b/frontend/app/svg/icons/fflag-single.svg
@@ -0,0 +1,6 @@
+
diff --git a/frontend/app/svg/icons/toggles.svg b/frontend/app/svg/icons/toggles.svg
new file mode 100644
index 000000000..149614309
--- /dev/null
+++ b/frontend/app/svg/icons/toggles.svg
@@ -0,0 +1,10 @@
+
diff --git a/frontend/app/svg/no-fflags.svg b/frontend/app/svg/no-fflags.svg
new file mode 100644
index 000000000..babb3e944
--- /dev/null
+++ b/frontend/app/svg/no-fflags.svg
@@ -0,0 +1,89 @@
+
diff --git a/frontend/app/types/filter/newFilter.js b/frontend/app/types/filter/newFilter.js
index 9aeedf9ff..866d73062 100644
--- a/frontend/app/types/filter/newFilter.js
+++ b/frontend/app/types/filter/newFilter.js
@@ -1,3 +1,4 @@
+import { KEYS } from 'Types/filter/customFilter';
import Record from 'Types/Record';
import { FilterType, FilterKey, FilterCategory } from './filterType'
import filterOptions, { countries, platformOptions } from 'App/constants';
@@ -55,13 +56,32 @@ export const filters = [
{ key: FilterKey.ISSUE, type: FilterType.ISSUE, category: FilterCategory.JAVASCRIPT, label: 'Issue', placeholder: 'Select an issue', operator: 'is', operatorOptions: filterOptions.getOperatorsByKeys(['is', 'isAny', 'isNot']), icon: 'filters/click', options: filterOptions.issueOptions },
];
+export const flagConditionFilters = [
+ { key: FilterKey.USER_OS, type: FilterType.MULTIPLE, category: FilterCategory.GEAR, label: 'User OS', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/os' },
+ { key: FilterKey.USER_BROWSER, type: FilterType.MULTIPLE, category: FilterCategory.GEAR, label: 'User Browser', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/browser' },
+ { key: FilterKey.USER_DEVICE, type: FilterType.MULTIPLE, category: FilterCategory.GEAR, label: 'User Device', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/device' },
+ { key: FilterKey.REFERRER, type: FilterType.MULTIPLE, category: FilterCategory.USER, label: 'Referrer', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/arrow-return-right' },
+ { key: FilterKey.USER_COUNTRY, type: FilterType.MULTIPLE_DROPDOWN, category: FilterCategory.USER, label: 'User Country', operator: 'is', operatorOptions: filterOptions.getOperatorsByKeys(['is', 'isAny', 'isNot']), icon: 'filters/country', options: countryOptions },
+ { key: FilterKey.USER_CITY, type: FilterType.MULTIPLE, category: FilterCategory.USER, label: 'User City', operator: 'is', operatorOptions: filterOptions.getOperatorsByKeys(['is', 'isAny', 'isNot']), icon: 'filters/country', options: countryOptions },
+ { key: FilterKey.USER_STATE, type: FilterType.MULTIPLE, category: FilterCategory.USER, label: 'User State', operator: 'is', operatorOptions: filterOptions.getOperatorsByKeys(['is', 'isAny', 'isNot']), icon: 'filters/country', options: countryOptions },
+ { key: FilterKey.USERID, type: FilterType.MULTIPLE, category: FilterCategory.USER, label: 'User Id', operator: 'isUndefined', operatorOptions: [{ label: 'is undefined', value: 'isUndefined'}, { key: 'isAny', label: 'is any', value: 'isAny' }], icon: 'filters/userid' },
+]
+
export const eventKeys = filters.filter((i) => i.isEvent).map(i => i.key);
+export const nonFlagFilters = filters.filter(i => {
+ return flagConditionFilters.findIndex(f => f.key === i.key) === -1
+}).map(i => i.key);
export const clickmapFilter = {
key: FilterKey.LOCATION,
type: FilterType.MULTIPLE,
category: FilterCategory.INTERACTIONS,
- label: 'Visited URL', placeholder: 'Enter URL or path', operator: filterOptions.pageUrlOperators[0].value, operatorOptions: filterOptions.pageUrlOperators, icon: 'filters/location', isEvent: true }
+ label: 'Visited URL', placeholder: 'Enter URL or path',
+ operator: filterOptions.pageUrlOperators[0].value,
+ operatorOptions: filterOptions.pageUrlOperators,
+ icon: 'filters/location',
+ isEvent: true,
+}
const mapFilters = (list) => {
return list.reduce((acc, filter) => {
@@ -99,6 +119,7 @@ export const filterLabelMap = filters.reduce((acc, filter) => {
export let filtersMap = mapFilters(filters)
export let liveFiltersMap = mapLiveFilters(filters)
+export let fflagsConditionsMap = mapFilters(flagConditionFilters)
export const clearMetaFilters = () => {
filtersMap = mapFilters(filters);
@@ -125,6 +146,17 @@ export const addElementToFiltersMap = (
filtersMap[key] = { key, type, category, label: capitalize(key), operator: operator, operatorOptions, icon, isLive: true }
}
+export const addElementToFlagConditionsMap = (
+ category = FilterCategory.METADATA,
+ key,
+ type = FilterType.MULTIPLE,
+ operator = 'is',
+ operatorOptions = filterOptions.stringOperators,
+ icon = 'filters/metadata'
+) => {
+ fflagsConditionsMap[key] = { key, type, category, label: capitalize(key), operator: operator, operatorOptions, icon, isLive: true }
+}
+
export const addElementToLiveFiltersMap = (
category = FilterCategory.METADATA,
key,
@@ -233,6 +265,21 @@ export const generateFilterOptions = (map) => {
return filterSection;
}
+export const generateFlagConditionOptions = (map) => {
+ const filterSection = {};
+ Object.keys(map).forEach(key => {
+ const filter = map[key];
+ if (filterSection.hasOwnProperty(filter.category)) {
+ filterSection[filter.category].push(filter);
+ } else {
+ filterSection[filter.category] = [filter];
+ }
+ });
+ return filterSection;
+}
+
+
+
export const generateLiveFilterOptions = (map) => {
const filterSection = {};
diff --git a/frontend/package.json b/frontend/package.json
index 5a9267ad2..ebd253de7 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -93,7 +93,6 @@
"@babel/preset-typescript": "^7.17.12",
"@babel/runtime": "^7.17.9",
"@jest/globals": "^29.3.1",
- "@mdx-js/react": "^1.6.22",
"@openreplay/sourcemap-uploader": "^3.0.0",
"@storybook/addon-actions": "^6.5.12",
"@storybook/addon-docs": "^6.5.12",
@@ -124,13 +123,10 @@
"cssnano": "^5.0.12",
"cypress": "^12.3.0",
"cypress-image-snapshot": "^4.0.1",
- "deasync-promise": "^1.0.1",
- "deploy-aws-s3-cloudfront": "^3.6.0",
"dotenv": "^6.2.0",
"eslint": "^8.15.0",
"eslint-plugin-react": "^7.29.4",
"eslint-plugin-react-hooks": "^4.5.0",
- "faker": "^5.5.3",
"file-loader": "^6.2.0",
"flow-bin": "^0.115.0",
"html-webpack-plugin": "^5.5.0",
diff --git a/tracker/tracker/src/main/app/index.ts b/tracker/tracker/src/main/app/index.ts
index 2b4ea4837..6d16f277a 100644
--- a/tracker/tracker/src/main/app/index.ts
+++ b/tracker/tracker/src/main/app/index.ts
@@ -109,12 +109,14 @@ export default class App {
private readonly startCallbacks: Array = []
private readonly stopCallbacks: Array<() => any> = []
private readonly commitCallbacks: Array = []
- private readonly options: AppOptions
+ public readonly options: AppOptions
public readonly networkOptions?: NetworkOptions
private readonly revID: string
private activityState: ActivityState = ActivityState.NotActive
private readonly version = 'TRACKER_VERSION' // TODO: version compatability check inside each plugin.
private readonly worker?: TypedWorker
+ private featureFlags: string[] = []
+
private compressionThreshold = 24 * 1000
private restartAttempts = 0
private readonly bc: BroadcastChannel = new BroadcastChannel('rick')
@@ -449,6 +451,14 @@ export default class App {
return this.activityState === ActivityState.Active
}
+ isFeatureActive(feature: string): boolean {
+ return this.featureFlags.includes(feature)
+ }
+
+ getFeatureFlags(): string[] {
+ return this.featureFlags
+ }
+
resetNextPageSession(flag: boolean) {
if (flag) {
this.sessionStorage.setItem(this.options.session_reset_key, 't')
@@ -547,7 +557,14 @@ export default class App {
delay, // derived from token
sessionID, // derived from token
startTimestamp, // real startTS (server time), derived from sessionID
+ userBrowser,
+ userCity,
+ userCountry,
+ userDevice,
+ userOS,
+ userState,
} = r
+ // TODO: insert feature flags here
if (
typeof token !== 'string' ||
typeof userUUID !== 'string' ||
@@ -560,6 +577,14 @@ export default class App {
}
this.delay = delay
this.session.setSessionToken(token)
+ this.session.setUserInfo({
+ userBrowser,
+ userCity,
+ userCountry,
+ userDevice,
+ userOS,
+ userState,
+ })
this.session.assign({
sessionID,
timestamp: startTimestamp || timestamp,
diff --git a/tracker/tracker/src/main/app/session.ts b/tracker/tracker/src/main/app/session.ts
index ecf1a984c..c308fdc99 100644
--- a/tracker/tracker/src/main/app/session.ts
+++ b/tracker/tracker/src/main/app/session.ts
@@ -1,6 +1,15 @@
import type App from './index.js'
import { generateRandomId } from '../utils.js'
+interface UserInfo {
+ userBrowser: string
+ userCity: string
+ userCountry: string
+ userDevice: string
+ userOS: string
+ userState: string
+}
+
interface SessionInfo {
sessionID: string | undefined
metadata: Record
@@ -24,6 +33,7 @@ export default class Session {
private timestamp = 0
private projectID: string | undefined
private tabId: string
+ public userInfo: UserInfo
constructor(private readonly app: App, private readonly options: Options) {
this.createTabId()
@@ -72,6 +82,10 @@ export default class Session {
this.handleUpdate({ userID })
}
+ setUserInfo(userInfo: UserInfo) {
+ this.userInfo = userInfo
+ }
+
private getPageNumber(): number | undefined {
const pageNoStr = this.app.sessionStorage.getItem(this.options.session_pageno_key)
if (pageNoStr == null) {
diff --git a/tracker/tracker/src/main/index.ts b/tracker/tracker/src/main/index.ts
index d15fc23fa..1ba6d43c9 100644
--- a/tracker/tracker/src/main/index.ts
+++ b/tracker/tracker/src/main/index.ts
@@ -27,6 +27,7 @@ import ConstructedStyleSheets from './modules/constructedStyleSheets.js'
import Selection from './modules/selection.js'
import Tabs from './modules/tabs.js'
import { IN_BROWSER, deprecationWarn, DOCS_HOST } from './utils.js'
+import FeatureFlags, { IFeatureFlag } from './modules/featureFlags.js'
import type { Options as AppOptions } from './app/index.js'
import type { Options as ConsoleOptions } from './modules/console.js'
@@ -51,6 +52,9 @@ export type Options = Partial<
autoResetOnWindowOpen?: boolean
network?: NetworkOptions
mouse?: MouseHandlerOptions
+ flags?: {
+ onFlagsLoad?: (flags: IFeatureFlag[]) => void
+ }
// dev only
__DISABLE_SECURE_MODE?: boolean
}
@@ -88,6 +92,7 @@ function processOptions(obj: any): obj is Options {
}
export default class API {
+ public featureFlags: FeatureFlags
private readonly app: App | null = null
constructor(private readonly options: Options) {
if (!IN_BROWSER || !processOptions(options)) {
@@ -138,8 +143,15 @@ export default class API {
Network(app, options.network)
Selection(app)
Tabs(app)
+ this.featureFlags = new FeatureFlags(app)
;(window as any).__OPENREPLAY__ = this
+ app.attachStartCallback(() => {
+ if (options.flags?.onFlagsLoad) {
+ this.featureFlags.onFlagsLoad(options.flags.onFlagsLoad)
+ }
+ void this.featureFlags.reloadFlags()
+ })
if (options.autoResetOnWindowOpen) {
const wOpen = window.open
app.attachStartCallback(() => {
@@ -174,6 +186,22 @@ export default class API {
}
}
+ isFlagEnabled(flagName: string): boolean {
+ return this.featureFlags.isFlagEnabled(flagName)
+ }
+
+ onFlagsLoad(callback: (flags: IFeatureFlag[]) => void): void {
+ this.featureFlags.onFlagsLoad(callback)
+ }
+
+ clearPersistFlags() {
+ this.featureFlags.clearPersistFlags()
+ }
+
+ reloadFlags() {
+ return this.featureFlags.reloadFlags()
+ }
+
use(fn: (app: App | null, options?: Options) => T): T {
return fn(this.app, this.options)
}
@@ -198,6 +226,7 @@ export default class API {
// TODO: check argument type
return this.app.start(startOpts)
}
+
stop(): string | undefined {
if (this.app === null) {
return
diff --git a/tracker/tracker/src/main/modules/featureFlags.ts b/tracker/tracker/src/main/modules/featureFlags.ts
new file mode 100644
index 000000000..8c40a022f
--- /dev/null
+++ b/tracker/tracker/src/main/modules/featureFlags.ts
@@ -0,0 +1,106 @@
+import App from '../app/index.js'
+
+export interface IFeatureFlag {
+ key: string
+ is_persist: boolean
+ value: string | boolean
+ payload: string
+}
+
+export interface FetchPersistFlagsData {
+ key: string
+ value: string | boolean
+}
+
+export default class FeatureFlags {
+ flags: IFeatureFlag[]
+ storageKey = '__openreplay_flags'
+ onFlagsCb: (flags: IFeatureFlag[]) => void
+
+ constructor(private readonly app: App) {
+ const persistFlags = this.app.sessionStorage.getItem(this.storageKey)
+ if (persistFlags) {
+ const persistFlagsStrArr = persistFlags.split(';').filter(Boolean)
+ this.flags = persistFlagsStrArr.map((flag) => JSON.parse(flag))
+ }
+ }
+
+ isFlagEnabled(flagName: string): boolean {
+ return this.flags.findIndex((flag) => flag.key === flagName) !== -1
+ }
+
+ onFlagsLoad(cb: (flags: IFeatureFlag[]) => void) {
+ this.onFlagsCb = cb
+ }
+
+ async reloadFlags() {
+ const persistFlagsStr = this.app.sessionStorage.getItem(this.storageKey)
+ const persistFlags: Record = {}
+ if (persistFlagsStr) {
+ const persistArray = persistFlagsStr.split(';').filter(Boolean)
+ persistArray.forEach((flag) => {
+ const flagObj = JSON.parse(flag)
+ persistFlags[flagObj.key] = { key: flagObj.key, value: flagObj.value }
+ })
+ }
+ const sessionInfo = this.app.session.getInfo()
+ const userInfo = this.app.session.userInfo
+ const requestObject = {
+ projectID: sessionInfo.projectID,
+ userID: sessionInfo.userID,
+ metadata: sessionInfo.metadata,
+ referrer: document.referrer,
+ // todo: get from backend
+ os: userInfo.userOS,
+ device: userInfo.userDevice,
+ country: userInfo.userCountry,
+ state: userInfo.userState,
+ city: userInfo.userCity,
+ browser: userInfo.userBrowser,
+ persistFlags: persistFlags,
+ }
+
+ const resp = await fetch(this.app.options.ingestPoint + '/v1/web/feature-flags', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: `Bearer ${this.app.session.getSessionToken() as string}`,
+ },
+ body: JSON.stringify(requestObject),
+ })
+ if (resp.status === 200) {
+ const data: { flags: IFeatureFlag[] } = await resp.json()
+ return this.handleFlags(data.flags)
+ }
+ }
+
+ handleFlags(flags: IFeatureFlag[]) {
+ const persistFlags: IFeatureFlag[] = []
+ flags.forEach((flag) => {
+ if (flag.is_persist) persistFlags.push(flag)
+ })
+
+ let str = ''
+ const uniquePersistFlags = this.diffPersist(persistFlags)
+ uniquePersistFlags.forEach((flag) => {
+ str += `${JSON.stringify(flag)};`
+ })
+
+ this.app.sessionStorage.setItem(this.storageKey, str)
+ this.flags = flags
+ return this.onFlagsCb?.(flags)
+ }
+
+ clearPersistFlags() {
+ this.app.sessionStorage.removeItem(this.storageKey)
+ }
+
+ diffPersist(flags: IFeatureFlag[]) {
+ const persistFlags = this.app.sessionStorage.getItem(this.storageKey)
+ if (!persistFlags) return flags
+ const persistFlagsStrArr = persistFlags.split(';').filter(Boolean)
+ const persistFlagsArr = persistFlagsStrArr.map((flag) => JSON.parse(flag))
+
+ return flags.filter((flag) => persistFlagsArr.findIndex((pf) => pf.key === flag.key) === -1)
+ }
+}
diff --git a/tracker/tracker/src/tests/featureFlags.unit.test.ts b/tracker/tracker/src/tests/featureFlags.unit.test.ts
new file mode 100644
index 000000000..95d204f73
--- /dev/null
+++ b/tracker/tracker/src/tests/featureFlags.unit.test.ts
@@ -0,0 +1,136 @@
+import FeatureFlags, { FetchPersistFlagsData, IFeatureFlag } from '../main/modules/FeatureFlags'
+import { describe, expect, jest, afterEach, beforeEach, test } from '@jest/globals'
+
+jest.mock('../main/app/index.js')
+
+const sessionInfo = {
+ projectID: 'project1',
+ userID: 'user1',
+ metadata: {},
+}
+const userInfo = {
+ userOS: 'test',
+ userDevice: 'test',
+ userCountry: 'test',
+ userState: 'test',
+ userCity: 'test',
+ userBrowser: 'test',
+}
+describe('FeatureFlags', () => {
+ // @ts-ignore
+ let featureFlags: FeatureFlags
+ let appMock = {
+ sessionStorage: { setItem: jest.fn(), getItem: jest.fn(), removeItem: jest.fn() },
+ options: {
+ ingestPoint: 'test',
+ },
+ session: {
+ getInfo: () => sessionInfo,
+ getSessionToken: () => '123',
+ userInfo: userInfo,
+ },
+ }
+
+ beforeEach(() => {
+ // @ts-ignore
+ featureFlags = new FeatureFlags(appMock)
+ })
+
+ afterEach(() => {
+ jest.restoreAllMocks()
+ })
+
+ test('should check if a flag is enabled', () => {
+ const flagName = 'flag1'
+ featureFlags.flags = [
+ { payload: '', is_persist: false, key: 'flag1', value: '' },
+ { payload: '', is_persist: false, key: 'flag2', value: '' },
+ ]
+
+ const result = featureFlags.isFlagEnabled(flagName)
+
+ expect(result).toBe(true)
+ })
+
+ test('should invoke the callback function when flags are loaded', () => {
+ const flags = [{ key: 'flag1', is_persist: false, value: true, payload: 'payload1' }]
+ const callback = jest.fn()
+
+ featureFlags.onFlagsLoad(callback)
+ featureFlags.handleFlags(flags)
+
+ expect(callback).toHaveBeenCalledWith(flags)
+ })
+
+ test('should reload flags and handle the response', async () => {
+ const flags = [
+ { key: 'flag1', is_persist: true, value: true, payload: 'payload1' },
+ { key: 'flag2', is_persist: false, value: false, payload: 'payload2' },
+ ]
+ const expectedRequestObject = {
+ projectID: sessionInfo.projectID,
+ userID: sessionInfo.userID,
+ metadata: sessionInfo.metadata,
+ referrer: '',
+ featureFlags: featureFlags.flags,
+ os: 'test',
+ device: 'test',
+ country: 'test',
+ state: 'test',
+ city: 'test',
+ browser: 'test',
+ persistFlags: {},
+ }
+ const spyOnHandle = jest.spyOn(featureFlags, 'handleFlags')
+ const expectedResponse = { flags }
+ // @ts-ignore
+ global.fetch = jest.fn().mockResolvedValue({
+ status: 200,
+ // @ts-ignore
+ json: jest.fn().mockResolvedValue(expectedResponse),
+ })
+
+ await featureFlags.reloadFlags()
+
+ expect(fetch).toHaveBeenCalledWith(
+ `${appMock.options.ingestPoint}/v1/web/feature-flags`,
+ expect.objectContaining({
+ method: 'POST',
+ headers: expect.objectContaining({
+ 'Content-Type': 'application/json',
+ Authorization: `Bearer ${appMock.session.getSessionToken()}`,
+ }),
+ body: JSON.stringify(expectedRequestObject),
+ }),
+ )
+ expect(spyOnHandle).toHaveBeenCalledWith(flags)
+ })
+
+ test('should clear persisted flags', () => {
+ featureFlags.clearPersistFlags()
+
+ expect(appMock.sessionStorage.removeItem).toHaveBeenCalledWith(featureFlags.storageKey)
+ })
+
+ test('should calculate the diff of persisted flags', () => {
+ const flags: IFeatureFlag[] = [
+ { key: 'flag1', value: true, payload: '', is_persist: true },
+ { key: 'flag2', value: false, payload: '123', is_persist: true },
+ { key: 'flag3', value: false, payload: '123', is_persist: true },
+ ]
+ const existingFlags: IFeatureFlag[] = [
+ { key: 'flag1', value: true, payload: '', is_persist: true },
+ { key: 'flag2', value: false, payload: '123', is_persist: true },
+ ]
+ let str = ''
+ existingFlags.forEach((flag) => {
+ str += `${JSON.stringify(flag)};`
+ })
+ // @ts-ignore
+ appMock.sessionStorage.getItem = jest.fn().mockReturnValue(str)
+
+ const result = featureFlags.diffPersist(flags)
+
+ expect(result).toEqual([flags[2]])
+ })
+})