Skip to content

Commit 0ec5bf6

Browse files
committed
fix(replay): Streamline session creation/refresh
1 parent 448406a commit 0ec5bf6

14 files changed

+756
-350
lines changed

packages/replay/src/replay.ts

Lines changed: 85 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import { handleKeyboardEvent } from './coreHandlers/handleKeyboardEvent';
1818
import { setupPerformanceObserver } from './coreHandlers/performanceObserver';
1919
import { createEventBuffer } from './eventBuffer';
2020
import { clearSession } from './session/clearSession';
21-
import { getSession } from './session/getSession';
21+
import { loadOrCreateSession, maybeRefreshSession } from './session/getSession';
2222
import { saveSession } from './session/saveSession';
2323
import type {
2424
AddEventResult,
@@ -228,28 +228,22 @@ export class ReplayContainer implements ReplayContainerInterface {
228228

229229
// Otherwise if there is _any_ sample rate set, try to load an existing
230230
// session, or create a new one.
231-
const isSessionSampled = this._loadAndCheckSession();
232-
233-
if (!isSessionSampled) {
234-
// This should only occur if `errorSampleRate` is 0 and was unsampled for
235-
// session-based replay. In this case there is nothing to do.
236-
return;
237-
}
231+
this._initializeSessionForSampling();
238232

239233
if (!this.session) {
240234
// This should not happen, something wrong has occurred
241235
this._handleException(new Error('Unable to initialize and create session'));
242236
return;
243237
}
244238

245-
if (this.session.sampled && this.session.sampled !== 'session') {
246-
// If not sampled as session-based, then recording mode will be `buffer`
247-
// Note that we don't explicitly check if `sampled === 'buffer'` because we
248-
// could have sessions from Session storage that are still `error` from
249-
// prior SDK version.
250-
this.recordingMode = 'buffer';
239+
if (this.session.sampled === false) {
240+
// This should only occur if `errorSampleRate` is 0 and was unsampled for
241+
// session-based replay. In this case there is nothing to do.
242+
return;
251243
}
252244

245+
this.recordingMode = this.session.sampled === 'buffer' ? 'buffer' : 'session';
246+
253247
logInfoNextTick(
254248
`[Replay] Starting replay in ${this.recordingMode} mode`,
255249
this._options._experiments.traceInternals,
@@ -276,19 +270,20 @@ export class ReplayContainer implements ReplayContainerInterface {
276270

277271
logInfoNextTick('[Replay] Starting replay in session mode', this._options._experiments.traceInternals);
278272

279-
const previousSessionId = this.session && this.session.id;
280-
281-
const { session } = getSession({
282-
timeouts: this.timeouts,
283-
stickySession: Boolean(this._options.stickySession),
284-
currentSession: this.session,
285-
// This is intentional: create a new session-based replay when calling `start()`
286-
sessionSampleRate: 1,
287-
allowBuffering: false,
288-
traceInternals: this._options._experiments.traceInternals,
289-
});
273+
const session = loadOrCreateSession(
274+
this.session,
275+
{
276+
timeouts: this.timeouts,
277+
traceInternals: this._options._experiments.traceInternals,
278+
},
279+
{
280+
stickySession: this._options.stickySession,
281+
// This is intentional: create a new session-based replay when calling `start()`
282+
sessionSampleRate: 1,
283+
allowBuffering: false,
284+
},
285+
);
290286

291-
session.previousSessionId = previousSessionId;
292287
this.session = session;
293288

294289
this._initializeRecording();
@@ -305,18 +300,19 @@ export class ReplayContainer implements ReplayContainerInterface {
305300

306301
logInfoNextTick('[Replay] Starting replay in buffer mode', this._options._experiments.traceInternals);
307302

308-
const previousSessionId = this.session && this.session.id;
309-
310-
const { session } = getSession({
311-
timeouts: this.timeouts,
312-
stickySession: Boolean(this._options.stickySession),
313-
currentSession: this.session,
314-
sessionSampleRate: 0,
315-
allowBuffering: true,
316-
traceInternals: this._options._experiments.traceInternals,
317-
});
303+
const session = loadOrCreateSession(
304+
this.session,
305+
{
306+
timeouts: this.timeouts,
307+
traceInternals: this._options._experiments.traceInternals,
308+
},
309+
{
310+
stickySession: this._options.stickySession,
311+
sessionSampleRate: 0,
312+
allowBuffering: true,
313+
},
314+
);
318315

319-
session.previousSessionId = previousSessionId;
320316
this.session = session;
321317

322318
this.recordingMode = 'buffer';
@@ -427,7 +423,7 @@ export class ReplayContainer implements ReplayContainerInterface {
427423
* new DOM checkout.`
428424
*/
429425
public resume(): void {
430-
if (!this._isPaused || !this._loadAndCheckSession()) {
426+
if (!this._isPaused || !this._checkSession()) {
431427
return;
432428
}
433429

@@ -535,7 +531,7 @@ export class ReplayContainer implements ReplayContainerInterface {
535531
if (!this._stopRecording) {
536532
// Create a new session, otherwise when the user action is flushed, it
537533
// will get rejected due to an expired session.
538-
if (!this._loadAndCheckSession()) {
534+
if (!this._checkSession()) {
539535
return;
540536
}
541537

@@ -634,7 +630,7 @@ export class ReplayContainer implements ReplayContainerInterface {
634630

635631
// --- There is recent user activity --- //
636632
// This will create a new session if expired, based on expiry length
637-
if (!this._loadAndCheckSession()) {
633+
if (!this._checkSession()) {
638634
return;
639635
}
640636

@@ -751,31 +747,63 @@ export class ReplayContainer implements ReplayContainerInterface {
751747

752748
/**
753749
* Loads (or refreshes) the current session.
750+
*/
751+
private _initializeSessionForSampling(): void {
752+
// Whenever there is _any_ error sample rate, we always allow buffering
753+
// Because we decide on sampling when an error occurs, we need to buffer at all times if sampling for errors
754+
const allowBuffering = this._options.errorSampleRate > 0;
755+
756+
const session = loadOrCreateSession(
757+
this.session,
758+
{
759+
timeouts: this.timeouts,
760+
traceInternals: this._options._experiments.traceInternals,
761+
},
762+
{
763+
stickySession: this._options.stickySession,
764+
sessionSampleRate: this._options.sessionSampleRate,
765+
allowBuffering,
766+
},
767+
);
768+
769+
this.session = session;
770+
}
771+
772+
/**
773+
* Checks and potentially refreshes the current session.
754774
* Returns false if session is not recorded.
755775
*/
756-
private _loadAndCheckSession(): boolean {
757-
const { type, session } = getSession({
758-
timeouts: this.timeouts,
759-
stickySession: Boolean(this._options.stickySession),
760-
currentSession: this.session,
761-
sessionSampleRate: this._options.sessionSampleRate,
762-
allowBuffering: this._options.errorSampleRate > 0 || this.recordingMode === 'buffer',
763-
traceInternals: this._options._experiments.traceInternals,
764-
});
776+
private _checkSession(): boolean {
777+
// If there is no session yet, we do not want to refresh anything
778+
// This should generally not happen, but to be safe....
779+
if (!this.session) {
780+
return false;
781+
}
782+
783+
const currentSession = this.session;
784+
785+
const newSession = maybeRefreshSession(
786+
currentSession,
787+
{
788+
timeouts: this.timeouts,
789+
traceInternals: this._options._experiments.traceInternals,
790+
},
791+
{
792+
stickySession: Boolean(this._options.stickySession),
793+
sessionSampleRate: this._options.sessionSampleRate,
794+
allowBuffering: this._options.errorSampleRate > 0,
795+
},
796+
);
797+
798+
const isNew = newSession.id !== currentSession.id;
765799

766800
// If session was newly created (i.e. was not loaded from storage), then
767801
// enable flag to create the root replay
768-
if (type === 'new') {
802+
if (isNew) {
769803
this.setInitialState();
804+
this.session = newSession;
770805
}
771806

772-
const currentSessionId = this.getSessionId();
773-
if (session.id !== currentSessionId) {
774-
session.previousSessionId = currentSessionId;
775-
}
776-
777-
this.session = session;
778-
779807
if (!this.session.sampled) {
780808
void this.stop({ reason: 'session not refreshed' });
781809
return false;

packages/replay/src/session/Session.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export function makeSession(session: Partial<Session> & { sampled: Sampled }): S
1414
const segmentId = session.segmentId || 0;
1515
const sampled = session.sampled;
1616
const shouldRefresh = typeof session.shouldRefresh === 'boolean' ? session.shouldRefresh : true;
17+
const previousSessionId = session.previousSessionId;
1718

1819
return {
1920
id,
@@ -22,5 +23,6 @@ export function makeSession(session: Partial<Session> & { sampled: Sampled }): S
2223
segmentId,
2324
sampled,
2425
shouldRefresh,
26+
previousSessionId,
2527
};
2628
}

packages/replay/src/session/createSession.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,14 @@ export function getSessionSampleType(sessionSampleRate: number, allowBuffering:
1515
* that all replays will be saved to as attachments. Currently, we only expect
1616
* one of these Sentry events per "replay session".
1717
*/
18-
export function createSession({ sessionSampleRate, allowBuffering, stickySession = false }: SessionOptions): Session {
18+
export function createSession(
19+
{ sessionSampleRate, allowBuffering, stickySession = false }: SessionOptions,
20+
{ previousSessionId }: { previousSessionId?: string } = {},
21+
): Session {
1922
const sampled = getSessionSampleType(sessionSampleRate, allowBuffering);
2023
const session = makeSession({
2124
sampled,
25+
previousSessionId,
2226
});
2327

2428
if (stickySession) {

packages/replay/src/session/getSession.ts

Lines changed: 81 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -5,59 +5,95 @@ import { createSession } from './createSession';
55
import { fetchSession } from './fetchSession';
66
import { makeSession } from './Session';
77

8-
interface GetSessionParams extends SessionOptions {
9-
timeouts: Timeouts;
8+
/**
9+
* Check a session, and either return it or a refreshed version of it.
10+
* The refreshed version may be unsampled.
11+
* You can check if the session has changed by comparing the session IDs.
12+
*/
13+
export function maybeRefreshSession(
14+
session: Session,
15+
{
16+
timeouts,
17+
traceInternals,
18+
}: {
19+
timeouts: Timeouts;
20+
traceInternals?: boolean;
21+
},
22+
sessionOptions: SessionOptions,
23+
): Session {
24+
const isExpired = isSessionExpired(session, timeouts);
25+
26+
// If not expired, all good, just keep the session
27+
if (!isExpired) {
28+
return session;
29+
}
30+
31+
const isBuffering = session.sampled === 'buffer';
32+
33+
// If we are buffering & the session may be refreshed, just return it
34+
if (isBuffering && session.shouldRefresh) {
35+
return session;
36+
}
37+
38+
// If we are buffering & the session may not be refreshed (=it was converted to session previously already)
39+
// We return an unsampled new session
40+
if (isBuffering) {
41+
logInfoNextTick('[Replay] Session should not be refreshed', traceInternals);
42+
return makeSession({ sampled: false });
43+
}
1044

11-
/**
12-
* The current session (e.g. if stickySession is off)
13-
*/
14-
currentSession?: Session;
45+
// Else, we are not buffering, and the session is expired, so we need to create a new one
46+
logInfoNextTick('[Replay] Session has expired, creating new one...', traceInternals);
1547

16-
traceInternals?: boolean;
48+
const newSession = createSession(sessionOptions, { previousSessionId: session.id });
49+
50+
return newSession;
1751
}
1852

1953
/**
20-
* Get or create a session
54+
* Get or create a session, when initializing the replay.
55+
* Returns a session that may be unsampled.
2156
*/
22-
export function getSession({
23-
timeouts,
24-
currentSession,
25-
stickySession,
26-
sessionSampleRate,
27-
allowBuffering,
28-
traceInternals,
29-
}: GetSessionParams): { type: 'new' | 'saved'; session: Session } {
57+
export function loadOrCreateSession(
58+
currentSession: Session | undefined,
59+
{
60+
timeouts,
61+
traceInternals,
62+
}: {
63+
timeouts: Timeouts;
64+
traceInternals?: boolean;
65+
},
66+
sessionOptions: SessionOptions,
67+
): Session {
3068
// If session exists and is passed, use it instead of always hitting session storage
31-
const session = currentSession || (stickySession && fetchSession(traceInternals));
32-
33-
if (session) {
34-
// If there is a session, check if it is valid (e.g. "last activity" time
35-
// should be within the "session idle time", and "session started" time is
36-
// within "max session time").
37-
const isExpired = isSessionExpired(session, timeouts);
38-
39-
if (!isExpired || (allowBuffering && session.shouldRefresh)) {
40-
return { type: 'saved', session };
41-
} else if (!session.shouldRefresh) {
42-
// This is the case if we have an error session that is completed
43-
// (=triggered an error). Session will continue as session-based replay,
44-
// and when this session is expired, it will not be renewed until user
45-
// reloads.
46-
const discardedSession = makeSession({ sampled: false });
47-
logInfoNextTick('[Replay] Session should not be refreshed', traceInternals);
48-
return { type: 'new', session: discardedSession };
49-
} else {
50-
logInfoNextTick('[Replay] Session has expired', traceInternals);
51-
}
52-
// Otherwise continue to create a new session
69+
const existingSession = currentSession || (sessionOptions.stickySession && fetchSession(traceInternals));
70+
71+
// No session exists yet, just create a new one
72+
if (!existingSession) {
73+
logInfoNextTick('[Replay] Created new session', traceInternals);
74+
return createSession(sessionOptions);
5375
}
5476

55-
const newSession = createSession({
56-
stickySession,
57-
sessionSampleRate,
58-
allowBuffering,
59-
});
60-
logInfoNextTick('[Replay] Created new session', traceInternals);
77+
// If a session exists, and it is not expired, just return it
78+
if (!isSessionExpired(existingSession, timeouts)) {
79+
return existingSession;
80+
}
81+
82+
// If expired & we have a buffering session that should be refreshed, return it
83+
if (existingSession.sampled === 'buffer' && existingSession.shouldRefresh) {
84+
return existingSession;
85+
}
86+
87+
// If expired & we have a buffering session that should _not_ be refreshed, return a new unsampled session
88+
if (existingSession.sampled === 'buffer') {
89+
logInfoNextTick('[Replay] Session should not be refreshed', traceInternals);
90+
return makeSession({ sampled: false });
91+
}
92+
93+
// Else, we have an expired session that should be refreshed & re-sampled
94+
logInfoNextTick('[Replay] Session has expired, creating new one...', traceInternals);
95+
96+
const newSession = createSession(sessionOptions, { previousSessionId: existingSession.id });
6197

62-
return { type: 'new', session: newSession };
98+
return newSession;
6399
}

0 commit comments

Comments
 (0)