Skip to content

Commit 194b0da

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

File tree

3 files changed

+121
-3
lines changed

3 files changed

+121
-3
lines changed

packages/browser-integration-tests/suites/replay/sessionMaxAge/init.js

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,6 @@ Sentry.init({
1111
sampleRate: 0,
1212
replaysSessionSampleRate: 1.0,
1313
replaysOnErrorSampleRate: 0.0,
14-
debug: true,
15-
1614
integrations: [window.Replay],
1715
});
1816

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

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

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)