Skip to content

Commit e8ff315

Browse files
mydeabillyvg
authored andcommitted
fix(rrweb): Use unpatched requestAnimationFrame when possible (#150)
As explained here: getsentry/sentry-javascript#6946 (comment) the usage of `requestAnimationFrame` can lead to issues when working with Zone.js. With this fix, where possible we should now use the `requestAnimationFrame` implementation from an iframe, if possible, and else fall back on just using `window.requestAnimationFrame` as before. We already do something similar in sentry-javascript to get an unpatched `fetch`: https://github.com/getsentry/sentry-javascript/blob/23ef22b115c8868861896cc9003bd4bb6afb0690/packages/browser/src/transports/utils.ts#L65-L71 so this should hopefully work fine!
1 parent 3d2edd3 commit e8ff315

File tree

4 files changed

+57
-12
lines changed

4 files changed

+57
-12
lines changed

packages/rrweb/src/record/observers/canvas/canvas-manager.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import type {
1414
CanvasArg,
1515
ImageBitmapDataURLWorkerResponse,
1616
} from '@sentry-internal/rrweb-types';
17-
import { isBlocked } from '../../../utils';
17+
import { isBlocked, onRequestAnimationFrame } from '../../../utils';
1818
import { CanvasContext } from '@sentry-internal/rrweb-types';
1919
import initCanvas2DMutationObserver from './2d';
2020
import initCanvasContextObserver from './canvas';
@@ -227,7 +227,7 @@ export class CanvasManager implements CanvasManagerInterface {
227227
lastSnapshotTime &&
228228
timestamp - lastSnapshotTime < timeBetweenSnapshots
229229
) {
230-
rafId = requestAnimationFrame(takeCanvasSnapshots);
230+
rafId = onRequestAnimationFrame(takeCanvasSnapshots);
231231
return;
232232
}
233233
lastSnapshotTime = timestamp;
@@ -284,10 +284,10 @@ export class CanvasManager implements CanvasManagerInterface {
284284
})();
285285
});
286286
});
287-
rafId = requestAnimationFrame(takeCanvasSnapshots);
287+
rafId = onRequestAnimationFrame(takeCanvasSnapshots);
288288
};
289289

290-
rafId = requestAnimationFrame(takeCanvasSnapshots);
290+
rafId = onRequestAnimationFrame(takeCanvasSnapshots);
291291

292292
this.resetObservers = () => {
293293
canvasContextReset();
@@ -336,15 +336,15 @@ export class CanvasManager implements CanvasManagerInterface {
336336
}
337337

338338
private startPendingCanvasMutationFlusher() {
339-
requestAnimationFrame(() => this.flushPendingCanvasMutations());
339+
onRequestAnimationFrame(() => this.flushPendingCanvasMutations());
340340
}
341341

342342
private startRAFTimestamping() {
343343
const setLatestRAFTimestamp = (timestamp: DOMHighResTimeStamp) => {
344344
this.rafStamps.latestId = timestamp;
345-
requestAnimationFrame(setLatestRAFTimestamp);
345+
onRequestAnimationFrame(setLatestRAFTimestamp);
346346
};
347-
requestAnimationFrame(setLatestRAFTimestamp);
347+
onRequestAnimationFrame(setLatestRAFTimestamp);
348348
}
349349

350350
flushPendingCanvasMutations() {
@@ -354,7 +354,7 @@ export class CanvasManager implements CanvasManagerInterface {
354354
this.flushPendingCanvasMutationFor(canvas, id);
355355
},
356356
);
357-
requestAnimationFrame(() => this.flushPendingCanvasMutations());
357+
onRequestAnimationFrame(() => this.flushPendingCanvasMutations());
358358
}
359359

360360
flushPendingCanvasMutationFor(canvas: HTMLCanvasElement, id: number) {

packages/rrweb/src/record/processed-node-manager.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { onRequestAnimationFrame } from '../utils';
12
import type MutationBuffer from './mutation';
23

34
/**
@@ -13,7 +14,7 @@ export default class ProcessedNodeManager {
1314
}
1415

1516
private periodicallyClear() {
16-
requestAnimationFrame(() => {
17+
onRequestAnimationFrame(() => {
1718
this.clear();
1819
if (this.loop) this.periodicallyClear();
1920
});

packages/rrweb/src/replay/timer.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
EventType,
55
IncrementalSource,
66
} from '@sentry-internal/rrweb-types';
7+
import { onRequestAnimationFrame } from '../utils';
78

89
export class Timer {
910
public timeOffset = 0;
@@ -39,14 +40,14 @@ export class Timer {
3940
this.actions.splice(index, 0, action);
4041
}
4142
if (rafWasActive) {
42-
this.raf = requestAnimationFrame(this.rafCheck.bind(this));
43+
this.raf = onRequestAnimationFrame(this.rafCheck.bind(this));
4344
}
4445
}
4546

4647
public start() {
4748
this.timeOffset = 0;
4849
this.lastTimestamp = performance.now();
49-
this.raf = requestAnimationFrame(this.rafCheck.bind(this));
50+
this.raf = onRequestAnimationFrame(this.rafCheck.bind(this));
5051
}
5152

5253
private rafCheck() {
@@ -64,7 +65,7 @@ export class Timer {
6465
}
6566
}
6667
if (this.actions.length > 0) {
67-
this.raf = requestAnimationFrame(this.rafCheck.bind(this));
68+
this.raf = onRequestAnimationFrame(this.rafCheck.bind(this));
6869
} else {
6970
this.raf = true; // was active
7071
}

packages/rrweb/src/utils.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -606,3 +606,46 @@ export function inDom(n: Node): boolean {
606606
if (!doc) return false;
607607
return doc.contains(n) || shadowHostInDom(n);
608608
}
609+
610+
let cachedRequestAnimationFrameImplementation:
611+
| undefined
612+
| typeof requestAnimationFrame;
613+
614+
/**
615+
* We generally want to use window.requestAnimationFrame.
616+
* However, in some cases this may be wrapped (e.g. by Zone.js for Angular),
617+
* so we try to get an unpatched version of this from a sandboxed iframe.
618+
*/
619+
function getRequestAnimationFrameImplementation(): typeof requestAnimationFrame {
620+
if (cachedRequestAnimationFrameImplementation) {
621+
return cachedRequestAnimationFrameImplementation;
622+
}
623+
624+
const document = window.document;
625+
let requestAnimationFrameImplementation = window.requestAnimationFrame;
626+
if (document && typeof document.createElement === 'function') {
627+
try {
628+
const sandbox = document.createElement('iframe');
629+
sandbox.hidden = true;
630+
document.head.appendChild(sandbox);
631+
const contentWindow = sandbox.contentWindow;
632+
if (contentWindow && contentWindow.requestAnimationFrame) {
633+
requestAnimationFrameImplementation =
634+
// eslint-disable-next-line @typescript-eslint/unbound-method
635+
contentWindow.requestAnimationFrame;
636+
}
637+
document.head.removeChild(sandbox);
638+
} catch (e) {
639+
// Could not create sandbox iframe, just use window.requestAnimationFrame
640+
}
641+
}
642+
643+
return (cachedRequestAnimationFrameImplementation =
644+
requestAnimationFrameImplementation.bind(window));
645+
}
646+
647+
export function onRequestAnimationFrame(
648+
...rest: Parameters<typeof requestAnimationFrame>
649+
): ReturnType<typeof requestAnimationFrame> {
650+
return getRequestAnimationFrameImplementation()(...rest);
651+
}

0 commit comments

Comments
 (0)