Skip to content

Commit 2748691

Browse files
authored
fix(replay): Ensure buffer sessions end after capturing an error (#8713)
We haven't been persisting the `shouldRefresh` property of the session. This combined with the fact that we do not update the `sampled` field on the session to `session` when an error occurs, but keep it at `buffer`, means that if a user reloads the page or the session is otherwise re-fetched from sessionStorage, if previously an error occurs, we'll keep buffering forever again (like for a "fresh" buffer session), and if an error happens we convert it again to a `session` session, but since the session ID was never updated this will be "added" to the previous session instead. I made a reproduction test that failed before and works after this fix. Fixes #8400
1 parent 60c1081 commit 2748691

File tree

2 files changed

+81
-1
lines changed

2 files changed

+81
-1
lines changed

packages/replay/src/session/Session.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,14 @@ export function makeSession(session: Partial<Session> & { sampled: Sampled }): S
1313
const lastActivity = session.lastActivity || now;
1414
const segmentId = session.segmentId || 0;
1515
const sampled = session.sampled;
16+
const shouldRefresh = typeof session.shouldRefresh === 'boolean' ? session.shouldRefresh : true;
1617

1718
return {
1819
id,
1920
started,
2021
lastActivity,
2122
segmentId,
2223
sampled,
23-
shouldRefresh: true,
24+
shouldRefresh,
2425
};
2526
}

packages/replay/test/integration/errorSampleRate.test.ts

+79
Original file line numberDiff line numberDiff line change
@@ -819,6 +819,85 @@ describe('Integration | errorSampleRate', () => {
819819
});
820820
});
821821

822+
/**
823+
* If an error happens, we switch the recordingMode to `session`, set `shouldRefresh=false` on the session,
824+
* but keep `sampled=buffer`.
825+
* This test should verify that if we load such a session from sessionStorage, the session is eventually correctly ended.
826+
*/
827+
it('handles buffer sessions that previously had an error', async () => {
828+
// Pretend that a session is already saved before loading replay
829+
WINDOW.sessionStorage.setItem(
830+
REPLAY_SESSION_KEY,
831+
`{"segmentId":0,"id":"fd09adfc4117477abc8de643e5a5798a","sampled":"buffer","started":${BASE_TIMESTAMP},"lastActivity":${BASE_TIMESTAMP},"shouldRefresh":false}`,
832+
);
833+
const { mockRecord, replay, integration } = await resetSdkMock({
834+
replayOptions: {
835+
stickySession: true,
836+
},
837+
sentryOptions: {
838+
replaysOnErrorSampleRate: 1.0,
839+
},
840+
autoStart: false,
841+
});
842+
integration['_initialize']();
843+
844+
jest.runAllTimers();
845+
846+
await new Promise(process.nextTick);
847+
const TEST_EVENT = { data: {}, timestamp: BASE_TIMESTAMP, type: 3 };
848+
mockRecord._emitter(TEST_EVENT);
849+
850+
expect(replay).not.toHaveLastSentReplay();
851+
852+
// Waiting for max life should eventually stop recording
853+
// We simulate a full checkout which would otherwise be done automatically
854+
for (let i = 0; i < MAX_SESSION_LIFE / 60_000; i++) {
855+
jest.advanceTimersByTime(60_000);
856+
await new Promise(process.nextTick);
857+
mockRecord.takeFullSnapshot(true);
858+
}
859+
860+
expect(replay).not.toHaveLastSentReplay();
861+
expect(replay.isEnabled()).toBe(false);
862+
});
863+
864+
it('handles buffer sessions that never had an error', async () => {
865+
// Pretend that a session is already saved before loading replay
866+
WINDOW.sessionStorage.setItem(
867+
REPLAY_SESSION_KEY,
868+
`{"segmentId":0,"id":"fd09adfc4117477abc8de643e5a5798a","sampled":"buffer","started":${BASE_TIMESTAMP},"lastActivity":${BASE_TIMESTAMP}}`,
869+
);
870+
const { mockRecord, replay, integration } = await resetSdkMock({
871+
replayOptions: {
872+
stickySession: true,
873+
},
874+
sentryOptions: {
875+
replaysOnErrorSampleRate: 1.0,
876+
},
877+
autoStart: false,
878+
});
879+
integration['_initialize']();
880+
881+
jest.runAllTimers();
882+
883+
await new Promise(process.nextTick);
884+
const TEST_EVENT = { data: {}, timestamp: BASE_TIMESTAMP, type: 3 };
885+
mockRecord._emitter(TEST_EVENT);
886+
887+
expect(replay).not.toHaveLastSentReplay();
888+
889+
// Waiting for max life should eventually stop recording
890+
// We simulate a full checkout which would otherwise be done automatically
891+
for (let i = 0; i < MAX_SESSION_LIFE / 60_000; i++) {
892+
jest.advanceTimersByTime(60_000);
893+
await new Promise(process.nextTick);
894+
mockRecord.takeFullSnapshot(true);
895+
}
896+
897+
expect(replay).not.toHaveLastSentReplay();
898+
expect(replay.isEnabled()).toBe(true);
899+
});
900+
822901
/**
823902
* This is testing a case that should only happen with error-only sessions.
824903
* Previously we had assumed that loading a session from session storage meant

0 commit comments

Comments
 (0)