|
@@ -1,4 +1,5 @@
|
|
|
import * as Sentry from '@sentry/react';
|
|
|
+import type {eventWithTime} from '@sentry-internal/rrweb';
|
|
|
import memoize from 'lodash/memoize';
|
|
|
import {type Duration, duration} from 'moment-timezone';
|
|
|
|
|
@@ -6,6 +7,7 @@ import {defined} from 'sentry/utils';
|
|
|
import domId from 'sentry/utils/domId';
|
|
|
import localStorageWrapper from 'sentry/utils/localStorage';
|
|
|
import clamp from 'sentry/utils/number/clamp';
|
|
|
+import extractHtml from 'sentry/utils/replays/extractHtml';
|
|
|
import hydrateBreadcrumbs, {
|
|
|
replayInitBreadcrumb,
|
|
|
} from 'sentry/utils/replays/hydrateBreadcrumbs';
|
|
@@ -17,6 +19,7 @@ import {
|
|
|
} from 'sentry/utils/replays/hydrateRRWebRecordingFrames';
|
|
|
import hydrateSpans from 'sentry/utils/replays/hydrateSpans';
|
|
|
import {replayTimestamps} from 'sentry/utils/replays/replayDataUtils';
|
|
|
+import replayerStepper from 'sentry/utils/replays/replayerStepper';
|
|
|
import type {
|
|
|
BreadcrumbFrame,
|
|
|
ClipWindow,
|
|
@@ -26,6 +29,7 @@ import type {
|
|
|
MemoryFrame,
|
|
|
OptionFrame,
|
|
|
RecordingFrame,
|
|
|
+ ReplayFrame,
|
|
|
serializedNodeWithId,
|
|
|
SlowClickFrame,
|
|
|
SpanFrame,
|
|
@@ -34,6 +38,7 @@ import type {
|
|
|
import {
|
|
|
BreadcrumbCategories,
|
|
|
EventType,
|
|
|
+ getNodeId,
|
|
|
IncrementalSource,
|
|
|
isDeadClick,
|
|
|
isDeadRageClick,
|
|
@@ -137,6 +142,53 @@ function removeDuplicateNavCrumbs(
|
|
|
return otherBreadcrumbFrames.concat(uniqueNavCrumbs);
|
|
|
}
|
|
|
|
|
|
+const extractDomNodes = {
|
|
|
+ shouldVisitFrame: frame => {
|
|
|
+ const nodeId = getNodeId(frame);
|
|
|
+ return nodeId !== undefined && nodeId !== -1;
|
|
|
+ },
|
|
|
+ onVisitFrame: (frame, collection, replayer) => {
|
|
|
+ const mirror = replayer.getMirror();
|
|
|
+ const nodeId = getNodeId(frame);
|
|
|
+ const html = extractHtml(nodeId as number, mirror);
|
|
|
+ collection.set(frame as ReplayFrame, {
|
|
|
+ frame,
|
|
|
+ html,
|
|
|
+ timestamp: frame.timestampMs,
|
|
|
+ });
|
|
|
+ },
|
|
|
+};
|
|
|
+
|
|
|
+const countDomNodes = function (frames: eventWithTime[]) {
|
|
|
+ let frameCount = 0;
|
|
|
+ const length = frames?.length ?? 0;
|
|
|
+ const frameStep = Math.max(Math.round(length * 0.007), 1);
|
|
|
+
|
|
|
+ let prevIds: number[] = [];
|
|
|
+
|
|
|
+ return {
|
|
|
+ shouldVisitFrame() {
|
|
|
+ frameCount++;
|
|
|
+ return frameCount % frameStep === 0;
|
|
|
+ },
|
|
|
+ onVisitFrame(frame, collection, replayer) {
|
|
|
+ const ids = replayer.getMirror().getIds(); // gets list of DOM nodes present
|
|
|
+ const count = ids.length;
|
|
|
+ const added = ids.filter(id => !prevIds.includes(id)).length;
|
|
|
+ const removed = prevIds.filter(id => !ids.includes(id)).length;
|
|
|
+ collection.set(frame as RecordingFrame, {
|
|
|
+ count,
|
|
|
+ added,
|
|
|
+ removed,
|
|
|
+ timestampMs: frame.timestamp,
|
|
|
+ startTimestampMs: frame.timestamp,
|
|
|
+ endTimestampMs: frame.timestamp,
|
|
|
+ });
|
|
|
+ prevIds = ids;
|
|
|
+ },
|
|
|
+ };
|
|
|
+};
|
|
|
+
|
|
|
export default class ReplayReader {
|
|
|
static factory({
|
|
|
attachments,
|
|
@@ -412,6 +464,34 @@ export default class ReplayReader {
|
|
|
return this.processingErrors().length;
|
|
|
};
|
|
|
|
|
|
+ getCountDomNodes = memoize(async () => {
|
|
|
+ const {onVisitFrame, shouldVisitFrame} = countDomNodes(this.getRRWebMutations());
|
|
|
+
|
|
|
+ const results = await replayerStepper({
|
|
|
+ frames: this.getRRWebMutations(),
|
|
|
+ rrwebEvents: this.getRRWebFrames(),
|
|
|
+ startTimestampMs: this.getReplay().started_at.getTime() ?? 0,
|
|
|
+ onVisitFrame,
|
|
|
+ shouldVisitFrame,
|
|
|
+ });
|
|
|
+
|
|
|
+ return results;
|
|
|
+ });
|
|
|
+
|
|
|
+ getExtractDomNodes = memoize(async () => {
|
|
|
+ const {onVisitFrame, shouldVisitFrame} = extractDomNodes;
|
|
|
+
|
|
|
+ const results = await replayerStepper({
|
|
|
+ frames: this.getDOMFrames(),
|
|
|
+ rrwebEvents: this.getRRWebFrames(),
|
|
|
+ startTimestampMs: this.getReplay().started_at.getTime() ?? 0,
|
|
|
+ onVisitFrame,
|
|
|
+ shouldVisitFrame,
|
|
|
+ });
|
|
|
+
|
|
|
+ return results;
|
|
|
+ });
|
|
|
+
|
|
|
getClipWindow = () => this._clipWindow;
|
|
|
|
|
|
/**
|