Skip to content

Commit 5354ee5

Browse files
authored
feat(replay): Allow to configure maxReplayDuration (#8769)
This defaults to 60min, and is capped at max. 60min (=you cannot specify a longer max duration than 60min). Closes #8758
1 parent 1d64a06 commit 5354ee5

File tree

25 files changed

+157
-154
lines changed

25 files changed

+157
-154
lines changed

packages/browser-integration-tests/suites/replay/errors/errorsInSession/init.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,5 +19,4 @@ Sentry.init({
1919
return event;
2020
},
2121
integrations: [window.Replay],
22-
debug: true,
2322
});

packages/browser-integration-tests/suites/replay/largeMutations/mutationLimit/init.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ Sentry.init({
1313
sampleRate: 0,
1414
replaysSessionSampleRate: 1.0,
1515
replaysOnErrorSampleRate: 0.0,
16-
debug: true,
1716

1817
integrations: [window.Replay],
1918
});

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/maxReplayDuration/test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { sentryTest } from '../../../utils/fixtures';
44
import { getExpectedReplayEvent } from '../../../utils/replayEventTemplates';
55
import { getReplayEvent, shouldSkipReplayTest, waitForReplayRequest } from '../../../utils/replayHelpers';
66

7-
const SESSION_MAX_AGE = 2000;
7+
const MAX_REPLAY_DURATION = 2000;
88

