Skip to content

Commit 7e93d4b

Browse files
committed
feat(replay): Stop without retry when receiving bad API response
1 parent 56d3a74 commit 7e93d4b

File tree

7 files changed

+74
-10
lines changed

7 files changed

+74
-10
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<meta charset="utf-8" />
5+
</head>
6+
<body>
7+
<button onclick="console.log('Test log')">Click me</button>
8+
</body>
9+
</html>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { expect } from '@playwright/test';
2+
3+
import { sentryTest } from '../../../utils/fixtures';
4+
import { getReplaySnapshot } from '../../../utils/helpers';
5+
6+
sentryTest('errorResponse', async ({ getLocalTestPath, page }) => {
7+
// Currently bundle tests are not supported for replay
8+
if (process.env.PW_BUNDLE && process.env.PW_BUNDLE.startsWith('bundle_')) {
9+
sentryTest.skip();
10+
}
11+
12+
let called = 0;
13+
14+
await page.route('https://dsn.ingest.sentry.io/**/*', route => {
15+
called++;
16+
17+
return route.fulfill({
18+
status: 400,
19+
});
20+
});
21+
22+
const url = await getLocalTestPath({ testDir: __dirname });
23+
await page.goto(url);
24+
25+
await page.click('button');
26+
await page.waitForTimeout(300);
27+
28+
expect(called).toBe(1);
29+
30+
// Should immediately skip retrying and just cancel, no backoff
31+
await page.waitForTimeout(5001);
32+
33+
expect(called).toBe(1);
34+
const replay = await getReplaySnapshot(page);
35+
36+
expect(replay['_isEnabled']).toBe(false);
37+
});

packages/replay/src/replay.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,7 @@ export class ReplayContainer implements ReplayContainerInterface {
234234
this._stopRecording?.();
235235
this.eventBuffer?.destroy();
236236
this.eventBuffer = null;
237+
this._debouncedFlush.cancel();
237238
} catch (err) {
238239
this._handleException(err);
239240
}
@@ -907,7 +908,7 @@ export class ReplayContainer implements ReplayContainerInterface {
907908
if (rateLimitDuration > 0) {
908909
__DEBUG_BUILD__ && logger.warn('[Replay]', `Rate limit hit, pausing replay for ${rateLimitDuration}ms`);
909910
this.pause();
910-
this._debouncedFlush && this._debouncedFlush.cancel();
911+
this._debouncedFlush.cancel();
911912

912913
setTimeout(() => {
913914
__DEBUG_BUILD__ && logger.info('[Replay]', 'Resuming replay after rate limit');

packages/replay/src/util/sendReplay.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { captureException, setContext } from '@sentry/core';
22

33
import { RETRY_BASE_INTERVAL, RETRY_MAX_COUNT, UNABLE_TO_SEND_REPLAY } from '../constants';
44
import type { SendReplayData } from '../types';
5-
import { RateLimitError, sendReplayRequest } from './sendReplayRequest';
5+
import { RateLimitError, sendReplayRequest, TransportStatusCodeError } from './sendReplayRequest';
66

77
/**
88
* Finalize and send the current replay event to Sentry
@@ -25,7 +25,7 @@ export async function sendReplay(
2525
await sendReplayRequest(replayData);
2626
return true;
2727
} catch (err) {
28-
if (err instanceof RateLimitError) {
28+
if (err instanceof RateLimitError || err instanceof TransportStatusCodeError) {
2929
throw err;
3030
}
3131

packages/replay/src/util/sendReplayRequest.ts

+22-5
Original file line numberDiff line numberDiff line change
@@ -116,12 +116,20 @@ export async function sendReplayRequest({
116116
}
117117

118118
// TODO (v8): we can remove this guard once transport.send's type signature doesn't include void anymore
119-
if (response) {
120-
const rateLimits = updateRateLimits({}, response);
121-
if (isRateLimited(rateLimits, 'replay')) {
122-
throw new RateLimitError(rateLimits);
123-
}
119+
if (!response) {
120+
return response;
124121
}
122+
123+
const rateLimits = updateRateLimits({}, response);
124+
if (isRateLimited(rateLimits, 'replay')) {
125+
throw new RateLimitError(rateLimits);
126+
}
127+
128+
// If the status code is invalid, we want to immediately stop & not retry
129+
if (typeof response.statusCode === 'number' && (response.statusCode < 200 || response.statusCode >= 300)) {
130+
throw new TransportStatusCodeError(response.statusCode);
131+
}
132+
125133
return response;
126134
}
127135

@@ -136,3 +144,12 @@ export class RateLimitError extends Error {
136144
this.rateLimits = rateLimits;
137145
}
138146
}
147+
148+
/**
149+
* This error indicates that the transport returned an invalid status code.
150+
*/
151+
export class TransportStatusCodeError extends Error {
152+
public constructor(statusCode: number) {
153+
super(`Transport returned status code ${statusCode}`);
154+
}
155+
}

packages/replay/test/integration/coreHandlers/handleGlobalEvent.test.ts

-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ describe('Integration | coreHandlers | handleGlobalEvent', () => {
2626
replaysSessionSampleRate: 0.0,
2727
replaysOnErrorSampleRate: 1.0,
2828
},
29-
autoStart: false,
3029
}));
3130
});
3231

packages/replay/test/mocks/resetSdkMock.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import type { DomHandler } from './../types';
55
import type { MockSdkParams } from './mockSdk';
66
import { mockSdk } from './mockSdk';
77

8-
export async function resetSdkMock({ replayOptions, sentryOptions }: MockSdkParams): Promise<{
8+
export async function resetSdkMock({ replayOptions, sentryOptions, autoStart }: MockSdkParams): Promise<{
99
domHandler: DomHandler;
1010
mockRecord: RecordMock;
1111
replay: ReplayContainer;
@@ -30,6 +30,7 @@ export async function resetSdkMock({ replayOptions, sentryOptions }: MockSdkPara
3030
const { replay } = await mockSdk({
3131
replayOptions,
3232
sentryOptions,
33+
autoStart,
3334
});
3435

3536
// XXX: This is needed to ensure `domHandler` is set

0 commit comments

Comments
 (0)