diff --git a/static/app/components/replays/breadcrumbs/breadcrumbItem.tsx b/static/app/components/replays/breadcrumbs/breadcrumbItem.tsx index bc1ffcc65752a7..7651993712a5a1 100644 --- a/static/app/components/replays/breadcrumbs/breadcrumbItem.tsx +++ b/static/app/components/replays/breadcrumbs/breadcrumbItem.tsx @@ -18,7 +18,7 @@ import {useHasNewTimelineUI} from 'sentry/components/timeline/utils'; import {Tooltip} from 'sentry/components/tooltip'; import {t} from 'sentry/locale'; import {space} from 'sentry/styles/space'; -import type {Extraction} from 'sentry/utils/replays/extractDomNodes'; +import type {Extraction} from 'sentry/utils/replays/extractHtml'; import {getReplayDiffOffsetsFromFrame} from 'sentry/utils/replays/getDiffTimestamps'; import getFrameDetails from 'sentry/utils/replays/getFrameDetails'; import type ReplayReader from 'sentry/utils/replays/replayReader'; diff --git a/static/app/components/replays/diff/replayTextDiff.tsx b/static/app/components/replays/diff/replayTextDiff.tsx index 50fd76f2e745c7..2bafcd5458ef40 100644 --- a/static/app/components/replays/diff/replayTextDiff.tsx +++ b/static/app/components/replays/diff/replayTextDiff.tsx @@ -7,7 +7,7 @@ import {CopyToClipboardButton} from 'sentry/components/copyToClipboardButton'; import SplitDiff from 'sentry/components/splitDiff'; import {t} from 'sentry/locale'; import {space} from 'sentry/styles/space'; -import useExtractedPageHtml from 'sentry/utils/replays/hooks/useExtractedPageHtml'; +import useExtractPageHtml from 'sentry/utils/replays/hooks/useExtractPageHtml'; import type ReplayReader from 'sentry/utils/replays/replayReader'; interface Props { @@ -16,8 +16,8 @@ interface Props { rightOffsetMs: number; } -export function ReplayTextDiff({leftOffsetMs, replay, rightOffsetMs}: Props) { - const {data} = useExtractedPageHtml({ +export function ReplayTextDiff({replay, leftOffsetMs, rightOffsetMs}: Props) { + const {data} = useExtractPageHtml({ replay, offsetMsToStopAt: [leftOffsetMs, rightOffsetMs], }); diff --git a/static/app/utils/replays/countDomNodes.tsx b/static/app/utils/replays/countDomNodes.tsx deleted file mode 100644 index f219f504e0258c..00000000000000 --- a/static/app/utils/replays/countDomNodes.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import replayerStepper from 'sentry/utils/replays/replayerStepper'; -import type {RecordingFrame} from 'sentry/utils/replays/types'; - -export type DomNodeChartDatapoint = { - added: number; - count: number; - endTimestampMs: number; - removed: number; - startTimestampMs: number; - timestampMs: number; -}; - -type Args = { - frames: RecordingFrame[] | undefined; - rrwebEvents: RecordingFrame[] | undefined; - startTimestampMs: number; -}; - -export default function countDomNodes({ - frames, - rrwebEvents, - startTimestampMs, -}: Args): Promise> { - let frameCount = 0; - const length = frames?.length ?? 0; - const frameStep = Math.max(Math.round(length * 0.007), 1); - - let prevIds: number[] = []; - - return replayerStepper({ - frames, - rrwebEvents, - startTimestampMs, - 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; - }, - }); -} diff --git a/static/app/utils/replays/extractDomNodes.tsx b/static/app/utils/replays/extractHtml.tsx similarity index 54% rename from static/app/utils/replays/extractDomNodes.tsx rename to static/app/utils/replays/extractHtml.tsx index c225aa1f6f2d58..cb6333177d5396 100644 --- a/static/app/utils/replays/extractDomNodes.tsx +++ b/static/app/utils/replays/extractHtml.tsx @@ -1,11 +1,6 @@ import type {Mirror} from '@sentry-internal/rrweb-snapshot'; -import replayerStepper from 'sentry/utils/replays/replayerStepper'; -import { - getNodeId, - type RecordingFrame, - type ReplayFrame, -} from 'sentry/utils/replays/types'; +import type {ReplayFrame} from 'sentry/utils/replays/types'; export type Extraction = { frame: ReplayFrame; @@ -13,50 +8,7 @@ export type Extraction = { timestamp: number; }; -type Args = { - /** - * Frames where we should stop and extract html for a given dom node - */ - frames: ReplayFrame[] | undefined; - - /** - * The rrweb events that constitute the replay - */ - rrwebEvents: RecordingFrame[] | undefined; - - /** - * The replay start time, in ms - */ - startTimestampMs: number; -}; - -export default function extractDomNodes({ - frames, - rrwebEvents, - startTimestampMs, -}: Args): Promise> { - return replayerStepper({ - frames, - rrwebEvents, - startTimestampMs, - 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, - }); - }, - }); -} - -function extractHtml(nodeId: number, mirror: Mirror): string | null { +export default function extractHtml(nodeId: number, mirror: Mirror): string | null { const node = mirror.getNode(nodeId); const html = diff --git a/static/app/utils/replays/hooks/useCountDomNodes.tsx b/static/app/utils/replays/hooks/useCountDomNodes.tsx new file mode 100644 index 00000000000000..af11197e056fd1 --- /dev/null +++ b/static/app/utils/replays/hooks/useCountDomNodes.tsx @@ -0,0 +1,26 @@ +import {useQuery, type UseQueryResult} from 'sentry/utils/queryClient'; +import type ReplayReader from 'sentry/utils/replays/replayReader'; +import type {RecordingFrame} from 'sentry/utils/replays/types'; + +export type DomNodeChartDatapoint = { + added: number; + count: number; + endTimestampMs: number; + removed: number; + startTimestampMs: number; + timestampMs: number; +}; + +export default function useCountDomNodes({ + replay, +}: { + replay: null | ReplayReader; +}): UseQueryResult> { + return useQuery( + ['countDomNodes', replay], + () => { + return replay?.getCountDomNodes(); + }, + {enabled: Boolean(replay), cacheTime: Infinity} + ); +} diff --git a/static/app/utils/replays/hooks/useExtractDomNodes.tsx b/static/app/utils/replays/hooks/useExtractDomNodes.tsx new file mode 100644 index 00000000000000..29dc0609cc97cd --- /dev/null +++ b/static/app/utils/replays/hooks/useExtractDomNodes.tsx @@ -0,0 +1,18 @@ +import {useQuery, type UseQueryResult} from 'sentry/utils/queryClient'; +import type {Extraction} from 'sentry/utils/replays/extractHtml'; +import type ReplayReader from 'sentry/utils/replays/replayReader'; +import type {ReplayFrame} from 'sentry/utils/replays/types'; + +export default function useExtractDomNodes({ + replay, +}: { + replay: null | ReplayReader; +}): UseQueryResult> { + return useQuery( + ['getDomNodes', replay], + () => { + return replay?.getExtractDomNodes(); + }, + {enabled: Boolean(replay), cacheTime: Infinity} + ); +} diff --git a/static/app/utils/replays/extractPageHtml.tsx b/static/app/utils/replays/hooks/useExtractPageHtml.tsx similarity index 65% rename from static/app/utils/replays/extractPageHtml.tsx rename to static/app/utils/replays/hooks/useExtractPageHtml.tsx index 00d5d97bca4786..8cb4dff0d1b908 100644 --- a/static/app/utils/replays/extractPageHtml.tsx +++ b/static/app/utils/replays/hooks/useExtractPageHtml.tsx @@ -1,4 +1,6 @@ +import {useQuery} from 'sentry/utils/queryClient'; import replayerStepper from 'sentry/utils/replays/replayerStepper'; +import type ReplayReader from 'sentry/utils/replays/replayReader'; import type {RecordingFrame, ReplayFrame} from 'sentry/utils/replays/types'; type Args = { @@ -18,7 +20,7 @@ type Args = { startTimestampMs: number; }; -export default async function extactPageHtml({ +async function extractPageHtml({ offsetMsToStopAt, rrwebEvents, startTimestampMs, @@ -32,11 +34,11 @@ export default async function extactPageHtml({ frames, rrwebEvents, startTimestampMs, - shouldVisitFrame(_frame) { + shouldVisitFrame: () => { // Visit all the timestamps (converted to frames) that were passed in above return true; }, - onVisitFrame(frame, collection, replayer) { + onVisitFrame: (frame, collection, replayer) => { const doc = replayer.getMirror().getNode(1); const html = (doc as Document)?.body.outerHTML ?? ''; collection.set(frame, html); @@ -46,3 +48,21 @@ export default async function extactPageHtml({ return [frame.offsetMs, html]; }); } + +interface Props { + offsetMsToStopAt: number[]; + replay: ReplayReader | null; +} + +export default function useExtractPageHtml({replay, offsetMsToStopAt}: Props) { + return useQuery( + ['extactPageHtml', replay, offsetMsToStopAt], + () => + extractPageHtml({ + offsetMsToStopAt, + rrwebEvents: replay?.getRRWebFrames(), + startTimestampMs: replay?.getReplay().started_at.getTime() ?? 0, + }), + {enabled: Boolean(replay), cacheTime: Infinity} + ); +} diff --git a/static/app/utils/replays/hooks/useExtractedDomNodes.tsx b/static/app/utils/replays/hooks/useExtractedDomNodes.tsx deleted file mode 100644 index c5eb4c7566c217..00000000000000 --- a/static/app/utils/replays/hooks/useExtractedDomNodes.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import {useQuery} from 'sentry/utils/queryClient'; -import extractDomNodes from 'sentry/utils/replays/extractDomNodes'; -import type ReplayReader from 'sentry/utils/replays/replayReader'; - -export default function useExtractedDomNodes({replay}: {replay: null | ReplayReader}) { - return useQuery( - ['getDomNodes', replay], - () => - extractDomNodes({ - frames: replay?.getDOMFrames(), - rrwebEvents: replay?.getRRWebFrames(), - startTimestampMs: replay?.getReplay().started_at.getTime() ?? 0, - }), - {enabled: Boolean(replay), cacheTime: Infinity} - ); -} diff --git a/static/app/utils/replays/hooks/useExtractedPageHtml.tsx b/static/app/utils/replays/hooks/useExtractedPageHtml.tsx deleted file mode 100644 index 2906cff4395fbd..00000000000000 --- a/static/app/utils/replays/hooks/useExtractedPageHtml.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import {useQuery} from 'sentry/utils/queryClient'; -import extractPageHtml from 'sentry/utils/replays/extractPageHtml'; -import type ReplayReader from 'sentry/utils/replays/replayReader'; - -interface Props { - offsetMsToStopAt: number[]; - replay: ReplayReader | null; -} - -export default function useExtractedPageHtml({replay, offsetMsToStopAt}: Props) { - return useQuery( - ['extactPageHtml', replay, offsetMsToStopAt], - () => - extractPageHtml({ - offsetMsToStopAt, - rrwebEvents: replay?.getRRWebFrames(), - startTimestampMs: replay?.getReplay().started_at.getTime() ?? 0, - }), - {enabled: Boolean(replay), cacheTime: Infinity} - ); -} diff --git a/static/app/utils/replays/replayReader.tsx b/static/app/utils/replays/replayReader.tsx index 80699955c698f7..5b81ef33436a76 100644 --- a/static/app/utils/replays/replayReader.tsx +++ b/static/app/utils/replays/replayReader.tsx @@ -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, isBackgroundFrame, isDeadClick, @@ -139,6 +144,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, @@ -414,6 +466,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; /** diff --git a/static/app/utils/replays/replayerStepper.tsx b/static/app/utils/replays/replayerStepper.tsx index 77dae4de4f7238..ad9cf47b31f23a 100644 --- a/static/app/utils/replays/replayerStepper.tsx +++ b/static/app/utils/replays/replayerStepper.tsx @@ -17,7 +17,7 @@ interface Args { } type FrameRef = { - frame: Frame | undefined; + current: Frame | undefined; }; export default function replayerStepper< @@ -62,25 +62,25 @@ export default function replayerStepper< }; const frameRef: FrameRef = { - frame: undefined, + current: undefined, }; const considerFrame = (frame: Frame) => { if (shouldVisitFrame(frame, replayer)) { - frameRef.frame = frame; - window.setTimeout(() => { + frameRef.current = frame; + window.requestAnimationFrame(() => { const timestamp = 'offsetMs' in frame ? frame.offsetMs : frame.timestamp - startTimestampMs; replayer.pause(timestamp); - }, 0); + }); } else { - frameRef.frame = undefined; + frameRef.current = undefined; nextOrDone(); } }; const handlePause = () => { - onVisitFrame(frameRef.frame!, collection, replayer); + onVisitFrame(frameRef.current!, collection, replayer); nextOrDone(); }; diff --git a/static/app/views/replays/detail/breadcrumbs/breadcrumbRow.tsx b/static/app/views/replays/detail/breadcrumbs/breadcrumbRow.tsx index 33f9eac5a6bf2c..f0a0a818ef1bcf 100644 --- a/static/app/views/replays/detail/breadcrumbs/breadcrumbRow.tsx +++ b/static/app/views/replays/detail/breadcrumbs/breadcrumbRow.tsx @@ -4,7 +4,7 @@ import classNames from 'classnames'; import BreadcrumbItem from 'sentry/components/replays/breadcrumbs/breadcrumbItem'; import {useReplayContext} from 'sentry/components/replays/replayContext'; -import type {Extraction} from 'sentry/utils/replays/extractDomNodes'; +import type {Extraction} from 'sentry/utils/replays/extractHtml'; import useCrumbHandlers from 'sentry/utils/replays/hooks/useCrumbHandlers'; import useCurrentHoverTime from 'sentry/utils/replays/playback/providers/useCurrentHoverTime'; import type {ReplayFrame} from 'sentry/utils/replays/types'; diff --git a/static/app/views/replays/detail/breadcrumbs/index.tsx b/static/app/views/replays/detail/breadcrumbs/index.tsx index 9423985810a2d0..5cc7c9a98d0bae 100644 --- a/static/app/views/replays/detail/breadcrumbs/index.tsx +++ b/static/app/views/replays/detail/breadcrumbs/index.tsx @@ -8,7 +8,7 @@ import {useReplayContext} from 'sentry/components/replays/replayContext'; import useJumpButtons from 'sentry/components/replays/useJumpButtons'; import {t} from 'sentry/locale'; import useCrumbHandlers from 'sentry/utils/replays/hooks/useCrumbHandlers'; -import useExtractedDomNodes from 'sentry/utils/replays/hooks/useExtractedDomNodes'; +import useExtractDomNodes from 'sentry/utils/replays/hooks/useExtractDomNodes'; import useVirtualizedInspector from 'sentry/views/replays/detail//useVirtualizedInspector'; import BreadcrumbFilters from 'sentry/views/replays/detail/breadcrumbs/breadcrumbFilters'; import BreadcrumbRow from 'sentry/views/replays/detail/breadcrumbs/breadcrumbRow'; @@ -30,8 +30,10 @@ const cellMeasurer = { function Breadcrumbs() { const {currentTime, replay} = useReplayContext(); const {onClickTimestamp} = useCrumbHandlers(); - const {data: frameToExtraction, isFetching: isFetchingExtractions} = - useExtractedDomNodes({replay}); + + const {data: frameToExtraction, isFetching: isFetchingExtractions} = useExtractDomNodes( + {replay} + ); const startTimestampMs = replay?.getStartTimestampMs() ?? 0; const frames = replay?.getChapterFrames(); diff --git a/static/app/views/replays/detail/memoryPanel/domNodesChart.tsx b/static/app/views/replays/detail/memoryPanel/domNodesChart.tsx index a9ba0aa74df53c..4437e483633150 100644 --- a/static/app/views/replays/detail/memoryPanel/domNodesChart.tsx +++ b/static/app/views/replays/detail/memoryPanel/domNodesChart.tsx @@ -16,7 +16,7 @@ import {getFormattedDate} from 'sentry/utils/dates'; import {axisLabelFormatter} from 'sentry/utils/discover/charts'; import domId from 'sentry/utils/domId'; import formatReplayDuration from 'sentry/utils/duration/formatReplayDuration'; -import type {DomNodeChartDatapoint} from 'sentry/utils/replays/countDomNodes'; +import type {DomNodeChartDatapoint} from 'sentry/utils/replays/hooks/useCountDomNodes'; interface Props extends Pick, 'currentTime' | 'setCurrentTime'> { diff --git a/static/app/views/replays/detail/memoryPanel/index.tsx b/static/app/views/replays/detail/memoryPanel/index.tsx index 9358cb2d18fb74..6fc2c0d206c71a 100644 --- a/static/app/views/replays/detail/memoryPanel/index.tsx +++ b/static/app/views/replays/detail/memoryPanel/index.tsx @@ -6,33 +6,20 @@ import Placeholder from 'sentry/components/placeholder'; import {useReplayContext} from 'sentry/components/replays/replayContext'; import {t} from 'sentry/locale'; import {space} from 'sentry/styles/space'; -import {useQuery} from 'sentry/utils/queryClient'; -import countDomNodes from 'sentry/utils/replays/countDomNodes'; +import useCountDomNodes from 'sentry/utils/replays/hooks/useCountDomNodes'; import useCurrentHoverTime from 'sentry/utils/replays/playback/providers/useCurrentHoverTime'; -import type ReplayReader from 'sentry/utils/replays/replayReader'; import DomNodesChart from 'sentry/views/replays/detail/memoryPanel/domNodesChart'; import MemoryChart from 'sentry/views/replays/detail/memoryPanel/memoryChart'; -function useCountDomNodes({replay}: {replay: null | ReplayReader}) { - return useQuery( - ['countDomNodes', replay], - () => - countDomNodes({ - frames: replay?.getRRWebMutations(), - rrwebEvents: replay?.getRRWebFrames(), - startTimestampMs: replay?.getStartTimestampMs() ?? 0, - }), - {enabled: Boolean(replay), cacheTime: Infinity} - ); -} - export default function MemoryPanel() { const {currentTime, isFetching, replay, setCurrentTime} = useReplayContext(); const [currentHoverTime, setCurrentHoverTime] = useCurrentHoverTime(); const memoryFrames = replay?.getMemoryFrames(); - const {data: frameToCount} = useCountDomNodes({replay}); + const {data: frameToCount, isLoading: isDomNodeDataLoading} = useCountDomNodes({ + replay, + }); const domNodeData = useMemo( () => Array.from(frameToCount?.values() || []), [frameToCount] @@ -65,7 +52,7 @@ export default function MemoryPanel() { ); const domNodesChart = - !replay || isFetching ? ( + !replay || isDomNodeDataLoading ? ( ) : (