Skip to content

Commit ecdb40c

Browse files
committed
feat(replay): Extend session idle time until expire to 15min
Now, a session will only expire & trigger a new session if no user activity happened for 15min. After 5min of inactivity, we will pause recording events. If the user resumes in the next 10 minutes, we'll resume the session, else re-create it if they resume later.
1 parent 8b97f80 commit ecdb40c

File tree

13 files changed

+193
-62
lines changed

13 files changed

+193
-62
lines changed

packages/browser-integration-tests/suites/replay/sessionExpiry/init.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ Sentry.init({
1717
});
1818

1919
window.Replay._replay.timeouts = {
20-
sessionIdle: 2000, // this is usually 5min, but we want to test this with shorter times
20+
sessionIdlePause: 1000, // this is usually 5min, but we want to test this with shorter times
21+
sessionIdleExpire: 2000, // this is usually 15min, but we want to test this with shorter times
2122
maxSessionLife: 3600000, // default: 60min
2223
};

packages/browser-integration-tests/suites/replay/sessionInactive/init.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ Sentry.init({
1717
});
1818

1919
window.Replay._replay.timeouts = {
20-
sessionIdle: 1000, // default: 5min
21-
maxSessionLife: 2000, // this is usually 60min, but we want to test this with shorter times
20+
sessionIdlePause: 1000, // this is usually 5min, but we want to test this with shorter times
21+
sessionIdleExpire: 900000, // defayult: 15min
22+
maxSessionLife: 3600000, // default: 60min
2223
};

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

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ import {
1111
waitForReplayRequest,
1212
} from '../../../utils/replayHelpers';
1313

14-
// Session should expire after 2s - keep in sync with init.js
15-
const SESSION_TIMEOUT = 2000;
14+
// Session should be paused after 2s - keep in sync with init.js
15+
const SESSION_PAUSED = 2000;
1616

