Skip to content

Commit ca4d679

Browse files
authored
fix: Do refresh buffered sessions (#7931)
1 parent 79381a7 commit ca4d679

File tree

8 files changed

+117
-35
lines changed

8 files changed

+117
-35
lines changed

packages/browser-integration-tests/suites/replay/bufferMode/test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -245,7 +245,7 @@ sentryTest(
245245

246246
await page.evaluate(async () => {
247247
const replayIntegration = (window as unknown as Window & { Replay: Replay }).Replay;
248-
await replayIntegration.flush({continueRecording: false});
248+
await replayIntegration.flush({ continueRecording: false });
249249
});
250250

251251
const req0 = await reqPromise0;

packages/replay/src/replay.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,12 @@ export class ReplayContainer implements ReplayContainerInterface {
394394
// Reset all "capture on error" configuration before
395395
// starting a new recording
396396
this.recordingMode = 'session';
397+
398+
// Once this session ends, we do not want to refresh it
399+
if (this.session) {
400+
this.session.shouldRefresh = false;
401+
this._maybeSaveSession();
402+
}
397403
this.startRecording();
398404
}
399405
}

packages/replay/src/session/Session.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export function makeSession(session: Partial<Session> & { sampled: Sampled }): S
2121
lastActivity,
2222
segmentId,
2323
sampled,
24+
shouldRefresh: true,
2425
};
2526
}
2627

