Skip to content

Commit bedb0fe

Browse files
committed
feat(replay): Add event to capture options on checkouts
Add a custom event that captures configuration options on checkout + segment 0.
1 parent 9ea2bca commit bedb0fe

File tree

12 files changed

+138
-45
lines changed

12 files changed

+138
-45
lines changed

packages/replay/src/eventBuffer/EventBufferArray.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { AddEventResult, EventBuffer, RecordingEvent } from '../types';
1+
import type { AddEventResult, EventBuffer, EventBufferType, RecordingEvent } from '../types';
22
import { timestampToMs } from '../util/timestampToMs';
33

44
/**
@@ -18,6 +18,11 @@ export class EventBufferArray implements EventBuffer {
1818
return this.events.length > 0;
1919
}
2020

21+
/** @inheritdoc */
22+
public get type(): EventBufferType {
23+
return 'default';
24+
}
25+
2126
/** @inheritdoc */
2227
public destroy(): void {
2328
this.events = [];

packages/replay/src/eventBuffer/EventBufferCompressionWorker.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { ReplayRecordingData } from '@sentry/types';
22

3-
import type { AddEventResult, EventBuffer, RecordingEvent } from '../types';
3+
import type { AddEventResult, EventBuffer, EventBufferType, RecordingEvent } from '../types';
44
import { timestampToMs } from '../util/timestampToMs';
55
import { WorkerHandler } from './WorkerHandler';
66

@@ -22,6 +22,11 @@ export class EventBufferCompressionWorker implements EventBuffer {
2222
return !!this._earliestTimestamp;
2323
}
2424

25+
/** @inheritdoc */
26+
public get type(): EventBufferType {
27+
return 'worker';
28+
}
29+
2530
/**
2631
* Ensure the worker is ready (or not).
2732
* This will either resolve when the worker is ready, or reject if an error occured.

packages/replay/src/eventBuffer/EventBufferProxy.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { ReplayRecordingData } from '@sentry/types';
22
import { logger } from '@sentry/utils';
33

4-
import type { AddEventResult, EventBuffer, RecordingEvent } from '../types';
4+
import type { AddEventResult, EventBuffer, EventBufferType, RecordingEvent } from '../types';
55
import { EventBufferArray } from './EventBufferArray';
66
import { EventBufferCompressionWorker } from './EventBufferCompressionWorker';
77

@@ -24,6 +24,11 @@ export class EventBufferProxy implements EventBuffer {
2424
this._ensureWorkerIsLoadedPromise = this._ensureWorkerIsLoaded();
2525
}
2626

27+
/** @inheritdoc */
28+
public get type(): EventBufferType {
29+
return this._used.type;
30+
}
31+
2732
/** @inheritDoc */
2833
public get hasEvents(): boolean {
2934
return this._used.hasEvents;

packages/replay/src/integration.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,8 @@ export class Replay implements Integration {
123123
errorSampleRate,
124124
useCompression,
125125
blockAllMedia,
126+
maskAllInputs,
127+
maskAllText,
126128
networkDetailAllowUrls,
127129
networkCaptureBodies,
128130
networkRequestHeaders: _getMergedNetworkHeaders(networkRequestHeaders),

packages/replay/src/types.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,16 @@ export interface ReplayPluginOptions extends ReplayNetworkOptions {
257257
*/
258258
blockAllMedia: boolean;
259259

260+
/**
261+
* Mask all inputs in recordings
262+
*/
263+
maskAllInputs: boolean;
264+
265+
/**
266+
* Mask all text in recordings
267+
*/
268+
maskAllText: boolean;
269+
260270
/**
261271
* _experiments allows users to enable experimental or internal features.
262272
* We don't consider such features as part of the public API and hence we don't guarantee semver for them.
@@ -435,12 +445,19 @@ export interface Session {
435445
shouldRefresh: boolean;
436446
}
437447

448+
export type EventBufferType = 'default' | 'worker';
449+
438450
export interface EventBuffer {
439451
/**
440452
* If any events have been added to the buffer.
441453
*/
442454
readonly hasEvents: boolean;
443455

456+
/**
457+
* The buffer type
458+
*/
459+
readonly type: EventBufferType;
460+
444461
/**
445462
* Destroy the event buffer.
446463
*/

packages/replay/src/types/rrweb.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
type blockClass = string | RegExp;
44
type maskTextClass = string | RegExp;
55

6-
enum EventType {
6+
export enum EventType {
77
DomContentLoaded = 0,
88
Load = 1,
99
FullSnapshot = 2,

packages/replay/src/util/handleRecordingEmit.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { logger } from '@sentry/utils';
22

33
import { saveSession } from '../session/saveSession';
44
import type { RecordingEvent, ReplayContainer } from '../types';
5+
import { EventType } from '../types/rrweb';
56
import { addEvent } from './addEvent';
67

78
type RecordingEmitCallback = (event: RecordingEvent, isCheckout?: boolean) => void;
@@ -48,6 +49,14 @@ export function getHandleRecordingEmit(replay: ReplayContainer): RecordingEmitCa
4849
return false;
4950
}
5051

52+
// Additionally, create a meta event that will capture certain SDK settings.
53+
// In order to handle buffer mode, this needs to either be done when we
54+
// receive checkout events or at flush time.
55+
//
56+
// `isCheckout` is always true, but want to be explicit that it should
57+
// only be added for checkouts
58+
void addSettingsEvent(replay, isCheckout);
59+
5160
// If there is a previousSessionId after a full snapshot occurs, then
5261
// the replay session was started due to session expiration. The new session
5362
// is started before triggering a new checkout and contains the id
@@ -84,3 +93,38 @@ export function getHandleRecordingEmit(replay: ReplayContainer): RecordingEmitCa
8493
});
8594
};
8695
}
96+
97+
/**
98+
* Add an event to the event buffer.
99+
* `isCheckout` is true if this is either the very first event, or an event triggered by `checkoutEveryNms`.
100+
*/
101+
export async function addSettingsEvent(replay: ReplayContainer, isCheckout?: boolean): Promise<void | null> {
102+
// Only need to add this event when sending the first segment
103+
if (!isCheckout || !replay.session || replay.session.segmentId !== 0) {
104+
return null;
105+
}
106+
107+
const options = replay.getOptions();
108+
const event = {
109+
type: EventType.Custom,
110+
timestamp: new Date().getTime(),
111+
data: {
112+
tag: 'options',
113+
payload: {
114+
sessionSampleRate: options.sessionSampleRate,
115+
errorSampleRate: options.errorSampleRate,
116+
useCompressionOption: options.useCompression,
117+
blockAllMedia: options.blockAllMedia,
118+
maskAllText: options.maskAllText,
119+
maskAllInputs: options.maskAllInputs,
120+
useCompression: replay.eventBuffer && replay.eventBuffer.type === 'worker',
121+
networkDetailHasUrls: options.networkDetailAllowUrls.length > 0,
122+
networkCaptureBodies: options.networkCaptureBodies,
123+
networkRequestHeaders: options.networkRequestHeaders.length > 0,
124+
networkResponseHeaders: options.networkResponseHeaders.length > 0,
125+
},
126+
},
127+
};
128+
129+
return addEvent(replay, event, true);
130+
}

packages/replay/src/util/sendReplayRequest.ts

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ export async function sendReplayRequest({
1818
eventContext,
1919
timestamp,
2020
session,
21-
options,
2221
}: SendReplayData): Promise<void | TransportMakeRequestResponse> {
2322
const preparedRecordingData = prepareRecordingData({
2423
recordingData,
@@ -60,15 +59,6 @@ export async function sendReplayRequest({
6059
return;
6160
}
6261

63-
replayEvent.contexts = {
64-
...replayEvent.contexts,
65-
replay: {
66-
...(replayEvent.contexts && replayEvent.contexts.replay),
67-
session_sample_rate: options.sessionSampleRate,
68-
error_sample_rate: options.errorSampleRate,
69-
},
70-
};
71-
7262
/*
7363
For reference, the fully built event looks something like this:
7464
{
@@ -99,10 +89,7 @@ export async function sendReplayRequest({
9989
},
10090
"sdkProcessingMetadata": {},
10191
"contexts": {
102-
"replay": {
103-
"session_sample_rate": 1,
104-
"error_sample_rate": 0,
105-
},
92+
"replay": { },
10693
},
10794
}
10895
*/

packages/replay/test/integration/errorSampleRate.test.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -183,12 +183,6 @@ describe('Integration | errorSampleRate', () => {
183183
recordingPayloadHeader: { segment_id: 0 },
184184
replayEventPayload: expect.objectContaining({
185185
replay_type: 'buffer',
186-
contexts: {
187-
replay: {
188-
error_sample_rate: 1,
189-
session_sample_rate: 0,
190-
},
191-
},
192186
}),
193187
recordingData: JSON.stringify([
194188
{ data: { isCheckout: true }, timestamp: BASE_TIMESTAMP, type: 2 },

packages/replay/test/integration/session.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { createPerformanceSpans } from '../../src/util/createPerformanceSpans';
1717
import { BASE_TIMESTAMP } from '../index';
1818
import type { RecordMock } from '../mocks/mockRrweb';
1919
import { resetSdkMock } from '../mocks/resetSdkMock';
20+
import { DEFAULT_OPTIONS_EVENT_PAYLOAD } from '../utils/setupReplayContainer';
2021
import { useFakeTimers } from '../utils/use-fake-timers';
2122

2223
useFakeTimers();
@@ -388,7 +389,7 @@ describe('Integration | session', () => {
388389
expect(replay).toHaveLastSentReplay({
389390
recordingPayloadHeader: { segment_id: 0 },
390391
recordingData: JSON.stringify([
391-
{ data: { isCheckout: true }, timestamp: newTimestamp, type: 2 },
392+
{ type: 5, timestamp: newTimestamp, data: { tag: 'options', payload: DEFAULT_OPTIONS_EVENT_PAYLOAD } },
392393
{
393394
type: 5,
394395
timestamp: newTimestamp,

packages/replay/test/unit/util/handleRecordingEmit.test.ts

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,20 @@ import { EventType } from '@sentry-internal/rrweb';
33
import { BASE_TIMESTAMP } from '../..';
44
import * as SentryAddEvent from '../../../src/util/addEvent';
55
import { getHandleRecordingEmit } from '../../../src/util/handleRecordingEmit';
6-
import { setupReplayContainer } from '../../utils/setupReplayContainer';
6+
import { DEFAULT_OPTIONS_EVENT_PAYLOAD, setupReplayContainer } from '../../utils/setupReplayContainer';
77
import { useFakeTimers } from '../../utils/use-fake-timers';
88

99
useFakeTimers();
1010

11+
const optionsEvent = {
12+
type: 5,
13+
data: {
14+
tag: 'options',
15+
payload: DEFAULT_OPTIONS_EVENT_PAYLOAD,
16+
},
17+
timestamp: BASE_TIMESTAMP,
18+
};
19+
1120
describe('Unit | util | handleRecordingEmit', () => {
1221
let addEventMock: jest.SpyInstance;
1322

@@ -43,13 +52,14 @@ describe('Unit | util | handleRecordingEmit', () => {
4352
handler(event);
4453
await new Promise(process.nextTick);
4554

46-
expect(addEventMock).toBeCalledTimes(1);
47-
expect(addEventMock).toHaveBeenLastCalledWith(replay, event, true);
55+
expect(addEventMock).toBeCalledTimes(2);
56+
expect(addEventMock).toHaveBeenNthCalledWith(1, replay, event, true);
57+
expect(addEventMock).toHaveBeenLastCalledWith(replay, optionsEvent, true);
4858

4959
handler(event);
5060
await new Promise(process.nextTick);
5161

52-
expect(addEventMock).toBeCalledTimes(2);
62+
expect(addEventMock).toBeCalledTimes(3);
5363
expect(addEventMock).toHaveBeenLastCalledWith(replay, event, false);
5464
});
5565

@@ -74,13 +84,16 @@ describe('Unit | util | handleRecordingEmit', () => {
7484
handler(event, true);
7585
await new Promise(process.nextTick);
7686

77-
expect(addEventMock).toBeCalledTimes(1);
78-
expect(addEventMock).toHaveBeenLastCalledWith(replay, event, true);
87+
// Called twice, once for event and once for settings on checkout only
88+
expect(addEventMock).toBeCalledTimes(2);
89+
expect(addEventMock).toHaveBeenNthCalledWith(1, replay, event, true);
90+
expect(addEventMock).toHaveBeenLastCalledWith(replay, optionsEvent, true);
7991

8092
handler(event, true);
8193
await new Promise(process.nextTick);
8294

83-
expect(addEventMock).toBeCalledTimes(2);
84-
expect(addEventMock).toHaveBeenLastCalledWith(replay, event, true);
95+
expect(addEventMock).toBeCalledTimes(4);
96+
expect(addEventMock).toHaveBeenNthCalledWith(3, replay, event, true);
97+
expect(addEventMock).toHaveBeenLastCalledWith(replay, { ...optionsEvent, timestamp: BASE_TIMESTAMP + 20 }, true);
8598
});
8699
});

packages/replay/test/utils/setupReplayContainer.ts

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,30 @@ import { ReplayContainer } from '../../src/replay';
33
import { clearSession } from '../../src/session/clearSession';
44
import type { RecordingOptions, ReplayPluginOptions } from '../../src/types';
55

6+
const DEFAULT_OPTIONS = {
7+
flushMinDelay: 100,
8+
flushMaxDelay: 100,
9+
stickySession: false,
10+
sessionSampleRate: 0,
11+
errorSampleRate: 1,
12+
useCompression: false,
13+
blockAllMedia: true,
14+
networkDetailAllowUrls: [],
15+
networkCaptureBodies: true,
16+
networkRequestHeaders: [],
17+
networkResponseHeaders: [],
18+
_experiments: {},
19+
};
20+
621
export function setupReplayContainer({
722
options,
823
recordingOptions,
924
}: { options?: Partial<ReplayPluginOptions>; recordingOptions?: Partial<RecordingOptions> } = {}): ReplayContainer {
1025
const replay = new ReplayContainer({
1126
options: {
12-
flushMinDelay: 100,
13-
flushMaxDelay: 100,
14-
stickySession: false,
15-
sessionSampleRate: 0,
16-
errorSampleRate: 1,
17-
useCompression: false,
18-
blockAllMedia: true,
19-
networkDetailAllowUrls: [],
20-
networkCaptureBodies: true,
21-
networkRequestHeaders: [],
22-
networkResponseHeaders: [],
23-
_experiments: {},
27+
...DEFAULT_OPTIONS,
28+
maskAllInputs: !!recordingOptions?.maskAllInputs,
29+
maskAllText: !!recordingOptions?.maskAllText,
2430
...options,
2531
},
2632
recordingOptions: {
@@ -39,3 +45,17 @@ export function setupReplayContainer({
3945

4046
return replay;
4147
}
48+
49+
export const DEFAULT_OPTIONS_EVENT_PAYLOAD = {
50+
sessionSampleRate: DEFAULT_OPTIONS.sessionSampleRate,
51+
errorSampleRate: DEFAULT_OPTIONS.errorSampleRate,
52+
useCompressionOption: false,
53+
blockAllMedia: DEFAULT_OPTIONS.blockAllMedia,
54+
maskAllText: false,
55+
maskAllInputs: false,
56+
useCompression: DEFAULT_OPTIONS.useCompression,
57+
networkDetailHasUrls: DEFAULT_OPTIONS.networkDetailAllowUrls.length > 0,
58+
networkCaptureBodies: DEFAULT_OPTIONS.networkCaptureBodies,
59+
networkRequestHeaders: DEFAULT_OPTIONS.networkRequestHeaders.length > 0,
60+
networkResponseHeaders: DEFAULT_OPTIONS.networkResponseHeaders.length > 0,
61+
};

0 commit comments

Comments
 (0)