diff --git a/packages/rrweb-snapshot/src/snapshot.ts b/packages/rrweb-snapshot/src/snapshot.ts index b1e8d8740f..3b8cda963f 100644 --- a/packages/rrweb-snapshot/src/snapshot.ts +++ b/packages/rrweb-snapshot/src/snapshot.ts @@ -237,8 +237,8 @@ function getHref() { export function transformAttribute( doc: Document, element: HTMLElement, - tagName: string, - name: string, + _tagName: string, + _name: string, value: string | null, maskAllText: boolean, unmaskTextSelector: string | undefined | null, @@ -248,6 +248,9 @@ export function transformAttribute( return value; } + const name = _name.toLowerCase(); + const tagName = _tagName.toLowerCase(); + // relative path in attribute if (name === 'src' || name === 'href') { return absoluteToDoc(doc, value); diff --git a/packages/rrweb-snapshot/typings/snapshot.d.ts b/packages/rrweb-snapshot/typings/snapshot.d.ts index 56a967f751..2fcf57508f 100644 --- a/packages/rrweb-snapshot/typings/snapshot.d.ts +++ b/packages/rrweb-snapshot/typings/snapshot.d.ts @@ -2,7 +2,7 @@ import { serializedNodeWithId, INode, idNodeMap, MaskInputOptions, SlimDOMOption 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, element: HTMLElement, tagName: string, name: string, value: string | null, maskAllText: boolean, unmaskTextSelector: string | undefined | null, maskTextFn: MaskTextFn | undefined): string | null; +export declare function transformAttribute(doc: Document, element: HTMLElement, _tagName: string, _name: string, value: string | null, maskAllText: boolean, unmaskTextSelector: string | undefined | null, maskTextFn: MaskTextFn | undefined): string | null; export declare function _isBlockedElement(element: HTMLElement, blockClass: string | RegExp, blockSelector: string | null, unblockSelector: string | null): boolean; export declare function needMaskingText(node: Node | null, maskTextClass: string | RegExp, maskTextSelector: string | null, unmaskTextSelector: string | null, maskAllText: boolean): boolean; export declare function serializeNodeWithId(n: Node | INode, options: { diff --git a/packages/rrweb/src/record/observer.ts b/packages/rrweb/src/record/observer.ts index 3894322062..e5f0efb5c1 100644 --- a/packages/rrweb/src/record/observer.ts +++ b/packages/rrweb/src/record/observer.ts @@ -113,7 +113,7 @@ export function initMutationObserver( // If this callback returns `false`, we do not want to process the mutations // This can be used to e.g. do a manual full snapshot when mutations become too large, or similar. if (options.onMutation && options.onMutation(mutations) === false) { - return; + return; } mutationBuffer.processMutations(mutations); }), @@ -231,7 +231,9 @@ function initMouseInteractionObserver({ const getHandler = (eventKey: keyof typeof MouseInteractions) => { return (event: MouseEvent | TouchEvent) => { const target = getEventTarget(event) as Node; - if (isBlocked(target as Node, blockClass, blockSelector, unblockSelector)) { + if ( + isBlocked(target as Node, blockClass, blockSelector, unblockSelector) + ) { return; } const e = isTouchEvent(event) ? event.changedTouches[0] : event; @@ -275,11 +277,20 @@ export function initScrollObserver({ sampling, }: Pick< observerParam, - 'scrollCb' | 'doc' | 'mirror' | 'blockClass' | 'blockSelector' | 'unblockSelector' | 'sampling' + | 'scrollCb' + | 'doc' + | 'mirror' + | 'blockClass' + | 'blockSelector' + | 'unblockSelector' + | 'sampling' >): listenerHandler { const updatePosition = throttle((evt) => { const target = getEventTarget(evt); - if (!target || isBlocked(target as Node, blockClass, blockSelector, unblockSelector)) { + if ( + !target || + isBlocked(target as Node, blockClass, blockSelector, unblockSelector) + ) { return; } const id = mirror.getId(target as INode); @@ -350,17 +361,18 @@ function initInputObserver({ }: observerParam): listenerHandler { function eventHandler(event: Event) { let target = getEventTarget(event); + const tagName = target && (target as Element).tagName; + const userTriggered = event.isTrusted; /** * If a site changes the value 'selected' of an option element, the value of its parent element, usually a select element, will be changed as well. * We can treat this change as a value change of the select element the current target belongs to. */ - if (target && (target as Element).tagName === 'OPTION') - target = (target as Element).parentElement; + if (tagName === 'OPTION') target = (target as Element).parentElement; if ( !target || - !(target as Element).tagName || - INPUT_TAGS.indexOf((target as Element).tagName) < 0 || + !tagName || + INPUT_TAGS.indexOf(tagName) < 0 || isBlocked(target as Node, blockClass, blockSelector, unblockSelector) ) { return; @@ -386,7 +398,7 @@ function initInputObserver({ hasInputMaskOptions({ maskInputOptions, maskInputSelector, - tagName: (target as HTMLElement).tagName, + tagName, type, }) ) { @@ -395,7 +407,7 @@ function initInputObserver({ maskInputOptions, maskInputSelector, unmaskInputSelector, - tagName: (target as HTMLElement).tagName, + tagName, type, value: text, maskInputFn, @@ -754,7 +766,10 @@ function initMediaInteractionObserver({ throttle( callbackWrapper((event: Event) => { const target = getEventTarget(event); - if (!target || isBlocked(target as Node, blockClass, blockSelector, unblockSelector)) { + if ( + !target || + isBlocked(target as Node, blockClass, blockSelector, unblockSelector) + ) { return; } const { currentTime, volume, muted } = target as HTMLMediaElement; diff --git a/packages/rrweb/test/__snapshots__/integration.test.ts.snap b/packages/rrweb/test/__snapshots__/integration.test.ts.snap index f0dd3aa94d..b86ccbd08f 100644 --- a/packages/rrweb/test/__snapshots__/integration.test.ts.snap +++ b/packages/rrweb/test/__snapshots__/integration.test.ts.snap @@ -4440,13 +4440,13 @@ exports[`record integration tests correctly masks & unmasks attribute values 1`] { "id": 32, "attributes": { - "value": "new value" + "value": "*** *****" } }, { "id": 34, "attributes": { - "value": "new value" + "value": "*** *****" } } ], @@ -6524,6 +6524,23 @@ exports[`record integration tests should mask text in form elements 1`] = ` "top": 0 } } + }, + { + "type": 3, + "data": { + "source": 0, + "texts": [], + "attributes": [ + { + "id": 87, + "attributes": { + "value": "*** *****" + } + } + ], + "removes": [], + "adds": [] + } } ]" `; diff --git a/packages/rrweb/test/integration.test.ts b/packages/rrweb/test/integration.test.ts index 30a526675b..54afdf31e4 100644 --- a/packages/rrweb/test/integration.test.ts +++ b/packages/rrweb/test/integration.test.ts @@ -598,6 +598,13 @@ describe('record integration tests', function (this: ISuite) { getHtml.call(this, 'form.html', { maskAllText: true }), ); + // Ensure also masked when we change stuff + await page.evaluate(() => { + document + .querySelector('input[type="submit"]') + ?.setAttribute('value', 'new value'); + }); + const snapshots = await page.evaluate('window.snapshots'); assertSnapshot(snapshots); });