diff --git a/frontend/src/scenes/session-recordings/player/__snapshots__/sessionRecordingPlayerLogic.test.ts.snap b/frontend/src/scenes/session-recordings/player/__snapshots__/sessionRecordingPlayerLogic.test.ts.snap index 8feae316bda5b..4dd12a28be780 100644 --- a/frontend/src/scenes/session-recordings/player/__snapshots__/sessionRecordingPlayerLogic.test.ts.snap +++ b/frontend/src/scenes/session-recordings/player/__snapshots__/sessionRecordingPlayerLogic.test.ts.snap @@ -62,11 +62,11 @@ exports[`sessionRecordingPlayerLogic loading session core loads metadata and sna "windowId": "187d7c761a0525d-05f175487d4b65-1d525634-384000-187d7c761a149d0", }, { - "durationMs": 525, - "endTimestamp": 1682952388658, + "durationMs": 527, + "endTimestamp": 1682952388659, "isActive": false, "kind": "gap", - "startTimestamp": 1682952388133, + "startTimestamp": 1682952388132, "windowId": "187d7c77dfe1d45-08bdcaf91135a2-1d525634-384000-187d7c77dff39a6", }, { diff --git a/frontend/src/scenes/session-recordings/player/controller/Seekbar.tsx b/frontend/src/scenes/session-recordings/player/controller/Seekbar.tsx index c46cba8f72abc..c2ed3c6ef3bc1 100644 --- a/frontend/src/scenes/session-recordings/player/controller/Seekbar.tsx +++ b/frontend/src/scenes/session-recordings/player/controller/Seekbar.tsx @@ -49,7 +49,7 @@ export function Seekbar(): JSX.Element {
{sessionPlayerData.segments?.map((segment: RecordingSegment) => (
( plugins.push(CorsPlugin) } + cache.debug?.('tryInitReplayer', { + windowId, + rootFrame: values.rootFrame, + snapshots: values.sessionPlayerData.snapshotsByWindowId[windowId], + }) + const replayer = new Replayer(values.sessionPlayerData.snapshotsByWindowId[windowId], { root: values.rootFrame, ...COMMON_REPLAYER_CONFIG, @@ -652,6 +658,11 @@ export const sessionRecordingPlayerLogic = kea( actions.pauseIframePlayback() actions.syncPlayerSpeed() // hotfix: speed changes on player state change values.player?.replayer?.pause() + + cache.debug?.('pause', { + currentTimestamp: values.currentTimestamp, + currentSegment: values.currentSegment, + }) }, setEndReached: ({ reached }) => { if (reached) { @@ -776,6 +787,7 @@ export const sessionRecordingPlayerLogic = kea( values.currentPlayerState === SessionPlayerState.SKIP) && values.timestampChangeTracking.timestampMatchesPrevious > 10 ) { + cache.debug?.('stuck session player detected', values.timestampChangeTracking) actions.skipPlayerForward(rrwebPlayerTime, skip) newTimestamp = newTimestamp + skip } @@ -783,7 +795,9 @@ export const sessionRecordingPlayerLogic = kea( if (newTimestamp == undefined && values.currentTimestamp) { // This can happen if the player is not loaded due to us being in a "gap" segment // In this case, we should progress time forward manually + if (values.currentSegment?.kind === 'gap') { + cache.debug?.('gap segment: skipping forward') newTimestamp = values.currentTimestamp + skip } } @@ -796,6 +810,12 @@ export const sessionRecordingPlayerLogic = kea( actions.setCurrentTimestamp(Math.max(newTimestamp, nextSegment.startTimestamp)) actions.setCurrentSegment(nextSegment) } else { + cache.debug('end of recording reached', { + newTimestamp, + segments: values.sessionPlayerData.segments, + currentSegment: values.currentSegment, + segmentIndex: values.sessionPlayerData.segments.indexOf(values.currentSegment), + }) // At the end of the recording. Pause the player and set fully to the end actions.setEndReached() } @@ -809,6 +829,7 @@ export const sessionRecordingPlayerLogic = kea( values.player?.replayer?.pause() actions.startBuffer() actions.setErrorPlayerState(false) + cache.debug('buffering') return } @@ -946,6 +967,8 @@ export const sessionRecordingPlayerLogic = kea( return } + delete (window as any).__debug_player + actions.stopAnimation() cache.resetConsoleWarn?.() cache.hasInitialized = false @@ -975,7 +998,20 @@ export const sessionRecordingPlayerLogic = kea( ) }), - afterMount(({ props, actions, cache }) => { + afterMount(({ props, actions, cache, values }) => { + cache.debugging = localStorage.getItem('ph_debug_player') === 'true' + cache.debug = (...args: any[]) => { + if (cache.debugging) { + // eslint-disable-next-line no-console + console.log('[⏯️ PostHog Replayer]', ...args) + } + } + ;(window as any).__debug_player = () => { + cache.debugging = !cache.debugging + localStorage.setItem('ph_debug_player', JSON.stringify(cache.debugging)) + cache.debug('player data', values.sessionPlayerData) + } + if (props.mode === SessionRecordingPlayerMode.Preview) { return } 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 904cce8e134b2..321fb07545db6 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,63 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`segmenter includes inactive events in the active segment until a threshold 1`] = ` +[ + { + "durationMs": 4000, + "endTimestamp": 1672531204000, + "isActive": true, + "kind": "window", + "startTimestamp": 1672531200000, + "windowId": "A", + }, + { + "durationMs": 2000, + "endTimestamp": 1672531206000, + "isActive": false, + "kind": "gap", + "startTimestamp": 1672531204000, + "windowId": "A", + }, + { + "durationMs": 594000, + "endTimestamp": 1672531800000, + "isActive": false, + "kind": "window", + "startTimestamp": 1672531206000, + "windowId": "A", + }, +] +`; + +exports[`segmenter inserts gaps inclusively 1`] = ` +[ + { + "durationMs": 100, + "endTimestamp": 1672531200100, + "isActive": true, + "kind": "window", + "startTimestamp": 1672531200000, + "windowId": "A", + }, + { + "durationMs": 599800, + "endTimestamp": 1672531799900, + "isActive": false, + "kind": "gap", + "startTimestamp": 1672531200100, + "windowId": undefined, + }, + { + "durationMs": 100, + "endTimestamp": 1672531800000, + "isActive": false, + "kind": "window", + "startTimestamp": 1672531799900, + "windowId": "B", + }, +] +`; + exports[`segmenter matches snapshots 1`] = ` [ { @@ -11,11 +69,11 @@ exports[`segmenter matches snapshots 1`] = ` "windowId": "187d7c761a0525d-05f175487d4b65-1d525634-384000-187d7c761a149d0", }, { - "durationMs": 525, - "endTimestamp": 1682952388658, + "durationMs": 527, + "endTimestamp": 1682952388659, "isActive": false, "kind": "gap", - "startTimestamp": 1682952388133, + "startTimestamp": 1682952388132, "windowId": "187d7c77dfe1d45-08bdcaf91135a2-1d525634-384000-187d7c77dff39a6", }, { 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 60842a44bb268..435a97a699015 100644 --- a/frontend/src/scenes/session-recordings/player/utils/segmenter.test.ts +++ b/frontend/src/scenes/session-recordings/player/utils/segmenter.test.ts @@ -31,7 +31,9 @@ describe('segmenter', () => { ]) }) - it('inserts gaps', () => { + it('inserts gaps inclusively', () => { + // 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 = dayjs('2023-01-01T00:10:00.000Z') @@ -44,32 +46,7 @@ describe('segmenter', () => { const segments = createSegments(snapshots, start, end) - expect(segments).toEqual([ - { - kind: 'window', - startTimestamp: 1672531200000, - windowId: 'A', - isActive: true, - endTimestamp: 1672531200100, - durationMs: 100, - }, - { - durationMs: 599798, - endTimestamp: 1672531799899, - isActive: false, - kind: 'gap', - startTimestamp: 1672531200101, - windowId: undefined, - }, - { - kind: 'window', - startTimestamp: 1672531799900, - windowId: 'B', - isActive: false, - endTimestamp: 1672531800000, - durationMs: 100, - }, - ]) + expect(segments).toMatchSnapshot() }) it('includes inactive events in the active segment until a threshold', () => { @@ -86,31 +63,6 @@ describe('segmenter', () => { const segments = createSegments(snapshots, start, end) - expect(segments).toEqual([ - { - kind: 'window', - startTimestamp: start.valueOf(), - windowId: 'A', - isActive: true, - endTimestamp: start.valueOf() + 4000, - durationMs: 4000, - }, - { - kind: 'gap', - startTimestamp: start.valueOf() + 4000 + 1, - endTimestamp: start.valueOf() + 6000 - 1, - windowId: 'A', - isActive: false, - durationMs: 1998, - }, - { - kind: 'window', - startTimestamp: start.valueOf() + 6000, - windowId: 'A', - isActive: false, - endTimestamp: end.valueOf(), - durationMs: 594000, - }, - ]) + 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 39fe407e0fa14..9e284fd6bdb45 100644 --- a/frontend/src/scenes/session-recordings/player/utils/segmenter.ts +++ b/frontend/src/scenes/session-recordings/player/utils/segmenter.ts @@ -116,10 +116,12 @@ export const createSegments = (snapshots: RecordingSnapshot[], start?: Dayjs, en const previousSegment = segments[index - 1] const list = [...acc] - if (previousSegment && segment.startTimestamp - previousSegment.endTimestamp > 1) { - const startTimestamp = previousSegment.endTimestamp + 1 - const endTimestamp = segment.startTimestamp - 1 - const windowId = findWindowIdForTimestamp(startTimestamp, previousSegment.windowId) + if (previousSegment && segment.startTimestamp !== previousSegment.endTimestamp) { + // If the segments do not immediately follow each other then we add a "gap" segment + 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 gapSegment: Partial = { kind: 'gap', startTimestamp,