Skip to content

Commit 984d26d

Browse files
committed
fix(replay): Do not renew session in error mode
1 parent 67fec37 commit 984d26d

File tree

3 files changed

+83
-13
lines changed

3 files changed

+83
-13
lines changed

packages/replay/src/replay.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,11 @@ export class ReplayContainer implements ReplayContainerInterface {
258258
* new DOM checkout.`
259259
*/
260260
public resume(): void {
261+
if (!this.session || !this.session.sampled) {
262+
this.stop();
263+
return;
264+
}
265+
261266
this._isPaused = false;
262267
this.startRecording();
263268
}
@@ -306,6 +311,13 @@ export class ReplayContainer implements ReplayContainerInterface {
306311
// will get rejected due to an expired session.
307312
this._loadSession({ expiry: SESSION_IDLE_DURATION });
308313

314+
// We know this is set, because it is always set in _loadSession
315+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
316+
if (!this.session!.sampled) {
317+
this.stop();
318+
return;
319+
}
320+
309321
// Note: This will cause a new DOM checkout
310322
this.resume();
311323
return;
@@ -359,6 +371,13 @@ export class ReplayContainer implements ReplayContainerInterface {
359371
// This will create a new session if expired, based on expiry length
360372
this._loadSession({ expiry });
361373

374+
// We know this is set, because it is always set in _loadSession
375+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
376+
if (!this.session!.sampled) {
377+
this.stop();
378+
return;
379+
}
380+
362381
// Session was expired if session ids do not match
363382
const expired = oldSessionId !== this.getSessionId();
364383

packages/replay/src/session/getSession.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type { Session, SessionOptions } from '../types';
44
import { isSessionExpired } from '../util/isSessionExpired';
55
import { createSession } from './createSession';
66
import { fetchSession } from './fetchSession';
7+
import { makeSession } from './Session';
78

89
interface GetSessionParams extends SessionOptions {
910
/**
@@ -38,6 +39,10 @@ export function getSession({
3839

3940
if (!isExpired) {
4041
return { type: 'saved', session };
42+
} else if (session.sampled === 'error') {
43+
// Error samples should not be re-created when expired, but instead we stop when the replay is done
44+
const discardedSession = makeSession({ sampled: false });
45+
return { type: 'new', session: discardedSession };
4146
} else {
4247
__DEBUG_BUILD__ && logger.log('[Replay] Session has expired');
4348
}

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

Lines changed: 59 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
1-
import { captureException } from '@sentry/core';
2-
3-
import { DEFAULT_FLUSH_MIN_DELAY, REPLAY_SESSION_KEY, VISIBILITY_CHANGE_TIMEOUT, WINDOW } from '../../src/constants';
1+
import { captureException, getCurrentHub } from '@sentry/core';
2+
3+
import {
4+
DEFAULT_FLUSH_MIN_DELAY,
5+
MAX_SESSION_LIFE,
6+
REPLAY_SESSION_KEY,
7+
VISIBILITY_CHANGE_TIMEOUT,
8+
WINDOW,
9+
} from '../../src/constants';
410
import type { ReplayContainer } from '../../src/replay';
511
import { addEvent } from '../../src/util/addEvent';
612
import { PerformanceEntryResource } from '../fixtures/performanceEntry/resource';
@@ -258,12 +264,10 @@ describe('Integration | errorSampleRate', () => {
258264
expect(replay).not.toHaveLastSentReplay();
259265
});
260266

261-
it('does not upload if user has been idle for more than 15 minutes and comes back to move their mouse', async () => {
267+
it('stops replay if user has been idle for more than 15 minutes and comes back to move their mouse', async () => {
262268
// Idle for 15 minutes
263269
jest.advanceTimersByTime(15 * 60000);
264270

265-
// TBD: We are currently deciding that this event will get dropped, but
266-
// this could/should change in the future.
267271
const TEST_EVENT = {
268272
data: { name: 'lost event' },
269273
timestamp: BASE_TIMESTAMP,
@@ -275,15 +279,11 @@ describe('Integration | errorSampleRate', () => {
275279
jest.runAllTimers();
276280
await new Promise(process.nextTick);
277281

278-
// Instead of recording the above event, a full snapshot will occur.
279-
//
280-
// TODO: We could potentially figure out a way to save the last session,
281-
// and produce a checkout based on a previous checkout + updates, and then
282-
// replay the event on top. Or maybe replay the event on top of a refresh
283-
// snapshot.
282+
// We stop recording after 15 minutes of inactivity in error mode
284283

285284
expect(replay).not.toHaveLastSentReplay();
286-
expect(mockRecord.takeFullSnapshot).toHaveBeenCalledWith(true);
285+
expect(replay.isEnabled()).toBe(false);
286+
expect(mockRecord.takeFullSnapshot).not.toHaveBeenCalled();
287287
});
288288

289289
it('has the correct timestamps with deferred root event and last replay update', async () => {
@@ -369,6 +369,52 @@ describe('Integration | errorSampleRate', () => {
369369
]),
370370
});
371371
});
372+
373+
it('stops replay when session expires', async () => {
374+
jest.setSystemTime(BASE_TIMESTAMP);
375+
376+
const TEST_EVENT = { data: {}, timestamp: BASE_TIMESTAMP, type: 3 };
377+
mockRecord._emitter(TEST_EVENT);
378+
379+
expect(mockRecord.takeFullSnapshot).not.toHaveBeenCalled();
380+
expect(replay).not.toHaveLastSentReplay();
381+
382+
jest.runAllTimers();
383+
await new Promise(process.nextTick);
384+
385+
captureException(new Error('testing'));
386+
387+
jest.advanceTimersByTime(DEFAULT_FLUSH_MIN_DELAY);
388+
await new Promise(process.nextTick);
389+
390+
expect(replay).toHaveLastSentReplay();
391+
392+
// Wait a bit, shortly before session expires
393+
jest.advanceTimersByTime(MAX_SESSION_LIFE - 1000);
394+
await new Promise(process.nextTick);
395+
396+
mockRecord._emitter(TEST_EVENT);
397+
replay.triggerUserActivity();
398+
399+
expect(replay).toHaveLastSentReplay();
400+
401+
// Now wait after session expires - should stop recording
402+
mockRecord.takeFullSnapshot.mockClear();
403+
(getCurrentHub().getClient()!.getTransport()!.send as unknown as jest.SpyInstance<any>).mockClear();
404+
405+
jest.advanceTimersByTime(10_000);
406+
await new Promise(process.nextTick);
407+
408+
mockRecord._emitter(TEST_EVENT);
409+
replay.triggerUserActivity();
410+
411+
jest.advanceTimersByTime(DEFAULT_FLUSH_MIN_DELAY);
412+
await new Promise(process.nextTick);
413+
414+
expect(replay).not.toHaveLastSentReplay();
415+
expect(mockRecord.takeFullSnapshot).toHaveBeenCalledTimes(0);
416+
expect(replay.isEnabled()).toBe(false);
417+
});
372418
});
373419

374420
/**

0 commit comments

Comments
 (0)