From 201fe43e38cbd3ede3e459646a6cf3f0c2a8086e Mon Sep 17 00:00:00 2001 From: Eoghan Murray Date: Wed, 13 May 2026 16:53:09 +0100 Subject: [PATCH 1/6] Fix problem where without canvas recording, a element can be rendered without dimensions, leading to layout issues if the element is e.g. the only positioned element in container. See lazysizes-1.3.2.js --- packages/rrweb-snapshot/package.json | 2 + packages/rrweb-snapshot/src/rebuild.ts | 3 + packages/rrweb-snapshot/src/snapshot.ts | 14 ++++- .../canvas-layout-rebuilt.png | Bin 0 -> 16565 bytes .../__snapshots__/integration.test.ts.snap | 9 +++ .../test/html/canvas-layout.html | 8 +++ .../rrweb-snapshot/test/integration.test.ts | 58 ++++++++++++++++++ 7 files changed, 91 insertions(+), 3 deletions(-) create mode 100644 packages/rrweb-snapshot/test/__image_snapshots__/canvas-layout-rebuilt.png create mode 100644 packages/rrweb-snapshot/test/html/canvas-layout.html diff --git a/packages/rrweb-snapshot/package.json b/packages/rrweb-snapshot/package.json index 39e4b35e64..a9175fa763 100644 --- a/packages/rrweb-snapshot/package.json +++ b/packages/rrweb-snapshot/package.json @@ -55,6 +55,7 @@ }, "homepage": "https://github.com/rrweb-io/rrweb/tree/master/packages/rrweb-snapshot#readme", "devDependencies": { + "@types/jest-image-snapshot": "^6.1.0", "@rrweb/types": "^2.0.0-alpha.20", "@rrweb/utils": "^2.0.0-alpha.20", "@types/jsdom": "^20.0.0", @@ -66,6 +67,7 @@ "typescript": "^5.4.5", "vite": "^6.0.1", "vite-plugin-dts": "^3.9.1", + "jest-image-snapshot": "^6.2.0", "vitest": "^1.4.0" }, "dependencies": { diff --git a/packages/rrweb-snapshot/src/rebuild.ts b/packages/rrweb-snapshot/src/rebuild.ts index 692da2d281..0159df7492 100644 --- a/packages/rrweb-snapshot/src/rebuild.ts +++ b/packages/rrweb-snapshot/src/rebuild.ts @@ -366,6 +366,9 @@ function buildNode( if (name === 'rr_width') { (node as HTMLElement).style.setProperty('width', value.toString()); + if (tagName === 'canvas') { + (node as HTMLElement).style.setProperty('display', 'block'); + } } else if (name === 'rr_height') { (node as HTMLElement).style.setProperty('height', value.toString()); } else if ( diff --git a/packages/rrweb-snapshot/src/snapshot.ts b/packages/rrweb-snapshot/src/snapshot.ts index b5426a1751..badff9d465 100644 --- a/packages/rrweb-snapshot/src/snapshot.ts +++ b/packages/rrweb-snapshot/src/snapshot.ts @@ -655,8 +655,8 @@ function serializeElementNode( } // canvas image data - if (tagName === 'canvas' && recordCanvas) { - if ((n as ICanvas).__context === '2d') { + if (tagName === 'canvas') { + if (recordCanvas && (n as ICanvas).__context === '2d') { // only record this on 2d canvas if (!is2DCanvasBlank(n as HTMLCanvasElement)) { attributes.rr_dataURL = (n as HTMLCanvasElement).toDataURL( @@ -664,7 +664,7 @@ function serializeElementNode( dataURLOptions.quality, ); } - } else if (!('__context' in n)) { + } else if (recordCanvas && !('__context' in n)) { // context is unknown, better not call getContext to trigger it const canvasDataURL = (n as HTMLCanvasElement).toDataURL( dataURLOptions.type, @@ -684,6 +684,14 @@ function serializeElementNode( if (canvasDataURL !== blankCanvasDataURL) { attributes.rr_dataURL = canvasDataURL; } + } else { + // explicitly record dimensions in case the unrecorded canvas affects layout; + // normally with UNSAFE_replayCanvas during replay the `canvas.getContext()` + // initialization will force browser to commit to the canvas's intrisic size + // as layout size + const { width, height } = n.getBoundingClientRect(); + attributes.rr_width = `${width}px`; + attributes.rr_height = `${height}px`; } } // save image offline diff --git a/packages/rrweb-snapshot/test/__image_snapshots__/canvas-layout-rebuilt.png b/packages/rrweb-snapshot/test/__image_snapshots__/canvas-layout-rebuilt.png new file mode 100644 index 0000000000000000000000000000000000000000..1e2f5a28438dc9f2e6464430e3f09033fed91df9 GIT binary patch literal 16565 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 [html file]: block-element.html 1`] = ` " `; +exports[`integration tests > [html file]: canvas-layout.html 1`] = ` +" +
+ \\"This + +
+ " +`; + exports[`integration tests > [html file]: compat-mode.html 1`] = ` " Compat Mode; image resizing diff --git a/packages/rrweb-snapshot/test/html/canvas-layout.html b/packages/rrweb-snapshot/test/html/canvas-layout.html new file mode 100644 index 0000000000..68b427c893 --- /dev/null +++ b/packages/rrweb-snapshot/test/html/canvas-layout.html @@ -0,0 +1,8 @@ + + +
+ This is a robot + +
+ + diff --git a/packages/rrweb-snapshot/test/integration.test.ts b/packages/rrweb-snapshot/test/integration.test.ts index 1cc6acffea..ebbac2e92e 100644 --- a/packages/rrweb-snapshot/test/integration.test.ts +++ b/packages/rrweb-snapshot/test/integration.test.ts @@ -14,6 +14,7 @@ import { vi, } from 'vitest'; +import { toMatchImageSnapshot } from 'jest-image-snapshot'; import { getServerURL, waitForRAF } from './utils'; const htmlFolder = path.join(__dirname, 'html'); @@ -559,3 +560,60 @@ describe('shadow DOM integration tests', function (this: ISuite) { await assertSnapshot(snapshotResult); }); }); + +describe('snapshot/rebuild image tests', function (this: ISuite) { + expect.extend({ toMatchImageSnapshot }); + vi.setConfig({ testTimeout: 30_000 }); + let server: ISuite['server']; + let serverURL: ISuite['serverURL']; + let browser: ISuite['browser']; + let code: ISuite['code']; + + beforeAll(async () => { + server = await startServer(); + serverURL = getServerURL(server); + browser = await puppeteer.launch({ + args: ['--no-sandbox', '--disable-setuid-sandbox'], + }); + code = fs.readFileSync( + path.resolve(__dirname, '../dist/rrweb-snapshot.umd.cjs'), + 'utf-8', + ); + }); + + afterAll(async () => { + await browser.close(); + await server.close(); + }); + + it('canvas dimensions are preserved after rebuild', async () => { + const page: puppeteer.Page = await browser.newPage(); + page.on('console', (msg) => console.log(msg.text())); + + // background-grey should extend to full rhs if
is dimensioned properly + await page.goto(`${serverURL}/html/canvas-layout.html`, { + waitUntil: 'load', + }); + await waitForRAF(page); + + await page.evaluate(`${code} + const snap = rrwebSnapshot.snapshot(document); + const iframe = document.createElement('iframe'); + iframe.id = 'rebuild-iframe'; + iframe.setAttribute('width', document.body.clientWidth); + iframe.setAttribute('height', document.body.clientHeight); + iframe.setAttribute('sandbox', 'allow-same-origin'); // apply other restrictions including !allow-scripts + document.body.appendChild(iframe); + rrwebSnapshot.rebuild(snap, { doc: iframe.contentDocument }); + `); + await waitForRAF(page); + + const iframeElement = await page.$('#rebuild-iframe'); + const rebuildImage = await iframeElement!.screenshot(); + expect(rebuildImage).toMatchImageSnapshot({ + customSnapshotIdentifier: 'canvas-layout-rebuilt', + failureThreshold: 0.05, + failureThresholdType: 'percent', + }); + }); +}); From 01979634fa7ced7cf66f38fc6962d687710a89e6 Mon Sep 17 00:00:00 2001 From: Eoghan Murray Date: Thu, 14 May 2026 13:38:27 +0100 Subject: [PATCH 2/6] Add changeset --- .changeset/canvas-layout-dimensions.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/canvas-layout-dimensions.md diff --git a/.changeset/canvas-layout-dimensions.md b/.changeset/canvas-layout-dimensions.md new file mode 100644 index 0000000000..c2fc5a5913 --- /dev/null +++ b/.changeset/canvas-layout-dimensions.md @@ -0,0 +1,6 @@ +---- +"rrweb-snapshot": patch +"rrweb": patch +---- + +Fix problem where without `recordCanvas: true`, a canvas element is replayed without dimensions, potentially affecting layout From 58c1d675623dccb95998e367ec637bfa5ad3778e Mon Sep 17 00:00:00 2001 From: eoghanmurray Date: Thu, 14 May 2026 12:40:59 +0000 Subject: [PATCH 3/6] Apply formatting changes --- .changeset/canvas-layout-dimensions.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.changeset/canvas-layout-dimensions.md b/.changeset/canvas-layout-dimensions.md index c2fc5a5913..64e04b5918 100644 --- a/.changeset/canvas-layout-dimensions.md +++ b/.changeset/canvas-layout-dimensions.md @@ -1,6 +1,8 @@ ----- +--- + "rrweb-snapshot": patch "rrweb": patch ----- + +--- Fix problem where without `recordCanvas: true`, a canvas element is replayed without dimensions, potentially affecting layout From ddb1cd918196c8b6e49b4d6ca7eb447e82136b81 Mon Sep 17 00:00:00 2001 From: Eoghan Murray Date: Thu, 14 May 2026 13:42:57 +0100 Subject: [PATCH 4/6] Fixup markdown --- .changeset/canvas-layout-dimensions.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/.changeset/canvas-layout-dimensions.md b/.changeset/canvas-layout-dimensions.md index 64e04b5918..4dc85a4fe1 100644 --- a/.changeset/canvas-layout-dimensions.md +++ b/.changeset/canvas-layout-dimensions.md @@ -1,8 +1,6 @@ --- - "rrweb-snapshot": patch "rrweb": patch - --- Fix problem where without `recordCanvas: true`, a canvas element is replayed without dimensions, potentially affecting layout From dbc4a8a3ef6acf8117ae536e0032236dadd358fd Mon Sep 17 00:00:00 2001 From: Eoghan Murray Date: Thu, 14 May 2026 16:56:20 +0100 Subject: [PATCH 5/6] Forgot to update tests in rrweb package --- .../__snapshots__/integration.test.ts.snap | 35 +++++++++++++++---- .../cross-origin-iframes.test.ts.snap | 5 ++- .../record/__snapshots__/webgl.test.ts.snap | 10 ++++-- 3 files changed, 40 insertions(+), 10 deletions(-) diff --git a/packages/rrweb/test/__snapshots__/integration.test.ts.snap b/packages/rrweb/test/__snapshots__/integration.test.ts.snap index 738f2fe8e2..c199012c9a 100644 --- a/packages/rrweb/test/__snapshots__/integration.test.ts.snap +++ b/packages/rrweb/test/__snapshots__/integration.test.ts.snap @@ -698,7 +698,10 @@ exports[`record integration tests > can mask character data mutations 1`] = ` { \\"type\\": 2, \\"tagName\\": \\"canvas\\", - \\"attributes\\": {}, + \\"attributes\\": { + \\"rr_width\\": \\"300px\\", + \\"rr_height\\": \\"150px\\" + }, \\"childNodes\\": [], \\"id\\": 15 }, @@ -916,7 +919,10 @@ exports[`record integration tests > can mask character data mutations with regex { \\"type\\": 2, \\"tagName\\": \\"canvas\\", - \\"attributes\\": {}, + \\"attributes\\": { + \\"rr_width\\": \\"300px\\", + \\"rr_height\\": \\"150px\\" + }, \\"childNodes\\": [], \\"id\\": 15 }, @@ -1803,7 +1809,10 @@ exports[`record integration tests > can record attribute mutation 1`] = ` { \\"type\\": 2, \\"tagName\\": \\"canvas\\", - \\"attributes\\": {}, + \\"attributes\\": { + \\"rr_width\\": \\"300px\\", + \\"rr_height\\": \\"150px\\" + }, \\"childNodes\\": [], \\"id\\": 15 }, @@ -1974,7 +1983,10 @@ exports[`record integration tests > can record character data muatations 1`] = ` { \\"type\\": 2, \\"tagName\\": \\"canvas\\", - \\"attributes\\": {}, + \\"attributes\\": { + \\"rr_width\\": \\"300px\\", + \\"rr_height\\": \\"150px\\" + }, \\"childNodes\\": [], \\"id\\": 15 }, @@ -2152,7 +2164,10 @@ exports[`record integration tests > can record childList mutations 1`] = ` { \\"type\\": 2, \\"tagName\\": \\"canvas\\", - \\"attributes\\": {}, + \\"attributes\\": { + \\"rr_width\\": \\"300px\\", + \\"rr_height\\": \\"150px\\" + }, \\"childNodes\\": [], \\"id\\": 15 }, @@ -5470,7 +5485,10 @@ exports[`record integration tests > handles null attribute values 1`] = ` { \\"type\\": 2, \\"tagName\\": \\"canvas\\", - \\"attributes\\": {}, + \\"attributes\\": { + \\"rr_width\\": \\"300px\\", + \\"rr_height\\": \\"150px\\" + }, \\"childNodes\\": [], \\"id\\": 15 }, @@ -17415,7 +17433,10 @@ exports[`record integration tests > will serialize node before record 1`] = ` { \\"type\\": 2, \\"tagName\\": \\"canvas\\", - \\"attributes\\": {}, + \\"attributes\\": { + \\"rr_width\\": \\"300px\\", + \\"rr_height\\": \\"150px\\" + }, \\"childNodes\\": [], \\"id\\": 15 }, diff --git a/packages/rrweb/test/record/__snapshots__/cross-origin-iframes.test.ts.snap b/packages/rrweb/test/record/__snapshots__/cross-origin-iframes.test.ts.snap index 8cd9fce959..f986d78ce1 100644 --- a/packages/rrweb/test/record/__snapshots__/cross-origin-iframes.test.ts.snap +++ b/packages/rrweb/test/record/__snapshots__/cross-origin-iframes.test.ts.snap @@ -4873,7 +4873,10 @@ exports[`cross origin iframes > move-node.html > should record canvas elements 1 \\"node\\": { \\"type\\": 2, \\"tagName\\": \\"canvas\\", - \\"attributes\\": {}, + \\"attributes\\": { + \\"rr_width\\": \\"300px\\", + \\"rr_height\\": \\"150px\\" + }, \\"childNodes\\": [], \\"rootId\\": 11, \\"id\\": 33 diff --git a/packages/rrweb/test/record/__snapshots__/webgl.test.ts.snap b/packages/rrweb/test/record/__snapshots__/webgl.test.ts.snap index 8a9b0c1fc2..2b4609374e 100644 --- a/packages/rrweb/test/record/__snapshots__/webgl.test.ts.snap +++ b/packages/rrweb/test/record/__snapshots__/webgl.test.ts.snap @@ -467,7 +467,10 @@ exports[`record webgl > will record changes to a canvas element before the canva \\"node\\": { \\"type\\": 2, \\"tagName\\": \\"canvas\\", - \\"attributes\\": {}, + \\"attributes\\": { + \\"rr_width\\": \\"300px\\", + \\"rr_height\\": \\"150px\\" + }, \\"childNodes\\": [], \\"id\\": 11 } @@ -611,7 +614,10 @@ exports[`record webgl > will record changes to a canvas element before the canva \\"node\\": { \\"type\\": 2, \\"tagName\\": \\"canvas\\", - \\"attributes\\": {}, + \\"attributes\\": { + \\"rr_width\\": \\"300px\\", + \\"rr_height\\": \\"150px\\" + }, \\"childNodes\\": [], \\"id\\": 11 } From 94feee4d42b8481797725bd0828c3a4baf575acc Mon Sep 17 00:00:00 2001 From: Eoghan Murray Date: Wed, 20 May 2026 17:03:23 +0100 Subject: [PATCH 6/6] Fixup test after security restrictions imposed in #1834 --- packages/rrweb-snapshot/test/integration.test.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/rrweb-snapshot/test/integration.test.ts b/packages/rrweb-snapshot/test/integration.test.ts index c685d3591b..d64e3df29e 100644 --- a/packages/rrweb-snapshot/test/integration.test.ts +++ b/packages/rrweb-snapshot/test/integration.test.ts @@ -599,13 +599,14 @@ describe('snapshot/rebuild image tests', function (this: ISuite) { await page.evaluate(`${code} const snap = rrwebSnapshot.snapshot(document); - const iframe = document.createElement('iframe'); + const { iframe, node } = rrwebSnapshot.rebuildIntoSandboxedIframe( + snap, { + root: document.body, + } + ); iframe.id = 'rebuild-iframe'; iframe.setAttribute('width', document.body.clientWidth); iframe.setAttribute('height', document.body.clientHeight); - iframe.setAttribute('sandbox', 'allow-same-origin'); // apply other restrictions including !allow-scripts - document.body.appendChild(iframe); - rrwebSnapshot.rebuild(snap, { doc: iframe.contentDocument }); `); await waitForRAF(page);