Skip to content

Commit 7e54ff4

Browse files
committed
fix(replay): Handle edge cases & more logging
1 parent c59d333 commit 7e54ff4

File tree

2 files changed

+123
-1
lines changed
  • packages

2 files changed

+123
-1
lines changed

packages/browser-integration-tests/suites/replay/sessionMaxAge/test.ts

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,3 +82,101 @@ sentryTest('handles session that exceeds max age', async ({ getLocalTestPath, pa
8282
const stringifiedSnapshot2 = normalize(fullSnapshots2[0]);
8383
expect(stringifiedSnapshot2).toMatchSnapshot('snapshot-2.json');
8484
});
85+
86+
sentryTest('handles many consequitive events in session that exceeds max age', async ({ getLocalTestPath, page }) => {
87+
if (shouldSkipReplayTest()) {
88+
sentryTest.skip();
89+
}
90+
91+
page.on('console', msg => console.log(msg.text()));
92+
93+
const reqPromise0 = waitForReplayRequest(page, 0);
94+
const reqPromise1 = waitForReplayRequest(page, 1);
95+
96+
await page.route('https://dsn.ingest.sentry.io/**/*', route => {
97+
return route.fulfill({
98+
status: 200,
99+
contentType: 'application/json',
100+
body: JSON.stringify({ id: 'test-id' }),
101+
});
102+
});
103+
104+
const url = await getLocalTestPath({ testDir: __dirname });
105+
106+
await page.goto(url);
107+
108+
const replay0 = await getReplaySnapshot(page);
109+
// We use the `initialTimestamp` of the replay to do any time based calculations
110+
const startTimestamp = replay0._context.initialTimestamp;
111+
112+
const req0 = await reqPromise0;
113+
114+
const replayEvent0 = getReplayEvent(req0);
115+
expect(replayEvent0).toEqual(getExpectedReplayEvent({}));
116+
117+
const fullSnapshots0 = getFullRecordingSnapshots(req0);
118+
expect(fullSnapshots0.length).toEqual(1);
119+
const stringifiedSnapshot = normalize(fullSnapshots0[0]);
120+
expect(stringifiedSnapshot).toMatchSnapshot('snapshot-0.json');
121+
122+
// Wait again for a new segment 0 (=new session)
123+
const reqPromise2 = waitForReplayRequest(page, 0);
124+
125+
// Wait for an incremental snapshot
126+
// Wait half of the session max age (after initial flush), but account for potentially slow runners
127+
const timePassed1 = Date.now() - startTimestamp;
128+
await new Promise(resolve => setTimeout(resolve, Math.max(SESSION_MAX_AGE / 2 - timePassed1, 0)));
129+
130+
await page.click('#button1');
131+
132+
const req1 = await reqPromise1;
133+
const replayEvent1 = getReplayEvent(req1);
134+
135+
expect(replayEvent1).toEqual(getExpectedReplayEvent({ segment_id: 1, urls: [] }));
136+
137+
const replay1 = await getReplaySnapshot(page);
138+
const oldSessionId = replay1.session?.id;
139+
140+
// Wait for session to expire
141+
const timePassed2 = Date.now() - startTimestamp;
142+
await new Promise(resolve => setTimeout(resolve, Math.max(SESSION_MAX_AGE - timePassed2, 0)));
143+
144+
await page.evaluate(`
145+
Object.defineProperty(document, 'visibilityState', {
146+
configurable: true,
147+
get: function () {
148+
return 'hidden';
149+
},
150+
});
151+
document.dispatchEvent(new Event('visibilitychange'));
152+
`);
153+
154+
// Many things going on at the same time...
155+
void page.evaluate(`
156+
Object.defineProperty(document, 'visibilityState', {
157+
configurable: true,
158+
get: function () {
159+
return 'visible';
160+
},
161+
});
162+
document.dispatchEvent(new Event('visibilitychange'));
163+
`);
164+
void page.click('#button1');
165+
void page.click('#button2');
166+
await new Promise(resolve => setTimeout(resolve, 1));
167+
void page.click('#button1');
168+
void page.click('#button2');
169+
170+
const req2 = await reqPromise2;
171+
const replay2 = await getReplaySnapshot(page);
172+
173+
expect(replay2.session?.id).not.toEqual(oldSessionId);
174+
175+
const replayEvent2 = getReplayEvent(req2);
176+
expect(replayEvent2).toEqual(getExpectedReplayEvent({}));
177+
178+
const fullSnapshots2 = getFullRecordingSnapshots(req2);
179+
expect(fullSnapshots2.length).toEqual(1);
180+
const stringifiedSnapshot2 = normalize(fullSnapshots2[0]);
181+
expect(stringifiedSnapshot2).toMatchSnapshot('snapshot-2.json');
182+
});

packages/replay/src/replay.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -389,6 +389,12 @@ export class ReplayContainer implements ReplayContainerInterface {
389389

390390
// Re-start recording, but in "session" recording mode
391391

392+
// When `traceInternals` is enabled, we want to log this to the console
393+
// Else, use the regular debug output
394+
// eslint-disable-next-line
395+
const log = this.getOptions()._experiments.traceInternals ? console.warn : logger.log;
396+
log('[Replay] An error was detected in buffer mode, starting to send replay...');
397+
392398
// Reset all "capture on error" configuration before
393399
// starting a new recording
394400
this.recordingMode = 'session';
@@ -639,6 +645,11 @@ export class ReplayContainer implements ReplayContainerInterface {
639645
}
640646

641647
void this.stop('session expired with refreshing').then(() => {
648+
// Just to double-check we haven't started anywhere else
649+
if (this.isEnabled()) {
650+
return;
651+
}
652+
642653
if (sampled === 'session') {
643654
return this.start();
644655
}
@@ -947,6 +958,17 @@ export class ReplayContainer implements ReplayContainerInterface {
947958
return;
948959
}
949960

961+
// Unless force is true (which is the case when stop() is called),
962+
// _flush should never be called when not in session mode
963+
// Apart from `stop()`, only debounced flush triggers `_flush()`, which shouldn't happen
964+
if (this.recordingMode !== 'session' && !force) {
965+
// When `traceInternals` is enabled, we want to log this to the console
966+
// Else, use the regular debug output
967+
// eslint-disable-next-line
968+
const log = this.getOptions()._experiments.traceInternals ? console.warn : logger.warn;
969+
log('[Replay] Flushing replay while not in session mode.');
970+
}
971+
950972
return new Promise(resolve => {
951973
if (!this.session) {
952974
resolve();
@@ -959,7 +981,9 @@ export class ReplayContainer implements ReplayContainerInterface {
959981
},
960982
ensureResumed: () => this.resume(),
961983
onEnd: () => {
962-
__DEBUG_BUILD__ && logger.error('[Replay] Attempting to finish replay event after session expired.');
984+
if (!force) {
985+
__DEBUG_BUILD__ && logger.warn('[Replay] Attempting to finish replay event after session expired.');
986+
}
963987
this._refreshSession();
964988
resolve();
965989
},

0 commit comments

Comments
 (0)