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,