diff --git a/packages/browser-integration-tests/suites/replay/bufferModeReload/init.js b/packages/browser-integration-tests/suites/replay/bufferModeReload/init.js
new file mode 100644
index 000000000000..89c185dacc7f
--- /dev/null
+++ b/packages/browser-integration-tests/suites/replay/bufferModeReload/init.js
@@ -0,0 +1,17 @@
+import * as Sentry from '@sentry/browser';
+
+window.Sentry = Sentry;
+window.Replay = new Sentry.Replay({
+ flushMinDelay: 200,
+ flushMaxDelay: 200,
+ minReplayDuration: 0,
+});
+
+Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ sampleRate: 1,
+ replaysSessionSampleRate: 0.0,
+ replaysOnErrorSampleRate: 1.0,
+
+ integrations: [window.Replay],
+});
diff --git a/packages/browser-integration-tests/suites/replay/bufferModeReload/template.html b/packages/browser-integration-tests/suites/replay/bufferModeReload/template.html
new file mode 100644
index 000000000000..084254db29e1
--- /dev/null
+++ b/packages/browser-integration-tests/suites/replay/bufferModeReload/template.html
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/browser-integration-tests/suites/replay/bufferModeReload/test.ts b/packages/browser-integration-tests/suites/replay/bufferModeReload/test.ts
new file mode 100644
index 000000000000..95e2bf399592
--- /dev/null
+++ b/packages/browser-integration-tests/suites/replay/bufferModeReload/test.ts
@@ -0,0 +1,51 @@
+import { expect } from '@playwright/test';
+
+import { sentryTest } from '../../../utils/fixtures';
+import {
+ getReplaySnapshot,
+ shouldSkipReplayTest,
+ waitForReplayRequest,
+ waitForReplayRunning,
+} from '../../../utils/replayHelpers';
+
+sentryTest('continues buffer session in session mode after error & reload', async ({ getLocalTestPath, page }) => {
+ if (shouldSkipReplayTest()) {
+ sentryTest.skip();
+ }
+
+ const reqPromise1 = waitForReplayRequest(page, 0);
+
+ await page.route('https://dsn.ingest.sentry.io/**/*', route => {
+ return route.fulfill({
+ status: 200,
+ contentType: 'application/json',
+ body: JSON.stringify({ id: 'test-id' }),
+ });
+ });
+
+ const url = await getLocalTestPath({ testDir: __dirname });
+
+ await page.goto(url);
+
+ // buffer session captures an error & switches to session mode
+ await page.click('#buttonError');
+ await new Promise(resolve => setTimeout(resolve, 300));
+ await reqPromise1;
+
+ await waitForReplayRunning(page);
+ const replay1 = await getReplaySnapshot(page);
+
+ expect(replay1.recordingMode).toEqual('session');
+ expect(replay1.session?.sampled).toEqual('buffer');
+ expect(replay1.session?.segmentId).toBeGreaterThan(0);
+
+ // Reload to ensure the session is correctly recovered from sessionStorage
+ await page.reload();
+
+ await waitForReplayRunning(page);
+ const replay2 = await getReplaySnapshot(page);
+
+ expect(replay2.recordingMode).toEqual('session');
+ expect(replay2.session?.sampled).toEqual('buffer');
+ expect(replay2.session?.segmentId).toBeGreaterThan(0);
+});
diff --git a/packages/replay/src/replay.ts b/packages/replay/src/replay.ts
index 6bba4eb66a0a..eec3edf45083 100644
--- a/packages/replay/src/replay.ts
+++ b/packages/replay/src/replay.ts
@@ -18,7 +18,8 @@ import { handleKeyboardEvent } from './coreHandlers/handleKeyboardEvent';
import { setupPerformanceObserver } from './coreHandlers/performanceObserver';
import { createEventBuffer } from './eventBuffer';
import { clearSession } from './session/clearSession';
-import { getSession } from './session/getSession';
+import { loadOrCreateSession } from './session/loadOrCreateSession';
+import { maybeRefreshSession } from './session/maybeRefreshSession';
import { saveSession } from './session/saveSession';
import type {
AddEventResult,
@@ -228,13 +229,7 @@ export class ReplayContainer implements ReplayContainerInterface {
// Otherwise if there is _any_ sample rate set, try to load an existing
// session, or create a new one.
- const isSessionSampled = this._loadAndCheckSession();
-
- if (!isSessionSampled) {
- // This should only occur if `errorSampleRate` is 0 and was unsampled for
- // session-based replay. In this case there is nothing to do.
- return;
- }
+ this._initializeSessionForSampling();
if (!this.session) {
// This should not happen, something wrong has occurred
@@ -242,14 +237,16 @@ export class ReplayContainer implements ReplayContainerInterface {
return;
}
- if (this.session.sampled && this.session.sampled !== 'session') {
- // If not sampled as session-based, then recording mode will be `buffer`
- // Note that we don't explicitly check if `sampled === 'buffer'` because we
- // could have sessions from Session storage that are still `error` from
- // prior SDK version.
- this.recordingMode = 'buffer';
+ if (this.session.sampled === false) {
+ // This should only occur if `errorSampleRate` is 0 and was unsampled for
+ // session-based replay. In this case there is nothing to do.
+ return;
}
+ // If segmentId > 0, it means we've previously already captured this session
+ // In this case, we still want to continue in `session` recording mode
+ this.recordingMode = this.session.sampled === 'buffer' && this.session.segmentId === 0 ? 'buffer' : 'session';
+
logInfoNextTick(
`[Replay] Starting replay in ${this.recordingMode} mode`,
this._options._experiments.traceInternals,
@@ -276,19 +273,20 @@ export class ReplayContainer implements ReplayContainerInterface {
logInfoNextTick('[Replay] Starting replay in session mode', this._options._experiments.traceInternals);
- const previousSessionId = this.session && this.session.id;
-
- const { session } = getSession({
- timeouts: this.timeouts,
- stickySession: Boolean(this._options.stickySession),
- currentSession: this.session,
- // This is intentional: create a new session-based replay when calling `start()`
- sessionSampleRate: 1,
- allowBuffering: false,
- traceInternals: this._options._experiments.traceInternals,
- });
+ const session = loadOrCreateSession(
+ this.session,
+ {
+ timeouts: this.timeouts,
+ traceInternals: this._options._experiments.traceInternals,
+ },
+ {
+ stickySession: this._options.stickySession,
+ // This is intentional: create a new session-based replay when calling `start()`
+ sessionSampleRate: 1,
+ allowBuffering: false,
+ },
+ );
- session.previousSessionId = previousSessionId;
this.session = session;
this._initializeRecording();
@@ -305,18 +303,19 @@ export class ReplayContainer implements ReplayContainerInterface {
logInfoNextTick('[Replay] Starting replay in buffer mode', this._options._experiments.traceInternals);
- const previousSessionId = this.session && this.session.id;
-
- const { session } = getSession({
- timeouts: this.timeouts,
- stickySession: Boolean(this._options.stickySession),
- currentSession: this.session,
- sessionSampleRate: 0,
- allowBuffering: true,
- traceInternals: this._options._experiments.traceInternals,
- });
+ const session = loadOrCreateSession(
+ this.session,
+ {
+ timeouts: this.timeouts,
+ traceInternals: this._options._experiments.traceInternals,
+ },
+ {
+ stickySession: this._options.stickySession,
+ sessionSampleRate: 0,
+ allowBuffering: true,
+ },
+ );
- session.previousSessionId = previousSessionId;
this.session = session;
this.recordingMode = 'buffer';
@@ -427,7 +426,7 @@ export class ReplayContainer implements ReplayContainerInterface {
* new DOM checkout.`
*/
public resume(): void {
- if (!this._isPaused || !this._loadAndCheckSession()) {
+ if (!this._isPaused || !this._checkSession()) {
return;
}
@@ -535,7 +534,7 @@ export class ReplayContainer implements ReplayContainerInterface {
if (!this._stopRecording) {
// Create a new session, otherwise when the user action is flushed, it
// will get rejected due to an expired session.
- if (!this._loadAndCheckSession()) {
+ if (!this._checkSession()) {
return;
}
@@ -634,7 +633,7 @@ export class ReplayContainer implements ReplayContainerInterface {
// --- There is recent user activity --- //
// This will create a new session if expired, based on expiry length
- if (!this._loadAndCheckSession()) {
+ if (!this._checkSession()) {
return;
}
@@ -751,31 +750,63 @@ export class ReplayContainer implements ReplayContainerInterface {
/**
* Loads (or refreshes) the current session.
+ */
+ private _initializeSessionForSampling(): void {
+ // Whenever there is _any_ error sample rate, we always allow buffering
+ // Because we decide on sampling when an error occurs, we need to buffer at all times if sampling for errors
+ const allowBuffering = this._options.errorSampleRate > 0;
+
+ const session = loadOrCreateSession(
+ this.session,
+ {
+ timeouts: this.timeouts,
+ traceInternals: this._options._experiments.traceInternals,
+ },
+ {
+ stickySession: this._options.stickySession,
+ sessionSampleRate: this._options.sessionSampleRate,
+ allowBuffering,
+ },
+ );
+
+ this.session = session;
+ }
+
+ /**
+ * Checks and potentially refreshes the current session.
* Returns false if session is not recorded.
*/
- private _loadAndCheckSession(): boolean {
- const { type, session } = getSession({
- timeouts: this.timeouts,
- stickySession: Boolean(this._options.stickySession),
- currentSession: this.session,
- sessionSampleRate: this._options.sessionSampleRate,
- allowBuffering: this._options.errorSampleRate > 0 || this.recordingMode === 'buffer',
- traceInternals: this._options._experiments.traceInternals,
- });
+ private _checkSession(): boolean {
+ // If there is no session yet, we do not want to refresh anything
+ // This should generally not happen, but to be safe....
+ if (!this.session) {
+ return false;
+ }
+
+ const currentSession = this.session;
+
+ const newSession = maybeRefreshSession(
+ currentSession,
+ {
+ timeouts: this.timeouts,
+ traceInternals: this._options._experiments.traceInternals,
+ },
+ {
+ stickySession: Boolean(this._options.stickySession),
+ sessionSampleRate: this._options.sessionSampleRate,
+ allowBuffering: this._options.errorSampleRate > 0,
+ },
+ );
+
+ const isNew = newSession.id !== currentSession.id;
// If session was newly created (i.e. was not loaded from storage), then
// enable flag to create the root replay
- if (type === 'new') {
+ if (isNew) {
this.setInitialState();
+ this.session = newSession;
}
- const currentSessionId = this.getSessionId();
- if (session.id !== currentSessionId) {
- session.previousSessionId = currentSessionId;
- }
-
- this.session = session;
-
if (!this.session.sampled) {
void this.stop({ reason: 'session not refreshed' });
return false;
diff --git a/packages/replay/src/session/Session.ts b/packages/replay/src/session/Session.ts
index e373d50dfaa2..80b32aed345a 100644
--- a/packages/replay/src/session/Session.ts
+++ b/packages/replay/src/session/Session.ts
@@ -14,6 +14,7 @@ export function makeSession(session: Partial & { sampled: Sampled }): S
const segmentId = session.segmentId || 0;
const sampled = session.sampled;
const shouldRefresh = typeof session.shouldRefresh === 'boolean' ? session.shouldRefresh : true;
+ const previousSessionId = session.previousSessionId;
return {
id,
@@ -22,5 +23,6 @@ export function makeSession(session: Partial & { sampled: Sampled }): S
segmentId,
sampled,
shouldRefresh,
+ previousSessionId,
};
}
diff --git a/packages/replay/src/session/createSession.ts b/packages/replay/src/session/createSession.ts
index 0ecd940a4dae..2cb9c0853b09 100644
--- a/packages/replay/src/session/createSession.ts
+++ b/packages/replay/src/session/createSession.ts
@@ -15,10 +15,14 @@ export function getSessionSampleType(sessionSampleRate: number, allowBuffering:
* that all replays will be saved to as attachments. Currently, we only expect
* one of these Sentry events per "replay session".
*/
-export function createSession({ sessionSampleRate, allowBuffering, stickySession = false }: SessionOptions): Session {
+export function createSession(
+ { sessionSampleRate, allowBuffering, stickySession = false }: SessionOptions,
+ { previousSessionId }: { previousSessionId?: string } = {},
+): Session {
const sampled = getSessionSampleType(sessionSampleRate, allowBuffering);
const session = makeSession({
sampled,
+ previousSessionId,
});
if (stickySession) {
diff --git a/packages/replay/src/session/getSession.ts b/packages/replay/src/session/getSession.ts
deleted file mode 100644
index da3184f05296..000000000000
--- a/packages/replay/src/session/getSession.ts
+++ /dev/null
@@ -1,63 +0,0 @@
-import type { Session, SessionOptions, Timeouts } from '../types';
-import { isSessionExpired } from '../util/isSessionExpired';
-import { logInfoNextTick } from '../util/log';
-import { createSession } from './createSession';
-import { fetchSession } from './fetchSession';
-import { makeSession } from './Session';
-
-interface GetSessionParams extends SessionOptions {
- timeouts: Timeouts;
-
- /**
- * The current session (e.g. if stickySession is off)
- */
- currentSession?: Session;
-
- traceInternals?: boolean;
-}
-
-/**
- * Get or create a session
- */
-export function getSession({
- timeouts,
- currentSession,
- stickySession,
- sessionSampleRate,
- allowBuffering,
- traceInternals,
-}: GetSessionParams): { type: 'new' | 'saved'; session: Session } {
- // If session exists and is passed, use it instead of always hitting session storage
- const session = currentSession || (stickySession && fetchSession(traceInternals));
-
- if (session) {
- // If there is a session, check if it is valid (e.g. "last activity" time
- // should be within the "session idle time", and "session started" time is
- // within "max session time").
- const isExpired = isSessionExpired(session, timeouts);
-
- if (!isExpired || (allowBuffering && session.shouldRefresh)) {
- return { type: 'saved', session };
- } else if (!session.shouldRefresh) {
- // This is the case if we have an error session that is completed
- // (=triggered an error). Session will continue as session-based replay,
- // and when this session is expired, it will not be renewed until user
- // reloads.
- const discardedSession = makeSession({ sampled: false });
- logInfoNextTick('[Replay] Session should not be refreshed', traceInternals);
- return { type: 'new', session: discardedSession };
- } else {
- logInfoNextTick('[Replay] Session has expired', traceInternals);
- }
- // Otherwise continue to create a new session
- }
-
- const newSession = createSession({
- stickySession,
- sessionSampleRate,
- allowBuffering,
- });
- logInfoNextTick('[Replay] Created new session', traceInternals);
-
- return { type: 'new', session: newSession };
-}
diff --git a/packages/replay/src/session/loadOrCreateSession.ts b/packages/replay/src/session/loadOrCreateSession.ts
new file mode 100644
index 000000000000..9695eef56102
--- /dev/null
+++ b/packages/replay/src/session/loadOrCreateSession.ts
@@ -0,0 +1,32 @@
+import type { Session, SessionOptions, Timeouts } from '../types';
+import { logInfoNextTick } from '../util/log';
+import { createSession } from './createSession';
+import { fetchSession } from './fetchSession';
+import { maybeRefreshSession } from './maybeRefreshSession';
+
+/**
+ * Get or create a session, when initializing the replay.
+ * Returns a session that may be unsampled.
+ */
+export function loadOrCreateSession(
+ currentSession: Session | undefined,
+ {
+ timeouts,
+ traceInternals,
+ }: {
+ timeouts: Timeouts;
+ traceInternals?: boolean;
+ },
+ sessionOptions: SessionOptions,
+): Session {
+ // If session exists and is passed, use it instead of always hitting session storage
+ const existingSession = currentSession || (sessionOptions.stickySession && fetchSession(traceInternals));
+
+ // No session exists yet, just create a new one
+ if (!existingSession) {
+ logInfoNextTick('[Replay] Created new session', traceInternals);
+ return createSession(sessionOptions);
+ }
+
+ return maybeRefreshSession(existingSession, { timeouts, traceInternals }, sessionOptions);
+}
diff --git a/packages/replay/src/session/maybeRefreshSession.ts b/packages/replay/src/session/maybeRefreshSession.ts
new file mode 100644
index 000000000000..51e4925d074d
--- /dev/null
+++ b/packages/replay/src/session/maybeRefreshSession.ts
@@ -0,0 +1,48 @@
+import type { Session, SessionOptions, Timeouts } from '../types';
+import { isSessionExpired } from '../util/isSessionExpired';
+import { logInfoNextTick } from '../util/log';
+import { createSession } from './createSession';
+import { makeSession } from './Session';
+
+/**
+ * Check a session, and either return it or a refreshed version of it.
+ * The refreshed version may be unsampled.
+ * You can check if the session has changed by comparing the session IDs.
+ */
+export function maybeRefreshSession(
+ session: Session,
+ {
+ timeouts,
+ traceInternals,
+ }: {
+ timeouts: Timeouts;
+ traceInternals?: boolean;
+ },
+ sessionOptions: SessionOptions,
+): Session {
+ // If not expired, all good, just keep the session
+ if (!isSessionExpired(session, timeouts)) {
+ return session;
+ }
+
+ const isBuffering = session.sampled === 'buffer';
+
+ // If we are buffering & the session may be refreshed, just return it
+ if (isBuffering && session.shouldRefresh) {
+ return session;
+ }
+
+ // If we are buffering & the session may not be refreshed (=it was converted to session previously already)
+ // We return an unsampled new session
+ if (isBuffering) {
+ logInfoNextTick('[Replay] Session should not be refreshed', traceInternals);
+ return makeSession({ sampled: false });
+ }
+
+ // Else, we are not buffering, and the session is expired, so we need to create a new one
+ logInfoNextTick('[Replay] Session has expired, creating new one...', traceInternals);
+
+ const newSession = createSession(sessionOptions, { previousSessionId: session.id });
+
+ return newSession;
+}
diff --git a/packages/replay/test/integration/beforeAddRecordingEvent.test.ts b/packages/replay/test/integration/beforeAddRecordingEvent.test.ts
index 6bf33f182e66..4059b71fe195 100644
--- a/packages/replay/test/integration/beforeAddRecordingEvent.test.ts
+++ b/packages/replay/test/integration/beforeAddRecordingEvent.test.ts
@@ -84,7 +84,8 @@ describe('Integration | beforeAddRecordingEvent', () => {
// Create a new session and clear mocks because a segment (from initial
// checkout) will have already been uploaded by the time the tests run
clearSession(replay);
- replay['_loadAndCheckSession']();
+ replay['_initializeSessionForSampling']();
+ replay.setInitialState();
mockSendReplayRequest.mockClear();
});
@@ -94,7 +95,6 @@ describe('Integration | beforeAddRecordingEvent', () => {
await new Promise(process.nextTick);
jest.setSystemTime(new Date(BASE_TIMESTAMP));
clearSession(replay);
- replay['_loadAndCheckSession']();
});
afterAll(() => {
diff --git a/packages/replay/test/integration/errorSampleRate.test.ts b/packages/replay/test/integration/errorSampleRate.test.ts
index 777cb437f7e3..e56edae0f723 100644
--- a/packages/replay/test/integration/errorSampleRate.test.ts
+++ b/packages/replay/test/integration/errorSampleRate.test.ts
@@ -295,10 +295,11 @@ describe('Integration | errorSampleRate', () => {
it('does not upload a replay event if error is not sampled', async () => {
// We are trying to replicate the case where error rate is 0 and session
// rate is > 0, we can't set them both to 0 otherwise
- // `_loadAndCheckSession` is not called when initializing the plugin.
+ // `_initializeSessionForSampling` is not called when initializing the plugin.
replay.stop();
replay['_options']['errorSampleRate'] = 0;
- replay['_loadAndCheckSession']();
+ replay['_initializeSessionForSampling']();
+ replay.setInitialState();
jest.runAllTimers();
await new Promise(process.nextTick);
diff --git a/packages/replay/test/integration/events.test.ts b/packages/replay/test/integration/events.test.ts
index b95faffa59da..c90f8ceed125 100644
--- a/packages/replay/test/integration/events.test.ts
+++ b/packages/replay/test/integration/events.test.ts
@@ -40,7 +40,8 @@ describe('Integration | events', () => {
// Create a new session and clear mocks because a segment (from initial
// checkout) will have already been uploaded by the time the tests run
clearSession(replay);
- replay['_loadAndCheckSession']();
+ replay['_initializeSessionForSampling']();
+ replay.setInitialState();
mockTransportSend.mockClear();
});
@@ -93,7 +94,8 @@ describe('Integration | events', () => {
it('has correct timestamps when there are events earlier than initial timestamp', async function () {
clearSession(replay);
- replay['_loadAndCheckSession']();
+ replay['_initializeSessionForSampling']();
+ replay.setInitialState();
mockTransportSend.mockClear();
Object.defineProperty(document, 'visibilityState', {
configurable: true,
diff --git a/packages/replay/test/integration/flush.test.ts b/packages/replay/test/integration/flush.test.ts
index 29ce2ba527fd..6f2d3b7d8ccd 100644
--- a/packages/replay/test/integration/flush.test.ts
+++ b/packages/replay/test/integration/flush.test.ts
@@ -85,7 +85,8 @@ describe('Integration | flush', () => {
sessionStorage.clear();
clearSession(replay);
- replay['_loadAndCheckSession']();
+ replay['_initializeSessionForSampling']();
+ replay.setInitialState();
if (replay.eventBuffer) {
jest.spyOn(replay.eventBuffer, 'finish');
@@ -276,7 +277,8 @@ describe('Integration | flush', () => {
sessionStorage.clear();
clearSession(replay);
- replay['_loadAndCheckSession']();
+ replay['_initializeSessionForSampling']();
+ replay.setInitialState();
// click happens first
domHandler({
@@ -307,10 +309,12 @@ describe('Integration | flush', () => {
sessionStorage.clear();
clearSession(replay);
- replay['_loadAndCheckSession']();
- // No-op _loadAndCheckSession to avoid us resetting the session for this test
- const _tmp = replay['_loadAndCheckSession'];
- replay['_loadAndCheckSession'] = () => {
+ replay['_initializeSessionForSampling']();
+ replay.setInitialState();
+
+ // No-op _checkSession to avoid us resetting the session for this test
+ const _tmp = replay['_checkSession'];
+ replay['_checkSession'] = () => {
return true;
};
@@ -331,7 +335,7 @@ describe('Integration | flush', () => {
expect(mockSendReplay).toHaveBeenCalledTimes(0);
replay.timeouts.maxSessionLife = MAX_SESSION_LIFE;
- replay['_loadAndCheckSession'] = _tmp;
+ replay['_checkSession'] = _tmp;
});
it('logs warning if flushing initial segment without checkout', async () => {
@@ -339,7 +343,8 @@ describe('Integration | flush', () => {
sessionStorage.clear();
clearSession(replay);
- replay['_loadAndCheckSession']();
+ replay['_initializeSessionForSampling']();
+ replay.setInitialState();
await new Promise(process.nextTick);
jest.setSystemTime(BASE_TIMESTAMP);
@@ -399,7 +404,8 @@ describe('Integration | flush', () => {
sessionStorage.clear();
clearSession(replay);
- replay['_loadAndCheckSession']();
+ replay['_initializeSessionForSampling']();
+ replay.setInitialState();
await new Promise(process.nextTick);
jest.setSystemTime(BASE_TIMESTAMP);
@@ -454,7 +460,8 @@ describe('Integration | flush', () => {
sessionStorage.clear();
clearSession(replay);
- replay['_loadAndCheckSession']();
+ replay['_initializeSessionForSampling']();
+ replay.setInitialState();
await new Promise(process.nextTick);
jest.setSystemTime(BASE_TIMESTAMP);
diff --git a/packages/replay/test/integration/rateLimiting.test.ts b/packages/replay/test/integration/rateLimiting.test.ts
index 723dc682d100..291a95c4f94e 100644
--- a/packages/replay/test/integration/rateLimiting.test.ts
+++ b/packages/replay/test/integration/rateLimiting.test.ts
@@ -46,7 +46,8 @@ describe('Integration | rate-limiting behaviour', () => {
// Create a new session and clear mocks because a segment (from initial
// checkout) will have already been uploaded by the time the tests run
clearSession(replay);
- replay['_loadAndCheckSession']();
+ replay['_initializeSessionForSampling']();
+ replay.setInitialState();
mockSendReplayRequest.mockClear();
});
@@ -57,7 +58,6 @@ describe('Integration | rate-limiting behaviour', () => {
jest.setSystemTime(new Date(BASE_TIMESTAMP));
clearSession(replay);
jest.clearAllMocks();
- replay['_loadAndCheckSession']();
});
afterAll(() => {
diff --git a/packages/replay/test/integration/sendReplayEvent.test.ts b/packages/replay/test/integration/sendReplayEvent.test.ts
index d7a9974bcaa9..d6f26db6653c 100644
--- a/packages/replay/test/integration/sendReplayEvent.test.ts
+++ b/packages/replay/test/integration/sendReplayEvent.test.ts
@@ -59,7 +59,8 @@ describe('Integration | sendReplayEvent', () => {
// Create a new session and clear mocks because a segment (from initial
// checkout) will have already been uploaded by the time the tests run
clearSession(replay);
- replay['_loadAndCheckSession']();
+ replay['_initializeSessionForSampling']();
+ replay.setInitialState();
mockSendReplayRequest.mockClear();
});
@@ -69,7 +70,6 @@ describe('Integration | sendReplayEvent', () => {
await new Promise(process.nextTick);
jest.setSystemTime(new Date(BASE_TIMESTAMP));
clearSession(replay);
- replay['_loadAndCheckSession']();
});
afterAll(() => {
diff --git a/packages/replay/test/integration/session.test.ts b/packages/replay/test/integration/session.test.ts
index 304059659078..6e62b71ca09c 100644
--- a/packages/replay/test/integration/session.test.ts
+++ b/packages/replay/test/integration/session.test.ts
@@ -424,7 +424,8 @@ describe('Integration | session', () => {
it('increases segment id after each event', async () => {
clearSession(replay);
- replay['_loadAndCheckSession']();
+ replay['_initializeSessionForSampling']();
+ replay.setInitialState();
Object.defineProperty(document, 'visibilityState', {
configurable: true,
diff --git a/packages/replay/test/integration/stop.test.ts b/packages/replay/test/integration/stop.test.ts
index cc0e28195244..a88c5de6a839 100644
--- a/packages/replay/test/integration/stop.test.ts
+++ b/packages/replay/test/integration/stop.test.ts
@@ -52,7 +52,8 @@ describe('Integration | stop', () => {
jest.setSystemTime(new Date(BASE_TIMESTAMP));
sessionStorage.clear();
clearSession(replay);
- replay['_loadAndCheckSession']();
+ replay['_initializeSessionForSampling']();
+ replay.setInitialState();
mockRecord.takeFullSnapshot.mockClear();
mockAddInstrumentationHandler.mockClear();
Object.defineProperty(WINDOW, 'location', {
diff --git a/packages/replay/test/unit/session/getSession.test.ts b/packages/replay/test/unit/session/getSession.test.ts
deleted file mode 100644
index aa3110d114f2..000000000000
--- a/packages/replay/test/unit/session/getSession.test.ts
+++ /dev/null
@@ -1,291 +0,0 @@
-import {
- MAX_SESSION_LIFE,
- SESSION_IDLE_EXPIRE_DURATION,
- SESSION_IDLE_PAUSE_DURATION,
- WINDOW,
-} from '../../../src/constants';
-import * as CreateSession from '../../../src/session/createSession';
-import * as FetchSession from '../../../src/session/fetchSession';
-import { getSession } from '../../../src/session/getSession';
-import { saveSession } from '../../../src/session/saveSession';
-import { makeSession } from '../../../src/session/Session';
-
-jest.mock('@sentry/utils', () => {
- return {
- ...(jest.requireActual('@sentry/utils') as { string: unknown }),
- uuid4: jest.fn(() => 'test_session_uuid'),
- };
-});
-
-const SAMPLE_OPTIONS = {
- sessionSampleRate: 1.0,
- allowBuffering: false,
-};
-
-function createMockSession(when: number = Date.now()) {
- return makeSession({
- id: 'test_session_id',
- segmentId: 0,
- lastActivity: when,
- started: when,
- sampled: 'session',
- shouldRefresh: true,
- });
-}
-
-describe('Unit | session | getSession', () => {
- beforeAll(() => {
- jest.spyOn(CreateSession, 'createSession');
- jest.spyOn(FetchSession, 'fetchSession');
- WINDOW.sessionStorage.clear();
- });
-
- afterEach(() => {
- WINDOW.sessionStorage.clear();
- (CreateSession.createSession as jest.MockedFunction).mockClear();
- (FetchSession.fetchSession as jest.MockedFunction).mockClear();
- });
-
- it('creates a non-sticky session when one does not exist', function () {
- const { session } = getSession({
- timeouts: {
- sessionIdlePause: SESSION_IDLE_PAUSE_DURATION,
- sessionIdleExpire: SESSION_IDLE_EXPIRE_DURATION,
- maxSessionLife: MAX_SESSION_LIFE,
- },
- stickySession: false,
- ...SAMPLE_OPTIONS,
- });
-
- expect(FetchSession.fetchSession).not.toHaveBeenCalled();
- expect(CreateSession.createSession).toHaveBeenCalled();
-
- expect(session).toEqual({
- id: 'test_session_uuid',
- segmentId: 0,
- lastActivity: expect.any(Number),
- sampled: 'session',
- started: expect.any(Number),
- shouldRefresh: true,
- });
-
- // Should not have anything in storage
- expect(FetchSession.fetchSession()).toBe(null);
- });
-
- it('creates a non-sticky session, regardless of session existing in sessionStorage', function () {
- saveSession(createMockSession(Date.now() - 10000));
-
- const { session } = getSession({
- timeouts: {
- sessionIdlePause: SESSION_IDLE_PAUSE_DURATION,
- sessionIdleExpire: 1000,
- maxSessionLife: MAX_SESSION_LIFE,
- },
- stickySession: false,
- ...SAMPLE_OPTIONS,
- });
-
- expect(FetchSession.fetchSession).not.toHaveBeenCalled();
- expect(CreateSession.createSession).toHaveBeenCalled();
-
- expect(session).toBeDefined();
- });
-
- it('creates a non-sticky session, when one is expired', function () {
- const { session } = getSession({
- timeouts: {
- sessionIdlePause: SESSION_IDLE_PAUSE_DURATION,
- sessionIdleExpire: 1000,
- maxSessionLife: MAX_SESSION_LIFE,
- },
- stickySession: false,
- ...SAMPLE_OPTIONS,
- currentSession: makeSession({
- id: 'old_session_id',
- lastActivity: Date.now() - 1001,
- started: Date.now() - 1001,
- segmentId: 0,
- sampled: 'session',
- }),
- });
-
- expect(FetchSession.fetchSession).not.toHaveBeenCalled();
- expect(CreateSession.createSession).toHaveBeenCalled();
-
- expect(session).toBeDefined();
- expect(session.id).not.toBe('old_session_id');
- });
-
- it('creates a sticky session when one does not exist', function () {
- expect(FetchSession.fetchSession()).toBe(null);
-
- const { session } = getSession({
- timeouts: {
- sessionIdlePause: SESSION_IDLE_PAUSE_DURATION,
- sessionIdleExpire: SESSION_IDLE_EXPIRE_DURATION,
- maxSessionLife: MAX_SESSION_LIFE,
- },
- stickySession: true,
- sessionSampleRate: 1.0,
- allowBuffering: false,
- });
-
- expect(FetchSession.fetchSession).toHaveBeenCalled();
- expect(CreateSession.createSession).toHaveBeenCalled();
-
- expect(session).toEqual({
- id: 'test_session_uuid',
- segmentId: 0,
- lastActivity: expect.any(Number),
- sampled: 'session',
- started: expect.any(Number),
- shouldRefresh: true,
- });
-
- // Should not have anything in storage
- expect(FetchSession.fetchSession()).toEqual({
- id: 'test_session_uuid',
- segmentId: 0,
- lastActivity: expect.any(Number),
- sampled: 'session',
- started: expect.any(Number),
- shouldRefresh: true,
- });
- });
-
- it('fetches an existing sticky session', function () {
- const now = Date.now();
- saveSession(createMockSession(now));
-
- const { session } = getSession({
- timeouts: {
- sessionIdlePause: SESSION_IDLE_PAUSE_DURATION,
- sessionIdleExpire: 1000,
- maxSessionLife: MAX_SESSION_LIFE,
- },
- stickySession: true,
- sessionSampleRate: 1.0,
- allowBuffering: false,
- });
-
- expect(FetchSession.fetchSession).toHaveBeenCalled();
- expect(CreateSession.createSession).not.toHaveBeenCalled();
-
- expect(session).toEqual({
- id: 'test_session_id',
- segmentId: 0,
- lastActivity: now,
- sampled: 'session',
- started: now,
- shouldRefresh: true,
- });
- });
-
- it('fetches an expired sticky session', function () {
- const now = Date.now();
- saveSession(createMockSession(Date.now() - 2000));
-
- const { session } = getSession({
- timeouts: {
- sessionIdlePause: SESSION_IDLE_PAUSE_DURATION,
- sessionIdleExpire: 1000,
- maxSessionLife: MAX_SESSION_LIFE,
- },
- stickySession: true,
- ...SAMPLE_OPTIONS,
- });
-
- expect(FetchSession.fetchSession).toHaveBeenCalled();
- expect(CreateSession.createSession).toHaveBeenCalled();
-
- expect(session.id).toBe('test_session_uuid');
- expect(session.lastActivity).toBeGreaterThanOrEqual(now);
- expect(session.started).toBeGreaterThanOrEqual(now);
- expect(session.segmentId).toBe(0);
- });
-
- it('fetches a non-expired non-sticky session', function () {
- const { session } = getSession({
- timeouts: {
- sessionIdlePause: SESSION_IDLE_PAUSE_DURATION,
- sessionIdleExpire: 1000,
- maxSessionLife: MAX_SESSION_LIFE,
- },
- stickySession: false,
- ...SAMPLE_OPTIONS,
- currentSession: makeSession({
- id: 'test_session_uuid_2',
- lastActivity: +new Date() - 500,
- started: +new Date() - 500,
- segmentId: 0,
- sampled: 'session',
- }),
- });
-
- expect(FetchSession.fetchSession).not.toHaveBeenCalled();
- expect(CreateSession.createSession).not.toHaveBeenCalled();
-
- expect(session.id).toBe('test_session_uuid_2');
- expect(session.segmentId).toBe(0);
- });
-
- it('re-uses the same "buffer" session if it is expired and has never sent a buffered replay', function () {
- const { type, session } = getSession({
- timeouts: {
- sessionIdlePause: SESSION_IDLE_PAUSE_DURATION,
- sessionIdleExpire: 1000,
- maxSessionLife: MAX_SESSION_LIFE,
- },
- stickySession: false,
- ...SAMPLE_OPTIONS,
- currentSession: makeSession({
- id: 'test_session_uuid_2',
- lastActivity: +new Date() - MAX_SESSION_LIFE - 1,
- started: +new Date() - MAX_SESSION_LIFE - 1,
- segmentId: 0,
- sampled: 'buffer',
- }),
- allowBuffering: true,
- });
-
- expect(FetchSession.fetchSession).not.toHaveBeenCalled();
- expect(CreateSession.createSession).not.toHaveBeenCalled();
-
- expect(type).toBe('saved');
- expect(session.id).toBe('test_session_uuid_2');
- expect(session.sampled).toBe('buffer');
- expect(session.segmentId).toBe(0);
- });
-
- it('creates a new session if it is expired and it was a "buffer" session that has sent a replay', function () {
- const currentSession = makeSession({
- id: 'test_session_uuid_2',
- lastActivity: +new Date() - MAX_SESSION_LIFE - 1,
- started: +new Date() - MAX_SESSION_LIFE - 1,
- segmentId: 0,
- sampled: 'buffer',
- });
- currentSession.shouldRefresh = false;
-
- const { type, session } = getSession({
- timeouts: {
- sessionIdlePause: SESSION_IDLE_PAUSE_DURATION,
- sessionIdleExpire: 1000,
- maxSessionLife: MAX_SESSION_LIFE,
- },
- stickySession: false,
- ...SAMPLE_OPTIONS,
- currentSession,
- allowBuffering: true,
- });
-
- expect(FetchSession.fetchSession).not.toHaveBeenCalled();
- expect(CreateSession.createSession).not.toHaveBeenCalled();
-
- expect(type).toBe('new');
- expect(session.id).not.toBe('test_session_uuid_2');
- expect(session.sampled).toBe(false);
- expect(session.segmentId).toBe(0);
- });
-});
diff --git a/packages/replay/test/unit/session/loadOrCreateSession.test.ts b/packages/replay/test/unit/session/loadOrCreateSession.test.ts
new file mode 100644
index 000000000000..907e078c75d3
--- /dev/null
+++ b/packages/replay/test/unit/session/loadOrCreateSession.test.ts
@@ -0,0 +1,396 @@
+import {
+ MAX_SESSION_LIFE,
+ SESSION_IDLE_EXPIRE_DURATION,
+ SESSION_IDLE_PAUSE_DURATION,
+ WINDOW,
+} from '../../../src/constants';
+import * as CreateSession from '../../../src/session/createSession';
+import * as FetchSession from '../../../src/session/fetchSession';
+import { loadOrCreateSession } from '../../../src/session/loadOrCreateSession';
+import { saveSession } from '../../../src/session/saveSession';
+import { makeSession } from '../../../src/session/Session';
+import type { SessionOptions, Timeouts } from '../../../src/types';
+
+jest.mock('@sentry/utils', () => {
+ return {
+ ...(jest.requireActual('@sentry/utils') as { string: unknown }),
+ uuid4: jest.fn(() => 'test_session_uuid'),
+ };
+});
+
+const SAMPLE_OPTIONS: SessionOptions = {
+ stickySession: false,
+ sessionSampleRate: 1.0,
+ allowBuffering: false,
+};
+
+const timeouts: Timeouts = {
+ sessionIdlePause: SESSION_IDLE_PAUSE_DURATION,
+ sessionIdleExpire: SESSION_IDLE_EXPIRE_DURATION,
+ maxSessionLife: MAX_SESSION_LIFE,
+};
+
+function createMockSession(when: number = Date.now(), id = 'test_session_id') {
+ return makeSession({
+ id,
+ segmentId: 0,
+ lastActivity: when,
+ started: when,
+ sampled: 'session',
+ shouldRefresh: true,
+ });
+}
+
+describe('Unit | session | loadOrCreateSession', () => {
+ beforeAll(() => {
+ jest.spyOn(CreateSession, 'createSession');
+ jest.spyOn(FetchSession, 'fetchSession');
+ WINDOW.sessionStorage.clear();
+ });
+
+ afterEach(() => {
+ WINDOW.sessionStorage.clear();
+ (CreateSession.createSession as jest.MockedFunction).mockClear();
+ (FetchSession.fetchSession as jest.MockedFunction).mockClear();
+ });
+
+ describe('stickySession: false', () => {
+ it('creates new session if none is passed in', function () {
+ const session = loadOrCreateSession(
+ undefined,
+ {
+ timeouts,
+ },
+ {
+ ...SAMPLE_OPTIONS,
+ stickySession: false,
+ },
+ );
+
+ expect(FetchSession.fetchSession).not.toHaveBeenCalled();
+ expect(CreateSession.createSession).toHaveBeenCalled();
+
+ expect(session).toEqual({
+ id: 'test_session_uuid',
+ segmentId: 0,
+ lastActivity: expect.any(Number),
+ sampled: 'session',
+ started: expect.any(Number),
+ shouldRefresh: true,
+ });
+
+ // Should not have anything in storage
+ expect(FetchSession.fetchSession()).toBe(null);
+ });
+
+ it('creates new session, even if something is in sessionStorage', function () {
+ const sessionInStorage = createMockSession(Date.now() - 10000, 'test_old_session_uuid');
+ saveSession(sessionInStorage);
+
+ const session = loadOrCreateSession(
+ undefined,
+ {
+ timeouts: { ...timeouts, sessionIdleExpire: 1000 },
+ },
+ {
+ ...SAMPLE_OPTIONS,
+ stickySession: false,
+ },
+ );
+
+ expect(FetchSession.fetchSession).not.toHaveBeenCalled();
+ expect(CreateSession.createSession).toHaveBeenCalled();
+
+ expect(session).toEqual({
+ id: 'test_session_uuid',
+ segmentId: 0,
+ lastActivity: expect.any(Number),
+ sampled: 'session',
+ started: expect.any(Number),
+ shouldRefresh: true,
+ });
+
+ // Should not have anything in storage
+ expect(FetchSession.fetchSession()).toEqual(sessionInStorage);
+ });
+
+ it('uses passed in session', function () {
+ const now = Date.now();
+ const currentSession = createMockSession(now - 2000);
+
+ const session = loadOrCreateSession(
+ currentSession,
+ {
+ timeouts,
+ },
+ {
+ ...SAMPLE_OPTIONS,
+ stickySession: false,
+ },
+ );
+
+ expect(FetchSession.fetchSession).not.toHaveBeenCalled();
+ expect(CreateSession.createSession).not.toHaveBeenCalled();
+
+ expect(session).toEqual(currentSession);
+ });
+ });
+
+ describe('stickySession: true', () => {
+ it('creates new session if none exists', function () {
+ const session = loadOrCreateSession(
+ undefined,
+ {
+ timeouts,
+ },
+ {
+ ...SAMPLE_OPTIONS,
+ stickySession: true,
+ },
+ );
+
+ expect(FetchSession.fetchSession).toHaveBeenCalled();
+ expect(CreateSession.createSession).toHaveBeenCalled();
+
+ const expectedSession = {
+ id: 'test_session_uuid',
+ segmentId: 0,
+ lastActivity: expect.any(Number),
+ sampled: 'session',
+ started: expect.any(Number),
+ shouldRefresh: true,
+ };
+ expect(session).toEqual(expectedSession);
+
+ // Should also be stored in storage
+ expect(FetchSession.fetchSession()).toEqual(expectedSession);
+ });
+
+ it('creates new session if session in sessionStorage is expired', function () {
+ const now = Date.now();
+ const date = now - 2000;
+ saveSession(createMockSession(date, 'test_old_session_uuid'));
+
+ const session = loadOrCreateSession(
+ undefined,
+ {
+ timeouts: { ...timeouts, sessionIdleExpire: 1000 },
+ },
+ {
+ ...SAMPLE_OPTIONS,
+ stickySession: true,
+ },
+ );
+
+ expect(FetchSession.fetchSession).toHaveBeenCalled();
+ expect(CreateSession.createSession).toHaveBeenCalled();
+
+ const expectedSession = {
+ id: 'test_session_uuid',
+ segmentId: 0,
+ lastActivity: expect.any(Number),
+ sampled: 'session',
+ started: expect.any(Number),
+ shouldRefresh: true,
+ previousSessionId: 'test_old_session_uuid',
+ };
+ expect(session).toEqual(expectedSession);
+ expect(session.lastActivity).toBeGreaterThanOrEqual(now);
+ expect(session.started).toBeGreaterThanOrEqual(now);
+ expect(FetchSession.fetchSession()).toEqual(expectedSession);
+ });
+
+ it('returns session from sessionStorage if not expired', function () {
+ const date = Date.now() - 2000;
+ saveSession(createMockSession(date, 'test_old_session_uuid'));
+
+ const session = loadOrCreateSession(
+ undefined,
+ {
+ timeouts: { ...timeouts, sessionIdleExpire: 5000 },
+ },
+ {
+ ...SAMPLE_OPTIONS,
+ stickySession: true,
+ },
+ );
+
+ expect(FetchSession.fetchSession).toHaveBeenCalled();
+ expect(CreateSession.createSession).not.toHaveBeenCalled();
+
+ expect(session).toEqual({
+ id: 'test_old_session_uuid',
+ segmentId: 0,
+ lastActivity: date,
+ sampled: 'session',
+ started: date,
+ shouldRefresh: true,
+ });
+ });
+
+ it('uses passed in session instead of fetching from sessionStorage', function () {
+ const now = Date.now();
+ saveSession(createMockSession(now - 10000, 'test_storage_session_uuid'));
+ const currentSession = createMockSession(now - 2000);
+
+ const session = loadOrCreateSession(
+ currentSession,
+ {
+ timeouts,
+ },
+ {
+ ...SAMPLE_OPTIONS,
+ stickySession: true,
+ },
+ );
+
+ expect(FetchSession.fetchSession).not.toHaveBeenCalled();
+ expect(CreateSession.createSession).not.toHaveBeenCalled();
+
+ expect(session).toEqual(currentSession);
+ });
+ });
+
+ describe('buffering', () => {
+ it('returns current session when buffering, even if expired', function () {
+ const now = Date.now();
+ const currentSession = makeSession({
+ id: 'test_session_uuid_2',
+ lastActivity: now - 2000,
+ started: now - 2000,
+ segmentId: 0,
+ sampled: 'buffer',
+ shouldRefresh: true,
+ });
+
+ const session = loadOrCreateSession(
+ currentSession,
+ {
+ timeouts: { ...timeouts, sessionIdleExpire: 1000 },
+ },
+ {
+ ...SAMPLE_OPTIONS,
+ },
+ );
+
+ expect(FetchSession.fetchSession).not.toHaveBeenCalled();
+ expect(CreateSession.createSession).not.toHaveBeenCalled();
+
+ expect(session).toEqual(currentSession);
+ });
+
+ it('returns new unsampled session when buffering & expired, if shouldRefresh===false', function () {
+ const now = Date.now();
+ const currentSession = makeSession({
+ id: 'test_session_uuid_2',
+ lastActivity: now - 2000,
+ started: now - 2000,
+ segmentId: 0,
+ sampled: 'buffer',
+ shouldRefresh: false,
+ });
+
+ const session = loadOrCreateSession(
+ currentSession,
+ {
+ timeouts: { ...timeouts, sessionIdleExpire: 1000 },
+ },
+ {
+ ...SAMPLE_OPTIONS,
+ },
+ );
+
+ expect(FetchSession.fetchSession).not.toHaveBeenCalled();
+ expect(CreateSession.createSession).not.toHaveBeenCalled();
+
+ expect(session).not.toEqual(currentSession);
+ expect(session.sampled).toBe(false);
+ expect(session.started).toBeGreaterThanOrEqual(now);
+ });
+
+ it('returns existing session when buffering & not expired, if shouldRefresh===false', function () {
+ const now = Date.now();
+ const currentSession = makeSession({
+ id: 'test_session_uuid_2',
+ lastActivity: now - 2000,
+ started: now - 2000,
+ segmentId: 0,
+ sampled: 'buffer',
+ shouldRefresh: false,
+ });
+
+ const session = loadOrCreateSession(
+ currentSession,
+ {
+ timeouts: { ...timeouts, sessionIdleExpire: 5000 },
+ },
+ {
+ ...SAMPLE_OPTIONS,
+ },
+ );
+
+ expect(FetchSession.fetchSession).not.toHaveBeenCalled();
+ expect(CreateSession.createSession).not.toHaveBeenCalled();
+
+ expect(session).toEqual(currentSession);
+ });
+ });
+
+ describe('sampling', () => {
+ it('returns unsampled session if sample rates are 0', function () {
+ const session = loadOrCreateSession(
+ undefined,
+ {
+ timeouts,
+ },
+ {
+ ...SAMPLE_OPTIONS,
+ sessionSampleRate: 0,
+ allowBuffering: false,
+ },
+ );
+
+ const expectedSession = {
+ id: 'test_session_uuid',
+ segmentId: 0,
+ lastActivity: expect.any(Number),
+ sampled: false,
+ started: expect.any(Number),
+ shouldRefresh: true,
+ };
+ expect(session).toEqual(expectedSession);
+ });
+
+ it('returns `session` session if sessionSampleRate===1', function () {
+ const session = loadOrCreateSession(
+ undefined,
+ {
+ timeouts,
+ },
+ {
+ ...SAMPLE_OPTIONS,
+ sessionSampleRate: 1.0,
+ allowBuffering: false,
+ },
+ );
+
+ expect(session.sampled).toBe('session');
+ });
+
+ it('returns `buffer` session if allowBuffering===true', function () {
+ const session = loadOrCreateSession(
+ undefined,
+ {
+ timeouts,
+ },
+ {
+ ...SAMPLE_OPTIONS,
+ sessionSampleRate: 0.0,
+ allowBuffering: true,
+ },
+ );
+
+ expect(session.sampled).toBe('buffer');
+ });
+ });
+});
diff --git a/packages/replay/test/unit/session/maybeRefreshSession.test.ts b/packages/replay/test/unit/session/maybeRefreshSession.test.ts
new file mode 100644
index 000000000000..5bcc8bf4481c
--- /dev/null
+++ b/packages/replay/test/unit/session/maybeRefreshSession.test.ts
@@ -0,0 +1,266 @@
+import {
+ MAX_SESSION_LIFE,
+ SESSION_IDLE_EXPIRE_DURATION,
+ SESSION_IDLE_PAUSE_DURATION,
+ WINDOW,
+} from '../../../src/constants';
+import * as CreateSession from '../../../src/session/createSession';
+import { maybeRefreshSession } from '../../../src/session/maybeRefreshSession';
+import { makeSession } from '../../../src/session/Session';
+import type { SessionOptions, Timeouts } from '../../../src/types';
+
+jest.mock('@sentry/utils', () => {
+ return {
+ ...(jest.requireActual('@sentry/utils') as { string: unknown }),
+ uuid4: jest.fn(() => 'test_session_uuid'),
+ };
+});
+
+const SAMPLE_OPTIONS: SessionOptions = {
+ stickySession: false,
+ sessionSampleRate: 1.0,
+ allowBuffering: false,
+};
+
+const timeouts: Timeouts = {
+ sessionIdlePause: SESSION_IDLE_PAUSE_DURATION,
+ sessionIdleExpire: SESSION_IDLE_EXPIRE_DURATION,
+ maxSessionLife: MAX_SESSION_LIFE,
+};
+
+function createMockSession(when: number = Date.now(), id = 'test_session_id') {
+ return makeSession({
+ id,
+ segmentId: 0,
+ lastActivity: when,
+ started: when,
+ sampled: 'session',
+ shouldRefresh: true,
+ });
+}
+
+describe('Unit | session | maybeRefreshSession', () => {
+ beforeAll(() => {
+ jest.spyOn(CreateSession, 'createSession');
+ });
+
+ afterEach(() => {
+ WINDOW.sessionStorage.clear();
+ (CreateSession.createSession as jest.MockedFunction).mockClear();
+ });
+
+ it('returns session if not expired', function () {
+ const now = Date.now();
+ const currentSession = createMockSession(now - 2000);
+
+ const session = maybeRefreshSession(
+ currentSession,
+ {
+ timeouts,
+ },
+ {
+ ...SAMPLE_OPTIONS,
+ },
+ );
+
+ expect(CreateSession.createSession).not.toHaveBeenCalled();
+
+ expect(session).toEqual(currentSession);
+ });
+
+ it('creates new session if expired', function () {
+ const now = Date.now();
+ const currentSession = createMockSession(now - 2000, 'test_old_session_uuid');
+
+ const session = maybeRefreshSession(
+ currentSession,
+ {
+ timeouts: { ...timeouts, sessionIdleExpire: 1000 },
+ },
+ {
+ ...SAMPLE_OPTIONS,
+ },
+ );
+
+ expect(CreateSession.createSession).toHaveBeenCalled();
+
+ expect(session).not.toEqual(currentSession);
+ const expectedSession = {
+ id: 'test_session_uuid',
+ segmentId: 0,
+ lastActivity: expect.any(Number),
+ sampled: 'session',
+ started: expect.any(Number),
+ shouldRefresh: true,
+ previousSessionId: 'test_old_session_uuid',
+ };
+ expect(session).toEqual(expectedSession);
+ expect(session.lastActivity).toBeGreaterThanOrEqual(now);
+ expect(session.started).toBeGreaterThanOrEqual(now);
+ });
+
+ describe('buffering', () => {
+ it('returns session when buffering, even if expired', function () {
+ const now = Date.now();
+ const currentSession = makeSession({
+ id: 'test_session_uuid_2',
+ lastActivity: now - 2000,
+ started: now - 2000,
+ segmentId: 0,
+ sampled: 'buffer',
+ shouldRefresh: true,
+ });
+
+ const session = maybeRefreshSession(
+ currentSession,
+ {
+ timeouts: { ...timeouts, sessionIdleExpire: 1000 },
+ },
+ {
+ ...SAMPLE_OPTIONS,
+ },
+ );
+
+ expect(CreateSession.createSession).not.toHaveBeenCalled();
+
+ expect(session).toEqual(currentSession);
+ });
+
+ it('returns new unsampled session when buffering & expired, if shouldRefresh===false', function () {
+ const now = Date.now();
+ const currentSession = makeSession({
+ id: 'test_session_uuid_2',
+ lastActivity: now - 2000,
+ started: now - 2000,
+ segmentId: 0,
+ sampled: 'buffer',
+ shouldRefresh: false,
+ });
+
+ const session = maybeRefreshSession(
+ currentSession,
+ {
+ timeouts: { ...timeouts, sessionIdleExpire: 1000 },
+ },
+ {
+ ...SAMPLE_OPTIONS,
+ },
+ );
+
+ expect(CreateSession.createSession).not.toHaveBeenCalled();
+
+ expect(session).not.toEqual(currentSession);
+ expect(session.sampled).toBe(false);
+ expect(session.started).toBeGreaterThanOrEqual(now);
+ });
+
+ it('returns existing session when buffering & not expired, if shouldRefresh===false', function () {
+ const now = Date.now();
+ const currentSession = makeSession({
+ id: 'test_session_uuid_2',
+ lastActivity: now - 2000,
+ started: now - 2000,
+ segmentId: 0,
+ sampled: 'buffer',
+ shouldRefresh: false,
+ });
+
+ const session = maybeRefreshSession(
+ currentSession,
+ {
+ timeouts: { ...timeouts, sessionIdleExpire: 5000 },
+ },
+ {
+ ...SAMPLE_OPTIONS,
+ },
+ );
+
+ expect(CreateSession.createSession).not.toHaveBeenCalled();
+
+ expect(session).toEqual(currentSession);
+ });
+ });
+
+ describe('sampling', () => {
+ it('creates unsampled session if sample rates are 0', function () {
+ const now = Date.now();
+ const currentSession = makeSession({
+ id: 'test_session_uuid_2',
+ lastActivity: now - 2000,
+ started: now - 2000,
+ segmentId: 0,
+ sampled: 'session',
+ shouldRefresh: true,
+ });
+
+ const session = maybeRefreshSession(
+ currentSession,
+ {
+ timeouts: { ...timeouts, sessionIdleExpire: 1000 },
+ },
+ {
+ ...SAMPLE_OPTIONS,
+ sessionSampleRate: 0,
+ allowBuffering: false,
+ },
+ );
+
+ expect(session.id).toBe('test_session_uuid');
+ expect(session.sampled).toBe(false);
+ });
+
+ it('creates `session` session if sessionSampleRate===1', function () {
+ const now = Date.now();
+ const currentSession = makeSession({
+ id: 'test_session_uuid_2',
+ lastActivity: now - 2000,
+ started: now - 2000,
+ segmentId: 0,
+ sampled: 'session',
+ shouldRefresh: true,
+ });
+
+ const session = maybeRefreshSession(
+ currentSession,
+ {
+ timeouts: { ...timeouts, sessionIdleExpire: 1000 },
+ },
+ {
+ ...SAMPLE_OPTIONS,
+ sessionSampleRate: 1.0,
+ allowBuffering: false,
+ },
+ );
+
+ expect(session.id).toBe('test_session_uuid');
+ expect(session.sampled).toBe('session');
+ });
+
+ it('creates `buffer` session if allowBuffering===true', function () {
+ const now = Date.now();
+ const currentSession = makeSession({
+ id: 'test_session_uuid_2',
+ lastActivity: now - 2000,
+ started: now - 2000,
+ segmentId: 0,
+ sampled: 'session',
+ shouldRefresh: true,
+ });
+
+ const session = maybeRefreshSession(
+ currentSession,
+ {
+ timeouts: { ...timeouts, sessionIdleExpire: 1000 },
+ },
+ {
+ ...SAMPLE_OPTIONS,
+ sessionSampleRate: 0.0,
+ allowBuffering: true,
+ },
+ );
+
+ expect(session.id).toBe('test_session_uuid');
+ expect(session.sampled).toBe('buffer');
+ });
+ });
+});
diff --git a/packages/replay/test/utils/setupReplayContainer.ts b/packages/replay/test/utils/setupReplayContainer.ts
index 02a965b7d9c2..cb70c85bbe54 100644
--- a/packages/replay/test/utils/setupReplayContainer.ts
+++ b/packages/replay/test/utils/setupReplayContainer.ts
@@ -42,8 +42,8 @@ export function setupReplayContainer({
});
clearSession(replay);
+ replay['_initializeSessionForSampling']();
replay.setInitialState();
- replay['_loadAndCheckSession']();
replay['_isEnabled'] = true;
replay.eventBuffer = createEventBuffer({
useCompression: options?.useCompression || false,