Skip to content

Commit f8cfe73

Browse files
committed
feat(replay): Add beforeAddRecordingEvent Replay option
Allows you to modify/filter recording events for replays. Note this is only a recording event, not the replay event.
1 parent ae9ebe3 commit f8cfe73

File tree

5 files changed

+165
-1
lines changed

5 files changed

+165
-1
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
## Unreleased
44

5+
- feat(replay): Add `beforeAddRecordingEvent` Replay option
6+
57
- "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott
68

79
## 7.52.1

packages/replay/src/integration.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@ export class Replay implements Integration {
7272
ignore = [],
7373
maskFn,
7474

75+
beforeAddRecordingEvent,
76+
7577
// eslint-disable-next-line deprecation/deprecation
7678
blockClass,
7779
// eslint-disable-next-line deprecation/deprecation
@@ -129,6 +131,7 @@ export class Replay implements Integration {
129131
networkCaptureBodies,
130132
networkRequestHeaders: _getMergedNetworkHeaders(networkRequestHeaders),
131133
networkResponseHeaders: _getMergedNetworkHeaders(networkResponseHeaders),
134+
beforeAddRecordingEvent,
132135

133136
_experiments,
134137
};

packages/replay/src/types.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,10 @@ export interface WorkerResponse {
183183

184184
export type AddEventResult = void;
185185

186+
export interface BeforeAddRecoringEvent {
187+
(event: RecordingEvent): RecordingEvent | null | undefined;
188+
}
189+
186190
export interface ReplayNetworkOptions {
187191
/**
188192
* Capture request/response details for XHR/Fetch requests that match the given URLs.
@@ -267,6 +271,11 @@ export interface ReplayPluginOptions extends ReplayNetworkOptions {
267271
*/
268272
maskAllText: boolean;
269273

274+
/**
275+
* Callback before adding a recording event
276+
*/
277+
beforeAddRecordingEvent?: BeforeAddRecoringEvent;
278+
270279
/**
271280
* _experiments allows users to enable experimental or internal features.
272281
* We don't consider such features as part of the public API and hence we don't guarantee semver for them.

packages/replay/src/util/addEvent.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,18 @@ export async function addEvent(
3838
replay.eventBuffer.clear();
3939
}
4040

41-
return await replay.eventBuffer.addEvent(event);
41+
const replayOptions = replay.getOptions();
42+
43+
const eventAfterPossibleCallback =
44+
typeof replayOptions.beforeAddRecordingEvent === 'function'
45+
? replayOptions.beforeAddRecordingEvent(event)
46+
: event;
47+
48+
if (!eventAfterPossibleCallback) {
49+
return;
50+
}
51+
52+
return await replay.eventBuffer.addEvent(eventAfterPossibleCallback);
4253
} catch (error) {
4354
__DEBUG_BUILD__ && logger.error(error);
4455
await replay.stop('addEvent');
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import * as SentryCore from '@sentry/core';
2+
import type { Transport } from '@sentry/types';
3+
import * as SentryUtils from '@sentry/utils';
4+
5+
import type { Replay } from '../../src';
6+
import type { ReplayContainer } from '../../src/replay';
7+
import { clearSession } from '../../src/session/clearSession';
8+
import * as SendReplayRequest from '../../src/util/sendReplayRequest';
9+
import { BASE_TIMESTAMP, mockRrweb, mockSdk } from '../index';
10+
import { useFakeTimers } from '../utils/use-fake-timers';
11+
12+
useFakeTimers();
13+
14+
async function advanceTimers(time: number) {
15+
jest.advanceTimersByTime(time);
16+
await new Promise(process.nextTick);
17+
}
18+
19+
type MockTransportSend = jest.MockedFunction<Transport['send']>;
20+
21+
describe('Integration | beforeAddRecordingEvent', () => {
22+
let replay: ReplayContainer;
23+
let integration: Replay;
24+
let mockTransportSend: MockTransportSend;
25+
let mockSendReplayRequest: jest.SpyInstance<any>;
26+
let domHandler: (args: any) => any;
27+
const { record: mockRecord } = mockRrweb();
28+
29+
beforeAll(async () => {
30+
jest.setSystemTime(new Date(BASE_TIMESTAMP));
31+
jest.spyOn(SentryUtils, 'addInstrumentationHandler').mockImplementation((type, handler: (args: any) => any) => {
32+
if (type === 'dom') {
33+
domHandler = handler;
34+
}
35+
});
36+
37+
({ replay, integration } = await mockSdk({
38+
replayOptions: {
39+
beforeAddRecordingEvent: event => {
40+
const eventData = event.data as Record<string, any>;
41+
42+
if (eventData.tag === 'breadcrumb' && eventData.payload.category === 'ui.click') {
43+
return {
44+
...event,
45+
data: {
46+
...eventData,
47+
payload: {
48+
...eventData.payload,
49+
message: 'beforeAddRecordingEvent',
50+
},
51+
},
52+
};
53+
}
54+
55+
if (eventData.tag === 'options') {
56+
return null;
57+
}
58+
59+
return event;
60+
},
61+
_experiments: {
62+
captureExceptions: true,
63+
},
64+
},
65+
}));
66+
67+
mockSendReplayRequest = jest.spyOn(SendReplayRequest, 'sendReplayRequest');
68+
69+
jest.runAllTimers();
70+
mockTransportSend = SentryCore.getCurrentHub()?.getClient()?.getTransport()?.send as MockTransportSend;
71+
});
72+
73+
beforeEach(() => {
74+
jest.setSystemTime(new Date(BASE_TIMESTAMP));
75+
mockRecord.takeFullSnapshot.mockClear();
76+
mockTransportSend.mockClear();
77+
78+
// Create a new session and clear mocks because a segment (from initial
79+
// checkout) will have already been uploaded by the time the tests run
80+
clearSession(replay);
81+
replay['_loadAndCheckSession']();
82+
83+
mockSendReplayRequest.mockClear();
84+
});
85+
86+
afterEach(async () => {
87+
jest.runAllTimers();
88+
await new Promise(process.nextTick);
89+
jest.setSystemTime(new Date(BASE_TIMESTAMP));
90+
clearSession(replay);
91+
replay['_loadAndCheckSession']();
92+
});
93+
94+
afterAll(() => {
95+
integration && integration.stop();
96+
});
97+
98+
it('changes click breadcrumbs message', async () => {
99+
domHandler({
100+
name: 'click',
101+
});
102+
103+
await advanceTimers(5000);
104+
105+
expect(replay).toHaveLastSentReplay({
106+
recordingPayloadHeader: { segment_id: 0 },
107+
recordingData: JSON.stringify([
108+
{
109+
type: 5,
110+
timestamp: BASE_TIMESTAMP,
111+
data: {
112+
tag: 'breadcrumb',
113+
payload: {
114+
timestamp: BASE_TIMESTAMP / 1000,
115+
type: 'default',
116+
category: 'ui.click',
117+
message: 'beforeAddRecordingEvent',
118+
data: {},
119+
},
120+
},
121+
},
122+
]),
123+
});
124+
});
125+
126+
it('filters out the options event', async () => {
127+
mockTransportSend.mockClear();
128+
await integration.stop();
129+
130+
integration.start();
131+
132+
jest.runAllTimers();
133+
await new Promise(process.nextTick);
134+
expect(replay).toHaveLastSentReplay({
135+
recordingPayloadHeader: { segment_id: 0 },
136+
recordingData: JSON.stringify([{ data: { isCheckout: true }, timestamp: BASE_TIMESTAMP, type: 2 }]),
137+
});
138+
});
139+
});

0 commit comments

Comments
 (0)