From 7782d245c58a65e690cbab6e7c47885a8d6c3ca4 Mon Sep 17 00:00:00 2001 From: Michelle Zhang <56095982+michellewzhang@users.noreply.github.com> Date: Thu, 18 Jul 2024 16:03:55 -0700 Subject: [PATCH 1/8] ref(replay): refactor replayerStepper to be called once --- .../replays/diff/replayTextDiff.tsx | 14 ++-- static/app/utils/replays/countDomNodes.tsx | 54 ------------ .../{extractDomNodes.tsx => extractHtml.tsx} | 52 +----------- static/app/utils/replays/extractPageHtml.tsx | 48 ----------- .../utils/replays/hooks/extractDomNodes.tsx | 5 ++ .../utils/replays/hooks/extractPageHtml.tsx | 10 +++ .../replays/hooks/useExtractedDomNodes.tsx | 16 ---- .../replays/hooks/useExtractedPageHtml.tsx | 21 ----- static/app/utils/replays/replayReader.tsx | 83 ++++++++++++++++++- static/app/utils/replays/replayerStepper.tsx | 65 +++++++++------ .../replays/detail/breadcrumbs/index.tsx | 18 ++-- .../replays/detail/memoryPanel/index.tsx | 17 +--- 12 files changed, 164 insertions(+), 239 deletions(-) delete mode 100644 static/app/utils/replays/countDomNodes.tsx rename static/app/utils/replays/{extractDomNodes.tsx => extractHtml.tsx} (54%) delete mode 100644 static/app/utils/replays/extractPageHtml.tsx create mode 100644 static/app/utils/replays/hooks/extractDomNodes.tsx create mode 100644 static/app/utils/replays/hooks/extractPageHtml.tsx delete mode 100644 static/app/utils/replays/hooks/useExtractedDomNodes.tsx delete mode 100644 static/app/utils/replays/hooks/useExtractedPageHtml.tsx diff --git a/static/app/components/replays/diff/replayTextDiff.tsx b/static/app/components/replays/diff/replayTextDiff.tsx index 50fd76f2e745c7..d52e1782959237 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 extractPageHtml from 'sentry/utils/replays/hooks/extractPageHtml'; import type ReplayReader from 'sentry/utils/replays/replayReader'; interface Props { @@ -16,10 +16,14 @@ interface Props { rightOffsetMs: number; } -export function ReplayTextDiff({leftOffsetMs, replay, rightOffsetMs}: Props) { - const {data} = useExtractedPageHtml({ - replay, - offsetMsToStopAt: [leftOffsetMs, rightOffsetMs], +export function ReplayTextDiff({replay}: Props) { + const results = + extractPageHtml({ + replay, + // offsetMsToStopAt: [leftOffsetMs, rightOffsetMs], + }) ?? new Map(); + const data = Array.from(results.entries()).map(([frame, html]) => { + return [frame.offsetMs, html]; }); const [leftBody, rightBody] = useMemo( 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/extractPageHtml.tsx b/static/app/utils/replays/extractPageHtml.tsx deleted file mode 100644 index 00d5d97bca4786..00000000000000 --- a/static/app/utils/replays/extractPageHtml.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import replayerStepper from 'sentry/utils/replays/replayerStepper'; -import type {RecordingFrame, ReplayFrame} from 'sentry/utils/replays/types'; - -type Args = { - /** - * Offsets where we should stop and take a snapshot of the rendered HTML - */ - offsetMsToStopAt: number[]; - - /** - * The rrweb events that constitute the replay - */ - rrwebEvents: RecordingFrame[] | undefined; - - /** - * The replay startTimestampMs - */ - startTimestampMs: number; -}; - -export default async function extactPageHtml({ - offsetMsToStopAt, - rrwebEvents, - startTimestampMs, -}: Args): Promise<[number, string][]> { - const frames: ReplayFrame[] = offsetMsToStopAt.map(offsetMs => ({ - offsetMs, - timestamp: new Date(startTimestampMs + offsetMs), - timestampMs: startTimestampMs + offsetMs, - })) as ReplayFrame[]; // TODO Don't smash types into `as ReplayFrame[]`, instead make the object really conform - const results = await replayerStepper({ - frames, - rrwebEvents, - startTimestampMs, - shouldVisitFrame(_frame) { - // Visit all the timestamps (converted to frames) that were passed in above - return true; - }, - onVisitFrame(frame, collection, replayer) { - const doc = replayer.getMirror().getNode(1); - const html = (doc as Document)?.body.outerHTML ?? ''; - collection.set(frame, html); - }, - }); - return Array.from(results.entries()).map(([frame, html]) => { - return [frame.offsetMs, html]; - }); -} diff --git a/static/app/utils/replays/hooks/extractDomNodes.tsx b/static/app/utils/replays/hooks/extractDomNodes.tsx new file mode 100644 index 00000000000000..2ec8c3fd70cc8c --- /dev/null +++ b/static/app/utils/replays/hooks/extractDomNodes.tsx @@ -0,0 +1,5 @@ +import type ReplayReader from 'sentry/utils/replays/replayReader'; + +export default function extractDomNodes({replay}: {replay: null | ReplayReader}) { + return replay?.getExtractDomNodes(); +} diff --git a/static/app/utils/replays/hooks/extractPageHtml.tsx b/static/app/utils/replays/hooks/extractPageHtml.tsx new file mode 100644 index 00000000000000..9d72b4d1ebd328 --- /dev/null +++ b/static/app/utils/replays/hooks/extractPageHtml.tsx @@ -0,0 +1,10 @@ +import type ReplayReader from 'sentry/utils/replays/replayReader'; + +interface Props { + // offsetMsToStopAt: number[]; + replay: ReplayReader | null; +} + +export default function extractPageHtml({replay}: Props) { + return replay?.getExtractPageHtml(); +} 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..252d7bb59e241a 100644 --- a/static/app/utils/replays/replayReader.tsx +++ b/static/app/utils/replays/replayReader.tsx @@ -6,6 +6,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 +18,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 +28,7 @@ import type { MemoryFrame, OptionFrame, RecordingFrame, + ReplayFrame, serializedNodeWithId, SlowClickFrame, SpanFrame, @@ -34,6 +37,7 @@ import type { import { BreadcrumbCategories, EventType, + getNodeId, IncrementalSource, isBackgroundFrame, isDeadClick, @@ -139,6 +143,65 @@ function removeDuplicateNavCrumbs( return otherBreadcrumbFrames.concat(uniqueNavCrumbs); } +const extractPageHtml = { + shouldVisitFrame: () => { + // Visit all the timestamps (converted to frames) that are passed in + return true; + }, + onVisitFrame: (frame, collection, replayer) => { + const doc = replayer.getMirror().getNode(1); + const html = (doc as Document)?.body.outerHTML ?? ''; + collection.set(frame, html); + }, +}; + +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 () { + 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, @@ -291,8 +354,9 @@ export default class ReplayReader { private _startOffsetMs = 0; private _videoEvents: VideoEvent[] = []; private _clipWindow: ClipWindow | undefined = undefined; + private _collections: Record> = {}; - private _applyClipWindow = (clipWindow: ClipWindow) => { + private _applyClipWindow = async (clipWindow: ClipWindow) => { const clipStartTimestampMs = clamp( clipWindow.startTimestampMs, this._replayRecord.started_at.getTime(), @@ -342,6 +406,17 @@ export default class ReplayReader { return; } + this._collections = await replayerStepper({ + frames: this.getRRWebMutations(), + rrwebEvents: this.getRRWebFrames(), + startTimestampMs: this.getReplay().started_at.getTime() ?? 0, + visitFrameCallbacks: { + extractDomNodes, + extractPageHtml, + countDomNodes: countDomNodes(), + }, + }); + // For RRWeb frames we only trim from the end because playback will // not work otherwise. The start offset is used to begin playback at // the correct time. @@ -414,6 +489,12 @@ export default class ReplayReader { return this.processingErrors().length; }; + getCountDomNodes = () => this._collections.countDomNodes; + + getExtractDomNodes = () => this._collections.extractDomNodes; + + getExtractPageHtml = () => this._collections.extractPageHtml; + getClipWindow = () => this._clipWindow; /** diff --git a/static/app/utils/replays/replayerStepper.tsx b/static/app/utils/replays/replayerStepper.tsx index 77dae4de4f7238..f33572313ad9f5 100644 --- a/static/app/utils/replays/replayerStepper.tsx +++ b/static/app/utils/replays/replayerStepper.tsx @@ -4,16 +4,27 @@ import type {RecordingFrame, ReplayFrame} from 'sentry/utils/replays/types'; import {createHiddenPlayer} from './createHiddenPlayer'; +type OnVisitFrameType = ( + frame: Frame, + collection: Map, + replayer: Replayer +) => void; + +type ShouldVisitFrameType = ( + frame: Frame, + replayer: Replayer +) => boolean; + +type CallbackArgs = { + onVisitFrame: OnVisitFrameType; + shouldVisitFrame: ShouldVisitFrameType; +}; + interface Args { frames: Frame[] | undefined; - onVisitFrame: ( - frame: Frame, - collection: Map, - replayer: Replayer - ) => void; rrwebEvents: RecordingFrame[] | undefined; - shouldVisitFrame: (frame: Frame, replayer: Replayer) => boolean; startTimestampMs: number; + visitFrameCallbacks: Record>; } type FrameRef = { @@ -25,16 +36,18 @@ export default function replayerStepper< CollectionData, >({ frames, - onVisitFrame, rrwebEvents, - shouldVisitFrame, + visitFrameCallbacks, startTimestampMs, -}: Args): Promise> { - const collection = new Map(); +}: Args): Promise>> { + const collection = {}; + Object.keys(visitFrameCallbacks).forEach( + k => (collection[k] = new Map()) + ); return new Promise(resolve => { if (!frames?.length || !rrwebEvents?.length) { - resolve(new Map()); + resolve({result: new Map()}); return; } @@ -66,22 +79,26 @@ export default function replayerStepper< }; 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(); - } + Object.values(visitFrameCallbacks).forEach(v => { + if (v.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(); + Object.entries(visitFrameCallbacks).forEach(([k, v]) => { + v.onVisitFrame(frameRef.frame!, collection[k], replayer); + nextOrDone(); + }); }; replayer.on('pause', handlePause); diff --git a/static/app/views/replays/detail/breadcrumbs/index.tsx b/static/app/views/replays/detail/breadcrumbs/index.tsx index 9423985810a2d0..1f456647bef765 100644 --- a/static/app/views/replays/detail/breadcrumbs/index.tsx +++ b/static/app/views/replays/detail/breadcrumbs/index.tsx @@ -7,8 +7,8 @@ import JumpButtons from 'sentry/components/replays/jumpButtons'; import {useReplayContext} from 'sentry/components/replays/replayContext'; import useJumpButtons from 'sentry/components/replays/useJumpButtons'; import {t} from 'sentry/locale'; +import extractDomNodes from 'sentry/utils/replays/hooks/extractDomNodes'; import useCrumbHandlers from 'sentry/utils/replays/hooks/useCrumbHandlers'; -import useExtractedDomNodes from 'sentry/utils/replays/hooks/useExtractedDomNodes'; 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,12 @@ const cellMeasurer = { function Breadcrumbs() { const {currentTime, replay} = useReplayContext(); const {onClickTimestamp} = useCrumbHandlers(); - const {data: frameToExtraction, isFetching: isFetchingExtractions} = - useExtractedDomNodes({replay}); + // const {data: frameToExtraction, isFetching: isFetchingExtractions} = + // useExtractedDomNodes({replay}); + + const frameToExtraction = extractDomNodes({ + replay, + }); const startTimestampMs = replay?.getStartTimestampMs() ?? 0; const frames = replay?.getChapterFrames(); @@ -75,10 +79,11 @@ function Breadcrumbs() { // Need to refresh the item dimensions as DOM data gets loaded useEffect(() => { - if (!isFetchingExtractions) { + // if (!isFetchingExtractions) { + if (frameToExtraction) { updateList(); } - }, [isFetchingExtractions, updateList]); + }, [frameToExtraction, updateList]); const renderRow = ({index, key, style, parent}: ListRowProps) => { const item = (items || [])[index]; @@ -109,7 +114,8 @@ function Breadcrumbs() { return ( - + {/* */} + diff --git a/static/app/views/replays/detail/memoryPanel/index.tsx b/static/app/views/replays/detail/memoryPanel/index.tsx index 9358cb2d18fb74..734f13e91d62f6 100644 --- a/static/app/views/replays/detail/memoryPanel/index.tsx +++ b/static/app/views/replays/detail/memoryPanel/index.tsx @@ -6,24 +6,13 @@ 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 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} - ); +function countDomNodes({replay}: {replay: null | ReplayReader}) { + return replay?.getCountDomNodes(); } export default function MemoryPanel() { @@ -32,7 +21,7 @@ export default function MemoryPanel() { const memoryFrames = replay?.getMemoryFrames(); - const {data: frameToCount} = useCountDomNodes({replay}); + const frameToCount = countDomNodes({replay}); const domNodeData = useMemo( () => Array.from(frameToCount?.values() || []), [frameToCount] From e31f109912115d8bf8443ea8f02e746e4735c32b Mon Sep 17 00:00:00 2001 From: Michelle Zhang <56095982+michellewzhang@users.noreply.github.com> Date: Thu, 18 Jul 2024 23:05:21 -0700 Subject: [PATCH 2/8] :recycle: ref --- .../replays/diff/replayTextDiff.tsx | 9 ++--- .../utils/replays/hooks/extractPageHtml.tsx | 12 +++--- .../replays/hooks/useExtractedDomNodes.tsx | 14 +++++++ .../replays/hooks/useExtractedPageHtml.tsx | 19 +++++++++ static/app/utils/replays/replayReader.tsx | 40 +++++++++++-------- .../replays/detail/breadcrumbs/index.tsx | 22 +++++----- .../replays/detail/memoryPanel/index.tsx | 14 ++++++- 7 files changed, 88 insertions(+), 42 deletions(-) create mode 100644 static/app/utils/replays/hooks/useExtractedDomNodes.tsx create mode 100644 static/app/utils/replays/hooks/useExtractedPageHtml.tsx diff --git a/static/app/components/replays/diff/replayTextDiff.tsx b/static/app/components/replays/diff/replayTextDiff.tsx index d52e1782959237..bfd7979376a403 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 extractPageHtml from 'sentry/utils/replays/hooks/extractPageHtml'; +import useExtractedPageHtml from 'sentry/utils/replays/hooks/useExtractedPageHtml'; import type ReplayReader from 'sentry/utils/replays/replayReader'; interface Props { @@ -17,14 +17,11 @@ interface Props { } export function ReplayTextDiff({replay}: Props) { - const results = - extractPageHtml({ + const {data} = + useExtractedPageHtml({ replay, // offsetMsToStopAt: [leftOffsetMs, rightOffsetMs], }) ?? new Map(); - const data = Array.from(results.entries()).map(([frame, html]) => { - return [frame.offsetMs, html]; - }); const [leftBody, rightBody] = useMemo( () => data?.map(([_, html]) => beautify.html(html, {indent_size: 2})) ?? [], diff --git a/static/app/utils/replays/hooks/extractPageHtml.tsx b/static/app/utils/replays/hooks/extractPageHtml.tsx index 9d72b4d1ebd328..cac0e422e2af28 100644 --- a/static/app/utils/replays/hooks/extractPageHtml.tsx +++ b/static/app/utils/replays/hooks/extractPageHtml.tsx @@ -1,10 +1,8 @@ import type ReplayReader from 'sentry/utils/replays/replayReader'; -interface Props { - // offsetMsToStopAt: number[]; - replay: ReplayReader | null; -} - -export default function extractPageHtml({replay}: Props) { - return replay?.getExtractPageHtml(); +export default async function extractPageHtml({replay}: {replay: null | ReplayReader}) { + const results = await replay?.getExtractPageHtml(); + return Array.from(results?.entries() ?? []).map(([frame, html]) => { + return [frame.offsetMs, html]; + }); } diff --git a/static/app/utils/replays/hooks/useExtractedDomNodes.tsx b/static/app/utils/replays/hooks/useExtractedDomNodes.tsx new file mode 100644 index 00000000000000..9fefa3c8a38355 --- /dev/null +++ b/static/app/utils/replays/hooks/useExtractedDomNodes.tsx @@ -0,0 +1,14 @@ +import {useQuery} from 'sentry/utils/queryClient'; +import extractDomNodes from 'sentry/utils/replays/hooks/extractDomNodes'; +import type ReplayReader from 'sentry/utils/replays/replayReader'; + +export default function useExtractedDomNodes({replay}: {replay: null | ReplayReader}) { + return useQuery( + ['getDomNodes', replay], + () => + extractDomNodes({ + replay, + }), + {enabled: Boolean(replay), cacheTime: Infinity} + ); +} diff --git a/static/app/utils/replays/hooks/useExtractedPageHtml.tsx b/static/app/utils/replays/hooks/useExtractedPageHtml.tsx new file mode 100644 index 00000000000000..155895616010b0 --- /dev/null +++ b/static/app/utils/replays/hooks/useExtractedPageHtml.tsx @@ -0,0 +1,19 @@ +import {useQuery} from 'sentry/utils/queryClient'; +import extractPageHtml from 'sentry/utils/replays/hooks/extractPageHtml'; +import type ReplayReader from 'sentry/utils/replays/replayReader'; + +interface Props { + // offsetMsToStopAt: number[]; + replay: ReplayReader | null; +} + +export default function useExtractedPageHtml({replay}: Props) { + return useQuery( + ['extactPageHtml', replay], + () => + extractPageHtml({ + replay, + }), + {enabled: Boolean(replay), cacheTime: Infinity} + ); +} diff --git a/static/app/utils/replays/replayReader.tsx b/static/app/utils/replays/replayReader.tsx index 252d7bb59e241a..26a64d8da1b45e 100644 --- a/static/app/utils/replays/replayReader.tsx +++ b/static/app/utils/replays/replayReader.tsx @@ -354,9 +354,8 @@ export default class ReplayReader { private _startOffsetMs = 0; private _videoEvents: VideoEvent[] = []; private _clipWindow: ClipWindow | undefined = undefined; - private _collections: Record> = {}; - private _applyClipWindow = async (clipWindow: ClipWindow) => { + private _applyClipWindow = (clipWindow: ClipWindow) => { const clipStartTimestampMs = clamp( clipWindow.startTimestampMs, this._replayRecord.started_at.getTime(), @@ -406,17 +405,6 @@ export default class ReplayReader { return; } - this._collections = await replayerStepper({ - frames: this.getRRWebMutations(), - rrwebEvents: this.getRRWebFrames(), - startTimestampMs: this.getReplay().started_at.getTime() ?? 0, - visitFrameCallbacks: { - extractDomNodes, - extractPageHtml, - countDomNodes: countDomNodes(), - }, - }); - // For RRWeb frames we only trim from the end because playback will // not work otherwise. The start offset is used to begin playback at // the correct time. @@ -489,11 +477,20 @@ export default class ReplayReader { return this.processingErrors().length; }; - getCountDomNodes = () => this._collections.countDomNodes; + getCountDomNodes = async () => { + const results = await this._collections; + return results.countDomNodes; + }; - getExtractDomNodes = () => this._collections.extractDomNodes; + getExtractDomNodes = async () => { + const results = await this._collections; + return results.extractDomNodes; + }; - getExtractPageHtml = () => this._collections.extractPageHtml; + getExtractPageHtml = async () => { + const results = await this._collections; + return results.extractPageHtml; + }; getClipWindow = () => this._clipWindow; @@ -648,6 +645,17 @@ export default class ReplayReader { getSDKOptions = () => this._optionFrame; + private _collections = replayerStepper({ + frames: this.getRRWebMutations(), + rrwebEvents: this.getRRWebFrames(), + startTimestampMs: this.getReplay().started_at.getTime() ?? 0, + visitFrameCallbacks: { + extractDomNodes, + extractPageHtml, + countDomNodes: countDomNodes(), + }, + }); + /** * Checks the replay to see if user has any canvas elements in their * application. Needed to inform them that we now support canvas in replays. diff --git a/static/app/views/replays/detail/breadcrumbs/index.tsx b/static/app/views/replays/detail/breadcrumbs/index.tsx index 1f456647bef765..e1bbf19bfb9dc1 100644 --- a/static/app/views/replays/detail/breadcrumbs/index.tsx +++ b/static/app/views/replays/detail/breadcrumbs/index.tsx @@ -1,14 +1,15 @@ import {useEffect, useMemo, useRef, useState} from 'react'; import type {ListRowProps} from 'react-virtualized'; import {AutoSizer, CellMeasurer, List as ReactVirtualizedList} from 'react-virtualized'; +import type {eventWithTime} from '@sentry-internal/rrweb'; import Placeholder from 'sentry/components/placeholder'; import JumpButtons from 'sentry/components/replays/jumpButtons'; import {useReplayContext} from 'sentry/components/replays/replayContext'; import useJumpButtons from 'sentry/components/replays/useJumpButtons'; import {t} from 'sentry/locale'; -import extractDomNodes from 'sentry/utils/replays/hooks/extractDomNodes'; import useCrumbHandlers from 'sentry/utils/replays/hooks/useCrumbHandlers'; +import useExtractedDomNodes from 'sentry/utils/replays/hooks/useExtractedDomNodes'; 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,12 +31,11 @@ const cellMeasurer = { function Breadcrumbs() { const {currentTime, replay} = useReplayContext(); const {onClickTimestamp} = useCrumbHandlers(); - // const {data: frameToExtraction, isFetching: isFetchingExtractions} = - // useExtractedDomNodes({replay}); - const frameToExtraction = extractDomNodes({ - replay, - }); + const {data: frameToExtraction, isFetching: isFetchingExtractions} = + useExtractedDomNodes({ + replay, + }); const startTimestampMs = replay?.getStartTimestampMs() ?? 0; const frames = replay?.getChapterFrames(); @@ -79,11 +79,10 @@ function Breadcrumbs() { // Need to refresh the item dimensions as DOM data gets loaded useEffect(() => { - // if (!isFetchingExtractions) { - if (frameToExtraction) { + if (!isFetchingExtractions) { updateList(); } - }, [frameToExtraction, updateList]); + }, [isFetchingExtractions, updateList]); const renderRow = ({index, key, style, parent}: ListRowProps) => { const item = (items || [])[index]; @@ -99,7 +98,7 @@ function Breadcrumbs() { - {/* */} - + diff --git a/static/app/views/replays/detail/memoryPanel/index.tsx b/static/app/views/replays/detail/memoryPanel/index.tsx index 734f13e91d62f6..a7cea94c9fc1d2 100644 --- a/static/app/views/replays/detail/memoryPanel/index.tsx +++ b/static/app/views/replays/detail/memoryPanel/index.tsx @@ -6,6 +6,7 @@ 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 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'; @@ -15,13 +16,24 @@ function countDomNodes({replay}: {replay: null | ReplayReader}) { return replay?.getCountDomNodes(); } +function useCountDomNodes({replay}: {replay: null | ReplayReader}) { + return useQuery( + ['countDomNodes', replay], + () => + countDomNodes({ + replay, + }), + {enabled: Boolean(replay), cacheTime: Infinity} + ); +} + export default function MemoryPanel() { const {currentTime, isFetching, replay, setCurrentTime} = useReplayContext(); const [currentHoverTime, setCurrentHoverTime] = useCurrentHoverTime(); const memoryFrames = replay?.getMemoryFrames(); - const frameToCount = countDomNodes({replay}); + const {data: frameToCount} = useCountDomNodes({replay}); const domNodeData = useMemo( () => Array.from(frameToCount?.values() || []), [frameToCount] From ec15b19010134610ba71d9324df65c263103c29c Mon Sep 17 00:00:00 2001 From: Michelle Zhang <56095982+michellewzhang@users.noreply.github.com> Date: Thu, 18 Jul 2024 23:17:28 -0700 Subject: [PATCH 3/8] :recycle: consolidate & standardize hooks --- .../utils/replays/hooks/extractDomNodes.tsx | 5 ----- .../utils/replays/hooks/extractPageHtml.tsx | 8 -------- .../utils/replays/hooks/useCountDomNodes.tsx | 17 +++++++++++++++++ .../replays/hooks/useExtractedDomNodes.tsx | 5 ++++- .../replays/hooks/useExtractedPageHtml.tsx | 8 +++++++- .../views/replays/detail/memoryPanel/index.tsx | 18 +----------------- 6 files changed, 29 insertions(+), 32 deletions(-) delete mode 100644 static/app/utils/replays/hooks/extractDomNodes.tsx delete mode 100644 static/app/utils/replays/hooks/extractPageHtml.tsx create mode 100644 static/app/utils/replays/hooks/useCountDomNodes.tsx diff --git a/static/app/utils/replays/hooks/extractDomNodes.tsx b/static/app/utils/replays/hooks/extractDomNodes.tsx deleted file mode 100644 index 2ec8c3fd70cc8c..00000000000000 --- a/static/app/utils/replays/hooks/extractDomNodes.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import type ReplayReader from 'sentry/utils/replays/replayReader'; - -export default function extractDomNodes({replay}: {replay: null | ReplayReader}) { - return replay?.getExtractDomNodes(); -} diff --git a/static/app/utils/replays/hooks/extractPageHtml.tsx b/static/app/utils/replays/hooks/extractPageHtml.tsx deleted file mode 100644 index cac0e422e2af28..00000000000000 --- a/static/app/utils/replays/hooks/extractPageHtml.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import type ReplayReader from 'sentry/utils/replays/replayReader'; - -export default async function extractPageHtml({replay}: {replay: null | ReplayReader}) { - const results = await replay?.getExtractPageHtml(); - return Array.from(results?.entries() ?? []).map(([frame, html]) => { - return [frame.offsetMs, 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..cbe013cf5d35b2 --- /dev/null +++ b/static/app/utils/replays/hooks/useCountDomNodes.tsx @@ -0,0 +1,17 @@ +import {useQuery} from 'sentry/utils/queryClient'; +import type ReplayReader from 'sentry/utils/replays/replayReader'; + +function countDomNodes({replay}: {replay: null | ReplayReader}) { + return replay?.getCountDomNodes(); +} + +export default function useCountDomNodes({replay}: {replay: null | ReplayReader}) { + return useQuery( + ['countDomNodes', replay], + () => + countDomNodes({ + replay, + }), + {enabled: Boolean(replay), cacheTime: Infinity} + ); +} diff --git a/static/app/utils/replays/hooks/useExtractedDomNodes.tsx b/static/app/utils/replays/hooks/useExtractedDomNodes.tsx index 9fefa3c8a38355..bf94296314d2f9 100644 --- a/static/app/utils/replays/hooks/useExtractedDomNodes.tsx +++ b/static/app/utils/replays/hooks/useExtractedDomNodes.tsx @@ -1,7 +1,10 @@ import {useQuery} from 'sentry/utils/queryClient'; -import extractDomNodes from 'sentry/utils/replays/hooks/extractDomNodes'; import type ReplayReader from 'sentry/utils/replays/replayReader'; +function extractDomNodes({replay}: {replay: null | ReplayReader}) { + return replay?.getExtractDomNodes(); +} + export default function useExtractedDomNodes({replay}: {replay: null | ReplayReader}) { return useQuery( ['getDomNodes', replay], diff --git a/static/app/utils/replays/hooks/useExtractedPageHtml.tsx b/static/app/utils/replays/hooks/useExtractedPageHtml.tsx index 155895616010b0..f99b406f9ee4ac 100644 --- a/static/app/utils/replays/hooks/useExtractedPageHtml.tsx +++ b/static/app/utils/replays/hooks/useExtractedPageHtml.tsx @@ -1,5 +1,4 @@ import {useQuery} from 'sentry/utils/queryClient'; -import extractPageHtml from 'sentry/utils/replays/hooks/extractPageHtml'; import type ReplayReader from 'sentry/utils/replays/replayReader'; interface Props { @@ -7,6 +6,13 @@ interface Props { replay: ReplayReader | null; } +async function extractPageHtml({replay}: {replay: null | ReplayReader}) { + const results = await replay?.getExtractPageHtml(); + return Array.from(results?.entries() ?? []).map(([frame, html]) => { + return [frame.offsetMs, html]; + }); +} + export default function useExtractedPageHtml({replay}: Props) { return useQuery( ['extactPageHtml', replay], diff --git a/static/app/views/replays/detail/memoryPanel/index.tsx b/static/app/views/replays/detail/memoryPanel/index.tsx index a7cea94c9fc1d2..22cac41d0d6afe 100644 --- a/static/app/views/replays/detail/memoryPanel/index.tsx +++ b/static/app/views/replays/detail/memoryPanel/index.tsx @@ -6,27 +6,11 @@ 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 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 countDomNodes({replay}: {replay: null | ReplayReader}) { - return replay?.getCountDomNodes(); -} - -function useCountDomNodes({replay}: {replay: null | ReplayReader}) { - return useQuery( - ['countDomNodes', replay], - () => - countDomNodes({ - replay, - }), - {enabled: Boolean(replay), cacheTime: Infinity} - ); -} - export default function MemoryPanel() { const {currentTime, isFetching, replay, setCurrentTime} = useReplayContext(); const [currentHoverTime, setCurrentHoverTime] = useCurrentHoverTime(); From f3154c6e5b0e26889e5d6b183bb728bfec7b44fe Mon Sep 17 00:00:00 2001 From: Michelle Zhang <56095982+michellewzhang@users.noreply.github.com> Date: Thu, 18 Jul 2024 23:34:06 -0700 Subject: [PATCH 4/8] :truck: rename files --- static/app/components/replays/diff/replayTextDiff.tsx | 4 ++-- .../{useExtractedDomNodes.tsx => useExtractDomNodes.tsx} | 0 .../{useExtractedPageHtml.tsx => useExtractPageHtml.tsx} | 2 +- static/app/utils/replays/replayerStepper.tsx | 2 +- static/app/views/replays/detail/breadcrumbs/index.tsx | 9 +++++---- 5 files changed, 9 insertions(+), 8 deletions(-) rename static/app/utils/replays/hooks/{useExtractedDomNodes.tsx => useExtractDomNodes.tsx} (100%) rename static/app/utils/replays/hooks/{useExtractedPageHtml.tsx => useExtractPageHtml.tsx} (90%) diff --git a/static/app/components/replays/diff/replayTextDiff.tsx b/static/app/components/replays/diff/replayTextDiff.tsx index bfd7979376a403..5f42587069fba4 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 { @@ -18,7 +18,7 @@ interface Props { export function ReplayTextDiff({replay}: Props) { const {data} = - useExtractedPageHtml({ + useExtractPageHtml({ replay, // offsetMsToStopAt: [leftOffsetMs, rightOffsetMs], }) ?? new Map(); diff --git a/static/app/utils/replays/hooks/useExtractedDomNodes.tsx b/static/app/utils/replays/hooks/useExtractDomNodes.tsx similarity index 100% rename from static/app/utils/replays/hooks/useExtractedDomNodes.tsx rename to static/app/utils/replays/hooks/useExtractDomNodes.tsx diff --git a/static/app/utils/replays/hooks/useExtractedPageHtml.tsx b/static/app/utils/replays/hooks/useExtractPageHtml.tsx similarity index 90% rename from static/app/utils/replays/hooks/useExtractedPageHtml.tsx rename to static/app/utils/replays/hooks/useExtractPageHtml.tsx index f99b406f9ee4ac..3f75e92a095268 100644 --- a/static/app/utils/replays/hooks/useExtractedPageHtml.tsx +++ b/static/app/utils/replays/hooks/useExtractPageHtml.tsx @@ -13,7 +13,7 @@ async function extractPageHtml({replay}: {replay: null | ReplayReader}) { }); } -export default function useExtractedPageHtml({replay}: Props) { +export default function useExtractPageHtml({replay}: Props) { return useQuery( ['extactPageHtml', replay], () => diff --git a/static/app/utils/replays/replayerStepper.tsx b/static/app/utils/replays/replayerStepper.tsx index f33572313ad9f5..98236c4407aa1d 100644 --- a/static/app/utils/replays/replayerStepper.tsx +++ b/static/app/utils/replays/replayerStepper.tsx @@ -47,7 +47,7 @@ export default function replayerStepper< return new Promise(resolve => { if (!frames?.length || !rrwebEvents?.length) { - resolve({result: new Map()}); + resolve({}); return; } diff --git a/static/app/views/replays/detail/breadcrumbs/index.tsx b/static/app/views/replays/detail/breadcrumbs/index.tsx index e1bbf19bfb9dc1..280eaaf617a975 100644 --- a/static/app/views/replays/detail/breadcrumbs/index.tsx +++ b/static/app/views/replays/detail/breadcrumbs/index.tsx @@ -9,7 +9,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'; @@ -32,10 +32,11 @@ function Breadcrumbs() { const {currentTime, replay} = useReplayContext(); const {onClickTimestamp} = useCrumbHandlers(); - const {data: frameToExtraction, isFetching: isFetchingExtractions} = - useExtractedDomNodes({ + const {data: frameToExtraction, isFetching: isFetchingExtractions} = useExtractDomNodes( + { replay, - }); + } + ); const startTimestampMs = replay?.getStartTimestampMs() ?? 0; const frames = replay?.getChapterFrames(); From af73b5ade6e5d0898091e1f1a043233548b79776 Mon Sep 17 00:00:00 2001 From: Michelle Zhang <56095982+michellewzhang@users.noreply.github.com> Date: Thu, 18 Jul 2024 23:49:12 -0700 Subject: [PATCH 5/8] :recycle: some types --- .../components/replays/breadcrumbs/breadcrumbItem.tsx | 2 +- static/app/utils/replays/hooks/useCountDomNodes.tsx | 9 +++++++++ .../views/replays/detail/breadcrumbs/breadcrumbRow.tsx | 2 +- .../views/replays/detail/memoryPanel/domNodesChart.tsx | 2 +- 4 files changed, 12 insertions(+), 3 deletions(-) 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/utils/replays/hooks/useCountDomNodes.tsx b/static/app/utils/replays/hooks/useCountDomNodes.tsx index cbe013cf5d35b2..ee0b12324ff9cf 100644 --- a/static/app/utils/replays/hooks/useCountDomNodes.tsx +++ b/static/app/utils/replays/hooks/useCountDomNodes.tsx @@ -1,6 +1,15 @@ import {useQuery} from 'sentry/utils/queryClient'; import type ReplayReader from 'sentry/utils/replays/replayReader'; +export type DomNodeChartDatapoint = { + added: number; + count: number; + endTimestampMs: number; + removed: number; + startTimestampMs: number; + timestampMs: number; +}; + function countDomNodes({replay}: {replay: null | ReplayReader}) { return replay?.getCountDomNodes(); } 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/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'> { From 873e5c99ea6028641fa44e404673b8db3fb1cb19 Mon Sep 17 00:00:00 2001 From: Michelle Zhang <56095982+michellewzhang@users.noreply.github.com> Date: Thu, 18 Jul 2024 23:57:22 -0700 Subject: [PATCH 6/8] :label: types --- static/app/utils/replays/hooks/useCountDomNodes.tsx | 9 +++++++-- .../app/utils/replays/hooks/useExtractDomNodes.tsx | 12 +++++++++--- .../app/views/replays/detail/breadcrumbs/index.tsx | 3 +-- 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/static/app/utils/replays/hooks/useCountDomNodes.tsx b/static/app/utils/replays/hooks/useCountDomNodes.tsx index ee0b12324ff9cf..033114f62900bb 100644 --- a/static/app/utils/replays/hooks/useCountDomNodes.tsx +++ b/static/app/utils/replays/hooks/useCountDomNodes.tsx @@ -1,5 +1,6 @@ import {useQuery} 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; @@ -10,8 +11,12 @@ export type DomNodeChartDatapoint = { timestampMs: number; }; -function countDomNodes({replay}: {replay: null | ReplayReader}) { - return replay?.getCountDomNodes(); +function countDomNodes({ + replay, +}: { + replay: null | ReplayReader; +}): Promise> { + return replay?.getCountDomNodes() ?? Promise.resolve(new Map()); } export default function useCountDomNodes({replay}: {replay: null | ReplayReader}) { diff --git a/static/app/utils/replays/hooks/useExtractDomNodes.tsx b/static/app/utils/replays/hooks/useExtractDomNodes.tsx index bf94296314d2f9..ce90c8813ff900 100644 --- a/static/app/utils/replays/hooks/useExtractDomNodes.tsx +++ b/static/app/utils/replays/hooks/useExtractDomNodes.tsx @@ -1,11 +1,17 @@ import {useQuery} 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'; -function extractDomNodes({replay}: {replay: null | ReplayReader}) { - return replay?.getExtractDomNodes(); +function extractDomNodes({ + replay, +}: { + replay: null | ReplayReader; +}): Promise> { + return replay?.getExtractDomNodes() ?? Promise.resolve(new Map()); } -export default function useExtractedDomNodes({replay}: {replay: null | ReplayReader}) { +export default function useExtractDomNodes({replay}: {replay: null | ReplayReader}) { return useQuery( ['getDomNodes', replay], () => diff --git a/static/app/views/replays/detail/breadcrumbs/index.tsx b/static/app/views/replays/detail/breadcrumbs/index.tsx index 280eaaf617a975..4fdf4fcdb6c700 100644 --- a/static/app/views/replays/detail/breadcrumbs/index.tsx +++ b/static/app/views/replays/detail/breadcrumbs/index.tsx @@ -1,7 +1,6 @@ import {useEffect, useMemo, useRef, useState} from 'react'; import type {ListRowProps} from 'react-virtualized'; import {AutoSizer, CellMeasurer, List as ReactVirtualizedList} from 'react-virtualized'; -import type {eventWithTime} from '@sentry-internal/rrweb'; import Placeholder from 'sentry/components/placeholder'; import JumpButtons from 'sentry/components/replays/jumpButtons'; @@ -99,7 +98,7 @@ function Breadcrumbs() { Date: Fri, 19 Jul 2024 11:31:37 -0700 Subject: [PATCH 7/8] :recycle: ref so it works --- .../replays/diff/replayTextDiff.tsx | 11 ++- .../utils/replays/hooks/useCountDomNodes.tsx | 16 +---- .../replays/hooks/useExtractPageHtml.tsx | 67 ++++++++++++++++--- static/app/utils/replays/replayReader.tsx | 61 +++++++++-------- static/app/utils/replays/replayerStepper.tsx | 48 ++++++++----- .../replays/detail/memoryPanel/index.tsx | 6 +- 6 files changed, 129 insertions(+), 80 deletions(-) diff --git a/static/app/components/replays/diff/replayTextDiff.tsx b/static/app/components/replays/diff/replayTextDiff.tsx index 5f42587069fba4..2bafcd5458ef40 100644 --- a/static/app/components/replays/diff/replayTextDiff.tsx +++ b/static/app/components/replays/diff/replayTextDiff.tsx @@ -16,12 +16,11 @@ interface Props { rightOffsetMs: number; } -export function ReplayTextDiff({replay}: Props) { - const {data} = - useExtractPageHtml({ - replay, - // offsetMsToStopAt: [leftOffsetMs, rightOffsetMs], - }) ?? new Map(); +export function ReplayTextDiff({replay, leftOffsetMs, rightOffsetMs}: Props) { + const {data} = useExtractPageHtml({ + replay, + offsetMsToStopAt: [leftOffsetMs, rightOffsetMs], + }); const [leftBody, rightBody] = useMemo( () => data?.map(([_, html]) => beautify.html(html, {indent_size: 2})) ?? [], diff --git a/static/app/utils/replays/hooks/useCountDomNodes.tsx b/static/app/utils/replays/hooks/useCountDomNodes.tsx index 033114f62900bb..1bcb8ed379b081 100644 --- a/static/app/utils/replays/hooks/useCountDomNodes.tsx +++ b/static/app/utils/replays/hooks/useCountDomNodes.tsx @@ -1,6 +1,5 @@ import {useQuery} 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; @@ -11,21 +10,12 @@ export type DomNodeChartDatapoint = { timestampMs: number; }; -function countDomNodes({ - replay, -}: { - replay: null | ReplayReader; -}): Promise> { - return replay?.getCountDomNodes() ?? Promise.resolve(new Map()); -} - export default function useCountDomNodes({replay}: {replay: null | ReplayReader}) { return useQuery( ['countDomNodes', replay], - () => - countDomNodes({ - replay, - }), + () => { + return replay?.getCountDomNodes(); + }, {enabled: Boolean(replay), cacheTime: Infinity} ); } diff --git a/static/app/utils/replays/hooks/useExtractPageHtml.tsx b/static/app/utils/replays/hooks/useExtractPageHtml.tsx index 3f75e92a095268..db83985d0c1cca 100644 --- a/static/app/utils/replays/hooks/useExtractPageHtml.tsx +++ b/static/app/utils/replays/hooks/useExtractPageHtml.tsx @@ -1,24 +1,71 @@ 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'; -interface Props { - // offsetMsToStopAt: number[]; - replay: ReplayReader | null; -} +type Args = { + /** + * Offsets where we should stop and take a snapshot of the rendered HTML + */ + offsetMsToStopAt: number[]; + + /** + * The rrweb events that constitute the replay + */ + rrwebEvents: RecordingFrame[] | undefined; + + /** + * The replay startTimestampMs + */ + startTimestampMs: number; +}; -async function extractPageHtml({replay}: {replay: null | ReplayReader}) { - const results = await replay?.getExtractPageHtml(); - return Array.from(results?.entries() ?? []).map(([frame, html]) => { +async function extractPageHtml({ + offsetMsToStopAt, + rrwebEvents, + startTimestampMs, +}: Args): Promise<[number, string][]> { + const frames: ReplayFrame[] = offsetMsToStopAt.map(offsetMs => ({ + offsetMs, + timestamp: new Date(startTimestampMs + offsetMs), + timestampMs: startTimestampMs + offsetMs, + })) as ReplayFrame[]; // TODO Don't smash types into `as ReplayFrame[]`, instead make the object really conform + const results = await replayerStepper({ + frames, + rrwebEvents, + startTimestampMs, + visitFrameCallbacks: { + extractPageHtml: { + shouldVisitFrame: () => { + // Visit all the timestamps (converted to frames) that were passed in above + return true; + }, + onVisitFrame: (frame, collection, replayer) => { + const doc = replayer.getMirror().getNode(1); + const html = (doc as Document)?.body.outerHTML ?? ''; + collection.set(frame, html); + }, + }, + }, + }); + return Array.from(results.extractPageHtml.entries()).map(([frame, html]) => { return [frame.offsetMs, html]; }); } -export default function useExtractPageHtml({replay}: Props) { +interface Props { + offsetMsToStopAt: number[]; + replay: ReplayReader | null; +} + +export default function useExtractedPageHtml({replay, offsetMsToStopAt}: Props) { return useQuery( - ['extactPageHtml', replay], + ['extactPageHtml', replay, offsetMsToStopAt], () => extractPageHtml({ - replay, + 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 26a64d8da1b45e..d9c66a9ef6fe66 100644 --- a/static/app/utils/replays/replayReader.tsx +++ b/static/app/utils/replays/replayReader.tsx @@ -143,18 +143,6 @@ function removeDuplicateNavCrumbs( return otherBreadcrumbFrames.concat(uniqueNavCrumbs); } -const extractPageHtml = { - shouldVisitFrame: () => { - // Visit all the timestamps (converted to frames) that are passed in - return true; - }, - onVisitFrame: (frame, collection, replayer) => { - const doc = replayer.getMirror().getNode(1); - const html = (doc as Document)?.body.outerHTML ?? ''; - collection.set(frame, html); - }, -}; - const extractDomNodes = { shouldVisitFrame: frame => { const nodeId = getNodeId(frame); @@ -172,7 +160,7 @@ const extractDomNodes = { }, }; -const countDomNodes = function () { +const countDomNodes = function (frames) { let frameCount = 0; const length = frames?.length ?? 0; const frameStep = Math.max(Math.round(length * 0.007), 1); @@ -354,6 +342,33 @@ export default class ReplayReader { private _startOffsetMs = 0; private _videoEvents: VideoEvent[] = []; private _clipWindow: ClipWindow | undefined = undefined; + private _collections: any = undefined; + private _allFrames: any[] = []; + + private _getCollections = () => { + if (this._collections) { + return this._collections; + } + + if (this.getRRWebFrames().length > 2) { + this._allFrames = (this.getRRWebMutations() as (RecordingFrame | ReplayFrame)[]) + .concat(this.getDOMFrames()) + .sort(sortFrames); + + this._collections = replayerStepper({ + frames: this._allFrames, + rrwebEvents: this.getRRWebFrames(), + startTimestampMs: this.getReplay().started_at.getTime() ?? 0, + visitFrameCallbacks: { + extractDomNodes, + countDomNodes: countDomNodes(this.getRRWebMutations()), + }, + }); + + return this._collections; + } + return this._collections; + }; private _applyClipWindow = (clipWindow: ClipWindow) => { const clipStartTimestampMs = clamp( @@ -478,20 +493,15 @@ export default class ReplayReader { }; getCountDomNodes = async () => { - const results = await this._collections; + const results = await this._getCollections(); return results.countDomNodes; }; getExtractDomNodes = async () => { - const results = await this._collections; + const results = await this._getCollections(); return results.extractDomNodes; }; - getExtractPageHtml = async () => { - const results = await this._collections; - return results.extractPageHtml; - }; - getClipWindow = () => this._clipWindow; /** @@ -645,17 +655,6 @@ export default class ReplayReader { getSDKOptions = () => this._optionFrame; - private _collections = replayerStepper({ - frames: this.getRRWebMutations(), - rrwebEvents: this.getRRWebFrames(), - startTimestampMs: this.getReplay().started_at.getTime() ?? 0, - visitFrameCallbacks: { - extractDomNodes, - extractPageHtml, - countDomNodes: countDomNodes(), - }, - }); - /** * Checks the replay to see if user has any canvas elements in their * application. Needed to inform them that we now support canvas in replays. diff --git a/static/app/utils/replays/replayerStepper.tsx b/static/app/utils/replays/replayerStepper.tsx index 98236c4407aa1d..29bea6547e5144 100644 --- a/static/app/utils/replays/replayerStepper.tsx +++ b/static/app/utils/replays/replayerStepper.tsx @@ -28,7 +28,7 @@ interface Args { } type FrameRef = { - frame: Frame | undefined; + current: Frame | undefined; }; export default function replayerStepper< @@ -75,30 +75,42 @@ export default function replayerStepper< }; const frameRef: FrameRef = { - frame: undefined, + current: undefined, + }; + + const activeCallbacks: { + current: Record>; + } = { + current: {}, }; const considerFrame = (frame: Frame) => { - Object.values(visitFrameCallbacks).forEach(v => { - if (v.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(); - } - }); + activeCallbacks.current = Object.fromEntries( + Object.entries(visitFrameCallbacks).filter(([_, v]) => { + return v.shouldVisitFrame(frame, replayer); + }) + ); + // console.log(activeCallbacks.current); + + if (Object.values(activeCallbacks.current).length) { + frameRef.current = frame; + window.setTimeout(() => { + const timestamp = + 'offsetMs' in frame ? frame.offsetMs : frame.timestamp - startTimestampMs; + replayer.pause(timestamp); + }, 0); + } else { + frameRef.current = undefined; + nextOrDone(); + } }; const handlePause = () => { - Object.entries(visitFrameCallbacks).forEach(([k, v]) => { - v.onVisitFrame(frameRef.frame!, collection[k], replayer); - nextOrDone(); + // console.log(collection); + Object.entries(activeCallbacks.current).forEach(([k, v]) => { + v.onVisitFrame(frameRef.current!, collection[k], replayer); }); + nextOrDone(); }; replayer.on('pause', handlePause); diff --git a/static/app/views/replays/detail/memoryPanel/index.tsx b/static/app/views/replays/detail/memoryPanel/index.tsx index 22cac41d0d6afe..75e2ba83e074e8 100644 --- a/static/app/views/replays/detail/memoryPanel/index.tsx +++ b/static/app/views/replays/detail/memoryPanel/index.tsx @@ -6,7 +6,9 @@ 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 useCountDomNodes from 'sentry/utils/replays/hooks/useCountDomNodes'; +import useCountDomNodes, { + type DomNodeChartDatapoint, +} from 'sentry/utils/replays/hooks/useCountDomNodes'; import useCurrentHoverTime from 'sentry/utils/replays/playback/providers/useCurrentHoverTime'; import DomNodesChart from 'sentry/views/replays/detail/memoryPanel/domNodesChart'; import MemoryChart from 'sentry/views/replays/detail/memoryPanel/memoryChart'; @@ -59,7 +61,7 @@ export default function MemoryPanel() { currentHoverTime={currentHoverTime} currentTime={currentTime} durationMs={replay.getDurationMs()} - datapoints={domNodeData} + datapoints={domNodeData as DomNodeChartDatapoint[]} setCurrentHoverTime={setCurrentHoverTime} setCurrentTime={setCurrentTime} startTimestampMs={replay.getStartTimestampMs()} From 70c90544f39dcc3d87dea8c85c0a7669c9585403 Mon Sep 17 00:00:00 2001 From: Michelle Zhang <56095982+michellewzhang@users.noreply.github.com> Date: Fri, 19 Jul 2024 11:44:34 -0700 Subject: [PATCH 8/8] :recycle: clean up --- .../utils/replays/hooks/useExtractDomNodes.tsx | 17 +++-------------- static/app/utils/replays/replayReader.tsx | 13 ++++++------- static/app/utils/replays/replayerStepper.tsx | 6 ++---- 3 files changed, 11 insertions(+), 25 deletions(-) diff --git a/static/app/utils/replays/hooks/useExtractDomNodes.tsx b/static/app/utils/replays/hooks/useExtractDomNodes.tsx index ce90c8813ff900..7d4b9258ffb7ff 100644 --- a/static/app/utils/replays/hooks/useExtractDomNodes.tsx +++ b/static/app/utils/replays/hooks/useExtractDomNodes.tsx @@ -1,23 +1,12 @@ import {useQuery} 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'; - -function extractDomNodes({ - replay, -}: { - replay: null | ReplayReader; -}): Promise> { - return replay?.getExtractDomNodes() ?? Promise.resolve(new Map()); -} export default function useExtractDomNodes({replay}: {replay: null | ReplayReader}) { return useQuery( ['getDomNodes', replay], - () => - extractDomNodes({ - replay, - }), + () => { + return replay?.getExtractDomNodes(); + }, {enabled: Boolean(replay), cacheTime: Infinity} ); } diff --git a/static/app/utils/replays/replayReader.tsx b/static/app/utils/replays/replayReader.tsx index d9c66a9ef6fe66..7ee04443d8c94e 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'; @@ -160,7 +161,7 @@ const extractDomNodes = { }, }; -const countDomNodes = function (frames) { +const countDomNodes = function (frames: eventWithTime[]) { let frameCount = 0; const length = frames?.length ?? 0; const frameStep = Math.max(Math.round(length * 0.007), 1); @@ -342,8 +343,8 @@ export default class ReplayReader { private _startOffsetMs = 0; private _videoEvents: VideoEvent[] = []; private _clipWindow: ClipWindow | undefined = undefined; - private _collections: any = undefined; - private _allFrames: any[] = []; + private _collections: Record | undefined = undefined; + private _allFrames: (RecordingFrame | ReplayFrame)[] = []; private _getCollections = () => { if (this._collections) { @@ -364,8 +365,6 @@ export default class ReplayReader { countDomNodes: countDomNodes(this.getRRWebMutations()), }, }); - - return this._collections; } return this._collections; }; @@ -494,12 +493,12 @@ export default class ReplayReader { getCountDomNodes = async () => { const results = await this._getCollections(); - return results.countDomNodes; + return results?.countDomNodes; }; getExtractDomNodes = async () => { const results = await this._getCollections(); - return results.extractDomNodes; + return results?.extractDomNodes; }; getClipWindow = () => this._clipWindow; diff --git a/static/app/utils/replays/replayerStepper.tsx b/static/app/utils/replays/replayerStepper.tsx index 29bea6547e5144..e4b8b208affab4 100644 --- a/static/app/utils/replays/replayerStepper.tsx +++ b/static/app/utils/replays/replayerStepper.tsx @@ -47,7 +47,7 @@ export default function replayerStepper< return new Promise(resolve => { if (!frames?.length || !rrwebEvents?.length) { - resolve({}); + resolve(collection); return; } @@ -86,11 +86,10 @@ export default function replayerStepper< const considerFrame = (frame: Frame) => { activeCallbacks.current = Object.fromEntries( - Object.entries(visitFrameCallbacks).filter(([_, v]) => { + Object.entries(visitFrameCallbacks).filter(([, v]) => { return v.shouldVisitFrame(frame, replayer); }) ); - // console.log(activeCallbacks.current); if (Object.values(activeCallbacks.current).length) { frameRef.current = frame; @@ -106,7 +105,6 @@ export default function replayerStepper< }; const handlePause = () => { - // console.log(collection); Object.entries(activeCallbacks.current).forEach(([k, v]) => { v.onVisitFrame(frameRef.current!, collection[k], replayer); });