Skip to content

Commit c02c204

Browse files
committed
fix(replay): Ensure replay_id is removed from frozen DSC
1 parent 3f0926e commit c02c204

File tree

5 files changed

+108
-3
lines changed

5 files changed

+108
-3
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import * as Sentry from '@sentry/browser';
2+
3+
window._triggerError = function (errorCount) {
4+
Sentry.captureException(new Error(`This is error #${errorCount}`));
5+
};

dev-packages/browser-integration-tests/suites/replay/dsc/test.ts

+70-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,12 @@ import type * as Sentry from '@sentry/browser';
33
import type { EventEnvelopeHeaders } from '@sentry/types';
44

55
import { sentryTest } from '../../../utils/fixtures';
6-
import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../utils/helpers';
6+
import {
7+
envelopeRequestParser,
8+
shouldSkipTracingTest,
9+
waitForErrorRequest,
10+
waitForTransactionRequest,
11+
} from '../../../utils/helpers';
712
import { getReplaySnapshot, shouldSkipReplayTest, waitForReplayRunning } from '../../../utils/replayHelpers';
813

914
type TestWindow = Window & {
@@ -216,3 +221,67 @@ sentryTest(
216221
});
217222
},
218223
);
224+
225+
sentryTest('should add replay_id to error DSC while replay is active', async ({ getLocalTestPath, page }) => {
226+
if (shouldSkipReplayTest()) {
227+
sentryTest.skip();
228+
}
229+
230+
await page.route('https://dsn.ingest.sentry.io/**/*', route => {
231+
return route.fulfill({
232+
status: 200,
233+
contentType: 'application/json',
234+
body: JSON.stringify({ id: 'test-id' }),
235+
});
236+
});
237+
238+
const url = await getLocalTestPath({ testDir: __dirname });
239+
await page.goto(url);
240+
241+
const error1Req = waitForErrorRequest(page, event => event.exception?.values?.[0].value === 'This is error #1');
242+
const error2Req = waitForErrorRequest(page, event => event.exception?.values?.[0].value === 'This is error #2');
243+
244+
// We want to wait for the transaction to be done, to ensure we have a consistent test
245+
const transactionReq = shouldSkipTracingTest() ? Promise.resolve() : waitForTransactionRequest(page);
246+
247+
// Wait for this to be available
248+
await page.waitForFunction('!!window.Replay');
249+
250+
// We have to start replay before we finish the transaction, otherwise the DSC will not be frozen with the Replay ID
251+
await page.evaluate('window.Replay.start();');
252+
await waitForReplayRunning(page);
253+
await transactionReq;
254+
255+
await page.evaluate('window._triggerError(1)');
256+
257+
const error1Header = envelopeRequestParser(await error1Req, 0) as EventEnvelopeHeaders;
258+
const replay = await getReplaySnapshot(page);
259+
260+
expect(replay.session?.id).toBeDefined();
261+
262+
expect(error1Header.trace).toBeDefined();
263+
expect(error1Header.trace).toEqual({
264+
environment: 'production',
265+
sample_rate: '1',
266+
trace_id: expect.any(String),
267+
public_key: 'public',
268+
replay_id: replay.session?.id,
269+
sampled: 'true',
270+
});
271+
272+
// Now end replay and trigger another error, it should not have a replay_id in DSC anymore
273+
await page.evaluate('window.Replay.stop();');
274+
await page.waitForFunction('!window.Replay.getReplayId();');
275+
await page.evaluate('window._triggerError(2)');
276+
277+
const error2Header = envelopeRequestParser(await error2Req, 0) as EventEnvelopeHeaders;
278+
279+
expect(error2Header.trace).toBeDefined();
280+
expect(error2Header.trace).toEqual({
281+
environment: 'production',
282+
sample_rate: '1',
283+
trace_id: expect.any(String),
284+
public_key: 'public',
285+
sampled: 'true',
286+
});
287+
});

dev-packages/browser-integration-tests/utils/helpers.ts

+10-2
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,7 @@ export async function waitForTransactionRequestOnUrl(page: Page, url: string): P
199199
return req;
200200
}
201201

202-
export function waitForErrorRequest(page: Page): Promise<Request> {
202+
export function waitForErrorRequest(page: Page, callback?: (event: Event) => boolean): Promise<Request> {
203203
return page.waitForRequest(req => {
204204
const postData = req.postData();
205205
if (!postData) {
@@ -209,7 +209,15 @@ export function waitForErrorRequest(page: Page): Promise<Request> {
209209
try {
210210
const event = envelopeRequestParser(req);
211211

212-
return !event.type;
212+
if (event.type) {
213+
return false;
214+
}
215+
216+
if (callback) {
217+
return callback(event);
218+
}
219+
220+
return true;
213221
} catch {
214222
return false;
215223
}

packages/replay-internal/src/replay.ts

+3
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ import { debounce } from './util/debounce';
5353
import { getHandleRecordingEmit } from './util/handleRecordingEmit';
5454
import { isExpired } from './util/isExpired';
5555
import { isSessionExpired } from './util/isSessionExpired';
56+
import { resetReplayIdOnDynamicSamplingContext } from './util/resetReplayIdOnDynamicSamplingContext';
5657
import { sendReplay } from './util/sendReplay';
5758
import { RateLimitError } from './util/sendReplayRequest';
5859
import type { SKIPPED } from './util/throttle';
@@ -446,6 +447,8 @@ export class ReplayContainer implements ReplayContainerInterface {
446447
try {
447448
DEBUG_BUILD && logger.info(`Stopping Replay${reason ? ` triggered by ${reason}` : ''}`);
448449

450+
resetReplayIdOnDynamicSamplingContext();
451+
449452
this._removeListeners();
450453
this.stopRecording();
451454

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { getActiveSpan, getCurrentScope, getDynamicSamplingContextFromSpan } from '@sentry/core';
2+
import type { DynamicSamplingContext } from '@sentry/types';
3+
4+
/**
5+
* Reset the `replay_id` field on the DSC.
6+
*/
7+
export function resetReplayIdOnDynamicSamplingContext(): void {
8+
// Reset DSC on the current scope, if there is one
9+
const dsc = getCurrentScope().getPropagationContext().dsc;
10+
if (dsc) {
11+
delete dsc.replay_id;
12+
}
13+
14+
// Clear it from frozen DSC on the active span
15+
const activeSpan = getActiveSpan();
16+
if (activeSpan) {
17+
const dsc = getDynamicSamplingContextFromSpan(activeSpan);
18+
delete (dsc as Partial<DynamicSamplingContext>).replay_id;
19+
}
20+
}

0 commit comments

Comments
 (0)