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/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..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; @@ -190,6 +194,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; 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..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 @@ -307,6 +307,418 @@ 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 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 b580ba8a61..0ad704e92b 100644 --- a/packages/rrweb/test/record/cross-origin-iframes.test.ts +++ b/packages/rrweb/test/record/cross-origin-iframes.test.ts @@ -13,9 +13,9 @@ import { getServerURL, launchPuppeteer, startServer, - stripBase64, waitForRAF, } from '../utils'; +import { unpack } from '../../src/packer/unpack'; import type * as http from 'http'; interface ISuite { @@ -33,42 +33,61 @@ 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 ctx = {} as ISuite; +const setup = function ( + this: ISuite, + content: string, + options?: ExtraOptions, +): 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'); @@ -90,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 () => { @@ -100,7 +119,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 +503,61 @@ describe('cross origin iframes', function (this: ISuite) { assertSnapshot(snapshots); }); }); + + describe('blank.html', function (this: ISuite) { + const content = ` + + + + + + + `; + const ctx = setup.call(this, content) 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('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); + }); + }); + }); }); 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);