From 85e56a08819e0b69a8fe8cc919e2ad011b4e6f32 Mon Sep 17 00:00:00 2001 From: p-mazhnik Date: Tue, 12 Mar 2024 00:47:55 +0300 Subject: [PATCH 01/18] support for iframes in canvas replay --- packages/rrweb/src/record/index.ts | 6 +- packages/rrweb/src/record/mutation.ts | 4 + .../record/observers/canvas/canvas-manager.ts | 87 +++++++++++++------ 3 files changed, 69 insertions(+), 28 deletions(-) diff --git a/packages/rrweb/src/record/index.ts b/packages/rrweb/src/record/index.ts index 0945f585d7..85de8bc8ea 100644 --- a/packages/rrweb/src/record/index.ts +++ b/packages/rrweb/src/record/index.ts @@ -27,6 +27,7 @@ import { scrollCallback, canvasMutationParam, adoptedStyleSheetParam, + IWindow, } from '@sentry-internal/rrweb-types'; import type { CrossOriginIframeMessageEventContent } from '../types'; import { @@ -341,7 +342,6 @@ function record( getCanvasManager, { mirror, - win: window, mutationCb: (p: canvasMutationParam) => wrappedEmit( wrapEvent({ @@ -361,6 +361,7 @@ function record( errorHandler, }, ); + canvasManager.addWindow(window); const shadowDomManager: ShadowDomManagerInterface = typeof __RRWEB_EXCLUDE_SHADOW_DOM__ === 'boolean' && @@ -450,6 +451,9 @@ function record( onIframeLoad: (iframe, childSn) => { iframeManager.attachIframe(iframe, childSn); shadowDomManager.observeAttachShadow(iframe); + if (iframe.contentWindow) { + canvasManager.addWindow(iframe.contentWindow as IWindow); + } }, onStylesheetLoad: (linkEl, childSn) => { stylesheetManager.attachLinkElement(linkEl, childSn); diff --git a/packages/rrweb/src/record/mutation.ts b/packages/rrweb/src/record/mutation.ts index 588a267b58..6b30380f9f 100644 --- a/packages/rrweb/src/record/mutation.ts +++ b/packages/rrweb/src/record/mutation.ts @@ -21,6 +21,7 @@ import type { removedNodeMutation, addedNodeMutation, Optional, + IWindow, } from '@sentry-internal/rrweb-types'; import { isBlocked, @@ -342,6 +343,9 @@ export default class MutationBuffer { onIframeLoad: (iframe, childSn) => { this.iframeManager.attachIframe(iframe, childSn); this.shadowDomManager.observeAttachShadow(iframe); + if (iframe.contentWindow) { + this.canvasManager.addWindow(iframe.contentWindow as IWindow); + } }, onStylesheetLoad: (link, childSn) => { this.stylesheetManager.attachLinkElement(link, childSn); diff --git a/packages/rrweb/src/record/observers/canvas/canvas-manager.ts b/packages/rrweb/src/record/observers/canvas/canvas-manager.ts index c6e29a2e47..0988e8f723 100644 --- a/packages/rrweb/src/record/observers/canvas/canvas-manager.ts +++ b/packages/rrweb/src/record/observers/canvas/canvas-manager.ts @@ -37,13 +37,14 @@ export interface CanvasManagerInterface { lock(): void; unlock(): void; snapshot(canvasElement?: HTMLCanvasElement): void; + addWindow(win: IWindow): void; } export interface CanvasManagerConstructorOptions { recordCanvas: boolean; enableManualSnapshot?: boolean; mutationCb: canvasMutationCallback; - win: IWindow; + // win: IWindow; blockClass: blockClass; blockSelector: string | null; unblockSelector: string | null; @@ -72,6 +73,9 @@ export class CanvasManagerNoop implements CanvasManagerInterface { public snapshot() { // noop } + public addWindow() { + // noop + } } export class CanvasManager implements CanvasManagerInterface { @@ -81,13 +85,20 @@ export class CanvasManager implements CanvasManagerInterface { private mirror: Mirror; private mutationCb: canvasMutationCallback; - private resetObservers?: listenerHandler; + private restoreHandlers: listenerHandler[] = []; private frozen = false; private locked = false; public reset() { this.pendingCanvasMutations.clear(); - this.resetObservers && this.resetObservers(); + this.restoreHandlers.forEach((handler) => { + try { + handler(); + } catch (e) { + // + } + }); + this.restoreHandlers = []; } public freeze() { @@ -109,12 +120,7 @@ export class CanvasManager implements CanvasManagerInterface { constructor(options: CanvasManagerConstructorOptions) { const { sampling = 'all', - win, - blockClass, - blockSelector, - unblockSelector, recordCanvas, - dataURLOptions, errorHandler, } = options; this.mutationCb = options.mutationCb; @@ -124,19 +130,38 @@ export class CanvasManager implements CanvasManagerInterface { if (errorHandler) { registerErrorHandler(errorHandler); } - if (options.enableManualSnapshot) { return; } + if (recordCanvas && sampling === 'all') { + this.startRAFTimestamping(); + this.startPendingCanvasMutationFlusher(); + } + } + + public addWindow(win: IWindow) { + const { + sampling = 'all', + blockClass, + blockSelector, + unblockSelector, + recordCanvas, + dataURLOptions, + enableManualSnapshot, + } = this.options; + if (enableManualSnapshot) { + return; + } callbackWrapper(() => { - if (recordCanvas && sampling === 'all') + if (recordCanvas && sampling === 'all') { this.initCanvasMutationObserver( win, blockClass, blockSelector, unblockSelector, ); + } if (recordCanvas && typeof sampling === 'number') this.initCanvasFPSObserver( sampling, @@ -195,10 +220,10 @@ export class CanvasManager implements CanvasManagerInterface { options.dataURLOptions, ); - this.resetObservers = () => { + this.restoreHandlers.push(() => { canvasContextReset(); cancelAnimationFrame(rafId); - }; + }); } private initCanvasMutationObserver( @@ -207,9 +232,6 @@ export class CanvasManager implements CanvasManagerInterface { blockSelector: string | null, unblockSelector: string | null, ): void { - this.startRAFTimestamping(); - this.startPendingCanvasMutationFlusher(); - const canvasContextReset = initCanvasContextObserver( win, blockClass, @@ -234,11 +256,11 @@ export class CanvasManager implements CanvasManagerInterface { this.mirror, ); - this.resetObservers = () => { + this.restoreHandlers.push(() => { canvasContextReset(); canvas2DReset(); canvasWebGL1and2Reset(); - }; + }); } public snapshot(canvasElement?: HTMLCanvasElement) { @@ -246,7 +268,7 @@ export class CanvasManager implements CanvasManagerInterface { const rafId = this.takeSnapshot( true, options.sampling === 'all' ? 2 : options.sampling || 2, - options.win, + window, options.blockClass, options.blockSelector, options.unblockSelector, @@ -254,9 +276,9 @@ export class CanvasManager implements CanvasManagerInterface { canvasElement, ); - this.resetObservers = () => { + this.restoreHandlers.push(() => { cancelAnimationFrame(rafId); - }; + }); } private takeSnapshot( @@ -320,13 +342,24 @@ export class CanvasManager implements CanvasManagerInterface { } const matchedCanvas: HTMLCanvasElement[] = []; - win.document.querySelectorAll('canvas').forEach((canvas) => { - if ( - !isBlocked(canvas, blockClass, blockSelector, unblockSelector, true) - ) { - matchedCanvas.push(canvas); - } - }); + // traverse DOM and Shadow DOM + const traverseDom = (root: Document | ShadowRoot) => { + root.querySelectorAll('canvas').forEach((canvas) => { + if ( + !isBlocked(canvas, blockClass, blockSelector, unblockSelector, true) + ) { + matchedCanvas.push(canvas); + } + }); + + root.querySelectorAll('*').forEach((elem) => { + if (elem.shadowRoot) { + traverseDom(elem.shadowRoot); + } + }); + }; + + traverseDom(win.document); return matchedCanvas; }; From f1b9488a3fb6c0eb5a5ec8ce99d23930f5f955c1 Mon Sep 17 00:00:00 2001 From: p-mazhnik Date: Tue, 12 Mar 2024 15:15:51 +0300 Subject: [PATCH 02/18] add canvas tests --- .../__snapshots__/integration.test.ts.snap | 906 ++++++++++++++++++ packages/rrweb/test/html/canvas-iframe.html | 6 + .../rrweb/test/html/canvas-shadow-dom.html | 30 + packages/rrweb/test/integration.test.ts | 91 +- packages/rrweb/test/record/webgl.test.ts | 51 + packages/rrweb/test/utils.ts | 3 +- 6 files changed, 1084 insertions(+), 3 deletions(-) create mode 100644 packages/rrweb/test/html/canvas-iframe.html create mode 100644 packages/rrweb/test/html/canvas-shadow-dom.html diff --git a/packages/rrweb/test/__snapshots__/integration.test.ts.snap b/packages/rrweb/test/__snapshots__/integration.test.ts.snap index 5386b192d0..5b0d0fb8d9 100644 --- a/packages/rrweb/test/__snapshots__/integration.test.ts.snap +++ b/packages/rrweb/test/__snapshots__/integration.test.ts.snap @@ -6491,6 +6491,912 @@ exports[`record integration tests can use maskTextSelector to configure which in ]" `; +exports[`record integration tests canvas should record canvas within iframe 1`] = ` +"[ + { + \\"type\\": 0, + \\"data\\": {} + }, + { + \\"type\\": 1, + \\"data\\": {} + }, + { + \\"type\\": 4, + \\"data\\": { + \\"href\\": \\"about:blank\\", + \\"width\\": 1920, + \\"height\\": 1080 + } + }, + { + \\"type\\": 2, + \\"data\\": { + \\"node\\": { + \\"type\\": 0, + \\"childNodes\\": [ + { + \\"type\\": 1, + \\"name\\": \\"html\\", + \\"publicId\\": \\"\\", + \\"systemId\\": \\"\\", + \\"id\\": 2 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"html\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"head\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 4 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 6 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"iframe\\", + \\"attributes\\": { + \\"id\\": \\"iframe-canvas\\", + \\"width\\": \\"100%\\", + \\"height\\": \\"100%\\" + }, + \\"childNodes\\": [], + \\"id\\": 7 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n\\\\n \\", + \\"id\\": 8 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 10 + } + ], + \\"id\\": 9 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\\\n\\\\n\\", + \\"id\\": 11 + } + ], + \\"id\\": 5 + } + ], + \\"id\\": 3 + } + ], + \\"id\\": 1 + }, + \\"initialOffset\\": { + \\"left\\": 0, + \\"top\\": 0 + } + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"adds\\": [ + { + \\"parentId\\": 7, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 0, + \\"childNodes\\": [ + { + \\"type\\": 1, + \\"name\\": \\"html\\", + \\"publicId\\": \\"\\", + \\"systemId\\": \\"\\", + \\"rootId\\": 12, + \\"id\\": 13 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"html\\", + \\"attributes\\": { + \\"lang\\": \\"en\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"head\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"rootId\\": 12, + \\"id\\": 16 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"meta\\", + \\"attributes\\": { + \\"charset\\": \\"UTF-8\\" + }, + \\"childNodes\\": [], + \\"rootId\\": 12, + \\"id\\": 17 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"rootId\\": 12, + \\"id\\": 18 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"meta\\", + \\"attributes\\": { + \\"name\\": \\"viewport\\", + \\"content\\": \\"width=device-width, initial-scale=1.0\\" + }, + \\"childNodes\\": [], + \\"rootId\\": 12, + \\"id\\": 19 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"rootId\\": 12, + \\"id\\": 20 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"title\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"canvas\\", + \\"rootId\\": 12, + \\"id\\": 22 + } + ], + \\"rootId\\": 12, + \\"id\\": 21 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"rootId\\": 12, + \\"id\\": 23 + } + ], + \\"rootId\\": 12, + \\"id\\": 15 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"rootId\\": 12, + \\"id\\": 24 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"rootId\\": 12, + \\"id\\": 26 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"canvas\\", + \\"attributes\\": { + \\"id\\": \\"myCanvas\\", + \\"width\\": \\"200\\", + \\"height\\": \\"100\\", + \\"style\\": \\"border: 1px solid #000000;\\", + \\"rr_dataURL\\": \\"data:image/png;base64,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\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"rootId\\": 12, + \\"id\\": 28 + } + ], + \\"rootId\\": 12, + \\"id\\": 27 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"rootId\\": 12, + \\"id\\": 29 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"rootId\\": 12, + \\"id\\": 31 + } + ], + \\"rootId\\": 12, + \\"id\\": 30 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n\\\\n\\", + \\"rootId\\": 12, + \\"id\\": 32 + } + ], + \\"rootId\\": 12, + \\"id\\": 25 + } + ], + \\"rootId\\": 12, + \\"id\\": 14 + } + ], + \\"id\\": 12 + } + } + ], + \\"removes\\": [], + \\"texts\\": [], + \\"attributes\\": [], + \\"isAttachIframe\\": true + } + } +]" +`; + +exports[`record integration tests canvas should record canvas within iframe with sampling 1`] = ` +"[ + { + \\"type\\": 0, + \\"data\\": {} + }, + { + \\"type\\": 1, + \\"data\\": {} + }, + { + \\"type\\": 4, + \\"data\\": { + \\"href\\": \\"about:blank\\", + \\"width\\": 1920, + \\"height\\": 1080 + } + }, + { + \\"type\\": 2, + \\"data\\": { + \\"node\\": { + \\"type\\": 0, + \\"childNodes\\": [ + { + \\"type\\": 1, + \\"name\\": \\"html\\", + \\"publicId\\": \\"\\", + \\"systemId\\": \\"\\", + \\"id\\": 2 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"html\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"head\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 4 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 6 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"iframe\\", + \\"attributes\\": { + \\"id\\": \\"iframe-canvas\\", + \\"width\\": \\"100%\\", + \\"height\\": \\"100%\\" + }, + \\"childNodes\\": [], + \\"id\\": 7 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n\\\\n \\", + \\"id\\": 8 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 10 + } + ], + \\"id\\": 9 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\\\n\\\\n\\", + \\"id\\": 11 + } + ], + \\"id\\": 5 + } + ], + \\"id\\": 3 + } + ], + \\"id\\": 1 + }, + \\"initialOffset\\": { + \\"left\\": 0, + \\"top\\": 0 + } + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"adds\\": [ + { + \\"parentId\\": 7, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 0, + \\"childNodes\\": [ + { + \\"type\\": 1, + \\"name\\": \\"html\\", + \\"publicId\\": \\"\\", + \\"systemId\\": \\"\\", + \\"rootId\\": 12, + \\"id\\": 13 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"html\\", + \\"attributes\\": { + \\"lang\\": \\"en\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"head\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"rootId\\": 12, + \\"id\\": 16 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"meta\\", + \\"attributes\\": { + \\"charset\\": \\"UTF-8\\" + }, + \\"childNodes\\": [], + \\"rootId\\": 12, + \\"id\\": 17 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"rootId\\": 12, + \\"id\\": 18 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"meta\\", + \\"attributes\\": { + \\"name\\": \\"viewport\\", + \\"content\\": \\"width=device-width, initial-scale=1.0\\" + }, + \\"childNodes\\": [], + \\"rootId\\": 12, + \\"id\\": 19 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"rootId\\": 12, + \\"id\\": 20 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"title\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"canvas\\", + \\"rootId\\": 12, + \\"id\\": 22 + } + ], + \\"rootId\\": 12, + \\"id\\": 21 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"rootId\\": 12, + \\"id\\": 23 + } + ], + \\"rootId\\": 12, + \\"id\\": 15 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"rootId\\": 12, + \\"id\\": 24 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"rootId\\": 12, + \\"id\\": 26 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"canvas\\", + \\"attributes\\": { + \\"id\\": \\"myCanvas\\", + \\"width\\": \\"200\\", + \\"height\\": \\"100\\", + \\"style\\": \\"border: 1px solid #000000;\\", + \\"rr_dataURL\\": \\"data:image/png;base64,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\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"rootId\\": 12, + \\"id\\": 28 + } + ], + \\"rootId\\": 12, + \\"id\\": 27 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"rootId\\": 12, + \\"id\\": 29 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"rootId\\": 12, + \\"id\\": 31 + } + ], + \\"rootId\\": 12, + \\"id\\": 30 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n\\\\n\\", + \\"rootId\\": 12, + \\"id\\": 32 + } + ], + \\"rootId\\": 12, + \\"id\\": 25 + } + ], + \\"rootId\\": 12, + \\"id\\": 14 + } + ], + \\"id\\": 12 + } + } + ], + \\"removes\\": [], + \\"texts\\": [], + \\"attributes\\": [], + \\"isAttachIframe\\": true + } + } +]" +`; + +exports[`record integration tests canvas should record canvas within shadow dom 1`] = ` +"[ + { + \\"type\\": 0, + \\"data\\": {} + }, + { + \\"type\\": 1, + \\"data\\": {} + }, + { + \\"type\\": 4, + \\"data\\": { + \\"href\\": \\"about:blank\\", + \\"width\\": 1920, + \\"height\\": 1080 + } + }, + { + \\"type\\": 2, + \\"data\\": { + \\"node\\": { + \\"type\\": 0, + \\"childNodes\\": [ + { + \\"type\\": 1, + \\"name\\": \\"html\\", + \\"publicId\\": \\"\\", + \\"systemId\\": \\"\\", + \\"id\\": 2 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"html\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"head\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 4 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 6 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"div\\", + \\"attributes\\": { + \\"id\\": \\"shadow-root\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"canvas\\", + \\"attributes\\": { + \\"width\\": \\"200\\", + \\"height\\": \\"200\\" + }, + \\"childNodes\\": [], + \\"id\\": 8, + \\"isShadow\\": true + } + ], + \\"id\\": 7, + \\"isShadowHost\\": true + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n\\\\n \\", + \\"id\\": 9 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 11 + } + ], + \\"id\\": 10 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\\\n\\", + \\"id\\": 12 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 14 + } + ], + \\"id\\": 13 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n\\\\n\\", + \\"id\\": 15 + } + ], + \\"id\\": 5 + } + ], + \\"id\\": 3 + } + ], + \\"id\\": 1 + }, + \\"initialOffset\\": { + \\"left\\": 0, + \\"top\\": 0 + } + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 9, + \\"id\\": 8, + \\"type\\": 0, + \\"commands\\": [ + { + \\"property\\": \\"clearRect\\", + \\"args\\": [ + 0, + 0, + 200, + 200 + ] + }, + { + \\"property\\": \\"fillStyle\\", + \\"args\\": [ + \\"green\\" + ], + \\"setter\\": true + }, + { + \\"property\\": \\"fillRect\\", + \\"args\\": [ + 2000, + 2000, + 50, + 50 + ] + } + ] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 9, + \\"id\\": 8, + \\"type\\": 0, + \\"commands\\": [ + { + \\"property\\": \\"clearRect\\", + \\"args\\": [ + 0, + 0, + 200, + 200 + ] + }, + { + \\"property\\": \\"fillStyle\\", + \\"args\\": [ + \\"green\\" + ], + \\"setter\\": true + }, + { + \\"property\\": \\"fillRect\\", + \\"args\\": [ + 4000, + 4000, + 50, + 50 + ] + } + ] + } + } +]" +`; + +exports[`record integration tests canvas should record canvas within shadow dom with sampling 1`] = ` +"[ + { + \\"type\\": 0, + \\"data\\": {} + }, + { + \\"type\\": 1, + \\"data\\": {} + }, + { + \\"type\\": 4, + \\"data\\": { + \\"href\\": \\"about:blank\\", + \\"width\\": 1920, + \\"height\\": 1080 + } + }, + { + \\"type\\": 2, + \\"data\\": { + \\"node\\": { + \\"type\\": 0, + \\"childNodes\\": [ + { + \\"type\\": 1, + \\"name\\": \\"html\\", + \\"publicId\\": \\"\\", + \\"systemId\\": \\"\\", + \\"id\\": 2 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"html\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"head\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 4 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 6 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"div\\", + \\"attributes\\": { + \\"id\\": \\"shadow-root\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"canvas\\", + \\"attributes\\": { + \\"width\\": \\"200\\", + \\"height\\": \\"200\\" + }, + \\"childNodes\\": [], + \\"id\\": 8, + \\"isShadow\\": true + } + ], + \\"id\\": 7, + \\"isShadowHost\\": true + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n\\\\n \\", + \\"id\\": 9 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 11 + } + ], + \\"id\\": 10 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\\\n\\", + \\"id\\": 12 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 14 + } + ], + \\"id\\": 13 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n\\\\n\\", + \\"id\\": 15 + } + ], + \\"id\\": 5 + } + ], + \\"id\\": 3 + } + ], + \\"id\\": 1 + }, + \\"initialOffset\\": { + \\"left\\": 0, + \\"top\\": 0 + } + } + } +]" +`; + exports[`record integration tests handles null attribute values 1`] = ` "[ { diff --git a/packages/rrweb/test/html/canvas-iframe.html b/packages/rrweb/test/html/canvas-iframe.html new file mode 100644 index 0000000000..1ae3b84224 --- /dev/null +++ b/packages/rrweb/test/html/canvas-iframe.html @@ -0,0 +1,6 @@ + + + + + + diff --git a/packages/rrweb/test/html/canvas-shadow-dom.html b/packages/rrweb/test/html/canvas-shadow-dom.html new file mode 100644 index 0000000000..ef02955350 --- /dev/null +++ b/packages/rrweb/test/html/canvas-shadow-dom.html @@ -0,0 +1,30 @@ + + + +
+ + + diff --git a/packages/rrweb/test/integration.test.ts b/packages/rrweb/test/integration.test.ts index b196f39b0e..f3b838bf32 100644 --- a/packages/rrweb/test/integration.test.ts +++ b/packages/rrweb/test/integration.test.ts @@ -10,8 +10,8 @@ import { waitForIFrameLoad, replaceLast, generateRecordSnippet, - ISuite, -} from './utils'; + ISuite, stripBase64, +} from './utils' import type { recordOptions } from '../src/types'; import { eventWithTime, @@ -1064,6 +1064,93 @@ describe('record integration tests', function (this: ISuite) { assertSnapshot(snapshots); }); + describe('canvas', function (this: ISuite) { + jest.setTimeout(10_000); + it('should record canvas within iframe', async () => { + const page: puppeteer.Page = await browser.newPage(); + await page.goto(`${serverURL}/html`); + await page.setContent( + getHtml.call(this, 'canvas-iframe.html', { + recordCanvas: true, + }), + ); + + const frameId = await waitForIFrameLoad(page, '#iframe-canvas'); + await frameId.waitForFunction('window.canvasMutationApplied'); + await waitForRAF(page); + + const snapshots = (await page.evaluate( + 'window.snapshots', + )) as eventWithTime[]; + assertSnapshot(stripBase64(snapshots)); + }); + + it ('should record canvas within iframe with sampling', async () => { + const maxFPS = 60; + const page: puppeteer.Page = await browser.newPage(); + await page.goto(`${serverURL}/html`); + await page.setContent( + getHtml.call(this, 'canvas-iframe.html', { + recordCanvas: true, + sampling: { + canvas: maxFPS, + } + }), + ); + + const frameId = await waitForIFrameLoad(page, '#iframe-canvas'); + await frameId.waitForFunction('window.canvasMutationApplied'); + await waitForRAF(page); + + const snapshots = (await page.evaluate( + 'window.snapshots', + )) as eventWithTime[]; + assertSnapshot(stripBase64(snapshots)); + }); + + it('should record canvas within shadow dom', async () => { + const page: puppeteer.Page = await browser.newPage(); + await page.goto(`${serverURL}/html`); + await page.setContent( + getHtml.call(this, 'canvas-shadow-dom.html', { + recordCanvas: true, + // sampling: { + // canvas: 1, + // }, + }), + ); + + await page.waitForFunction('window.canvasMutationApplied'); + await waitForRAF(page); + + const snapshots = (await page.evaluate( + 'window.snapshots', + )) as eventWithTime[]; + assertSnapshot(stripBase64(snapshots)); + }); + + it('should record canvas within shadow dom with sampling', async () => { + const page: puppeteer.Page = await browser.newPage(); + await page.goto(`${serverURL}/html`); + await page.setContent( + getHtml.call(this, 'canvas-shadow-dom.html', { + recordCanvas: true, + sampling: { + canvas: 60, + }, + }), + ); + + await page.waitForFunction('window.canvasMutationApplied'); + await waitForRAF(page); + + const snapshots = (await page.evaluate( + 'window.snapshots', + )) as eventWithTime[]; + assertSnapshot(stripBase64(snapshots)); + }); + }) + it('should record images with blob url', async () => { const page: puppeteer.Page = await browser.newPage(); await page.goto(`${serverURL}/html`); diff --git a/packages/rrweb/test/record/webgl.test.ts b/packages/rrweb/test/record/webgl.test.ts index 1688cd6ecb..38c5c2edce 100644 --- a/packages/rrweb/test/record/webgl.test.ts +++ b/packages/rrweb/test/record/webgl.test.ts @@ -14,6 +14,7 @@ import { launchPuppeteer, stripBase64, waitForRAF, + waitForIFrameLoad, } from '../utils'; import type { ICanvas } from '@sentry-internal/rrweb-snapshot'; import type { CanvasManager } from '../../src/record/observers/canvas/canvas-manager'; @@ -318,4 +319,54 @@ describe('record webgl', function (this: ISuite) { assertSnapshot(stripBase64(ctx.events)); }); }); + + describe('record canvas within iframe', function (this: ISuite) { + jest.setTimeout(10_000); + + const ctx: ISuite = setup.call( + this, + ` + + + + + + + ` + ); + + it('will record changes to a canvas element', async () => { + const frame = await waitForIFrameLoad(ctx.page, '#iframe1'); + await frame.evaluate(() => { + const canvas = document.createElement('canvas'); + canvas.id = 'canvas'; + document.body.appendChild(canvas); + }); + + await ctx.page.waitForTimeout(50); + + await frame.evaluate(() => { + const canvas = document.getElementById('canvas') as HTMLCanvasElement; + const gl = canvas.getContext('webgl')!; + + gl.clear(gl.COLOR_BUFFER_BIT); + }); + await ctx.page.waitForTimeout(50); + + const lastEvent = ctx.events[ctx.events.length - 1]; + expect(lastEvent).toMatchObject({ + data: { + source: IncrementalSource.CanvasMutation, + type: CanvasContext.WebGL, + commands: [ + { + args: [16384], + property: 'clear', + }, + ], + }, + }); + assertSnapshot(stripBase64(ctx.events)); + }); + }) }); diff --git a/packages/rrweb/test/utils.ts b/packages/rrweb/test/utils.ts index 7f406ad239..c1206d7373 100644 --- a/packages/rrweb/test/utils.ts +++ b/packages/rrweb/test/utils.ts @@ -715,7 +715,8 @@ export function generateRecordSnippet(options: recordOptions) { options.recordCanvas ? '(opts) => new rrweb.CanvasManager(opts)' : 'undefined' - } + }, + sampling: ${JSON.stringify(options.sampling)}, }); `; } From 38895d4c05e0284b2f121842f6b421738f6b5c44 Mon Sep 17 00:00:00 2001 From: p-mazhnik Date: Tue, 12 Mar 2024 15:44:27 +0300 Subject: [PATCH 03/18] update tests --- packages/rrweb/test/integration.test.ts | 47 ++++- .../record/__snapshots__/webgl.test.ts.snap | 166 ++++++++++++++++++ 2 files changed, 209 insertions(+), 4 deletions(-) diff --git a/packages/rrweb/test/integration.test.ts b/packages/rrweb/test/integration.test.ts index f3b838bf32..8d65d837de 100644 --- a/packages/rrweb/test/integration.test.ts +++ b/packages/rrweb/test/integration.test.ts @@ -18,7 +18,8 @@ import { EventType, RecordPlugin, IncrementalSource, -} from '@sentry-internal/rrweb-types'; + CanvasContext, +} from '@sentry-internal/rrweb-types' import { visitSnapshot, NodeType } from '@sentry-internal/rrweb-snapshot'; describe('record integration tests', function (this: ISuite) { @@ -1082,6 +1083,16 @@ describe('record integration tests', function (this: ISuite) { const snapshots = (await page.evaluate( 'window.snapshots', )) as eventWithTime[]; + // expect(snapshots[snapshots.length - 1].data).toEqual(expect.objectContaining({ + // source: IncrementalSource.CanvasMutation, + // type: CanvasContext['2D'], + // commands: expect.arrayContaining([ + // { + // args: [200, 100], + // property: 'lineTo', + // }, + // ]), + // })) assertSnapshot(stripBase64(snapshots)); }); @@ -1101,10 +1112,20 @@ describe('record integration tests', function (this: ISuite) { const frameId = await waitForIFrameLoad(page, '#iframe-canvas'); await frameId.waitForFunction('window.canvasMutationApplied'); await waitForRAF(page); + await page.waitForTimeout(50); const snapshots = (await page.evaluate( 'window.snapshots', )) as eventWithTime[]; + // expect(snapshots[snapshots.length - 1].data).toEqual(expect.objectContaining({ + // source: IncrementalSource.CanvasMutation, + // type: CanvasContext['2D'], + // commands: expect.arrayContaining([ + // expect.objectContaining({ + // property: 'drawImage', + // }), + // ]), + // })) assertSnapshot(stripBase64(snapshots)); }); @@ -1114,9 +1135,6 @@ describe('record integration tests', function (this: ISuite) { await page.setContent( getHtml.call(this, 'canvas-shadow-dom.html', { recordCanvas: true, - // sampling: { - // canvas: 1, - // }, }), ); @@ -1126,6 +1144,16 @@ describe('record integration tests', function (this: ISuite) { const snapshots = (await page.evaluate( 'window.snapshots', )) as eventWithTime[]; + expect(snapshots[snapshots.length - 1].data).toEqual(expect.objectContaining({ + source: IncrementalSource.CanvasMutation, + type: CanvasContext['2D'], + commands: expect.arrayContaining([ + { + args: [4000, 4000, 50, 50], + property: 'fillRect', + }, + ]), + })) assertSnapshot(stripBase64(snapshots)); }); @@ -1144,9 +1172,20 @@ describe('record integration tests', function (this: ISuite) { await page.waitForFunction('window.canvasMutationApplied'); await waitForRAF(page); + await page.waitForTimeout(50); + const snapshots = (await page.evaluate( 'window.snapshots', )) as eventWithTime[]; + // expect(snapshots[snapshots.length - 1].data).toEqual(expect.objectContaining({ + // source: IncrementalSource.CanvasMutation, + // type: CanvasContext['2D'], + // commands: expect.arrayContaining([ + // expect.objectContaining({ + // property: 'drawImage', + // }), + // ]), + // })) assertSnapshot(stripBase64(snapshots)); }); }) diff --git a/packages/rrweb/test/record/__snapshots__/webgl.test.ts.snap b/packages/rrweb/test/record/__snapshots__/webgl.test.ts.snap index c7aeb17e1c..e465828cfa 100644 --- a/packages/rrweb/test/record/__snapshots__/webgl.test.ts.snap +++ b/packages/rrweb/test/record/__snapshots__/webgl.test.ts.snap @@ -1,5 +1,171 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`record webgl record canvas within iframe will record changes to a canvas element 1`] = ` +"[ + { + \\"type\\": 4, + \\"data\\": { + \\"href\\": \\"about:blank\\", + \\"width\\": 1920, + \\"height\\": 1080 + } + }, + { + \\"type\\": 2, + \\"data\\": { + \\"node\\": { + \\"type\\": 0, + \\"childNodes\\": [ + { + \\"type\\": 1, + \\"name\\": \\"html\\", + \\"publicId\\": \\"\\", + \\"systemId\\": \\"\\", + \\"id\\": 2 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"html\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"head\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 4 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 6 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"iframe\\", + \\"attributes\\": { + \\"id\\": \\"iframe1\\" + }, + \\"childNodes\\": [], + \\"id\\": 7 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\\\n \\", + \\"id\\": 8 + } + ], + \\"id\\": 5 + } + ], + \\"id\\": 3 + } + ], + \\"id\\": 1 + }, + \\"initialOffset\\": { + \\"left\\": 0, + \\"top\\": 0 + } + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"adds\\": [ + { + \\"parentId\\": 7, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 0, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"html\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"head\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"rootId\\": 9, + \\"id\\": 11 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"rootId\\": 9, + \\"id\\": 12 + } + ], + \\"rootId\\": 9, + \\"id\\": 10 + } + ], + \\"compatMode\\": \\"BackCompat\\", + \\"id\\": 9 + } + } + ], + \\"removes\\": [], + \\"texts\\": [], + \\"attributes\\": [], + \\"isAttachIframe\\": true + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [], + \\"removes\\": [], + \\"adds\\": [ + { + \\"parentId\\": 12, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"canvas\\", + \\"attributes\\": { + \\"id\\": \\"canvas\\" + }, + \\"childNodes\\": [], + \\"rootId\\": 9, + \\"id\\": 13 + } + } + ] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 9, + \\"id\\": 13, + \\"type\\": 1, + \\"commands\\": [ + { + \\"property\\": \\"clear\\", + \\"args\\": [ + 16384 + ] + } + ] + } + } +]" +`; + exports[`record webgl recordCanvas FPS should record snapshots 1`] = ` "[ { From 099ff359c65a8b3bd7a3fb2ab1b50702bd1fb1b5 Mon Sep 17 00:00:00 2001 From: p-mazhnik Date: Tue, 12 Mar 2024 16:29:25 +0300 Subject: [PATCH 04/18] fix tests --- .../test/__snapshots__/integration.test.ts.snap | 14 ++++++++------ packages/rrweb/test/html/canvas-shadow-dom.html | 6 +++--- packages/rrweb/test/integration.test.ts | 2 +- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/packages/rrweb/test/__snapshots__/integration.test.ts.snap b/packages/rrweb/test/__snapshots__/integration.test.ts.snap index 5b0d0fb8d9..a07a6440a1 100644 --- a/packages/rrweb/test/__snapshots__/integration.test.ts.snap +++ b/packages/rrweb/test/__snapshots__/integration.test.ts.snap @@ -7124,7 +7124,8 @@ exports[`record integration tests canvas should record canvas within shadow dom \\"tagName\\": \\"canvas\\", \\"attributes\\": { \\"width\\": \\"200\\", - \\"height\\": \\"200\\" + \\"height\\": \\"200\\", + \\"rr_dataURL\\": \\"data:image/png;base64,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\\" }, \\"childNodes\\": [], \\"id\\": 8, @@ -7216,8 +7217,8 @@ exports[`record integration tests canvas should record canvas within shadow dom { \\"property\\": \\"fillRect\\", \\"args\\": [ - 2000, - 2000, + 40, + 40, 50, 50 ] @@ -7251,8 +7252,8 @@ exports[`record integration tests canvas should record canvas within shadow dom { \\"property\\": \\"fillRect\\", \\"args\\": [ - 4000, - 4000, + 100, + 100, 50, 50 ] @@ -7328,7 +7329,8 @@ exports[`record integration tests canvas should record canvas within shadow dom \\"tagName\\": \\"canvas\\", \\"attributes\\": { \\"width\\": \\"200\\", - \\"height\\": \\"200\\" + \\"height\\": \\"200\\", + \\"rr_dataURL\\": \\"data:image/png;base64,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\\" }, \\"childNodes\\": [], \\"id\\": 8, diff --git a/packages/rrweb/test/html/canvas-shadow-dom.html b/packages/rrweb/test/html/canvas-shadow-dom.html index ef02955350..3c29f4cf3f 100644 --- a/packages/rrweb/test/html/canvas-shadow-dom.html +++ b/packages/rrweb/test/html/canvas-shadow-dom.html @@ -8,7 +8,7 @@ const shadowRoot = shadowRootContainer.attachShadow({ mode: 'open' }) // Creating canvas inside the Shadow DOM - const shadowCanvas = shadowRoot.ownerDocument.createElement('canvas') + const shadowCanvas = document.createElement('canvas') shadowRoot.appendChild(shadowCanvas); const shadowCtx = shadowCanvas.getContext('2d') @@ -20,10 +20,10 @@ shadowCtx.fillStyle = 'green'; shadowCtx.fillRect(num * shadowCanvas.width, num * shadowCanvas.height, 50, 50); } - moveSquare(10) + moveSquare(0.2) setTimeout(() => { - moveSquare(20) + moveSquare(0.5) window.canvasMutationApplied = true; }, 10); diff --git a/packages/rrweb/test/integration.test.ts b/packages/rrweb/test/integration.test.ts index 8d65d837de..814d15cc73 100644 --- a/packages/rrweb/test/integration.test.ts +++ b/packages/rrweb/test/integration.test.ts @@ -1149,7 +1149,7 @@ describe('record integration tests', function (this: ISuite) { type: CanvasContext['2D'], commands: expect.arrayContaining([ { - args: [4000, 4000, 50, 50], + args: [100, 100, 50, 50], property: 'fillRect', }, ]), From 1e27150fd96e92b93e40cb79279e7ad459f3b44e Mon Sep 17 00:00:00 2001 From: p-mazhnik Date: Tue, 12 Mar 2024 16:32:50 +0300 Subject: [PATCH 05/18] tests passing --- .../__snapshots__/integration.test.ts.snap | 163 ++++++++++++++++-- packages/rrweb/test/integration.test.ts | 56 +++--- 2 files changed, 180 insertions(+), 39 deletions(-) diff --git a/packages/rrweb/test/__snapshots__/integration.test.ts.snap b/packages/rrweb/test/__snapshots__/integration.test.ts.snap index a07a6440a1..122fbbc95f 100644 --- a/packages/rrweb/test/__snapshots__/integration.test.ts.snap +++ b/packages/rrweb/test/__snapshots__/integration.test.ts.snap @@ -6771,6 +6771,34 @@ exports[`record integration tests canvas should record canvas within iframe 1`] \\"attributes\\": [], \\"isAttachIframe\\": true } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 9, + \\"id\\": 27, + \\"type\\": 0, + \\"commands\\": [ + { + \\"property\\": \\"moveTo\\", + \\"args\\": [ + 0, + 0 + ] + }, + { + \\"property\\": \\"lineTo\\", + \\"args\\": [ + 200, + 100 + ] + }, + { + \\"property\\": \\"stroke\\", + \\"args\\": [] + } + ] + } } ]" `; @@ -7055,6 +7083,47 @@ exports[`record integration tests canvas should record canvas within iframe with \\"attributes\\": [], \\"isAttachIframe\\": true } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 9, + \\"id\\": 27, + \\"type\\": 0, + \\"commands\\": [ + { + \\"property\\": \\"clearRect\\", + \\"args\\": [ + 0, + 0, + 200, + 100 + ] + }, + { + \\"property\\": \\"drawImage\\", + \\"args\\": [ + { + \\"rr_type\\": \\"ImageBitmap\\", + \\"args\\": [ + { + \\"rr_type\\": \\"Blob\\", + \\"data\\": [ + { + \\"rr_type\\": \\"ArrayBuffer\\", + \\"base64\\": \\"base64-0\\" + } + ], + \\"type\\": \\"image/png\\" + } + ] + }, + 0, + 0 + ] + } + ] + } } ]" `; @@ -7222,17 +7291,7 @@ exports[`record integration tests canvas should record canvas within shadow dom 50, 50 ] - } - ] - } - }, - { - \\"type\\": 3, - \\"data\\": { - \\"source\\": 9, - \\"id\\": 8, - \\"type\\": 0, - \\"commands\\": [ + }, { \\"property\\": \\"clearRect\\", \\"args\\": [ @@ -7395,6 +7454,88 @@ exports[`record integration tests canvas should record canvas within shadow dom \\"top\\": 0 } } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 9, + \\"id\\": 8, + \\"type\\": 0, + \\"commands\\": [ + { + \\"property\\": \\"clearRect\\", + \\"args\\": [ + 0, + 0, + 200, + 200 + ] + }, + { + \\"property\\": \\"drawImage\\", + \\"args\\": [ + { + \\"rr_type\\": \\"ImageBitmap\\", + \\"args\\": [ + { + \\"rr_type\\": \\"Blob\\", + \\"data\\": [ + { + \\"rr_type\\": \\"ArrayBuffer\\", + \\"base64\\": \\"base64-0\\" + } + ], + \\"type\\": \\"image/png\\" + } + ] + }, + 0, + 0 + ] + } + ] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 9, + \\"id\\": 8, + \\"type\\": 0, + \\"commands\\": [ + { + \\"property\\": \\"clearRect\\", + \\"args\\": [ + 0, + 0, + 200, + 200 + ] + }, + { + \\"property\\": \\"drawImage\\", + \\"args\\": [ + { + \\"rr_type\\": \\"ImageBitmap\\", + \\"args\\": [ + { + \\"rr_type\\": \\"Blob\\", + \\"data\\": [ + { + \\"rr_type\\": \\"ArrayBuffer\\", + \\"base64\\": \\"base64-1\\" + } + ], + \\"type\\": \\"image/png\\" + } + ] + }, + 0, + 0 + ] + } + ] + } } ]" `; diff --git a/packages/rrweb/test/integration.test.ts b/packages/rrweb/test/integration.test.ts index 814d15cc73..6fc4a966e1 100644 --- a/packages/rrweb/test/integration.test.ts +++ b/packages/rrweb/test/integration.test.ts @@ -1083,16 +1083,16 @@ describe('record integration tests', function (this: ISuite) { const snapshots = (await page.evaluate( 'window.snapshots', )) as eventWithTime[]; - // expect(snapshots[snapshots.length - 1].data).toEqual(expect.objectContaining({ - // source: IncrementalSource.CanvasMutation, - // type: CanvasContext['2D'], - // commands: expect.arrayContaining([ - // { - // args: [200, 100], - // property: 'lineTo', - // }, - // ]), - // })) + expect(snapshots[snapshots.length - 1].data).toEqual(expect.objectContaining({ + source: IncrementalSource.CanvasMutation, + type: CanvasContext['2D'], + commands: expect.arrayContaining([ + { + args: [200, 100], + property: 'lineTo', + }, + ]), + })) assertSnapshot(stripBase64(snapshots)); }); @@ -1117,15 +1117,15 @@ describe('record integration tests', function (this: ISuite) { const snapshots = (await page.evaluate( 'window.snapshots', )) as eventWithTime[]; - // expect(snapshots[snapshots.length - 1].data).toEqual(expect.objectContaining({ - // source: IncrementalSource.CanvasMutation, - // type: CanvasContext['2D'], - // commands: expect.arrayContaining([ - // expect.objectContaining({ - // property: 'drawImage', - // }), - // ]), - // })) + expect(snapshots[snapshots.length - 1].data).toEqual(expect.objectContaining({ + source: IncrementalSource.CanvasMutation, + type: CanvasContext['2D'], + commands: expect.arrayContaining([ + expect.objectContaining({ + property: 'drawImage', + }), + ]), + })) assertSnapshot(stripBase64(snapshots)); }); @@ -1177,15 +1177,15 @@ describe('record integration tests', function (this: ISuite) { const snapshots = (await page.evaluate( 'window.snapshots', )) as eventWithTime[]; - // expect(snapshots[snapshots.length - 1].data).toEqual(expect.objectContaining({ - // source: IncrementalSource.CanvasMutation, - // type: CanvasContext['2D'], - // commands: expect.arrayContaining([ - // expect.objectContaining({ - // property: 'drawImage', - // }), - // ]), - // })) + expect(snapshots[snapshots.length - 1].data).toEqual(expect.objectContaining({ + source: IncrementalSource.CanvasMutation, + type: CanvasContext['2D'], + commands: expect.arrayContaining([ + expect.objectContaining({ + property: 'drawImage', + }), + ]), + })) assertSnapshot(stripBase64(snapshots)); }); }) From cf824d15984f43d2bcf46c7accba28c354e6be1c Mon Sep 17 00:00:00 2001 From: p-mazhnik Date: Tue, 12 Mar 2024 21:03:12 +0300 Subject: [PATCH 06/18] cleanup --- .../record/observers/canvas/canvas-manager.ts | 49 ++- .../rrweb/src/record/shadow-dom-manager.ts | 2 + .../__snapshots__/integration.test.ts.snap | 337 +----------------- packages/rrweb/test/integration.test.ts | 3 +- packages/rrweb/tsconfig.json | 3 +- 5 files changed, 57 insertions(+), 337 deletions(-) diff --git a/packages/rrweb/src/record/observers/canvas/canvas-manager.ts b/packages/rrweb/src/record/observers/canvas/canvas-manager.ts index 0988e8f723..0703f51986 100644 --- a/packages/rrweb/src/record/observers/canvas/canvas-manager.ts +++ b/packages/rrweb/src/record/observers/canvas/canvas-manager.ts @@ -38,6 +38,8 @@ export interface CanvasManagerInterface { unlock(): void; snapshot(canvasElement?: HTMLCanvasElement): void; addWindow(win: IWindow): void; + addShadowRoot(shadowRoot: ShadowRoot): void; + resetShadowRoots(): void; } export interface CanvasManagerConstructorOptions { @@ -76,6 +78,14 @@ export class CanvasManagerNoop implements CanvasManagerInterface { public addWindow() { // noop } + + public addShadowRoot() { + // noop + } + + public resetShadowRoots() { + // noop + } } export class CanvasManager implements CanvasManagerInterface { @@ -84,6 +94,9 @@ export class CanvasManager implements CanvasManagerInterface { private options: CanvasManagerConstructorOptions; private mirror: Mirror; + private shadowDoms = new Set>(); + private windows = new WeakSet(); + private mutationCb: canvasMutationCallback; private restoreHandlers: listenerHandler[] = []; private frozen = false; @@ -99,6 +112,8 @@ export class CanvasManager implements CanvasManagerInterface { } }); this.restoreHandlers = []; + this.windows = new WeakSet(); + this.shadowDoms = new Set(); } public freeze() { @@ -149,6 +164,9 @@ export class CanvasManager implements CanvasManagerInterface { dataURLOptions, enableManualSnapshot, } = this.options; + if (this.windows.has(win)) return; + this.windows.add(win); + if (enableManualSnapshot) { return; } @@ -176,6 +194,14 @@ export class CanvasManager implements CanvasManagerInterface { })(); } + public addShadowRoot(shadowRoot: ShadowRoot) { + this.shadowDoms.add(new WeakRef(shadowRoot)); + } + + public resetShadowRoots() { + this.shadowDoms = new Set(); + } + private processMutation: canvasManagerMutationCallback = ( target, mutation, @@ -268,7 +294,7 @@ export class CanvasManager implements CanvasManagerInterface { const rafId = this.takeSnapshot( true, options.sampling === 'all' ? 2 : options.sampling || 2, - window, + canvasElement?.ownerDocument?.defaultView ?? window, options.blockClass, options.blockSelector, options.unblockSelector, @@ -294,6 +320,9 @@ export class CanvasManager implements CanvasManagerInterface { const snapshotInProgressMap: Map = new Map(); const worker = new Worker(getImageBitmapDataUrlWorkerURL()); worker.onmessage = (e) => { + if (!this.windows.has(win)) { + return; + } const data = e.data as ImageBitmapDataURLWorkerResponse; const { id } = data; snapshotInProgressMap.set(id, false); @@ -342,7 +371,7 @@ export class CanvasManager implements CanvasManagerInterface { } const matchedCanvas: HTMLCanvasElement[] = []; - // traverse DOM and Shadow DOM + const traverseDom = (root: Document | ShadowRoot) => { root.querySelectorAll('canvas').forEach((canvas) => { if ( @@ -351,19 +380,23 @@ export class CanvasManager implements CanvasManagerInterface { matchedCanvas.push(canvas); } }); - - root.querySelectorAll('*').forEach((elem) => { - if (elem.shadowRoot) { - traverseDom(elem.shadowRoot); - } - }); }; traverseDom(win.document); + for (const item of this.shadowDoms) { + const shadowRoot = item.deref() + if (shadowRoot?.ownerDocument == win.document) { + traverseDom(shadowRoot); + } + } return matchedCanvas; }; const takeCanvasSnapshots = (timestamp: DOMHighResTimeStamp) => { + if (!this.windows.has(win)) { + // exit loop if window no longer in the list + return; + } if ( lastSnapshotTime && timestamp - lastSnapshotTime < timeBetweenSnapshots diff --git a/packages/rrweb/src/record/shadow-dom-manager.ts b/packages/rrweb/src/record/shadow-dom-manager.ts index ed814f49c6..753387fb8d 100644 --- a/packages/rrweb/src/record/shadow-dom-manager.ts +++ b/packages/rrweb/src/record/shadow-dom-manager.ts @@ -74,6 +74,7 @@ export class ShadowDomManager implements ShadowDomManagerInterface { if (!isNativeShadowDom(shadowRoot)) return; if (this.shadowDoms.has(shadowRoot)) return; this.shadowDoms.add(shadowRoot); + this.bypassOptions.canvasManager.addShadowRoot(shadowRoot); const observer = initMutationObserver( { ...this.bypassOptions, @@ -173,5 +174,6 @@ export class ShadowDomManager implements ShadowDomManagerInterface { }); this.restoreHandlers = []; this.shadowDoms = new WeakSet(); + this.bypassOptions.canvasManager.resetShadowRoots(); } } diff --git a/packages/rrweb/test/__snapshots__/integration.test.ts.snap b/packages/rrweb/test/__snapshots__/integration.test.ts.snap index 122fbbc95f..9ddc866942 100644 --- a/packages/rrweb/test/__snapshots__/integration.test.ts.snap +++ b/packages/rrweb/test/__snapshots__/integration.test.ts.snap @@ -6803,331 +6803,6 @@ exports[`record integration tests canvas should record canvas within iframe 1`] ]" `; -exports[`record integration tests canvas should record canvas within iframe with sampling 1`] = ` -"[ - { - \\"type\\": 0, - \\"data\\": {} - }, - { - \\"type\\": 1, - \\"data\\": {} - }, - { - \\"type\\": 4, - \\"data\\": { - \\"href\\": \\"about:blank\\", - \\"width\\": 1920, - \\"height\\": 1080 - } - }, - { - \\"type\\": 2, - \\"data\\": { - \\"node\\": { - \\"type\\": 0, - \\"childNodes\\": [ - { - \\"type\\": 1, - \\"name\\": \\"html\\", - \\"publicId\\": \\"\\", - \\"systemId\\": \\"\\", - \\"id\\": 2 - }, - { - \\"type\\": 2, - \\"tagName\\": \\"html\\", - \\"attributes\\": {}, - \\"childNodes\\": [ - { - \\"type\\": 2, - \\"tagName\\": \\"head\\", - \\"attributes\\": {}, - \\"childNodes\\": [], - \\"id\\": 4 - }, - { - \\"type\\": 2, - \\"tagName\\": \\"body\\", - \\"attributes\\": {}, - \\"childNodes\\": [ - { - \\"type\\": 3, - \\"textContent\\": \\"\\\\n \\", - \\"id\\": 6 - }, - { - \\"type\\": 2, - \\"tagName\\": \\"iframe\\", - \\"attributes\\": { - \\"id\\": \\"iframe-canvas\\", - \\"width\\": \\"100%\\", - \\"height\\": \\"100%\\" - }, - \\"childNodes\\": [], - \\"id\\": 7 - }, - { - \\"type\\": 3, - \\"textContent\\": \\"\\\\n\\\\n \\", - \\"id\\": 8 - }, - { - \\"type\\": 2, - \\"tagName\\": \\"script\\", - \\"attributes\\": {}, - \\"childNodes\\": [ - { - \\"type\\": 3, - \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", - \\"id\\": 10 - } - ], - \\"id\\": 9 - }, - { - \\"type\\": 3, - \\"textContent\\": \\"\\\\n \\\\n \\\\n\\\\n\\", - \\"id\\": 11 - } - ], - \\"id\\": 5 - } - ], - \\"id\\": 3 - } - ], - \\"id\\": 1 - }, - \\"initialOffset\\": { - \\"left\\": 0, - \\"top\\": 0 - } - } - }, - { - \\"type\\": 3, - \\"data\\": { - \\"source\\": 0, - \\"adds\\": [ - { - \\"parentId\\": 7, - \\"nextId\\": null, - \\"node\\": { - \\"type\\": 0, - \\"childNodes\\": [ - { - \\"type\\": 1, - \\"name\\": \\"html\\", - \\"publicId\\": \\"\\", - \\"systemId\\": \\"\\", - \\"rootId\\": 12, - \\"id\\": 13 - }, - { - \\"type\\": 2, - \\"tagName\\": \\"html\\", - \\"attributes\\": { - \\"lang\\": \\"en\\" - }, - \\"childNodes\\": [ - { - \\"type\\": 2, - \\"tagName\\": \\"head\\", - \\"attributes\\": {}, - \\"childNodes\\": [ - { - \\"type\\": 3, - \\"textContent\\": \\"\\\\n \\", - \\"rootId\\": 12, - \\"id\\": 16 - }, - { - \\"type\\": 2, - \\"tagName\\": \\"meta\\", - \\"attributes\\": { - \\"charset\\": \\"UTF-8\\" - }, - \\"childNodes\\": [], - \\"rootId\\": 12, - \\"id\\": 17 - }, - { - \\"type\\": 3, - \\"textContent\\": \\"\\\\n \\", - \\"rootId\\": 12, - \\"id\\": 18 - }, - { - \\"type\\": 2, - \\"tagName\\": \\"meta\\", - \\"attributes\\": { - \\"name\\": \\"viewport\\", - \\"content\\": \\"width=device-width, initial-scale=1.0\\" - }, - \\"childNodes\\": [], - \\"rootId\\": 12, - \\"id\\": 19 - }, - { - \\"type\\": 3, - \\"textContent\\": \\"\\\\n \\", - \\"rootId\\": 12, - \\"id\\": 20 - }, - { - \\"type\\": 2, - \\"tagName\\": \\"title\\", - \\"attributes\\": {}, - \\"childNodes\\": [ - { - \\"type\\": 3, - \\"textContent\\": \\"canvas\\", - \\"rootId\\": 12, - \\"id\\": 22 - } - ], - \\"rootId\\": 12, - \\"id\\": 21 - }, - { - \\"type\\": 3, - \\"textContent\\": \\"\\\\n \\", - \\"rootId\\": 12, - \\"id\\": 23 - } - ], - \\"rootId\\": 12, - \\"id\\": 15 - }, - { - \\"type\\": 3, - \\"textContent\\": \\"\\\\n \\", - \\"rootId\\": 12, - \\"id\\": 24 - }, - { - \\"type\\": 2, - \\"tagName\\": \\"body\\", - \\"attributes\\": {}, - \\"childNodes\\": [ - { - \\"type\\": 3, - \\"textContent\\": \\"\\\\n \\", - \\"rootId\\": 12, - \\"id\\": 26 - }, - { - \\"type\\": 2, - \\"tagName\\": \\"canvas\\", - \\"attributes\\": { - \\"id\\": \\"myCanvas\\", - \\"width\\": \\"200\\", - \\"height\\": \\"100\\", - \\"style\\": \\"border: 1px solid #000000;\\", - \\"rr_dataURL\\": \\"data:image/png;base64,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\\" - }, - \\"childNodes\\": [ - { - \\"type\\": 3, - \\"textContent\\": \\"\\\\n \\", - \\"rootId\\": 12, - \\"id\\": 28 - } - ], - \\"rootId\\": 12, - \\"id\\": 27 - }, - { - \\"type\\": 3, - \\"textContent\\": \\"\\\\n \\", - \\"rootId\\": 12, - \\"id\\": 29 - }, - { - \\"type\\": 2, - \\"tagName\\": \\"script\\", - \\"attributes\\": {}, - \\"childNodes\\": [ - { - \\"type\\": 3, - \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", - \\"rootId\\": 12, - \\"id\\": 31 - } - ], - \\"rootId\\": 12, - \\"id\\": 30 - }, - { - \\"type\\": 3, - \\"textContent\\": \\"\\\\n \\\\n\\\\n\\", - \\"rootId\\": 12, - \\"id\\": 32 - } - ], - \\"rootId\\": 12, - \\"id\\": 25 - } - ], - \\"rootId\\": 12, - \\"id\\": 14 - } - ], - \\"id\\": 12 - } - } - ], - \\"removes\\": [], - \\"texts\\": [], - \\"attributes\\": [], - \\"isAttachIframe\\": true - } - }, - { - \\"type\\": 3, - \\"data\\": { - \\"source\\": 9, - \\"id\\": 27, - \\"type\\": 0, - \\"commands\\": [ - { - \\"property\\": \\"clearRect\\", - \\"args\\": [ - 0, - 0, - 200, - 100 - ] - }, - { - \\"property\\": \\"drawImage\\", - \\"args\\": [ - { - \\"rr_type\\": \\"ImageBitmap\\", - \\"args\\": [ - { - \\"rr_type\\": \\"Blob\\", - \\"data\\": [ - { - \\"rr_type\\": \\"ArrayBuffer\\", - \\"base64\\": \\"base64-0\\" - } - ], - \\"type\\": \\"image/png\\" - } - ] - }, - 0, - 0 - ] - } - ] - } - } -]" -`; - exports[`record integration tests canvas should record canvas within shadow dom 1`] = ` "[ { @@ -7291,7 +6966,17 @@ exports[`record integration tests canvas should record canvas within shadow dom 50, 50 ] - }, + } + ] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 9, + \\"id\\": 8, + \\"type\\": 0, + \\"commands\\": [ { \\"property\\": \\"clearRect\\", \\"args\\": [ diff --git a/packages/rrweb/test/integration.test.ts b/packages/rrweb/test/integration.test.ts index 6fc4a966e1..f3f3ed92b2 100644 --- a/packages/rrweb/test/integration.test.ts +++ b/packages/rrweb/test/integration.test.ts @@ -1112,7 +1112,7 @@ describe('record integration tests', function (this: ISuite) { const frameId = await waitForIFrameLoad(page, '#iframe-canvas'); await frameId.waitForFunction('window.canvasMutationApplied'); await waitForRAF(page); - await page.waitForTimeout(50); + await page.waitForTimeout(1000 / maxFPS); const snapshots = (await page.evaluate( 'window.snapshots', @@ -1126,7 +1126,6 @@ describe('record integration tests', function (this: ISuite) { }), ]), })) - assertSnapshot(stripBase64(snapshots)); }); it('should record canvas within shadow dom', async () => { diff --git a/packages/rrweb/tsconfig.json b/packages/rrweb/tsconfig.json index ea01a9a95c..febd94c328 100644 --- a/packages/rrweb/tsconfig.json +++ b/packages/rrweb/tsconfig.json @@ -11,7 +11,8 @@ "outDir": "build", "lib": [ "es6", - "dom" + "dom", + "ES2021.WeakRef" ], "downlevelIteration": true, "importsNotUsedAsValues": "error", From 7be88f4590ab237cd6e20588ec26640afd92fae5 Mon Sep 17 00:00:00 2001 From: p-mazhnik Date: Tue, 12 Mar 2024 21:01:05 +0000 Subject: [PATCH 07/18] Apply formatting changes --- .../record/observers/canvas/canvas-manager.ts | 8 +- packages/rrweb/test/integration.test.ts | 97 ++++++++++--------- packages/rrweb/test/record/webgl.test.ts | 4 +- 3 files changed, 57 insertions(+), 52 deletions(-) diff --git a/packages/rrweb/src/record/observers/canvas/canvas-manager.ts b/packages/rrweb/src/record/observers/canvas/canvas-manager.ts index 0703f51986..19978def3c 100644 --- a/packages/rrweb/src/record/observers/canvas/canvas-manager.ts +++ b/packages/rrweb/src/record/observers/canvas/canvas-manager.ts @@ -133,11 +133,7 @@ export class CanvasManager implements CanvasManagerInterface { } constructor(options: CanvasManagerConstructorOptions) { - const { - sampling = 'all', - recordCanvas, - errorHandler, - } = options; + const { sampling = 'all', recordCanvas, errorHandler } = options; this.mutationCb = options.mutationCb; this.mirror = options.mirror; this.options = options; @@ -384,7 +380,7 @@ export class CanvasManager implements CanvasManagerInterface { traverseDom(win.document); for (const item of this.shadowDoms) { - const shadowRoot = item.deref() + const shadowRoot = item.deref(); if (shadowRoot?.ownerDocument == win.document) { traverseDom(shadowRoot); } diff --git a/packages/rrweb/test/integration.test.ts b/packages/rrweb/test/integration.test.ts index f3f3ed92b2..a231910cd1 100644 --- a/packages/rrweb/test/integration.test.ts +++ b/packages/rrweb/test/integration.test.ts @@ -10,8 +10,9 @@ import { waitForIFrameLoad, replaceLast, generateRecordSnippet, - ISuite, stripBase64, -} from './utils' + ISuite, + stripBase64, +} from './utils'; import type { recordOptions } from '../src/types'; import { eventWithTime, @@ -19,7 +20,7 @@ import { RecordPlugin, IncrementalSource, CanvasContext, -} from '@sentry-internal/rrweb-types' +} from '@sentry-internal/rrweb-types'; import { visitSnapshot, NodeType } from '@sentry-internal/rrweb-snapshot'; describe('record integration tests', function (this: ISuite) { @@ -1083,20 +1084,22 @@ describe('record integration tests', function (this: ISuite) { const snapshots = (await page.evaluate( 'window.snapshots', )) as eventWithTime[]; - expect(snapshots[snapshots.length - 1].data).toEqual(expect.objectContaining({ - source: IncrementalSource.CanvasMutation, - type: CanvasContext['2D'], - commands: expect.arrayContaining([ - { - args: [200, 100], - property: 'lineTo', - }, - ]), - })) + expect(snapshots[snapshots.length - 1].data).toEqual( + expect.objectContaining({ + source: IncrementalSource.CanvasMutation, + type: CanvasContext['2D'], + commands: expect.arrayContaining([ + { + args: [200, 100], + property: 'lineTo', + }, + ]), + }), + ); assertSnapshot(stripBase64(snapshots)); }); - it ('should record canvas within iframe with sampling', async () => { + it('should record canvas within iframe with sampling', async () => { const maxFPS = 60; const page: puppeteer.Page = await browser.newPage(); await page.goto(`${serverURL}/html`); @@ -1105,7 +1108,7 @@ describe('record integration tests', function (this: ISuite) { recordCanvas: true, sampling: { canvas: maxFPS, - } + }, }), ); @@ -1117,15 +1120,17 @@ describe('record integration tests', function (this: ISuite) { const snapshots = (await page.evaluate( 'window.snapshots', )) as eventWithTime[]; - expect(snapshots[snapshots.length - 1].data).toEqual(expect.objectContaining({ - source: IncrementalSource.CanvasMutation, - type: CanvasContext['2D'], - commands: expect.arrayContaining([ - expect.objectContaining({ - property: 'drawImage', - }), - ]), - })) + expect(snapshots[snapshots.length - 1].data).toEqual( + expect.objectContaining({ + source: IncrementalSource.CanvasMutation, + type: CanvasContext['2D'], + commands: expect.arrayContaining([ + expect.objectContaining({ + property: 'drawImage', + }), + ]), + }), + ); }); it('should record canvas within shadow dom', async () => { @@ -1143,16 +1148,18 @@ describe('record integration tests', function (this: ISuite) { const snapshots = (await page.evaluate( 'window.snapshots', )) as eventWithTime[]; - expect(snapshots[snapshots.length - 1].data).toEqual(expect.objectContaining({ - source: IncrementalSource.CanvasMutation, - type: CanvasContext['2D'], - commands: expect.arrayContaining([ - { - args: [100, 100, 50, 50], - property: 'fillRect', - }, - ]), - })) + expect(snapshots[snapshots.length - 1].data).toEqual( + expect.objectContaining({ + source: IncrementalSource.CanvasMutation, + type: CanvasContext['2D'], + commands: expect.arrayContaining([ + { + args: [100, 100, 50, 50], + property: 'fillRect', + }, + ]), + }), + ); assertSnapshot(stripBase64(snapshots)); }); @@ -1176,18 +1183,20 @@ describe('record integration tests', function (this: ISuite) { const snapshots = (await page.evaluate( 'window.snapshots', )) as eventWithTime[]; - expect(snapshots[snapshots.length - 1].data).toEqual(expect.objectContaining({ - source: IncrementalSource.CanvasMutation, - type: CanvasContext['2D'], - commands: expect.arrayContaining([ - expect.objectContaining({ - property: 'drawImage', - }), - ]), - })) + expect(snapshots[snapshots.length - 1].data).toEqual( + expect.objectContaining({ + source: IncrementalSource.CanvasMutation, + type: CanvasContext['2D'], + commands: expect.arrayContaining([ + expect.objectContaining({ + property: 'drawImage', + }), + ]), + }), + ); assertSnapshot(stripBase64(snapshots)); }); - }) + }); it('should record images with blob url', async () => { const page: puppeteer.Page = await browser.newPage(); diff --git a/packages/rrweb/test/record/webgl.test.ts b/packages/rrweb/test/record/webgl.test.ts index 38c5c2edce..17df2927d4 100644 --- a/packages/rrweb/test/record/webgl.test.ts +++ b/packages/rrweb/test/record/webgl.test.ts @@ -332,7 +332,7 @@ describe('record webgl', function (this: ISuite) { - ` + `, ); it('will record changes to a canvas element', async () => { @@ -368,5 +368,5 @@ describe('record webgl', function (this: ISuite) { }); assertSnapshot(stripBase64(ctx.events)); }); - }) + }); }); From 7de4d41248d53b4c10deba2db5ba674fa0c00fc6 Mon Sep 17 00:00:00 2001 From: p-mazhnik Date: Wed, 13 Mar 2024 00:14:17 +0300 Subject: [PATCH 08/18] cleanup --- packages/rrweb/test/integration.test.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/rrweb/test/integration.test.ts b/packages/rrweb/test/integration.test.ts index f3f3ed92b2..646d9c2ad5 100644 --- a/packages/rrweb/test/integration.test.ts +++ b/packages/rrweb/test/integration.test.ts @@ -10,8 +10,9 @@ import { waitForIFrameLoad, replaceLast, generateRecordSnippet, - ISuite, stripBase64, -} from './utils' + ISuite, + stripBase64, +} from './utils'; import type { recordOptions } from '../src/types'; import { eventWithTime, @@ -19,7 +20,7 @@ import { RecordPlugin, IncrementalSource, CanvasContext, -} from '@sentry-internal/rrweb-types' +} from '@sentry-internal/rrweb-types'; import { visitSnapshot, NodeType } from '@sentry-internal/rrweb-snapshot'; describe('record integration tests', function (this: ISuite) { @@ -1096,7 +1097,7 @@ describe('record integration tests', function (this: ISuite) { assertSnapshot(stripBase64(snapshots)); }); - it ('should record canvas within iframe with sampling', async () => { + it('should record canvas within iframe with sampling', async () => { const maxFPS = 60; const page: puppeteer.Page = await browser.newPage(); await page.goto(`${serverURL}/html`); From d440f8d3cc0845c84e00a692577b59a0f88afe94 Mon Sep 17 00:00:00 2001 From: p-mazhnik Date: Wed, 13 Mar 2024 02:13:52 +0300 Subject: [PATCH 09/18] cleanup --- packages/rrweb/src/record/index.ts | 2 +- packages/rrweb/src/record/observers/canvas/canvas-manager.ts | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/rrweb/src/record/index.ts b/packages/rrweb/src/record/index.ts index 85de8bc8ea..daa49534df 100644 --- a/packages/rrweb/src/record/index.ts +++ b/packages/rrweb/src/record/index.ts @@ -342,6 +342,7 @@ function record( getCanvasManager, { mirror, + win: window, mutationCb: (p: canvasMutationParam) => wrappedEmit( wrapEvent({ @@ -361,7 +362,6 @@ function record( errorHandler, }, ); - canvasManager.addWindow(window); const shadowDomManager: ShadowDomManagerInterface = typeof __RRWEB_EXCLUDE_SHADOW_DOM__ === 'boolean' && diff --git a/packages/rrweb/src/record/observers/canvas/canvas-manager.ts b/packages/rrweb/src/record/observers/canvas/canvas-manager.ts index 19978def3c..008ddea5fc 100644 --- a/packages/rrweb/src/record/observers/canvas/canvas-manager.ts +++ b/packages/rrweb/src/record/observers/canvas/canvas-manager.ts @@ -46,7 +46,7 @@ export interface CanvasManagerConstructorOptions { recordCanvas: boolean; enableManualSnapshot?: boolean; mutationCb: canvasMutationCallback; - // win: IWindow; + win: IWindow; blockClass: blockClass; blockSelector: string | null; unblockSelector: string | null; @@ -133,7 +133,7 @@ export class CanvasManager implements CanvasManagerInterface { } constructor(options: CanvasManagerConstructorOptions) { - const { sampling = 'all', recordCanvas, errorHandler } = options; + const { sampling = 'all', win, recordCanvas, errorHandler } = options this.mutationCb = options.mutationCb; this.mirror = options.mirror; this.options = options; @@ -148,6 +148,7 @@ export class CanvasManager implements CanvasManagerInterface { this.startRAFTimestamping(); this.startPendingCanvasMutationFlusher(); } + this.addWindow(win); } public addWindow(win: IWindow) { From e435c5dcd99d4c56523ef2d5b04d0e12500329ce Mon Sep 17 00:00:00 2001 From: p-mazhnik Date: Tue, 12 Mar 2024 23:14:56 +0000 Subject: [PATCH 10/18] Apply formatting changes --- packages/rrweb/src/record/observers/canvas/canvas-manager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/rrweb/src/record/observers/canvas/canvas-manager.ts b/packages/rrweb/src/record/observers/canvas/canvas-manager.ts index 008ddea5fc..b0807f3902 100644 --- a/packages/rrweb/src/record/observers/canvas/canvas-manager.ts +++ b/packages/rrweb/src/record/observers/canvas/canvas-manager.ts @@ -133,7 +133,7 @@ export class CanvasManager implements CanvasManagerInterface { } constructor(options: CanvasManagerConstructorOptions) { - const { sampling = 'all', win, recordCanvas, errorHandler } = options + const { sampling = 'all', win, recordCanvas, errorHandler } = options; this.mutationCb = options.mutationCb; this.mirror = options.mirror; this.options = options; From 6ade2b466f8a66ea5949a4c62b781d88435e682c Mon Sep 17 00:00:00 2001 From: p-mazhnik Date: Thu, 14 Mar 2024 18:23:06 +0300 Subject: [PATCH 11/18] performance optimizations --- .../src/_image-bitmap-data-url-worker.ts | 10 +- packages/rrweb/src/record/index.ts | 2 +- packages/rrweb/src/record/mutation.ts | 2 +- .../record/observers/canvas/canvas-manager.ts | 189 +++++++++++------- 4 files changed, 122 insertions(+), 81 deletions(-) diff --git a/packages/rrweb-worker/src/_image-bitmap-data-url-worker.ts b/packages/rrweb-worker/src/_image-bitmap-data-url-worker.ts index dd55134a61..2e4a0fc392 100644 --- a/packages/rrweb-worker/src/_image-bitmap-data-url-worker.ts +++ b/packages/rrweb-worker/src/_image-bitmap-data-url-worker.ts @@ -46,8 +46,7 @@ async function getTransparentBlobFor( // `as any` because: https://github.com/Microsoft/TypeScript/issues/20595 const worker: ImageBitmapDataURLResponseWorker = self; -// eslint-disable-next-line @typescript-eslint/no-misused-promises -worker.onmessage = async function (e) { +const processEvent = async function (e: MessageEvent) { if ('OffscreenCanvas' in globalThis) { const { id, bitmap, width, height, dataURLOptions } = e.data; @@ -87,3 +86,10 @@ worker.onmessage = async function (e) { return worker.postMessage({ id: e.data.id }); } }; + +worker.onmessage = function (e) { + // OffscreenCanvas operations sometimes block UI operations in main thread in Chrome, + // https://stackoverflow.com/questions/61475472/does-web-worker-block-on-the-main-thread-for-some-offscreencanvas-functions + // setTimeout(fn, 1) fixes the issue + setTimeout(() => processEvent(e), 1) +} diff --git a/packages/rrweb/src/record/index.ts b/packages/rrweb/src/record/index.ts index daa49534df..9301d198dc 100644 --- a/packages/rrweb/src/record/index.ts +++ b/packages/rrweb/src/record/index.ts @@ -450,10 +450,10 @@ function record( }, onIframeLoad: (iframe, childSn) => { iframeManager.attachIframe(iframe, childSn); - shadowDomManager.observeAttachShadow(iframe); if (iframe.contentWindow) { canvasManager.addWindow(iframe.contentWindow as IWindow); } + shadowDomManager.observeAttachShadow(iframe); }, onStylesheetLoad: (linkEl, childSn) => { stylesheetManager.attachLinkElement(linkEl, childSn); diff --git a/packages/rrweb/src/record/mutation.ts b/packages/rrweb/src/record/mutation.ts index 6b30380f9f..e8b48bddae 100644 --- a/packages/rrweb/src/record/mutation.ts +++ b/packages/rrweb/src/record/mutation.ts @@ -342,10 +342,10 @@ export default class MutationBuffer { }, onIframeLoad: (iframe, childSn) => { this.iframeManager.attachIframe(iframe, childSn); - this.shadowDomManager.observeAttachShadow(iframe); if (iframe.contentWindow) { this.canvasManager.addWindow(iframe.contentWindow as IWindow); } + this.shadowDomManager.observeAttachShadow(iframe); }, onStylesheetLoad: (link, childSn) => { this.stylesheetManager.attachLinkElement(link, childSn); diff --git a/packages/rrweb/src/record/observers/canvas/canvas-manager.ts b/packages/rrweb/src/record/observers/canvas/canvas-manager.ts index b0807f3902..beb23ed161 100644 --- a/packages/rrweb/src/record/observers/canvas/canvas-manager.ts +++ b/packages/rrweb/src/record/observers/canvas/canvas-manager.ts @@ -95,13 +95,17 @@ export class CanvasManager implements CanvasManagerInterface { private mirror: Mirror; private shadowDoms = new Set>(); - private windows = new WeakSet(); + private windowsSet = new WeakSet(); + private windows: WeakRef[] = [] private mutationCb: canvasMutationCallback; private restoreHandlers: listenerHandler[] = []; private frozen = false; private locked = false; + private snapshotInProgressMap: Map = new Map(); + private worker: Worker | null = null; + public reset() { this.pendingCanvasMutations.clear(); this.restoreHandlers.forEach((handler) => { @@ -112,8 +116,15 @@ export class CanvasManager implements CanvasManagerInterface { } }); this.restoreHandlers = []; - this.windows = new WeakSet(); + this.windowsSet = new WeakSet(); + this.windows = []; this.shadowDoms = new Set(); + this.worker?.terminate(); + this.worker = null; + this.snapshotInProgressMap = new Map(); + if ((this.options.recordCanvas && typeof this.options.sampling === 'number') || this.options.enableManualSnapshot) { + this.worker = this.initFPSWorker(); + } } public freeze() { @@ -133,7 +144,16 @@ export class CanvasManager implements CanvasManagerInterface { } constructor(options: CanvasManagerConstructorOptions) { - const { sampling = 'all', win, recordCanvas, errorHandler } = options; + const { + sampling = 'all', + win, + blockClass, + blockSelector, + unblockSelector, + recordCanvas, + dataURLOptions, + errorHandler, + } = options; this.mutationCb = options.mutationCb; this.mirror = options.mirror; this.options = options; @@ -141,13 +161,29 @@ export class CanvasManager implements CanvasManagerInterface { if (errorHandler) { registerErrorHandler(errorHandler); } + if ((recordCanvas && typeof sampling === 'number') || options.enableManualSnapshot) { + this.worker = this.initFPSWorker(); + } if (options.enableManualSnapshot) { return; } - if (recordCanvas && sampling === 'all') { - this.startRAFTimestamping(); - this.startPendingCanvasMutationFlusher(); - } + callbackWrapper(() => { + if (recordCanvas && sampling === 'all') { + this.startRAFTimestamping(); + this.startPendingCanvasMutationFlusher(); + } + if (recordCanvas && typeof sampling === 'number') { + this.initCanvasFPSObserver( + sampling, + blockClass, + blockSelector, + unblockSelector, + { + dataURLOptions, + }, + ); + } + })(); this.addWindow(win); } @@ -158,13 +194,13 @@ export class CanvasManager implements CanvasManagerInterface { blockSelector, unblockSelector, recordCanvas, - dataURLOptions, enableManualSnapshot, } = this.options; - if (this.windows.has(win)) return; - this.windows.add(win); + if (this.windowsSet.has(win)) return; if (enableManualSnapshot) { + this.windowsSet.add(win); + this.windows.push(new WeakRef(win)); return; } @@ -177,18 +213,21 @@ export class CanvasManager implements CanvasManagerInterface { unblockSelector, ); } - if (recordCanvas && typeof sampling === 'number') - this.initCanvasFPSObserver( - sampling, + if (recordCanvas && typeof sampling === 'number') { + const canvasContextReset = initCanvasContextObserver( win, blockClass, blockSelector, unblockSelector, - { - dataURLOptions, - }, + true, ); + this.restoreHandlers.push(() => { + canvasContextReset(); + }); + } })(); + this.windowsSet.add(win); + this.windows.push(new WeakRef(win)); } public addShadowRoot(shadowRoot: ShadowRoot) { @@ -199,6 +238,47 @@ export class CanvasManager implements CanvasManagerInterface { this.shadowDoms = new Set(); } + private initFPSWorker(): Worker { + const worker = new Worker(getImageBitmapDataUrlWorkerURL()); + worker.onmessage = (e) => { + const data = e.data as ImageBitmapDataURLWorkerResponse; + const { id } = data; + this.snapshotInProgressMap.set(id, false); + + if (!('base64' in data)) return; + + const { base64, type, width, height } = data; + this.mutationCb({ + id, + type: CanvasContext['2D'], + commands: [ + { + property: 'clearRect', // wipe canvas + args: [0, 0, width, height], + }, + { + property: 'drawImage', // draws (semi-transparent) image + args: [ + { + rr_type: 'ImageBitmap', + args: [ + { + rr_type: 'Blob', + data: [{ rr_type: 'ArrayBuffer', base64 }], + type, + }, + ], + } as CanvasArg, + 0, + 0, + ], + }, + ], + }); + }; + return worker; + } + private processMutation: canvasManagerMutationCallback = ( target, mutation, @@ -218,7 +298,6 @@ export class CanvasManager implements CanvasManagerInterface { private initCanvasFPSObserver( fps: number, - win: IWindow, blockClass: blockClass, blockSelector: string | null, unblockSelector: string | null, @@ -226,17 +305,9 @@ export class CanvasManager implements CanvasManagerInterface { dataURLOptions: DataURLOptions; }, ) { - const canvasContextReset = initCanvasContextObserver( - win, - blockClass, - blockSelector, - unblockSelector, - true, - ); const rafId = this.takeSnapshot( false, fps, - win, blockClass, blockSelector, unblockSelector, @@ -244,7 +315,6 @@ export class CanvasManager implements CanvasManagerInterface { ); this.restoreHandlers.push(() => { - canvasContextReset(); cancelAnimationFrame(rafId); }); } @@ -291,7 +361,6 @@ export class CanvasManager implements CanvasManagerInterface { const rafId = this.takeSnapshot( true, options.sampling === 'all' ? 2 : options.sampling || 2, - canvasElement?.ownerDocument?.defaultView ?? window, options.blockClass, options.blockSelector, options.unblockSelector, @@ -307,55 +376,12 @@ export class CanvasManager implements CanvasManagerInterface { private takeSnapshot( isManualSnapshot: boolean, fps: number, - win: IWindow, blockClass: blockClass, blockSelector: string | null, unblockSelector: string | null, dataURLOptions: DataURLOptions, canvasElement?: HTMLCanvasElement, ) { - const snapshotInProgressMap: Map = new Map(); - const worker = new Worker(getImageBitmapDataUrlWorkerURL()); - worker.onmessage = (e) => { - if (!this.windows.has(win)) { - return; - } - const data = e.data as ImageBitmapDataURLWorkerResponse; - const { id } = data; - snapshotInProgressMap.set(id, false); - - if (!('base64' in data)) return; - - const { base64, type, width, height } = data; - this.mutationCb({ - id, - type: CanvasContext['2D'], - commands: [ - { - property: 'clearRect', // wipe canvas - args: [0, 0, width, height], - }, - { - property: 'drawImage', // draws (semi-transparent) image - args: [ - { - rr_type: 'ImageBitmap', - args: [ - { - rr_type: 'Blob', - data: [{ rr_type: 'ArrayBuffer', base64 }], - type, - }, - ], - } as CanvasArg, - 0, - 0, - ], - }, - ], - }); - }; - const timeBetweenSnapshots = 1000 / fps; let lastSnapshotTime = 0; let rafId: number; @@ -379,10 +405,16 @@ export class CanvasManager implements CanvasManagerInterface { }); }; - traverseDom(win.document); + for (const item of this.windows) { + const window = item.deref(); + if (window) { + traverseDom(window.document); + } + } + for (const item of this.shadowDoms) { const shadowRoot = item.deref(); - if (shadowRoot?.ownerDocument == win.document) { + if (shadowRoot) { traverseDom(shadowRoot); } } @@ -390,8 +422,8 @@ export class CanvasManager implements CanvasManagerInterface { }; const takeCanvasSnapshots = (timestamp: DOMHighResTimeStamp) => { - if (!this.windows.has(win)) { - // exit loop if window no longer in the list + if (!this.windows.length) { + // exit loop if windows list is empty return; } if ( @@ -404,9 +436,12 @@ export class CanvasManager implements CanvasManagerInterface { lastSnapshotTime = timestamp; getCanvas(canvasElement).forEach((canvas: HTMLCanvasElement) => { + if (!this.mirror.hasNode(canvas)) { + return; + } const id = this.mirror.getId(canvas); - if (snapshotInProgressMap.get(id)) return; - snapshotInProgressMap.set(id, true); + if (this.snapshotInProgressMap.get(id)) return; + this.snapshotInProgressMap.set(id, true); if ( !isManualSnapshot && ['webgl', 'webgl2'].includes((canvas as ICanvas).__context) @@ -435,7 +470,7 @@ export class CanvasManager implements CanvasManagerInterface { createImageBitmap(canvas) .then((bitmap) => { - worker.postMessage( + this.worker?.postMessage( { id, bitmap, From cd056204b6be007b4491020d32efdaf6212d3836 Mon Sep 17 00:00:00 2001 From: p-mazhnik Date: Thu, 14 Mar 2024 15:49:56 +0000 Subject: [PATCH 12/18] Apply formatting changes --- .../src/_image-bitmap-data-url-worker.ts | 8 +++++--- .../src/record/observers/canvas/canvas-manager.ts | 13 ++++++++++--- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/packages/rrweb-worker/src/_image-bitmap-data-url-worker.ts b/packages/rrweb-worker/src/_image-bitmap-data-url-worker.ts index 2e4a0fc392..add63c0fad 100644 --- a/packages/rrweb-worker/src/_image-bitmap-data-url-worker.ts +++ b/packages/rrweb-worker/src/_image-bitmap-data-url-worker.ts @@ -46,7 +46,9 @@ async function getTransparentBlobFor( // `as any` because: https://github.com/Microsoft/TypeScript/issues/20595 const worker: ImageBitmapDataURLResponseWorker = self; -const processEvent = async function (e: MessageEvent) { +const processEvent = async function ( + e: MessageEvent, +) { if ('OffscreenCanvas' in globalThis) { const { id, bitmap, width, height, dataURLOptions } = e.data; @@ -91,5 +93,5 @@ worker.onmessage = function (e) { // OffscreenCanvas operations sometimes block UI operations in main thread in Chrome, // https://stackoverflow.com/questions/61475472/does-web-worker-block-on-the-main-thread-for-some-offscreencanvas-functions // setTimeout(fn, 1) fixes the issue - setTimeout(() => processEvent(e), 1) -} + setTimeout(() => processEvent(e), 1); +}; diff --git a/packages/rrweb/src/record/observers/canvas/canvas-manager.ts b/packages/rrweb/src/record/observers/canvas/canvas-manager.ts index beb23ed161..b50fae5a21 100644 --- a/packages/rrweb/src/record/observers/canvas/canvas-manager.ts +++ b/packages/rrweb/src/record/observers/canvas/canvas-manager.ts @@ -96,7 +96,7 @@ export class CanvasManager implements CanvasManagerInterface { private shadowDoms = new Set>(); private windowsSet = new WeakSet(); - private windows: WeakRef[] = [] + private windows: WeakRef[] = []; private mutationCb: canvasMutationCallback; private restoreHandlers: listenerHandler[] = []; @@ -122,7 +122,11 @@ export class CanvasManager implements CanvasManagerInterface { this.worker?.terminate(); this.worker = null; this.snapshotInProgressMap = new Map(); - if ((this.options.recordCanvas && typeof this.options.sampling === 'number') || this.options.enableManualSnapshot) { + if ( + (this.options.recordCanvas && + typeof this.options.sampling === 'number') || + this.options.enableManualSnapshot + ) { this.worker = this.initFPSWorker(); } } @@ -161,7 +165,10 @@ export class CanvasManager implements CanvasManagerInterface { if (errorHandler) { registerErrorHandler(errorHandler); } - if ((recordCanvas && typeof sampling === 'number') || options.enableManualSnapshot) { + if ( + (recordCanvas && typeof sampling === 'number') || + options.enableManualSnapshot + ) { this.worker = this.initFPSWorker(); } if (options.enableManualSnapshot) { From 995901fd1961273e1c0b70bf598a2a8619168be2 Mon Sep 17 00:00:00 2001 From: p-mazhnik Date: Fri, 15 Mar 2024 03:59:00 +0300 Subject: [PATCH 13/18] revert worker changes --- .../src/_image-bitmap-data-url-worker.ts | 12 ++---------- packages/rrweb/tsconfig.json | 2 +- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/packages/rrweb-worker/src/_image-bitmap-data-url-worker.ts b/packages/rrweb-worker/src/_image-bitmap-data-url-worker.ts index add63c0fad..dd55134a61 100644 --- a/packages/rrweb-worker/src/_image-bitmap-data-url-worker.ts +++ b/packages/rrweb-worker/src/_image-bitmap-data-url-worker.ts @@ -46,9 +46,8 @@ async function getTransparentBlobFor( // `as any` because: https://github.com/Microsoft/TypeScript/issues/20595 const worker: ImageBitmapDataURLResponseWorker = self; -const processEvent = async function ( - e: MessageEvent, -) { +// eslint-disable-next-line @typescript-eslint/no-misused-promises +worker.onmessage = async function (e) { if ('OffscreenCanvas' in globalThis) { const { id, bitmap, width, height, dataURLOptions } = e.data; @@ -88,10 +87,3 @@ const processEvent = async function ( return worker.postMessage({ id: e.data.id }); } }; - -worker.onmessage = function (e) { - // OffscreenCanvas operations sometimes block UI operations in main thread in Chrome, - // https://stackoverflow.com/questions/61475472/does-web-worker-block-on-the-main-thread-for-some-offscreencanvas-functions - // setTimeout(fn, 1) fixes the issue - setTimeout(() => processEvent(e), 1); -}; diff --git a/packages/rrweb/tsconfig.json b/packages/rrweb/tsconfig.json index febd94c328..2091bd1943 100644 --- a/packages/rrweb/tsconfig.json +++ b/packages/rrweb/tsconfig.json @@ -12,7 +12,7 @@ "lib": [ "es6", "dom", - "ES2021.WeakRef" + "es2021.WeakRef" ], "downlevelIteration": true, "importsNotUsedAsValues": "error", From b616c180557e2fd69c3b4ed9e8cbe23337ac102f Mon Sep 17 00:00:00 2001 From: p-mazhnik Date: Fri, 15 Mar 2024 04:22:06 +0300 Subject: [PATCH 14/18] strip base64 from snapshots --- .../rrweb/test/__snapshots__/integration.test.ts.snap | 8 ++++---- packages/rrweb/test/utils.ts | 5 +++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/rrweb/test/__snapshots__/integration.test.ts.snap b/packages/rrweb/test/__snapshots__/integration.test.ts.snap index 9ddc866942..2e80e01fa6 100644 --- a/packages/rrweb/test/__snapshots__/integration.test.ts.snap +++ b/packages/rrweb/test/__snapshots__/integration.test.ts.snap @@ -6869,7 +6869,7 @@ exports[`record integration tests canvas should record canvas within shadow dom \\"attributes\\": { \\"width\\": \\"200\\", \\"height\\": \\"200\\", - \\"rr_dataURL\\": \\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAADICAYAAACtWK6eAAAAAXNSR0IArs4c6QAABd9JREFUeF7t1VFOY1EMRMGwc3YOG4BINFYT0TXfY65cztF7e/hHgMC3Am9sCBD4XkAgfh0EnggIxM+DgED8BghkAr4gmZupEQGBjBzampmAQDI3UyMCAhk5tDUzAYFkbqZGBAQycmhrZgICydxMjQgIZOTQ1swEBJK5mRoREMjIoa2ZCQgkczM1IiCQkUNbMxMQSOZmakRAICOHtmYmIJDMzdSIgEBGDm3NTEAgmZupEQGBjBzampmAQDI3UyMCAhk5tDUzAYFkbqZGBAQycmhrZgICydxMjQgIZOTQ1swEBJK5mRoREMjIoa2ZCQgkczM1IiCQkUNbMxMQSOZmakRAICOHtmYmIJDMzdSIgEBGDm3NTEAgmZupEQGBjBzampmAQDI3UyMCrx/I++PjX9zi/fH61v8C+naJ1z+aQG4v7q/9SEAgP+L6xX/2BfkF3t+NCqRlL5CW9Ok7AjnlfPLHBNKSPn1HIKecAmlxtt4RSEvaF6QlffqOQE45fUFanK13BNKS9gVpSZ++I5BTTl+QFmfrHYG0pH1BWtKn7wjklNMXpMXZekcgLWlfkJb06TsCOeX0BWlxtt4RSEvaF6QlffqOQE45fUFanK13BNKS9gVpSZ++I5BTTl+QFmfrHYG0pH1BWtKn7wjklNMXpMXZekcgLWlfkJb06TsCOeX0BWlxtt4RSEvaF6QlffqOQE45fUFanK13BNKS9gVpSZ++I5BTTl+QFmfrHYG0pH1BWtKn7wjklNMXpMXZekcgLWlfkJb06TsCOeX0BWlxtt4RSEvaF6QlffqOQE45fUFanK13BNKS9gVpSZ++I5BTTl+QFmfrHYG0pH1BWtKn7wjklNMXpMXZekcgLWlfkJb06TsCOeX0BWlxtt4RSEvaF6QlffqOQE45fUFanK13BNKS9gVpSZ++I5BTTl+QFmfrHYG0pH1BWtKn7wjklNMXpMXZekcgLWlfkJb06TsCOeX0BWlxtt4RSEvaF6QlffqOQE45fUFanK13BNKS9gVpSZ++I5BTTl+QFmfrndcPpCXhHQJfCAjEz4LAEwGB+HkQEIjfAIFMwBckczM1IiCQkUNbMxMQSOZmakRAICOHtmYmIJDMzdSIgEBGDm3NTEAgmZupEQGBjBzampmAQDI3UyMCAhk5tDUzAYFkbqZGBAQycmhrZgICydxMjQgIZOTQ1swEBJK5mRoREMjIoa2ZCQgkczM1IiCQkUNbMxMQSOZmakRAICOHtmYmIJDMzdSIgEBGDm3NTEAgmZupEQGBjBzampmAQDI3UyMCAhk5tDUzAYFkbqZGBAQycmhrZgICydxMjQgIZOTQ1swEBJK5mRoREMjIoa2ZCQgkczM1IiCQkUNbMxMQSOZmakRAICOHtmYmIJDMzdSIgEBGDm3NTEAgmZupEQGBjBzampmAQDI3UyMCAhk5tDUzAYFkbqZGBAQycmhrZgICydxMjQgIZOTQ1swEBJK5mRoREMjIoa2ZCQgkczM1IiCQkUNbMxMQSOZmakRAICOHtmYmIJDMzdSIgEBGDm3NTEAgmZupEQGBjBzampmAQDI3UyMCAhk5tDUzAYFkbqZGBAQycmhrZgICydxMjQgIZOTQ1swEBJK5mRoREMjIoa2ZCQgkczM1IiCQkUNbMxMQSOZmakRAICOHtmYmIJDMzdSIgEBGDm3NTEAgmZupEQGBjBzampmAQDI3UyMCAhk5tDUzAYFkbqZGBAQycmhrZgICydxMjQgIZOTQ1swEBJK5mRoREMjIoa2ZCQgkczM1IiCQkUNbMxMQSOZmakRAICOHtmYmIJDMzdSIgEBGDm3NTEAgmZupEQGBjBzampmAQDI3UyMCAhk5tDUzAYFkbqZGBAQycmhrZgICydxMjQgIZOTQ1swEBJK5mRoREMjIoa2ZCQgkczM1IiCQkUNbMxMQSOZmakRAICOHtmYmIJDMzdSIgEBGDm3NTEAgmZupEQGBjBzampmAQDI3UyMCAhk5tDUzgU8uIWTJUfgCOQAAAABJRU5ErkJggg==\\" + \\"rr_dataURL\\": \\"base64-0\\" }, \\"childNodes\\": [], \\"id\\": 8, @@ -7074,7 +7074,7 @@ exports[`record integration tests canvas should record canvas within shadow dom \\"attributes\\": { \\"width\\": \\"200\\", \\"height\\": \\"200\\", - \\"rr_dataURL\\": \\"data:image/png;base64,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\\" + \\"rr_dataURL\\": \\"base64-0\\" }, \\"childNodes\\": [], \\"id\\": 8, @@ -7167,7 +7167,7 @@ exports[`record integration tests canvas should record canvas within shadow dom \\"data\\": [ { \\"rr_type\\": \\"ArrayBuffer\\", - \\"base64\\": \\"base64-0\\" + \\"base64\\": \\"base64-1\\" } ], \\"type\\": \\"image/png\\" @@ -7208,7 +7208,7 @@ exports[`record integration tests canvas should record canvas within shadow dom \\"data\\": [ { \\"rr_type\\": \\"ArrayBuffer\\", - \\"base64\\": \\"base64-1\\" + \\"base64\\": \\"base64-2\\" } ], \\"type\\": \\"image/png\\" diff --git a/packages/rrweb/test/utils.ts b/packages/rrweb/test/utils.ts index c1206d7373..0a5d3fd166 100644 --- a/packages/rrweb/test/utils.ts +++ b/packages/rrweb/test/utils.ts @@ -341,7 +341,7 @@ export function stripBase64(events: eventWithTime[]) { const newObj: Partial = {}; for (const prop in obj) { const value = obj[prop]; - if (prop === 'base64' && typeof value === 'string') { + if ((prop === 'base64' || prop === 'rr_dataURL') && typeof value === 'string') { let index = base64Strings.indexOf(value); if (index === -1) { index = base64Strings.push(value) - 1; @@ -356,11 +356,12 @@ export function stripBase64(events: eventWithTime[]) { return events.map((evt) => { if ( + evt.type === EventType.FullSnapshot || evt.type === EventType.IncrementalSnapshot && evt.data.source === IncrementalSource.CanvasMutation ) { const newData = walk(evt.data); - return { ...evt, data: newData }; + return { ...evt, data: newData } as eventWithTime; } return evt; }); From d4b21f3f755964154d20702dc0c0a14f958b3b6a Mon Sep 17 00:00:00 2001 From: p-mazhnik Date: Fri, 15 Mar 2024 01:28:33 +0000 Subject: [PATCH 15/18] Apply formatting changes --- packages/rrweb/test/utils.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/rrweb/test/utils.ts b/packages/rrweb/test/utils.ts index 0a5d3fd166..a144ba6e09 100644 --- a/packages/rrweb/test/utils.ts +++ b/packages/rrweb/test/utils.ts @@ -341,7 +341,10 @@ export function stripBase64(events: eventWithTime[]) { const newObj: Partial = {}; for (const prop in obj) { const value = obj[prop]; - if ((prop === 'base64' || prop === 'rr_dataURL') && typeof value === 'string') { + if ( + (prop === 'base64' || prop === 'rr_dataURL') && + typeof value === 'string' + ) { let index = base64Strings.indexOf(value); if (index === -1) { index = base64Strings.push(value) - 1; @@ -357,8 +360,8 @@ export function stripBase64(events: eventWithTime[]) { return events.map((evt) => { if ( evt.type === EventType.FullSnapshot || - evt.type === EventType.IncrementalSnapshot && - evt.data.source === IncrementalSource.CanvasMutation + (evt.type === EventType.IncrementalSnapshot && + evt.data.source === IncrementalSource.CanvasMutation) ) { const newData = walk(evt.data); return { ...evt, data: newData } as eventWithTime; From b06055b9628662fdfd8633c97db0e94d23f137a7 Mon Sep 17 00:00:00 2001 From: p-mazhnik Date: Fri, 15 Mar 2024 04:34:48 +0300 Subject: [PATCH 16/18] strip base64 from snapshots --- packages/rrweb/test/__snapshots__/integration.test.ts.snap | 2 +- packages/rrweb/test/utils.ts | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/rrweb/test/__snapshots__/integration.test.ts.snap b/packages/rrweb/test/__snapshots__/integration.test.ts.snap index 2e80e01fa6..c37058a96e 100644 --- a/packages/rrweb/test/__snapshots__/integration.test.ts.snap +++ b/packages/rrweb/test/__snapshots__/integration.test.ts.snap @@ -6713,7 +6713,7 @@ exports[`record integration tests canvas should record canvas within iframe 1`] \\"width\\": \\"200\\", \\"height\\": \\"100\\", \\"style\\": \\"border: 1px solid #000000;\\", - \\"rr_dataURL\\": \\"data:image/png;base64,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\\" + \\"rr_dataURL\\": \\"base64-0\\" }, \\"childNodes\\": [ { diff --git a/packages/rrweb/test/utils.ts b/packages/rrweb/test/utils.ts index a144ba6e09..4ca238b73a 100644 --- a/packages/rrweb/test/utils.ts +++ b/packages/rrweb/test/utils.ts @@ -360,8 +360,7 @@ export function stripBase64(events: eventWithTime[]) { return events.map((evt) => { if ( evt.type === EventType.FullSnapshot || - (evt.type === EventType.IncrementalSnapshot && - evt.data.source === IncrementalSource.CanvasMutation) + evt.type === EventType.IncrementalSnapshot ) { const newData = walk(evt.data); return { ...evt, data: newData } as eventWithTime; From 15d3b3838ecc8d0538421c0f7d9675829c003333 Mon Sep 17 00:00:00 2001 From: p-mazhnik Date: Fri, 15 Mar 2024 13:34:59 +0300 Subject: [PATCH 17/18] cleanup --- .../rrweb/src/record/observers/canvas/canvas-manager.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/rrweb/src/record/observers/canvas/canvas-manager.ts b/packages/rrweb/src/record/observers/canvas/canvas-manager.ts index b50fae5a21..53b4d0b608 100644 --- a/packages/rrweb/src/record/observers/canvas/canvas-manager.ts +++ b/packages/rrweb/src/record/observers/canvas/canvas-manager.ts @@ -402,7 +402,7 @@ export class CanvasManager implements CanvasManagerInterface { const matchedCanvas: HTMLCanvasElement[] = []; - const traverseDom = (root: Document | ShadowRoot) => { + const searchCanvas = (root: Document | ShadowRoot) => { root.querySelectorAll('canvas').forEach((canvas) => { if ( !isBlocked(canvas, blockClass, blockSelector, unblockSelector, true) @@ -415,14 +415,14 @@ export class CanvasManager implements CanvasManagerInterface { for (const item of this.windows) { const window = item.deref(); if (window) { - traverseDom(window.document); + searchCanvas(window.document); } } for (const item of this.shadowDoms) { const shadowRoot = item.deref(); if (shadowRoot) { - traverseDom(shadowRoot); + searchCanvas(shadowRoot); } } return matchedCanvas; From 7816b49e96416da44292223a1ac2f11a9dcbf877 Mon Sep 17 00:00:00 2001 From: Billy Vong Date: Mon, 27 May 2024 17:41:39 -0400 Subject: [PATCH 18/18] update snapshots --- .../rrweb/test/__snapshots__/integration.test.ts.snap | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/rrweb/test/__snapshots__/integration.test.ts.snap b/packages/rrweb/test/__snapshots__/integration.test.ts.snap index 276167f067..5a30ad033f 100644 --- a/packages/rrweb/test/__snapshots__/integration.test.ts.snap +++ b/packages/rrweb/test/__snapshots__/integration.test.ts.snap @@ -7175,7 +7175,9 @@ exports[`record integration tests canvas should record canvas within shadow dom ] }, 0, - 0 + 0, + 200, + 200 ] } ] @@ -7216,7 +7218,9 @@ exports[`record integration tests canvas should record canvas within shadow dom ] }, 0, - 0 + 0, + 200, + 200 ] } ]