diff --git a/frontend/src/scenes/session-recordings/player/PlayerMeta.tsx b/frontend/src/scenes/session-recordings/player/PlayerMeta.tsx index 28662322f5f7a..6f08a928b1cb9 100644 --- a/frontend/src/scenes/session-recordings/player/PlayerMeta.tsx +++ b/frontend/src/scenes/session-recordings/player/PlayerMeta.tsx @@ -1,8 +1,8 @@ import './PlayerMeta.scss' -import { Link } from '@posthog/lemon-ui' +import { LemonSelect, LemonSelectOption, Link } from '@posthog/lemon-ui' import clsx from 'clsx' -import { useValues } from 'kea' +import { useActions, useValues } from 'kea' import { CopyToClipboardInline } from 'lib/components/CopyToClipboard' import { useResizeBreakpoints } from 'lib/hooks/useResizeObserver' import { LemonSkeleton } from 'lib/lemon-ui/LemonSkeleton' @@ -61,8 +61,18 @@ function URLOrScreen({ lastUrl }: { lastUrl: string | undefined }): JSX.Element export function PlayerMeta(): JSX.Element { const { logicProps, isFullScreen } = useValues(sessionRecordingPlayerLogic) - const { resolution, lastPageviewEvent, lastUrl, scale, currentWindowIndex, sessionPlayerMetaDataLoading } = - useValues(playerMetaLogic(logicProps)) + const { + windowIds, + trackedWindow, + resolution, + lastPageviewEvent, + lastUrl, + scale, + currentWindowIndex, + sessionPlayerMetaDataLoading, + } = useValues(playerMetaLogic(logicProps)) + + const { setTrackedWindow } = useActions(playerMetaLogic(logicProps)) const { ref, size } = useResizeBreakpoints({ 0: 'compact', @@ -120,6 +130,25 @@ export function PlayerMeta(): JSX.Element { ) } + const windowOptions: LemonSelectOption[] = [ + { + label: , + value: null, + labelInMenu: <>Follow the user, + }, + ] + windowIds.forEach((windowId, index) => { + windowOptions.push({ + label: , + labelInMenu: ( +
+ Follow window: +
+ ), + value: windowId, + }) + }) + return (
) : ( <> - - Window {currentWindowIndex + 1}. -
- Each recording window translates to a distinct browser tab or window. - - } - > - - - -
+ setTrackedWindow(value)} + /> {lastPageviewEvent?.properties?.['$screen_name'] && ( diff --git a/frontend/src/scenes/session-recordings/player/playerMetaLogic.ts b/frontend/src/scenes/session-recordings/player/playerMetaLogic.ts index f84612b8c894b..a4f1e2667ae36 100644 --- a/frontend/src/scenes/session-recordings/player/playerMetaLogic.ts +++ b/frontend/src/scenes/session-recordings/player/playerMetaLogic.ts @@ -26,6 +26,7 @@ export const playerMetaLogic = kea([ 'sessionPlayerMetaData', 'sessionPlayerMetaDataLoading', 'windowIds', + 'trackedWindow', ], sessionRecordingPlayerLogic(props), ['scale', 'currentTimestamp', 'currentPlayerTime', 'currentSegment'], @@ -34,7 +35,7 @@ export const playerMetaLogic = kea([ ], actions: [ sessionRecordingDataLogic(props), - ['loadRecordingMetaSuccess'], + ['loadRecordingMetaSuccess', 'setTrackedWindow'], sessionRecordingsListPropertiesLogic, ['maybeLoadPropertiesForSessions'], ], @@ -89,7 +90,7 @@ export const playerMetaLogic = kea([ const index = windowIds.findIndex((windowId) => currentSegment?.windowId ? windowId === currentSegment?.windowId : -1 ) - return index === -1 ? 0 : index + return index === -1 ? 1 : index + 1 }, ], lastUrl: [ diff --git a/frontend/src/scenes/session-recordings/player/sessionRecordingDataLogic.ts b/frontend/src/scenes/session-recordings/player/sessionRecordingDataLogic.ts index abc0b097117ba..5b3439b13e117 100644 --- a/frontend/src/scenes/session-recordings/player/sessionRecordingDataLogic.ts +++ b/frontend/src/scenes/session-recordings/player/sessionRecordingDataLogic.ts @@ -273,8 +273,15 @@ export const sessionRecordingDataLogic = kea([ persistRecording: true, maybePersistRecording: true, pollRealtimeSnapshots: true, + setTrackedWindow: (windowId: string | null) => ({ windowId }), }), reducers(() => ({ + trackedWindow: [ + null as string | null, + { + setTrackedWindow: (_, { windowId }) => windowId, + }, + ], filters: [ {} as Partial, { @@ -741,9 +748,9 @@ export const sessionRecordingDataLogic = kea([ ], segments: [ - (s) => [s.snapshots, s.start, s.end], - (snapshots, start, end): RecordingSegment[] => { - return createSegments(snapshots || [], start, end) + (s) => [s.snapshots, s.start, s.end, s.trackedWindow], + (snapshots, start, end, trackedWindow): RecordingSegment[] => { + return createSegments(snapshots || [], start, end, trackedWindow) }, ], diff --git a/frontend/src/scenes/session-recordings/player/utils/__snapshots__/segmenter.test.ts.snap b/frontend/src/scenes/session-recordings/player/utils/__snapshots__/segmenter.test.ts.snap index 0e07868a1eb50..4ab3c11f932c8 100644 --- a/frontend/src/scenes/session-recordings/player/utils/__snapshots__/segmenter.test.ts.snap +++ b/frontend/src/scenes/session-recordings/player/utils/__snapshots__/segmenter.test.ts.snap @@ -1,5 +1,57 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`segmenter can segment to include only one window 1`] = ` +[ + { + "durationMs": 0, + "endTimestamp": 1672531200010, + "isActive": false, + "kind": "gap", + "startTimestamp": 1672531200010, + "windowId": "C", + }, + { + "durationMs": 90, + "endTimestamp": 1672531200100, + "isActive": false, + "kind": "gap", + "startTimestamp": 1672531200010, + "windowId": undefined, + }, + { + "durationMs": 60, + "endTimestamp": 1672531200160, + "isActive": false, + "kind": "window", + "startTimestamp": 1672531200100, + "windowId": "C", + }, + { + "durationMs": 40, + "endTimestamp": 1672531200200, + "isActive": false, + "kind": "gap", + "startTimestamp": 1672531200160, + "windowId": "B", + }, + { + "durationMs": 0, + "endTimestamp": 1672531200200, + "isActive": false, + "kind": "gap", + "startTimestamp": 1672531200200, + "windowId": "C", + }, + { + "durationMs": 9, + "endTimestamp": 1672531200210, + "isActive": false, + "kind": "buffer", + "startTimestamp": 1672531200201, + }, +] +`; + exports[`segmenter ends a segment if it is the last window 1`] = ` [ { diff --git a/frontend/src/scenes/session-recordings/player/utils/segmenter.test.ts b/frontend/src/scenes/session-recordings/player/utils/segmenter.test.ts index 1b9c17602ad87..d422e7afc1cc5 100644 --- a/frontend/src/scenes/session-recordings/player/utils/segmenter.test.ts +++ b/frontend/src/scenes/session-recordings/player/utils/segmenter.test.ts @@ -85,4 +85,23 @@ describe('segmenter', () => { expect(segments).toMatchSnapshot() }) + + it('can segment to include only one window', () => { + // NOTE: It is important that the segments are "inclusive" of the start and end timestamps as the player logic + // depends on this to choose which segment should be played next + const start = dayjs('2023-01-01T00:00:00.000Z') + const end = start.add(210, 'milliseconds') + + const snapshots: RecordingSnapshot[] = [ + { windowId: 'A', timestamp: start.valueOf() + 10, type: 3, data: {} } as any, + { windowId: 'C', timestamp: start.valueOf() + 100, type: 3, data: {} } as any, + { windowId: 'B', timestamp: start.valueOf() + 120, type: 3, data: {} } as any, + { windowId: 'C', timestamp: start.valueOf() + 160, type: 3, data: {} } as any, + { windowId: 'B', timestamp: start.valueOf() + 200, type: 3, data: {} } as any, + ] + + const segments = createSegments(snapshots, start, end, 'C') + + expect(segments).toMatchSnapshot() + }) }) diff --git a/frontend/src/scenes/session-recordings/player/utils/segmenter.ts b/frontend/src/scenes/session-recordings/player/utils/segmenter.ts index 2549c35965671..a8ffe60efb57e 100644 --- a/frontend/src/scenes/session-recordings/player/utils/segmenter.ts +++ b/frontend/src/scenes/session-recordings/player/utils/segmenter.ts @@ -44,7 +44,12 @@ export const mapSnapshotsToWindowId = (snapshots: RecordingSnapshot[]): Record { +export const createSegments = ( + snapshots: RecordingSnapshot[], + start?: Dayjs, + end?: Dayjs, + trackedWindow?: string | null +): RecordingSegment[] => { let segments: RecordingSegment[] = [] let activeSegment!: Partial let lastActiveEventTimestamp = 0 @@ -105,7 +110,9 @@ export const createSegments = (snapshots: RecordingSnapshot[], start?: Dayjs, en } // We've built the segments, but this might not account for "gaps" in them - // To account for this we build up a new segment list filling in gaps with the whatever window is available (preferably the previous one) + // To account for this we build up a new segment list filling in gaps with + // either the tracked window if the viewing window is fixed + // or whatever window is available (preferably the previous one) // Or a "null" window if there is nothing (like if they navigated away to a different site) const findWindowIdForTimestamp = (timestamp: number, preferredWindowId?: string): string | undefined => { @@ -125,6 +132,21 @@ export const createSegments = (snapshots: RecordingSnapshot[], start?: Dayjs, en } } + if (trackedWindow) { + segments = segments.map((segment) => { + if (segment.windowId === trackedWindow) { + return segment + } + // every window segment that isn't the tracked window is a gap + return { + ...segment, + windowId: trackedWindow, + isActive: false, + kind: 'gap', + } + }) + } + segments = segments.reduce((acc, segment, index) => { const previousSegment = segments[index - 1] const list = [...acc] @@ -134,7 +156,7 @@ export const createSegments = (snapshots: RecordingSnapshot[], start?: Dayjs, en const startTimestamp = previousSegment.endTimestamp const endTimestamp = segment.startTimestamp // Offset the window ID check so we look for a subsequent segment - const windowId = findWindowIdForTimestamp(startTimestamp + 1, previousSegment.windowId) + const windowId = findWindowIdForTimestamp(startTimestamp + 1, trackedWindow || previousSegment.windowId) const gapSegment: Partial = { kind: 'gap', startTimestamp,