Skip to content

Commit 5ba721a

Browse files
authored
feat(replay): Flush immediately on DOM checkouts (#6463)
We originally added this delay to flushing to prevent recording short (low value) recordings. However, this causes some issues if the user were to refresh/leave the page before the first recording segment is sent as the backend will consider the replay invalid if it does not have segment id 0 present. Change to flush immediately to try to reduce the number of replays with missing first segments. Our UI has a default duration filter to hide replays < 5 seconds.
1 parent 047d107 commit 5ba721a

File tree

2 files changed

+34
-47
lines changed

2 files changed

+34
-47
lines changed

packages/replay/src/replay.ts

Lines changed: 10 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -195,9 +195,10 @@ export class ReplayContainer implements ReplayContainerInterface {
195195

196196
this.addListeners();
197197

198-
this.startRecording();
199-
198+
// Need to set as enabled before we start recording, as `record()` can trigger a flush with a new checkout
200199
this._isEnabled = true;
200+
201+
this.startRecording();
201202
}
202203

203204
/**
@@ -485,21 +486,13 @@ export class ReplayContainer implements ReplayContainerInterface {
485486
this._maybeSaveSession();
486487
}
487488

488-
// If the full snapshot is due to an initial load, we will not have
489-
// a previous session ID. In this case, we want to buffer events
490-
// for a set amount of time before flushing. This can help avoid
491-
// capturing replays of users that immediately close the window.
492-
setTimeout(() => this.conditionalFlush(), this._options.initialFlushDelay);
493-
494-
// Cancel any previously debounced flushes to ensure there are no [near]
495-
// simultaneous flushes happening. The latter request should be
496-
// insignificant in this case, so wait for additional user interaction to
497-
// trigger a new flush.
498-
//
499-
// This can happen because there's no guarantee that a recording event
500-
// happens first. e.g. a mouse click can happen and trigger a debounced
501-
// flush before the checkout.
502-
this._debouncedFlush?.cancel();
489+
// Flush immediately so that we do not miss the first segment, otherwise
490+
// it can prevent loading on the UI. This will cause an increase in short
491+
// replays (e.g. opening and closing a tab quickly), but these can be
492+
// filtered on the UI.
493+
if (this.recordingMode === 'session') {
494+
void this.flushImmediate();
495+
}
503496

504497
return true;
505498
});

packages/replay/test/unit/index-errorSampleRate.test.ts

Lines changed: 24 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { captureException, getCurrentHub } from '@sentry/core';
1+
import { captureException } from '@sentry/core';
22

33
import { REPLAY_SESSION_KEY, VISIBILITY_CHANGE_TIMEOUT, WINDOW } from '../../src/constants';
44
import { addEvent } from '../../src/util/addEvent';
@@ -19,7 +19,6 @@ async function advanceTimers(time: number) {
1919
describe('Replay (errorSampleRate)', () => {
2020
let replay: ReplayContainer;
2121
let mockRecord: RecordMock;
22-
2322
let domHandler: DomHandler;
2423

2524
beforeEach(async () => {
@@ -59,6 +58,7 @@ describe('Replay (errorSampleRate)', () => {
5958
await new Promise(process.nextTick);
6059

6160
expect(replay).toHaveSentReplay({
61+
recordingPayloadHeader: { segment_id: 0 },
6262
replayEventPayload: expect.objectContaining({
6363
tags: expect.objectContaining({
6464
errorSampleRate: 1,
@@ -86,6 +86,19 @@ describe('Replay (errorSampleRate)', () => {
8686
]),
8787
});
8888

89+
// This is from when we stop recording and start a session recording
90+
expect(replay).toHaveLastSentReplay({
91+
recordingPayloadHeader: { segment_id: 1 },
92+
replayEventPayload: expect.objectContaining({
93+
tags: expect.objectContaining({
94+
errorSampleRate: 1,
95+
replayType: 'error',
96+
sessionSampleRate: 0,
97+
}),
98+
}),
99+
events: JSON.stringify([{ data: { isCheckout: true }, timestamp: BASE_TIMESTAMP + 5020, type: 2 }]),
100+
});
101+
89102
jest.runAllTimers();
90103
await new Promise(process.nextTick);
91104

@@ -111,11 +124,11 @@ describe('Replay (errorSampleRate)', () => {
111124
events: JSON.stringify([
112125
{
113126
type: 5,
114-
timestamp: BASE_TIMESTAMP + 15000 + 40,
127+
timestamp: BASE_TIMESTAMP + 10000 + 40,
115128
data: {
116129
tag: 'breadcrumb',
117130
payload: {
118-
timestamp: (BASE_TIMESTAMP + 15000 + 40) / 1000,
131+
timestamp: (BASE_TIMESTAMP + 10000 + 40) / 1000,
119132
type: 'default',
120133
category: 'ui.click',
121134
message: '<unknown>',
@@ -287,7 +300,7 @@ describe('Replay (errorSampleRate)', () => {
287300
jest.runAllTimers();
288301
await new Promise(process.nextTick);
289302

290-
expect(replay).toHaveLastSentReplay({
303+
expect(replay).toHaveSentReplay({
291304
events: JSON.stringify([{ data: { isCheckout: true }, timestamp: BASE_TIMESTAMP, type: 2 }, TEST_EVENT]),
292305
replayEventPayload: expect.objectContaining({
293306
replay_start_timestamp: BASE_TIMESTAMP / 1000,
@@ -338,7 +351,8 @@ describe('Replay (errorSampleRate)', () => {
338351
expect(replay.session?.started).toBe(BASE_TIMESTAMP + ELAPSED + 20);
339352

340353
// Does not capture mouse click
341-
expect(replay).toHaveLastSentReplay({
354+
expect(replay).toHaveSentReplay({
355+
recordingPayloadHeader: { segment_id: 0 },
342356
replayEventPayload: expect.objectContaining({
343357
// Make sure the old performance event is thrown out
344358
replay_start_timestamp: (BASE_TIMESTAMP + ELAPSED + 20) / 1000,
@@ -373,12 +387,6 @@ it('sends a replay after loading the session multiple times', async () => {
373387
},
374388
autoStart: false,
375389
});
376-
377-
const fn = getCurrentHub()?.getClient()?.getTransport()?.send;
378-
const mockTransportSend = fn
379-
? (jest.spyOn(getCurrentHub().getClient()!.getTransport()!, 'send') as jest.MockedFunction<any>)
380-
: jest.fn();
381-
382390
replay.start();
383391

384392
jest.runAllTimers();
@@ -393,27 +401,13 @@ it('sends a replay after loading the session multiple times', async () => {
393401
jest.runAllTimers();
394402
await new Promise(process.nextTick);
395403

396-
expect(replay).toHaveLastSentReplay({
404+
expect(replay).toHaveSentReplay({
397405
events: JSON.stringify([{ data: { isCheckout: true }, timestamp: BASE_TIMESTAMP, type: 2 }, TEST_EVENT]),
398406
});
399407

400-
mockTransportSend.mockClear();
401-
expect(replay).not.toHaveLastSentReplay();
402-
403-
jest.runAllTimers();
404-
await new Promise(process.nextTick);
405-
jest.runAllTimers();
406-
await new Promise(process.nextTick);
407-
408-
// New checkout when we call `startRecording` again after uploading segment
409-
// after an error occurs
408+
// Latest checkout when we call `startRecording` again after uploading segment
409+
// after an error occurs (e.g. when we switch to session replay recording)
410410
expect(replay).toHaveLastSentReplay({
411-
events: JSON.stringify([
412-
{
413-
data: { isCheckout: true },
414-
timestamp: BASE_TIMESTAMP + 10000 + 20,
415-
type: 2,
416-
},
417-
]),
411+
events: JSON.stringify([{ data: { isCheckout: true }, timestamp: BASE_TIMESTAMP + 5020, type: 2 }]),
418412
});
419413
});

0 commit comments

Comments
 (0)