Skip to content

Commit

Permalink
ref(replay): Refactor extractDomNodes & countDomNodes to return an in…
Browse files Browse the repository at this point in the history
…dexed map (#58092)

I want to use an indexed map to more easily merge extractedDomNode
information with traces inside one tab.

This refactor changes the return type for `extractDomNodes` to do that.
Also `countDomNodes` to keep things consistent.

I went and made a base class for them both because i noticed that
`createHiddenPlayer` was copy+pasted, and then got carried away.

Related to #50065
  • Loading branch information
ryan953 authored Oct 16, 2023
1 parent 6bb97c7 commit fb956d8
Show file tree
Hide file tree
Showing 6 changed files with 183 additions and 211 deletions.
102 changes: 19 additions & 83 deletions static/app/utils/replays/countDomNodes.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import {Replayer} from '@sentry-internal/rrweb';

import replayerStepper from 'sentry/utils/replays/replayerStepper';
import type {RecordingFrame} from 'sentry/utils/replays/types';

export type DomNodeChartDatapoint = {
Expand All @@ -16,93 +15,30 @@ type Args = {
};

export default function countDomNodes({
frames = [],
frames,
rrwebEvents,
startTimestampMs,
}: Args): Promise<DomNodeChartDatapoint[]> {
return new Promise(resolve => {
const datapoints = new Map<RecordingFrame, DomNodeChartDatapoint>();
const player = createPlayer(rrwebEvents);

const nextFrame = (function () {
let i = 0;
const len = frames.length;
// how many frames we look at depends on the number of total frames
return () => frames[(i += Math.max(Math.round(len * 0.007), 1))];
})();

const onDone = () => {
resolve(Array.from(datapoints.values()));
};

const nextOrDone = () => {
const next = nextFrame();
if (next) {
matchFrame(next);
} else {
onDone();
}
};

type FrameRef = {
frame: undefined | RecordingFrame;
};

const nodeIdRef: FrameRef = {
frame: undefined,
};

const handlePause = () => {
const frame = nodeIdRef.frame as RecordingFrame;
const idCount = player.getMirror().getIds().length; // gets number of DOM nodes present
datapoints.set(frame as RecordingFrame, {
}: Args): Promise<Map<RecordingFrame, DomNodeChartDatapoint>> {
let frameCount = 0;
const length = frames?.length ?? 0;
const frameStep = Math.max(Math.round(length * 0.007), 1);

return replayerStepper<RecordingFrame, DomNodeChartDatapoint>({
frames,
rrwebEvents,
startTimestampMs,
shouldVisitFrame: () => {
frameCount++;
return frameCount % frameStep === 0;
},
onVisitFrame: (frame, collection, replayer) => {
const idCount = replayer.getMirror().getIds().length; // gets number of DOM nodes present
collection.set(frame as RecordingFrame, {
count: idCount,
timestampMs: frame.timestamp,
startTimestampMs: frame.timestamp,
endTimestampMs: frame.timestamp,
});
nextOrDone();
};

const matchFrame = frame => {
if (!frame) {
nextOrDone();
return;
}
nodeIdRef.frame = frame;

window.setTimeout(() => {
player.pause(frame.timestamp - startTimestampMs);
}, 0);
};

player.on('pause', handlePause);
matchFrame(nextFrame());
});
}

function createPlayer(rrwebEvents): Replayer {
const domRoot = document.createElement('div');
domRoot.className = 'sentry-block';
const {style} = domRoot;

style.position = 'fixed';
style.inset = '0';
style.width = '0';
style.height = '0';
style.overflow = 'hidden';

document.body.appendChild(domRoot);

const replayerRef = new Replayer(rrwebEvents, {
root: domRoot,
loadTimeout: 1,
showWarning: false,
blockClass: 'sentry-block',
speed: 99999,
skipInactive: true,
triggerFocus: false,
mouseTail: false,
},
});
return replayerRef;
}
114 changes: 18 additions & 96 deletions static/app/utils/replays/extractDomNodes.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {Replayer} from '@sentry-internal/rrweb';
import type {Mirror} from '@sentry-internal/rrweb-snapshot';

import replayerStepper from 'sentry/utils/replays/replayerStepper';
import type {RecordingFrame, ReplayFrame} from 'sentry/utils/replays/types';

export type Extraction = {
Expand All @@ -12,111 +12,33 @@ export type Extraction = {
type Args = {
frames: ReplayFrame[] | undefined;
rrwebEvents: RecordingFrame[] | undefined;
startTimestampMs: number;
};

export default function extractDomNodes({
frames = [],
frames,
rrwebEvents,
}: Args): Promise<Extraction[]> {
return new Promise(resolve => {
if (!frames.length) {
resolve([]);
return;
}

const extractions = new Map<ReplayFrame, Extraction>();

const player = createPlayer(rrwebEvents);
const mirror = player.getMirror();

const nextFrame = (function () {
let i = 0;
return () => frames[i++];
})();

const onDone = () => {
resolve(Array.from(extractions.values()));
};

const nextOrDone = () => {
const next = nextFrame();
if (next) {
matchFrame(next);
} else {
onDone();
}
};

type FrameRef = {
frame: undefined | ReplayFrame;
nodeId: undefined | number;
};

const nodeIdRef: FrameRef = {
frame: undefined,
nodeId: undefined,
};

const handlePause = () => {
if (!nodeIdRef.nodeId && !nodeIdRef.frame) {
return;
}
const frame = nodeIdRef.frame as ReplayFrame;
const nodeId = nodeIdRef.nodeId as number;

startTimestampMs,
}: Args): Promise<Map<ReplayFrame, Extraction>> {
return replayerStepper({
frames,
rrwebEvents,
startTimestampMs,
shouldVisitFrame: frame => {
const nodeId = frame.data && 'nodeId' in frame.data ? frame.data.nodeId : undefined;
return nodeId !== undefined && nodeId !== -1;
},
onVisitFrame: (frame, collection, replayer) => {
const mirror = replayer.getMirror();
const nodeId = frame.data && 'nodeId' in frame.data ? frame.data.nodeId : undefined;
const html = extractHtml(nodeId as number, mirror);
extractions.set(frame as ReplayFrame, {
collection.set(frame as ReplayFrame, {
frame,
html,
timestamp: frame.timestampMs,
});
nextOrDone();
};

const matchFrame = frame => {
nodeIdRef.frame = frame;
nodeIdRef.nodeId =
frame.data && 'nodeId' in frame.data ? frame.data.nodeId : undefined;

if (nodeIdRef.nodeId === undefined || nodeIdRef.nodeId === -1) {
nextOrDone();
return;
}

window.setTimeout(() => {
player.pause(frame.offsetMs);
}, 0);
};

player.on('pause', handlePause);
matchFrame(nextFrame());
});
}

function createPlayer(rrwebEvents): Replayer {
const domRoot = document.createElement('div');
domRoot.className = 'sentry-block';
const {style} = domRoot;

style.position = 'fixed';
style.inset = '0';
style.width = '0';
style.height = '0';
style.overflow = 'hidden';

document.body.appendChild(domRoot);

const replayerRef = new Replayer(rrwebEvents, {
root: domRoot,
loadTimeout: 1,
showWarning: false,
blockClass: 'sentry-block',
speed: 99999,
skipInactive: true,
triggerFocus: false,
mouseTail: false,
},
});
return replayerRef;
}

function extractHtml(nodeId: number, mirror: Mirror): string | null {
Expand Down
17 changes: 0 additions & 17 deletions static/app/utils/replays/replayReader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ import {duration} from 'moment';

import domId from 'sentry/utils/domId';
import localStorageWrapper from 'sentry/utils/localStorage';
import countDomNodes from 'sentry/utils/replays/countDomNodes';
import extractDomNodes from 'sentry/utils/replays/extractDomNodes';
import hydrateBreadcrumbs, {
replayInitBreadcrumb,
} from 'sentry/utils/replays/hydrateBreadcrumbs';
Expand Down Expand Up @@ -259,21 +257,6 @@ export default class ReplayReader {
].sort(sortFrames)
);

countDomNodes = memoize(() =>
countDomNodes({
frames: this.getRRWebMutations(),
rrwebEvents: this.getRRWebFrames(),
startTimestampMs: this._replayRecord.started_at.getTime(),
})
);

getDomNodes = memoize(() =>
extractDomNodes({
frames: this.getDOMFrames(),
rrwebEvents: this.getRRWebFrames(),
})
);

getMemoryFrames = memoize(() =>
this._sortedSpanFrames.filter((frame): frame is MemoryFrame => frame.op === 'memory')
);
Expand Down
110 changes: 110 additions & 0 deletions static/app/utils/replays/replayerStepper.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import {Replayer} from '@sentry-internal/rrweb';

import type {RecordingFrame, ReplayFrame} from 'sentry/utils/replays/types';

interface Args<Frame extends ReplayFrame | RecordingFrame, CollectionData> {
frames: Frame[] | undefined;
onVisitFrame: (
frame: Frame,
collection: Map<Frame, CollectionData>,
replayer: Replayer
) => void;
rrwebEvents: RecordingFrame[] | undefined;
shouldVisitFrame: (frame: Frame, replayer: Replayer) => boolean;
startTimestampMs: number;
}

type FrameRef<Frame extends ReplayFrame | RecordingFrame> = {
frame: Frame | undefined;
};

export default function replayerStepper<
Frame extends ReplayFrame | RecordingFrame,
CollectionData,
>({
frames,
onVisitFrame,
rrwebEvents,
shouldVisitFrame,
startTimestampMs,
}: Args<Frame, CollectionData>): Promise<Map<Frame, CollectionData>> {
const collection = new Map<Frame, CollectionData>();

return new Promise(resolve => {
if (!frames?.length || !rrwebEvents?.length) {
resolve(new Map());
return;
}

const replayer = createHiddenPlayer(rrwebEvents);

const nextFrame = (function () {
let i = 0;
return () => frames[i++];
})();

const onDone = () => {
resolve(collection);
};

const nextOrDone = () => {
const next = nextFrame();
if (next) {
considerFrame(next);
} else {
onDone();
}
};

const frameRef: FrameRef<Frame> = {
frame: undefined,
};

const considerFrame = (frame: Frame) => {
if (shouldVisitFrame(frame, replayer)) {
frameRef.frame = frame;
window.setTimeout(() => {
const timestamp =
'offsetMs' in frame ? frame.offsetMs : frame.timestamp - startTimestampMs;
replayer.pause(timestamp);
}, 0);
} else {
frameRef.frame = undefined;
nextOrDone();
}
};

const handlePause = () => {
onVisitFrame(frameRef.frame!, collection, replayer);
nextOrDone();
};

replayer.on('pause', handlePause);
considerFrame(nextFrame());
});
}

function createHiddenPlayer(rrwebEvents: RecordingFrame[]): Replayer {
const domRoot = document.createElement('div');
domRoot.className = 'sentry-block';
const {style} = domRoot;

style.position = 'fixed';
style.inset = '0';
style.width = '0';
style.height = '0';
style.overflow = 'hidden';

document.body.appendChild(domRoot);

return new Replayer(rrwebEvents, {
root: domRoot,
loadTimeout: 1,
showWarning: false,
blockClass: 'sentry-block',
speed: 99999,
skipInactive: true,
triggerFocus: false,
mouseTail: false,
});
}
Loading

0 comments on commit fb956d8

Please sign in to comment.