From e8bfefefdb5aa0d32c3ffbb54a7a84b2537dbff7 Mon Sep 17 00:00:00 2001 From: Billy Vong Date: Fri, 11 Oct 2024 13:59:28 -0400 Subject: [PATCH 1/3] fix(replay): Fix onError sampling when loading an expired buffered session This fixes a bug where an older, saved session (that has a `previousSessionId`, i.e. session was recorded and expired) would cause buffered replays to not send when an error happens. This is because the session start timestamp would never update, causing our flush logic to skip sending the replay due to incorrect replay duration calculation. --- .../src/util/handleRecordingEmit.ts | 20 ++--- .../test/integration/errorSampleRate.test.ts | 84 +++++++++++++++++++ 2 files changed, 94 insertions(+), 10 deletions(-) diff --git a/packages/replay-internal/src/util/handleRecordingEmit.ts b/packages/replay-internal/src/util/handleRecordingEmit.ts index 0467edefa9a2..4f4637276116 100644 --- a/packages/replay-internal/src/util/handleRecordingEmit.ts +++ b/packages/replay-internal/src/util/handleRecordingEmit.ts @@ -71,16 +71,6 @@ export function getHandleRecordingEmit(replay: ReplayContainer): RecordingEmitCa // only be added for checkouts addSettingsEvent(replay, isCheckout); - // If there is a previousSessionId after a full snapshot occurs, then - // the replay session was started due to session expiration. The new session - // is started before triggering a new checkout and contains the id - // of the previous session. Do not immediately flush in this case - // to avoid capturing only the checkout and instead the replay will - // be captured if they perform any follow-up actions. - if (session && session.previousSessionId) { - return true; - } - // When in buffer mode, make sure we adjust the session started date to the current earliest event of the buffer // this should usually be the timestamp of the checkout event, but to be safe... if (replay.recordingMode === 'buffer' && session && replay.eventBuffer) { @@ -97,6 +87,16 @@ export function getHandleRecordingEmit(replay: ReplayContainer): RecordingEmitCa } } + // If there is a previousSessionId after a full snapshot occurs, then + // the replay session was started due to session expiration. The new session + // is started before triggering a new checkout and contains the id + // of the previous session. Do not immediately flush in this case + // to avoid capturing only the checkout and instead the replay will + // be captured if they perform any follow-up actions. + if (session && session.previousSessionId) { + return true; + } + if (replay.recordingMode === 'session') { // If the full snapshot is due to an initial load, we will not have // a previous session ID. In this case, we want to buffer events diff --git a/packages/replay-internal/test/integration/errorSampleRate.test.ts b/packages/replay-internal/test/integration/errorSampleRate.test.ts index 3763ef83de44..8670096c0e02 100644 --- a/packages/replay-internal/test/integration/errorSampleRate.test.ts +++ b/packages/replay-internal/test/integration/errorSampleRate.test.ts @@ -148,6 +148,90 @@ describe('Integration | errorSampleRate', () => { }); }); + it('loads an old session with a previousSessionId set and can send buffered replay', async () => { + vi.mock('../../src/session/fetchSession', () => ({ + fetchSession: () => ({ + id: 'newreplayid', + started: BASE_TIMESTAMP, + lastActivity: BASE_TIMESTAMP, + segmentId: 0, + sampled: 'buffer', + previousSessionId: 'previoussessionid', + }), + })); + + const ADVANCED_TIME = 86400000; + const optionsEvent = createOptionsEvent(replay); + + expect(replay.session.started).toBe(BASE_TIMESTAMP); + + // advance time to make sure replay duration is invalid + vi.advanceTimersByTime(ADVANCED_TIME); + + // full snapshot should update session start time + mockRecord.takeFullSnapshot(true); + expect(replay.session.started).toBe(BASE_TIMESTAMP + ADVANCED_TIME); + expect(replay.recordingMode).toBe('buffer'); + + // advance so we can flush + vi.advanceTimersByTime(DEFAULT_FLUSH_MIN_DELAY); + + captureException(new Error('testing')); + await vi.advanceTimersToNextTimerAsync(); + + // Converts to session mode + expect(replay.recordingMode).toBe('session'); + expect(replay).toHaveLastSentReplay({ + recordingPayloadHeader: { segment_id: 0 }, + replayEventPayload: expect.objectContaining({ + replay_type: 'buffer', + }), + recordingData: JSON.stringify([ + { data: { isCheckout: true }, timestamp: BASE_TIMESTAMP + ADVANCED_TIME, type: 2 }, + { ...optionsEvent, timestamp: BASE_TIMESTAMP + ADVANCED_TIME }, + ]), + }); + + // capture next event + domHandler({ + name: 'click', + event: new Event('click'), + }); + + vi.advanceTimersByTime(DEFAULT_FLUSH_MIN_DELAY); + await vi.advanceTimersToNextTimerAsync(); + + expect(replay).toHaveLastSentReplay({ + recordingPayloadHeader: { segment_id: 1 }, + replayEventPayload: expect.objectContaining({ + // We don't change replay_type as it starts in buffer mode and that's + // what we're interested in, even though recordingMode changes to + // 'session' + replay_type: 'buffer', + }), + recordingData: JSON.stringify([ + // There's a new checkout because we convert to session mode + { data: { isCheckout: true }, timestamp: BASE_TIMESTAMP + ADVANCED_TIME + DEFAULT_FLUSH_MIN_DELAY, type: 2 }, + { + type: 5, + timestamp: BASE_TIMESTAMP + ADVANCED_TIME + DEFAULT_FLUSH_MIN_DELAY, + data: { + tag: 'breadcrumb', + payload: { + timestamp: (BASE_TIMESTAMP + ADVANCED_TIME + DEFAULT_FLUSH_MIN_DELAY) / 1000, + type: 'default', + category: 'ui.click', + message: '', + data: {}, + }, + }, + }, + ]), + }); + vi.clearAllMocks(); + await waitForFlush(); + }); + it('manually flushes replay and does not continue to record', async () => { const TEST_EVENT = getTestEventIncremental({ timestamp: BASE_TIMESTAMP }); mockRecord._emitter(TEST_EVENT); From d793462a229fb8c25431a03205c132e36d67ebc1 Mon Sep 17 00:00:00 2001 From: Billy Vong Date: Fri, 11 Oct 2024 13:59:44 -0400 Subject: [PATCH 2/3] bump size limit --- .size-limit.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.size-limit.js b/.size-limit.js index 8a7670e6d638..a244ccb2a2a3 100644 --- a/.size-limit.js +++ b/.size-limit.js @@ -86,7 +86,7 @@ module.exports = [ path: 'packages/browser/build/npm/esm/index.js', import: createImport('init', 'browserTracingIntegration', 'replayIntegration', 'feedbackIntegration'), gzip: true, - limit: '91 KB', + limit: '95 KB', }, { name: '@sentry/browser (incl. Tracing, Replay, Feedback, metrics)', From d778a0d71766ed18e180f50248bb772eaa99bdeb Mon Sep 17 00:00:00 2001 From: Billy Vong Date: Fri, 11 Oct 2024 15:00:54 -0400 Subject: [PATCH 3/3] proper unmock --- .../replay-internal/test/integration/errorSampleRate.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/replay-internal/test/integration/errorSampleRate.test.ts b/packages/replay-internal/test/integration/errorSampleRate.test.ts index 8670096c0e02..e81d9df1b43d 100644 --- a/packages/replay-internal/test/integration/errorSampleRate.test.ts +++ b/packages/replay-internal/test/integration/errorSampleRate.test.ts @@ -228,7 +228,7 @@ describe('Integration | errorSampleRate', () => { }, ]), }); - vi.clearAllMocks(); + vi.unmock('../../src/session/fetchSession'); await waitForFlush(); });