Skip to content

Commit baa242e

Browse files
committed
fix(replay): Ensure we do not try to flush when we force stop replay
1 parent 0bade5d commit baa242e

File tree

7 files changed

+92
-10
lines changed

7 files changed

+92
-10
lines changed
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<meta charset="utf-8" />
5+
</head>
6+
<body>
7+
<button onclick="console.log('Test log')" id="button1">Click me</button>
8+
<button onclick="console.log('Test log 2')" id="button2">Click me</button>
9+
</body>
10+
</html>
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { expect } from '@playwright/test';
2+
3+
import { sentryTest } from '../../../utils/fixtures';
4+
import {
5+
getReplaySnapshot,
6+
REPLAY_DEFAULT_FLUSH_MAX_DELAY,
7+
shouldSkipReplayTest,
8+
waitForReplayRequest,
9+
} from '../../../utils/replayHelpers';
10+
11+
sentryTest(
12+
'should stop recording when running into eventBuffer error',
13+
async ({ getLocalTestPath, page, forceFlushReplay }) => {
14+
if (shouldSkipReplayTest()) {
15+
sentryTest.skip();
16+
}
17+
18+
let called = 0;
19+
20+
await page.route('https://dsn.ingest.sentry.io/**/*', route => {
21+
called++;
22+
23+
return route.fulfill({
24+
status: 200,
25+
});
26+
});
27+
28+
const url = await getLocalTestPath({ testDir: __dirname });
29+
await page.goto(url);
30+
31+
await waitForReplayRequest(page);
32+
33+
expect(called).toBe(1);
34+
const replay = await getReplaySnapshot(page);
35+
expect(replay._isEnabled).toBe(true);
36+
37+
await forceFlushReplay();
38+
39+
called = 0;
40+
41+
/**
42+
* We test the following here:
43+
* 1. First click should add an event (so the eventbuffer is not empty)
44+
* 2. Second click should throw an error in eventBuffer (which should lead to stopping the replay)
45+
* 3. Nothing should be sent to API, as we stop the replay due to the eventBuffer error.
46+
*/
47+
await page.evaluate(`
48+
window._count = 0;
49+
window._addEvent = window.Replay._replay.eventBuffer.addEvent.bind(window.Replay._replay.eventBuffer);
50+
window.Replay._replay.eventBuffer.addEvent = (...args) => {
51+
window._count++;
52+
if (window._count === 2) {
53+
throw new Error('provoked error');
54+
}
55+
window._addEvent(...args);
56+
};
57+
`);
58+
59+
void page.click('#button1');
60+
void page.click('#button2');
61+
62+
// Should immediately skip retrying and just cancel, no backoff
63+
// This waitForTimeout call should be okay, as we're not checking for any
64+
// further network requests afterwards.
65+
await page.waitForTimeout(REPLAY_DEFAULT_FLUSH_MAX_DELAY + 100);
66+
67+
expect(called).toBe(0);
68+
69+
const replay2 = await getReplaySnapshot(page);
70+
71+
expect(replay2._isEnabled).toBe(false);
72+
},
73+
);

packages/replay/src/integration.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -263,7 +263,7 @@ Sentry.init({ replaysOnErrorSampleRate: ${errorSampleRate} })`,
263263
return Promise.resolve();
264264
}
265265

266-
return this._replay.stop();
266+
return this._replay.stop({ forceFlush: this._replay.recordingMode === 'session' });
267267
}
268268

269269
/**

packages/replay/src/replay.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -368,7 +368,7 @@ export class ReplayContainer implements ReplayContainerInterface {
368368
* Currently, this needs to be manually called (e.g. for tests). Sentry SDK
369369
* does not support a teardown
370370
*/
371-
public async stop(reason?: string): Promise<void> {
371+
public async stop({ forceFlush = false, reason }: { forceFlush?: boolean; reason?: string } = {}): Promise<void> {
372372
if (!this._isEnabled) {
373373
return;
374374
}
@@ -388,7 +388,7 @@ export class ReplayContainer implements ReplayContainerInterface {
388388
this._debouncedFlush.cancel();
389389
// See comment above re: `_isEnabled`, we "force" a flush, ignoring the
390390
// `_isEnabled` state of the plugin since it was disabled above.
391-
if (this.recordingMode === 'session') {
391+
if (forceFlush) {
392392
await this._flush({ force: true });
393393
}
394394

@@ -777,7 +777,7 @@ export class ReplayContainer implements ReplayContainerInterface {
777777
this.session = session;
778778

779779
if (!this.session.sampled) {
780-
void this.stop('session not refreshed');
780+
void this.stop({ reason: 'session not refreshed' });
781781
return false;
782782
}
783783

@@ -1099,7 +1099,7 @@ export class ReplayContainer implements ReplayContainerInterface {
10991099
// This means we retried 3 times and all of them failed,
11001100
// or we ran into a problem we don't want to retry, like rate limiting.
11011101
// In this case, we want to completely stop the replay - otherwise, we may get inconsistent segments
1102-
void this.stop('sendReplay');
1102+
void this.stop({ reason: 'sendReplay' });
11031103

11041104
const client = getCurrentHub().getClient();
11051105

@@ -1223,7 +1223,7 @@ export class ReplayContainer implements ReplayContainerInterface {
12231223

12241224
// Stop replay if over the mutation limit
12251225
if (overMutationLimit) {
1226-
void this.stop('mutationLimit');
1226+
void this.stop({ reason: 'mutationLimit', forceFlush: this.recordingMode === 'session' });
12271227
return false;
12281228
}
12291229

packages/replay/src/types/replay.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -451,7 +451,7 @@ export interface ReplayContainer {
451451
getContext(): InternalEventContext;
452452
initializeSampling(): void;
453453
start(): void;
454-
stop(reason?: string): Promise<void>;
454+
stop(options?: { reason?: string; forceflush?: boolean }): Promise<void>;
455455
pause(): void;
456456
resume(): void;
457457
startRecording(): void;

packages/replay/src/util/addEvent.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ export async function addEvent(
7171
const reason = error && error instanceof EventBufferSizeExceededError ? 'addEventSizeExceeded' : 'addEvent';
7272

7373
__DEBUG_BUILD__ && logger.error(error);
74-
await replay.stop(reason);
74+
await replay.stop({ reason });
7575

7676
const client = getCurrentHub().getClient();
7777

packages/replay/test/integration/flush.test.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -472,10 +472,9 @@ describe('Integration | flush', () => {
472472
const TEST_EVENT = { data: {}, timestamp: BASE_TIMESTAMP + 100, type: 2 };
473473
mockRecord._emitter(TEST_EVENT);
474474

475-
await advanceTimers(DEFAULT_FLUSH_MIN_DELAY);
476475
await advanceTimers(160_000);
477476

478-
expect(mockFlush).toHaveBeenCalledTimes(2);
477+
expect(mockFlush).toHaveBeenCalledTimes(1);
479478
expect(mockSendReplay).toHaveBeenCalledTimes(0);
480479
expect(replay.isEnabled()).toBe(false);
481480

0 commit comments

Comments
 (0)