diff --git a/packages/replay/src/replay.ts b/packages/replay/src/replay.ts index 72a9dd88d7aa..aa500382cbc3 100644 --- a/packages/replay/src/replay.ts +++ b/packages/replay/src/replay.ts @@ -18,8 +18,8 @@ import { setupPerformanceObserver } from './coreHandlers/performanceObserver'; import { createEventBuffer } from './eventBuffer'; import { clearSession } from './session/clearSession'; import { loadOrCreateSession } from './session/loadOrCreateSession'; -import { maybeRefreshSession } from './session/maybeRefreshSession'; import { saveSession } from './session/saveSession'; +import { shouldRefreshSession } from './session/shouldRefreshSession'; import type { AddEventResult, AddUpdateCallback, @@ -217,7 +217,7 @@ export class ReplayContainer implements ReplayContainerInterface { * Initializes the plugin based on sampling configuration. Should not be * called outside of constructor. */ - public initializeSampling(): void { + public initializeSampling(previousSessionId?: string): void { const { errorSampleRate, sessionSampleRate } = this._options; // If neither sample rate is > 0, then do nothing - user will need to call one of @@ -228,7 +228,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. - this._initializeSessionForSampling(); + this._initializeSessionForSampling(previousSessionId); if (!this.session) { // This should not happen, something wrong has occurred @@ -273,7 +273,6 @@ export class ReplayContainer implements ReplayContainerInterface { logInfoNextTick('[Replay] Starting replay in session mode', this._options._experiments.traceInternals); const session = loadOrCreateSession( - this.session, { maxReplayDuration: this._options.maxReplayDuration, sessionIdleExpire: this.timeouts.sessionIdleExpire, @@ -304,7 +303,6 @@ export class ReplayContainer implements ReplayContainerInterface { logInfoNextTick('[Replay] Starting replay in buffer mode', this._options._experiments.traceInternals); const session = loadOrCreateSession( - this.session, { sessionIdleExpire: this.timeouts.sessionIdleExpire, maxReplayDuration: this._options.maxReplayDuration, @@ -373,15 +371,16 @@ export class ReplayContainer implements ReplayContainerInterface { return; } + // We can't move `_isEnabled` after awaiting a flush, otherwise we can + // enter into an infinite loop when `stop()` is called while flushing. + this._isEnabled = false; + try { logInfo( `[Replay] Stopping Replay${reason ? ` triggered by ${reason}` : ''}`, this._options._experiments.traceInternals, ); - // We can't move `_isEnabled` after awaiting a flush, otherwise we can - // enter into an infinite loop when `stop()` is called while flushing. - this._isEnabled = false; this._removeListeners(); this.stopRecording(); @@ -475,16 +474,6 @@ export class ReplayContainer implements ReplayContainerInterface { // Once this session ends, we do not want to refresh it if (this.session) { - this.session.shouldRefresh = false; - - // It's possible that the session lifespan is > max session lifespan - // because we have been buffering beyond max session lifespan (we ignore - // expiration given that `shouldRefresh` is true). Since we flip - // `shouldRefresh`, the session could be considered expired due to - // lifespan, which is not what we want. Update session start date to be - // the current timestamp, so that session is not considered to be - // expired. This means that max replay duration can be MAX_REPLAY_DURATION + - // (length of buffer), which we are ok with. this._updateUserActivity(activityTime); this._updateSessionActivity(activityTime); this._maybeSaveSession(); @@ -612,8 +601,6 @@ export class ReplayContainer implements ReplayContainerInterface { * @hidden */ public checkAndHandleExpiredSession(): boolean | void { - const oldSessionId = this.getSessionId(); - // Prevent starting a new session if the last user activity is older than // SESSION_IDLE_PAUSE_DURATION. Otherwise non-user activity can trigger a new // session+recording. This creates noisy replays that do not have much @@ -635,24 +622,11 @@ export class ReplayContainer implements ReplayContainerInterface { // --- There is recent user activity --- // // This will create a new session if expired, based on expiry length if (!this._checkSession()) { - return; - } - - // Session was expired if session ids do not match - const expired = oldSessionId !== this.getSessionId(); - - if (!expired) { - return true; - } - - // Session is expired, trigger a full snapshot (which will create a new session) - if (this.isPaused()) { - this.resume(); - } else { - this._triggerFullSnapshot(); + // Check session handles the refreshing itself + return false; } - return false; + return true; } /** @@ -740,6 +714,7 @@ export class ReplayContainer implements ReplayContainerInterface { // Need to set as enabled before we start recording, as `record()` can trigger a flush with a new checkout this._isEnabled = true; + this._isPaused = false; this.startRecording(); } @@ -756,17 +731,17 @@ export class ReplayContainer implements ReplayContainerInterface { /** * Loads (or refreshes) the current session. */ - private _initializeSessionForSampling(): void { + private _initializeSessionForSampling(previousSessionId?: string): 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, { sessionIdleExpire: this.timeouts.sessionIdleExpire, maxReplayDuration: this._options.maxReplayDuration, traceInternals: this._options._experiments.traceInternals, + previousSessionId, }, { stickySession: this._options.stickySession, @@ -791,37 +766,32 @@ export class ReplayContainer implements ReplayContainerInterface { const currentSession = this.session; - const newSession = maybeRefreshSession( - currentSession, - { + if ( + shouldRefreshSession(currentSession, { sessionIdleExpire: this.timeouts.sessionIdleExpire, - traceInternals: this._options._experiments.traceInternals, maxReplayDuration: this._options.maxReplayDuration, - }, - { - 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 (isNew) { - this.setInitialState(); - this.session = newSession; - } - - if (!this.session.sampled) { - void this.stop({ reason: 'session not refreshed' }); + }) + ) { + void this._refreshSession(currentSession); return false; } return true; } + /** + * Refresh a session with a new one. + * This stops the current session (without forcing a flush, as that would never work since we are expired), + * and then does a new sampling based on the refreshed session. + */ + private async _refreshSession(session: Session): Promise { + if (!this._isEnabled) { + return; + } + await this.stop({ reason: 'refresh session' }); + this.initializeSampling(session.id); + } + /** * Adds listeners to record events for the replay */ @@ -933,10 +903,14 @@ export class ReplayContainer implements ReplayContainerInterface { const expired = isSessionExpired(this.session, { maxReplayDuration: this._options.maxReplayDuration, - ...this.timeouts, + sessionIdleExpire: this.timeouts.sessionIdleExpire, }); - if (breadcrumb && !expired) { + if (expired) { + return; + } + + if (breadcrumb) { this._createCustomBreadcrumb(breadcrumb); } @@ -1081,7 +1055,9 @@ export class ReplayContainer implements ReplayContainerInterface { * Should never be called directly, only by `flush` */ private async _runFlush(): Promise { - if (!this.session || !this.eventBuffer) { + const replayId = this.getSessionId(); + + if (!this.session || !this.eventBuffer || !replayId) { __DEBUG_BUILD__ && logger.error('[Replay] No session or eventBuffer found to flush.'); return; } @@ -1101,13 +1077,15 @@ export class ReplayContainer implements ReplayContainerInterface { return; } + // if this changed in the meanwhile, e.g. because the session was refreshed or similar, we abort here + if (replayId !== this.getSessionId()) { + return; + } + try { // This uses the data from the eventBuffer, so we need to call this before `finish() this._updateInitialTimestampFromEventBuffer(); - // Note this empties the event buffer regardless of outcome of sending replay - const recordingData = await this.eventBuffer.finish(); - const timestamp = Date.now(); // Check total duration again, to avoid sending outdated stuff @@ -1117,14 +1095,14 @@ export class ReplayContainer implements ReplayContainerInterface { throw new Error('Session is too long, not sending replay'); } - // NOTE: Copy values from instance members, as it's possible they could - // change before the flush finishes. - const replayId = this.session.id; const eventContext = this._popEventContext(); // Always increment segmentId regardless of outcome of sending replay const segmentId = this.session.segmentId++; this._maybeSaveSession(); + // Note this empties the event buffer regardless of outcome of sending replay + const recordingData = await this.eventBuffer.finish(); + await sendReplay({ replayId, recordingData, diff --git a/packages/replay/src/session/Session.ts b/packages/replay/src/session/Session.ts index 80b32aed345a..be2bcdac1506 100644 --- a/packages/replay/src/session/Session.ts +++ b/packages/replay/src/session/Session.ts @@ -13,7 +13,6 @@ export function makeSession(session: Partial & { sampled: Sampled }): S const lastActivity = session.lastActivity || now; const segmentId = session.segmentId || 0; const sampled = session.sampled; - const shouldRefresh = typeof session.shouldRefresh === 'boolean' ? session.shouldRefresh : true; const previousSessionId = session.previousSessionId; return { @@ -22,7 +21,6 @@ export function makeSession(session: Partial & { sampled: Sampled }): S lastActivity, segmentId, sampled, - shouldRefresh, previousSessionId, }; } diff --git a/packages/replay/src/session/loadOrCreateSession.ts b/packages/replay/src/session/loadOrCreateSession.ts index 0766c537a4d2..1e1ac7664d40 100644 --- a/packages/replay/src/session/loadOrCreateSession.ts +++ b/packages/replay/src/session/loadOrCreateSession.ts @@ -2,33 +2,38 @@ import type { Session, SessionOptions } from '../types'; import { logInfoNextTick } from '../util/log'; import { createSession } from './createSession'; import { fetchSession } from './fetchSession'; -import { maybeRefreshSession } from './maybeRefreshSession'; +import { shouldRefreshSession } from './shouldRefreshSession'; /** * Get or create a session, when initializing the replay. * Returns a session that may be unsampled. */ export function loadOrCreateSession( - currentSession: Session | undefined, { traceInternals, sessionIdleExpire, maxReplayDuration, + previousSessionId, }: { sessionIdleExpire: number; maxReplayDuration: number; traceInternals?: boolean; + previousSessionId?: string; }, sessionOptions: SessionOptions, ): Session { - // If session exists and is passed, use it instead of always hitting session storage - const existingSession = currentSession || (sessionOptions.stickySession && fetchSession(traceInternals)); + const existingSession = sessionOptions.stickySession && fetchSession(traceInternals); // No session exists yet, just create a new one if (!existingSession) { - logInfoNextTick('[Replay] Created new session', traceInternals); - return createSession(sessionOptions); + logInfoNextTick('[Replay] Creating new session', traceInternals); + return createSession(sessionOptions, { previousSessionId }); } - return maybeRefreshSession(existingSession, { sessionIdleExpire, traceInternals, maxReplayDuration }, sessionOptions); + if (!shouldRefreshSession(existingSession, { sessionIdleExpire, maxReplayDuration })) { + return existingSession; + } + + logInfoNextTick('[Replay] Session in sessionStorage is expired, creating new one...'); + return createSession(sessionOptions, { previousSessionId: existingSession.id }); } diff --git a/packages/replay/src/session/maybeRefreshSession.ts b/packages/replay/src/session/maybeRefreshSession.ts deleted file mode 100644 index 14bc7f9534fa..000000000000 --- a/packages/replay/src/session/maybeRefreshSession.ts +++ /dev/null @@ -1,50 +0,0 @@ -import type { Session, SessionOptions } 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, - { - traceInternals, - maxReplayDuration, - sessionIdleExpire, - }: { - sessionIdleExpire: number; - maxReplayDuration: number; - traceInternals?: boolean; - }, - sessionOptions: SessionOptions, -): Session { - // If not expired, all good, just keep the session - if (!isSessionExpired(session, { sessionIdleExpire, maxReplayDuration })) { - 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/src/session/shouldRefreshSession.ts b/packages/replay/src/session/shouldRefreshSession.ts new file mode 100644 index 000000000000..0b37574cd3db --- /dev/null +++ b/packages/replay/src/session/shouldRefreshSession.ts @@ -0,0 +1,20 @@ +import type { Session } from '../types'; +import { isSessionExpired } from '../util/isSessionExpired'; + +/** If the session should be refreshed or not. */ +export function shouldRefreshSession( + session: Session, + { sessionIdleExpire, maxReplayDuration }: { sessionIdleExpire: number; maxReplayDuration: number }, +): boolean { + // If not expired, all good, just keep the session + if (!isSessionExpired(session, { sessionIdleExpire, maxReplayDuration })) { + return false; + } + + // If we are buffering & haven't ever flushed yet, always continue + if (session.sampled === 'buffer' && session.segmentId === 0) { + return false; + } + + return true; +} diff --git a/packages/replay/src/types/replay.ts b/packages/replay/src/types/replay.ts index b5c2318008a0..8ad157606c53 100644 --- a/packages/replay/src/types/replay.ts +++ b/packages/replay/src/types/replay.ts @@ -373,12 +373,6 @@ export interface Session { * Is the session sampled? `false` if not sampled, otherwise, `session` or `buffer` */ sampled: Sampled; - - /** - * If this is false, the session should not be refreshed when it was inactive. - * This can be the case if you had a buffered session which is now recording because an error happened. - */ - shouldRefresh: boolean; } export type EventBufferType = 'sync' | 'worker'; diff --git a/packages/replay/test/integration/errorSampleRate.test.ts b/packages/replay/test/integration/errorSampleRate.test.ts index a2dc7d9db2f4..ad3543ec810a 100644 --- a/packages/replay/test/integration/errorSampleRate.test.ts +++ b/packages/replay/test/integration/errorSampleRate.test.ts @@ -37,405 +37,400 @@ async function waitForFlush() { } describe('Integration | errorSampleRate', () => { - let replay: ReplayContainer; - let mockRecord: RecordMock; - let domHandler: DomHandler; - - beforeEach(async () => { - ({ mockRecord, domHandler, replay } = await resetSdkMock({ - replayOptions: { - stickySession: true, - }, - sentryOptions: { - replaysSessionSampleRate: 0.0, - replaysOnErrorSampleRate: 1.0, - }, - })); - }); + describe('basic', () => { + let replay: ReplayContainer; + let mockRecord: RecordMock; + let domHandler: DomHandler; + + beforeEach(async () => { + ({ mockRecord, domHandler, replay } = await resetSdkMock({ + replayOptions: { + stickySession: true, + }, + sentryOptions: { + replaysSessionSampleRate: 0.0, + replaysOnErrorSampleRate: 1.0, + }, + })); + }); - afterEach(async () => { - clearSession(replay); - replay.stop(); - }); + afterEach(async () => { + clearSession(replay); + replay.stop(); + }); - it('uploads a replay when `Sentry.captureException` is called and continues recording', async () => { - const TEST_EVENT = getTestEventIncremental({ timestamp: BASE_TIMESTAMP }); - mockRecord._emitter(TEST_EVENT); - const optionsEvent = createOptionsEvent(replay); + it('uploads a replay when `Sentry.captureException` is called and continues recording', async () => { + const TEST_EVENT = getTestEventIncremental({ timestamp: BASE_TIMESTAMP }); + mockRecord._emitter(TEST_EVENT); + const optionsEvent = createOptionsEvent(replay); - expect(mockRecord.takeFullSnapshot).not.toHaveBeenCalled(); - expect(replay).not.toHaveLastSentReplay(); + expect(mockRecord.takeFullSnapshot).not.toHaveBeenCalled(); + expect(replay).not.toHaveLastSentReplay(); - // Does not capture on mouse click - domHandler({ - name: 'click', - }); - jest.runAllTimers(); - await new Promise(process.nextTick); - expect(replay).not.toHaveLastSentReplay(); + // Does not capture on mouse click + domHandler({ + name: 'click', + }); + jest.runAllTimers(); + await new Promise(process.nextTick); + expect(replay).not.toHaveLastSentReplay(); - captureException(new Error('testing')); + captureException(new Error('testing')); - await waitForBufferFlush(); + await waitForBufferFlush(); - expect(replay).toHaveLastSentReplay({ - recordingPayloadHeader: { segment_id: 0 }, - replayEventPayload: expect.objectContaining({ - replay_type: 'buffer', - }), - recordingData: JSON.stringify([ - { data: { isCheckout: true }, timestamp: BASE_TIMESTAMP, type: 2 }, - optionsEvent, - TEST_EVENT, - { - type: 5, - timestamp: BASE_TIMESTAMP, - data: { - tag: 'breadcrumb', - payload: { - timestamp: BASE_TIMESTAMP / 1000, - type: 'default', - category: 'ui.click', - message: '', - data: {}, + expect(replay).toHaveLastSentReplay({ + recordingPayloadHeader: { segment_id: 0 }, + replayEventPayload: expect.objectContaining({ + replay_type: 'buffer', + }), + recordingData: JSON.stringify([ + { data: { isCheckout: true }, timestamp: BASE_TIMESTAMP, type: 2 }, + optionsEvent, + TEST_EVENT, + { + type: 5, + timestamp: BASE_TIMESTAMP, + data: { + tag: 'breadcrumb', + payload: { + timestamp: BASE_TIMESTAMP / 1000, + type: 'default', + category: 'ui.click', + message: '', + data: {}, + }, }, }, - }, - ]), - }); + ]), + }); - await waitForFlush(); + await waitForFlush(); - // This is from when we stop recording and start a session recording - expect(replay).toHaveLastSentReplay({ - recordingPayloadHeader: { segment_id: 1 }, - replayEventPayload: expect.objectContaining({ - replay_type: 'buffer', - }), - recordingData: JSON.stringify([{ data: { isCheckout: true }, timestamp: BASE_TIMESTAMP + 40, type: 2 }]), - }); + // This is from when we stop recording and start a session recording + expect(replay).toHaveLastSentReplay({ + recordingPayloadHeader: { segment_id: 1 }, + replayEventPayload: expect.objectContaining({ + replay_type: 'buffer', + }), + recordingData: JSON.stringify([{ data: { isCheckout: true }, timestamp: BASE_TIMESTAMP + 40, type: 2 }]), + }); - jest.advanceTimersByTime(DEFAULT_FLUSH_MIN_DELAY); + jest.advanceTimersByTime(DEFAULT_FLUSH_MIN_DELAY); - // Check that click will get captured - domHandler({ - name: 'click', - }); + // Check that click will get captured + domHandler({ + name: 'click', + }); - await waitForFlush(); + await waitForFlush(); - expect(replay).toHaveLastSentReplay({ - recordingData: JSON.stringify([ - { - type: 5, - timestamp: BASE_TIMESTAMP + 10000 + 80, - data: { - tag: 'breadcrumb', - payload: { - timestamp: (BASE_TIMESTAMP + 10000 + 80) / 1000, - type: 'default', - category: 'ui.click', - message: '', - data: {}, + expect(replay).toHaveLastSentReplay({ + recordingData: JSON.stringify([ + { + type: 5, + timestamp: BASE_TIMESTAMP + 10000 + 80, + data: { + tag: 'breadcrumb', + payload: { + timestamp: (BASE_TIMESTAMP + 10000 + 80) / 1000, + type: 'default', + category: 'ui.click', + message: '', + data: {}, + }, }, }, - }, - ]), + ]), + }); }); - }); - it('manually flushes replay and does not continue to record', async () => { - const TEST_EVENT = getTestEventIncremental({ timestamp: BASE_TIMESTAMP }); - mockRecord._emitter(TEST_EVENT); - const optionsEvent = createOptionsEvent(replay); + it('manually flushes replay and does not continue to record', async () => { + const TEST_EVENT = getTestEventIncremental({ timestamp: BASE_TIMESTAMP }); + mockRecord._emitter(TEST_EVENT); + const optionsEvent = createOptionsEvent(replay); - expect(mockRecord.takeFullSnapshot).not.toHaveBeenCalled(); - expect(replay).not.toHaveLastSentReplay(); + expect(mockRecord.takeFullSnapshot).not.toHaveBeenCalled(); + expect(replay).not.toHaveLastSentReplay(); - // Does not capture on mouse click - domHandler({ - name: 'click', - }); - jest.runAllTimers(); - await new Promise(process.nextTick); - expect(replay).not.toHaveLastSentReplay(); + // Does not capture on mouse click + domHandler({ + name: 'click', + }); + jest.runAllTimers(); + await new Promise(process.nextTick); + expect(replay).not.toHaveLastSentReplay(); - replay.sendBufferedReplayOrFlush({ continueRecording: false }); + replay.sendBufferedReplayOrFlush({ continueRecording: false }); - await waitForBufferFlush(); + await waitForBufferFlush(); - expect(replay).toHaveSentReplay({ - recordingPayloadHeader: { segment_id: 0 }, - replayEventPayload: expect.objectContaining({ - replay_type: 'buffer', - }), - recordingData: JSON.stringify([ - { data: { isCheckout: true }, timestamp: BASE_TIMESTAMP, type: 2 }, - optionsEvent, - TEST_EVENT, - { - type: 5, - timestamp: BASE_TIMESTAMP, - data: { - tag: 'breadcrumb', - payload: { - timestamp: BASE_TIMESTAMP / 1000, - type: 'default', - category: 'ui.click', - message: '', - data: {}, + expect(replay).toHaveSentReplay({ + recordingPayloadHeader: { segment_id: 0 }, + replayEventPayload: expect.objectContaining({ + replay_type: 'buffer', + }), + recordingData: JSON.stringify([ + { data: { isCheckout: true }, timestamp: BASE_TIMESTAMP, type: 2 }, + optionsEvent, + TEST_EVENT, + { + type: 5, + timestamp: BASE_TIMESTAMP, + data: { + tag: 'breadcrumb', + payload: { + timestamp: BASE_TIMESTAMP / 1000, + type: 'default', + category: 'ui.click', + message: '', + data: {}, + }, }, }, - }, - ]), - }); + ]), + }); - jest.advanceTimersByTime(DEFAULT_FLUSH_MIN_DELAY); - // Check that click will not get captured - domHandler({ - name: 'click', - }); + jest.advanceTimersByTime(DEFAULT_FLUSH_MIN_DELAY); + // Check that click will not get captured + domHandler({ + name: 'click', + }); - await waitForFlush(); + await waitForFlush(); - // This is still the last replay sent since we passed `continueRecording: - // false`. - expect(replay).toHaveLastSentReplay({ - recordingPayloadHeader: { segment_id: 0 }, - replayEventPayload: expect.objectContaining({ - replay_type: 'buffer', - }), - recordingData: JSON.stringify([ - { data: { isCheckout: true }, timestamp: BASE_TIMESTAMP, type: 2 }, - optionsEvent, - TEST_EVENT, - { - type: 5, - timestamp: BASE_TIMESTAMP, - data: { - tag: 'breadcrumb', - payload: { - timestamp: BASE_TIMESTAMP / 1000, - type: 'default', - category: 'ui.click', - message: '', - data: {}, + // This is still the last replay sent since we passed `continueRecording: + // false`. + expect(replay).toHaveLastSentReplay({ + recordingPayloadHeader: { segment_id: 0 }, + replayEventPayload: expect.objectContaining({ + replay_type: 'buffer', + }), + recordingData: JSON.stringify([ + { data: { isCheckout: true }, timestamp: BASE_TIMESTAMP, type: 2 }, + optionsEvent, + TEST_EVENT, + { + type: 5, + timestamp: BASE_TIMESTAMP, + data: { + tag: 'breadcrumb', + payload: { + timestamp: BASE_TIMESTAMP / 1000, + type: 'default', + category: 'ui.click', + message: '', + data: {}, + }, }, }, - }, - ]), + ]), + }); }); - }); - it('handles multiple simultaneous flushes', async () => { - const TEST_EVENT = getTestEventIncremental({ timestamp: BASE_TIMESTAMP }); - mockRecord._emitter(TEST_EVENT); - const optionsEvent = createOptionsEvent(replay); + it('handles multiple simultaneous flushes', async () => { + const TEST_EVENT = getTestEventIncremental({ timestamp: BASE_TIMESTAMP }); + mockRecord._emitter(TEST_EVENT); + const optionsEvent = createOptionsEvent(replay); - expect(mockRecord.takeFullSnapshot).not.toHaveBeenCalled(); - expect(replay).not.toHaveLastSentReplay(); + expect(mockRecord.takeFullSnapshot).not.toHaveBeenCalled(); + expect(replay).not.toHaveLastSentReplay(); - // Does not capture on mouse click - domHandler({ - name: 'click', - }); - jest.runAllTimers(); - await new Promise(process.nextTick); - expect(replay).not.toHaveLastSentReplay(); + // Does not capture on mouse click + domHandler({ + name: 'click', + }); + jest.runAllTimers(); + await new Promise(process.nextTick); + expect(replay).not.toHaveLastSentReplay(); - replay.sendBufferedReplayOrFlush({ continueRecording: true }); - replay.sendBufferedReplayOrFlush({ continueRecording: true }); + replay.sendBufferedReplayOrFlush({ continueRecording: true }); + replay.sendBufferedReplayOrFlush({ continueRecording: true }); - await waitForBufferFlush(); + await waitForBufferFlush(); - expect(replay).toHaveSentReplay({ - recordingPayloadHeader: { segment_id: 0 }, - replayEventPayload: expect.objectContaining({ - replay_type: 'buffer', - }), - recordingData: JSON.stringify([ - { data: { isCheckout: true }, timestamp: BASE_TIMESTAMP, type: 2 }, - optionsEvent, - TEST_EVENT, - { - type: 5, - timestamp: BASE_TIMESTAMP, - data: { - tag: 'breadcrumb', - payload: { - timestamp: BASE_TIMESTAMP / 1000, - type: 'default', - category: 'ui.click', - message: '', - data: {}, + expect(replay).toHaveSentReplay({ + recordingPayloadHeader: { segment_id: 0 }, + replayEventPayload: expect.objectContaining({ + replay_type: 'buffer', + }), + recordingData: JSON.stringify([ + { data: { isCheckout: true }, timestamp: BASE_TIMESTAMP, type: 2 }, + optionsEvent, + TEST_EVENT, + { + type: 5, + timestamp: BASE_TIMESTAMP, + data: { + tag: 'breadcrumb', + payload: { + timestamp: BASE_TIMESTAMP / 1000, + type: 'default', + category: 'ui.click', + message: '', + data: {}, + }, }, }, - }, - ]), - }); + ]), + }); - jest.advanceTimersByTime(DEFAULT_FLUSH_MIN_DELAY); - // Check that click will not get captured - domHandler({ - name: 'click', - }); + jest.advanceTimersByTime(DEFAULT_FLUSH_MIN_DELAY); + // Check that click will not get captured + domHandler({ + name: 'click', + }); - await waitForFlush(); + await waitForFlush(); - // This is still the last replay sent since we passed `continueRecording: - // false`. - expect(replay).toHaveLastSentReplay({ - recordingPayloadHeader: { segment_id: 1 }, - replayEventPayload: expect.objectContaining({ - replay_type: 'buffer', - }), + // This is still the last replay sent since we passed `continueRecording: + // false`. + expect(replay).toHaveLastSentReplay({ + recordingPayloadHeader: { segment_id: 1 }, + replayEventPayload: expect.objectContaining({ + replay_type: 'buffer', + }), + }); }); - }); - - // This tests a regression where we were calling flush indiscriminantly in `stop()` - 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 - // `_initializeSessionForSampling` is not called when initializing the plugin. - replay.stop(); - replay['_options']['errorSampleRate'] = 0; - replay['_initializeSessionForSampling'](); - replay.setInitialState(); - jest.runAllTimers(); - await new Promise(process.nextTick); - expect(mockRecord.takeFullSnapshot).not.toHaveBeenCalled(); - expect(replay).not.toHaveLastSentReplay(); - }); + // This tests a regression where we were calling flush indiscriminantly in `stop()` + 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 + // `_initializeSessionForSampling` is not called when initializing the plugin. + replay.stop(); + replay['_options']['errorSampleRate'] = 0; + replay['_initializeSessionForSampling'](); + replay.setInitialState(); - it('does not send a replay when triggering a full dom snapshot when document becomes visible after [SESSION_IDLE_EXPIRE_DURATION]ms', async () => { - Object.defineProperty(document, 'visibilityState', { - configurable: true, - get: function () { - return 'visible'; - }, + jest.runAllTimers(); + await new Promise(process.nextTick); + expect(mockRecord.takeFullSnapshot).not.toHaveBeenCalled(); + expect(replay).not.toHaveLastSentReplay(); }); - jest.advanceTimersByTime(SESSION_IDLE_EXPIRE_DURATION + 1); + it('does not send a replay when triggering a full dom snapshot when document becomes visible after [SESSION_IDLE_EXPIRE_DURATION]ms', async () => { + Object.defineProperty(document, 'visibilityState', { + configurable: true, + get: function () { + return 'visible'; + }, + }); - document.dispatchEvent(new Event('visibilitychange')); + jest.advanceTimersByTime(SESSION_IDLE_EXPIRE_DURATION + 1); - jest.runAllTimers(); - await new Promise(process.nextTick); + document.dispatchEvent(new Event('visibilitychange')); - expect(replay).not.toHaveLastSentReplay(); - }); + jest.runAllTimers(); + await new Promise(process.nextTick); - it('does not send a replay if user hides the tab and comes back within 60 seconds', async () => { - Object.defineProperty(document, 'visibilityState', { - configurable: true, - get: function () { - return 'hidden'; - }, + expect(replay).not.toHaveLastSentReplay(); }); - document.dispatchEvent(new Event('visibilitychange')); - jest.runAllTimers(); - await new Promise(process.nextTick); + it('does not send a replay if user hides the tab and comes back within 60 seconds', async () => { + Object.defineProperty(document, 'visibilityState', { + configurable: true, + get: function () { + return 'hidden'; + }, + }); + document.dispatchEvent(new Event('visibilitychange')); - expect(replay).not.toHaveLastSentReplay(); + jest.runAllTimers(); + await new Promise(process.nextTick); - // User comes back before `SESSION_IDLE_EXPIRE_DURATION` elapses - jest.advanceTimersByTime(SESSION_IDLE_EXPIRE_DURATION - 100); - Object.defineProperty(document, 'visibilityState', { - configurable: true, - get: function () { - return 'visible'; - }, - }); - document.dispatchEvent(new Event('visibilitychange')); + expect(replay).not.toHaveLastSentReplay(); - jest.runAllTimers(); - await new Promise(process.nextTick); + // User comes back before `SESSION_IDLE_EXPIRE_DURATION` elapses + jest.advanceTimersByTime(SESSION_IDLE_EXPIRE_DURATION - 100); + Object.defineProperty(document, 'visibilityState', { + configurable: true, + get: function () { + return 'visible'; + }, + }); + document.dispatchEvent(new Event('visibilitychange')); - expect(mockRecord.takeFullSnapshot).not.toHaveBeenCalled(); - expect(replay).not.toHaveLastSentReplay(); - }); + jest.runAllTimers(); + await new Promise(process.nextTick); - it('does not upload a replay event when document becomes hidden', async () => { - Object.defineProperty(document, 'visibilityState', { - configurable: true, - get: function () { - return 'hidden'; - }, + expect(mockRecord.takeFullSnapshot).not.toHaveBeenCalled(); + expect(replay).not.toHaveLastSentReplay(); }); - // Pretend 5 seconds have passed - const ELAPSED = 5000; - jest.advanceTimersByTime(ELAPSED); + it('does not upload a replay event when document becomes hidden', async () => { + Object.defineProperty(document, 'visibilityState', { + configurable: true, + get: function () { + return 'hidden'; + }, + }); - const TEST_EVENT = getTestEventCheckout({ timestamp: BASE_TIMESTAMP }); - addEvent(replay, TEST_EVENT); + // Pretend 5 seconds have passed + const ELAPSED = 5000; + jest.advanceTimersByTime(ELAPSED); - document.dispatchEvent(new Event('visibilitychange')); + const TEST_EVENT = getTestEventCheckout({ timestamp: BASE_TIMESTAMP }); + addEvent(replay, TEST_EVENT); - jest.runAllTimers(); - await new Promise(process.nextTick); + document.dispatchEvent(new Event('visibilitychange')); - expect(mockRecord.takeFullSnapshot).not.toHaveBeenCalled(); - expect(replay).not.toHaveLastSentReplay(); - }); + jest.runAllTimers(); + await new Promise(process.nextTick); - it('does not upload a replay event if 5 seconds have elapsed since the last replay event occurred', async () => { - const TEST_EVENT = getTestEventIncremental({ timestamp: BASE_TIMESTAMP }); - mockRecord._emitter(TEST_EVENT); - // Pretend 5 seconds have passed - const ELAPSED = 5000; - await advanceTimers(ELAPSED); + expect(mockRecord.takeFullSnapshot).not.toHaveBeenCalled(); + expect(replay).not.toHaveLastSentReplay(); + }); - expect(mockRecord.takeFullSnapshot).not.toHaveBeenCalled(); + it('does not upload a replay event if 5 seconds have elapsed since the last replay event occurred', async () => { + const TEST_EVENT = getTestEventIncremental({ timestamp: BASE_TIMESTAMP }); + mockRecord._emitter(TEST_EVENT); + // Pretend 5 seconds have passed + const ELAPSED = 5000; + await advanceTimers(ELAPSED); - jest.runAllTimers(); - await new Promise(process.nextTick); + expect(mockRecord.takeFullSnapshot).not.toHaveBeenCalled(); - expect(replay).not.toHaveLastSentReplay(); - }); + jest.runAllTimers(); + await new Promise(process.nextTick); - it('does not upload a replay event if 15 seconds have elapsed since the last replay upload', async () => { - const TEST_EVENT = getTestEventIncremental({ timestamp: BASE_TIMESTAMP }); - // Fire a new event every 4 seconds, 4 times - [...Array(4)].forEach(() => { - mockRecord._emitter(TEST_EVENT); - jest.advanceTimersByTime(4000); + expect(replay).not.toHaveLastSentReplay(); }); - // We are at time = +16seconds now (relative to BASE_TIMESTAMP) - // The next event should cause an upload immediately - mockRecord._emitter(TEST_EVENT); - await new Promise(process.nextTick); + it('does not upload a replay event if 15 seconds have elapsed since the last replay upload', async () => { + const TEST_EVENT = getTestEventIncremental({ timestamp: BASE_TIMESTAMP }); + // Fire a new event every 4 seconds, 4 times + [...Array(4)].forEach(() => { + mockRecord._emitter(TEST_EVENT); + jest.advanceTimersByTime(4000); + }); - expect(replay).not.toHaveLastSentReplay(); + // We are at time = +16seconds now (relative to BASE_TIMESTAMP) + // The next event should cause an upload immediately + mockRecord._emitter(TEST_EVENT); + await new Promise(process.nextTick); - // There should also not be another attempt at an upload 5 seconds after the last replay event - await waitForFlush(); - expect(replay).not.toHaveLastSentReplay(); + expect(replay).not.toHaveLastSentReplay(); - // Let's make sure it continues to work - mockRecord._emitter(TEST_EVENT); - await waitForFlush(); - jest.runAllTimers(); - await new Promise(process.nextTick); - expect(replay).not.toHaveLastSentReplay(); - }); + // There should also not be another attempt at an upload 5 seconds after the last replay event + await waitForFlush(); + expect(replay).not.toHaveLastSentReplay(); + + // Let's make sure it continues to work + mockRecord._emitter(TEST_EVENT); + await waitForFlush(); + jest.runAllTimers(); + await new Promise(process.nextTick); + expect(replay).not.toHaveLastSentReplay(); + }); - // When the error session records as a normal session, we want to stop - // recording after the session ends. Otherwise, we get into a state where the - // new session is a session type replay (this could conflict with the session - // sample rate of 0.0), or an error session that has no errors. Instead we - // simply stop the session replay completely and wait for a new page load to - // resample. - it.each([ - ['MAX_REPLAY_DURATION', MAX_REPLAY_DURATION], - ['SESSION_IDLE_DURATION', SESSION_IDLE_EXPIRE_DURATION], - ])( - 'stops replay if session had an error and exceeds %s and does not start a new session thereafter', - async (_label, waitTime) => { - expect(replay.session?.shouldRefresh).toBe(true); + // When the error session records as a normal session, we want to refresh + // sampling after the session ends. + it.each([ + ['MAX_REPLAY_DURATION', MAX_REPLAY_DURATION], + ['SESSION_IDLE_DURATION', SESSION_IDLE_EXPIRE_DURATION], + ])('refreshes replay if session had an error and exceeds %s', async (_label, waitTime) => { + expect(replay.session?.segmentId).toBe(0); captureException(new Error('testing')); @@ -457,7 +452,9 @@ describe('Integration | errorSampleRate', () => { replay_type: 'buffer', }), }); - expect(replay.session?.shouldRefresh).toBe(false); + expect(replay.session?.segmentId).toBeGreaterThan(0); + + const sessionId = replay.getSessionId(); // Idle for given time jest.advanceTimersByTime(waitTime + 1); @@ -482,586 +479,583 @@ describe('Integration | errorSampleRate', () => { }), }); - expect(replay.isEnabled()).toBe(false); - - domHandler({ - name: 'click', - }); - - // Remains disabled! - expect(replay.isEnabled()).toBe(false); - }, - ); + expect(replay.isEnabled()).toBe(true); + expect(replay.getSessionId()).not.toBe(sessionId); + }); - it.each([ - ['MAX_REPLAY_DURATION', MAX_REPLAY_DURATION], - ['SESSION_IDLE_EXPIRE_DURATION', SESSION_IDLE_EXPIRE_DURATION], - ])('continues buffering replay if session had no error and exceeds %s', async (_label, waitTime) => { - const oldSessionId = replay.session?.id; - expect(oldSessionId).toBeDefined(); + it.each([ + ['MAX_REPLAY_DURATION', MAX_REPLAY_DURATION], + ['SESSION_IDLE_EXPIRE_DURATION', SESSION_IDLE_EXPIRE_DURATION], + ])('continues buffering replay if session had no error and exceeds %s', async (_label, waitTime) => { + const oldSessionId = replay.session?.id; + expect(oldSessionId).toBeDefined(); - expect(replay).not.toHaveLastSentReplay(); + expect(replay).not.toHaveLastSentReplay(); - // Idle for given time - jest.advanceTimersByTime(waitTime + 1); - await new Promise(process.nextTick); + // Idle for given time + jest.advanceTimersByTime(waitTime + 1); + await new Promise(process.nextTick); - const TEST_EVENT = getTestEventIncremental({ - data: { name: 'lost event' }, - timestamp: BASE_TIMESTAMP, - }); - mockRecord._emitter(TEST_EVENT); + const TEST_EVENT = getTestEventIncremental({ + data: { name: 'lost event' }, + timestamp: BASE_TIMESTAMP, + }); + mockRecord._emitter(TEST_EVENT); - jest.runAllTimers(); - await new Promise(process.nextTick); + jest.runAllTimers(); + await new Promise(process.nextTick); - // in production, this happens at a time interval, here we mock this - mockRecord.takeFullSnapshot(true); + // in production, this happens at a time interval, here we mock this + mockRecord.takeFullSnapshot(true); - // still no new replay sent - expect(replay).not.toHaveLastSentReplay(); + // still no new replay sent + expect(replay).not.toHaveLastSentReplay(); - expect(replay.isEnabled()).toBe(true); - expect(replay.isPaused()).toBe(false); - expect(replay.recordingMode).toBe('buffer'); + expect(replay.isEnabled()).toBe(true); + expect(replay.isPaused()).toBe(false); + expect(replay.recordingMode).toBe('buffer'); - domHandler({ - name: 'click', - }); + domHandler({ + name: 'click', + }); - await waitForFlush(); + await waitForFlush(); - expect(replay).not.toHaveLastSentReplay(); - expect(replay.isEnabled()).toBe(true); - expect(replay.isPaused()).toBe(false); - expect(replay.recordingMode).toBe('buffer'); + expect(replay).not.toHaveLastSentReplay(); + expect(replay.isEnabled()).toBe(true); + expect(replay.isPaused()).toBe(false); + expect(replay.recordingMode).toBe('buffer'); - // should still react to errors later on - captureException(new Error('testing')); + // should still react to errors later on + captureException(new Error('testing')); - await waitForBufferFlush(); + await waitForBufferFlush(); - expect(replay.session?.id).toBe(oldSessionId); + expect(replay.session?.id).toBe(oldSessionId); - expect(replay).toHaveLastSentReplay({ - recordingPayloadHeader: { segment_id: 0 }, - replayEventPayload: expect.objectContaining({ - replay_type: 'buffer', - }), + expect(replay).toHaveLastSentReplay({ + recordingPayloadHeader: { segment_id: 0 }, + replayEventPayload: expect.objectContaining({ + replay_type: 'buffer', + }), + }); + + expect(replay.isEnabled()).toBe(true); + expect(replay.isPaused()).toBe(false); + expect(replay.recordingMode).toBe('session'); + expect(replay.session?.sampled).toBe('buffer'); + expect(replay.session?.segmentId).toBeGreaterThan(0); }); - expect(replay.isEnabled()).toBe(true); - expect(replay.isPaused()).toBe(false); - expect(replay.recordingMode).toBe('session'); - expect(replay.session?.sampled).toBe('buffer'); - expect(replay.session?.shouldRefresh).toBe(false); - }); + // Should behave the same as above test + it('stops replay if user has been idle for more than SESSION_IDLE_EXPIRE_DURATION and does not start a new session thereafter', async () => { + const oldSessionId = replay.session?.id; + expect(oldSessionId).toBeDefined(); - // Should behave the same as above test - it('stops replay if user has been idle for more than SESSION_IDLE_EXPIRE_DURATION and does not start a new session thereafter', async () => { - const oldSessionId = replay.session?.id; - expect(oldSessionId).toBeDefined(); + // Idle for 15 minutes + jest.advanceTimersByTime(SESSION_IDLE_EXPIRE_DURATION + 1); - // Idle for 15 minutes - jest.advanceTimersByTime(SESSION_IDLE_EXPIRE_DURATION + 1); + const TEST_EVENT = getTestEventIncremental({ + data: { name: 'lost event' }, + timestamp: BASE_TIMESTAMP, + }); + mockRecord._emitter(TEST_EVENT); + expect(replay).not.toHaveLastSentReplay(); - const TEST_EVENT = getTestEventIncremental({ - data: { name: 'lost event' }, - timestamp: BASE_TIMESTAMP, - }); - mockRecord._emitter(TEST_EVENT); - expect(replay).not.toHaveLastSentReplay(); + jest.runAllTimers(); + await new Promise(process.nextTick); - jest.runAllTimers(); - await new Promise(process.nextTick); + // We stop recording after SESSION_IDLE_EXPIRE_DURATION of inactivity in error mode + expect(replay).not.toHaveLastSentReplay(); + expect(replay.isEnabled()).toBe(true); + expect(replay.isPaused()).toBe(false); + expect(replay.recordingMode).toBe('buffer'); - // We stop recording after SESSION_IDLE_EXPIRE_DURATION of inactivity in error mode - expect(replay).not.toHaveLastSentReplay(); - expect(replay.isEnabled()).toBe(true); - expect(replay.isPaused()).toBe(false); - expect(replay.recordingMode).toBe('buffer'); + // should still react to errors later on + captureException(new Error('testing')); - // should still react to errors later on - captureException(new Error('testing')); + await new Promise(process.nextTick); + jest.advanceTimersByTime(DEFAULT_FLUSH_MIN_DELAY); + await new Promise(process.nextTick); + expect(replay.session?.id).toBe(oldSessionId); - await new Promise(process.nextTick); - jest.advanceTimersByTime(DEFAULT_FLUSH_MIN_DELAY); - await new Promise(process.nextTick); - expect(replay.session?.id).toBe(oldSessionId); + // buffered events + expect(replay).toHaveSentReplay({ + recordingPayloadHeader: { segment_id: 0 }, + replayEventPayload: expect.objectContaining({ + replay_type: 'buffer', + }), + }); - // buffered events - expect(replay).toHaveSentReplay({ - recordingPayloadHeader: { segment_id: 0 }, - replayEventPayload: expect.objectContaining({ - replay_type: 'buffer', - }), - }); + // `startRecording` full checkout + expect(replay).toHaveLastSentReplay({ + recordingPayloadHeader: { segment_id: 0 }, + replayEventPayload: expect.objectContaining({ + replay_type: 'buffer', + }), + }); - // `startRecording` full checkout - expect(replay).toHaveLastSentReplay({ - recordingPayloadHeader: { segment_id: 0 }, - replayEventPayload: expect.objectContaining({ - replay_type: 'buffer', - }), + expect(replay.isEnabled()).toBe(true); + expect(replay.isPaused()).toBe(false); + expect(replay.recordingMode).toBe('session'); + expect(replay.session?.sampled).toBe('buffer'); + expect(replay.session?.segmentId).toBeGreaterThan(0); }); - expect(replay.isEnabled()).toBe(true); - expect(replay.isPaused()).toBe(false); - expect(replay.recordingMode).toBe('session'); - expect(replay.session?.sampled).toBe('buffer'); - expect(replay.session?.shouldRefresh).toBe(false); - }); - - it('has the correct timestamps with deferred root event and last replay update', async () => { - const TEST_EVENT = getTestEventIncremental({ timestamp: BASE_TIMESTAMP }); - mockRecord._emitter(TEST_EVENT); - const optionsEvent = createOptionsEvent(replay); + it('has the correct timestamps with deferred root event and last replay update', async () => { + const TEST_EVENT = getTestEventIncremental({ timestamp: BASE_TIMESTAMP }); + mockRecord._emitter(TEST_EVENT); + const optionsEvent = createOptionsEvent(replay); - expect(mockRecord.takeFullSnapshot).not.toHaveBeenCalled(); - expect(replay).not.toHaveLastSentReplay(); + expect(mockRecord.takeFullSnapshot).not.toHaveBeenCalled(); + expect(replay).not.toHaveLastSentReplay(); - jest.runAllTimers(); - await new Promise(process.nextTick); + jest.runAllTimers(); + await new Promise(process.nextTick); - jest.advanceTimersByTime(DEFAULT_FLUSH_MIN_DELAY); + jest.advanceTimersByTime(DEFAULT_FLUSH_MIN_DELAY); - captureException(new Error('testing')); + captureException(new Error('testing')); - await new Promise(process.nextTick); - jest.advanceTimersByTime(DEFAULT_FLUSH_MIN_DELAY); - await new Promise(process.nextTick); + await new Promise(process.nextTick); + jest.advanceTimersByTime(DEFAULT_FLUSH_MIN_DELAY); + await new Promise(process.nextTick); - expect(replay).toHaveSentReplay({ - recordingData: JSON.stringify([ - { data: { isCheckout: true }, timestamp: BASE_TIMESTAMP, type: 2 }, - optionsEvent, - TEST_EVENT, - ]), - replayEventPayload: expect.objectContaining({ - replay_start_timestamp: BASE_TIMESTAMP / 1000, - // the exception happens roughly 10 seconds after BASE_TIMESTAMP - // (advance timers + waiting for flush after the checkout) and - // extra time is likely due to async of `addMemoryEntry()` - - timestamp: (BASE_TIMESTAMP + DEFAULT_FLUSH_MIN_DELAY + DEFAULT_FLUSH_MIN_DELAY + 40) / 1000, - error_ids: [expect.any(String)], - trace_ids: [], - urls: ['http://localhost/'], - replay_id: expect.any(String), - }), - recordingPayloadHeader: { segment_id: 0 }, + expect(replay).toHaveSentReplay({ + recordingData: JSON.stringify([ + { data: { isCheckout: true }, timestamp: BASE_TIMESTAMP, type: 2 }, + optionsEvent, + TEST_EVENT, + ]), + replayEventPayload: expect.objectContaining({ + replay_start_timestamp: BASE_TIMESTAMP / 1000, + // the exception happens roughly 10 seconds after BASE_TIMESTAMP + // (advance timers + waiting for flush after the checkout) and + // extra time is likely due to async of `addMemoryEntry()` + + timestamp: (BASE_TIMESTAMP + DEFAULT_FLUSH_MIN_DELAY + DEFAULT_FLUSH_MIN_DELAY + 40) / 1000, + error_ids: [expect.any(String)], + trace_ids: [], + urls: ['http://localhost/'], + replay_id: expect.any(String), + }), + recordingPayloadHeader: { segment_id: 0 }, + }); }); - }); - it('has correct timestamps when error occurs much later than initial pageload/checkout', async () => { - const ELAPSED = BUFFER_CHECKOUT_TIME; - const TICK = 20; - const TEST_EVENT = getTestEventIncremental({ timestamp: BASE_TIMESTAMP }); - mockRecord._emitter(TEST_EVENT); + it('has correct timestamps when error occurs much later than initial pageload/checkout', async () => { + const ELAPSED = BUFFER_CHECKOUT_TIME; + const TICK = 20; + const TEST_EVENT = getTestEventIncremental({ timestamp: BASE_TIMESTAMP }); + mockRecord._emitter(TEST_EVENT); - // add a mock performance event - replay.performanceEvents.push(PerformanceEntryResource()); + // add a mock performance event + replay.performanceEvents.push(PerformanceEntryResource()); - jest.runAllTimers(); - await new Promise(process.nextTick); + jest.runAllTimers(); + await new Promise(process.nextTick); - expect(mockRecord.takeFullSnapshot).not.toHaveBeenCalled(); - expect(replay).not.toHaveLastSentReplay(); + expect(mockRecord.takeFullSnapshot).not.toHaveBeenCalled(); + expect(replay).not.toHaveLastSentReplay(); - jest.advanceTimersByTime(ELAPSED); + jest.advanceTimersByTime(ELAPSED); - // in production, this happens at a time interval - // session started time should be updated to this current timestamp - mockRecord.takeFullSnapshot(true); - const optionsEvent = createOptionsEvent(replay); + // in production, this happens at a time interval + // session started time should be updated to this current timestamp + mockRecord.takeFullSnapshot(true); + const optionsEvent = createOptionsEvent(replay); - jest.runAllTimers(); - await new Promise(process.nextTick); + jest.runAllTimers(); + await new Promise(process.nextTick); - expect(replay).not.toHaveLastSentReplay(); + expect(replay).not.toHaveLastSentReplay(); - captureException(new Error('testing')); + captureException(new Error('testing')); - await waitForBufferFlush(); + await waitForBufferFlush(); - // This is still the timestamp from the full snapshot we took earlier - expect(replay.session?.started).toBe(BASE_TIMESTAMP + ELAPSED + TICK); + // This is still the timestamp from the full snapshot we took earlier + expect(replay.session?.started).toBe(BASE_TIMESTAMP + ELAPSED + TICK); - // Does not capture mouse click - expect(replay).toHaveSentReplay({ - recordingPayloadHeader: { segment_id: 0 }, - replayEventPayload: expect.objectContaining({ - // Make sure the old performance event is thrown out - replay_start_timestamp: (BASE_TIMESTAMP + ELAPSED + TICK) / 1000, - }), - recordingData: JSON.stringify([ - { - data: { isCheckout: true }, - timestamp: BASE_TIMESTAMP + ELAPSED + TICK, - type: 2, - }, - optionsEvent, - ]), + // Does not capture mouse click + expect(replay).toHaveSentReplay({ + recordingPayloadHeader: { segment_id: 0 }, + replayEventPayload: expect.objectContaining({ + // Make sure the old performance event is thrown out + replay_start_timestamp: (BASE_TIMESTAMP + ELAPSED + TICK) / 1000, + }), + recordingData: JSON.stringify([ + { + data: { isCheckout: true }, + timestamp: BASE_TIMESTAMP + ELAPSED + TICK, + type: 2, + }, + optionsEvent, + ]), + }); }); - }); - it('stops replay when user goes idle', async () => { - jest.setSystemTime(BASE_TIMESTAMP); + it('refreshes replay when user goes idle', async () => { + jest.setSystemTime(BASE_TIMESTAMP); - const TEST_EVENT = getTestEventIncremental({ timestamp: BASE_TIMESTAMP }); - mockRecord._emitter(TEST_EVENT); + const TEST_EVENT = getTestEventIncremental({ timestamp: BASE_TIMESTAMP }); + mockRecord._emitter(TEST_EVENT); - expect(mockRecord.takeFullSnapshot).not.toHaveBeenCalled(); - expect(replay).not.toHaveLastSentReplay(); + expect(mockRecord.takeFullSnapshot).not.toHaveBeenCalled(); + expect(replay).not.toHaveLastSentReplay(); - jest.runAllTimers(); - await new Promise(process.nextTick); + jest.runAllTimers(); + await new Promise(process.nextTick); - captureException(new Error('testing')); + captureException(new Error('testing')); - await waitForBufferFlush(); + await waitForBufferFlush(); - expect(replay).toHaveLastSentReplay(); + expect(replay).toHaveLastSentReplay(); - // Flush from calling `stopRecording` - await waitForFlush(); + // Flush from calling `stopRecording` + await waitForFlush(); - // Now wait after session expires - should stop recording - mockRecord.takeFullSnapshot.mockClear(); - (getCurrentHub().getClient()!.getTransport()!.send as unknown as jest.SpyInstance).mockClear(); + // Now wait after session expires - should stop recording + mockRecord.takeFullSnapshot.mockClear(); + (getCurrentHub().getClient()!.getTransport()!.send as unknown as jest.SpyInstance).mockClear(); - expect(replay).not.toHaveLastSentReplay(); + expect(replay).not.toHaveLastSentReplay(); - // Go idle - jest.advanceTimersByTime(SESSION_IDLE_EXPIRE_DURATION + 1); - await new Promise(process.nextTick); + const sessionId = replay.getSessionId(); - mockRecord._emitter(TEST_EVENT); + // Go idle + jest.advanceTimersByTime(SESSION_IDLE_EXPIRE_DURATION + 1); + await new Promise(process.nextTick); - expect(replay).not.toHaveLastSentReplay(); + mockRecord._emitter(TEST_EVENT); - await waitForFlush(); + expect(replay).not.toHaveLastSentReplay(); - expect(replay).not.toHaveLastSentReplay(); - expect(mockRecord.takeFullSnapshot).toHaveBeenCalledTimes(0); - expect(replay.isEnabled()).toBe(false); - }); + await waitForFlush(); - it('stops replay when session exceeds max length after latest captured error', async () => { - const sessionId = replay.session?.id; - jest.setSystemTime(BASE_TIMESTAMP); + expect(replay).not.toHaveLastSentReplay(); + expect(mockRecord.takeFullSnapshot).toHaveBeenCalledTimes(0); + expect(replay.isEnabled()).toBe(true); + expect(replay.getSessionId()).not.toBe(sessionId); + }); - const TEST_EVENT = getTestEventIncremental({ timestamp: BASE_TIMESTAMP }); - mockRecord._emitter(TEST_EVENT); + it('refreshes replay when session exceeds max length after latest captured error', async () => { + const sessionId = replay.session?.id; + jest.setSystemTime(BASE_TIMESTAMP); - expect(mockRecord.takeFullSnapshot).not.toHaveBeenCalled(); - expect(replay).not.toHaveLastSentReplay(); + const TEST_EVENT = getTestEventIncremental({ timestamp: BASE_TIMESTAMP }); + mockRecord._emitter(TEST_EVENT); - jest.runAllTimers(); - await new Promise(process.nextTick); + expect(mockRecord.takeFullSnapshot).not.toHaveBeenCalled(); + expect(replay).not.toHaveLastSentReplay(); - jest.advanceTimersByTime(2 * MAX_REPLAY_DURATION); + jest.runAllTimers(); + await new Promise(process.nextTick); - // in production, this happens at a time interval, here we mock this - mockRecord.takeFullSnapshot(true); + jest.advanceTimersByTime(2 * MAX_REPLAY_DURATION); - captureException(new Error('testing')); + // in production, this happens at a time interval, here we mock this + mockRecord.takeFullSnapshot(true); - // Flush due to exception - await new Promise(process.nextTick); - await waitForFlush(); + captureException(new Error('testing')); - expect(replay.session?.id).toBe(sessionId); - expect(replay).toHaveLastSentReplay({ - recordingPayloadHeader: { segment_id: 0 }, - }); + // Flush due to exception + await new Promise(process.nextTick); + await waitForFlush(); - // This comes from `startRecording()` in `sendBufferedReplayOrFlush()` - await waitForFlush(); - expect(replay).toHaveLastSentReplay({ - recordingPayloadHeader: { segment_id: 1 }, - recordingData: JSON.stringify([ - { - data: { - isCheckout: true, + expect(replay.session?.id).toBe(sessionId); + expect(replay).toHaveLastSentReplay({ + recordingPayloadHeader: { segment_id: 0 }, + }); + + // This comes from `startRecording()` in `sendBufferedReplayOrFlush()` + await waitForFlush(); + expect(replay).toHaveLastSentReplay({ + recordingPayloadHeader: { segment_id: 1 }, + recordingData: JSON.stringify([ + { + data: { + isCheckout: true, + }, + timestamp: BASE_TIMESTAMP + 2 * MAX_REPLAY_DURATION + DEFAULT_FLUSH_MIN_DELAY + 40, + type: 2, }, - timestamp: BASE_TIMESTAMP + 2 * MAX_REPLAY_DURATION + DEFAULT_FLUSH_MIN_DELAY + 40, - type: 2, - }, - ]), - }); + ]), + }); - // Now wait after session expires - should stop recording - mockRecord.takeFullSnapshot.mockClear(); - (getCurrentHub().getClient()!.getTransport()!.send as unknown as jest.SpyInstance).mockClear(); + // Now wait after session expires - should stop recording + mockRecord.takeFullSnapshot.mockClear(); + (getCurrentHub().getClient()!.getTransport()!.send as unknown as jest.SpyInstance).mockClear(); - jest.advanceTimersByTime(MAX_REPLAY_DURATION); - await new Promise(process.nextTick); + jest.advanceTimersByTime(MAX_REPLAY_DURATION); + await new Promise(process.nextTick); - mockRecord._emitter(TEST_EVENT); - jest.runAllTimers(); - await new Promise(process.nextTick); + mockRecord._emitter(TEST_EVENT); + jest.runAllTimers(); + await new Promise(process.nextTick); - expect(replay).not.toHaveLastSentReplay(); - expect(mockRecord.takeFullSnapshot).toHaveBeenCalledTimes(0); - expect(replay.isEnabled()).toBe(false); + expect(replay).not.toHaveLastSentReplay(); + expect(mockRecord.takeFullSnapshot).toHaveBeenCalledTimes(0); + expect(replay.isEnabled()).toBe(true); + expect(replay.getSessionId()).not.toBe(sessionId); - // Once the session is stopped after capturing a replay already - // (buffer-mode), another error will not trigger a new replay - captureException(new Error('testing')); + // Once the session is stopped after capturing a replay already + // (buffer-mode), another error will trigger a new replay + captureException(new Error('testing')); - await new Promise(process.nextTick); - jest.advanceTimersByTime(DEFAULT_FLUSH_MIN_DELAY); - await new Promise(process.nextTick); - expect(replay).not.toHaveLastSentReplay(); - }); + await new Promise(process.nextTick); + jest.advanceTimersByTime(DEFAULT_FLUSH_MIN_DELAY); + await new Promise(process.nextTick); + expect(replay).toHaveLastSentReplay(); + }); - it('does not stop replay based on earliest event in buffer', async () => { - jest.setSystemTime(BASE_TIMESTAMP); + it('does not refresh replay based on earliest event in buffer', async () => { + jest.setSystemTime(BASE_TIMESTAMP); - const TEST_EVENT = getTestEventIncremental({ timestamp: BASE_TIMESTAMP - 60000 }); - mockRecord._emitter(TEST_EVENT); + const TEST_EVENT = getTestEventIncremental({ timestamp: BASE_TIMESTAMP - 60000 }); + mockRecord._emitter(TEST_EVENT); - expect(mockRecord.takeFullSnapshot).not.toHaveBeenCalled(); - expect(replay).not.toHaveLastSentReplay(); + expect(mockRecord.takeFullSnapshot).not.toHaveBeenCalled(); + expect(replay).not.toHaveLastSentReplay(); - jest.runAllTimers(); - await new Promise(process.nextTick); + jest.runAllTimers(); + await new Promise(process.nextTick); - expect(replay).not.toHaveLastSentReplay(); - captureException(new Error('testing')); + expect(replay).not.toHaveLastSentReplay(); + captureException(new Error('testing')); - await waitForBufferFlush(); + await waitForBufferFlush(); - expect(replay).toHaveLastSentReplay(); + expect(replay).toHaveLastSentReplay(); - // Flush from calling `stopRecording` - await waitForFlush(); + // Flush from calling `stopRecording` + await waitForFlush(); - // Now wait after session expires - should stop recording - mockRecord.takeFullSnapshot.mockClear(); - (getCurrentHub().getClient()!.getTransport()!.send as unknown as jest.SpyInstance).mockClear(); + // Now wait after session expires - should stop recording + mockRecord.takeFullSnapshot.mockClear(); + (getCurrentHub().getClient()!.getTransport()!.send as unknown as jest.SpyInstance).mockClear(); - expect(replay).not.toHaveLastSentReplay(); + expect(replay).not.toHaveLastSentReplay(); - const TICKS = 80; + const TICKS = 80; - // We advance time so that we are on the border of expiring, taking into - // account that TEST_EVENT timestamp is 60000 ms before BASE_TIMESTAMP. The - // 3 DEFAULT_FLUSH_MIN_DELAY is to account for the `waitForFlush` that has - // happened, and for the next two that will happen. The first following - // `waitForFlush` does not expire session, but the following one will. - jest.advanceTimersByTime(SESSION_IDLE_EXPIRE_DURATION - 60000 - 3 * DEFAULT_FLUSH_MIN_DELAY - TICKS); - await new Promise(process.nextTick); + // We advance time so that we are on the border of expiring, taking into + // account that TEST_EVENT timestamp is 60000 ms before BASE_TIMESTAMP. The + // 3 DEFAULT_FLUSH_MIN_DELAY is to account for the `waitForFlush` that has + // happened, and for the next two that will happen. The first following + // `waitForFlush` does not expire session, but the following one will. + jest.advanceTimersByTime(SESSION_IDLE_EXPIRE_DURATION - 60000 - 3 * DEFAULT_FLUSH_MIN_DELAY - TICKS); + await new Promise(process.nextTick); - mockRecord._emitter(TEST_EVENT); - expect(replay).not.toHaveLastSentReplay(); - await waitForFlush(); + mockRecord._emitter(TEST_EVENT); + expect(replay).not.toHaveLastSentReplay(); + await waitForFlush(); - expect(replay).not.toHaveLastSentReplay(); - expect(mockRecord.takeFullSnapshot).toHaveBeenCalledTimes(0); - expect(replay.isEnabled()).toBe(true); + expect(replay).not.toHaveLastSentReplay(); + expect(mockRecord.takeFullSnapshot).toHaveBeenCalledTimes(0); + expect(replay.isEnabled()).toBe(true); - mockRecord._emitter(TEST_EVENT); - expect(replay).not.toHaveLastSentReplay(); - await waitForFlush(); + mockRecord._emitter(TEST_EVENT); + expect(replay).not.toHaveLastSentReplay(); + await waitForFlush(); - expect(replay).not.toHaveLastSentReplay(); - expect(mockRecord.takeFullSnapshot).toHaveBeenCalledTimes(0); - expect(replay.isEnabled()).toBe(true); + expect(replay).not.toHaveLastSentReplay(); + expect(mockRecord.takeFullSnapshot).toHaveBeenCalledTimes(0); + expect(replay.isEnabled()).toBe(true); - // It's hard to test, but if we advance the below time less 1 ms, it should - // be enabled, but we can't trigger a session check via flush without - // incurring another DEFAULT_FLUSH_MIN_DELAY timeout. - jest.advanceTimersByTime(60000 - DEFAULT_FLUSH_MIN_DELAY); - mockRecord._emitter(TEST_EVENT); - expect(replay).not.toHaveLastSentReplay(); - await waitForFlush(); + const sessionId = replay.getSessionId(); - expect(replay).not.toHaveLastSentReplay(); - expect(mockRecord.takeFullSnapshot).toHaveBeenCalledTimes(0); - expect(replay.isEnabled()).toBe(false); - }); + // It's hard to test, but if we advance the below time less 1 ms, it should + // be enabled, but we can't trigger a session check via flush without + // incurring another DEFAULT_FLUSH_MIN_DELAY timeout. + jest.advanceTimersByTime(60000 - DEFAULT_FLUSH_MIN_DELAY); + mockRecord._emitter(TEST_EVENT); + expect(replay).not.toHaveLastSentReplay(); + await waitForFlush(); - it('handles very long active buffer session', async () => { - const stepDuration = 10_000; - const steps = 5_000; + expect(replay).not.toHaveLastSentReplay(); + expect(mockRecord.takeFullSnapshot).toHaveBeenCalledTimes(0); + expect(replay.isEnabled()).toBe(true); + expect(replay.getSessionId()).not.toBe(sessionId); + }); - jest.setSystemTime(BASE_TIMESTAMP); + it('handles very long active buffer session', async () => { + const stepDuration = 10_000; + const steps = 5_000; - expect(replay).not.toHaveLastSentReplay(); + jest.setSystemTime(BASE_TIMESTAMP); - let optionsEvent = createOptionsEvent(replay); + expect(replay).not.toHaveLastSentReplay(); - for (let i = 1; i <= steps; i++) { - jest.advanceTimersByTime(stepDuration); - optionsEvent = createOptionsEvent(replay); - mockRecord._emitter({ data: { step: i }, timestamp: BASE_TIMESTAMP + stepDuration * i, type: 2 }, true); - mockRecord._emitter({ data: { step: i }, timestamp: BASE_TIMESTAMP + stepDuration * i + 5, type: 3 }); - } + let optionsEvent = createOptionsEvent(replay); - expect(replay).not.toHaveLastSentReplay(); + for (let i = 1; i <= steps; i++) { + jest.advanceTimersByTime(stepDuration); + optionsEvent = createOptionsEvent(replay); + mockRecord._emitter({ data: { step: i }, timestamp: BASE_TIMESTAMP + stepDuration * i, type: 2 }, true); + mockRecord._emitter({ data: { step: i }, timestamp: BASE_TIMESTAMP + stepDuration * i + 5, type: 3 }); + } - expect(replay.isEnabled()).toBe(true); - expect(replay.isPaused()).toBe(false); - expect(replay.recordingMode).toBe('buffer'); + expect(replay).not.toHaveLastSentReplay(); - // Now capture an error - captureException(new Error('testing')); - await waitForBufferFlush(); + expect(replay.isEnabled()).toBe(true); + expect(replay.isPaused()).toBe(false); + expect(replay.recordingMode).toBe('buffer'); - expect(replay).toHaveLastSentReplay({ - recordingData: JSON.stringify([ - { data: { step: steps }, timestamp: BASE_TIMESTAMP + stepDuration * steps, type: 2 }, - optionsEvent, - { data: { step: steps }, timestamp: BASE_TIMESTAMP + stepDuration * steps + 5, type: 3 }, - ]), - replayEventPayload: expect.objectContaining({ - replay_start_timestamp: (BASE_TIMESTAMP + stepDuration * steps) / 1000, - error_ids: [expect.any(String)], - trace_ids: [], - urls: ['http://localhost/'], - replay_id: expect.any(String), - }), - recordingPayloadHeader: { segment_id: 0 }, - }); - }); -}); + // Now capture an error + captureException(new Error('testing')); + await waitForBufferFlush(); -/** - * If an error happens, we switch the recordingMode to `session`, set `shouldRefresh=false` on the session, - * but keep `sampled=buffer`. - * This test should verify that if we load such a session from sessionStorage, the session is eventually correctly ended. - */ -it('handles buffer sessions that previously had an error', async () => { - // Pretend that a session is already saved before loading replay - WINDOW.sessionStorage.setItem( - REPLAY_SESSION_KEY, - `{"segmentId":0,"id":"fd09adfc4117477abc8de643e5a5798a","sampled":"buffer","started":${BASE_TIMESTAMP},"lastActivity":${BASE_TIMESTAMP},"shouldRefresh":false}`, - ); - const { mockRecord, replay, integration } = await resetSdkMock({ - replayOptions: { - stickySession: true, - }, - sentryOptions: { - replaysOnErrorSampleRate: 1.0, - }, - autoStart: false, + expect(replay).toHaveLastSentReplay({ + recordingData: JSON.stringify([ + { data: { step: steps }, timestamp: BASE_TIMESTAMP + stepDuration * steps, type: 2 }, + optionsEvent, + { data: { step: steps }, timestamp: BASE_TIMESTAMP + stepDuration * steps + 5, type: 3 }, + ]), + replayEventPayload: expect.objectContaining({ + replay_start_timestamp: (BASE_TIMESTAMP + stepDuration * steps) / 1000, + error_ids: [expect.any(String)], + trace_ids: [], + urls: ['http://localhost/'], + replay_id: expect.any(String), + }), + recordingPayloadHeader: { segment_id: 0 }, + }); + }); }); - integration['_initialize'](); - - jest.runAllTimers(); - - await new Promise(process.nextTick); - const TEST_EVENT = getTestEventIncremental({ timestamp: BASE_TIMESTAMP }); - mockRecord._emitter(TEST_EVENT); - expect(replay).not.toHaveLastSentReplay(); + /** + * If an error happens, we switch the recordingMode to `session`, + * but keep `sampled=buffer`. + * This test should verify that if we load such a session from sessionStorage, the session is eventually correctly ended. + */ + it('handles buffer sessions that previously had an error', async () => { + // Pretend that a session is already saved before loading replay + WINDOW.sessionStorage.setItem( + REPLAY_SESSION_KEY, + `{"segmentId":1,"id":"fd09adfc4117477abc8de643e5a5798a","sampled":"buffer","started":${BASE_TIMESTAMP},"lastActivity":${BASE_TIMESTAMP}}`, + ); + const { mockRecord, replay, integration } = await resetSdkMock({ + replayOptions: { + stickySession: true, + }, + sentryOptions: { + replaysOnErrorSampleRate: 1.0, + }, + autoStart: false, + }); + integration['_initialize'](); - // Waiting for max life should eventually stop recording - // We simulate a full checkout which would otherwise be done automatically - for (let i = 0; i < MAX_REPLAY_DURATION / 60_000; i++) { - jest.advanceTimersByTime(60_000); - await new Promise(process.nextTick); - mockRecord.takeFullSnapshot(true); - } + expect(replay.recordingMode).toBe('session'); + const sessionId = replay.getSessionId(); - expect(replay).not.toHaveLastSentReplay(); - expect(replay.isEnabled()).toBe(false); -}); + // Waiting for max life should eventually refresh the session + // We simulate a full checkout which would otherwise be done automatically + for (let i = 0; i < MAX_REPLAY_DURATION / 60_000; i++) { + jest.advanceTimersByTime(60_000); + await new Promise(process.nextTick); + mockRecord.takeFullSnapshot(true); + } -it('handles buffer sessions that never had an error', async () => { - // Pretend that a session is already saved before loading replay - WINDOW.sessionStorage.setItem( - REPLAY_SESSION_KEY, - `{"segmentId":0,"id":"fd09adfc4117477abc8de643e5a5798a","sampled":"buffer","started":${BASE_TIMESTAMP},"lastActivity":${BASE_TIMESTAMP}}`, - ); - const { mockRecord, replay, integration } = await resetSdkMock({ - replayOptions: { - stickySession: true, - }, - sentryOptions: { - replaysOnErrorSampleRate: 1.0, - }, - autoStart: false, + expect(replay.isEnabled()).toBe(true); + // New sessionId indicates that we refreshed the session + expect(replay.getSessionId()).not.toEqual(sessionId); }); - integration['_initialize'](); - - jest.runAllTimers(); - await new Promise(process.nextTick); - const TEST_EVENT = getTestEventIncremental({ timestamp: BASE_TIMESTAMP }); - mockRecord._emitter(TEST_EVENT); + it('handles buffer sessions that never had an error', async () => { + // Pretend that a session is already saved before loading replay + WINDOW.sessionStorage.setItem( + REPLAY_SESSION_KEY, + `{"segmentId":0,"id":"fd09adfc4117477abc8de643e5a5798a","sampled":"buffer","started":${BASE_TIMESTAMP},"lastActivity":${BASE_TIMESTAMP}}`, + ); + const { mockRecord, replay, integration } = await resetSdkMock({ + replayOptions: { + stickySession: true, + }, + sentryOptions: { + replaysOnErrorSampleRate: 1.0, + }, + autoStart: false, + }); + integration['_initialize'](); - expect(replay).not.toHaveLastSentReplay(); + jest.runAllTimers(); - // Waiting for max life should eventually stop recording - // We simulate a full checkout which would otherwise be done automatically - for (let i = 0; i < MAX_REPLAY_DURATION / 60_000; i++) { - jest.advanceTimersByTime(60_000); await new Promise(process.nextTick); - mockRecord.takeFullSnapshot(true); - } + const TEST_EVENT = getTestEventIncremental({ timestamp: BASE_TIMESTAMP }); + mockRecord._emitter(TEST_EVENT); - expect(replay).not.toHaveLastSentReplay(); - expect(replay.isEnabled()).toBe(true); -}); + expect(replay).not.toHaveLastSentReplay(); -/** - * This is testing a case that should only happen with error-only sessions. - * Previously we had assumed that loading a session from session storage meant - * that the session was not new. However, this is not the case with error-only - * sampling since we can load a saved session that did not have an error (and - * thus no replay was created). - */ -it('sends a replay after loading the session from storage', async () => { - // Pretend that a session is already saved before loading replay - WINDOW.sessionStorage.setItem( - REPLAY_SESSION_KEY, - `{"segmentId":0,"id":"fd09adfc4117477abc8de643e5a5798a","sampled":"buffer","started":${BASE_TIMESTAMP},"lastActivity":${BASE_TIMESTAMP}}`, - ); - const { mockRecord, replay, integration } = await resetSdkMock({ - replayOptions: { - stickySession: true, - }, - sentryOptions: { - replaysOnErrorSampleRate: 1.0, - }, - autoStart: false, + // Waiting for max life should eventually stop recording + // We simulate a full checkout which would otherwise be done automatically + for (let i = 0; i < MAX_REPLAY_DURATION / 60_000; i++) { + jest.advanceTimersByTime(60_000); + await new Promise(process.nextTick); + mockRecord.takeFullSnapshot(true); + } + + expect(replay).not.toHaveLastSentReplay(); + expect(replay.isEnabled()).toBe(true); }); - integration['_initialize'](); - const optionsEvent = createOptionsEvent(replay); - jest.runAllTimers(); + /** + * This is testing a case that should only happen with error-only sessions. + * Previously we had assumed that loading a session from session storage meant + * that the session was not new. However, this is not the case with error-only + * sampling since we can load a saved session that did not have an error (and + * thus no replay was created). + */ + it('sends a replay after loading the session from storage', async () => { + // Pretend that a session is already saved before loading replay + WINDOW.sessionStorage.setItem( + REPLAY_SESSION_KEY, + `{"segmentId":0,"id":"fd09adfc4117477abc8de643e5a5798a","sampled":"buffer","started":${BASE_TIMESTAMP},"lastActivity":${BASE_TIMESTAMP}}`, + ); + const { mockRecord, replay, integration } = await resetSdkMock({ + replayOptions: { + stickySession: true, + }, + sentryOptions: { + replaysOnErrorSampleRate: 1.0, + }, + autoStart: false, + }); + integration['_initialize'](); + const optionsEvent = createOptionsEvent(replay); - await new Promise(process.nextTick); - const TEST_EVENT = getTestEventIncremental({ timestamp: BASE_TIMESTAMP }); - mockRecord._emitter(TEST_EVENT); + jest.runAllTimers(); - expect(replay).not.toHaveLastSentReplay(); + await new Promise(process.nextTick); + const TEST_EVENT = getTestEventIncremental({ timestamp: BASE_TIMESTAMP }); + mockRecord._emitter(TEST_EVENT); - captureException(new Error('testing')); + expect(replay).not.toHaveLastSentReplay(); - // 2 ticks to send replay from an error - await waitForBufferFlush(); + captureException(new Error('testing')); - // Buffered events before error - expect(replay).toHaveSentReplay({ - recordingPayloadHeader: { segment_id: 0 }, - recordingData: JSON.stringify([ - { data: { isCheckout: true }, timestamp: BASE_TIMESTAMP, type: 2 }, - optionsEvent, - TEST_EVENT, - ]), - }); + // 2 ticks to send replay from an error + await waitForBufferFlush(); - // `startRecording()` after switching to session mode to continue recording - await waitForFlush(); + // Buffered events before error + expect(replay).toHaveSentReplay({ + recordingPayloadHeader: { segment_id: 0 }, + recordingData: JSON.stringify([ + { data: { isCheckout: true }, timestamp: BASE_TIMESTAMP, type: 2 }, + optionsEvent, + TEST_EVENT, + ]), + }); + + // `startRecording()` after switching to session mode to continue recording + await waitForFlush(); - // Latest checkout when we call `startRecording` again after uploading segment - // after an error occurs (e.g. when we switch to session replay recording) - expect(replay).toHaveLastSentReplay({ - recordingPayloadHeader: { segment_id: 1 }, - recordingData: JSON.stringify([{ data: { isCheckout: true }, timestamp: BASE_TIMESTAMP + 40, type: 2 }]), + // Latest checkout when we call `startRecording` again after uploading segment + // after an error occurs (e.g. when we switch to session replay recording) + expect(replay).toHaveLastSentReplay({ + recordingPayloadHeader: { segment_id: 1 }, + recordingData: JSON.stringify([{ data: { isCheckout: true }, timestamp: BASE_TIMESTAMP + 40, type: 2 }]), + }); }); }); diff --git a/packages/replay/test/integration/session.test.ts b/packages/replay/test/integration/session.test.ts index 80d09124401a..3716c8b33bc7 100644 --- a/packages/replay/test/integration/session.test.ts +++ b/packages/replay/test/integration/session.test.ts @@ -187,6 +187,10 @@ describe('Integration | session', () => { name: 'click', }); + const optionsEvent = createOptionsEvent(replay); + + await new Promise(process.nextTick); + // This is not called because we have to start recording expect(mockRecord.takeFullSnapshot).not.toHaveBeenCalled(); expect(mockRecord).toHaveBeenCalledTimes(2); @@ -197,9 +201,8 @@ describe('Integration | session', () => { // Replay does not send immediately because checkout was due to expired session expect(replay).not.toHaveLastSentReplay(); - const optionsEvent = createOptionsEvent(replay); - await advanceTimers(DEFAULT_FLUSH_MIN_DELAY); + await new Promise(process.nextTick); const newTimestamp = BASE_TIMESTAMP + ELAPSED + 20; @@ -208,20 +211,7 @@ describe('Integration | session', () => { recordingData: JSON.stringify([ { data: { isCheckout: true }, timestamp: newTimestamp, type: 2 }, optionsEvent, - { - type: 5, - timestamp: newTimestamp, - data: { - tag: 'breadcrumb', - payload: { - timestamp: newTimestamp / 1000, - type: 'default', - category: 'ui.click', - message: '', - data: {}, - }, - }, - }, + // the click is lost, but that's OK ]), }); @@ -364,25 +354,29 @@ describe('Integration | session', () => { }); mockRecord._emitter(TEST_EVENT); + const optionsEvent = createOptionsEvent(replay); + const timestampAtRefresh = BASE_TIMESTAMP + ELAPSED; + + jest.runAllTimers(); + await new Promise(process.nextTick); + expect(replay).not.toHaveSameSession(initialSession); - expect(mockRecord.takeFullSnapshot).toHaveBeenCalled(); expect(replay).not.toHaveLastSentReplay(); - // @ts-expect-error private - expect(replay._stopRecording).toBeDefined(); + expect(replay['_stopRecording']).toBeDefined(); // Now do a click domHandler({ name: 'click', }); - const newTimestamp = BASE_TIMESTAMP + ELAPSED; + // 20 is for the process.nextTick + const newTimestamp = timestampAtRefresh + 20; const NEW_TEST_EVENT = getTestEventIncremental({ data: { name: 'test' }, timestamp: newTimestamp + DEFAULT_FLUSH_MIN_DELAY + 20, }); mockRecord._emitter(NEW_TEST_EVENT); - const optionsEvent = createOptionsEvent(replay); jest.runAllTimers(); await advanceTimers(DEFAULT_FLUSH_MIN_DELAY); @@ -414,7 +408,7 @@ describe('Integration | session', () => { expect(replay.getContext()).toEqual( expect.objectContaining({ initialUrl: 'http://dummy/', - initialTimestamp: newTimestamp, + initialTimestamp: timestampAtRefresh, }), ); }); diff --git a/packages/replay/test/unit/session/fetchSession.test.ts b/packages/replay/test/unit/session/fetchSession.test.ts index cf1856e53356..526c9c7969d1 100644 --- a/packages/replay/test/unit/session/fetchSession.test.ts +++ b/packages/replay/test/unit/session/fetchSession.test.ts @@ -28,7 +28,6 @@ describe('Unit | session | fetchSession', () => { segmentId: 0, sampled: 'session', started: 1648827162630, - shouldRefresh: true, }); }); @@ -44,7 +43,6 @@ describe('Unit | session | fetchSession', () => { segmentId: 0, sampled: false, started: 1648827162630, - shouldRefresh: true, }); }); diff --git a/packages/replay/test/unit/session/loadOrCreateSession.test.ts b/packages/replay/test/unit/session/loadOrCreateSession.test.ts index 8f6e7a071c9c..417b9703d479 100644 --- a/packages/replay/test/unit/session/loadOrCreateSession.test.ts +++ b/packages/replay/test/unit/session/loadOrCreateSession.test.ts @@ -13,7 +13,13 @@ jest.mock('@sentry/utils', () => { }; }); -const SAMPLE_OPTIONS: SessionOptions = { +const OPTIONS_STICKY: SessionOptions = { + stickySession: true, + sessionSampleRate: 1.0, + allowBuffering: false, +}; + +const OPTIONS_NON_SICKY: SessionOptions = { stickySession: false, sessionSampleRate: 1.0, allowBuffering: false, @@ -31,7 +37,6 @@ function createMockSession(when: number = Date.now(), id = 'test_session_id') { lastActivity: when, started: when, sampled: 'session', - shouldRefresh: true, }); } @@ -49,16 +54,12 @@ describe('Unit | session | loadOrCreateSession', () => { }); describe('stickySession: false', () => { - it('creates new session if none is passed in', function () { + it('creates new session', function () { const session = loadOrCreateSession( - undefined, { ...DEFAULT_OPTIONS, }, - { - ...SAMPLE_OPTIONS, - stickySession: false, - }, + OPTIONS_NON_SICKY, ); expect(FetchSession.fetchSession).not.toHaveBeenCalled(); @@ -70,7 +71,6 @@ describe('Unit | session | loadOrCreateSession', () => { lastActivity: expect.any(Number), sampled: 'session', started: expect.any(Number), - shouldRefresh: true, }); // Should not have anything in storage @@ -82,15 +82,11 @@ describe('Unit | session | loadOrCreateSession', () => { saveSession(sessionInStorage); const session = loadOrCreateSession( - undefined, { sessionIdleExpire: 1000, maxReplayDuration: MAX_REPLAY_DURATION, }, - { - ...SAMPLE_OPTIONS, - stickySession: false, - }, + OPTIONS_NON_SICKY, ); expect(FetchSession.fetchSession).not.toHaveBeenCalled(); @@ -102,46 +98,42 @@ describe('Unit | session | loadOrCreateSession', () => { 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); - + it('uses passed in previousSessionId', function () { const session = loadOrCreateSession( - currentSession, { ...DEFAULT_OPTIONS, + previousSessionId: 'previous_session_id', }, - { - ...SAMPLE_OPTIONS, - stickySession: false, - }, + OPTIONS_NON_SICKY, ); expect(FetchSession.fetchSession).not.toHaveBeenCalled(); - expect(CreateSession.createSession).not.toHaveBeenCalled(); + expect(CreateSession.createSession).toHaveBeenCalled(); - expect(session).toEqual(currentSession); + expect(session).toEqual({ + id: 'test_session_uuid', + segmentId: 0, + lastActivity: expect.any(Number), + sampled: 'session', + started: expect.any(Number), + previousSessionId: 'previous_session_id', + }); }); }); describe('stickySession: true', () => { it('creates new session if none exists', function () { const session = loadOrCreateSession( - undefined, { ...DEFAULT_OPTIONS, }, - { - ...SAMPLE_OPTIONS, - stickySession: true, - }, + OPTIONS_STICKY, ); expect(FetchSession.fetchSession).toHaveBeenCalled(); @@ -153,7 +145,6 @@ describe('Unit | session | loadOrCreateSession', () => { lastActivity: expect.any(Number), sampled: 'session', started: expect.any(Number), - shouldRefresh: true, }; expect(session).toEqual(expectedSession); @@ -167,15 +158,11 @@ describe('Unit | session | loadOrCreateSession', () => { saveSession(createMockSession(date, 'test_old_session_uuid')); const session = loadOrCreateSession( - undefined, { sessionIdleExpire: 1000, maxReplayDuration: MAX_REPLAY_DURATION, }, - { - ...SAMPLE_OPTIONS, - stickySession: true, - }, + OPTIONS_STICKY, ); expect(FetchSession.fetchSession).toHaveBeenCalled(); @@ -187,7 +174,6 @@ describe('Unit | session | loadOrCreateSession', () => { lastActivity: expect.any(Number), sampled: 'session', started: expect.any(Number), - shouldRefresh: true, previousSessionId: 'test_old_session_uuid', }; expect(session).toEqual(expectedSession); @@ -201,15 +187,11 @@ describe('Unit | session | loadOrCreateSession', () => { saveSession(createMockSession(date, 'test_old_session_uuid')); const session = loadOrCreateSession( - undefined, { sessionIdleExpire: 5000, maxReplayDuration: MAX_REPLAY_DURATION, }, - { - ...SAMPLE_OPTIONS, - stickySession: true, - }, + OPTIONS_STICKY, ); expect(FetchSession.fetchSession).toHaveBeenCalled(); @@ -221,31 +203,53 @@ describe('Unit | session | loadOrCreateSession', () => { lastActivity: date, sampled: 'session', started: date, - shouldRefresh: true, }); }); - it('uses passed in session instead of fetching from sessionStorage', function () { + it('ignores previousSessionId when loading from sessionStorage', function () { const now = Date.now(); - saveSession(createMockSession(now - 10000, 'test_storage_session_uuid')); - const currentSession = createMockSession(now - 2000); + const currentSession = createMockSession(now - 10000, 'test_storage_session_uuid'); + saveSession(currentSession); const session = loadOrCreateSession( - currentSession, { ...DEFAULT_OPTIONS, + previousSessionId: 'previous_session_id', }, - { - ...SAMPLE_OPTIONS, - stickySession: true, - }, + OPTIONS_STICKY, ); - expect(FetchSession.fetchSession).not.toHaveBeenCalled(); + expect(FetchSession.fetchSession).toHaveBeenCalled(); expect(CreateSession.createSession).not.toHaveBeenCalled(); expect(session).toEqual(currentSession); }); + + it('uses previousSessionId when creating new session', function () { + const session = loadOrCreateSession( + { + ...DEFAULT_OPTIONS, + previousSessionId: 'previous_session_id', + }, + OPTIONS_STICKY, + ); + + 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), + previousSessionId: 'previous_session_id', + }; + expect(session).toEqual(expectedSession); + + // Should also be stored in storage + expect(FetchSession.fetchSession()).toEqual(expectedSession); + }); }); describe('buffering', () => { @@ -257,81 +261,64 @@ describe('Unit | session | loadOrCreateSession', () => { started: now - 2000, segmentId: 0, sampled: 'buffer', - shouldRefresh: true, }); + saveSession(currentSession); const session = loadOrCreateSession( - currentSession, { sessionIdleExpire: 1000, maxReplayDuration: MAX_REPLAY_DURATION, }, - { - ...SAMPLE_OPTIONS, - }, + OPTIONS_STICKY, ); - 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 () { + it('returns new session when buffering & expired, if segmentId>0', function () { const now = Date.now(); const currentSession = makeSession({ id: 'test_session_uuid_2', lastActivity: now - 2000, started: now - 2000, - segmentId: 0, + segmentId: 1, sampled: 'buffer', - shouldRefresh: false, }); + saveSession(currentSession); const session = loadOrCreateSession( - currentSession, { sessionIdleExpire: 1000, maxReplayDuration: MAX_REPLAY_DURATION, }, - { - ...SAMPLE_OPTIONS, - }, + OPTIONS_STICKY, ); - expect(FetchSession.fetchSession).not.toHaveBeenCalled(); - expect(CreateSession.createSession).not.toHaveBeenCalled(); - expect(session).not.toEqual(currentSession); - expect(session.sampled).toBe(false); + expect(session.sampled).toBe('session'); expect(session.started).toBeGreaterThanOrEqual(now); + expect(session.segmentId).toBe(0); }); - it('returns existing session when buffering & not expired, if shouldRefresh===false', function () { + it('returns existing session when buffering & not expired, if segmentId>0', function () { const now = Date.now(); const currentSession = makeSession({ id: 'test_session_uuid_2', lastActivity: now - 2000, started: now - 2000, - segmentId: 0, + segmentId: 1, sampled: 'buffer', - shouldRefresh: false, }); + saveSession(currentSession); const session = loadOrCreateSession( - currentSession, { sessionIdleExpire: 5000, maxReplayDuration: MAX_REPLAY_DURATION, }, - { - ...SAMPLE_OPTIONS, - }, + OPTIONS_STICKY, ); - expect(FetchSession.fetchSession).not.toHaveBeenCalled(); - expect(CreateSession.createSession).not.toHaveBeenCalled(); - expect(session).toEqual(currentSession); }); }); @@ -339,12 +326,11 @@ describe('Unit | session | loadOrCreateSession', () => { describe('sampling', () => { it('returns unsampled session if sample rates are 0', function () { const session = loadOrCreateSession( - undefined, { ...DEFAULT_OPTIONS, }, { - ...SAMPLE_OPTIONS, + stickySession: false, sessionSampleRate: 0, allowBuffering: false, }, @@ -356,19 +342,17 @@ describe('Unit | session | loadOrCreateSession', () => { 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, { ...DEFAULT_OPTIONS, }, { - ...SAMPLE_OPTIONS, + stickySession: false, sessionSampleRate: 1.0, allowBuffering: false, }, @@ -379,12 +363,11 @@ describe('Unit | session | loadOrCreateSession', () => { it('returns `buffer` session if allowBuffering===true', function () { const session = loadOrCreateSession( - undefined, { ...DEFAULT_OPTIONS, }, { - ...SAMPLE_OPTIONS, + stickySession: false, sessionSampleRate: 0.0, allowBuffering: true, }, diff --git a/packages/replay/test/unit/session/maybeRefreshSession.test.ts b/packages/replay/test/unit/session/maybeRefreshSession.test.ts deleted file mode 100644 index c4399a5e1188..000000000000 --- a/packages/replay/test/unit/session/maybeRefreshSession.test.ts +++ /dev/null @@ -1,267 +0,0 @@ -import { MAX_REPLAY_DURATION, SESSION_IDLE_EXPIRE_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 } 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 DEFAULT_OPTIONS = { - sessionIdleExpire: SESSION_IDLE_EXPIRE_DURATION, - maxReplayDuration: MAX_REPLAY_DURATION, -}; - -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, - { - ...DEFAULT_OPTIONS, - }, - { - ...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, - { - sessionIdleExpire: 1000, - maxReplayDuration: MAX_REPLAY_DURATION, - }, - { - ...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, - { - sessionIdleExpire: 1000, - maxReplayDuration: MAX_REPLAY_DURATION, - }, - { - ...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, - { - sessionIdleExpire: 1000, - maxReplayDuration: MAX_REPLAY_DURATION, - }, - { - ...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, - { - sessionIdleExpire: 5000, - maxReplayDuration: MAX_REPLAY_DURATION, - }, - { - ...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, - { - sessionIdleExpire: 1000, - maxReplayDuration: MAX_REPLAY_DURATION, - }, - { - ...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, - { - sessionIdleExpire: 1000, - maxReplayDuration: MAX_REPLAY_DURATION, - }, - { - ...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, - { - sessionIdleExpire: 1000, - maxReplayDuration: MAX_REPLAY_DURATION, - }, - { - ...SAMPLE_OPTIONS, - sessionSampleRate: 0.0, - allowBuffering: true, - }, - ); - - expect(session.id).toBe('test_session_uuid'); - expect(session.sampled).toBe('buffer'); - }); - }); -});