Skip to content

Commit fb956d8

Browse files
authored
ref(replay): Refactor extractDomNodes & countDomNodes to return an indexed 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
1 parent 6bb97c7 commit fb956d8

File tree

6 files changed

+183
-211
lines changed

6 files changed

+183
-211
lines changed
Lines changed: 19 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
import {Replayer} from '@sentry-internal/rrweb';
2-
1+
import replayerStepper from 'sentry/utils/replays/replayerStepper';
32
import type {RecordingFrame} from 'sentry/utils/replays/types';
43

54
export type DomNodeChartDatapoint = {
@@ -16,93 +15,30 @@ type Args = {
1615
};
1716

1817
export default function countDomNodes({
19-
frames = [],
18+
frames,
2019
rrwebEvents,
2120
startTimestampMs,
22-
}: Args): Promise<DomNodeChartDatapoint[]> {
23-
return new Promise(resolve => {
24-
const datapoints = new Map<RecordingFrame, DomNodeChartDatapoint>();
25-
const player = createPlayer(rrwebEvents);
26-
27-
const nextFrame = (function () {
28-
let i = 0;
29-
const len = frames.length;
30-
// how many frames we look at depends on the number of total frames
31-
return () => frames[(i += Math.max(Math.round(len * 0.007), 1))];
32-
})();
33-
34-
const onDone = () => {
35-
resolve(Array.from(datapoints.values()));
36-
};
37-
38-
const nextOrDone = () => {
39-
const next = nextFrame();
40-
if (next) {
41-
matchFrame(next);
42-
} else {
43-
onDone();
44-
}
45-
};
46-
47-
type FrameRef = {
48-
frame: undefined | RecordingFrame;
49-
};
50-
51-
const nodeIdRef: FrameRef = {
52-
frame: undefined,
53-
};
54-
55-
const handlePause = () => {
56-
const frame = nodeIdRef.frame as RecordingFrame;
57-
const idCount = player.getMirror().getIds().length; // gets number of DOM nodes present
58-
datapoints.set(frame as RecordingFrame, {
21+
}: Args): Promise<Map<RecordingFrame, DomNodeChartDatapoint>> {
22+
let frameCount = 0;
23+
const length = frames?.length ?? 0;
24+
const frameStep = Math.max(Math.round(length * 0.007), 1);
25+
26+
return replayerStepper<RecordingFrame, DomNodeChartDatapoint>({
27+
frames,
28+
rrwebEvents,
29+
startTimestampMs,
30+
shouldVisitFrame: () => {
31+
frameCount++;
32+
return frameCount % frameStep === 0;
33+
},
34+
onVisitFrame: (frame, collection, replayer) => {
35+
const idCount = replayer.getMirror().getIds().length; // gets number of DOM nodes present
36+
collection.set(frame as RecordingFrame, {
5937
count: idCount,
6038
timestampMs: frame.timestamp,
6139
startTimestampMs: frame.timestamp,
6240
endTimestampMs: frame.timestamp,
6341
});
64-
nextOrDone();
65-
};
66-
67-
const matchFrame = frame => {
68-
if (!frame) {
69-
nextOrDone();
70-
return;
71-
}
72-
nodeIdRef.frame = frame;
73-
74-
window.setTimeout(() => {
75-
player.pause(frame.timestamp - startTimestampMs);
76-
}, 0);
77-
};
78-
79-
player.on('pause', handlePause);
80-
matchFrame(nextFrame());
81-
});
82-
}
83-
84-
function createPlayer(rrwebEvents): Replayer {
85-
const domRoot = document.createElement('div');
86-
domRoot.className = 'sentry-block';
87-
const {style} = domRoot;
88-
89-
style.position = 'fixed';
90-
style.inset = '0';
91-
style.width = '0';
92-
style.height = '0';
93-
style.overflow = 'hidden';
94-
95-
document.body.appendChild(domRoot);
96-
97-
const replayerRef = new Replayer(rrwebEvents, {
98-
root: domRoot,
99-
loadTimeout: 1,
100-
showWarning: false,
101-
blockClass: 'sentry-block',
102-
speed: 99999,
103-
skipInactive: true,
104-
triggerFocus: false,
105-
mouseTail: false,
42+
},
10643
});
107-
return replayerRef;
10844
}

static/app/utils/replays/extractDomNodes.tsx

Lines changed: 18 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import {Replayer} from '@sentry-internal/rrweb';
21
import type {Mirror} from '@sentry-internal/rrweb-snapshot';
32

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

66
export type Extraction = {
@@ -12,111 +12,33 @@ export type Extraction = {
1212
type Args = {
1313
frames: ReplayFrame[] | undefined;
1414
rrwebEvents: RecordingFrame[] | undefined;
15+
startTimestampMs: number;
1516
};
1617

1718
export default function extractDomNodes({
18-
frames = [],
19+
frames,
1920
rrwebEvents,
20-
}: Args): Promise<Extraction[]> {
21-
return new Promise(resolve => {
22-
if (!frames.length) {
23-
resolve([]);
24-
return;
25-
}
26-
27-
const extractions = new Map<ReplayFrame, Extraction>();
28-
29-
const player = createPlayer(rrwebEvents);
30-
const mirror = player.getMirror();
31-
32-
const nextFrame = (function () {
33-
let i = 0;
34-
return () => frames[i++];
35-
})();
36-
37-
const onDone = () => {
38-
resolve(Array.from(extractions.values()));
39-
};
40-
41-
const nextOrDone = () => {
42-
const next = nextFrame();
43-
if (next) {
44-
matchFrame(next);
45-
} else {
46-
onDone();
47-
}
48-
};
49-
50-
type FrameRef = {
51-
frame: undefined | ReplayFrame;
52-
nodeId: undefined | number;
53-
};
54-
55-
const nodeIdRef: FrameRef = {
56-
frame: undefined,
57-
nodeId: undefined,
58-
};
59-
60-
const handlePause = () => {
61-
if (!nodeIdRef.nodeId && !nodeIdRef.frame) {
62-
return;
63-
}
64-
const frame = nodeIdRef.frame as ReplayFrame;
65-
const nodeId = nodeIdRef.nodeId as number;
66-
21+
startTimestampMs,
22+
}: Args): Promise<Map<ReplayFrame, Extraction>> {
23+
return replayerStepper({
24+
frames,
25+
rrwebEvents,
26+
startTimestampMs,
27+
shouldVisitFrame: frame => {
28+
const nodeId = frame.data && 'nodeId' in frame.data ? frame.data.nodeId : undefined;
29+
return nodeId !== undefined && nodeId !== -1;
30+
},
31+
onVisitFrame: (frame, collection, replayer) => {
32+
const mirror = replayer.getMirror();
33+
const nodeId = frame.data && 'nodeId' in frame.data ? frame.data.nodeId : undefined;
6734
const html = extractHtml(nodeId as number, mirror);
68-
extractions.set(frame as ReplayFrame, {
35+
collection.set(frame as ReplayFrame, {
6936
frame,
7037
html,
7138
timestamp: frame.timestampMs,
7239
});
73-
nextOrDone();
74-
};
75-
76-
const matchFrame = frame => {
77-
nodeIdRef.frame = frame;
78-
nodeIdRef.nodeId =
79-
frame.data && 'nodeId' in frame.data ? frame.data.nodeId : undefined;
80-
81-
if (nodeIdRef.nodeId === undefined || nodeIdRef.nodeId === -1) {
82-
nextOrDone();
83-
return;
84-
}
85-
86-
window.setTimeout(() => {
87-
player.pause(frame.offsetMs);
88-
}, 0);
89-
};
90-
91-
player.on('pause', handlePause);
92-
matchFrame(nextFrame());
93-
});
94-
}
95-
96-
function createPlayer(rrwebEvents): Replayer {
97-
const domRoot = document.createElement('div');
98-
domRoot.className = 'sentry-block';
99-
const {style} = domRoot;
100-
101-
style.position = 'fixed';
102-
style.inset = '0';
103-
style.width = '0';
104-
style.height = '0';
105-
style.overflow = 'hidden';
106-
107-
document.body.appendChild(domRoot);
108-
109-
const replayerRef = new Replayer(rrwebEvents, {
110-
root: domRoot,
111-
loadTimeout: 1,
112-
showWarning: false,
113-
blockClass: 'sentry-block',
114-
speed: 99999,
115-
skipInactive: true,
116-
triggerFocus: false,
117-
mouseTail: false,
40+
},
11841
});
119-
return replayerRef;
12042
}
12143

12244
function extractHtml(nodeId: number, mirror: Mirror): string | null {

static/app/utils/replays/replayReader.tsx

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@ import {duration} from 'moment';
55

66
import domId from 'sentry/utils/domId';
77
import localStorageWrapper from 'sentry/utils/localStorage';
8-
import countDomNodes from 'sentry/utils/replays/countDomNodes';
9-
import extractDomNodes from 'sentry/utils/replays/extractDomNodes';
108
import hydrateBreadcrumbs, {
119
replayInitBreadcrumb,
1210
} from 'sentry/utils/replays/hydrateBreadcrumbs';
@@ -259,21 +257,6 @@ export default class ReplayReader {
259257
].sort(sortFrames)
260258
);
261259

262-
countDomNodes = memoize(() =>
263-
countDomNodes({
264-
frames: this.getRRWebMutations(),
265-
rrwebEvents: this.getRRWebFrames(),
266-
startTimestampMs: this._replayRecord.started_at.getTime(),
267-
})
268-
);
269-
270-
getDomNodes = memoize(() =>
271-
extractDomNodes({
272-
frames: this.getDOMFrames(),
273-
rrwebEvents: this.getRRWebFrames(),
274-
})
275-
);
276-
277260
getMemoryFrames = memoize(() =>
278261
this._sortedSpanFrames.filter((frame): frame is MemoryFrame => frame.op === 'memory')
279262
);
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import {Replayer} from '@sentry-internal/rrweb';
2+
3+
import type {RecordingFrame, ReplayFrame} from 'sentry/utils/replays/types';
4+
5+
interface Args<Frame extends ReplayFrame | RecordingFrame, CollectionData> {
6+
frames: Frame[] | undefined;
7+
onVisitFrame: (
8+
frame: Frame,
9+
collection: Map<Frame, CollectionData>,
10+
replayer: Replayer
11+
) => void;
12+
rrwebEvents: RecordingFrame[] | undefined;
13+
shouldVisitFrame: (frame: Frame, replayer: Replayer) => boolean;
14+
startTimestampMs: number;
15+
}
16+
17+
type FrameRef<Frame extends ReplayFrame | RecordingFrame> = {
18+
frame: Frame | undefined;
19+
};
20+
21+
export default function replayerStepper<
22+
Frame extends ReplayFrame | RecordingFrame,
23+
CollectionData,
24+
>({
25+
frames,
26+
onVisitFrame,
27+
rrwebEvents,
28+
shouldVisitFrame,
29+
startTimestampMs,
30+
}: Args<Frame, CollectionData>): Promise<Map<Frame, CollectionData>> {
31+
const collection = new Map<Frame, CollectionData>();
32+
33+
return new Promise(resolve => {
34+
if (!frames?.length || !rrwebEvents?.length) {
35+
resolve(new Map());
36+
return;
37+
}
38+
39+
const replayer = createHiddenPlayer(rrwebEvents);
40+
41+
const nextFrame = (function () {
42+
let i = 0;
43+
return () => frames[i++];
44+
})();
45+
46+
const onDone = () => {
47+
resolve(collection);
48+
};
49+
50+
const nextOrDone = () => {
51+
const next = nextFrame();
52+
if (next) {
53+
considerFrame(next);
54+
} else {
55+
onDone();
56+
}
57+
};
58+
59+
const frameRef: FrameRef<Frame> = {
60+
frame: undefined,
61+
};
62+
63+
const considerFrame = (frame: Frame) => {
64+
if (shouldVisitFrame(frame, replayer)) {
65+
frameRef.frame = frame;
66+
window.setTimeout(() => {
67+
const timestamp =
68+
'offsetMs' in frame ? frame.offsetMs : frame.timestamp - startTimestampMs;
69+
replayer.pause(timestamp);
70+
}, 0);
71+
} else {
72+
frameRef.frame = undefined;
73+
nextOrDone();
74+
}
75+
};
76+
77+
const handlePause = () => {
78+
onVisitFrame(frameRef.frame!, collection, replayer);
79+
nextOrDone();
80+
};
81+
82+
replayer.on('pause', handlePause);
83+
considerFrame(nextFrame());
84+
});
85+
}
86+
87+
function createHiddenPlayer(rrwebEvents: RecordingFrame[]): Replayer {
88+
const domRoot = document.createElement('div');
89+
domRoot.className = 'sentry-block';
90+
const {style} = domRoot;
91+
92+
style.position = 'fixed';
93+
style.inset = '0';
94+
style.width = '0';
95+
style.height = '0';
96+
style.overflow = 'hidden';
97+
98+
document.body.appendChild(domRoot);
99+
100+
return new Replayer(rrwebEvents, {
101+
root: domRoot,
102+
loadTimeout: 1,
103+
showWarning: false,
104+
blockClass: 'sentry-block',
105+
speed: 99999,
106+
skipInactive: true,
107+
triggerFocus: false,
108+
mouseTail: false,
109+
});
110+
}

0 commit comments

Comments
 (0)