diff --git a/packages/replay-internal/src/replay.ts b/packages/replay-internal/src/replay.ts index cfeb26841911..06f81b6982c6 100644 --- a/packages/replay-internal/src/replay.ts +++ b/packages/replay-internal/src/replay.ts @@ -377,7 +377,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.max(360_000, this._options._experiments.continuousCheckout), + }), emit: getHandleRecordingEmit(this), onMutation: this._onMutationHandler, ...(canvasOptions diff --git a/packages/replay-internal/src/types/replay.ts b/packages/replay-internal/src/types/replay.ts index 1e510e2bc519..0605ba97449a 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 6b87845d793f..0467edefa9a2 100644 --- a/packages/replay-internal/src/util/handleRecordingEmit.ts +++ b/packages/replay-internal/src/util/handleRecordingEmit.ts @@ -58,9 +58,14 @@ 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 + // (handled in `addSettingsEvent`). // // `isCheckout` is always true, but want to be explicit that it should // only be added for checkouts @@ -72,22 +77,22 @@ 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) { DEBUG_BUILD && logger.info(`Updating session start time to earliest event in buffer to ${new Date(earliestEvent)}`); - replay.session.started = earliestEvent; + session.started = earliestEvent; if (replay.getOptions().stickySession) { - saveSession(replay.session); + saveSession(session); } } } 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": "", + } + `); + }); });