Skip to content

Commit

Permalink
feat(replay): Add option to pass in custom record fn
Browse files Browse the repository at this point in the history
Bringing back #8647 but it would be nice to test new versions of rrweb without having to bundle it with our replay SDK.
  • Loading branch information
billyvg committed Apr 19, 2024
1 parent d83c153 commit 9699435
Show file tree
Hide file tree
Showing 9 changed files with 74 additions and 33 deletions.
9 changes: 7 additions & 2 deletions packages/replay-internal/src/coreHandlers/handleClick.ts
@@ -1,4 +1,5 @@
import { IncrementalSource, MouseInteractions, record } from '@sentry-internal/rrweb';
import type { Mirror } from '@sentry-internal/rrweb-snapshot';
import type { Breadcrumb } from '@sentry/types';

import { WINDOW } from '../constants';
Expand Down Expand Up @@ -308,7 +309,11 @@ function nowInSeconds(): number {
}

/** Update the click detector based on a recording event of rrweb. */
export function updateClickDetectorForRecordingEvent(clickDetector: ReplayClickDetector, event: RecordingEvent): void {
export function updateClickDetectorForRecordingEvent(
clickDetector: ReplayClickDetector,
event: RecordingEvent,
mirror: Mirror,
): void {
try {
// note: We only consider incremental snapshots here
// This means that any full snapshot is ignored for mutation detection - the reason is that we simply cannot know if a mutation happened here.
Expand All @@ -333,7 +338,7 @@ export function updateClickDetectorForRecordingEvent(clickDetector: ReplayClickD

if (isIncrementalMouseInteraction(event)) {
const { type, id } = event.data;
const node = record.mirror.getNode(id);
const node = mirror.getNode(id);

if (node instanceof HTMLElement && type === MouseInteractions.Click) {
clickDetector.registerClick(node);
Expand Down
17 changes: 8 additions & 9 deletions packages/replay-internal/src/coreHandlers/handleDom.ts
@@ -1,5 +1,4 @@
import { record } from '@sentry-internal/rrweb';
import type { serializedElementNodeWithId, serializedNodeWithId } from '@sentry-internal/rrweb-snapshot';
import type { serializedElementNodeWithId, serializedNodeWithId, Mirror } from '@sentry-internal/rrweb-snapshot';
import { NodeType } from '@sentry-internal/rrweb-snapshot';
import type { Breadcrumb, HandlerDataDom } from '@sentry/types';
import { htmlTreeAsString } from '@sentry/utils';
Expand All @@ -19,7 +18,7 @@ export const handleDomListener: (replay: ReplayContainer) => (handlerData: Handl
return;
}

const result = handleDom(handlerData);
const result = handleDom(handlerData, replay.getDomMirror());

if (!result) {
return;
Expand Down Expand Up @@ -50,10 +49,10 @@ export const handleDomListener: (replay: ReplayContainer) => (handlerData: Handl
};

/** Get the base DOM breadcrumb. */
export function getBaseDomBreadcrumb(target: Node | null, message: string): Breadcrumb {
const nodeId = record.mirror.getId(target);
const node = nodeId && record.mirror.getNode(nodeId);
const meta = node && record.mirror.getMeta(node);
export function getBaseDomBreadcrumb(target: Node | null, message: string, mirror: Mirror): Breadcrumb {
const nodeId = mirror.getId(target);
const node = nodeId && mirror.getNode(nodeId);
const meta = node && mirror.getMeta(node);
const element = meta && isElement(meta) ? meta : null;

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

return createBreadcrumb({
category: `ui.${handlerData.name}`,
...getBaseDomBreadcrumb(target, message),
...getBaseDomBreadcrumb(target, message, mirror),
});
}

Expand Down
@@ -1,3 +1,4 @@
import type { Mirror } from '@sentry-internal/rrweb-snapshot';
import type { Breadcrumb } from '@sentry/types';
import { htmlTreeAsString } from '@sentry/utils';

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

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

const breadcrumb = getKeyboardBreadcrumb(event);
const breadcrumb = getKeyboardBreadcrumb(event, mirror);

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

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

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

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

return createBreadcrumb({
category: 'ui.keyDown',
Expand Down
Expand Up @@ -27,7 +27,7 @@ export function setupPerformanceObserver(replay: ReplayContainer): () => void {

clearCallbacks.push(
addLcpInstrumentationHandler(({ metric }) => {
replay.replayPerformanceEntries.push(getLargestContentfulPaint(metric));
replay.replayPerformanceEntries.push(getLargestContentfulPaint(metric, replay.getDomMirror()));
}),
);

Expand Down
25 changes: 22 additions & 3 deletions packages/replay-internal/src/replay.ts
@@ -1,5 +1,6 @@
/* eslint-disable max-lines */ // TODO: We might want to split this file up
import { EventType, record } from '@sentry-internal/rrweb';
import type { Mirror } from '@sentry-internal/rrweb-snapshot';
import {
SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
captureException,
Expand Down Expand Up @@ -139,6 +140,13 @@ export class ReplayContainer implements ReplayContainerInterface {
*/
private _hasInitializedCoreListeners: boolean;

/**
* The `record` function to use, defaults to package's `record()`, but we can
* opt to pass in a different version, i.e. if we wanted to test a different
* version.
*/
private _recordFn: typeof record;

/**
* Function to stop recording
*/
Expand Down Expand Up @@ -207,13 +215,22 @@ export class ReplayContainer implements ReplayContainerInterface {
if (slowClickConfig) {
this.clickDetector = new ClickDetector(this, slowClickConfig);
}

this._recordFn = options._experiments.recordFn || record;
}

/** Get the event context. */
public getContext(): InternalEventContext {
return this._context;
}

/**
* Returns rrweb's mirror
*/
public getDomMirror(): Mirror {
return this._recordFn.mirror;
}

/** If recording is currently enabled. */
public isEnabled(): boolean {
return this._isEnabled;
Expand Down Expand Up @@ -353,7 +370,7 @@ export class ReplayContainer implements ReplayContainerInterface {
try {
const canvasOptions = this._canvas;

this._stopRecording = record({
this._stopRecording = this._recordFn({
...this._recordingOptions,
// When running in error sampling mode, we need to overwrite `checkoutEveryNms`
// Without this, it would record forever, until an error happens, which we don't want
Expand Down Expand Up @@ -926,7 +943,7 @@ export class ReplayContainer implements ReplayContainerInterface {

/** Ensure page remains active when a key is pressed. */
private _handleKeyboardEvent: (event: KeyboardEvent) => void = (event: KeyboardEvent) => {
handleKeyboardEvent(this, event);
handleKeyboardEvent(this, event, this.getDomMirror());
};

/**
Expand Down Expand Up @@ -1021,7 +1038,9 @@ export class ReplayContainer implements ReplayContainerInterface {
* are included in the replay event before it is finished and sent to Sentry.
*/
private _addPerformanceEntries(): Promise<Array<AddEventResult | null>> {
const performanceEntries = createPerformanceEntries(this.performanceEntries).concat(this.replayPerformanceEntries);
const performanceEntries = createPerformanceEntries(this.performanceEntries, this.getDomMirror()).concat(
this.replayPerformanceEntries,
);

this.performanceEntries = [];
this.replayPerformanceEntries = [];
Expand Down
4 changes: 4 additions & 0 deletions packages/replay-internal/src/types/replay.ts
@@ -1,3 +1,5 @@
import type { record } from '@sentry-internal/rrweb';
import type { Mirror } from '@sentry-internal/rrweb-snapshot';
import type {
Breadcrumb,
ErrorEvent,
Expand Down Expand Up @@ -232,6 +234,7 @@ export interface ReplayPluginOptions extends ReplayNetworkOptions {
_experiments: Partial<{
captureExceptions: boolean;
traceInternals: boolean;
recordFn: typeof record;
}>;
}

Expand Down Expand Up @@ -465,6 +468,7 @@ export interface ReplayContainer {
isPaused(): boolean;
isRecordingCanvas(): boolean;
getContext(): InternalEventContext;
getDomMirror(): Mirror;
initializeSampling(): void;
start(): void;
stop(options?: { reason?: string; forceflush?: boolean }): Promise<void>;
Expand Down
2 changes: 1 addition & 1 deletion packages/replay-internal/src/types/rrweb.ts
Expand Up @@ -31,7 +31,7 @@ export type ReplayEventWithTime = {

/**
* This is a partial copy of rrweb's recording options which only contains the properties
* we specifically us in the SDK. Users can specify additional properties, hence we add the
* we specifically use in the SDK. Users can specify additional properties, hence we add the
* Record<string, unknown> union type.
*/
export type RrwebRecordOptions = {
Expand Down
37 changes: 25 additions & 12 deletions packages/replay-internal/src/util/createPerformanceEntries.ts
@@ -1,4 +1,4 @@
import { record } from '@sentry-internal/rrweb';
import type { Mirror } from '@sentry-internal/rrweb-snapshot';
import { browserPerformanceTimeOrigin } from '@sentry/utils';

import { WINDOW } from '../constants';
Expand All @@ -16,7 +16,7 @@ import type {
// Map entryType -> function to normalize data for event
const ENTRY_TYPES: Record<
string,
(entry: AllPerformanceEntry) => null | ReplayPerformanceEntry<AllPerformanceEntryData>
(entry: AllPerformanceEntry, mirror: Mirror) => null | ReplayPerformanceEntry<AllPerformanceEntryData>
> = {
// @ts-expect-error TODO: entry type does not fit the create* functions entry type
resource: createResourceEntry,
Expand All @@ -30,16 +30,22 @@ const ENTRY_TYPES: Record<
*/
export function createPerformanceEntries(
entries: AllPerformanceEntry[],
mirror: Mirror,
): ReplayPerformanceEntry<AllPerformanceEntryData>[] {
return entries.map(createPerformanceEntry).filter(Boolean) as ReplayPerformanceEntry<AllPerformanceEntryData>[];
return entries
.map(entry => createPerformanceEntry(entry, mirror))
.filter(Boolean) as ReplayPerformanceEntry<AllPerformanceEntryData>[];
}

function createPerformanceEntry(entry: AllPerformanceEntry): ReplayPerformanceEntry<AllPerformanceEntryData> | null {
function createPerformanceEntry(
entry: AllPerformanceEntry,
mirror: Mirror,
): ReplayPerformanceEntry<AllPerformanceEntryData> | null {
if (!ENTRY_TYPES[entry.entryType]) {
return null;
}

return ENTRY_TYPES[entry.entryType](entry);
return ENTRY_TYPES[entry.entryType](entry, mirror);
}

function getAbsoluteTime(time: number): number {
Expand All @@ -48,7 +54,7 @@ function getAbsoluteTime(time: number): number {
return ((browserPerformanceTimeOrigin || WINDOW.performance.timeOrigin) + time) / 1000;
}

function createPaintEntry(entry: PerformancePaintTiming): ReplayPerformanceEntry<PaintData> {
function createPaintEntry(entry: PerformancePaintTiming, _mirror: Mirror): ReplayPerformanceEntry<PaintData> {
const { duration, entryType, name, startTime } = entry;

const start = getAbsoluteTime(startTime);
Expand All @@ -61,7 +67,10 @@ function createPaintEntry(entry: PerformancePaintTiming): ReplayPerformanceEntry
};
}

function createNavigationEntry(entry: PerformanceNavigationTiming): ReplayPerformanceEntry<NavigationData> | null {
function createNavigationEntry(
entry: PerformanceNavigationTiming,
_mirror: Mirror,
): ReplayPerformanceEntry<NavigationData> | null {
const {
entryType,
name,
Expand Down Expand Up @@ -108,6 +117,7 @@ function createNavigationEntry(entry: PerformanceNavigationTiming): ReplayPerfor

function createResourceEntry(
entry: ExperimentalPerformanceResourceTiming,
_mirror: Mirror,
): ReplayPerformanceEntry<ResourceData> | null {
const {
entryType,
Expand Down Expand Up @@ -143,10 +153,13 @@ function createResourceEntry(
/**
* Add a LCP event to the replay based on an LCP metric.
*/
export function getLargestContentfulPaint(metric: {
value: number;
entries: PerformanceEntry[];
}): ReplayPerformanceEntry<LargestContentfulPaintData> {
export function getLargestContentfulPaint(
metric: {
value: number;
entries: PerformanceEntry[];
},
mirror: Mirror,
): ReplayPerformanceEntry<LargestContentfulPaintData> {
const entries = metric.entries;
const lastEntry = entries[entries.length - 1] as (PerformanceEntry & { element?: Element }) | undefined;
const element = lastEntry ? lastEntry.element : undefined;
Expand All @@ -163,7 +176,7 @@ export function getLargestContentfulPaint(metric: {
data: {
value,
size: value,
nodeId: element ? record.mirror.getId(element) : undefined,
nodeId: element ? mirror.getId(element) : undefined,
},
};

Expand Down
2 changes: 1 addition & 1 deletion packages/replay-internal/src/util/handleRecordingEmit.ts
Expand Up @@ -32,7 +32,7 @@ export function getHandleRecordingEmit(replay: ReplayContainer): RecordingEmitCa
hadFirstEvent = true;

if (replay.clickDetector) {
updateClickDetectorForRecordingEvent(replay.clickDetector, event);
updateClickDetectorForRecordingEvent(replay.clickDetector, event, replay.getDomMirror());
}

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

0 comments on commit 9699435

Please sign in to comment.