Skip to content

Commit 84295c4

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 84295c4

File tree

5 files changed

+171
-1
lines changed

5 files changed

+171
-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,8 +38,19 @@ export async function addEvent(
3838
replay.eventBuffer.clear();
3939
}
4040

41-
return await replay.eventBuffer.addEvent(event);
41+
const replayOptions = replay.getOptions();
42+
43+
console.log('add event: ', typeof replayOptions.beforeAddRecordingEvent, event.type)
44+
const eventAfterPossibleCallback = typeof replayOptions.beforeAddRecordingEvent === 'function' ?
45+
replayOptions.beforeAddRecordingEvent(event) : event;
46+
47+
if (!eventAfterPossibleCallback) {
48+
return;
49+
}
50+
51+
return await replay.eventBuffer.addEvent(eventAfterPossibleCallback);
4252
} catch (error) {
53+
console.error(error)
4354
__DEBUG_BUILD__ && logger.error(error);
4455
await replay.stop('addEvent');
4556

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

0 commit comments

Comments
 (0)