diff --git a/.changeset/inlineImage-maybeNot-crossOrigin.md b/.changeset/inlineImage-maybeNot-crossOrigin.md deleted file mode 100644 index 89eb7bb340..0000000000 --- a/.changeset/inlineImage-maybeNot-crossOrigin.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"rrweb": patch -"rrweb-snapshot": patch ---- - -inlineImages: during snapshot avoid adding an event listener for inlining of same-origin images (async listener mutates the snapshot which can be problematic) diff --git a/packages/rrweb-snapshot/src/snapshot.ts b/packages/rrweb-snapshot/src/snapshot.ts index 5a2eaa745b..81dc2133a0 100644 --- a/packages/rrweb-snapshot/src/snapshot.ts +++ b/packages/rrweb-snapshot/src/snapshot.ts @@ -747,9 +747,8 @@ function serializeElementNode( canvasCtx = canvasService.getContext('2d'); } const image = n as HTMLImageElement; - const imageSrc: string = - image.currentSrc || image.getAttribute('src') || ''; - const priorCrossOrigin = image.crossOrigin; + const oldValue = image.crossOrigin; + image.crossOrigin = 'anonymous'; const recordInlineImage = () => { image.removeEventListener('load', recordInlineImage); try { @@ -761,23 +760,13 @@ function serializeElementNode( dataURLOptions.quality, ); } catch (err) { - if (image.crossOrigin !== 'anonymous') { - image.crossOrigin = 'anonymous'; - if (image.complete && image.naturalWidth !== 0) - recordInlineImage(); // too early due to image reload - else image.addEventListener('load', recordInlineImage); - return; - } else { - console.warn( - `Cannot inline img src=${imageSrc}! Error: ${err as string}`, - ); - } - } - if (image.crossOrigin === 'anonymous') { - priorCrossOrigin - ? (attributes.crossOrigin = priorCrossOrigin) - : image.removeAttribute('crossorigin'); + console.warn( + `Cannot inline img src=${image.currentSrc}! Error: ${err as string}`, + ); } + oldValue + ? (attributes.crossOrigin = oldValue) + : image.removeAttribute('crossorigin'); }; // The image content may not have finished loading yet. if (image.complete && image.naturalWidth !== 0) recordInlineImage(); diff --git a/packages/rrweb-snapshot/test/__snapshots__/integration.test.ts.snap b/packages/rrweb-snapshot/test/__snapshots__/integration.test.ts.snap index e95a718645..39c8c49ee1 100644 --- a/packages/rrweb-snapshot/test/__snapshots__/integration.test.ts.snap +++ b/packages/rrweb-snapshot/test/__snapshots__/integration.test.ts.snap @@ -338,7 +338,6 @@ exports[`integration tests [html file]: mask-text.html 1`] = ` exports[`integration tests [html file]: picture.html 1`] = ` " - diff --git a/packages/rrweb-snapshot/test/html/picture.html b/packages/rrweb-snapshot/test/html/picture.html index 2401ca0c61..e005310b77 100644 --- a/packages/rrweb-snapshot/test/html/picture.html +++ b/packages/rrweb-snapshot/test/html/picture.html @@ -1,7 +1,6 @@ - diff --git a/packages/rrweb-snapshot/test/images/rrweb-favicon-20x20.png b/packages/rrweb-snapshot/test/images/rrweb-favicon-20x20.png deleted file mode 100644 index 561f9060d7..0000000000 Binary files a/packages/rrweb-snapshot/test/images/rrweb-favicon-20x20.png and /dev/null differ diff --git a/packages/rrweb-snapshot/test/integration.test.ts b/packages/rrweb-snapshot/test/integration.test.ts index f212b0fd4c..dcc6a3ec0b 100644 --- a/packages/rrweb-snapshot/test/integration.test.ts +++ b/packages/rrweb-snapshot/test/integration.test.ts @@ -6,7 +6,7 @@ import * as puppeteer from 'puppeteer'; import * as rollup from 'rollup'; import * as typescript from 'rollup-plugin-typescript2'; import * as assert from 'assert'; -import { waitForRAF, getServerURL } from './utils'; +import { waitForRAF } from './utils'; const _typescript = typescript as unknown as () => rollup.Plugin; @@ -209,63 +209,12 @@ iframe.contentDocument.querySelector('center').clientHeight inlineImages: true, inlineStylesheet: false })`); - // don't wait, as we want to ensure that the same-origin image can be inlined immediately - const bodyChildren = (await page.evaluate(` - snapshot.childNodes[0].childNodes[1].childNodes.filter((cn) => cn.type === 2); -`)) as any[]; - expect(bodyChildren[1]).toEqual( - expect.objectContaining({ - tagName: 'img', - attributes: { - src: expect.stringMatching(/images\/robot.png$/), - alt: 'This is a robot', - rr_dataURL: expect.stringMatching(/^data:image\/webp;base64,/), - }, - }), - ); - }); - - it('correctly saves cross-origin images offline', async () => { - const page: puppeteer.Page = await browser.newPage(); - - await page.goto('about:blank', { - waitUntil: 'load', - }); - await page.setContent( - ` - - - CORS restricted but has access-control-allow-origin: * - - -`, - { - waitUntil: 'load', - }, - ); - - await page.waitForSelector('img', { timeout: 1000 }); - await page.evaluate(`${code}var snapshot = rrweb.snapshot(document, { - dataURLOptions: { type: "image/webp", quality: 0.8 }, - inlineImages: true, - inlineStylesheet: false - })`); - await waitForRAF(page); // need a small wait, as after the crossOrigin="anonymous" change, the snapshot triggers a reload of the image (after which, the snapshot is mutated) - const bodyChildren = (await page.evaluate(` - snapshot.childNodes[0].childNodes[1].childNodes.filter((cn) => cn.type === 2); -`)) as any[]; - expect(bodyChildren[0]).toEqual( - expect.objectContaining({ - tagName: 'img', - attributes: { - src: getServerURL(server) + '/images/rrweb-favicon-20x20.png', - alt: 'CORS restricted but has access-control-allow-origin: *', - rr_dataURL: expect.stringMatching(/^data:image\/webp;base64,/), - }, - }), - ); + await waitForRAF(page); + const snapshot = (await page.evaluate( + 'JSON.stringify(snapshot, null, 2);', + )) as string; + assert(snapshot.includes('"rr_dataURL"')); + assert(snapshot.includes('data:image/webp;base64,')); }); it('correctly saves blob:images offline', async () => { diff --git a/packages/rrweb-snapshot/test/utils.ts b/packages/rrweb-snapshot/test/utils.ts index 631f8640a6..43d4484bb4 100644 --- a/packages/rrweb-snapshot/test/utils.ts +++ b/packages/rrweb-snapshot/test/utils.ts @@ -1,5 +1,4 @@ import * as puppeteer from 'puppeteer'; -import * as http from 'http'; export async function waitForRAF(page: puppeteer.Page) { return await page.evaluate(() => { @@ -10,12 +9,3 @@ export async function waitForRAF(page: puppeteer.Page) { }); }); } - -export function getServerURL(server: http.Server): string { - const address = server.address(); - if (address && typeof address !== 'string') { - return `http://localhost:${address.port}`; - } else { - return `${address}`; - } -} diff --git a/packages/rrweb/test/__snapshots__/integration.test.ts.snap b/packages/rrweb/test/__snapshots__/integration.test.ts.snap index 1572b675b6..f349bd2669 100644 --- a/packages/rrweb/test/__snapshots__/integration.test.ts.snap +++ b/packages/rrweb/test/__snapshots__/integration.test.ts.snap @@ -12777,6 +12777,40 @@ exports[`record integration tests should record images inside iframe with blob u } ] } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [ + { + \\"id\\": 41, + \\"attributes\\": { + \\"crossorigin\\": \\"anonymous\\" + } + } + ], + \\"removes\\": [], + \\"adds\\": [] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [ + { + \\"id\\": 41, + \\"attributes\\": { + \\"crossorigin\\": null + } + } + ], + \\"removes\\": [], + \\"adds\\": [] + } } ]" `; @@ -13211,6 +13245,40 @@ exports[`record integration tests should record images inside iframe with blob u } ] } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [ + { + \\"id\\": 47, + \\"attributes\\": { + \\"crossorigin\\": \\"anonymous\\" + } + } + ], + \\"removes\\": [], + \\"adds\\": [] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [ + { + \\"id\\": 47, + \\"attributes\\": { + \\"crossorigin\\": null + } + } + ], + \\"removes\\": [], + \\"adds\\": [] + } } ]" `; @@ -13418,6 +13486,40 @@ exports[`record integration tests should record images with blob url 1`] = ` } ] } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [ + { + \\"id\\": 24, + \\"attributes\\": { + \\"crossorigin\\": \\"anonymous\\" + } + } + ], + \\"removes\\": [], + \\"adds\\": [] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [ + { + \\"id\\": 24, + \\"attributes\\": { + \\"crossorigin\\": null + } + } + ], + \\"removes\\": [], + \\"adds\\": [] + } } ]" `;