From 83c1866e44d1793e1e7f0630b87ea05fa6e0297a Mon Sep 17 00:00:00 2001 From: David Seel Date: Tue, 10 May 2022 17:01:55 -0400 Subject: [PATCH 01/11] isBlocked factors in the selector --- packages/rrweb/src/record/index.ts | 1 + packages/rrweb/src/record/mutation.ts | 10 +++++----- packages/rrweb/src/record/observer.ts | 14 +++++++++----- packages/rrweb/src/record/observers/canvas/2d.ts | 3 ++- .../src/record/observers/canvas/canvas-manager.ts | 15 ++++++++++----- .../rrweb/src/record/observers/canvas/canvas.ts | 3 ++- .../rrweb/src/record/observers/canvas/webgl.ts | 6 +++++- packages/rrweb/src/utils.ts | 12 ++++++++---- packages/rrweb/typings/record/observer.d.ts | 2 +- packages/rrweb/typings/utils.d.ts | 2 +- 10 files changed, 44 insertions(+), 24 deletions(-) diff --git a/packages/rrweb/src/record/index.ts b/packages/rrweb/src/record/index.ts index 3240e2f42a..5a87dbc3e8 100644 --- a/packages/rrweb/src/record/index.ts +++ b/packages/rrweb/src/record/index.ts @@ -220,6 +220,7 @@ function record( mutationCb: wrappedCanvasMutationEmit, win: window, blockClass, + blockSelector, mirror, sampling: sampling.canvas, }); diff --git a/packages/rrweb/src/record/mutation.ts b/packages/rrweb/src/record/mutation.ts index aaa8900550..4637f6b743 100644 --- a/packages/rrweb/src/record/mutation.ts +++ b/packages/rrweb/src/record/mutation.ts @@ -432,7 +432,7 @@ export default class MutationBuffer { switch (m.type) { case 'characterData': { const value = m.target.textContent; - if (!isBlocked(m.target, this.blockClass) && value !== m.oldValue) { + if (!isBlocked(m.target, this.blockClass, this.blockSelector) && value !== m.oldValue) { this.texts.push({ value: needMaskingText( @@ -461,7 +461,7 @@ export default class MutationBuffer { maskInputFn: this.maskInputFn, }); } - if (isBlocked(m.target, this.blockClass) || value === m.oldValue) { + if (isBlocked(m.target, this.blockClass, this.blockSelector) || value === m.oldValue) { return; } let item: attributeCursor | undefined = this.attributes.find( @@ -525,7 +525,7 @@ export default class MutationBuffer { ? this.mirror.getId(m.target.host) : this.mirror.getId(m.target); if ( - isBlocked(m.target, this.blockClass) || + isBlocked(m.target, this.blockClass, this.blockSelector) || isIgnored(n, this.mirror) || !isSerialized(n, this.mirror) ) { @@ -573,7 +573,7 @@ export default class MutationBuffer { private genAdds = (n: Node, target?: Node) => { // parent was blocked, so we can ignore this node - if (target && isBlocked(target, this.blockClass)) { + if (target && isBlocked(target, this.blockClass, this.blockSelector)) { return; } @@ -596,7 +596,7 @@ export default class MutationBuffer { // if this node is blocked `serializeNode` will turn it into a placeholder element // but we have to remove it's children otherwise they will be added as placeholders too - if (!isBlocked(n, this.blockClass)) + if (!isBlocked(n, this.blockClass, this.blockSelector)) (n as Node).childNodes.forEach((childN) => this.genAdds(childN)); }; } diff --git a/packages/rrweb/src/record/observer.ts b/packages/rrweb/src/record/observer.ts index 5504377853..b3be73aaa3 100644 --- a/packages/rrweb/src/record/observer.ts +++ b/packages/rrweb/src/record/observer.ts @@ -206,6 +206,7 @@ function initMouseInteractionObserver({ doc, mirror, blockClass, + blockSelector, sampling, }: observerParam): listenerHandler { if (sampling.mouseInteraction === false) { @@ -221,7 +222,7 @@ function initMouseInteractionObserver({ const getHandler = (eventKey: keyof typeof MouseInteractions) => { return (event: MouseEvent | TouchEvent) => { const target = getEventTarget(event) as Node; - if (isBlocked(target, blockClass)) { + if (isBlocked(target, blockClass, blockSelector)) { return; } const e = isTouchEvent(event) ? event.changedTouches[0] : event; @@ -260,14 +261,15 @@ export function initScrollObserver({ doc, mirror, blockClass, + blockSelector, sampling, }: Pick< observerParam, - 'scrollCb' | 'doc' | 'mirror' | 'blockClass' | 'sampling' + 'scrollCb' | 'doc' | 'mirror' | 'blockClass' | 'blockSelector' | 'sampling' >): listenerHandler { const updatePosition = throttle((evt) => { const target = getEventTarget(evt); - if (!target || isBlocked(target as Node, blockClass)) { + if (!target || isBlocked(target as Node, blockClass, blockSelector)) { return; } const id = mirror.getId(target as Node); @@ -325,6 +327,7 @@ function initInputObserver({ doc, mirror, blockClass, + blockSelector, ignoreClass, maskInputOptions, maskInputFn, @@ -344,7 +347,7 @@ function initInputObserver({ !target || !(target as Element).tagName || INPUT_TAGS.indexOf((target as Element).tagName) < 0 || - isBlocked(target as Node, blockClass) + isBlocked(target as Node, blockClass, blockSelector) ) { return; } @@ -647,13 +650,14 @@ function initStyleDeclarationObserver( function initMediaInteractionObserver({ mediaInteractionCb, blockClass, + blockSelector, mirror, sampling, }: observerParam): listenerHandler { const handler = (type: MediaInteractions) => throttle((event: Event) => { const target = getEventTarget(event); - if (!target || isBlocked(target as Node, blockClass)) { + if (!target || isBlocked(target as Node, blockClass, blockSelector)) { return; } const { currentTime, volume, muted } = target as HTMLMediaElement; diff --git a/packages/rrweb/src/record/observers/canvas/2d.ts b/packages/rrweb/src/record/observers/canvas/2d.ts index 86cf0e396a..6e4ef50d9a 100644 --- a/packages/rrweb/src/record/observers/canvas/2d.ts +++ b/packages/rrweb/src/record/observers/canvas/2d.ts @@ -13,6 +13,7 @@ export default function initCanvas2DMutationObserver( cb: canvasManagerMutationCallback, win: IWindow, blockClass: blockClass, + blockSelector: string | null, mirror: Mirror, ): listenerHandler { const handlers: listenerHandler[] = []; @@ -36,7 +37,7 @@ export default function initCanvas2DMutationObserver( this: CanvasRenderingContext2D, ...args: Array ) { - if (!isBlocked(this.canvas, blockClass)) { + if (!isBlocked(this.canvas, blockClass, blockSelector)) { // Using setTimeout as toDataURL can be heavy // and we'd rather not block the main thread setTimeout(() => { diff --git a/packages/rrweb/src/record/observers/canvas/canvas-manager.ts b/packages/rrweb/src/record/observers/canvas/canvas-manager.ts index 79859dbbc8..bde15e19a1 100644 --- a/packages/rrweb/src/record/observers/canvas/canvas-manager.ts +++ b/packages/rrweb/src/record/observers/canvas/canvas-manager.ts @@ -59,17 +59,18 @@ export class CanvasManager { mutationCb: canvasMutationCallback; win: IWindow; blockClass: blockClass; + blockSelector: string | null, mirror: Mirror; sampling?: 'all' | number; }) { - const { sampling = 'all', win, blockClass, recordCanvas } = options; + const { sampling = 'all', win, blockClass, blockSelector, recordCanvas } = options; this.mutationCb = options.mutationCb; this.mirror = options.mirror; if (recordCanvas && sampling === 'all') - this.initCanvasMutationObserver(win, blockClass); + this.initCanvasMutationObserver(win, blockClass, blockSelector); if (recordCanvas && typeof sampling === 'number') - this.initCanvasFPSObserver(sampling, win, blockClass); + this.initCanvasFPSObserver(sampling, win, blockClass, blockSelector); } private processMutation: canvasManagerMutationCallback = function ( @@ -93,8 +94,9 @@ export class CanvasManager { fps: number, win: IWindow, blockClass: blockClass, + blockSelector: string | null, ) { - const canvasContextReset = initCanvasContextObserver(win, blockClass); + const canvasContextReset = initCanvasContextObserver(win, blockClass, blockSelector); const snapshotInProgressMap: Map = new Map(); const worker = new ImageBitmapDataURLWorker() as ImageBitmapDataURLRequestWorker; worker.onmessage = (e) => { @@ -197,15 +199,17 @@ export class CanvasManager { private initCanvasMutationObserver( win: IWindow, blockClass: blockClass, + blockSelector: string | null, ): void { this.startRAFTimestamping(); this.startPendingCanvasMutationFlusher(); - const canvasContextReset = initCanvasContextObserver(win, blockClass); + const canvasContextReset = initCanvasContextObserver(win, blockClass, blockSelector); const canvas2DReset = initCanvas2DMutationObserver( this.processMutation.bind(this), win, blockClass, + blockSelector, this.mirror, ); @@ -213,6 +217,7 @@ export class CanvasManager { this.processMutation.bind(this), win, blockClass, + blockSelector, this.mirror, ); diff --git a/packages/rrweb/src/record/observers/canvas/canvas.ts b/packages/rrweb/src/record/observers/canvas/canvas.ts index ff42c7a05d..fd37aa8355 100644 --- a/packages/rrweb/src/record/observers/canvas/canvas.ts +++ b/packages/rrweb/src/record/observers/canvas/canvas.ts @@ -5,6 +5,7 @@ import { isBlocked, patch } from '../../../utils'; export default function initCanvasContextObserver( win: IWindow, blockClass: blockClass, + blockSelector: string | null, ): listenerHandler { const handlers: listenerHandler[] = []; try { @@ -17,7 +18,7 @@ export default function initCanvasContextObserver( contextType: string, ...args: Array ) { - if (!isBlocked(this, blockClass)) { + if (!isBlocked(this, blockClass, blockSelector)) { if (!('__context' in this)) (this as ICanvas).__context = contextType; } diff --git a/packages/rrweb/src/record/observers/canvas/webgl.ts b/packages/rrweb/src/record/observers/canvas/webgl.ts index 1ebe2e19da..784cc08dc8 100644 --- a/packages/rrweb/src/record/observers/canvas/webgl.ts +++ b/packages/rrweb/src/record/observers/canvas/webgl.ts @@ -15,6 +15,7 @@ function patchGLPrototype( type: CanvasContext, cb: canvasManagerMutationCallback, blockClass: blockClass, + blockSelector: string | null, mirror: Mirror, win: IWindow, ): listenerHandler[] { @@ -31,7 +32,7 @@ function patchGLPrototype( return function (this: typeof prototype, ...args: Array) { const result = original.apply(this, args); saveWebGLVar(result, win, prototype); - if (!isBlocked(this.canvas, blockClass)) { + if (!isBlocked(this.canvas, blockClass, blockSelector)) { const id = mirror.getId(this.canvas); const recordArgs = serializeArgs([...args], win, prototype); @@ -71,6 +72,7 @@ export default function initCanvasWebGLMutationObserver( cb: canvasManagerMutationCallback, win: IWindow, blockClass: blockClass, + blockSelector: string | null, mirror: Mirror, ): listenerHandler { const handlers: listenerHandler[] = []; @@ -81,6 +83,7 @@ export default function initCanvasWebGLMutationObserver( CanvasContext.WebGL, cb, blockClass, + blockSelector, mirror, win, ), @@ -93,6 +96,7 @@ export default function initCanvasWebGLMutationObserver( CanvasContext.WebGL2, cb, blockClass, + blockSelector, mirror, win, ), diff --git a/packages/rrweb/src/utils.ts b/packages/rrweb/src/utils.ts index 6a556f4aac..b64750debf 100644 --- a/packages/rrweb/src/utils.ts +++ b/packages/rrweb/src/utils.ts @@ -185,7 +185,7 @@ export function getWindowWidth(): number { ); } -export function isBlocked(node: Node | null, blockClass: blockClass): boolean { +export function isBlocked(node: Node | null, blockClass: blockClass, blockSelector: string | null): boolean { if (!node) { return false; } @@ -204,13 +204,17 @@ export function isBlocked(node: Node | null, blockClass: blockClass): boolean { } }); } - return needBlock || isBlocked(node.parentNode, blockClass); + if (blockSelector) { + needBlock = (node as HTMLElement).matches(blockSelector); + } + + return needBlock || isBlocked(node.parentNode, blockClass, blockSelector); } if (node.nodeType === node.TEXT_NODE) { // check parent node since text node do not have class name - return isBlocked(node.parentNode, blockClass); + return isBlocked(node.parentNode, blockClass, blockSelector); } - return isBlocked(node.parentNode, blockClass); + return isBlocked(node.parentNode, blockClass, blockSelector); } export function isSerialized(n: Node, mirror: Mirror): boolean { diff --git a/packages/rrweb/typings/record/observer.d.ts b/packages/rrweb/typings/record/observer.d.ts index 86453a1465..0908615613 100644 --- a/packages/rrweb/typings/record/observer.d.ts +++ b/packages/rrweb/typings/record/observer.d.ts @@ -2,6 +2,6 @@ import { observerParam, listenerHandler, hooksParam, MutationBufferParam } from import MutationBuffer from './mutation'; export declare const mutationBuffers: MutationBuffer[]; export declare function initMutationObserver(options: MutationBufferParam, rootEl: Node): MutationObserver; -export declare function initScrollObserver({ scrollCb, doc, mirror, blockClass, sampling, }: Pick): listenerHandler; +export declare function initScrollObserver({ scrollCb, doc, mirror, blockClass, blockSelector, sampling, }: Pick): listenerHandler; export declare const INPUT_TAGS: string[]; export declare function initObservers(o: observerParam, hooks?: hooksParam): listenerHandler; diff --git a/packages/rrweb/typings/utils.d.ts b/packages/rrweb/typings/utils.d.ts index fb6dcaba94..950ee517f5 100644 --- a/packages/rrweb/typings/utils.d.ts +++ b/packages/rrweb/typings/utils.d.ts @@ -9,7 +9,7 @@ export declare function patch(source: { }, name: string, replacement: (...args: any[]) => any): () => void; export declare function getWindowHeight(): number; export declare function getWindowWidth(): number; -export declare function isBlocked(node: Node | null, blockClass: blockClass): boolean; +export declare function isBlocked(node: Node | null, blockClass: blockClass, blockSelector: string | null): boolean; export declare function isSerialized(n: Node, mirror: Mirror): boolean; export declare function isIgnored(n: Node, mirror: Mirror): boolean; export declare function isAncestorRemoved(target: Node, mirror: Mirror): boolean; From 97da83fe403097b92d04f556f0a0484dfb5c0410 Mon Sep 17 00:00:00 2001 From: David Seel Date: Thu, 12 May 2022 12:08:46 -0400 Subject: [PATCH 02/11] Ensure contains parameter is a node --- packages/rrweb/src/record/mutation.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/rrweb/src/record/mutation.ts b/packages/rrweb/src/record/mutation.ts index 4637f6b743..62c9a8f7ad 100644 --- a/packages/rrweb/src/record/mutation.ts +++ b/packages/rrweb/src/record/mutation.ts @@ -267,10 +267,10 @@ export default class MutationBuffer { rootShadowHost = (rootShadowHost?.getRootNode?.() as ShadowRoot | undefined)?.host || null; - // ensure shadowHost is a Node, or doc.contains will throw an error + // ensure contains is passed a Node, or it will throw an error const notInDoc = !this.doc.contains(n) && - (!rootShadowHost || !this.doc.contains(rootShadowHost)); + (!rootShadowHost || (!(rootShadowHost instanceof Node) || !this.doc.contains(rootShadowHost))); if (!n.parentNode || notInDoc) { return; } From 6ae3795ff057287658a724ed3301a09aea6149a5 Mon Sep 17 00:00:00 2001 From: David Seel Date: Thu, 26 May 2022 12:35:49 -0400 Subject: [PATCH 03/11] Fix blockSelector blocking for closest nodes --- packages/rrweb-snapshot/src/snapshot.ts | 44 ++++++++++++------- packages/rrweb-snapshot/typings/snapshot.d.ts | 2 +- packages/rrweb/src/utils.ts | 4 +- 3 files changed, 31 insertions(+), 19 deletions(-) diff --git a/packages/rrweb-snapshot/src/snapshot.ts b/packages/rrweb-snapshot/src/snapshot.ts index 90ca8f3765..27f08fabe8 100644 --- a/packages/rrweb-snapshot/src/snapshot.ts +++ b/packages/rrweb-snapshot/src/snapshot.ts @@ -259,30 +259,42 @@ export function transformAttribute( } export function _isBlockedElement( - element: HTMLElement, + node: Node | null, blockClass: string | RegExp, blockSelector: string | null, ): boolean { - if (typeof blockClass === 'string') { - if (element.classList.contains(blockClass)) { - return true; - } - } else { - // tslint:disable-next-line: prefer-for-of - for (let eIndex = 0; eIndex < element.classList.length; eIndex++) { - const className = element.classList[eIndex]; - if (blockClass.test(className)) { - return true; + if (!node) { + return false; + } + if (node.nodeType === node.ELEMENT_NODE) { + let needBlock = false; + if (typeof blockClass === 'string') { + if ((node as HTMLElement).closest !== undefined) { + needBlock = (node as HTMLElement).closest('.' + blockClass) !== null; + } else { + needBlock = (node as HTMLElement).classList.contains(blockClass); } + } else { + (node as HTMLElement).classList.forEach((className) => { + if (blockClass.test(className)) { + needBlock = true; + } + }); + } + if (blockSelector) { + needBlock = needBlock || (node as HTMLElement).matches(blockSelector); } + + return needBlock || _isBlockedElement(node.parentNode, blockClass, blockSelector); } - if (blockSelector) { - return element.matches(blockSelector); + if (node.nodeType === node.TEXT_NODE) { + // check parent node since text node do not have class name + return _isBlockedElement(node.parentNode, blockClass, blockSelector); } - - return false; + return _isBlockedElement(node.parentNode, blockClass, blockSelector); } + export function needMaskingText( node: Node | null, maskTextClass: string | RegExp, @@ -439,7 +451,7 @@ function serializeNode( }; case n.ELEMENT_NODE: const needBlock = _isBlockedElement( - n as HTMLElement, + n, blockClass, blockSelector, ); diff --git a/packages/rrweb-snapshot/typings/snapshot.d.ts b/packages/rrweb-snapshot/typings/snapshot.d.ts index 0cdca439df..9dbf57c958 100644 --- a/packages/rrweb-snapshot/typings/snapshot.d.ts +++ b/packages/rrweb-snapshot/typings/snapshot.d.ts @@ -4,7 +4,7 @@ export declare const IGNORED_NODE = -2; export declare function absoluteToStylesheet(cssText: string | null, href: string): string; export declare function absoluteToDoc(doc: Document, attributeValue: string): string; export declare function transformAttribute(doc: Document, tagName: string, name: string, value: string): string; -export declare function _isBlockedElement(element: HTMLElement, blockClass: string | RegExp, blockSelector: string | null): boolean; +export declare function _isBlockedElement(node: Node | null, blockClass: string | RegExp, blockSelector: string | null): boolean; export declare function needMaskingText(node: Node | null, maskTextClass: string | RegExp, maskTextSelector: string | null): boolean; export declare function serializeNodeWithId(n: Node, options: { doc: Document; diff --git a/packages/rrweb/src/utils.ts b/packages/rrweb/src/utils.ts index 94d866ebde..0655412f14 100644 --- a/packages/rrweb/src/utils.ts +++ b/packages/rrweb/src/utils.ts @@ -188,7 +188,7 @@ export function isBlocked(node: Node | null, blockClass: blockClass, blockSelect let needBlock = false; if (typeof blockClass === 'string') { if ((node as HTMLElement).closest !== undefined) { - return (node as HTMLElement).closest('.' + blockClass) !== null; + needBlock = (node as HTMLElement).closest('.' + blockClass) !== null; } else { needBlock = (node as HTMLElement).classList.contains(blockClass); } @@ -200,7 +200,7 @@ export function isBlocked(node: Node | null, blockClass: blockClass, blockSelect }); } if (blockSelector) { - needBlock = (node as HTMLElement).matches(blockSelector); + needBlock = needBlock || (node as HTMLElement).matches(blockSelector); } return needBlock || isBlocked(node.parentNode, blockClass, blockSelector); From e4bd73c6c52182dc60fd47cb8cd0bab28655d70f Mon Sep 17 00:00:00 2001 From: David Seel Date: Mon, 30 May 2022 12:59:40 -0400 Subject: [PATCH 04/11] Fix integration test --- packages/rrweb/src/record/mutation.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/rrweb/src/record/mutation.ts b/packages/rrweb/src/record/mutation.ts index 3cda49874b..e27bbfe204 100644 --- a/packages/rrweb/src/record/mutation.ts +++ b/packages/rrweb/src/record/mutation.ts @@ -270,7 +270,7 @@ export default class MutationBuffer { // ensure contains is passed a Node, or it will throw an error const notInDoc = !this.doc.contains(n) && - (!rootShadowHost || (!(rootShadowHost instanceof Node) || !this.doc.contains(rootShadowHost))); + (!rootShadowHost || !this.doc.contains(rootShadowHost)); if (!n.parentNode || notInDoc) { return; } From 4ea83064b3eb19230d31c5adb02fb72a921c9b7b Mon Sep 17 00:00:00 2001 From: Filip Date: Wed, 8 Jun 2022 14:31:18 -0400 Subject: [PATCH 05/11] adding ignoreCSSAttributes to ignore the addition of certain css attributes --- packages/rrweb/src/record/index.ts | 6 + packages/rrweb/src/record/observer.ts | 12 +- packages/rrweb/src/types.ts | 2 + .../test/__snapshots__/record.test.ts.snap | 129 ++++++++++++++++++ packages/rrweb/test/record.test.ts | 28 ++++ 5 files changed, 176 insertions(+), 1 deletion(-) diff --git a/packages/rrweb/src/record/index.ts b/packages/rrweb/src/record/index.ts index 5a87dbc3e8..48eecc4cd1 100644 --- a/packages/rrweb/src/record/index.ts +++ b/packages/rrweb/src/record/index.ts @@ -68,7 +68,12 @@ function record( inlineImages = false, plugins, keepIframeSrcFn = () => false, + ignoreCSSAttributes = new Set([]), } = options; + options.ignoreCSSAttributes?.forEach((s) => { + console.log(`element: ${s}`); + }); + // runtime checks for user options if (!emit) { throw new Error('emit function is required'); @@ -438,6 +443,7 @@ function record( iframeManager, shadowDomManager, canvasManager, + ignoreCSSAttributes, plugins: plugins ?.filter((p) => p.observer) diff --git a/packages/rrweb/src/record/observer.ts b/packages/rrweb/src/record/observer.ts index 2cb9cf1616..41eee1871e 100644 --- a/packages/rrweb/src/record/observer.ts +++ b/packages/rrweb/src/record/observer.ts @@ -598,7 +598,7 @@ function initStyleSheetObserver( } function initStyleDeclarationObserver( - { styleDeclarationCb, mirror }: observerParam, + { styleDeclarationCb, mirror, ignoreCSSAttributes }: observerParam, { win }: { win: IWindow }, ): listenerHandler { const setProperty = win.CSSStyleDeclaration.prototype.setProperty; @@ -608,6 +608,11 @@ function initStyleDeclarationObserver( value: string, priority: string, ) { + // ignore this mutation if we do not care about this css attribute + if (ignoreCSSAttributes.has(property)) { + console.log('here?'); + return setProperty.apply(this, arguments); + } const id = mirror.getId(this.parentRule?.parentStyleSheet?.ownerNode); if (id !== -1) { styleDeclarationCb({ @@ -628,6 +633,11 @@ function initStyleDeclarationObserver( this: CSSStyleDeclaration, property: string, ) { + // ignore this mutation if we do not care about this css attribute + if (ignoreCSSAttributes.has(property)) { + console.log('here??'); + return setProperty.apply(this, arguments); + } const id = mirror.getId(this.parentRule?.parentStyleSheet?.ownerNode); if (id !== -1) { styleDeclarationCb({ diff --git a/packages/rrweb/src/types.ts b/packages/rrweb/src/types.ts index a74a091c75..0678da406e 100644 --- a/packages/rrweb/src/types.ts +++ b/packages/rrweb/src/types.ts @@ -236,6 +236,7 @@ export type recordOptions = { maskInputFn?: MaskInputFn; maskTextFn?: MaskTextFn; slimDOMOptions?: SlimDOMOptions | 'all' | true; + ignoreCSSAttributes?:Set; inlineStylesheet?: boolean; hooks?: hooksParam; packFn?: PackFn; @@ -282,6 +283,7 @@ export type observerParam = { iframeManager: IframeManager; shadowDomManager: ShadowDomManager; canvasManager: CanvasManager; + ignoreCSSAttributes:Set; plugins: Array<{ observer: Function; callback: Function; diff --git a/packages/rrweb/test/__snapshots__/record.test.ts.snap b/packages/rrweb/test/__snapshots__/record.test.ts.snap index 74c951afa8..2bf3ada823 100644 --- a/packages/rrweb/test/__snapshots__/record.test.ts.snap +++ b/packages/rrweb/test/__snapshots__/record.test.ts.snap @@ -640,6 +640,135 @@ exports[`record captures stylesheet rules 1`] = ` ]" `; +exports[`record does not capture style property changes when the css element is ignored 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\\": [], + \\"id\\": 4 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 6 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"input\\", + \\"attributes\\": { + \\"type\\": \\"text\\", + \\"size\\": \\"40\\" + }, + \\"childNodes\\": [], + \\"id\\": 7 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\\\n \\", + \\"id\\": 8 + } + ], + \\"id\\": 5 + } + ], + \\"id\\": 3 + } + ], + \\"id\\": 1 + }, + \\"initialOffset\\": { + \\"left\\": 0, + \\"top\\": 0 + } + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [], + \\"removes\\": [], + \\"adds\\": [ + { + \\"parentId\\": 4, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"style\\", + \\"attributes\\": { + \\"_cssText\\": \\"body { background: rgb(0, 0, 0); }\\" + }, + \\"childNodes\\": [], + \\"id\\": 9 + } + } + ] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 13, + \\"id\\": 9, + \\"set\\": { + \\"property\\": \\"color\\", + \\"value\\": \\"green\\" + }, + \\"index\\": [ + 0 + ] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 13, + \\"id\\": 9, + \\"remove\\": { + \\"property\\": \\"background\\" + }, + \\"index\\": [ + 0 + ] + } + } +]" +`; + exports[`record iframes captures stylesheet mutations in iframes 1`] = ` "[ { diff --git a/packages/rrweb/test/record.test.ts b/packages/rrweb/test/record.test.ts index 1d36fa5ac1..37ce5d2437 100644 --- a/packages/rrweb/test/record.test.ts +++ b/packages/rrweb/test/record.test.ts @@ -345,6 +345,34 @@ describe('record', function (this: ISuite) { assertSnapshot(ctx.events); }); + // it('does not capture style property changes when the css element is ignored', async () => { + // await ctx.page.evaluate(() => { + // const { record } = ((window as unknown) as IWindow).rrweb; + + // record({ + // emit: ((window as unknown) as IWindow).emit, + // ignoreCSSAttributes: new Set(['color']), + // }); + + // 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 ctx.page.waitForTimeout(50); + // assertSnapshot(ctx.events); + // }); + it('captures inserted style text nodes correctly', async () => { await ctx.page.evaluate(() => { const { record } = ((window as unknown) as IWindow).rrweb; From a55b7f88161cda732954c8076d94270667aa0efa Mon Sep 17 00:00:00 2001 From: Filip Date: Wed, 8 Jun 2022 14:41:43 -0400 Subject: [PATCH 06/11] tested ignoreCSSAttributes --- packages/rrweb/src/record/index.ts | 3 -- packages/rrweb/src/record/observer.ts | 2 - .../test/__snapshots__/record.test.ts.snap | 2 +- packages/rrweb/test/record.test.ts | 37 +++++-------------- 4 files changed, 10 insertions(+), 34 deletions(-) diff --git a/packages/rrweb/src/record/index.ts b/packages/rrweb/src/record/index.ts index 48eecc4cd1..d9f95f767b 100644 --- a/packages/rrweb/src/record/index.ts +++ b/packages/rrweb/src/record/index.ts @@ -70,9 +70,6 @@ function record( keepIframeSrcFn = () => false, ignoreCSSAttributes = new Set([]), } = options; - options.ignoreCSSAttributes?.forEach((s) => { - console.log(`element: ${s}`); - }); // runtime checks for user options if (!emit) { diff --git a/packages/rrweb/src/record/observer.ts b/packages/rrweb/src/record/observer.ts index 41eee1871e..f0c81a80bd 100644 --- a/packages/rrweb/src/record/observer.ts +++ b/packages/rrweb/src/record/observer.ts @@ -610,7 +610,6 @@ function initStyleDeclarationObserver( ) { // ignore this mutation if we do not care about this css attribute if (ignoreCSSAttributes.has(property)) { - console.log('here?'); return setProperty.apply(this, arguments); } const id = mirror.getId(this.parentRule?.parentStyleSheet?.ownerNode); @@ -635,7 +634,6 @@ function initStyleDeclarationObserver( ) { // ignore this mutation if we do not care about this css attribute if (ignoreCSSAttributes.has(property)) { - console.log('here??'); return setProperty.apply(this, arguments); } const id = mirror.getId(this.parentRule?.parentStyleSheet?.ownerNode); diff --git a/packages/rrweb/test/__snapshots__/record.test.ts.snap b/packages/rrweb/test/__snapshots__/record.test.ts.snap index 2bf3ada823..0fde04f2a2 100644 --- a/packages/rrweb/test/__snapshots__/record.test.ts.snap +++ b/packages/rrweb/test/__snapshots__/record.test.ts.snap @@ -478,7 +478,7 @@ exports[`record captures style property changes 1`] = ` \\"source\\": 13, \\"id\\": 9, \\"set\\": { - \\"property\\": \\"color\\", + \\"property\\": \\"border-color\\", \\"value\\": \\"green\\" }, \\"index\\": [ diff --git a/packages/rrweb/test/record.test.ts b/packages/rrweb/test/record.test.ts index 37ce5d2437..32a450a78e 100644 --- a/packages/rrweb/test/record.test.ts +++ b/packages/rrweb/test/record.test.ts @@ -324,6 +324,7 @@ describe('record', function (this: ISuite) { record({ emit: ((window as unknown) as IWindow).emit, + ignoreCSSAttributes: new Set(['color']), }); const styleElement = document.createElement('style'); @@ -332,10 +333,18 @@ describe('record', function (this: ISuite) { const styleSheet = styleElement.sheet; styleSheet.insertRule('body { background: #000; }'); setTimeout(() => { + // should be ignored (styleSheet.cssRules[0] as CSSStyleRule).style.setProperty( 'color', 'green', ); + + // should be captured because we did not block it + (styleSheet.cssRules[0] as CSSStyleRule).style.setProperty( + 'border-color', + 'green', + ); + (styleSheet.cssRules[0] as CSSStyleRule).style.removeProperty( 'background', ); @@ -345,34 +354,6 @@ describe('record', function (this: ISuite) { assertSnapshot(ctx.events); }); - // it('does not capture style property changes when the css element is ignored', async () => { - // await ctx.page.evaluate(() => { - // const { record } = ((window as unknown) as IWindow).rrweb; - - // record({ - // emit: ((window as unknown) as IWindow).emit, - // ignoreCSSAttributes: new Set(['color']), - // }); - - // 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 ctx.page.waitForTimeout(50); - // assertSnapshot(ctx.events); - // }); - it('captures inserted style text nodes correctly', async () => { await ctx.page.evaluate(() => { const { record } = ((window as unknown) as IWindow).rrweb; From 3341e7f209669ab83986f430b6cde25f15819e05 Mon Sep 17 00:00:00 2001 From: David Seel Date: Tue, 14 Jun 2022 16:07:34 -0400 Subject: [PATCH 07/11] Update test snapshot --- .../test/__snapshots__/record.test.ts.snap | 129 ------------------ 1 file changed, 129 deletions(-) diff --git a/packages/rrweb/test/__snapshots__/record.test.ts.snap b/packages/rrweb/test/__snapshots__/record.test.ts.snap index f856ffca3a..3bc0c09598 100644 --- a/packages/rrweb/test/__snapshots__/record.test.ts.snap +++ b/packages/rrweb/test/__snapshots__/record.test.ts.snap @@ -640,135 +640,6 @@ exports[`record captures stylesheet rules 1`] = ` ]" `; -exports[`record does not capture style property changes when the css element is ignored 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\\": [], - \\"id\\": 4 - }, - { - \\"type\\": 2, - \\"tagName\\": \\"body\\", - \\"attributes\\": {}, - \\"childNodes\\": [ - { - \\"type\\": 3, - \\"textContent\\": \\"\\\\n \\", - \\"id\\": 6 - }, - { - \\"type\\": 2, - \\"tagName\\": \\"input\\", - \\"attributes\\": { - \\"type\\": \\"text\\", - \\"size\\": \\"40\\" - }, - \\"childNodes\\": [], - \\"id\\": 7 - }, - { - \\"type\\": 3, - \\"textContent\\": \\"\\\\n \\\\n \\\\n \\", - \\"id\\": 8 - } - ], - \\"id\\": 5 - } - ], - \\"id\\": 3 - } - ], - \\"id\\": 1 - }, - \\"initialOffset\\": { - \\"left\\": 0, - \\"top\\": 0 - } - } - }, - { - \\"type\\": 3, - \\"data\\": { - \\"source\\": 0, - \\"texts\\": [], - \\"attributes\\": [], - \\"removes\\": [], - \\"adds\\": [ - { - \\"parentId\\": 4, - \\"nextId\\": null, - \\"node\\": { - \\"type\\": 2, - \\"tagName\\": \\"style\\", - \\"attributes\\": { - \\"_cssText\\": \\"body { background: rgb(0, 0, 0); }\\" - }, - \\"childNodes\\": [], - \\"id\\": 9 - } - } - ] - } - }, - { - \\"type\\": 3, - \\"data\\": { - \\"source\\": 13, - \\"id\\": 9, - \\"set\\": { - \\"property\\": \\"color\\", - \\"value\\": \\"green\\" - }, - \\"index\\": [ - 0 - ] - } - }, - { - \\"type\\": 3, - \\"data\\": { - \\"source\\": 13, - \\"id\\": 9, - \\"remove\\": { - \\"property\\": \\"background\\" - }, - \\"index\\": [ - 0 - ] - } - } -]" -`; - exports[`record iframes captures stylesheet mutations in iframes 1`] = ` "[ { From 05241379f6c3f6020ac157fe0dd61af568c3aff0 Mon Sep 17 00:00:00 2001 From: David Seel Date: Fri, 19 Aug 2022 11:55:05 -0400 Subject: [PATCH 08/11] swapped the wrapping of htmlelement to be element --- packages/rrweb/src/record/shadow-dom-manager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/rrweb/src/record/shadow-dom-manager.ts b/packages/rrweb/src/record/shadow-dom-manager.ts index 90a1d321d6..b2aeae01b2 100644 --- a/packages/rrweb/src/record/shadow-dom-manager.ts +++ b/packages/rrweb/src/record/shadow-dom-manager.ts @@ -39,7 +39,7 @@ export class ShadowDomManager { const manager = this; this.restorePatches.push( patch( - HTMLElement.prototype, + Element.prototype, 'attachShadow', function (original: (init: ShadowRootInit) => ShadowRoot) { return function (this: HTMLElement, option: ShadowRootInit) { From 0f1c6000a535a6143a19e6157241f2b5ab9f7dbb Mon Sep 17 00:00:00 2001 From: David Seel Date: Fri, 19 Aug 2022 12:11:01 -0400 Subject: [PATCH 09/11] Fix linter errors --- packages/rrweb/src/record/observer.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/rrweb/src/record/observer.ts b/packages/rrweb/src/record/observer.ts index 320b9bc226..5322df3cd2 100644 --- a/packages/rrweb/src/record/observer.ts +++ b/packages/rrweb/src/record/observer.ts @@ -629,7 +629,7 @@ function initStyleDeclarationObserver( ) { // ignore this mutation if we do not care about this css attribute if (ignoreCSSAttributes.has(property)) { - return setProperty.apply(this, arguments); + return setProperty.apply(this, [property, value, priority]); } const id = mirror.getId(this.parentRule?.parentStyleSheet?.ownerNode); if (id !== -1) { @@ -654,7 +654,7 @@ function initStyleDeclarationObserver( ) { // ignore this mutation if we do not care about this css attribute if (ignoreCSSAttributes.has(property)) { - return setProperty.apply(this, arguments); + return removeProperty.apply(this, [property]); } const id = mirror.getId(this.parentRule?.parentStyleSheet?.ownerNode); if (id !== -1) { @@ -767,7 +767,7 @@ function initFontObserver({ fontCb, doc }: observerParam): listenerHandler { } function initSelectionObserver(param: observerParam): listenerHandler { - const { doc, mirror, blockClass, selectionCb } = param; + const { doc, mirror, blockClass, blockSelector, selectionCb } = param; let collapsed = true; const updateSelection = () => { @@ -786,8 +786,8 @@ function initSelectionObserver(param: observerParam): listenerHandler { const { startContainer, startOffset, endContainer, endOffset } = range; const blocked = - isBlocked(startContainer, blockClass, true) || - isBlocked(endContainer, blockClass, true); + isBlocked(startContainer, blockClass, blockSelector, true) || + isBlocked(endContainer, blockClass, blockSelector, true); if (blocked) continue; From bd4d1013beeafe792cc05a810d58b5f1330e46ea Mon Sep 17 00:00:00 2001 From: David Seel Date: Mon, 29 Aug 2022 11:08:27 -0400 Subject: [PATCH 10/11] Address MR feedback --- guide.md | 1 + guide.zh_CN.md | 1 + packages/rrweb-snapshot/src/snapshot.ts | 40 +++++++++---------------- packages/rrweb/src/utils.ts | 3 +- 4 files changed, 18 insertions(+), 27 deletions(-) diff --git a/guide.md b/guide.md index 54e9f9f94e..6d2173df37 100644 --- a/guide.md +++ b/guide.md @@ -143,6 +143,7 @@ The parameter of `rrweb.record` accepts the following options. | blockClass | 'rr-block' | Use a string or RegExp to configure which elements should be blocked, refer to the [privacy](#privacy) chapter | | blockSelector | null | Use a string to configure which selector should be blocked, refer to the [privacy](#privacy) chapter | | ignoreClass | 'rr-ignore' | Use a string or RegExp to configure which elements should be ignored, refer to the [privacy](#privacy) chapter | +| ignoreCSSAttributes | null | array of CSS attributes that should be ignored | | maskTextClass | 'rr-mask' | Use a string or RegExp to configure which elements should be masked, refer to the [privacy](#privacy) chapter | | maskTextSelector | null | Use a string to configure which selector should be masked, refer to the [privacy](#privacy) chapter | | maskAllInputs | false | mask all input content as \* | diff --git a/guide.zh_CN.md b/guide.zh_CN.md index d69f5c77c6..926357121d 100644 --- a/guide.zh_CN.md +++ b/guide.zh_CN.md @@ -139,6 +139,7 @@ setInterval(save, 10 * 1000); | blockClass | 'rr-block' | 字符串或正则表达式,可用于自定义屏蔽元素的类名,详见[“隐私”](#隐私)章节 | | blockSelector | null | 所有 element.matches(blockSelector)为 true 的元素都不会被录制,回放时取而代之的是一个同等宽高的占位元素 | | ignoreClass | 'rr-ignore' | 字符串或正则表达式,可用于自定义忽略元素的类名,详见[“隐私”](#隐私)章节 | +| ignoreCSSAttributes | null | 应该被忽略的 CSS 属性数组 | | maskTextClass | 'rr-mask' | 字符串或正则表达式,可用于自定义忽略元素 text 内容的类名,详见[“隐私”](#隐私)章节 | | maskTextSelector | null | 所有 element.matches(maskTextSelector)为 true 的元素及其子元素的 text 内容将会被屏蔽 | | maskAllInputs | false | 将所有输入内容记录为 \* | diff --git a/packages/rrweb-snapshot/src/snapshot.ts b/packages/rrweb-snapshot/src/snapshot.ts index 462aa11de6..38df3b6954 100644 --- a/packages/rrweb-snapshot/src/snapshot.ts +++ b/packages/rrweb-snapshot/src/snapshot.ts @@ -276,39 +276,27 @@ export function transformAttribute( } export function _isBlockedElement( - node: Node | null, + element: HTMLElement, blockClass: string | RegExp, blockSelector: string | null, ): boolean { - if (!node) { - return false; - } - if (node.nodeType === node.ELEMENT_NODE) { - let needBlock = false; - if (typeof blockClass === 'string') { - if ((node as HTMLElement).closest !== undefined) { - needBlock = (node as HTMLElement).closest('.' + blockClass) !== null; - } else { - needBlock = (node as HTMLElement).classList.contains(blockClass); - } - } else { - (node as HTMLElement).classList.forEach((className) => { - if (blockClass.test(className)) { - needBlock = true; - } - }); + if (typeof blockClass === 'string') { + if (element.classList.contains(blockClass)) { + return true; } - if (blockSelector) { - needBlock = needBlock || (node as HTMLElement).matches(blockSelector); + } else { + for (let eIndex = element.classList.length; eIndex--; ) { + const className = element.classList[eIndex]; + if (blockClass.test(className)) { + return true; + } } - - return needBlock || _isBlockedElement(node.parentNode, blockClass, blockSelector); } - if (node.nodeType === node.TEXT_NODE) { - // check parent node since text node do not have class name - return _isBlockedElement(node.parentNode, blockClass, blockSelector); + if (blockSelector) { + return element.matches(blockSelector); } - return _isBlockedElement(node.parentNode, blockClass, blockSelector); + + return false; } export function classMatchesRegex( diff --git a/packages/rrweb/src/utils.ts b/packages/rrweb/src/utils.ts index 435a887f54..209d99d22c 100644 --- a/packages/rrweb/src/utils.ts +++ b/packages/rrweb/src/utils.ts @@ -214,8 +214,9 @@ export function isBlocked( } if (blockSelector) { if ((node as HTMLElement).matches(blockSelector)) return true; + if (checkAncestors && el.closest(blockSelector) !== null) return true; } - return isBlocked(node.parentNode, blockClass, blockSelector, checkAncestors); + return false; } export function isSerialized(n: Node, mirror: Mirror): boolean { From 55387b0cf6c8f258bc9064c8c2aaa26dfeb96ae3 Mon Sep 17 00:00:00 2001 From: David Seel Date: Mon, 29 Aug 2022 11:26:18 -0400 Subject: [PATCH 11/11] Rebase --- packages/rrweb/src/record/observers/canvas/canvas-manager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/rrweb/src/record/observers/canvas/canvas-manager.ts b/packages/rrweb/src/record/observers/canvas/canvas-manager.ts index c1cb994c6c..5b2a874d01 100644 --- a/packages/rrweb/src/record/observers/canvas/canvas-manager.ts +++ b/packages/rrweb/src/record/observers/canvas/canvas-manager.ts @@ -143,7 +143,7 @@ export class CanvasManager { const getCanvas = (): HTMLCanvasElement[] => { const matchedCanvas: HTMLCanvasElement[] = []; win.document.querySelectorAll('canvas').forEach(canvas => { - if (!isBlocked(canvas, blockClass, true)) { + if (!isBlocked(canvas, blockClass, blockSelector, true)) { matchedCanvas.push(canvas); } })