Skip to content

Commit 8cc5a7f

Browse files
committed
feat(replay): Allow to configure maxReplayDuration
This defaults to 60min, and is capped at max. 60min (=you cannot specify a longer max duration than 60min).
1 parent 84ea658 commit 8cc5a7f

File tree

16 files changed

+232
-211
lines changed

16 files changed

+232
-211
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ window.Replay = new Sentry.Replay({
55
flushMinDelay: 200,
66
flushMaxDelay: 200,
77
minReplayDuration: 0,
8+
maxReplayDuration: 2000,
89
});
910

1011
Sentry.init({
@@ -19,5 +20,4 @@ Sentry.init({
1920
window.Replay._replay.timeouts = {
2021
sessionIdlePause: 1000, // this is usually 5min, but we want to test this with shorter times
2122
sessionIdleExpire: 2000, // this is usually 15min, but we want to test this with shorter times
22-
maxSessionLife: 2000, // default: 60min
2323
};

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,5 +20,4 @@ Sentry.init({
2020
window.Replay._replay.timeouts = {
2121
sessionIdlePause: 1000, // this is usually 5min, but we want to test this with shorter times
2222
sessionIdleExpire: 2000, // this is usually 15min, but we want to test this with shorter times
23-
maxSessionLife: 3600000, // default: 60min
2423
};

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,5 +20,4 @@ Sentry.init({
2020
window.Replay._replay.timeouts = {
2121
sessionIdlePause: 1000, // this is usually 5min, but we want to test this with shorter times
2222
sessionIdleExpire: 900000, // defayult: 15min
23-
maxSessionLife: 3600000, // default: 60min
2423
};

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ window.Replay = new Sentry.Replay({
55
flushMinDelay: 200,
66
flushMaxDelay: 200,
77
minReplayDuration: 0,
8+
maxReplayDuration: 4000,
89
});
910

1011
Sentry.init({
@@ -20,5 +21,4 @@ Sentry.init({
2021
window.Replay._replay.timeouts = {
2122
sessionIdlePause: 300000, // default: 5min
2223
sessionIdleExpire: 900000, // default: 15min
23-
maxSessionLife: 4000, // this is usually 60min, but we want to test this with shorter times
2424
};

packages/replay/src/constants.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,6 @@ export const SESSION_IDLE_PAUSE_DURATION = 300_000; // 5 minutes in ms
1717
// The idle limit for a session after which the session expires.
1818
export const SESSION_IDLE_EXPIRE_DURATION = 900_000; // 15 minutes in ms
1919

20-
// The maximum length of a session
21-
export const MAX_SESSION_LIFE = 3_600_000; // 60 minutes in ms
22-
2320
/** Default flush delays */
2421
export const DEFAULT_FLUSH_MIN_DELAY = 5_000;
2522
// XXX: Temp fix for our debounce logic where `maxWait` would never occur if it
@@ -50,3 +47,6 @@ export const REPLAY_MAX_EVENT_BUFFER_SIZE = 20_000_000; // ~20MB
5047
export const MIN_REPLAY_DURATION = 4_999;
5148
/* The max. allowed value that the minReplayDuration can be set to. */
5249
export const MIN_REPLAY_DURATION_LIMIT = 15_000;
50+
51+
/** The max. length of a replay. */
52+
export const MAX_REPLAY_DURATION = 3_600_000; // 60 minutes in ms;

packages/replay/src/integration.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { dropUndefinedKeys } from '@sentry/utils';
55
import {
66
DEFAULT_FLUSH_MAX_DELAY,
77
DEFAULT_FLUSH_MIN_DELAY,
8+
MAX_REPLAY_DURATION,
89
MIN_REPLAY_DURATION,
910
MIN_REPLAY_DURATION_LIMIT,
1011
} from './constants';
@@ -57,6 +58,7 @@ export class Replay implements Integration {
5758
flushMinDelay = DEFAULT_FLUSH_MIN_DELAY,
5859
flushMaxDelay = DEFAULT_FLUSH_MAX_DELAY,
5960
minReplayDuration = MIN_REPLAY_DURATION,
61+
maxReplayDuration = MAX_REPLAY_DURATION,
6062
stickySession = true,
6163
useCompression = true,
6264
_experiments = {},
@@ -136,6 +138,7 @@ export class Replay implements Integration {
136138
flushMinDelay,
137139
flushMaxDelay,
138140
minReplayDuration: Math.min(minReplayDuration, MIN_REPLAY_DURATION_LIMIT),
141+
maxReplayDuration: Math.max(maxReplayDuration, MAX_REPLAY_DURATION),
139142
stickySession,
140143
sessionSampleRate,
141144
errorSampleRate,

packages/replay/src/replay.ts

Lines changed: 10 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import { logger } from '@sentry/utils';
66

77
import {
88
BUFFER_CHECKOUT_TIME,
9-
MAX_SESSION_LIFE,
109
SESSION_IDLE_EXPIRE_DURATION,
1110
SESSION_IDLE_PAUSE_DURATION,
1211
SLOW_CLICK_SCROLL_TIMEOUT,
@@ -148,7 +147,6 @@ export class ReplayContainer implements ReplayContainerInterface {
148147
this.timeouts = {
149148
sessionIdlePause: SESSION_IDLE_PAUSE_DURATION,
150149
sessionIdleExpire: SESSION_IDLE_EXPIRE_DURATION,
151-
maxSessionLife: MAX_SESSION_LIFE,
152150
} as const;
153151
this._lastActivity = Date.now();
154152
this._isEnabled = false;
@@ -278,14 +276,10 @@ export class ReplayContainer implements ReplayContainerInterface {
278276

279277
const previousSessionId = this.session && this.session.id;
280278

281-
const { session } = getSession({
282-
timeouts: this.timeouts,
283-
stickySession: Boolean(this._options.stickySession),
284-
currentSession: this.session,
279+
const { session } = getSession(this, {
285280
// This is intentional: create a new session-based replay when calling `start()`
286281
sessionSampleRate: 1,
287282
allowBuffering: false,
288-
traceInternals: this._options._experiments.traceInternals,
289283
});
290284

291285
session.previousSessionId = previousSessionId;
@@ -307,13 +301,9 @@ export class ReplayContainer implements ReplayContainerInterface {
307301

308302
const previousSessionId = this.session && this.session.id;
309303

310-
const { session } = getSession({
311-
timeouts: this.timeouts,
312-
stickySession: Boolean(this._options.stickySession),
313-
currentSession: this.session,
304+
const { session } = getSession(this, {
314305
sessionSampleRate: 0,
315306
allowBuffering: true,
316-
traceInternals: this._options._experiments.traceInternals,
317307
});
318308

319309
session.previousSessionId = previousSessionId;
@@ -483,7 +473,7 @@ export class ReplayContainer implements ReplayContainerInterface {
483473
// `shouldRefresh`, the session could be considered expired due to
484474
// lifespan, which is not what we want. Update session start date to be
485475
// the current timestamp, so that session is not considered to be
486-
// expired. This means that max replay duration can be MAX_SESSION_LIFE +
476+
// expired. This means that max replay duration can be MAX_REPLAY_DURATION +
487477
// (length of buffer), which we are ok with.
488478
this._updateUserActivity(activityTime);
489479
this._updateSessionActivity(activityTime);
@@ -754,13 +744,9 @@ export class ReplayContainer implements ReplayContainerInterface {
754744
* Returns false if session is not recorded.
755745
*/
756746
private _loadAndCheckSession(): boolean {
757-
const { type, session } = getSession({
758-
timeouts: this.timeouts,
759-
stickySession: Boolean(this._options.stickySession),
760-
currentSession: this.session,
747+
const { type, session } = getSession(this, {
761748
sessionSampleRate: this._options.sessionSampleRate,
762749
allowBuffering: this._options.errorSampleRate > 0 || this.recordingMode === 'buffer',
763-
traceInternals: this._options._experiments.traceInternals,
764750
});
765751

766752
// If session was newly created (i.e. was not loaded from storage), then
@@ -893,7 +879,10 @@ export class ReplayContainer implements ReplayContainerInterface {
893879
return;
894880
}
895881

896-
const expired = isSessionExpired(this.session, this.timeouts);
882+
const expired = isSessionExpired(this.session, {
883+
maxReplayDuration: this._options.maxReplayDuration,
884+
...this.timeouts,
885+
});
897886

898887
if (breadcrumb && !expired) {
899888
this._createCustomBreadcrumb(breadcrumb);
@@ -1136,10 +1125,10 @@ export class ReplayContainer implements ReplayContainerInterface {
11361125
// A flush is about to happen, cancel any queued flushes
11371126
this._debouncedFlush.cancel();
11381127

1139-
// If session is too short, or too long (allow some wiggle room over maxSessionLife), do not send it
1128+
// If session is too short, or too long (allow some wiggle room over maxReplayDuration), do not send it
11401129
// This _should_ not happen, but it may happen if flush is triggered due to a page activity change or similar
11411130
const tooShort = duration < this._options.minReplayDuration;
1142-
const tooLong = duration > this.timeouts.maxSessionLife + 5_000;
1131+
const tooLong = duration > this._options.maxReplayDuration + 5_000;
11431132
if (tooShort || tooLong) {
11441133
logInfo(
11451134
`[Replay] Session duration (${Math.floor(duration / 1000)}s) is too ${

packages/replay/src/session/getSession.ts

Lines changed: 13 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,32 @@
1-
import type { Session, SessionOptions, Timeouts } from '../types';
1+
import type { ReplayContainer, Session } from '../types';
22
import { isSessionExpired } from '../util/isSessionExpired';
33
import { logInfoNextTick } from '../util/log';
44
import { createSession } from './createSession';
55
import { fetchSession } from './fetchSession';
66
import { makeSession } from './Session';
77

8-
interface GetSessionParams extends SessionOptions {
9-
timeouts: Timeouts;
10-
11-
/**
12-
* The current session (e.g. if stickySession is off)
13-
*/
14-
currentSession?: Session;
15-
16-
traceInternals?: boolean;
17-
}
18-
198
/**
209
* Get or create a session
2110
*/
22-
export function getSession({
23-
timeouts,
24-
currentSession,
25-
stickySession,
26-
sessionSampleRate,
27-
allowBuffering,
28-
traceInternals,
29-
}: GetSessionParams): { type: 'new' | 'saved'; session: Session } {
11+
export function getSession(
12+
replay: ReplayContainer,
13+
{ sessionSampleRate, allowBuffering }: { sessionSampleRate: number; allowBuffering: boolean },
14+
): { type: 'new' | 'saved'; session: Session } {
15+
const currentSession = replay.session;
16+
const stickySession = replay.getOptions().stickySession;
17+
const { traceInternals } = replay.getOptions()._experiments;
18+
3019
// If session exists and is passed, use it instead of always hitting session storage
3120
const session = currentSession || (stickySession && fetchSession(traceInternals));
3221

3322
if (session) {
3423
// If there is a session, check if it is valid (e.g. "last activity" time
3524
// should be within the "session idle time", and "session started" time is
3625
// within "max session time").
37-
const isExpired = isSessionExpired(session, timeouts);
26+
const isExpired = isSessionExpired(session, {
27+
maxReplayDuration: replay.getOptions().maxReplayDuration,
28+
...replay.timeouts,
29+
});
3830

3931
if (!isExpired || (allowBuffering && session.shouldRefresh)) {
4032
return { type: 'saved', session };

packages/replay/src/types/replay.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ export interface SendReplayData {
3131
export interface Timeouts {
3232
sessionIdlePause: number;
3333
sessionIdleExpire: number;
34-
maxSessionLife: number;
3534
}
3635

3736
/**
@@ -187,6 +186,12 @@ export interface ReplayPluginOptions extends ReplayNetworkOptions {
187186
*/
188187
minReplayDuration: number;
189188

189+
/**
190+
* The max. duration (in ms) a replay session may be.
191+
* This is capped at max. 60min.
192+
*/
193+
maxReplayDuration: number;
194+
190195
/**
191196
* Callback before adding a custom recording event
192197
*
Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,22 @@
1-
import type { Session, Timeouts } from '../types';
1+
import type { Session } from '../types';
22
import { isExpired } from './isExpired';
33

44
/**
55
* Checks to see if session is expired
66
*/
7-
export function isSessionExpired(session: Session, timeouts: Timeouts, targetTime: number = +new Date()): boolean {
7+
export function isSessionExpired(
8+
session: Session,
9+
{
10+
maxReplayDuration,
11+
sessionIdleExpire,
12+
targetTime = Date.now(),
13+
}: { maxReplayDuration: number; sessionIdleExpire: number; targetTime?: number },
14+
): boolean {
815
return (
916
// First, check that maximum session length has not been exceeded
10-
isExpired(session.started, timeouts.maxSessionLife, targetTime) ||
17+
isExpired(session.started, maxReplayDuration, targetTime) ||
1118
// check that the idle timeout has not been exceeded (i.e. user has
1219
// performed an action within the last `sessionIdleExpire` ms)
13-
isExpired(session.lastActivity, timeouts.sessionIdleExpire, targetTime)
20+
isExpired(session.lastActivity, sessionIdleExpire, targetTime)
1421
);
1522
}

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

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { captureException, getCurrentHub } from '@sentry/core';
33
import {
44
BUFFER_CHECKOUT_TIME,
55
DEFAULT_FLUSH_MIN_DELAY,
6-
MAX_SESSION_LIFE,
6+
MAX_REPLAY_DURATION,
77
REPLAY_SESSION_KEY,
88
SESSION_IDLE_EXPIRE_DURATION,
99
WINDOW,
@@ -428,7 +428,7 @@ describe('Integration | errorSampleRate', () => {
428428
// simply stop the session replay completely and wait for a new page load to
429429
// resample.
430430
it.each([
431-
['MAX_SESSION_LIFE', MAX_SESSION_LIFE],
431+
['MAX_REPLAY_DURATION', MAX_REPLAY_DURATION],
432432
['SESSION_IDLE_DURATION', SESSION_IDLE_EXPIRE_DURATION],
433433
])(
434434
'stops replay if session had an error and exceeds %s and does not start a new session thereafter',
@@ -493,7 +493,7 @@ describe('Integration | errorSampleRate', () => {
493493
);
494494

495495
it.each([
496-
['MAX_SESSION_LIFE', MAX_SESSION_LIFE],
496+
['MAX_REPLAY_DURATION', MAX_REPLAY_DURATION],
497497
['SESSION_IDLE_EXPIRE_DURATION', SESSION_IDLE_EXPIRE_DURATION],
498498
])('continues buffering replay if session had no error and exceeds %s', async (_label, waitTime) => {
499499
const oldSessionId = replay.session?.id;
@@ -761,7 +761,7 @@ describe('Integration | errorSampleRate', () => {
761761
jest.runAllTimers();
762762
await new Promise(process.nextTick);
763763

764-
jest.advanceTimersByTime(2 * MAX_SESSION_LIFE);
764+
jest.advanceTimersByTime(2 * MAX_REPLAY_DURATION);
765765

766766
// in production, this happens at a time interval, here we mock this
767767
mockRecord.takeFullSnapshot(true);
@@ -786,7 +786,7 @@ describe('Integration | errorSampleRate', () => {
786786
data: {
787787
isCheckout: true,
788788
},
789-
timestamp: BASE_TIMESTAMP + 2 * MAX_SESSION_LIFE + DEFAULT_FLUSH_MIN_DELAY + 40,
789+
timestamp: BASE_TIMESTAMP + 2 * MAX_REPLAY_DURATION + DEFAULT_FLUSH_MIN_DELAY + 40,
790790
type: 2,
791791
},
792792
]),
@@ -796,7 +796,7 @@ describe('Integration | errorSampleRate', () => {
796796
mockRecord.takeFullSnapshot.mockClear();
797797
(getCurrentHub().getClient()!.getTransport()!.send as unknown as jest.SpyInstance<any>).mockClear();
798798

799-
jest.advanceTimersByTime(MAX_SESSION_LIFE);
799+
jest.advanceTimersByTime(MAX_REPLAY_DURATION);
800800
await new Promise(process.nextTick);
801801

802802
mockRecord._emitter(TEST_EVENT);
@@ -917,7 +917,7 @@ it('handles buffer sessions that previously had an error', async () => {
917917

918918
// Waiting for max life should eventually stop recording
919919
// We simulate a full checkout which would otherwise be done automatically
920-
for (let i = 0; i < MAX_SESSION_LIFE / 60_000; i++) {
920+
for (let i = 0; i < MAX_REPLAY_DURATION / 60_000; i++) {
921921
jest.advanceTimersByTime(60_000);
922922
await new Promise(process.nextTick);
923923
mockRecord.takeFullSnapshot(true);
@@ -954,7 +954,7 @@ it('handles buffer sessions that never had an error', async () => {
954954

955955
// Waiting for max life should eventually stop recording
956956
// We simulate a full checkout which would otherwise be done automatically
957-
for (let i = 0; i < MAX_SESSION_LIFE / 60_000; i++) {
957+
for (let i = 0; i < MAX_REPLAY_DURATION / 60_000; i++) {
958958
jest.advanceTimersByTime(60_000);
959959
await new Promise(process.nextTick);
960960
mockRecord.takeFullSnapshot(true);

packages/replay/test/integration/flush.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as SentryUtils from '@sentry/utils';
22

3-
import { DEFAULT_FLUSH_MIN_DELAY, MAX_SESSION_LIFE, WINDOW } from '../../src/constants';
3+
import { DEFAULT_FLUSH_MIN_DELAY, MAX_REPLAY_DURATION, WINDOW } from '../../src/constants';
44
import type { ReplayContainer } from '../../src/replay';
55
import { clearSession } from '../../src/session/clearSession';
66
import type { EventBuffer } from '../../src/types';
@@ -291,7 +291,7 @@ describe('Integration | flush', () => {
291291
});
292292

293293
it('does not flush if session is too long', async () => {
294-
replay.timeouts.maxSessionLife = 100_000;
294+
replay.getOptions().maxReplayDuration = 100_000;
295295
jest.setSystemTime(BASE_TIMESTAMP);
296296

297297
sessionStorage.clear();
@@ -319,7 +319,7 @@ describe('Integration | flush', () => {
319319
expect(mockFlush).toHaveBeenCalledTimes(1);
320320
expect(mockSendReplay).toHaveBeenCalledTimes(0);
321321

322-
replay.timeouts.maxSessionLife = MAX_SESSION_LIFE;
322+
replay.getOptions().maxReplayDuration = MAX_REPLAY_DURATION;
323323
replay['_loadAndCheckSession'] = _tmp;
324324
});
325325

0 commit comments

Comments
 (0)