Skip to content

Commit e944daa

Browse files
authored
feat(replay): Add experimental option to allow for a checkout every 6 minutes (#13069)
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).
1 parent 8fcee67 commit e944daa

File tree

4 files changed

+64
-6
lines changed

4 files changed

+64
-6
lines changed

packages/replay-internal/src/replay.ts

+13-1
Original file line numberDiff line numberDiff line change
@@ -377,7 +377,19 @@ export class ReplayContainer implements ReplayContainerInterface {
377377
// When running in error sampling mode, we need to overwrite `checkoutEveryNms`
378378
// Without this, it would record forever, until an error happens, which we don't want
379379
// instead, we'll always keep the last 60 seconds of replay before an error happened
380-
...(this.recordingMode === 'buffer' && { checkoutEveryNms: BUFFER_CHECKOUT_TIME }),
380+
...(this.recordingMode === 'buffer'
381+
? { checkoutEveryNms: BUFFER_CHECKOUT_TIME }
382+
: // Otherwise, use experimental option w/ min checkout time of 6 minutes
383+
// This is to improve playback seeking as there could potentially be
384+
// less mutations to process in the worse cases.
385+
//
386+
// checkout by "N" events is probably ideal, but means we have less
387+
// control about the number of checkouts we make (which generally
388+
// increases replay size)
389+
this._options._experiments.continuousCheckout && {
390+
// Minimum checkout time is 6 minutes
391+
checkoutEveryNms: Math.max(360_000, this._options._experiments.continuousCheckout),
392+
}),
381393
emit: getHandleRecordingEmit(this),
382394
onMutation: this._onMutationHandler,
383395
...(canvasOptions

packages/replay-internal/src/types/replay.ts

+1
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,7 @@ export interface ReplayPluginOptions extends ReplayNetworkOptions {
232232
_experiments: Partial<{
233233
captureExceptions: boolean;
234234
traceInternals: boolean;
235+
continuousCheckout: number;
235236
}>;
236237
}
237238

packages/replay-internal/src/util/handleRecordingEmit.ts

+10-5
Original file line numberDiff line numberDiff line change
@@ -58,9 +58,14 @@ export function getHandleRecordingEmit(replay: ReplayContainer): RecordingEmitCa
5858
return false;
5959
}
6060

61+
const session = replay.session;
62+
6163
// Additionally, create a meta event that will capture certain SDK settings.
6264
// In order to handle buffer mode, this needs to either be done when we
63-
// receive checkout events or at flush time.
65+
// receive checkout events or at flush time. We have an experimental mode
66+
// to perform multiple checkouts a session (the idea is to improve
67+
// seeking during playback), so also only include if segmentId is 0
68+
// (handled in `addSettingsEvent`).
6469
//
6570
// `isCheckout` is always true, but want to be explicit that it should
6671
// only be added for checkouts
@@ -72,22 +77,22 @@ export function getHandleRecordingEmit(replay: ReplayContainer): RecordingEmitCa
7277
// of the previous session. Do not immediately flush in this case
7378
// to avoid capturing only the checkout and instead the replay will
7479
// be captured if they perform any follow-up actions.
75-
if (replay.session && replay.session.previousSessionId) {
80+
if (session && session.previousSessionId) {
7681
return true;
7782
}
7883

7984
// When in buffer mode, make sure we adjust the session started date to the current earliest event of the buffer
8085
// this should usually be the timestamp of the checkout event, but to be safe...
81-
if (replay.recordingMode === 'buffer' && replay.session && replay.eventBuffer) {
86+
if (replay.recordingMode === 'buffer' && session && replay.eventBuffer) {
8287
const earliestEvent = replay.eventBuffer.getEarliestTimestamp();
8388
if (earliestEvent) {
8489
DEBUG_BUILD &&
8590
logger.info(`Updating session start time to earliest event in buffer to ${new Date(earliestEvent)}`);
8691

87-
replay.session.started = earliestEvent;
92+
session.started = earliestEvent;
8893

8994
if (replay.getOptions().stickySession) {
90-
saveSession(replay.session);
95+
saveSession(session);
9196
}
9297
}
9398
}

packages/replay-internal/test/integration/rrweb.test.ts

+40
Original file line numberDiff line numberDiff line change
@@ -46,4 +46,44 @@ describe('Integration | rrweb', () => {
4646
}
4747
`);
4848
});
49+
50+
it('calls rrweb.record with checkoutEveryNms', async () => {
51+
const { mockRecord } = await resetSdkMock({
52+
replayOptions: {
53+
_experiments: {
54+
continuousCheckout: 1,
55+
},
56+
},
57+
sentryOptions: {
58+
replaysOnErrorSampleRate: 0.0,
59+
replaysSessionSampleRate: 1.0,
60+
},
61+
});
62+
63+
expect(mockRecord.mock.calls[0]?.[0]).toMatchInlineSnapshot(`
64+
{
65+
"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"]",
66+
"checkoutEveryNms": 360000,
67+
"collectFonts": true,
68+
"emit": [Function],
69+
"errorHandler": [Function],
70+
"ignoreSelector": ".sentry-ignore,[data-sentry-ignore],input[type="file"]",
71+
"inlineImages": false,
72+
"inlineStylesheet": true,
73+
"maskAllInputs": true,
74+
"maskAllText": true,
75+
"maskAttributeFn": [Function],
76+
"maskInputFn": undefined,
77+
"maskInputOptions": {
78+
"password": true,
79+
},
80+
"maskTextFn": undefined,
81+
"maskTextSelector": ".sentry-mask,[data-sentry-mask]",
82+
"onMutation": [Function],
83+
"slimDOMOptions": "all",
84+
"unblockSelector": "",
85+
"unmaskTextSelector": "",
86+
}
87+
`);
88+
});
4989
});

0 commit comments

Comments
 (0)