99
sentryTest('keeps track of max duration across reloads', async ({ getLocalTestPath, page }) => {
1010
if (shouldSkipReplayTest()) {
@@ -26,15 +26,15 @@ sentryTest('keeps track of max duration across reloads', async ({ getLocalTestPa
2626

2727
await page.goto(url);
2828

29-
await new Promise(resolve => setTimeout(resolve, SESSION_MAX_AGE / 2));
29+
await new Promise(resolve => setTimeout(resolve, MAX_REPLAY_DURATION / 2));
3030

3131
await page.reload();
3232
await page.click('#button1');
3333

3434
// After the second reload, we should have a new session (because we exceeded max age)
3535
const reqPromise3 = waitForReplayRequest(page, 0);
3636

37-
await new Promise(resolve => setTimeout(resolve, SESSION_MAX_AGE / 2 + 100));
37+
await new Promise(resolve => setTimeout(resolve, MAX_REPLAY_DURATION / 2 + 100));
3838

3939
void page.click('#button1');
4040
await page.evaluate(`Object.defineProperty(document, 'visibilityState', {

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ Sentry.init({
1212
sampleRate: 0,
1313
replaysSessionSampleRate: 1.0,
1414
replaysOnErrorSampleRate: 0.0,
15-
debug: true,
1615

1716
integrations: [window.Replay],
1817
});

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

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,11 @@ Sentry.init({
1212
sampleRate: 0,
1313
replaysSessionSampleRate: 1.0,
1414
replaysOnErrorSampleRate: 0.0,
15-
debug: true,
1615

1716
integrations: [window.Replay],
1817
});
1918

2019
window.Replay._replay.timeouts = {
2120
sessionIdlePause: 1000, // this is usually 5min, but we want to test this with shorter times
2221
sessionIdleExpire: 2000, // this is usually 15min, but we want to test this with shorter times
23-
maxSessionLife: 3600000, // default: 60min
2422
};

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

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,11 @@ Sentry.init({
1212
sampleRate: 0,
1313
replaysSessionSampleRate: 1.0,
1414
replaysOnErrorSampleRate: 0.0,
15-
debug: true,
1615

1716
integrations: [window.Replay],
1817
});
1918

2019
window.Replay._replay.timeouts = {
2120
sessionIdlePause: 1000, // this is usually 5min, but we want to test this with shorter times
2221
sessionIdleExpire: 900000, // defayult: 15min
23-
maxSessionLife: 3600000, // default: 60min
2422
};

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

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

1011
Sentry.init({
1112
dsn: 'https://[email protected]/1337',
1213
sampleRate: 0,
1314
replaysSessionSampleRate: 1.0,
1415
replaysOnErrorSampleRate: 0.0,
15-
debug: true,
1616

1717
integrations: [window.Replay],
1818
});
1919

2020
window.Replay._replay.timeouts = {
2121
sessionIdlePause: 300000, // default: 5min
2222
sessionIdleExpire: 900000, // default: 15min
23-
maxSessionLife: 4000, // this is usually 60min, but we want to test this with shorter times
2423
};

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

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

1414
// Session should be max. 4s long
15-
const SESSION_MAX_AGE = 4000;
15+
const MAX_REPLAY_DURATION = 4000;
1616

1717
/*
1818
The main difference between this and sessionExpiry test, is that here we wait for the overall time (4s)
@@ -58,7 +58,7 @@ sentryTest('handles session that exceeds max age', async ({ getLocalTestPath, pa
5858
// Wait for an incremental snapshot
5959
// Wait half of the session max age (after initial flush), but account for potentially slow runners
6060
const timePassed1 = Date.now() - startTimestamp;
61-
await new Promise(resolve => setTimeout(resolve, Math.max(SESSION_MAX_AGE / 2 - timePassed1, 0)));
61+
await new Promise(resolve => setTimeout(resolve, Math.max(MAX_REPLAY_DURATION / 2 - timePassed1, 0)));
6262
await page.click('#button1');
6363

6464
const req1 = await reqPromise1;
@@ -71,7 +71,7 @@ sentryTest('handles session that exceeds max age', async ({ getLocalTestPath, pa
7171

7272
// Wait for session to expire
7373
const timePassed2 = Date.now() - startTimestamp;
74-
await new Promise(resolve => setTimeout(resolve, Math.max(SESSION_MAX_AGE - timePassed2, 0)));
74+
await new Promise(resolve => setTimeout(resolve, Math.max(MAX_REPLAY_DURATION - timePassed2, 0)));
7575
await page.click('#button2');
7676

7777
const req2 = await reqPromise2;

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.min(maxReplayDuration, MAX_REPLAY_DURATION),
139142
stickySession,
140143
sessionSampleRate,
141144
errorSampleRate,

packages/replay/src/replay.ts

Lines changed: 16 additions & 11 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,
@@ -150,7 +149,6 @@ export class ReplayContainer implements ReplayContainerInterface {
150149
this.timeouts = {
151150
sessionIdlePause: SESSION_IDLE_PAUSE_DURATION,
152151
sessionIdleExpire: SESSION_IDLE_EXPIRE_DURATION,
153-
maxSessionLife: MAX_SESSION_LIFE,
154152
} as const;
155153
this._lastActivity = Date.now();
156154
this._isEnabled = false;
@@ -277,7 +275,8 @@ export class ReplayContainer implements ReplayContainerInterface {
277275
const session = loadOrCreateSession(
278276
this.session,
279277
{
280-
timeouts: this.timeouts,
278+
maxReplayDuration: this._options.maxReplayDuration,
279+
sessionIdleExpire: this.timeouts.sessionIdleExpire,
281280
traceInternals: this._options._experiments.traceInternals,
282281
},
283282
{
@@ -307,7 +306,8 @@ export class ReplayContainer implements ReplayContainerInterface {
307306
const session = loadOrCreateSession(
308307
this.session,
309308
{
310-
timeouts: this.timeouts,
309+
sessionIdleExpire: this.timeouts.sessionIdleExpire,
310+
maxReplayDuration: this._options.maxReplayDuration,
311311
traceInternals: this._options._experiments.traceInternals,
312312
},
313313
{
@@ -483,7 +483,7 @@ export class ReplayContainer implements ReplayContainerInterface {
483483
// `shouldRefresh`, the session could be considered expired due to
484484
// lifespan, which is not what we want. Update session start date to be
485485
// the current timestamp, so that session is not considered to be
486-
// expired. This means that max replay duration can be MAX_SESSION_LIFE +
486+
// expired. This means that max replay duration can be MAX_REPLAY_DURATION +
487487
// (length of buffer), which we are ok with.
488488
this._updateUserActivity(activityTime);
489489
this._updateSessionActivity(activityTime);
@@ -764,7 +764,8 @@ export class ReplayContainer implements ReplayContainerInterface {
764764
const session = loadOrCreateSession(
765765
this.session,
766766
{
767-
timeouts: this.timeouts,
767+
sessionIdleExpire: this.timeouts.sessionIdleExpire,
768+
maxReplayDuration: this._options.maxReplayDuration,
768769
traceInternals: this._options._experiments.traceInternals,
769770
},
770771
{
@@ -793,8 +794,9 @@ export class ReplayContainer implements ReplayContainerInterface {
793794
const newSession = maybeRefreshSession(
794795
currentSession,
795796
{
796-
timeouts: this.timeouts,
797+
sessionIdleExpire: this.timeouts.sessionIdleExpire,
797798
traceInternals: this._options._experiments.traceInternals,
799+
maxReplayDuration: this._options.maxReplayDuration,
798800
},
799801
{
800802
stickySession: Boolean(this._options.stickySession),
@@ -929,7 +931,10 @@ export class ReplayContainer implements ReplayContainerInterface {
929931
return;
930932
}
931933

932-
const expired = isSessionExpired(this.session, this.timeouts);
934+
const expired = isSessionExpired(this.session, {
935+
maxReplayDuration: this._options.maxReplayDuration,
936+
...this.timeouts,
937+
});
933938

934939
if (breadcrumb && !expired) {
935940
this._createCustomBreadcrumb(breadcrumb);
@@ -1108,7 +1113,7 @@ export class ReplayContainer implements ReplayContainerInterface {
11081113
// Check total duration again, to avoid sending outdated stuff
11091114
// We leave 30s wiggle room to accomodate late flushing etc.
11101115
// This _could_ happen when the browser is suspended during flushing, in which case we just want to stop
1111-
if (timestamp - this._context.initialTimestamp > this.timeouts.maxSessionLife + 30_000) {
1116+
if (timestamp - this._context.initialTimestamp > this._options.maxReplayDuration + 30_000) {
11121117
throw new Error('Session is too long, not sending replay');
11131118
}
11141119

@@ -1181,10 +1186,10 @@ export class ReplayContainer implements ReplayContainerInterface {
11811186
// A flush is about to happen, cancel any queued flushes
11821187
this._debouncedFlush.cancel();
11831188

1184-
// If session is too short, or too long (allow some wiggle room over maxSessionLife), do not send it
1189+
// If session is too short, or too long (allow some wiggle room over maxReplayDuration), do not send it
11851190
// This _should_ not happen, but it may happen if flush is triggered due to a page activity change or similar
11861191
const tooShort = duration < this._options.minReplayDuration;
1187-
const tooLong = duration > this.timeouts.maxSessionLife + 5_000;
1192+
const tooLong = duration > this._options.maxReplayDuration + 5_000;
11881193
if (tooShort || tooLong) {
11891194
logInfo(
11901195
`[Replay] Session duration (${Math.floor(duration / 1000)}s) is too ${

packages/replay/src/session/loadOrCreateSession.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { Session, SessionOptions, Timeouts } from '../types';
1+
import type { Session, SessionOptions } from '../types';
22
import { logInfoNextTick } from '../util/log';
33
import { createSession } from './createSession';
44
import { fetchSession } from './fetchSession';
@@ -11,10 +11,12 @@ import { maybeRefreshSession } from './maybeRefreshSession';
1111
export function loadOrCreateSession(
1212
currentSession: Session | undefined,
1313
{
14-
timeouts,
1514
traceInternals,
15+
sessionIdleExpire,
16+
maxReplayDuration,
1617
}: {
17-
timeouts: Timeouts;
18+
sessionIdleExpire: number;
19+
maxReplayDuration: number;
1820
traceInternals?: boolean;
1921
},
2022
sessionOptions: SessionOptions,
@@ -28,5 +30,5 @@ export function loadOrCreateSession(
2830
return createSession(sessionOptions);
2931
}
3032

31-
return maybeRefreshSession(existingSession, { timeouts, traceInternals }, sessionOptions);
33+
return maybeRefreshSession(existingSession, { sessionIdleExpire, traceInternals, maxReplayDuration }, sessionOptions);
3234
}

packages/replay/src/session/maybeRefreshSession.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { Session, SessionOptions, Timeouts } from '../types';
1+
import type { Session, SessionOptions } from '../types';
22
import { isSessionExpired } from '../util/isSessionExpired';
33
import { logInfoNextTick } from '../util/log';
44
import { createSession } from './createSession';
@@ -12,16 +12,18 @@ import { makeSession } from './Session';
1212
export function maybeRefreshSession(
1313
session: Session,
1414
{
15-
timeouts,
1615
traceInternals,
16+
maxReplayDuration,
17+
sessionIdleExpire,
1718
}: {
18-
timeouts: Timeouts;
19+
sessionIdleExpire: number;
20+
maxReplayDuration: number;
1921
traceInternals?: boolean;
2022
},
2123
sessionOptions: SessionOptions,
2224
): Session {
2325
// If not expired, all good, just keep the session
24-
if (!isSessionExpired(session, timeouts)) {
26+
if (!isSessionExpired(session, { sessionIdleExpire, maxReplayDuration })) {
2527
return session;
2628
}
2729

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
*

packages/replay/src/util/addEvent.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,9 @@ export async function addEvent(
4141
}
4242

4343
// Throw out events that are +60min from the initial timestamp
44-
if (timestampInMs > replay.getContext().initialTimestamp + replay.timeouts.maxSessionLife) {
44+
if (timestampInMs > replay.getContext().initialTimestamp + replay.getOptions().maxReplayDuration) {
4545
logInfo(
46-
`[Replay] Skipping event with timestamp ${timestampInMs} because it is after maxSessionLife`,
46+
`[Replay] Skipping event with timestamp ${timestampInMs} because it is after maxReplayDuration`,
4747
replay.getOptions()._experiments.traceInternals,
4848
);
4949
return null;
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
}

0 commit comments

Comments
 (0)