From aac555caa24916e2f6a2c1b9e31e2968b8ef0879 Mon Sep 17 00:00:00 2001 From: John Pham Date: Mon, 4 Oct 2021 11:56:32 -0700 Subject: [PATCH 1/2] Monkeypatch each iframe --- package.json | 2 +- src/plugins/console/record/index.ts | 17 +- src/record/observer.ts | 206 +++++++++++++++------- src/replay/index.ts | 6 +- src/types.ts | 2 +- src/utils.ts | 6 +- typings/plugins/console/record/index.d.ts | 2 +- typings/types.d.ts | 2 +- 8 files changed, 160 insertions(+), 83 deletions(-) diff --git a/package.json b/package.json index 6ba9e9f2..20ea0a63 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@highlight-run/rrweb", - "version": "0.12.13", + "version": "0.12.14", "description": "record and replay the web", "scripts": { "test": "npm run bundle:browser && cross-env TS_NODE_CACHE=false TS_NODE_FILES=true mocha -r ts-node/register -r ignore-styles -r jsdom-global/register test/**.test.ts", diff --git a/src/plugins/console/record/index.ts b/src/plugins/console/record/index.ts index 250ffd2b..1d7928ac 100644 --- a/src/plugins/console/record/index.ts +++ b/src/plugins/console/record/index.ts @@ -17,7 +17,7 @@ type LogRecordOptions = { level?: LogLevel[] | undefined; lengthThreshold?: number; stringifyOptions?: StringifyOptions; - logger?: Logger; + logger?: Logger | string; }; const defaultLogOptions: LogRecordOptions = { @@ -43,7 +43,7 @@ const defaultLogOptions: LogRecordOptions = { 'warn', ], lengthThreshold: 1000, - logger: console, + logger: 'console', }; export type LogData = { @@ -101,12 +101,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 @@ -139,7 +146,7 @@ function initLogObserver( } } for (const levelType of logOptions.level!) { - cancelHandlers.push(replace(logger, levelType)); + cancelHandlers.push(replace(loggerType, levelType)); } return () => { cancelHandlers.forEach((h) => h()); diff --git a/src/record/observer.ts b/src/record/observer.ts index 4c977dd5..2f718d7e 100644 --- a/src/record/observer.ts +++ b/src/record/observer.ts @@ -5,6 +5,7 @@ import { maskInputValue, MaskInputFn, MaskTextFn, + documentNode, } from '../snapshot'; import { FontFaceDescriptors, FontFaceSet } from 'css-font-loading-module'; import { @@ -61,6 +62,9 @@ type WindowWithAngularZone = Window & { export const mutationBuffers: MutationBuffer[] = []; const isCSSGroupingRuleSupported = typeof CSSGroupingRule !== 'undefined'; +const isCSSMediaRuleSupported = typeof CSSMediaRule !== 'undefined'; +const isCSSSupportsRuleSupported = typeof CSSSupportsRule !== 'undefined'; +const isCSSConditionRuleSupported = typeof CSSConditionRule !== 'undefined'; function getEventTarget(event: Event): EventTarget | null { try { @@ -499,6 +503,17 @@ function initInputObserver( }; } +type GroupingCSSRule = + | CSSGroupingRule + | CSSMediaRule + | CSSSupportsRule + | CSSConditionRule; +type GroupingCSSRuleTypes = + | typeof CSSGroupingRule + | typeof CSSMediaRule + | typeof CSSSupportsRule + | typeof CSSConditionRule; + function getNestedCSSRulePositions(rule: CSSRule): number[] { const positions: number[] = []; function recurse(childRule: CSSRule, pos: number[]) { @@ -523,10 +538,14 @@ 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({ @@ -537,8 +556,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,66 +568,98 @@ function initStyleSheetObserver( return deleteRule.apply(this, arguments); }; - if (!isCSSGroupingRuleSupported) { - return () => { - CSSStyleSheet.prototype.insertRule = insertRule; - CSSStyleSheet.prototype.deleteRule = deleteRule; - }; - } + const supportedNestedCSSRuleTypes: { + [key: string]: GroupingCSSRuleTypes; + } = {}; + if (isCSSGroupingRuleSupported) { + 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 - const groupingInsertRule = CSSGroupingRule.prototype.insertRule; - CSSGroupingRule.prototype.insertRule = function ( - rule: string, - index?: number, - ) { - const id = mirror.getId(this.parentStyleSheet.ownerNode as INode); - if (id !== -1) { - cb({ - id, - adds: [ - { - rule, - index: [ - ...getNestedCSSRulePositions(this), - index || 0, // defaults to 0 - ], - }, - ], - }); + if (isCSSMediaRuleSupported) { + supportedNestedCSSRuleTypes['CSSMediaRule'] = (win as any).CSSMediaRule; } - return groupingInsertRule.apply(this, arguments); - }; - - const groupingDeleteRule = CSSGroupingRule.prototype.deleteRule; - CSSGroupingRule.prototype.deleteRule = function (index: number) { - const id = mirror.getId(this.parentStyleSheet.ownerNode as INode); - if (id !== -1) { - cb({ - id, - removes: [{ index: [...getNestedCSSRulePositions(this), index] }], - }); + if (isCSSConditionRuleSupported) { + supportedNestedCSSRuleTypes[ + 'CSSConditionRule' + ] = (win as any).CSSConditionRule; } - return groupingDeleteRule.apply(this, arguments); - }; + if (isCSSSupportsRuleSupported) { + supportedNestedCSSRuleTypes[ + 'CSSSupportsRule' + ] = (win as any).CSSSupportsRule; + } + } + + const unmodifiedFunctions: { + [key: string]: { + insertRule: (rule: string, index?: number) => number; + deleteRule: (index: number) => void; + }; + } = {}; + + Object.entries(supportedNestedCSSRuleTypes).forEach(([typeKey, type]) => { + unmodifiedFunctions[typeKey] = { + insertRule: (type as GroupingCSSRuleTypes).prototype.insertRule, + deleteRule: (type as GroupingCSSRuleTypes).prototype.deleteRule, + }; + + type.prototype.insertRule = function (rule: string, index?: number) { + const id = mirror.getId(this.parentStyleSheet.ownerNode as INode); + if (id !== -1) { + cb({ + id, + adds: [ + { + rule, + index: [ + ...getNestedCSSRulePositions(this), + index || 0, // defaults to 0 + ], + }, + ], + }); + } + return unmodifiedFunctions[typeKey].insertRule.apply(this, arguments); + }; + + type.prototype.deleteRule = function (index: number) { + const id = mirror.getId(this.parentStyleSheet.ownerNode as INode); + if (id !== -1) { + cb({ + id, + removes: [{ index: [...getNestedCSSRulePositions(this), index] }], + }); + } + return unmodifiedFunctions[typeKey].deleteRule.apply(this, arguments); + }; + }); return () => { - CSSStyleSheet.prototype.insertRule = insertRule; - CSSStyleSheet.prototype.deleteRule = deleteRule; - CSSGroupingRule.prototype.insertRule = groupingInsertRule; - CSSGroupingRule.prototype.deleteRule = groupingDeleteRule; + (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; + }); }; } 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, @@ -627,10 +678,11 @@ 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, @@ -648,8 +700,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; }; } @@ -681,22 +733,25 @@ 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 ( @@ -737,7 +792,7 @@ function initCanvasMutationObserver( handlers.push(restoreHandler); } catch { const hookHandler = hookSetter( - CanvasRenderingContext2D.prototype, + (win as any).CanvasRenderingContext2D.prototype, prop, { set(v) { @@ -758,14 +813,15 @@ 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, @@ -784,7 +840,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); @@ -799,7 +855,7 @@ function initFontObserver(cb: fontCallback): listenerHandler { handlers.push(() => { // tslint:disable-next-line: no-any - (window as any).FonFace = originalFontFace; + (win as any).FonFace = originalFontFace; }); handlers.push(restoreHandler); @@ -951,22 +1007,36 @@ 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) + ? initCanvasMutationObserver( + o.canvasMutationCb, + currentWindow, + o.blockClass, + o.mirror, + ) + : () => {}; + const fontObserver = o.collectFonts + ? initFontObserver(o.fontCb, o.doc) : () => {}; - const fontObserver = o.collectFonts ? initFontObserver(o.fontCb) : () => {}; // 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/src/replay/index.ts b/src/replay/index.ts index fb8f91e0..5ac03ba7 100644 --- a/src/replay/index.ts +++ b/src/replay/index.ts @@ -877,7 +877,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; @@ -902,7 +902,7 @@ export class Replayer { } this.emitter.emit(ReplayerEvents.LoadStylesheetEnd); if (timer) { - window.clearTimeout(timer); + clearTimeout(timer); } unsubscribe(); } @@ -914,7 +914,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/src/types.ts b/src/types.ts index 3039d316..5b36580b 100644 --- a/src/types.ts +++ b/src/types.ts @@ -208,7 +208,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/src/utils.ts b/src/utils.ts index 627efeb5..bbf1f147 100644 --- a/src/utils.ts +++ b/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/typings/plugins/console/record/index.d.ts b/typings/plugins/console/record/index.d.ts index 9461dd1d..1b0f4efa 100644 --- a/typings/plugins/console/record/index.d.ts +++ b/typings/plugins/console/record/index.d.ts @@ -7,7 +7,7 @@ declare type LogRecordOptions = { level?: LogLevel[] | undefined; lengthThreshold?: number; stringifyOptions?: StringifyOptions; - logger?: Logger; + logger?: Logger | string; }; export declare type LogData = { level: LogLevel; diff --git a/typings/types.d.ts b/typings/types.d.ts index c9df7b7c..038294c6 100644 --- a/typings/types.d.ts +++ b/typings/types.d.ts @@ -128,7 +128,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 = { From 11ba5aa66570983e84760b41f1691f5589cb4111 Mon Sep 17 00:00:00 2001 From: John Pham Date: Mon, 4 Oct 2021 12:00:58 -0700 Subject: [PATCH 2/2] Fix type error --- src/plugins/console/record/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/console/record/index.ts b/src/plugins/console/record/index.ts index 1d7928ac..71f3645d 100644 --- a/src/plugins/console/record/index.ts +++ b/src/plugins/console/record/index.ts @@ -146,7 +146,7 @@ function initLogObserver( } } for (const levelType of logOptions.level!) { - cancelHandlers.push(replace(loggerType, levelType)); + cancelHandlers.push(replace(logger, levelType)); } return () => { cancelHandlers.forEach((h) => h());