From fe524b836e643a37c8fc1389c3ca2b4a4459c945 Mon Sep 17 00:00:00 2001 From: Yun Feng Date: Sun, 15 Jan 2023 15:42:05 +1100 Subject: [PATCH 1/3] fix: error data while recording some websites are integrated with stripe These websites will usually have an iframe with src "https://js.stripe.com/v3/m-outer-xxx.html" --- packages/rrweb/src/record/iframe-manager.ts | 36 ++++++++++++--------- packages/rrweb/src/record/index.ts | 1 + packages/rrweb/src/types.ts | 2 ++ 3 files changed, 24 insertions(+), 15 deletions(-) diff --git a/packages/rrweb/src/record/iframe-manager.ts b/packages/rrweb/src/record/iframe-manager.ts index 775aa6500d..58e830304b 100644 --- a/packages/rrweb/src/record/iframe-manager.ts +++ b/packages/rrweb/src/record/iframe-manager.ts @@ -83,24 +83,30 @@ export class IframeManager { ); } private handleMessage(message: MessageEvent | CrossOriginIframeMessageEvent) { - if ((message as CrossOriginIframeMessageEvent).data.type === 'rrweb') { - const iframeSourceWindow = message.source; - if (!iframeSourceWindow) return; + const crossOriginMessageEvent = message as CrossOriginIframeMessageEvent; + if ( + crossOriginMessageEvent.data.type !== 'rrweb' || + // To filter out the rrweb messages which are forwarded by some sites. + crossOriginMessageEvent.origin !== crossOriginMessageEvent.data.origin + ) + return; - const iframeEl = this.crossOriginIframeMap.get(message.source); - if (!iframeEl) return; + const iframeSourceWindow = message.source; + if (!iframeSourceWindow) return; - const transformedEvent = this.transformCrossOriginEvent( - iframeEl, - (message as CrossOriginIframeMessageEvent).data.event, - ); + const iframeEl = this.crossOriginIframeMap.get(message.source); + if (!iframeEl) return; - if (transformedEvent) - this.wrappedEmit( - transformedEvent, - (message as CrossOriginIframeMessageEvent).data.isCheckout, - ); - } + const transformedEvent = this.transformCrossOriginEvent( + iframeEl, + crossOriginMessageEvent.data.event, + ); + + if (transformedEvent) + this.wrappedEmit( + transformedEvent, + crossOriginMessageEvent.data.isCheckout, + ); } private transformCrossOriginEvent( diff --git a/packages/rrweb/src/record/index.ts b/packages/rrweb/src/record/index.ts index acc42acae4..9d194b19a4 100644 --- a/packages/rrweb/src/record/index.ts +++ b/packages/rrweb/src/record/index.ts @@ -190,6 +190,7 @@ function record( const message: CrossOriginIframeMessageEventContent = { type: 'rrweb', event: eventProcessor(e), + origin: window.location.origin, isCheckout, }; window.parent.postMessage(message, '*'); diff --git a/packages/rrweb/src/types.ts b/packages/rrweb/src/types.ts index b204e97407..fb3a300b40 100644 --- a/packages/rrweb/src/types.ts +++ b/packages/rrweb/src/types.ts @@ -200,6 +200,8 @@ declare global { export type CrossOriginIframeMessageEventContent = { type: 'rrweb'; event: T; + // The origin of the iframe which originally emits this message. It is used to check the integrity of message and to filter out the rrweb messages which are forwarded by some sites. + origin: string; isCheckout?: boolean; }; export type CrossOriginIframeMessageEvent = MessageEvent; From bb55679cc5359fa2de116bdc80082d11fb149208 Mon Sep 17 00:00:00 2001 From: Yun Feng Date: Mon, 16 Jan 2023 13:19:05 +1100 Subject: [PATCH 2/3] add test case for the bug --- .../cross-origin-iframes.test.ts.snap | 249 ++++++++++++++++++ .../test/record/cross-origin-iframes.test.ts | 54 +++- packages/rrweb/test/utils.ts | 6 +- 3 files changed, 302 insertions(+), 7 deletions(-) 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 f1c697d34c..829b56fe97 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 @@ -307,6 +307,255 @@ exports[`cross origin iframes audio.html should emit contents of iframe once 1`] ]" `; +exports[`cross origin iframes blank.html should filter out forwarded cross origin rrweb messages 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\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": { + \\"type\\": \\"\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 6 + } + ], + \\"id\\": 5 + } + ], + \\"id\\": 4 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 8 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"iframe\\", + \\"attributes\\": { + \\"rr_src\\": \\"http://localhost:3030/html/blank.html\\" + }, + \\"childNodes\\": [], + \\"id\\": 9 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\\\n \\", + \\"id\\": 10 + } + ], + \\"id\\": 7 + } + ], + \\"id\\": 3 + } + ], + \\"id\\": 1 + }, + \\"initialOffset\\": { + \\"left\\": 0, + \\"top\\": 0 + } + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"adds\\": [ + { + \\"parentId\\": 9, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 0, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"html\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"head\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": { + \\"type\\": \\"\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 15 + } + ], + \\"id\\": 14 + } + ], + \\"id\\": 13 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n\\\\n\\", + \\"id\\": 17 + } + ], + \\"id\\": 16 + } + ], + \\"id\\": 12 + } + ], + \\"compatMode\\": \\"BackCompat\\", + \\"id\\": 11 + } + } + ], + \\"removes\\": [], + \\"texts\\": [], + \\"attributes\\": [], + \\"isAttachIframe\\": true + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [], + \\"removes\\": [], + \\"adds\\": [ + { + \\"parentId\\": 16, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"iframe\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 18 + } + } + ] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"adds\\": [ + { + \\"parentId\\": 18, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 0, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"html\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"head\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": { + \\"type\\": \\"\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 23 + } + ], + \\"id\\": 22 + } + ], + \\"id\\": 21 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n\\\\n\\", + \\"id\\": 25 + } + ], + \\"id\\": 24 + } + ], + \\"id\\": 20 + } + ], + \\"compatMode\\": \\"BackCompat\\", + \\"id\\": 19 + } + } + ], + \\"removes\\": [], + \\"texts\\": [], + \\"attributes\\": [], + \\"isAttachIframe\\": true + } + } +]" +`; + exports[`cross origin iframes form.html should map input events correctly 1`] = ` "[ { diff --git a/packages/rrweb/test/record/cross-origin-iframes.test.ts b/packages/rrweb/test/record/cross-origin-iframes.test.ts index b580ba8a61..1b30e224dc 100644 --- a/packages/rrweb/test/record/cross-origin-iframes.test.ts +++ b/packages/rrweb/test/record/cross-origin-iframes.test.ts @@ -13,7 +13,6 @@ import { getServerURL, launchPuppeteer, startServer, - stripBase64, waitForRAF, } from '../utils'; import type * as http from 'http'; @@ -61,14 +60,17 @@ async function injectRecordScript(frame: puppeteer.Frame) { } const setup = function (this: ISuite, content: string): ISuite { - const ctx = {} as ISuite; + const ctx = {} as ISuite & { + serverB: http.Server; + serverBURL: string; + }; beforeAll(async () => { ctx.browser = await launchPuppeteer(); ctx.server = await startServer(); ctx.serverURL = getServerURL(ctx.server); - // ctx.serverB = await startServer(); - // ctx.serverBURL = getServerURL(ctx.serverB); + ctx.serverB = await startServer(); + ctx.serverBURL = getServerURL(ctx.serverB); const bundlePath = path.resolve(__dirname, '../../dist/rrweb.js'); ctx.code = fs.readFileSync(bundlePath, 'utf8'); @@ -100,7 +102,7 @@ const setup = function (this: ISuite, content: string): ISuite { afterAll(async () => { await ctx.browser.close(); ctx.server.close(); - // ctx.serverB.close(); + ctx.serverB.close(); }); return ctx; @@ -484,6 +486,48 @@ describe('cross origin iframes', function (this: ISuite) { assertSnapshot(snapshots); }); }); + + describe('blank.html', function (this: ISuite) { + const ctx = setup.call( + this, + ` + + + + + + + `, + ) as ISuite & { + serverBURL: string; + }; + + it('should filter out forwarded cross origin rrweb messages', async () => { + const frame = ctx.page.mainFrame().childFrames()[0]; + const iframe2URL = `${ctx.serverBURL}/html/blank.html`; + await frame.evaluate((iframe2URL) => { + // Add a message proxy to forward messages from child frames to its parent frame. + window.addEventListener('message', (event) => { + if (event.source !== window) + window.parent.postMessage(event.data, '*'); + }); + const iframe2 = document.createElement('iframe'); + iframe2.src = iframe2URL; + document.body.appendChild(iframe2); + }, iframe2URL); + + // Wait for iframe2 to load + await ctx.page.waitForFrame(iframe2URL); + // Record iframe2 + await injectRecordScript(frame.childFrames()[0]); + + await waitForRAF(frame.childFrames()[0]); + const snapshots = (await ctx.page.evaluate( + 'window.snapshots', + )) as eventWithTime[]; + assertSnapshot(snapshots); + }); + }); }); describe('same origin iframes', function (this: ISuite) { diff --git a/packages/rrweb/test/utils.ts b/packages/rrweb/test/utils.ts index d4bd396f63..1d9b7a861e 100644 --- a/packages/rrweb/test/utils.ts +++ b/packages/rrweb/test/utils.ts @@ -576,8 +576,10 @@ export const polyfillWebGLGlobals = () => { global.WebGL2RenderingContext = WebGL2RenderingContext as any; }; -export async function waitForRAF(page: puppeteer.Page) { - return await page.evaluate(() => { +export async function waitForRAF( + pageOrFrame: puppeteer.Page | puppeteer.Frame, +) { + return await pageOrFrame.evaluate(() => { return new Promise((resolve) => { requestAnimationFrame(() => { requestAnimationFrame(resolve); From cf50d391536df712156c626bbff50449393dfa8f Mon Sep 17 00:00:00 2001 From: Yun Feng Date: Mon, 16 Jan 2023 16:40:23 +1100 Subject: [PATCH 3/3] fix: recordCrossOriginIframes: true does not work with pack/unpack fn 1. bugfix 2. add test case 3. add rrweb-all.js to the result of command: bundle:browser 4. make puppeteer headless in CI by default to increase the speed and stability --- .github/workflows/ci-cd.yml | 2 +- packages/rrweb/rollup.config.js | 5 + packages/rrweb/src/record/index.ts | 6 +- .../cross-origin-iframes.test.ts.snap | 167 +++++++++++++++++- .../test/record/cross-origin-iframes.test.ts | 72 +++++--- 5 files changed, 227 insertions(+), 25 deletions(-) diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 4a0355b07a..b94c259fcb 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -30,7 +30,7 @@ jobs: run: yarn turbo run check-types - name: Run tests - run: xvfb-run --server-args="-screen 0 1920x1080x24" yarn test + run: PUPPETEER_HEADLESS=true xvfb-run --server-args="-screen 0 1920x1080x24" yarn test - name: Upload diff images to GitHub uses: actions/upload-artifact@v3 diff --git a/packages/rrweb/rollup.config.js b/packages/rrweb/rollup.config.js index 1875ea2064..beb5fc1ee2 100644 --- a/packages/rrweb/rollup.config.js +++ b/packages/rrweb/rollup.config.js @@ -214,6 +214,11 @@ if (process.env.BROWSER_ONLY) { name: 'rrweb', pathFn: (p) => p, }, + { + input: './src/entries/all.ts', + name: 'rrweb', + pathFn: toAllPath, + }, { input: './src/plugins/console/record/index.ts', name: 'rrwebConsoleRecord', diff --git a/packages/rrweb/src/record/index.ts b/packages/rrweb/src/record/index.ts index 9d194b19a4..c28ec28a2c 100644 --- a/packages/rrweb/src/record/index.ts +++ b/packages/rrweb/src/record/index.ts @@ -165,7 +165,11 @@ function record( e = plugin.eventProcessor(e); } } - if (packFn) { + if ( + packFn && + // Disable packing events which will be emitted to parent frames. + !passEmitsToParent + ) { e = (packFn(e) as unknown) as eventWithTime; } return (e as unknown) as T; 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 829b56fe97..da4ff93d25 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 @@ -365,7 +365,7 @@ exports[`cross origin iframes blank.html should filter out forwarded cross origi \\"childNodes\\": [ { \\"type\\": 3, - \\"textContent\\": \\"\\\\n \\", + \\"textContent\\": \\"\\\\n \\", \\"id\\": 8 }, { @@ -379,7 +379,7 @@ exports[`cross origin iframes blank.html should filter out forwarded cross origi }, { \\"type\\": 3, - \\"textContent\\": \\"\\\\n \\\\n \\\\n \\", + \\"textContent\\": \\"\\\\n \\\\n \\\\n \\", \\"id\\": 10 } ], @@ -556,6 +556,169 @@ exports[`cross origin iframes blank.html should filter out forwarded cross origi ]" `; +exports[`cross origin iframes blank.html should support packFn option in record() 1`] = ` +"[ + { + \\"type\\": 4, + \\"data\\": { + \\"href\\": \\"about:blank\\", + \\"width\\": 1920, + \\"height\\": 1080 + }, + \\"v\\": \\"v1\\" + }, + { + \\"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\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": { + \\"type\\": \\"\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 6 + } + ], + \\"id\\": 5 + } + ], + \\"id\\": 4 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 8 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"iframe\\", + \\"attributes\\": { + \\"rr_src\\": \\"http://localhost:3030/html/blank.html\\" + }, + \\"childNodes\\": [], + \\"id\\": 9 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\\\n \\", + \\"id\\": 10 + } + ], + \\"id\\": 7 + } + ], + \\"id\\": 3 + } + ], + \\"id\\": 1 + }, + \\"initialOffset\\": { + \\"left\\": 0, + \\"top\\": 0 + } + }, + \\"v\\": \\"v1\\" + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"adds\\": [ + { + \\"parentId\\": 9, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 0, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"html\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"head\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": { + \\"type\\": \\"\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 15 + } + ], + \\"id\\": 14 + } + ], + \\"id\\": 13 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n\\\\n\\", + \\"id\\": 17 + } + ], + \\"id\\": 16 + } + ], + \\"id\\": 12 + } + ], + \\"compatMode\\": \\"BackCompat\\", + \\"id\\": 11 + } + } + ], + \\"removes\\": [], + \\"texts\\": [], + \\"attributes\\": [], + \\"isAttachIframe\\": true + }, + \\"v\\": \\"v1\\" + } +]" +`; + exports[`cross origin iframes form.html should map input events correctly 1`] = ` "[ { diff --git a/packages/rrweb/test/record/cross-origin-iframes.test.ts b/packages/rrweb/test/record/cross-origin-iframes.test.ts index 1b30e224dc..0ad704e92b 100644 --- a/packages/rrweb/test/record/cross-origin-iframes.test.ts +++ b/packages/rrweb/test/record/cross-origin-iframes.test.ts @@ -15,6 +15,7 @@ import { startServer, waitForRAF, } from '../utils'; +import { unpack } from '../../src/packer/unpack'; import type * as http from 'http'; interface ISuite { @@ -32,34 +33,50 @@ interface IWindow extends Window { options: recordOptions, ) => listenerHandler | undefined; addCustomEvent(tag: string, payload: T): void; + pack: (e: eventWithTime) => string; }; emit: (e: eventWithTime) => undefined; snapshots: eventWithTime[]; } +type ExtraOptions = { + usePackFn?: boolean; +}; -async function injectRecordScript(frame: puppeteer.Frame) { +async function injectRecordScript( + frame: puppeteer.Frame, + options?: ExtraOptions, +) { await frame.addScriptTag({ - path: path.resolve(__dirname, '../../dist/rrweb.js'), + path: path.resolve(__dirname, '../../dist/rrweb-all.js'), }); - await frame.evaluate(() => { + options = options || {}; + await frame.evaluate((options) => { ((window as unknown) as IWindow).snapshots = []; - const { record } = ((window as unknown) as IWindow).rrweb; - record({ + const { record, pack } = ((window as unknown) as IWindow).rrweb; + const config: recordOptions = { recordCrossOriginIframes: true, recordCanvas: true, emit(event) { ((window as unknown) as IWindow).snapshots.push(event); ((window as unknown) as IWindow).emit(event); }, - }); - }); + }; + if (options.usePackFn) { + config.packFn = pack; + } + record(config); + }, options); for (const child of frame.childFrames()) { - await injectRecordScript(child); + await injectRecordScript(child, options); } } -const setup = function (this: ISuite, content: string): ISuite { +const setup = function ( + this: ISuite, + content: string, + options?: ExtraOptions, +): ISuite { const ctx = {} as ISuite & { serverB: http.Server; serverBURL: string; @@ -92,7 +109,7 @@ const setup = function (this: ISuite, content: string): ISuite { }); ctx.page.on('console', (msg) => console.log('PAGE LOG:', msg.text())); - await injectRecordScript(ctx.page.mainFrame()); + await injectRecordScript(ctx.page.mainFrame(), options); }); afterEach(async () => { @@ -488,17 +505,15 @@ describe('cross origin iframes', function (this: ISuite) { }); describe('blank.html', function (this: ISuite) { - const ctx = setup.call( - this, - ` - - - - - - - `, - ) as ISuite & { + const content = ` + + + + + + + `; + const ctx = setup.call(this, content) as ISuite & { serverBURL: string; }; @@ -527,6 +542,21 @@ describe('cross origin iframes', function (this: ISuite) { )) as eventWithTime[]; assertSnapshot(snapshots); }); + + describe('should support packFn option in record()', () => { + const ctx = setup.call(this, content, { usePackFn: true }); + it('', async () => { + const frame = ctx.page.mainFrame().childFrames()[0]; + await waitForRAF(frame); + const packedSnapshots = (await ctx.page.evaluate( + 'window.snapshots', + )) as string[]; + const unpackedSnapshots = packedSnapshots.map((packed) => + unpack(packed), + ) as eventWithTime[]; + assertSnapshot(unpackedSnapshots); + }); + }); }); });