Skip to content

Commit

Permalink
feat(replay): Add beforeAddRecordingEvent Replay option
Browse files Browse the repository at this point in the history
Allows you to modify/filter recording events for replays. Note this is only a recording event, not the replay event.
  • Loading branch information
billyvg committed May 15, 2023
1 parent ae9ebe3 commit f8cfe73
Show file tree
Hide file tree
Showing 5 changed files with 165 additions and 1 deletion.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## Unreleased

- feat(replay): Add `beforeAddRecordingEvent` Replay option

- "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott

## 7.52.1
Expand Down
3 changes: 3 additions & 0 deletions packages/replay/src/integration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ export class Replay implements Integration {
ignore = [],
maskFn,

beforeAddRecordingEvent,

// eslint-disable-next-line deprecation/deprecation
blockClass,
// eslint-disable-next-line deprecation/deprecation
Expand Down Expand Up @@ -129,6 +131,7 @@ export class Replay implements Integration {
networkCaptureBodies,
networkRequestHeaders: _getMergedNetworkHeaders(networkRequestHeaders),
networkResponseHeaders: _getMergedNetworkHeaders(networkResponseHeaders),
beforeAddRecordingEvent,

_experiments,
};
Expand Down
9 changes: 9 additions & 0 deletions packages/replay/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,10 @@ export interface WorkerResponse {

export type AddEventResult = void;

export interface BeforeAddRecoringEvent {
(event: RecordingEvent): RecordingEvent | null | undefined;
}

export interface ReplayNetworkOptions {
/**
* Capture request/response details for XHR/Fetch requests that match the given URLs.
Expand Down Expand Up @@ -267,6 +271,11 @@ export interface ReplayPluginOptions extends ReplayNetworkOptions {
*/
maskAllText: boolean;

/**
* Callback before adding a recording event
*/
beforeAddRecordingEvent?: BeforeAddRecoringEvent;

/**
* _experiments allows users to enable experimental or internal features.
* We don't consider such features as part of the public API and hence we don't guarantee semver for them.
Expand Down
13 changes: 12 additions & 1 deletion packages/replay/src/util/addEvent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,18 @@ export async function addEvent(
replay.eventBuffer.clear();
}

return await replay.eventBuffer.addEvent(event);
const replayOptions = replay.getOptions();

const eventAfterPossibleCallback =
typeof replayOptions.beforeAddRecordingEvent === 'function'
? replayOptions.beforeAddRecordingEvent(event)
: event;

if (!eventAfterPossibleCallback) {
return;
}

return await replay.eventBuffer.addEvent(eventAfterPossibleCallback);
} catch (error) {
__DEBUG_BUILD__ && logger.error(error);
await replay.stop('addEvent');
Expand Down
139 changes: 139 additions & 0 deletions packages/replay/test/integration/beforeAddRecordingEvent.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import * as SentryCore from '@sentry/core';
import type { Transport } from '@sentry/types';
import * as SentryUtils from '@sentry/utils';

import type { Replay } from '../../src';
import type { ReplayContainer } from '../../src/replay';
import { clearSession } from '../../src/session/clearSession';
import * as SendReplayRequest from '../../src/util/sendReplayRequest';
import { BASE_TIMESTAMP, mockRrweb, mockSdk } from '../index';
import { useFakeTimers } from '../utils/use-fake-timers';

useFakeTimers();

async function advanceTimers(time: number) {
jest.advanceTimersByTime(time);
await new Promise(process.nextTick);
}

type MockTransportSend = jest.MockedFunction<Transport['send']>;

describe('Integration | beforeAddRecordingEvent', () => {
let replay: ReplayContainer;
let integration: Replay;
let mockTransportSend: MockTransportSend;
let mockSendReplayRequest: jest.SpyInstance<any>;
let domHandler: (args: any) => any;
const { record: mockRecord } = mockRrweb();

beforeAll(async () => {
jest.setSystemTime(new Date(BASE_TIMESTAMP));
jest.spyOn(SentryUtils, 'addInstrumentationHandler').mockImplementation((type, handler: (args: any) => any) => {
if (type === 'dom') {
domHandler = handler;
}
});

({ replay, integration } = await mockSdk({
replayOptions: {
beforeAddRecordingEvent: event => {
const eventData = event.data as Record<string, any>;

if (eventData.tag === 'breadcrumb' && eventData.payload.category === 'ui.click') {
return {
...event,
data: {
...eventData,
payload: {
...eventData.payload,
message: 'beforeAddRecordingEvent',
},
},
};
}

if (eventData.tag === 'options') {
return null;
}

return event;
},
_experiments: {
captureExceptions: true,
},
},
}));

mockSendReplayRequest = jest.spyOn(SendReplayRequest, 'sendReplayRequest');

jest.runAllTimers();
mockTransportSend = SentryCore.getCurrentHub()?.getClient()?.getTransport()?.send as MockTransportSend;
});

beforeEach(() => {
jest.setSystemTime(new Date(BASE_TIMESTAMP));
mockRecord.takeFullSnapshot.mockClear();
mockTransportSend.mockClear();

// Create a new session and clear mocks because a segment (from initial
// checkout) will have already been uploaded by the time the tests run
clearSession(replay);
replay['_loadAndCheckSession']();

mockSendReplayRequest.mockClear();
});

afterEach(async () => {
jest.runAllTimers();
await new Promise(process.nextTick);
jest.setSystemTime(new Date(BASE_TIMESTAMP));
clearSession(replay);
replay['_loadAndCheckSession']();
});

afterAll(() => {
integration && integration.stop();
});

it('changes click breadcrumbs message', async () => {
domHandler({
name: 'click',
});

await advanceTimers(5000);

expect(replay).toHaveLastSentReplay({
recordingPayloadHeader: { segment_id: 0 },
recordingData: JSON.stringify([
{
type: 5,
timestamp: BASE_TIMESTAMP,
data: {
tag: 'breadcrumb',
payload: {
timestamp: BASE_TIMESTAMP / 1000,
type: 'default',
category: 'ui.click',
message: 'beforeAddRecordingEvent',
data: {},
},
},
},
]),
});
});

it('filters out the options event', async () => {
mockTransportSend.mockClear();
await integration.stop();

integration.start();

jest.runAllTimers();
await new Promise(process.nextTick);
expect(replay).toHaveLastSentReplay({
recordingPayloadHeader: { segment_id: 0 },
recordingData: JSON.stringify([{ data: { isCheckout: true }, timestamp: BASE_TIMESTAMP, type: 2 }]),
});
});
});

0 comments on commit f8cfe73

Please sign in to comment.