diff --git a/packages/rrweb/src/plugins/console/record/index.ts b/packages/rrweb/src/plugins/console/record/index.ts index 6f2547d6f0..7fc0040bf6 100644 --- a/packages/rrweb/src/plugins/console/record/index.ts +++ b/packages/rrweb/src/plugins/console/record/index.ts @@ -22,7 +22,7 @@ type LogRecordOptions = { level?: LogLevel[]; lengthThreshold?: number; stringifyOptions?: StringifyOptions; - logger?: Logger; + logger?: Logger | string; }; const defaultLogOptions: LogRecordOptions = { @@ -48,7 +48,7 @@ const defaultLogOptions: LogRecordOptions = { 'warn', ], lengthThreshold: 1000, - logger: console, + logger: 'console', }; export type LogData = { @@ -106,12 +106,19 @@ export type Logger = { function initLogObserver( cb: logCallback, + win: Window, // top window or in an iframe logOptions: LogRecordOptions, ): listenerHandler { - const logger = logOptions.logger; - if (!logger) { + const loggerType = logOptions.logger; + if (!loggerType) { return () => {}; } + let logger: Logger; + if (typeof loggerType === 'string') { + logger = (win as any)[loggerType]; + } else { + logger = loggerType; + } let logCount = 0; const cancelHandlers: listenerHandler[] = []; // add listener to thrown errors diff --git a/packages/rrweb/src/record/observer.ts b/packages/rrweb/src/record/observer.ts index 205483c81d..a944418363 100644 --- a/packages/rrweb/src/record/observer.ts +++ b/packages/rrweb/src/record/observer.ts @@ -519,10 +519,11 @@ function getNestedCSSRulePositions(rule: CSSRule): number[] { function initStyleSheetObserver( cb: styleSheetRuleCallback, + win: Window, mirror: Mirror, ): listenerHandler { - const insertRule = CSSStyleSheet.prototype.insertRule; - CSSStyleSheet.prototype.insertRule = function (rule: string, index?: number) { + const insertRule = (win as any).CSSStyleSheet.prototype.insertRule; + (win as any).CSSStyleSheet.prototype.insertRule = function (rule: string, index?: number) { const id = mirror.getId(this.ownerNode as INode); if (id !== -1) { cb({ @@ -533,8 +534,8 @@ function initStyleSheetObserver( return insertRule.apply(this, arguments); }; - const deleteRule = CSSStyleSheet.prototype.deleteRule; - CSSStyleSheet.prototype.deleteRule = function (index: number) { + const deleteRule = (win as any).CSSStyleSheet.prototype.deleteRule; + (win as any).CSSStyleSheet.prototype.deleteRule = function (index: number) { const id = mirror.getId(this.ownerNode as INode); if (id !== -1) { cb({ @@ -549,20 +550,20 @@ function initStyleSheetObserver( [key: string]: GroupingCSSRuleTypes; } = {}; if (isCSSGroupingRuleSupported) { - supportedNestedCSSRuleTypes['CSSGroupingRule'] = CSSGroupingRule; + supportedNestedCSSRuleTypes['CSSGroupingRule'] = (win as any).CSSGroupingRule; } else { // Some browsers (Safari) don't support CSSGroupingRule // https://caniuse.com/?search=cssgroupingrule // fall back to monkey patching classes that would have inherited from CSSGroupingRule if (isCSSMediaRuleSupported) { - supportedNestedCSSRuleTypes['CSSMediaRule'] = CSSMediaRule; + supportedNestedCSSRuleTypes['CSSMediaRule'] = (win as any).CSSMediaRule; } if (isCSSConditionRuleSupported) { - supportedNestedCSSRuleTypes['CSSConditionRule'] = CSSConditionRule; + supportedNestedCSSRuleTypes['CSSConditionRule'] = (win as any).CSSConditionRule; } if (isCSSSupportsRuleSupported) { - supportedNestedCSSRuleTypes['CSSSupportsRule'] = CSSSupportsRule; + supportedNestedCSSRuleTypes['CSSSupportsRule'] = (win as any).CSSSupportsRule; } } @@ -611,8 +612,8 @@ function initStyleSheetObserver( }); return () => { - CSSStyleSheet.prototype.insertRule = insertRule; - CSSStyleSheet.prototype.deleteRule = deleteRule; + (win as any).CSSStyleSheet.prototype.insertRule = insertRule; + (win as any).CSSStyleSheet.prototype.deleteRule = deleteRule; Object.entries(supportedNestedCSSRuleTypes).forEach(([typeKey, type]) => { type.prototype.insertRule = unmodifiedFunctions[typeKey].insertRule; type.prototype.deleteRule = unmodifiedFunctions[typeKey].deleteRule; @@ -622,14 +623,15 @@ function initStyleSheetObserver( function initStyleDeclarationObserver( cb: styleDeclarationCallback, + win: Window, mirror: Mirror, ): listenerHandler { - const setProperty = CSSStyleDeclaration.prototype.setProperty; - CSSStyleDeclaration.prototype.setProperty = function ( + const setProperty = (win as any).CSSStyleDeclaration.prototype.setProperty; + (win as any).CSSStyleDeclaration.prototype.setProperty = function ( this: CSSStyleDeclaration, - property, - value, - priority, + property: string, + value: string, + priority: string, ) { const id = mirror.getId( (this.parentRule?.parentStyleSheet?.ownerNode as unknown) as INode, @@ -648,10 +650,10 @@ function initStyleDeclarationObserver( return setProperty.apply(this, arguments); }; - const removeProperty = CSSStyleDeclaration.prototype.removeProperty; - CSSStyleDeclaration.prototype.removeProperty = function ( + const removeProperty = (win as any).CSSStyleDeclaration.prototype.removeProperty; + (win as any).CSSStyleDeclaration.prototype.removeProperty = function ( this: CSSStyleDeclaration, - property, + property: string, ) { const id = mirror.getId( (this.parentRule?.parentStyleSheet?.ownerNode as unknown) as INode, @@ -669,8 +671,8 @@ function initStyleDeclarationObserver( }; return () => { - CSSStyleDeclaration.prototype.setProperty = setProperty; - CSSStyleDeclaration.prototype.removeProperty = removeProperty; + (win as any).CSSStyleDeclaration.prototype.setProperty = setProperty; + (win as any).CSSStyleDeclaration.prototype.removeProperty = removeProperty; }; } @@ -702,22 +704,23 @@ function initMediaInteractionObserver( function initCanvasMutationObserver( cb: canvasMutationCallback, + win: Window, blockClass: blockClass, mirror: Mirror, ): listenerHandler { - const props = Object.getOwnPropertyNames(CanvasRenderingContext2D.prototype); + const props = Object.getOwnPropertyNames((win as any).CanvasRenderingContext2D.prototype); const handlers: listenerHandler[] = []; for (const prop of props) { try { if ( - typeof CanvasRenderingContext2D.prototype[ + typeof (win as any).CanvasRenderingContext2D.prototype[ prop as keyof CanvasRenderingContext2D ] !== 'function' ) { continue; } const restoreHandler = patch( - CanvasRenderingContext2D.prototype, + (win as any).CanvasRenderingContext2D.prototype, prop, function (original) { return function ( @@ -758,7 +761,7 @@ function initCanvasMutationObserver( handlers.push(restoreHandler); } catch { const hookHandler = hookSetter( - CanvasRenderingContext2D.prototype, + (win as any).CanvasRenderingContext2D.prototype, prop, { set(v) { @@ -779,14 +782,19 @@ function initCanvasMutationObserver( }; } -function initFontObserver(cb: fontCallback): listenerHandler { +function initFontObserver( + cb: fontCallback, + doc: Document, +): listenerHandler { + const win = doc.defaultView; + const handlers: listenerHandler[] = []; const fontMap = new WeakMap(); - const originalFontFace = FontFace; + const originalFontFace = (win as any).FontFace; // tslint:disable-next-line: no-any - (window as any).FontFace = function FontFace( + (win as any).FontFace = function FontFace( family: string, source: string | ArrayBufferView, descriptors?: FontFaceDescriptors, @@ -805,7 +813,7 @@ function initFontObserver(cb: fontCallback): listenerHandler { return fontFace; }; - const restoreHandler = patch(document.fonts, 'add', function (original) { + const restoreHandler = patch(doc.fonts, 'add', function (original) { return function (this: FontFaceSet, fontFace: FontFace) { setTimeout(() => { const p = fontMap.get(fontFace); @@ -820,7 +828,7 @@ function initFontObserver(cb: fontCallback): listenerHandler { handlers.push(() => { // tslint:disable-next-line: no-any - (window as any).FontFace = originalFontFace; + (win as any).FontFace = originalFontFace; }); handlers.push(restoreHandler); @@ -971,22 +979,35 @@ export function initObservers( o.blockClass, o.mirror, ); + + const currentWindow = o.doc.defaultView as Window; // basically document.window + const styleSheetObserver = initStyleSheetObserver( o.styleSheetRuleCb, + currentWindow, o.mirror, ); const styleDeclarationObserver = initStyleDeclarationObserver( o.styleDeclarationCb, + currentWindow, o.mirror, ); const canvasMutationObserver = o.recordCanvas - ? initCanvasMutationObserver(o.canvasMutationCb, o.blockClass, o.mirror) - : () => {}; - const fontObserver = o.collectFonts ? initFontObserver(o.fontCb) : () => {}; + ? initCanvasMutationObserver( + o.canvasMutationCb, + currentWindow, + o.blockClass, + o.mirror, + ) : () => {}; + const fontObserver = o.collectFonts ? initFontObserver(o.fontCb, o.doc) : () => {}; // plugins const pluginHandlers: listenerHandler[] = []; for (const plugin of o.plugins) { - pluginHandlers.push(plugin.observer(plugin.callback, plugin.options)); + pluginHandlers.push(plugin.observer( + plugin.callback, + currentWindow, + plugin.options, + )); } return () => { diff --git a/packages/rrweb/src/replay/index.ts b/packages/rrweb/src/replay/index.ts index 07d09b58d9..40e4726775 100644 --- a/packages/rrweb/src/replay/index.ts +++ b/packages/rrweb/src/replay/index.ts @@ -759,7 +759,7 @@ export class Replayer { const head = this.iframe.contentDocument?.head; if (head) { const unloadSheets: Set = new Set(); - let timer: number; + let timer: ReturnType | -1; let beforeLoadState = this.service.state; const stateHandler = () => { beforeLoadState = this.service.state; @@ -784,7 +784,7 @@ export class Replayer { } this.emitter.emit(ReplayerEvents.LoadStylesheetEnd); if (timer) { - window.clearTimeout(timer); + clearTimeout(timer); } unsubscribe(); } @@ -796,7 +796,7 @@ export class Replayer { // find some unload sheets after iterate this.service.send({ type: 'PAUSE' }); this.emitter.emit(ReplayerEvents.LoadStylesheetStart); - timer = window.setTimeout(() => { + timer = setTimeout(() => { if (beforeLoadState.matches('playing')) { this.play(this.getCurrentTime()); } diff --git a/packages/rrweb/src/types.ts b/packages/rrweb/src/types.ts index a596f64630..7442456735 100644 --- a/packages/rrweb/src/types.ts +++ b/packages/rrweb/src/types.ts @@ -201,7 +201,7 @@ export type SamplingStrategy = Partial<{ export type RecordPlugin = { name: string; - observer: (cb: Function, options: TOptions) => listenerHandler; + observer: (cb: Function, win: Window, options: TOptions) => listenerHandler; options: TOptions; }; diff --git a/packages/rrweb/src/utils.ts b/packages/rrweb/src/utils.ts index b92b44e929..c61f7afdf7 100644 --- a/packages/rrweb/src/utils.ts +++ b/packages/rrweb/src/utils.ts @@ -111,7 +111,7 @@ export function throttle( wait: number, options: throttleOptions = {}, ) { - let timeout: number | null = null; + let timeout: ReturnType | null = null; let previous = 0; // tslint:disable-next-line: only-arrow-functions return function (arg: T) { @@ -124,13 +124,13 @@ export function throttle( let args = arguments; if (remaining <= 0 || remaining > wait) { if (timeout) { - window.clearTimeout(timeout); + clearTimeout(timeout); timeout = null; } previous = now; func.apply(context, args); } else if (!timeout && options.trailing !== false) { - timeout = window.setTimeout(() => { + timeout = setTimeout(() => { previous = options.leading === false ? 0 : Date.now(); timeout = null; func.apply(context, args); diff --git a/packages/rrweb/test/__snapshots__/integration.test.ts.snap b/packages/rrweb/test/__snapshots__/integration.test.ts.snap index d336859c8b..33a448051f 100644 --- a/packages/rrweb/test/__snapshots__/integration.test.ts.snap +++ b/packages/rrweb/test/__snapshots__/integration.test.ts.snap @@ -3347,6 +3347,88 @@ exports[`log 1`] = ` \\"payload\\": [] } } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [], + \\"removes\\": [], + \\"adds\\": [ + { + \\"parentId\\": 16, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"iframe\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"id\\": 21 + } + } + ] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"adds\\": [ + { + \\"parentId\\": 21, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 0, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"html\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"head\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"rootId\\": 22, + \\"id\\": 24 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"rootId\\": 22, + \\"id\\": 25 + } + ], + \\"rootId\\": 22, + \\"id\\": 23 + } + ], + \\"id\\": 22 + } + } + ], + \\"removes\\": [], + \\"texts\\": [], + \\"attributes\\": [], + \\"isAttachIframe\\": true + } + }, + { + \\"type\\": 6, + \\"data\\": { + \\"plugin\\": \\"rrweb/console@1\\", + \\"payload\\": { + \\"level\\": \\"log\\", + \\"trace\\": [], + \\"payload\\": [ + \\"\\\\\\"from iframe\\\\\\"\\" + ] + } + } } ]" `; diff --git a/packages/rrweb/test/__snapshots__/record.test.ts.snap b/packages/rrweb/test/__snapshots__/record.test.ts.snap index 4845431bd9..23b50a5615 100644 --- a/packages/rrweb/test/__snapshots__/record.test.ts.snap +++ b/packages/rrweb/test/__snapshots__/record.test.ts.snap @@ -329,6 +329,234 @@ exports[`custom-event 1`] = ` ]" `; +exports[`iframe-stylesheet-mutations 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\\": \\"iframe\\", + \\"attributes\\": { + \\"srcdoc\\": \\"\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\\\n \\", + \\"id\\": 7 + } + ], + \\"id\\": 6 + } + ], + \\"id\\": 4 + } + ], + \\"id\\": 2 + } + ], + \\"id\\": 1 + }, + \\"initialOffset\\": { + \\"left\\": 0, + \\"top\\": 0 + } + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"adds\\": [ + { + \\"parentId\\": 6, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 0, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"html\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"head\\", + \\"attributes\\": {}, + \\"childNodes\\": [], + \\"rootId\\": 8, + \\"id\\": 10 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 2, + \\"tagName\\": \\"button\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"Mysterious Button\\", + \\"rootId\\": 8, + \\"id\\": 13 + } + ], + \\"rootId\\": 8, + \\"id\\": 12 + } + ], + \\"rootId\\": 8, + \\"id\\": 11 + } + ], + \\"rootId\\": 8, + \\"id\\": 9 + } + ], + \\"id\\": 8 + } + } + ], + \\"removes\\": [], + \\"texts\\": [], + \\"attributes\\": [], + \\"isAttachIframe\\": true + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [], + \\"removes\\": [], + \\"adds\\": [ + { + \\"parentId\\": 10, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"style\\", + \\"attributes\\": { + \\"_cssText\\": \\"body { background: rgb(0, 0, 0); }@media {\\\\n body { background: rgb(0, 0, 0); }\\\\n}\\" + }, + \\"childNodes\\": [], + \\"rootId\\": 8, + \\"id\\": 14 + } + } + ] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 8, + \\"id\\": 14, + \\"adds\\": [ + { + \\"rule\\": \\"body { color: #fff; }\\" + } + ] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 8, + \\"id\\": 14, + \\"adds\\": [ + { + \\"rule\\": \\"body { color: #ccc; }\\", + \\"index\\": [ + 2, + 0 + ] + } + ] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 8, + \\"id\\": 14, + \\"removes\\": [ + { + \\"index\\": 0 + } + ] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 13, + \\"id\\": 14, + \\"set\\": { + \\"property\\": \\"color\\", + \\"value\\": \\"green\\" + }, + \\"index\\": [ + 0 + ] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 8, + \\"id\\": 14, + \\"removes\\": [ + { + \\"index\\": [ + 1, + 0 + ] + } + ] + } + } +]" +`; + exports[`nested-stylesheet-rules 1`] = ` "[ { @@ -471,6 +699,128 @@ exports[`nested-stylesheet-rules 1`] = ` ]" `; +exports[`stylesheet-properties 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 \\\\n \\\\n \\", + \\"id\\": 7 + } + ], + \\"id\\": 4 + } + ], + \\"id\\": 2 + } + ], + \\"id\\": 1 + }, + \\"initialOffset\\": { + \\"left\\": 0, + \\"top\\": 0 + } + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [], + \\"removes\\": [], + \\"adds\\": [ + { + \\"parentId\\": 3, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"style\\", + \\"attributes\\": { + \\"_cssText\\": \\"body { background: rgb(0, 0, 0); }\\" + }, + \\"childNodes\\": [], + \\"id\\": 8 + } + } + ] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 13, + \\"id\\": 8, + \\"set\\": { + \\"property\\": \\"color\\", + \\"value\\": \\"green\\" + }, + \\"index\\": [ + 0 + ] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 13, + \\"id\\": 8, + \\"remove\\": { + \\"property\\": \\"background\\" + }, + \\"index\\": [ + 0 + ] + } + } +]" +`; + exports[`stylesheet-rules 1`] = ` "[ { diff --git a/packages/rrweb/test/integration.test.ts b/packages/rrweb/test/integration.test.ts index 723f890076..7a9c77950e 100644 --- a/packages/rrweb/test/integration.test.ts +++ b/packages/rrweb/test/integration.test.ts @@ -471,8 +471,15 @@ describe('record integration tests', function (this: ISuite) { console.trace('trace'); console.warn('warn'); console.clear(); + const iframe = document.createElement('iframe'); + document.body.appendChild(iframe); }); + await page.frames()[1].evaluate(() => { + console.log('from iframe'); + }); + + const snapshots = await page.evaluate('window.snapshots'); assertSnapshot(snapshots, __filename, 'log'); }); diff --git a/packages/rrweb/test/record.test.ts b/packages/rrweb/test/record.test.ts index a14c7cd10b..884a274933 100644 --- a/packages/rrweb/test/record.test.ts +++ b/packages/rrweb/test/record.test.ts @@ -220,9 +220,11 @@ describe('record', function (this: ISuite) { document.head.appendChild(styleElement); const styleSheet = styleElement.sheet; + // begin: pre-serialization const ruleIdx0 = styleSheet.insertRule('body { background: #000; }'); const ruleIdx1 = styleSheet.insertRule('body { background: #111; }'); styleSheet.deleteRule(ruleIdx1); + // end: pre-serialization setTimeout(() => { styleSheet.insertRule('body { color: #fff; }'); }, 0); @@ -239,14 +241,15 @@ describe('record', function (this: ISuite) { e.type === EventType.IncrementalSnapshot && e.data.source === IncrementalSource.StyleSheetRule, ); - const addRuleCount = styleSheetRuleEvents.filter((e) => + const addRules = styleSheetRuleEvents.filter((e) => Boolean((e.data as styleSheetRuleData).adds), - ).length; + ); const removeRuleCount = styleSheetRuleEvents.filter((e) => Boolean((e.data as styleSheetRuleData).removes), ).length; - // sync insert/delete should be ignored - expect(addRuleCount).to.equal(2); + // pre-serialization insert/delete should be ignored + expect(addRules.length).to.equal(2); + expect((addRules[0].data as styleSheetRuleData).adds).to.deep.include({rule: "body { color: #fff; }"}); expect(removeRuleCount).to.equal(1); assertSnapshot(this.events, __filename, 'stylesheet-rules'); }); @@ -311,6 +314,30 @@ describe('record', function (this: ISuite) { }); it('captures nested stylesheet rules', captureNestedStylesheetRulesTest); }); + + it('captures style property changes', async () => { + await this.page.evaluate(() => { + const { record } = ((window as unknown) as IWindow).rrweb; + + record({ + emit: ((window as unknown) as IWindow).emit, + }); + + const styleElement = document.createElement('style'); + document.head.appendChild(styleElement); + + const styleSheet = styleElement.sheet; + styleSheet.insertRule('body { background: #000; }'); + setTimeout(() => { + (styleSheet.cssRules[0] as CSSStyleRule).style.setProperty('color', 'green'); + (styleSheet.cssRules[0] as CSSStyleRule).style.removeProperty('background'); + }, 0); + }); + await this.page.waitForTimeout(50); + assertSnapshot(this.events, __filename, 'stylesheet-properties'); + }); + + }); describe('record iframes', function (this: ISuite) { @@ -350,4 +377,60 @@ describe('record iframes', function (this: ISuite) { EventType.IncrementalSnapshot, ]); }); + + it('captures stylesheet mutations in iframes', async () => { + await this.page.evaluate(() => { + const { record } = ((window as unknown) as IWindow).rrweb; + record({ + // need to reference window.top for when we are in an iframe! + emit: ((window.top as unknown) as IWindow).emit, + }); + + const iframe = document.querySelector('iframe'); + // outer timeout is needed to wait for initStyleSheetObserver on iframe to be set up + setTimeout(() => { + + const idoc = (iframe as HTMLIFrameElement).contentDocument!; + const styleElement = idoc.createElement('style'); + + idoc.head.appendChild(styleElement); + + const styleSheet = styleElement.sheet; + styleSheet.insertRule('@media {}'); + const atMediaRule = styleSheet.cssRules[0] as CSSMediaRule; + const atRuleIdx0 = atMediaRule.insertRule('body { background: #000; }', 0); + const ruleIdx0 = styleSheet.insertRule('body { background: #000; }'); // inserted before above + // pre-serialization insert/delete above should be ignored + setTimeout(() => { + styleSheet.insertRule('body { color: #fff; }'); + atMediaRule.insertRule('body { color: #ccc; }', 0); + }, 0); + setTimeout(() => { + styleSheet.deleteRule(ruleIdx0); + (styleSheet.cssRules[0] as CSSStyleRule).style.setProperty('color', 'green'); + }, 5); + setTimeout(() =>{ + atMediaRule.deleteRule(atRuleIdx0); + }, 10); + }, 10); + }); + await this.page.waitForTimeout(50); + const styleRelatedEvents = this.events.filter( + (e) => + e.type === EventType.IncrementalSnapshot && + (e.data.source === IncrementalSource.StyleSheetRule || + e.data.source === IncrementalSource.StyleDeclaration), + ); + const addRuleCount = styleRelatedEvents.filter((e) => + Boolean((e.data as styleSheetRuleData).adds), + ).length; + const removeRuleCount = styleRelatedEvents.filter((e) => + Boolean((e.data as styleSheetRuleData).removes), + ).length; + expect(styleRelatedEvents.length).to.equal(5); + expect(addRuleCount).to.equal(2); + expect(removeRuleCount).to.equal(2); + assertSnapshot(this.events, __filename, 'iframe-stylesheet-mutations'); + }); + }); diff --git a/packages/rrweb/typings/plugins/console/record/index.d.ts b/packages/rrweb/typings/plugins/console/record/index.d.ts index ffda9a87a1..a41ee5506b 100644 --- a/packages/rrweb/typings/plugins/console/record/index.d.ts +++ b/packages/rrweb/typings/plugins/console/record/index.d.ts @@ -8,7 +8,7 @@ declare type LogRecordOptions = { level?: LogLevel[]; lengthThreshold?: number; stringifyOptions?: StringifyOptions; - logger?: Logger; + logger?: Logger | string; }; export declare type LogData = { level: LogLevel; diff --git a/packages/rrweb/typings/types.d.ts b/packages/rrweb/typings/types.d.ts index 3becc33017..dda8f4b434 100644 --- a/packages/rrweb/typings/types.d.ts +++ b/packages/rrweb/typings/types.d.ts @@ -126,7 +126,7 @@ export declare type SamplingStrategy = Partial<{ }>; export declare type RecordPlugin = { name: string; - observer: (cb: Function, options: TOptions) => listenerHandler; + observer: (cb: Function, win: Window, options: TOptions) => listenerHandler; options: TOptions; }; export declare type recordOptions = {