From eb8ed5f33f5796f209f44a3f0b1a4a13a0e48445 Mon Sep 17 00:00:00 2001 From: Billy Vong Date: Fri, 26 Jul 2024 12:13:30 -0400 Subject: [PATCH 1/3] feat(replay): Add experimental option to allow for a checkout every 6 minutes Including more checkouts will improve replayer scrubbing since it will reduce the number of mutations that need to be processed (especially for longer replays). The downside is that it will increase the size of replays since we will have up to 9 more snapshots per replay (max replay duration is 60 minutes / 6 minute checkouts). --- packages/replay-internal/src/replay.ts | 16 +++++++++++++++- packages/replay-internal/src/types/replay.ts | 1 + .../src/util/handleRecordingEmit.ts | 18 ++++++++++++------ 3 files changed, 28 insertions(+), 7 deletions(-) diff --git a/packages/replay-internal/src/replay.ts b/packages/replay-internal/src/replay.ts index a0ef13276e1a..22e2dcd927aa 100644 --- a/packages/replay-internal/src/replay.ts +++ b/packages/replay-internal/src/replay.ts @@ -376,7 +376,19 @@ export class ReplayContainer implements ReplayContainerInterface { // When running in error sampling mode, we need to overwrite `checkoutEveryNms` // Without this, it would record forever, until an error happens, which we don't want // instead, we'll always keep the last 60 seconds of replay before an error happened - ...(this.recordingMode === 'buffer' && { checkoutEveryNms: BUFFER_CHECKOUT_TIME }), + ...(this.recordingMode === 'buffer' + ? { checkoutEveryNms: BUFFER_CHECKOUT_TIME } + : // Otherwise, use experimental option w/ min checkout time of 6 minutes + // This is to improve playback seeking as there could potentially be + // less mutations to process in the worse cases. + // + // checkout by "N" events is probably ideal, but means we have less + // control about the number of checkouts we make (which generally + // increases replay size) + this._options._experiments.continuousCheckout && { + // Minimum checkout time is 6 minutes + checkoutEveryNms: Math.min(360_000, this._options._experiments.continuousCheckout), + }), emit: getHandleRecordingEmit(this), onMutation: this._onMutationHandler, ...(canvasOptions @@ -388,6 +400,8 @@ export class ReplayContainer implements ReplayContainerInterface { } : {}), }); + + console.log('continuousCheckout', this._options._experiments); } catch (err) { this._handleException(err); } diff --git a/packages/replay-internal/src/types/replay.ts b/packages/replay-internal/src/types/replay.ts index 7ebacad9e100..521890fa590b 100644 --- a/packages/replay-internal/src/types/replay.ts +++ b/packages/replay-internal/src/types/replay.ts @@ -232,6 +232,7 @@ export interface ReplayPluginOptions extends ReplayNetworkOptions { _experiments: Partial<{ captureExceptions: boolean; traceInternals: boolean; + continuousCheckout: number; }>; } diff --git a/packages/replay-internal/src/util/handleRecordingEmit.ts b/packages/replay-internal/src/util/handleRecordingEmit.ts index eaec29be261a..8de4f972fa1e 100644 --- a/packages/replay-internal/src/util/handleRecordingEmit.ts +++ b/packages/replay-internal/src/util/handleRecordingEmit.ts @@ -59,13 +59,19 @@ export function getHandleRecordingEmit(replay: ReplayContainer): RecordingEmitCa return false; } + const session = replay.session; + // Additionally, create a meta event that will capture certain SDK settings. // In order to handle buffer mode, this needs to either be done when we - // receive checkout events or at flush time. + // receive checkout events or at flush time. We have an experimental mode + // to perform multiple checkouts a session (the idea is to improve + // seeking during playback), so also only include if segmentId is 0. // // `isCheckout` is always true, but want to be explicit that it should // only be added for checkouts - addSettingsEvent(replay, isCheckout); + if (session && session.segmentId === 0) { + addSettingsEvent(replay, isCheckout); + } // If there is a previousSessionId after a full snapshot occurs, then // the replay session was started due to session expiration. The new session @@ -73,13 +79,13 @@ export function getHandleRecordingEmit(replay: ReplayContainer): RecordingEmitCa // of the previous session. Do not immediately flush in this case // to avoid capturing only the checkout and instead the replay will // be captured if they perform any follow-up actions. - if (replay.session && replay.session.previousSessionId) { + if (session && session.previousSessionId) { return true; } // When in buffer mode, make sure we adjust the session started date to the current earliest event of the buffer // this should usually be the timestamp of the checkout event, but to be safe... - if (replay.recordingMode === 'buffer' && replay.session && replay.eventBuffer) { + if (replay.recordingMode === 'buffer' && session && replay.eventBuffer) { const earliestEvent = replay.eventBuffer.getEarliestTimestamp(); if (earliestEvent) { logInfo( @@ -87,10 +93,10 @@ export function getHandleRecordingEmit(replay: ReplayContainer): RecordingEmitCa replay.getOptions()._experiments.traceInternals, ); - replay.session.started = earliestEvent; + session.started = earliestEvent; if (replay.getOptions().stickySession) { - saveSession(replay.session); + saveSession(session); } } } From e791ff4c17c659a0e6cdf5c4864e8ad4d2ecbe8c Mon Sep 17 00:00:00 2001 From: Billy Vong Date: Fri, 26 Jul 2024 12:26:42 -0400 Subject: [PATCH 2/3] add test --- packages/replay-internal/src/replay.ts | 4 +- .../test/integration/rrweb.test.ts | 40 +++++++++++++++++++ 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/packages/replay-internal/src/replay.ts b/packages/replay-internal/src/replay.ts index 22e2dcd927aa..d746ae41b026 100644 --- a/packages/replay-internal/src/replay.ts +++ b/packages/replay-internal/src/replay.ts @@ -387,7 +387,7 @@ export class ReplayContainer implements ReplayContainerInterface { // increases replay size) this._options._experiments.continuousCheckout && { // Minimum checkout time is 6 minutes - checkoutEveryNms: Math.min(360_000, this._options._experiments.continuousCheckout), + checkoutEveryNms: Math.max(360_000, this._options._experiments.continuousCheckout), }), emit: getHandleRecordingEmit(this), onMutation: this._onMutationHandler, @@ -400,8 +400,6 @@ export class ReplayContainer implements ReplayContainerInterface { } : {}), }); - - console.log('continuousCheckout', this._options._experiments); } catch (err) { this._handleException(err); } diff --git a/packages/replay-internal/test/integration/rrweb.test.ts b/packages/replay-internal/test/integration/rrweb.test.ts index 863baab45bce..4327ddb21de1 100644 --- a/packages/replay-internal/test/integration/rrweb.test.ts +++ b/packages/replay-internal/test/integration/rrweb.test.ts @@ -46,4 +46,44 @@ describe('Integration | rrweb', () => { } `); }); + + it('calls rrweb.record with checkoutEveryNms', async () => { + const { mockRecord } = await resetSdkMock({ + replayOptions: { + _experiments: { + continuousCheckout: 1, + }, + }, + sentryOptions: { + replaysOnErrorSampleRate: 0.0, + replaysSessionSampleRate: 1.0, + }, + }); + + expect(mockRecord.mock.calls[0]?.[0]).toMatchInlineSnapshot(` + { + "blockSelector": ".sentry-block,[data-sentry-block],base[href="/"],img,image,svg,video,object,picture,embed,map,audio,link[rel="icon"],link[rel="apple-touch-icon"]", + "checkoutEveryNms": 360000, + "collectFonts": true, + "emit": [Function], + "errorHandler": [Function], + "ignoreSelector": ".sentry-ignore,[data-sentry-ignore],input[type="file"]", + "inlineImages": false, + "inlineStylesheet": true, + "maskAllInputs": true, + "maskAllText": true, + "maskAttributeFn": [Function], + "maskInputFn": undefined, + "maskInputOptions": { + "password": true, + }, + "maskTextFn": undefined, + "maskTextSelector": ".sentry-mask,[data-sentry-mask]", + "onMutation": [Function], + "slimDOMOptions": "all", + "unblockSelector": "", + "unmaskTextSelector": "", + } + `); + }); }); From 9014dbc7eb050996dc6cb99a39fd1b8ec6d92f69 Mon Sep 17 00:00:00 2001 From: Billy Vong Date: Mon, 29 Jul 2024 15:52:12 -0400 Subject: [PATCH 3/3] remove dupe segment 0 check --- packages/replay-internal/src/util/handleRecordingEmit.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/replay-internal/src/util/handleRecordingEmit.ts b/packages/replay-internal/src/util/handleRecordingEmit.ts index 8de4f972fa1e..6f71c3e79b96 100644 --- a/packages/replay-internal/src/util/handleRecordingEmit.ts +++ b/packages/replay-internal/src/util/handleRecordingEmit.ts @@ -65,13 +65,12 @@ export function getHandleRecordingEmit(replay: ReplayContainer): RecordingEmitCa // In order to handle buffer mode, this needs to either be done when we // receive checkout events or at flush time. We have an experimental mode // to perform multiple checkouts a session (the idea is to improve - // seeking during playback), so also only include if segmentId is 0. + // seeking during playback), so also only include if segmentId is 0 + // (handled in `addSettingsEvent`). // // `isCheckout` is always true, but want to be explicit that it should // only be added for checkouts - if (session && session.segmentId === 0) { - addSettingsEvent(replay, isCheckout); - } + addSettingsEvent(replay, isCheckout); // If there is a previousSessionId after a full snapshot occurs, then // the replay session was started due to session expiration. The new session