import {Replayer} from '@sentry-internal/rrweb'; import type {RecordingFrame, ReplayFrame} from 'sentry/utils/replays/types'; interface Args { frames: Frame[] | undefined; onVisitFrame: ( frame: Frame, collection: Map, replayer: Replayer ) => void; rrwebEvents: RecordingFrame[] | undefined; shouldVisitFrame: (frame: Frame, replayer: Replayer) => boolean; startTimestampMs: number; } type FrameRef = { frame: Frame | undefined; }; export default function replayerStepper< Frame extends ReplayFrame | RecordingFrame, CollectionData, >({ frames, onVisitFrame, rrwebEvents, shouldVisitFrame, startTimestampMs, }: Args): Promise> { const collection = new Map(); return new Promise(resolve => { if (!frames?.length || !rrwebEvents?.length) { resolve(new Map()); return; } const replayer = createHiddenPlayer(rrwebEvents); const nextFrame = (function () { let i = 0; return () => frames[i++]; })(); const onDone = () => { resolve(collection); }; const nextOrDone = () => { const next = nextFrame(); if (next) { considerFrame(next); } else { onDone(); } }; const frameRef: FrameRef = { frame: undefined, }; const considerFrame = (frame: Frame) => { if (shouldVisitFrame(frame, replayer)) { frameRef.frame = frame; window.setTimeout(() => { const timestamp = 'offsetMs' in frame ? frame.offsetMs : frame.timestamp - startTimestampMs; replayer.pause(timestamp); }, 0); } else { frameRef.frame = undefined; nextOrDone(); } }; const handlePause = () => { onVisitFrame(frameRef.frame!, collection, replayer); nextOrDone(); }; replayer.on('pause', handlePause); considerFrame(nextFrame()); }); } function createHiddenPlayer(rrwebEvents: RecordingFrame[]): Replayer { const domRoot = document.createElement('div'); domRoot.className = 'sentry-block'; const {style} = domRoot; style.position = 'fixed'; style.inset = '0'; style.width = '0'; style.height = '0'; style.overflow = 'hidden'; document.body.appendChild(domRoot); return new Replayer(rrwebEvents, { root: domRoot, loadTimeout: 1, showWarning: false, blockClass: 'sentry-block', speed: 99999, skipInactive: true, triggerFocus: false, mouseTail: false, }); }