1717
sentryTest('handles an inactive session', async ({ getLocalTestPath, page }) => {
1818
if (shouldSkipReplayTest()) {
@@ -44,11 +44,8 @@ sentryTest('handles an inactive session', async ({ getLocalTestPath, page }) =>
4444

4545
await page.click('#button1');
4646

47-
// We wait for another segment 0
48-
const reqPromise1 = waitForReplayRequest(page, 0);
49-
5047
// Now we wait for the session timeout, nothing should be sent in the meanwhile
51-
await new Promise(resolve => setTimeout(resolve, SESSION_TIMEOUT));
48+
await new Promise(resolve => setTimeout(resolve, SESSION_PAUSED));
5249

5350
// nothing happened because no activity/inactivity was detected
5451
const replay = await getReplaySnapshot(page);
@@ -64,17 +61,17 @@ sentryTest('handles an inactive session', async ({ getLocalTestPath, page }) =>
6461
expect(replay2._isEnabled).toEqual(true);
6562
expect(replay2._isPaused).toEqual(true);
6663

67-
// Trigger an action, should re-start the recording
64+
// We wait for next segment to be sent once we resume the session
65+
const reqPromise1 = waitForReplayRequest(page);
66+
67+
// Trigger an action, should resume the recording
6868
await page.click('#button2');
6969
const req1 = await reqPromise1;
7070

7171
const replay3 = await getReplaySnapshot(page);
7272
expect(replay3._isEnabled).toEqual(true);
7373
expect(replay3._isPaused).toEqual(false);
7474

75-
const replayEvent1 = getReplayEvent(req1);
76-
expect(replayEvent1).toEqual(getExpectedReplayEvent({}));
77-
7875
const fullSnapshots1 = getFullRecordingSnapshots(req1);
7976
expect(fullSnapshots1.length).toEqual(1);
8077
const stringifiedSnapshot1 = normalize(fullSnapshots1[0]);

packages/browser-integration-tests/suites/replay/sessionMaxAge/init.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ Sentry.init({
1717
});
1818

1919
window.Replay._replay.timeouts = {
20-
sessionIdle: 300000, // default: 5min
20+
sessionIdlePause: 300000, // default: 5min
21+
sessionIdleExpire: 900000, // default: 15min
2122
maxSessionLife: 4000, // this is usually 60min, but we want to test this with shorter times
2223
};

packages/replay/src/constants.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,14 @@ export const REPLAY_EVENT_NAME = 'replay_event';
1111
export const RECORDING_EVENT_NAME = 'replay_recording';
1212
export const UNABLE_TO_SEND_REPLAY = 'Unable to send Replay';
1313

14-
// The idle limit for a session
15-
export const SESSION_IDLE_DURATION = 300_000; // 5 minutes in ms
14+
// The idle limit for a session after which recording is paused.
15+
export const SESSION_IDLE_PAUSE_DURATION = 300_000; // 5 minutes in ms
16+
17+
// The idle limit for a session after which the session expires.
18+
export const SESSION_IDLE_EXPIRE_DURATION = 900_000; // 15 minutes in ms
1619

1720
// The maximum length of a session
18-
export const MAX_SESSION_LIFE = 3_600_000; // 60 minutes
21+
export const MAX_SESSION_LIFE = 3_600_000; // 60 minutes in ms
1922

2023
/** Default flush delays */
2124
export const DEFAULT_FLUSH_MIN_DELAY = 5_000;

packages/replay/src/replay.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,13 @@ import { captureException, getCurrentHub } from '@sentry/core';
44
import type { Breadcrumb, ReplayRecordingMode } from '@sentry/types';
55
import { logger } from '@sentry/utils';
66

7-
import { ERROR_CHECKOUT_TIME, MAX_SESSION_LIFE, SESSION_IDLE_DURATION, WINDOW } from './constants';
7+
import {
8+
ERROR_CHECKOUT_TIME,
9+
MAX_SESSION_LIFE,
10+
SESSION_IDLE_EXPIRE_DURATION,
11+
SESSION_IDLE_PAUSE_DURATION,
12+
WINDOW,
13+
} from './constants';
814
import { setupPerformanceObserver } from './coreHandlers/performanceObserver';
915
import { createEventBuffer } from './eventBuffer';
1016
import { getSession } from './session/getSession';
@@ -61,7 +67,8 @@ export class ReplayContainer implements ReplayContainerInterface {
6167
* @hidden
6268
*/
6369
public readonly timeouts: Timeouts = {
64-
sessionIdle: SESSION_IDLE_DURATION,
70+
sessionIdlePause: SESSION_IDLE_PAUSE_DURATION,
71+
sessionIdleExpire: SESSION_IDLE_EXPIRE_DURATION,
6572
maxSessionLife: MAX_SESSION_LIFE,
6673
} as const;
6774

@@ -423,12 +430,12 @@ export class ReplayContainer implements ReplayContainerInterface {
423430
const oldSessionId = this.getSessionId();
424431

425432
// Prevent starting a new session if the last user activity is older than
426-
// SESSION_IDLE_DURATION. Otherwise non-user activity can trigger a new
433+
// SESSION_IDLE_PAUSE_DURATION. Otherwise non-user activity can trigger a new
427434
// session+recording. This creates noisy replays that do not have much
428435
// content in them.
429436
if (
430437
this._lastActivity &&
431-
isExpired(this._lastActivity, this.timeouts.sessionIdle) &&
438+
isExpired(this._lastActivity, this.timeouts.sessionIdlePause) &&
432439
this.session &&
433440
this.session.sampled === 'session'
434441
) {
@@ -638,7 +645,7 @@ export class ReplayContainer implements ReplayContainerInterface {
638645
const isSessionActive = this.checkAndHandleExpiredSession();
639646

640647
if (!isSessionActive) {
641-
// If the user has come back to the page within SESSION_IDLE_DURATION
648+
// If the user has come back to the page within SESSION_IDLE_PAUSE_DURATION
642649
// ms, we will re-use the existing session, otherwise create a new
643650
// session
644651
__DEBUG_BUILD__ && logger.log('[Replay] Document has become active, but session has expired');

packages/replay/src/types.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ export interface SendReplayData {
2525
}
2626

2727
export interface Timeouts {
28-
sessionIdle: number;
28+
sessionIdlePause: number;
29+
sessionIdleExpire: number;
2930
maxSessionLife: number;
3031
}
3132

@@ -455,10 +456,7 @@ export interface ReplayContainer {
455456
performanceEvents: AllPerformanceEntry[];
456457
session: Session | undefined;
457458
recordingMode: ReplayRecordingMode;
458-
timeouts: {
459-
sessionIdle: number;
460-
maxSessionLife: number;
461-
};
459+
timeouts: Timeouts;
462460
isEnabled(): boolean;
463461
isPaused(): boolean;
464462
getContext(): InternalEventContext;

packages/replay/src/util/addEvent.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ export async function addEvent(
3131
// page has been left open and idle for a long period of time and user
3232
// comes back to trigger a new session. The performance entries rely on
3333
// `performance.timeOrigin`, which is when the page first opened.
34-
if (timestampInMs + replay.timeouts.sessionIdle < Date.now()) {
34+
if (timestampInMs + replay.timeouts.sessionIdlePause < Date.now()) {
3535
return null;
3636
}
3737

packages/replay/src/util/isSessionExpired.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ export function isSessionExpired(session: Session, timeouts: Timeouts, targetTim
99
// First, check that maximum session length has not been exceeded
1010
isExpired(session.started, timeouts.maxSessionLife, targetTime) ||
1111
// check that the idle timeout has not been exceeded (i.e. user has
12-
// performed an action within the last `idleTimeout` ms)
13-
isExpired(session.lastActivity, timeouts.sessionIdle, targetTime)
12+
// performed an action within the last `sessionIdleExpire` ms)
13+
isExpired(session.lastActivity, timeouts.sessionIdleExpire, targetTime)
1414
);
1515
}

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

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55
ERROR_CHECKOUT_TIME,
66
MAX_SESSION_LIFE,
77
REPLAY_SESSION_KEY,
8-
SESSION_IDLE_DURATION,
8+
SESSION_IDLE_EXPIRE_DURATION,
99
WINDOW,
1010
} from '../../src/constants';
1111
import type { ReplayContainer } from '../../src/replay';
@@ -252,15 +252,15 @@ describe('Integration | errorSampleRate', () => {
252252
});
253253
});
254254

255-
it('does not send a replay when triggering a full dom snapshot when document becomes visible after [SESSION_IDLE_DURATION]ms', async () => {
255+
it('does not send a replay when triggering a full dom snapshot when document becomes visible after [SESSION_IDLE_EXPIRE_DURATION]ms', async () => {
256256
Object.defineProperty(document, 'visibilityState', {
257257
configurable: true,
258258
get: function () {
259259
return 'visible';
260260
},
261261
});
262262

263-
jest.advanceTimersByTime(SESSION_IDLE_DURATION + 1);
263+
jest.advanceTimersByTime(SESSION_IDLE_EXPIRE_DURATION + 1);
264264

265265
document.dispatchEvent(new Event('visibilitychange'));
266266

@@ -284,8 +284,8 @@ describe('Integration | errorSampleRate', () => {
284284

285285
expect(replay).not.toHaveLastSentReplay();
286286

287-
// User comes back before `SESSION_IDLE_DURATION` elapses
288-
jest.advanceTimersByTime(SESSION_IDLE_DURATION - 100);
287+
// User comes back before `SESSION_IDLE_EXPIRE_DURATION` elapses
288+
jest.advanceTimersByTime(SESSION_IDLE_EXPIRE_DURATION - 100);
289289
Object.defineProperty(document, 'visibilityState', {
290290
configurable: true,
291291
get: function () {
@@ -403,9 +403,9 @@ describe('Integration | errorSampleRate', () => {
403403
});
404404

405405
// Should behave the same as above test
406-
it('stops replay if user has been idle for more than SESSION_IDLE_DURATION and does not start a new session thereafter', async () => {
406+
it('stops replay if user has been idle for more than SESSION_IDLE_EXPIRE_DURATION and does not start a new session thereafter', async () => {
407407
// Idle for 15 minutes
408-
jest.advanceTimersByTime(SESSION_IDLE_DURATION + 1);
408+
jest.advanceTimersByTime(SESSION_IDLE_EXPIRE_DURATION + 1);
409409

410410
const TEST_EVENT = {
411411
data: { name: 'lost event' },
@@ -418,7 +418,7 @@ describe('Integration | errorSampleRate', () => {
418418
jest.runAllTimers();
419419
await new Promise(process.nextTick);
420420

421-
// We stop recording after SESSION_IDLE_DURATION of inactivity in error mode
421+
// We stop recording after SESSION_IDLE_EXPIRE_DURATION of inactivity in error mode
422422
expect(replay).not.toHaveLastSentReplay();
423423
expect(replay.isEnabled()).toBe(false);
424424
expect(mockRecord.takeFullSnapshot).not.toHaveBeenCalled();
@@ -544,7 +544,7 @@ describe('Integration | errorSampleRate', () => {
544544
expect(replay).not.toHaveLastSentReplay();
545545

546546
// Go idle
547-
jest.advanceTimersByTime(SESSION_IDLE_DURATION + 1);
547+
jest.advanceTimersByTime(SESSION_IDLE_EXPIRE_DURATION + 1);
548548
await new Promise(process.nextTick);
549549

550550
mockRecord._emitter(TEST_EVENT);

0 commit comments

Comments
 (0)