diff --git a/src/__tests__/extensions/replay/sessionrecording.test.ts b/src/__tests__/extensions/replay/sessionrecording.test.ts index 7bdf8effd..a0205d962 100644 --- a/src/__tests__/extensions/replay/sessionrecording.test.ts +++ b/src/__tests__/extensions/replay/sessionrecording.test.ts @@ -89,11 +89,60 @@ const createIncrementalSnapshot = (event = {}): incrementalSnapshotEvent => ({ ...event, }) -const createCustomSnapshot = (event = {}): customEvent => ({ +const createIncrementalMouseEvent = () => { + return createIncrementalSnapshot({ + data: { + source: 2, + positions: [ + { + id: 1, + x: 100, + y: 200, + timeOffset: 100, + }, + ], + }, + }) +} + +const createIncrementalMutationEvent = () => { + const mutationData = { + texts: [], + attributes: [], + removes: [], + adds: [], + isAttachIframe: true, + } + return createIncrementalSnapshot({ + data: { + source: 0, + ...mutationData, + }, + }) +} + +const createIncrementalStyleSheetEvent = () => { + return createIncrementalSnapshot({ + data: { + // doesn't need to be a valid style sheet event + source: 8, + id: 1, + styleId: 1, + removes: [], + adds: [], + replace: 'something', + replaceSync: 'something', + }, + }) +} + +const createCustomSnapshot = (event = {}, payload = {}): customEvent => ({ type: EventType.Custom, data: { tag: 'custom', - payload: {}, + payload: { + ...payload, + }, }, ...event, }) @@ -1937,4 +1986,184 @@ describe('SessionRecording', () => { expect((sessionRecording as any)['_tryAddCustomEvent']).not.toHaveBeenCalled() }) }) + + describe('when compression is active', () => { + beforeEach(() => { + posthog.config.session_recording.compress_events = true + sessionRecording.afterDecideResponse(makeDecideResponse({ sessionRecording: { endpoint: '/s/' } })) + sessionRecording.startIfEnabledOrStop() + }) + + it('compresses full snapshot data', () => { + _emit(createFullSnapshot()) + sessionRecording['_flushBuffer']() + + expect(posthog.capture).toHaveBeenCalledWith( + '$snapshot', + { + $snapshot_data: [ + { + data: expect.any(String), + type: 2, + }, + ], + $session_id: sessionId, + $snapshot_bytes: expect.any(Number), + $window_id: 'windowId', + }, + { + _batchKey: 'recordings', + _noTruncate: true, + _url: 'https://test.com/s/', + skip_client_rate_limiting: true, + } + ) + }) + + it('compresses incremental snapshot mutation data', () => { + _emit(createIncrementalMutationEvent()) + sessionRecording['_flushBuffer']() + + expect(posthog.capture).toHaveBeenCalledWith( + '$snapshot', + { + $snapshot_data: [ + { + data: { + adds: expect.any(String), + texts: expect.any(String), + removes: expect.any(String), + attributes: expect.any(String), + isAttachIframe: true, + source: 0, + }, + type: 3, + }, + ], + $session_id: sessionId, + $snapshot_bytes: expect.any(Number), + $window_id: 'windowId', + }, + { + _batchKey: 'recordings', + _noTruncate: true, + _url: 'https://test.com/s/', + skip_client_rate_limiting: true, + } + ) + }) + + it('compresses incremental snapshot style data', () => { + _emit(createIncrementalStyleSheetEvent()) + sessionRecording['_flushBuffer']() + + expect(posthog.capture).toHaveBeenCalledWith( + '$snapshot', + { + $snapshot_data: [ + { + data: { + adds: expect.any(String), + id: 1, + removes: expect.any(String), + replace: 'something', + replaceSync: 'something', + source: 8, + styleId: 1, + }, + type: 3, + }, + ], + $session_id: sessionId, + $snapshot_bytes: expect.any(Number), + $window_id: 'windowId', + }, + { + _batchKey: 'recordings', + _noTruncate: true, + _url: 'https://test.com/s/', + skip_client_rate_limiting: true, + } + ) + }) + + it('does not compress incremental snapshot non full data', () => { + const mouseEvent = createIncrementalMouseEvent() + _emit(mouseEvent) + sessionRecording['_flushBuffer']() + + expect(posthog.capture).toHaveBeenCalledWith( + '$snapshot', + { + $snapshot_data: [mouseEvent], + $session_id: sessionId, + $snapshot_bytes: 86, + $window_id: 'windowId', + }, + { + _batchKey: 'recordings', + _noTruncate: true, + _url: 'https://test.com/s/', + skip_client_rate_limiting: true, + } + ) + }) + + it('does not compress custom events', () => { + _emit(createCustomSnapshot(undefined, { tag: 'wat' })) + sessionRecording['_flushBuffer']() + + expect(posthog.capture).toHaveBeenCalledWith( + '$snapshot', + { + $snapshot_data: [ + { + data: { + payload: { tag: 'wat' }, + tag: 'custom', + }, + type: 5, + }, + ], + $session_id: sessionId, + $snapshot_bytes: 58, + $window_id: 'windowId', + }, + { + _batchKey: 'recordings', + _noTruncate: true, + _url: 'https://test.com/s/', + skip_client_rate_limiting: true, + } + ) + }) + + it('does not compress meta events', () => { + _emit(createMetaSnapshot()) + sessionRecording['_flushBuffer']() + + expect(posthog.capture).toHaveBeenCalledWith( + '$snapshot', + { + $snapshot_data: [ + { + type: META_EVENT_TYPE, + data: { + href: 'https://has-to-be-present-or-invalid.com', + }, + }, + ], + $session_id: sessionId, + $snapshot_bytes: 69, + $window_id: 'windowId', + }, + { + _batchKey: 'recordings', + _noTruncate: true, + _url: 'https://test.com/s/', + skip_client_rate_limiting: true, + } + ) + }) + }) }) diff --git a/src/extensions/replay/sessionrecording.ts b/src/extensions/replay/sessionrecording.ts index d4eca6dac..2c9ef1b10 100644 --- a/src/extensions/replay/sessionrecording.ts +++ b/src/extensions/replay/sessionrecording.ts @@ -105,7 +105,23 @@ type compressedIncrementalSnapshotEvent = { } } -export type compressedEvent = compressedFullSnapshotEvent | compressedIncrementalSnapshotEvent +type compressedIncrementalStyleSnapshotEvent = { + type: EventType.IncrementalSnapshot + data: { + source: IncrementalSource.StyleSheetRule + id?: number + styleId?: number + replace?: string + replaceSync?: string + adds: string + removes: string + } +} + +export type compressedEvent = + | compressedIncrementalStyleSnapshotEvent + | compressedFullSnapshotEvent + | compressedIncrementalSnapshotEvent export type compressedEventWithTime = compressedEvent & { timestamp: number delay?: number @@ -138,6 +154,16 @@ function compressEvent(event: eventWithTime, ph: PostHog): eventWithTime | compr }, } } + if (event.type === EventType.IncrementalSnapshot && event.data.source === IncrementalSource.StyleSheetRule) { + return { + ...event, + data: { + ...event.data, + adds: gzipToString(event.data.adds), + removes: gzipToString(event.data.removes), + }, + } + } } catch (e: unknown) { logger.error(LOGGER_PREFIX + ' could not compress event', e) ph.captureException((e as Error) || 'e was not an error', { @@ -854,11 +880,10 @@ export class SessionRecording { // TODO: Re-add ensureMaxMessageSize once we are confident in it const event = truncateLargeConsoleLogs(throttledEvent) - const size = estimateSize(event) this._updateWindowAndSessionIds(event) - // When in an idle state we keep recording, but don't capture the events + // When in an idle state we keep recording, but don't capture the events, // but we allow custom events even when idle if (this.isIdle && event.type !== EventType.Custom) { return @@ -876,11 +901,13 @@ export class SessionRecording { } } + const eventToSend = this.instance.config.session_recording.compress_events + ? compressEvent(event, this.instance) + : event + const size = estimateSize(eventToSend) const properties = { $snapshot_bytes: size, - $snapshot_data: this.instance.config.session_recording.compress_events - ? compressEvent(event, this.instance) - : event, + $snapshot_data: eventToSend, $session_id: this.sessionId, $window_id: this.windowId, }