Skip to content

Commit e7b0fad

Browse files
committed
feat(replay): Add option to pass in custom record fn
Bringing back #8647 but it would be nice to test new versions of rrweb without having to bundle it with our replay SDK.
1 parent b2b2ee5 commit e7b0fad

File tree

7 files changed

+77
-38
lines changed

7 files changed

+77
-38
lines changed

packages/replay-internal/src/coreHandlers/handleClick.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { setTimeout } from '@sentry-internal/browser-utils';
2-
import { IncrementalSource, MouseInteractions, record } from '@sentry-internal/rrweb';
2+
import { IncrementalSource, MouseInteractions } from '@sentry-internal/rrweb';
3+
import type { Mirror } from '@sentry-internal/rrweb-snapshot';
34
import type { Breadcrumb } from '@sentry/types';
45

56
import { WINDOW } from '../constants';
@@ -309,7 +310,11 @@ function nowInSeconds(): number {
309310
}
310311

311312
/** Update the click detector based on a recording event of rrweb. */
312-
export function updateClickDetectorForRecordingEvent(clickDetector: ReplayClickDetector, event: RecordingEvent): void {
313+
export function updateClickDetectorForRecordingEvent(
314+
clickDetector: ReplayClickDetector,
315+
event: RecordingEvent,
316+
mirror: Mirror,
317+
): void {
313318
try {
314319
// note: We only consider incremental snapshots here
315320
// This means that any full snapshot is ignored for mutation detection - the reason is that we simply cannot know if a mutation happened here.
@@ -334,7 +339,7 @@ export function updateClickDetectorForRecordingEvent(clickDetector: ReplayClickD
334339

335340
if (isIncrementalMouseInteraction(event)) {
336341
const { type, id } = event.data;
337-
const node = record.mirror.getNode(id);
342+
const node = mirror.getNode(id);
338343

339344
if (node instanceof HTMLElement && type === MouseInteractions.Click) {
340345
clickDetector.registerClick(node);

packages/replay-internal/src/coreHandlers/handleDom.ts

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
import { record } from '@sentry-internal/rrweb';
2-
import type { serializedElementNodeWithId, serializedNodeWithId } from '@sentry-internal/rrweb-snapshot';
1+
import type { Mirror, serializedElementNodeWithId, serializedNodeWithId } from '@sentry-internal/rrweb-snapshot';
32
import { NodeType } from '@sentry-internal/rrweb-snapshot';
43
import type { Breadcrumb, HandlerDataDom } from '@sentry/types';
54
import { htmlTreeAsString } from '@sentry/utils';
@@ -19,7 +18,7 @@ export const handleDomListener: (replay: ReplayContainer) => (handlerData: Handl
1918
return;
2019
}
2120

22-
const result = handleDom(handlerData);
21+
const result = handleDom(handlerData, replay.getDomMirror());
2322

2423
if (!result) {
2524
return;
@@ -50,10 +49,10 @@ export const handleDomListener: (replay: ReplayContainer) => (handlerData: Handl
5049
};
5150

5251
/** Get the base DOM breadcrumb. */
53-
export function getBaseDomBreadcrumb(target: Node | null, message: string): Breadcrumb {
54-
const nodeId = record.mirror.getId(target);
55-
const node = nodeId && record.mirror.getNode(nodeId);
56-
const meta = node && record.mirror.getMeta(node);
52+
export function getBaseDomBreadcrumb(target: Node | null, message: string, mirror: Mirror): Breadcrumb {
53+
const nodeId = mirror.getId(target);
54+
const node = nodeId && mirror.getNode(nodeId);
55+
const meta = node && mirror.getMeta(node);
5756
const element = meta && isElement(meta) ? meta : null;
5857

5958
return {
@@ -80,12 +79,12 @@ export function getBaseDomBreadcrumb(target: Node | null, message: string): Brea
8079
* An event handler to react to DOM events.
8180
* Exported for tests.
8281
*/
83-
export function handleDom(handlerData: HandlerDataDom): Breadcrumb | null {
82+
export function handleDom(handlerData: HandlerDataDom, mirror: Mirror): Breadcrumb | null {
8483
const { target, message } = getDomTarget(handlerData);
8584

8685
return createBreadcrumb({
8786
category: `ui.${handlerData.name}`,
88-
...getBaseDomBreadcrumb(target, message),
87+
...getBaseDomBreadcrumb(target, message, mirror),
8988
});
9089
}
9190

packages/replay-internal/src/coreHandlers/handleKeyboardEvent.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { Mirror } from '@sentry-internal/rrweb-snapshot';
12
import type { Breadcrumb } from '@sentry/types';
23
import { htmlTreeAsString } from '@sentry/utils';
34

@@ -7,7 +8,7 @@ import { getBaseDomBreadcrumb } from './handleDom';
78
import { addBreadcrumbEvent } from './util/addBreadcrumbEvent';
89

910
/** Handle keyboard events & create breadcrumbs. */
10-
export function handleKeyboardEvent(replay: ReplayContainer, event: KeyboardEvent): void {
11+
export function handleKeyboardEvent(replay: ReplayContainer, event: KeyboardEvent, mirror: Mirror): void {
1112
if (!replay.isEnabled()) {
1213
return;
1314
}
@@ -17,7 +18,7 @@ export function handleKeyboardEvent(replay: ReplayContainer, event: KeyboardEven
1718
// session with a single "keydown" breadcrumb is created)
1819
replay.updateUserActivity();
1920

20-
const breadcrumb = getKeyboardBreadcrumb(event);
21+
const breadcrumb = getKeyboardBreadcrumb(event, mirror);
2122

2223
if (!breadcrumb) {
2324
return;
@@ -27,7 +28,7 @@ export function handleKeyboardEvent(replay: ReplayContainer, event: KeyboardEven
2728
}
2829

2930
/** exported only for tests */
30-
export function getKeyboardBreadcrumb(event: KeyboardEvent): Breadcrumb | null {
31+
export function getKeyboardBreadcrumb(event: KeyboardEvent, mirror: Mirror): Breadcrumb | null {
3132
const { metaKey, shiftKey, ctrlKey, altKey, key, target } = event;
3233

3334
// never capture for input fields
@@ -46,7 +47,7 @@ export function getKeyboardBreadcrumb(event: KeyboardEvent): Breadcrumb | null {
4647
}
4748

4849
const message = htmlTreeAsString(target, { maxStringLength: 200 }) || '<unknown>';
49-
const baseBreadcrumb = getBaseDomBreadcrumb(target as Node, message);
50+
const baseBreadcrumb = getBaseDomBreadcrumb(target as Node, message, mirror);
5051

5152
return createBreadcrumb({
5253
category: 'ui.keyDown',

packages/replay-internal/src/replay.ts

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
/* eslint-disable max-lines */ // TODO: We might want to split this file up
22
import { EventType, record } from '@sentry-internal/rrweb';
3+
import type { Mirror } from '@sentry-internal/rrweb-snapshot';
34
import {
45
SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
56
captureException,
@@ -143,6 +144,13 @@ export class ReplayContainer implements ReplayContainerInterface {
143144
*/
144145
private _hasInitializedCoreListeners: boolean;
145146

147+
/**
148+
* The `record` function to use, defaults to package's `record()`, but we can
149+
* opt to pass in a different version, i.e. if we wanted to test a different
150+
* version.
151+
*/
152+
private _recordFn: typeof record;
153+
146154
/**
147155
* Function to stop recording
148156
*/
@@ -212,13 +220,22 @@ export class ReplayContainer implements ReplayContainerInterface {
212220
if (slowClickConfig) {
213221
this.clickDetector = new ClickDetector(this, slowClickConfig);
214222
}
223+
224+
this._recordFn = options._experiments.recordFn || record;
215225
}
216226

217227
/** Get the event context. */
218228
public getContext(): InternalEventContext {
219229
return this._context;
220230
}
221231

232+
/**
233+
* Returns rrweb's mirror
234+
*/
235+
public getDomMirror(): Mirror {
236+
return this._recordFn.mirror;
237+
}
238+
222239
/** If recording is currently enabled. */
223240
public isEnabled(): boolean {
224241
return this._isEnabled;
@@ -368,7 +385,7 @@ export class ReplayContainer implements ReplayContainerInterface {
368385
try {
369386
const canvasOptions = this._canvas;
370387

371-
this._stopRecording = record({
388+
this._stopRecording = this._recordFn({
372389
...this._recordingOptions,
373390
// When running in error sampling mode, we need to overwrite `checkoutEveryNms`
374391
// Without this, it would record forever, until an error happens, which we don't want
@@ -941,7 +958,7 @@ export class ReplayContainer implements ReplayContainerInterface {
941958

942959
/** Ensure page remains active when a key is pressed. */
943960
private _handleKeyboardEvent: (event: KeyboardEvent) => void = (event: KeyboardEvent) => {
944-
handleKeyboardEvent(this, event);
961+
handleKeyboardEvent(this, event, this.getDomMirror());
945962
};
946963

947964
/**
@@ -1036,7 +1053,9 @@ export class ReplayContainer implements ReplayContainerInterface {
10361053
* are included in the replay event before it is finished and sent to Sentry.
10371054
*/
10381055
private _addPerformanceEntries(): Promise<Array<AddEventResult | null>> {
1039-
const performanceEntries = createPerformanceEntries(this.performanceEntries).concat(this.replayPerformanceEntries);
1056+
const performanceEntries = createPerformanceEntries(this.performanceEntries, this.getDomMirror()).concat(
1057+
this.replayPerformanceEntries,
1058+
);
10401059

10411060
this.performanceEntries = [];
10421061
this.replayPerformanceEntries = [];

packages/replay-internal/src/types/replay.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import type { record } from '@sentry-internal/rrweb';
2+
import type { Mirror } from '@sentry-internal/rrweb-snapshot';
13
import type {
24
Breadcrumb,
35
ErrorEvent,
@@ -232,6 +234,7 @@ export interface ReplayPluginOptions extends ReplayNetworkOptions {
232234
_experiments: Partial<{
233235
captureExceptions: boolean;
234236
traceInternals: boolean;
237+
recordFn: typeof record;
235238
}>;
236239
}
237240

@@ -465,6 +468,7 @@ export interface ReplayContainer {
465468
isPaused(): boolean;
466469
isRecordingCanvas(): boolean;
467470
getContext(): InternalEventContext;
471+
getDomMirror(): Mirror;
468472
initializeSampling(): void;
469473
start(): void;
470474
stop(options?: { reason?: string; forceflush?: boolean }): Promise<void>;

packages/replay-internal/src/util/createPerformanceEntries.ts

Lines changed: 29 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { record } from '@sentry-internal/rrweb';
1+
import type { Mirror } from '@sentry-internal/rrweb-snapshot';
22
import { browserPerformanceTimeOrigin } from '@sentry/utils';
33

44
import { WINDOW } from '../constants';
@@ -17,7 +17,7 @@ import type {
1717
// Map entryType -> function to normalize data for event
1818
const ENTRY_TYPES: Record<
1919
string,
20-
(entry: AllPerformanceEntry) => null | ReplayPerformanceEntry<AllPerformanceEntryData>
20+
(entry: AllPerformanceEntry, mirror: Mirror) => null | ReplayPerformanceEntry<AllPerformanceEntryData>
2121
> = {
2222
// @ts-expect-error TODO: entry type does not fit the create* functions entry type
2323
resource: createResourceEntry,
@@ -56,27 +56,33 @@ interface LayoutShiftAttribution {
5656
* Handler creater for web vitals
5757
*/
5858
export function webVitalHandler(
59-
getter: (metric: Metric) => ReplayPerformanceEntry<AllPerformanceEntryData>,
59+
getter: (metric: Metric, mirror: Mirror) => ReplayPerformanceEntry<AllPerformanceEntryData>,
6060
replay: ReplayContainer,
6161
): (data: { metric: Metric }) => void {
62-
return ({ metric }) => void replay.replayPerformanceEntries.push(getter(metric));
62+
return ({ metric }) => void replay.replayPerformanceEntries.push(getter(metric, replay.getDomMirror()));
6363
}
6464

6565
/**
6666
* Create replay performance entries from the browser performance entries.
6767
*/
6868
export function createPerformanceEntries(
6969
entries: AllPerformanceEntry[],
70+
mirror: Mirror,
7071
): ReplayPerformanceEntry<AllPerformanceEntryData>[] {
71-
return entries.map(createPerformanceEntry).filter(Boolean) as ReplayPerformanceEntry<AllPerformanceEntryData>[];
72+
return entries
73+
.map(entry => createPerformanceEntry(entry, mirror))
74+
.filter(Boolean) as ReplayPerformanceEntry<AllPerformanceEntryData>[];
7275
}
7376

74-
function createPerformanceEntry(entry: AllPerformanceEntry): ReplayPerformanceEntry<AllPerformanceEntryData> | null {
77+
function createPerformanceEntry(
78+
entry: AllPerformanceEntry,
79+
mirror: Mirror,
80+
): ReplayPerformanceEntry<AllPerformanceEntryData> | null {
7581
if (!ENTRY_TYPES[entry.entryType]) {
7682
return null;
7783
}
7884

79-
return ENTRY_TYPES[entry.entryType](entry);
85+
return ENTRY_TYPES[entry.entryType](entry, mirror);
8086
}
8187

8288
function getAbsoluteTime(time: number): number {
@@ -85,7 +91,7 @@ function getAbsoluteTime(time: number): number {
8591
return ((browserPerformanceTimeOrigin || WINDOW.performance.timeOrigin) + time) / 1000;
8692
}
8793

88-
function createPaintEntry(entry: PerformancePaintTiming): ReplayPerformanceEntry<PaintData> {
94+
function createPaintEntry(entry: PerformancePaintTiming, _mirror: Mirror): ReplayPerformanceEntry<PaintData> {
8995
const { duration, entryType, name, startTime } = entry;
9096

9197
const start = getAbsoluteTime(startTime);
@@ -98,7 +104,10 @@ function createPaintEntry(entry: PerformancePaintTiming): ReplayPerformanceEntry
98104
};
99105
}
100106

101-
function createNavigationEntry(entry: PerformanceNavigationTiming): ReplayPerformanceEntry<NavigationData> | null {
107+
function createNavigationEntry(
108+
entry: PerformanceNavigationTiming,
109+
_mirror: Mirror,
110+
): ReplayPerformanceEntry<NavigationData> | null {
102111
const {
103112
entryType,
104113
name,
@@ -145,6 +154,7 @@ function createNavigationEntry(entry: PerformanceNavigationTiming): ReplayPerfor
145154

146155
function createResourceEntry(
147156
entry: ExperimentalPerformanceResourceTiming,
157+
_mirror: Mirror,
148158
): ReplayPerformanceEntry<ResourceData> | null {
149159
const {
150160
entryType,
@@ -180,38 +190,38 @@ function createResourceEntry(
180190
/**
181191
* Add a LCP event to the replay based on a LCP metric.
182192
*/
183-
export function getLargestContentfulPaint(metric: Metric): ReplayPerformanceEntry<WebVitalData> {
193+
export function getLargestContentfulPaint(metric: Metric, mirror: Mirror): ReplayPerformanceEntry<WebVitalData> {
184194
const lastEntry = metric.entries[metric.entries.length - 1] as (PerformanceEntry & { element?: Node }) | undefined;
185195
const node = lastEntry ? lastEntry.element : undefined;
186-
return getWebVital(metric, 'largest-contentful-paint', node);
196+
return getWebVital(metric, 'largest-contentful-paint', node, mirror);
187197
}
188198

189199
/**
190200
* Add a CLS event to the replay based on a CLS metric.
191201
*/
192-
export function getCumulativeLayoutShift(metric: Metric): ReplayPerformanceEntry<WebVitalData> {
202+
export function getCumulativeLayoutShift(metric: Metric, mirror: Mirror): ReplayPerformanceEntry<WebVitalData> {
193203
// get first node that shifts
194204
const firstEntry = metric.entries[0] as (PerformanceEntry & { sources?: LayoutShiftAttribution[] }) | undefined;
195205
const node = firstEntry ? (firstEntry.sources ? firstEntry.sources[0].node : undefined) : undefined;
196-
return getWebVital(metric, 'cumulative-layout-shift', node);
206+
return getWebVital(metric, 'cumulative-layout-shift', node, mirror);
197207
}
198208

199209
/**
200210
* Add a FID event to the replay based on a FID metric.
201211
*/
202-
export function getFirstInputDelay(metric: Metric): ReplayPerformanceEntry<WebVitalData> {
212+
export function getFirstInputDelay(metric: Metric, mirror: Mirror): ReplayPerformanceEntry<WebVitalData> {
203213
const lastEntry = metric.entries[metric.entries.length - 1] as (PerformanceEntry & { target?: Node }) | undefined;
204214
const node = lastEntry ? lastEntry.target : undefined;
205-
return getWebVital(metric, 'first-input-delay', node);
215+
return getWebVital(metric, 'first-input-delay', node, mirror);
206216
}
207217

208218
/**
209219
* Add an INP event to the replay based on an INP metric.
210220
*/
211-
export function getInteractionToNextPaint(metric: Metric): ReplayPerformanceEntry<WebVitalData> {
221+
export function getInteractionToNextPaint(metric: Metric, mirror: Mirror): ReplayPerformanceEntry<WebVitalData> {
212222
const lastEntry = metric.entries[metric.entries.length - 1] as (PerformanceEntry & { target?: Node }) | undefined;
213223
const node = lastEntry ? lastEntry.target : undefined;
214-
return getWebVital(metric, 'interaction-to-next-paint', node);
224+
return getWebVital(metric, 'interaction-to-next-paint', node, mirror);
215225
}
216226

217227
/**
@@ -221,6 +231,7 @@ export function getWebVital(
221231
metric: Metric,
222232
name: string,
223233
node: Node | undefined,
234+
mirror: Mirror | undefined,
224235
): ReplayPerformanceEntry<WebVitalData> {
225236
const value = metric.value;
226237
const rating = metric.rating;
@@ -236,7 +247,7 @@ export function getWebVital(
236247
value,
237248
size: value,
238249
rating,
239-
nodeId: node ? record.mirror.getId(node) : undefined,
250+
nodeId: node && mirror ? mirror.getId(node) : undefined,
240251
},
241252
};
242253

packages/replay-internal/src/util/handleRecordingEmit.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ export function getHandleRecordingEmit(replay: ReplayContainer): RecordingEmitCa
3232
hadFirstEvent = true;
3333

3434
if (replay.clickDetector) {
35-
updateClickDetectorForRecordingEvent(replay.clickDetector, event);
35+
updateClickDetectorForRecordingEvent(replay.clickDetector, event, replay.getDomMirror());
3636
}
3737

3838
// The handler returns `true` if we do not want to trigger debounced flush, `false` if we want to debounce flush.

0 commit comments

Comments
 (0)