diff --git a/packages/rrweb/src/record/observers/canvas/canvas-manager.ts b/packages/rrweb/src/record/observers/canvas/canvas-manager.ts index f3290edcfe..6b09f56308 100644 --- a/packages/rrweb/src/record/observers/canvas/canvas-manager.ts +++ b/packages/rrweb/src/record/observers/canvas/canvas-manager.ts @@ -14,7 +14,7 @@ import type { CanvasArg, ImageBitmapDataURLWorkerResponse, } from '@sentry-internal/rrweb-types'; -import { isBlocked } from '../../../utils'; +import { isBlocked, onRequestAnimationFrame } from '../../../utils'; import { CanvasContext } from '@sentry-internal/rrweb-types'; import initCanvas2DMutationObserver from './2d'; import initCanvasContextObserver from './canvas'; @@ -227,7 +227,7 @@ export class CanvasManager implements CanvasManagerInterface { lastSnapshotTime && timestamp - lastSnapshotTime < timeBetweenSnapshots ) { - rafId = requestAnimationFrame(takeCanvasSnapshots); + rafId = onRequestAnimationFrame(takeCanvasSnapshots); return; } lastSnapshotTime = timestamp; @@ -278,10 +278,10 @@ export class CanvasManager implements CanvasManagerInterface { })(); }); }); - rafId = requestAnimationFrame(takeCanvasSnapshots); + rafId = onRequestAnimationFrame(takeCanvasSnapshots); }; - rafId = requestAnimationFrame(takeCanvasSnapshots); + rafId = onRequestAnimationFrame(takeCanvasSnapshots); this.resetObservers = () => { canvasContextReset(); @@ -330,15 +330,15 @@ export class CanvasManager implements CanvasManagerInterface { } private startPendingCanvasMutationFlusher() { - requestAnimationFrame(() => this.flushPendingCanvasMutations()); + onRequestAnimationFrame(() => this.flushPendingCanvasMutations()); } private startRAFTimestamping() { const setLatestRAFTimestamp = (timestamp: DOMHighResTimeStamp) => { this.rafStamps.latestId = timestamp; - requestAnimationFrame(setLatestRAFTimestamp); + onRequestAnimationFrame(setLatestRAFTimestamp); }; - requestAnimationFrame(setLatestRAFTimestamp); + onRequestAnimationFrame(setLatestRAFTimestamp); } flushPendingCanvasMutations() { @@ -348,7 +348,7 @@ export class CanvasManager implements CanvasManagerInterface { this.flushPendingCanvasMutationFor(canvas, id); }, ); - requestAnimationFrame(() => this.flushPendingCanvasMutations()); + onRequestAnimationFrame(() => this.flushPendingCanvasMutations()); } flushPendingCanvasMutationFor(canvas: HTMLCanvasElement, id: number) { diff --git a/packages/rrweb/src/record/processed-node-manager.ts b/packages/rrweb/src/record/processed-node-manager.ts index b5d6c4b679..89e63ea881 100644 --- a/packages/rrweb/src/record/processed-node-manager.ts +++ b/packages/rrweb/src/record/processed-node-manager.ts @@ -1,3 +1,4 @@ +import { onRequestAnimationFrame } from '../utils'; import type MutationBuffer from './mutation'; /** @@ -13,7 +14,7 @@ export default class ProcessedNodeManager { } private periodicallyClear() { - requestAnimationFrame(() => { + onRequestAnimationFrame(() => { this.clear(); if (this.loop) this.periodicallyClear(); }); diff --git a/packages/rrweb/src/replay/timer.ts b/packages/rrweb/src/replay/timer.ts index 325768e0a8..621c06de68 100644 --- a/packages/rrweb/src/replay/timer.ts +++ b/packages/rrweb/src/replay/timer.ts @@ -4,6 +4,7 @@ import { EventType, IncrementalSource, } from '@sentry-internal/rrweb-types'; +import { onRequestAnimationFrame } from '../utils'; export class Timer { public timeOffset = 0; @@ -39,14 +40,14 @@ export class Timer { this.actions.splice(index, 0, action); } if (rafWasActive) { - this.raf = requestAnimationFrame(this.rafCheck.bind(this)); + this.raf = onRequestAnimationFrame(this.rafCheck.bind(this)); } } public start() { this.timeOffset = 0; this.lastTimestamp = performance.now(); - this.raf = requestAnimationFrame(this.rafCheck.bind(this)); + this.raf = onRequestAnimationFrame(this.rafCheck.bind(this)); } private rafCheck() { @@ -64,7 +65,7 @@ export class Timer { } } if (this.actions.length > 0) { - this.raf = requestAnimationFrame(this.rafCheck.bind(this)); + this.raf = onRequestAnimationFrame(this.rafCheck.bind(this)); } else { this.raf = true; // was active } diff --git a/packages/rrweb/src/utils.ts b/packages/rrweb/src/utils.ts index 4dffcf4a63..eba0847128 100644 --- a/packages/rrweb/src/utils.ts +++ b/packages/rrweb/src/utils.ts @@ -589,3 +589,46 @@ export function inDom(n: Node): boolean { if (!doc) return false; return doc.contains(n) || shadowHostInDom(n); } + +let cachedRequestAnimationFrameImplementation: + | undefined + | typeof requestAnimationFrame; + +/** + * We generally want to use window.requestAnimationFrame. + * However, in some cases this may be wrapped (e.g. by Zone.js for Angular), + * so we try to get an unpatched version of this from a sandboxed iframe. + */ +function getRequestAnimationFrameImplementation(): typeof requestAnimationFrame { + if (cachedRequestAnimationFrameImplementation) { + return cachedRequestAnimationFrameImplementation; + } + + const document = window.document; + let requestAnimationFrameImplementation = window.requestAnimationFrame; + if (document && typeof document.createElement === 'function') { + try { + const sandbox = document.createElement('iframe'); + sandbox.hidden = true; + document.head.appendChild(sandbox); + const contentWindow = sandbox.contentWindow; + if (contentWindow && contentWindow.requestAnimationFrame) { + requestAnimationFrameImplementation = + // eslint-disable-next-line @typescript-eslint/unbound-method + contentWindow.requestAnimationFrame; + } + document.head.removeChild(sandbox); + } catch (e) { + // Could not create sandbox iframe, just use window.requestAnimationFrame + } + } + + return (cachedRequestAnimationFrameImplementation = + requestAnimationFrameImplementation.bind(window)); +} + +export function onRequestAnimationFrame( + ...rest: Parameters +): ReturnType { + return getRequestAnimationFrameImplementation()(...rest); +}