From ac4466840ed74e0c21162d4613d6cf4e30a8c9b2 Mon Sep 17 00:00:00 2001 From: Paul D'Ambra Date: Sun, 15 Sep 2024 19:58:51 +0100 Subject: [PATCH] feat: add replay to omnisearch (#24978) --- .../CommandPalette/commandPaletteLogic.tsx | 5 +- .../replayPaletteCommands.ts | 100 ++++++++++++++++++ 2 files changed, 104 insertions(+), 1 deletion(-) create mode 100644 frontend/src/scenes/session-recordings/replayPaletteCommands.ts diff --git a/frontend/src/lib/components/CommandPalette/commandPaletteLogic.tsx b/frontend/src/lib/components/CommandPalette/commandPaletteLogic.tsx index e462fed6e1c8f..54b435c8732c8 100644 --- a/frontend/src/lib/components/CommandPalette/commandPaletteLogic.tsx +++ b/frontend/src/lib/components/CommandPalette/commandPaletteLogic.tsx @@ -56,6 +56,7 @@ import posthog from 'posthog-js' import { newDashboardLogic } from 'scenes/dashboard/newDashboardLogic' import { insightTypeURL } from 'scenes/insights/utils' import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' +import { WATCH_RECORDINGS_OF_KEY, watchRecordingsOfCommand } from 'scenes/session-recordings/replayPaletteCommands' import { teamLogic } from 'scenes/teamLogic' import { urls } from 'scenes/urls' import { userLogic } from 'scenes/userLogic' @@ -119,7 +120,7 @@ export type RegExpCommandPairs = [RegExp | null, Command][] const RESULTS_MAX = 5 -const GLOBAL_COMMAND_SCOPE = 'global' +export const GLOBAL_COMMAND_SCOPE = 'global' function resolveCommand(source: Command | CommandFlow, argument?: string, prefixApplied?: string): CommandResult[] { // run resolver or use ready-made results @@ -931,6 +932,7 @@ export const commandPaletteLogic = kea([ actions.registerCommand(toggleHedgehogMode) actions.registerCommand(shortcuts) actions.registerCommand(sidepanel) + actions.registerCommand(watchRecordingsOfCommand(push)) }, beforeUnmount: () => { actions.deregisterCommand('go-to') @@ -945,6 +947,7 @@ export const commandPaletteLogic = kea([ actions.deregisterCommand('toggle-hedgehog-mode') actions.deregisterCommand('shortcuts') actions.deregisterCommand('sidepanel') + actions.deregisterCommand(WATCH_RECORDINGS_OF_KEY) }, })), ]) diff --git a/frontend/src/scenes/session-recordings/replayPaletteCommands.ts b/frontend/src/scenes/session-recordings/replayPaletteCommands.ts new file mode 100644 index 0000000000000..ff988498b2e95 --- /dev/null +++ b/frontend/src/scenes/session-recordings/replayPaletteCommands.ts @@ -0,0 +1,100 @@ +import { IconRewindPlay } from '@posthog/icons' +import { Command, GLOBAL_COMMAND_SCOPE } from 'lib/components/CommandPalette/commandPaletteLogic' +import { isURL } from 'lib/utils' +import { urls } from 'scenes/urls' + +import { + FilterLogicalOperator, + PropertyFilterType, + PropertyOperator, + RecordingDurationFilter, + RecordingUniversalFilters, + ReplayTabs, +} from '~/types' + +function isUUIDLike(candidate: string): boolean { + return candidate.length === 36 && !!candidate.toLowerCase().match(/^[0-9a-f-]+$/)?.length +} + +export const WATCH_RECORDINGS_OF_KEY = 'watch-recordings-of' +export const watchRecordingsOfCommand = ( + push: ( + url: string, + searchInput?: string | Record | undefined, + hashInput?: string | Record | undefined + ) => void +): Command => ({ + key: WATCH_RECORDINGS_OF_KEY, + scope: GLOBAL_COMMAND_SCOPE, + resolver: (argument: string | undefined) => { + if (argument === undefined) { + return null + } + + const replayFilter = ( + key: 'snapshot_source' | 'visited_page', + value: string + ): Partial => ({ + date_from: '-3d', + filter_group: { + type: FilterLogicalOperator.And, + values: [ + { + type: FilterLogicalOperator.And, + values: [ + { + key: key, + value: [value], + operator: PropertyOperator.Exact, + type: PropertyFilterType.Recording, + }, + ], + }, + ], + }, + duration: [ + { + type: PropertyFilterType.Recording, + key: 'duration', + value: 1, + operator: 'gt', + } as RecordingDurationFilter, + ], + }) + + const words = argument.split(' ') + if (words.includes('web') || words.includes('mobile')) { + return { + icon: IconRewindPlay, + display: `Watch ${argument} recordings`, + executor: () => { + push(urls.replay(ReplayTabs.Home, replayFilter('snapshot_source', argument))) + }, + } + } + + const url = words.find((word) => isURL(word)) + if (url) { + return { + icon: IconRewindPlay, + display: `Watch recordings of visits to ${url}`, + executor: () => { + push(urls.replay(ReplayTabs.Home, replayFilter('visited_page', url))) + }, + } + } + + const uuid = words.find((word) => isUUIDLike(word)) + if (uuid) { + return { + icon: IconRewindPlay, + display: `Watch recording of session: ${uuid}`, + executor: () => { + push(urls.replaySingle(uuid)) + }, + } + } + + return null + }, +})