diff --git a/guide.md b/guide.md index 206fa053ab..3fb9bf74af 100644 --- a/guide.md +++ b/guide.md @@ -160,6 +160,7 @@ The parameter of `rrweb.record` accepts the following options. | inlineImages | false | whether to record the image content | | collectFonts | false | whether to collect fonts in the website | | userTriggeredOnInput | false | whether to add `userTriggered` on input events that indicates if this event was triggered directly by the user or not. [What is `userTriggered`?](https://github.com/rrweb-io/rrweb/pull/495) | +| window | window | Window object to record. When rrweb is loaded from an iframe, inject here the parent `window` Element. | | plugins | [] | load plugins to provide extended record functions. [What is plugins?](./docs/recipes/plugin.md) | #### Privacy diff --git a/guide.zh_CN.md b/guide.zh_CN.md index e867c93de6..98e8e20573 100644 --- a/guide.zh_CN.md +++ b/guide.zh_CN.md @@ -156,6 +156,7 @@ setInterval(save, 10 * 1000); | inlineImages | false | 是否将图片内容记内联录制 | | collectFonts | false | 是否记录页面中的字体文件 | | userTriggeredOnInput | false | [什么是 `userTriggered`](https://github.com/rrweb-io/rrweb/pull/495) | +| window | window | | | plugins | [] | 加载插件以获得额外的录制功能. [什么是插件?](./docs/recipes/plugin.zh_CN.md) | #### 隐私 diff --git a/packages/rrweb/src/record/index.ts b/packages/rrweb/src/record/index.ts index 26e07fc83a..de8c254fc8 100644 --- a/packages/rrweb/src/record/index.ts +++ b/packages/rrweb/src/record/index.ts @@ -25,6 +25,7 @@ import { scrollCallback, canvasMutationParam, adoptedStyleSheetParam, + IWindow, } from '../types'; import { IframeManager } from './iframe-manager'; import { ShadowDomManager } from './shadow-dom-manager'; @@ -71,10 +72,12 @@ function record( userTriggeredOnInput = false, collectFonts = false, inlineImages = false, + window: _win = window, plugins, keepIframeSrcFn = () => false, ignoreCSSAttributes = new Set([]), } = options; + const win = (_win as unknown) as IWindow; // runtime checks for user options if (!emit) { @@ -250,7 +253,7 @@ function record( canvasManager = new CanvasManager({ recordCanvas, mutationCb: wrappedCanvasMutationEmit, - win: window, + win, blockClass, blockSelector, mirror, @@ -259,6 +262,7 @@ function record( }); const shadowDomManager = new ShadowDomManager({ + win, mutationCb: wrappedMutationEmit, scrollCb: wrappedScrollEmit, bypassOptions: { @@ -279,6 +283,7 @@ function record( stylesheetManager, canvasManager, keepIframeSrcFn, + window: win, }, mirror, }); @@ -288,9 +293,9 @@ function record( wrapEvent({ type: EventType.Meta, data: { - href: window.location.href, - width: getWindowWidth(), - height: getWindowHeight(), + href: win.location.href, + width: getWindowWidth(win), + height: getWindowHeight(win), }, }), isCheckout, @@ -300,7 +305,7 @@ function record( stylesheetManager.reset(); mutationBuffers.forEach((buf) => buf.lock()); // don't allow any mirror modifications during snapshotting - const node = snapshot(document, { + const node = snapshot(win.document, { mirror, blockClass, blockSelector, @@ -321,7 +326,7 @@ function record( stylesheetManager.trackLinkElement(n as HTMLLinkElement); } if (hasShadowRoot(n)) { - shadowDomManager.addShadowRoot(n.shadowRoot, document); + shadowDomManager.addShadowRoot(n.shadowRoot, win.document); } }, onIframeLoad: (iframe, childSn) => { @@ -345,18 +350,18 @@ function record( node, initialOffset: { left: - window.pageXOffset !== undefined - ? window.pageXOffset - : document?.documentElement.scrollLeft || - document?.body?.parentElement?.scrollLeft || - document?.body?.scrollLeft || + win.pageXOffset !== undefined + ? win.pageXOffset + : win.document?.documentElement.scrollLeft || + win.document?.body?.parentElement?.scrollLeft || + win.document?.body?.scrollLeft || 0, top: - window.pageYOffset !== undefined - ? window.pageYOffset - : document?.documentElement.scrollTop || - document?.body?.parentElement?.scrollTop || - document?.body?.scrollTop || + win.pageYOffset !== undefined + ? win.pageYOffset + : win.document?.documentElement.scrollTop || + win.document?.body?.parentElement?.scrollTop || + win.document?.body?.scrollTop || 0, }, }, @@ -365,10 +370,13 @@ function record( mutationBuffers.forEach((buf) => buf.unlock()); // generate & emit any mutations that happened during snapshotting, as can now apply against the newly built mirror // Some old browsers don't support adoptedStyleSheets. - if (document.adoptedStyleSheets && document.adoptedStyleSheets.length > 0) + if ( + win.document.adoptedStyleSheets && + win.document.adoptedStyleSheets.length > 0 + ) stylesheetManager.adoptStyleSheets( - document.adoptedStyleSheets, - mirror.getId(document), + win.document.adoptedStyleSheets, + mirror.getId(win.document), ); }; @@ -493,6 +501,7 @@ function record( inlineImages, userTriggeredOnInput, collectFonts, + window: win, doc, maskInputFn, maskTextFn, @@ -534,11 +543,11 @@ function record( const init = () => { takeFullSnapshot(); - handlers.push(observe(document)); + handlers.push(observe(win.document)); }; if ( - document.readyState === 'interactive' || - document.readyState === 'complete' + win.document.readyState === 'interactive' || + win.document.readyState === 'complete' ) { init(); } else { @@ -554,7 +563,7 @@ function record( ); init(); }, - window, + win, ), ); } diff --git a/packages/rrweb/src/record/mutation.ts b/packages/rrweb/src/record/mutation.ts index 5f54842b95..2fdf416b3c 100644 --- a/packages/rrweb/src/record/mutation.ts +++ b/packages/rrweb/src/record/mutation.ts @@ -170,6 +170,7 @@ export default class MutationBuffer { private inlineImages: observerParam['inlineImages']; private slimDOMOptions: observerParam['slimDOMOptions']; private dataURLOptions: observerParam['dataURLOptions']; + private window: observerParam['window']; private doc: observerParam['doc']; private mirror: observerParam['mirror']; private iframeManager: observerParam['iframeManager']; @@ -193,6 +194,7 @@ export default class MutationBuffer { 'inlineImages', 'slimDOMOptions', 'dataURLOptions', + 'window', 'doc', 'mirror', 'iframeManager', @@ -321,7 +323,10 @@ export default class MutationBuffer { ); } if (hasShadowRoot(n)) { - this.shadowDomManager.addShadowRoot(n.shadowRoot, document); + this.shadowDomManager.addShadowRoot( + n.shadowRoot, + this.window.document, + ); } }, onIframeLoad: (iframe, childSn) => { diff --git a/packages/rrweb/src/record/observer.ts b/packages/rrweb/src/record/observer.ts index 6fb2edad22..3c58f2006e 100644 --- a/packages/rrweb/src/record/observer.ts +++ b/packages/rrweb/src/record/observer.ts @@ -87,7 +87,7 @@ export function initMutationObserver( // see mutation.ts for details mutationBuffer.init(options); let mutationObserverCtor = - window.MutationObserver || + options.window.MutationObserver || /** * Some websites may disable MutationObserver by removing it from the window object. * If someone is using rrweb to build a browser extention or things like it, they @@ -96,17 +96,17 @@ export function initMutationObserver( * Then they can do this to store the native MutationObserver: * window.__rrMutationObserver = MutationObserver */ - (window as WindowWithStoredMutationObserver).__rrMutationObserver; - const angularZoneSymbol = (window as WindowWithAngularZone)?.Zone?.__symbol__?.( + (options.window as WindowWithStoredMutationObserver).__rrMutationObserver; + const angularZoneSymbol = (options.window as WindowWithAngularZone)?.Zone?.__symbol__?.( 'MutationObserver', ); if ( angularZoneSymbol && - ((window as unknown) as Record)[ + ((options.window as unknown) as Record)[ angularZoneSymbol ] ) { - mutationObserverCtor = ((window as unknown) as Record< + mutationObserverCtor = ((options.window as unknown) as Record< string, typeof MutationObserver >)[angularZoneSymbol]; @@ -299,13 +299,14 @@ export function initScrollObserver({ } function initViewportResizeObserver({ + window: win, viewportResizeCb, }: observerParam): listenerHandler { let lastH = -1; let lastW = -1; const updateDimension = throttle(() => { - const height = getWindowHeight(); - const width = getWindowWidth(); + const height = getWindowHeight(win); + const width = getWindowWidth(win); if (lastH !== height || lastW !== width) { viewportResizeCb({ width: Number(width), @@ -315,7 +316,7 @@ function initViewportResizeObserver({ lastW = width; } }, 200); - return on('resize', updateDimension, window); + return on('resize', updateDimension, win); } function wrapEventWithUserTriggeredFlag( diff --git a/packages/rrweb/src/record/shadow-dom-manager.ts b/packages/rrweb/src/record/shadow-dom-manager.ts index 16f679c861..7a1df8ec11 100644 --- a/packages/rrweb/src/record/shadow-dom-manager.ts +++ b/packages/rrweb/src/record/shadow-dom-manager.ts @@ -3,6 +3,7 @@ import type { scrollCallback, MutationBufferParam, SamplingStrategy, + IWindow, } from '../types'; import { initMutationObserver, @@ -21,6 +22,7 @@ type BypassOptions = Omit< }; export class ShadowDomManager { + private win: IWindow; private shadowDoms = new WeakSet(); private mutationCb: mutationCallBack; private scrollCb: scrollCallback; @@ -29,11 +31,13 @@ export class ShadowDomManager { private restorePatches: (() => void)[] = []; constructor(options: { + win: IWindow; mutationCb: mutationCallBack; scrollCb: scrollCallback; bypassOptions: BypassOptions; mirror: Mirror; }) { + this.win = options.win; this.mutationCb = options.mutationCb; this.scrollCb = options.scrollCb; this.bypassOptions = options.bypassOptions; @@ -65,6 +69,7 @@ export class ShadowDomManager { initMutationObserver( { ...this.bypassOptions, + window: this.win, doc, mutationCb: this.mutationCb, mirror: this.mirror, diff --git a/packages/rrweb/src/types.ts b/packages/rrweb/src/types.ts index 235005073a..447cf9295a 100644 --- a/packages/rrweb/src/types.ts +++ b/packages/rrweb/src/types.ts @@ -263,6 +263,7 @@ export type recordOptions = { userTriggeredOnInput?: boolean; collectFonts?: boolean; inlineImages?: boolean; + window?: Window; plugins?: RecordPlugin[]; // departed, please use sampling options mousemoveWait?: number; @@ -299,6 +300,7 @@ export type observerParam = { collectFonts: boolean; slimDOMOptions: SlimDOMOptions; dataURLOptions: DataURLOptions; + window: IWindow; doc: Document; mirror: Mirror; iframeManager: IframeManager; @@ -333,6 +335,7 @@ export type MutationBufferParam = Pick< | 'inlineImages' | 'slimDOMOptions' | 'dataURLOptions' + | 'window' | 'doc' | 'mirror' | 'iframeManager' diff --git a/packages/rrweb/src/utils.ts b/packages/rrweb/src/utils.ts index b782bd52e2..6fdee8cb6c 100644 --- a/packages/rrweb/src/utils.ts +++ b/packages/rrweb/src/utils.ts @@ -167,19 +167,21 @@ export function patch( } } -export function getWindowHeight(): number { +export function getWindowHeight(win: Window): number { return ( - window.innerHeight || - (document.documentElement && document.documentElement.clientHeight) || - (document.body && document.body.clientHeight) + win.innerHeight || + (win.document.documentElement && + win.document.documentElement.clientHeight) || + (win.document.body && win.document.body.clientHeight) ); } -export function getWindowWidth(): number { +export function getWindowWidth(win: Window): number { return ( - window.innerWidth || - (document.documentElement && document.documentElement.clientWidth) || - (document.body && document.body.clientWidth) + win.innerWidth || + (win.document.documentElement && + win.document.documentElement.clientWidth) || + (win.document.body && win.document.body.clientWidth) ); } diff --git a/packages/rrweb/test/__snapshots__/record.test.ts.snap b/packages/rrweb/test/__snapshots__/record.test.ts.snap index 7133805635..c29e542df3 100644 --- a/packages/rrweb/test/__snapshots__/record.test.ts.snap +++ b/packages/rrweb/test/__snapshots__/record.test.ts.snap @@ -1,5 +1,172 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`be loaded from an iframe captures activity in outer frame 1`] = ` +"[ + { + \\"type\\": 4, + \\"data\\": { + \\"href\\": \\"about:blank\\", + \\"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 + } + ], + \\"compatMode\\": \\"BackCompat\\", + \\"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[`record can add custom event 1`] = ` "[ { diff --git a/packages/rrweb/test/record.test.ts b/packages/rrweb/test/record.test.ts index e353559a26..b5335a9a07 100644 --- a/packages/rrweb/test/record.test.ts +++ b/packages/rrweb/test/record.test.ts @@ -69,6 +69,45 @@ const setup = function (this: ISuite, content: string): ISuite { return ctx; }; +const iframeSetup = function (this: ISuite, content: string): ISuite { + const ctx = {} as ISuite; + + beforeAll(async () => { + ctx.browser = await launchPuppeteer({ + devtools: true, + }); + + const bundlePath = path.resolve(__dirname, '../dist/rrweb.js'); + ctx.code = fs.readFileSync(bundlePath, 'utf8'); + }); + + beforeEach(async () => { + ctx.page = await ctx.browser.newPage(); + await ctx.page.goto('about:blank?outer'); + await ctx.page.setContent(content); + await ctx.page.frames()[1].evaluate(ctx.code); + ctx.events = []; + await ctx.page.exposeFunction('emit', (e: eventWithTime) => { + if (e.type === EventType.DomContentLoaded || e.type === EventType.Load) { + return; + } + ctx.events.push(e); + }); + + ctx.page.on('console', (msg) => console.log('PAGE LOG:', msg.text())); + }); + + afterEach(async () => { + await ctx.page.close(); + }); + + afterAll(async () => { + await ctx.browser.close(); + }); + + return ctx; +}; + describe('record', function (this: ISuite) { jest.setTimeout(10_000); @@ -861,3 +900,49 @@ describe('record iframes', function (this: ISuite) { assertSnapshot(ctx.events); }); }); + +describe('be loaded from an iframe', function (this: ISuite) { + jest.setTimeout(10_000); + + const ctx: ISuite = iframeSetup.call( + this, + ` + + + +