From 780b8718350f0e4350d406eaa5e659094b05eb0b Mon Sep 17 00:00:00 2001 From: Eoghan Murray Date: Wed, 29 Sep 2021 17:16:38 +0100 Subject: [PATCH 1/6] If rrweb is loaded within an iframe, break into the top frame and start recording there --- packages/rrweb/src/record/index.ts | 41 +++-- packages/rrweb/src/utils.ts | 30 +++- .../__snapshots__/integration.test.ts.snap | 2 +- .../test/__snapshots__/record.test.ts.snap | 166 ++++++++++++++++++ packages/rrweb/test/record.test.ts | 76 ++++++++ packages/rrweb/test/utils.ts | 4 +- packages/rrweb/typings/utils.d.ts | 1 + 7 files changed, 292 insertions(+), 28 deletions(-) diff --git a/packages/rrweb/src/record/index.ts b/packages/rrweb/src/record/index.ts index 5930913a8a..289ae9a34c 100644 --- a/packages/rrweb/src/record/index.ts +++ b/packages/rrweb/src/record/index.ts @@ -2,6 +2,7 @@ import { snapshot, MaskInputOptions, SlimDOMOptions } from 'rrweb-snapshot'; import { initObservers, mutationBuffers } from './observer'; import { on, + getTopWindow, getWindowWidth, getWindowHeight, polyfill, @@ -66,6 +67,10 @@ function record( if (!emit) { throw new Error('emit function is required'); } + + const twindow = getTopWindow(); + const tdoc = twindow.document; + // move departed options to new options if (mousemoveWait !== undefined && sampling.mousemove === undefined) { sampling.mousemove = mousemoveWait; @@ -209,7 +214,7 @@ function record( wrapEvent({ type: EventType.Meta, data: { - href: window.location.href, + href: twindow.location.href, width: getWindowWidth(), height: getWindowHeight(), }, @@ -218,7 +223,7 @@ function record( ); mutationBuffers.forEach((buf) => buf.lock()); // don't allow any mirror modifications during snapshotting - const [node, idNodeMap] = snapshot(document, { + const [node, idNodeMap] = snapshot(tdoc, { blockClass, blockSelector, maskTextClass, @@ -233,7 +238,7 @@ function record( iframeManager.addIframe(n); } if (hasShadowRoot(n)) { - shadowDomManager.addShadowRoot(n.shadowRoot, document); + shadowDomManager.addShadowRoot(n.shadowRoot, tdoc); } }, onIframeLoad: (iframe, childSn) => { @@ -254,18 +259,18 @@ function record( node, initialOffset: { left: - window.pageXOffset !== undefined - ? window.pageXOffset - : document?.documentElement.scrollLeft || - document?.body?.parentElement?.scrollLeft || - document?.body.scrollLeft || + twindow.pageXOffset !== undefined + ? twindow.pageXOffset + : tdoc?.documentElement.scrollLeft || + tdoc?.body?.parentElement?.scrollLeft || + tdoc?.body.scrollLeft || 0, top: - window.pageYOffset !== undefined - ? window.pageYOffset - : document?.documentElement.scrollTop || - document?.body?.parentElement?.scrollTop || - document?.body.scrollTop || + twindow.pageYOffset !== undefined + ? twindow.pageYOffset + : tdoc?.documentElement.scrollTop || + tdoc?.body?.parentElement?.scrollTop || + tdoc?.body.scrollTop || 0, }, }, @@ -284,7 +289,7 @@ function record( data: {}, }), ); - }), + }, tdoc), ); const observe = (doc: Document) => { @@ -426,11 +431,11 @@ function record( const init = () => { takeFullSnapshot(); - handlers.push(observe(document)); + handlers.push(observe(tdoc)); }; if ( - document.readyState === 'interactive' || - document.readyState === 'complete' + tdoc.readyState === 'interactive' || + tdoc.readyState === 'complete' ) { init(); } else { @@ -446,7 +451,7 @@ function record( ); init(); }, - window, + twindow, ), ); } diff --git a/packages/rrweb/src/utils.ts b/packages/rrweb/src/utils.ts index 8ae1635fe6..3b5def2769 100644 --- a/packages/rrweb/src/utils.ts +++ b/packages/rrweb/src/utils.ts @@ -207,19 +207,37 @@ export function patch( } } +export function getTopWindow(): Window { + let twindow: Window = window; + while (twindow.parent && twindow.parent != twindow) { + // check each parent in case the window.top.document fails, but an intermediary one would succeed + try { + let tdoc = twindow.parent.document; // this can fail apparently: https://stackoverflow.com/questions/2937118 + twindow = twindow.parent as Window; + } catch (err) { + break; + } + } + return twindow; +} + export function getWindowHeight(): number { + const twindow = getTopWindow(); + const tdoc = twindow.document; return ( - window.innerHeight || - (document.documentElement && document.documentElement.clientHeight) || - (document.body && document.body.clientHeight) + twindow.innerHeight || + (tdoc.documentElement && tdoc.documentElement.clientHeight) || + (tdoc.body && tdoc.body.clientHeight) ); } export function getWindowWidth(): number { + const twindow = getTopWindow(); + const tdoc = twindow.document; return ( - window.innerWidth || - (document.documentElement && document.documentElement.clientWidth) || - (document.body && document.body.clientWidth) + twindow.innerWidth || + (tdoc.documentElement && tdoc.documentElement.clientWidth) || + (tdoc.body && tdoc.body.clientWidth) ); } diff --git a/packages/rrweb/test/__snapshots__/integration.test.ts.snap b/packages/rrweb/test/__snapshots__/integration.test.ts.snap index 33a448051f..38486480ae 100644 --- a/packages/rrweb/test/__snapshots__/integration.test.ts.snap +++ b/packages/rrweb/test/__snapshots__/integration.test.ts.snap @@ -2024,7 +2024,7 @@ exports[`iframe 1`] = ` { \\"type\\": 4, \\"data\\": { - \\"href\\": \\"about:blank\\", + \\"href\\": \\"http://localhost:3030/html\\", \\"width\\": 1920, \\"height\\": 1080 } diff --git a/packages/rrweb/test/__snapshots__/record.test.ts.snap b/packages/rrweb/test/__snapshots__/record.test.ts.snap index 23b50a5615..8c68fe9a37 100644 --- a/packages/rrweb/test/__snapshots__/record.test.ts.snap +++ b/packages/rrweb/test/__snapshots__/record.test.ts.snap @@ -557,6 +557,172 @@ exports[`iframe-stylesheet-mutations 1`] = ` ]" `; +exports[`loaded-from-iframe 1`] = ` +"[ + { + \\"type\\": 4, + \\"data\\": { + \\"href\\": \\"about:blank?outer\\", + \\"width\\": 1920, + \\"height\\": 1080 + } + }, + { + \\"type\\": 2, + \\"data\\": { + \\"node\\": { + \\"type\\": 0, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"html\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"head\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 3 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 5 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"input\\", + \\"attributes\\": { + \\"type\\": \\"text\\", + \\"size\\": \\"40\\" + }, + \\"childNodes\\": [], + \\"id\\": 6 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 7 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"iframe\\", + \\"attributes\\": { + \\"srcdoc\\": \\"
rrweb loaded in here!
\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\\\n \\", + \\"id\\": 9 + } + ], + \\"id\\": 8 + } + ], + \\"id\\": 4 + } + ], + \\"id\\": 2 + } + ], + \\"id\\": 1 + }, + \\"initialOffset\\": { + \\"left\\": 0, + \\"top\\": 0 + } + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"adds\\": [ + { + \\"parentId\\": 8, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 0, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"html\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"head\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"rootId\\": 10, + \\"id\\": 12 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"div\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"rrweb loaded in here!\\", + \\"rootId\\": 10, + \\"id\\": 15 + } + ], + \\"rootId\\": 10, + \\"id\\": 14 + } + ], + \\"rootId\\": 10, + \\"id\\": 13 + } + ], + \\"rootId\\": 10, + \\"id\\": 11 + } + ], + \\"id\\": 10 + } + } + ], + \\"removes\\": [], + \\"texts\\": [], + \\"attributes\\": [], + \\"isAttachIframe\\": true + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 5, + \\"id\\": 6 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"a\\", + \\"isChecked\\": false, + \\"id\\": 6 + } + } +]" +`; + exports[`nested-stylesheet-rules 1`] = ` "[ { diff --git a/packages/rrweb/test/record.test.ts b/packages/rrweb/test/record.test.ts index 884a274933..47ee722b3d 100644 --- a/packages/rrweb/test/record.test.ts +++ b/packages/rrweb/test/record.test.ts @@ -434,3 +434,79 @@ describe('record iframes', function (this: ISuite) { }); }); + +const iframeSetup = async function (this: ISuite, content: string) { + before(async () => { + this.browser = await launchPuppeteer(); + + const bundlePath = path.resolve(__dirname, '../dist/rrweb.min.js'); + this.code = fs.readFileSync(bundlePath, 'utf8'); + }); + + beforeEach(async () => { + const page: puppeteer.Page = await this.browser.newPage(); + await page.goto('about:blank?outer'); + await page.setContent(content); + // page.frames()[0] is the main frame + await page.frames()[1].evaluate(this.code); + this.page = page; + this.events = []; + await this.page.exposeFunction('emit', (e: eventWithTime) => { + if (e.type === EventType.DomContentLoaded || e.type === EventType.Load) { + return; + } + this.events.push(e); + }); + + page.on('console', (msg) => console.log('PAGE LOG:', msg.text())); + }); + + afterEach(async () => { + await this.page.close(); + }); + + after(async () => { + await this.browser.close(); + }); +}; + +describe('be loaded from an iframe', function (this: ISuite) { + this.timeout(10_000); + + iframeSetup.call( + this, + ` + + + +