packages/replay/src/session/getSession.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,9 @@ export function getSession({
3636

3737
if (!isExpired) {
3838
return { type: 'saved', session };
39-
} else if (session.sampled === 'buffer') {
40-
// Buffered samples should not be re-created when expired, but instead we stop when the replay is done
39+
} else if (!session.shouldRefresh) {
40+
// In this case, stop
41+
// This is the case if we have an error session that is completed (=triggered an error)
4142
const discardedSession = makeSession({ sampled: false });
4243
return { type: 'new', session: discardedSession };
4344
} else {

packages/replay/src/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -395,6 +395,12 @@ export interface Session {
395395
* Is the session sampled? `false` if not sampled, otherwise, `session` or `buffer`
396396
*/
397397
sampled: Sampled;
398+
399+
/**
400+
* If this is false, the session should not be refreshed when it was inactive.
401+
* This can be the case if you had a buffered session which is now recording because an error happened.
402+
*/
403+
shouldRefresh: boolean;
398404
}
399405

400406
export interface EventBuffer {

packages/replay/test/integration/errorSampleRate.test.ts

Lines changed: 93 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -279,62 +279,123 @@ describe('Integration | errorSampleRate', () => {
279279
// sample rate of 0.0), or an error session that has no errors. Instead we
280280
// simply stop the session replay completely and wait for a new page load to
281281
// resample.
282-
it('stops replay if session exceeds MAX_SESSION_LIFE and does not start a new session thereafter', async () => {
283-
// Idle for 15 minutes
284-
jest.advanceTimersByTime(MAX_SESSION_LIFE + 1);
282+
it.each([
283+
['MAX_SESSION_LIFE', MAX_SESSION_LIFE],
284+
['SESSION_IDLE_DURATION', SESSION_IDLE_DURATION],
285+
])(
286+
'stops replay if session had an error and exceeds %s and does not start a new session thereafter',
287+
async (_label, waitTime) => {
288+
expect(replay.session?.shouldRefresh).toBe(true);
289+
290+
captureException(new Error('testing'));
291+
292+
await new Promise(process.nextTick);
293+
jest.advanceTimersByTime(DEFAULT_FLUSH_MIN_DELAY);
294+
await new Promise(process.nextTick);
295+
296+
// segment_id is 1 because it sends twice on error
297+
expect(replay).toHaveLastSentReplay({
298+
recordingPayloadHeader: { segment_id: 1 },
299+
replayEventPayload: expect.objectContaining({
300+
replay_type: 'buffer',
301+
}),
302+
});
303+
expect(replay.session?.shouldRefresh).toBe(false);
304+
305+
// Idle for given time
306+
jest.advanceTimersByTime(waitTime + 1);
307+
await new Promise(process.nextTick);
308+
309+
const TEST_EVENT = {
310+
data: { name: 'lost event' },
311+
timestamp: BASE_TIMESTAMP,
312+
type: 3,
313+
};
314+
mockRecord._emitter(TEST_EVENT);
285315

286-
const TEST_EVENT = {
287-
data: { name: 'lost event' },
288-
timestamp: BASE_TIMESTAMP,
289-
type: 3,
290-
};
291-
mockRecord._emitter(TEST_EVENT);
292-
expect(replay).not.toHaveLastSentReplay();
316+
jest.runAllTimers();
317+
await new Promise(process.nextTick);
293318

294-
jest.runAllTimers();
295-
await new Promise(process.nextTick);
319+
// We stop recording after 15 minutes of inactivity in error mode
296320

297-
// We stop recording after 15 minutes of inactivity in error mode
321+
// still no new replay sent
322+
expect(replay).toHaveLastSentReplay({
323+
recordingPayloadHeader: { segment_id: 1 },
324+
replayEventPayload: expect.objectContaining({
325+
replay_type: 'buffer',
326+
}),
327+
});
298328

299-
expect(replay).not.toHaveLastSentReplay();
300-
expect(replay.isEnabled()).toBe(false);
301-
expect(mockRecord.takeFullSnapshot).not.toHaveBeenCalled();
329+
expect(replay.isEnabled()).toBe(false);
302330

303-
domHandler({
304-
name: 'click',
305-
});
331+
domHandler({
332+
name: 'click',
333+
});
306334

307-
// Remains disabled!
308-
expect(replay.isEnabled()).toBe(false);
309-
});
335+
// Remains disabled!
336+
expect(replay.isEnabled()).toBe(false);
337+
},
338+
);
310339

311-
// Should behave the same as above test
312-
it('stops replay if user has been idle for more than SESSION_IDLE_DURATION and does not start a new session thereafter', async () => {
313-
// Idle for 15 minutes
314-
jest.advanceTimersByTime(SESSION_IDLE_DURATION + 1);
340+
it.each([
341+
['MAX_SESSION_LIFE', MAX_SESSION_LIFE],
342+
['SESSION_IDLE_DURATION', SESSION_IDLE_DURATION],
343+
])('continues buffering replay if session had no error and exceeds %s', async (_label, waitTime) => {
344+
expect(replay).not.toHaveLastSentReplay();
345+
346+
// Idle for given time
347+
jest.advanceTimersByTime(waitTime + 1);
348+
await new Promise(process.nextTick);
315349

316350
const TEST_EVENT = {
317351
data: { name: 'lost event' },
318352
timestamp: BASE_TIMESTAMP,
319353
type: 3,
320354
};
321355
mockRecord._emitter(TEST_EVENT);
322-
expect(replay).not.toHaveLastSentReplay();
323356

324357
jest.runAllTimers();
325358
await new Promise(process.nextTick);
326359

327-
// We stop recording after SESSION_IDLE_DURATION of inactivity in error mode
360+
// still no new replay sent
328361
expect(replay).not.toHaveLastSentReplay();
329-
expect(replay.isEnabled()).toBe(false);
330-
expect(mockRecord.takeFullSnapshot).not.toHaveBeenCalled();
362+
363+
expect(replay.isEnabled()).toBe(true);
364+
expect(replay.isPaused()).toBe(false);
365+
expect(replay.recordingMode).toBe('buffer');
331366

332367
domHandler({
333368
name: 'click',
334369
});
335370

336-
// Remains disabled!
337-
expect(replay.isEnabled()).toBe(false);
371+
await new Promise(process.nextTick);
372+
jest.advanceTimersByTime(DEFAULT_FLUSH_MIN_DELAY);
373+
await new Promise(process.nextTick);
374+
375+
expect(replay).not.toHaveLastSentReplay();
376+
expect(replay.isEnabled()).toBe(true);
377+
expect(replay.isPaused()).toBe(false);
378+
expect(replay.recordingMode).toBe('buffer');
379+
380+
// should still react to errors later on
381+
captureException(new Error('testing'));
382+
383+
await new Promise(process.nextTick);
384+
jest.advanceTimersByTime(DEFAULT_FLUSH_MIN_DELAY);
385+
await new Promise(process.nextTick);
386+
387+
expect(replay).toHaveLastSentReplay({
388+
recordingPayloadHeader: { segment_id: 0 },
389+
replayEventPayload: expect.objectContaining({
390+
replay_type: 'buffer',
391+
}),
392+
});
393+
394+
expect(replay.isEnabled()).toBe(true);
395+
expect(replay.isPaused()).toBe(false);
396+
expect(replay.recordingMode).toBe('session');
397+
expect(replay.session?.sampled).toBe('buffer');
398+
expect(replay.session?.shouldRefresh).toBe(false);
338399
});
339400

340401
it('has the correct timestamps with deferred root event and last replay update', async () => {

packages/replay/test/unit/session/fetchSession.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ describe('Unit | session | fetchSession', () => {
2828
segmentId: 0,
2929
sampled: 'session',
3030
started: 1648827162630,
31+
shouldRefresh: true,
3132
});
3233
});
3334

@@ -43,6 +44,7 @@ describe('Unit | session | fetchSession', () => {
4344
segmentId: 0,
4445
sampled: false,
4546
started: 1648827162630,
47+
shouldRefresh: true,
4648
});
4749
});
4850

packages/replay/test/unit/session/getSession.test.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ function createMockSession(when: number = Date.now()) {
2424
lastActivity: when,
2525
started: when,
2626
sampled: 'session',
27+
shouldRefresh: true,
2728
});
2829
}
2930

@@ -59,6 +60,7 @@ describe('Unit | session | getSession', () => {
5960
lastActivity: expect.any(Number),
6061
sampled: 'session',
6162
started: expect.any(Number),
63+
shouldRefresh: true,
6264
});
6365

6466
// Should not have anything in storage
@@ -129,6 +131,7 @@ describe('Unit | session | getSession', () => {
129131
lastActivity: expect.any(Number),
130132
sampled: 'session',
131133
started: expect.any(Number),
134+
shouldRefresh: true,
132135
});
133136

134137
// Should not have anything in storage
@@ -138,6 +141,7 @@ describe('Unit | session | getSession', () => {
138141
lastActivity: expect.any(Number),
139142
sampled: 'session',
140143
started: expect.any(Number),
144+
shouldRefresh: true,
141145
});
142146
});
143147

@@ -164,6 +168,7 @@ describe('Unit | session | getSession', () => {
164168
lastActivity: now,
165169
sampled: 'session',
166170
started: now,
171+
shouldRefresh: true,
167172
});
168173
});
169174

0 commit comments

Comments
 (0)