diff --git a/packages/rrweb-snapshot/src/snapshot.ts b/packages/rrweb-snapshot/src/snapshot.ts index 4dba1f536c..b1e8d8740f 100644 --- a/packages/rrweb-snapshot/src/snapshot.ts +++ b/packages/rrweb-snapshot/src/snapshot.ts @@ -241,6 +241,7 @@ export function transformAttribute( name: string, value: string | null, maskAllText: boolean, + unmaskTextSelector: string | undefined | null, maskTextFn: MaskTextFn | undefined, ): string | null { if (!value) { @@ -266,13 +267,7 @@ export function transformAttribute( return absoluteToDoc(doc, value); } else if ( maskAllText && - (['placeholder', 'title', 'aria-label'].indexOf(name) > -1 || - (tagName === 'input' && - name === 'value' && - element.getAttribute('type') && - ['submit', 'button'].indexOf( - element.getAttribute('type')!.toLowerCase(), - ) > -1)) + _shouldMaskAttribute(element, name, tagName, unmaskTextSelector) ) { return maskTextFn ? maskTextFn(value) : defaultMaskFn(value); } @@ -280,6 +275,26 @@ export function transformAttribute( return value; } +function _shouldMaskAttribute( + element: HTMLElement, + attribute: string, + tagName: string, + unmaskTextSelector: string | undefined | null, +): boolean { + if (unmaskTextSelector && element.matches(unmaskTextSelector)) { + return false; + } + return ( + ['placeholder', 'title', 'aria-label'].indexOf(attribute) > -1 || + (tagName === 'input' && + attribute === 'value' && + element.hasAttribute('type') && + ['submit', 'button'].indexOf( + element.getAttribute('type')!.toLowerCase(), + ) > -1) + ); +} + export function _isBlockedElement( element: HTMLElement, blockClass: string | RegExp, @@ -521,6 +536,7 @@ function serializeNode( name, value, maskAllText, + unmaskTextSelector, maskTextFn, ); } diff --git a/packages/rrweb-snapshot/typings/snapshot.d.ts b/packages/rrweb-snapshot/typings/snapshot.d.ts index 86233a5def..56a967f751 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, 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/mutation.ts b/packages/rrweb/src/record/mutation.ts index f6767ecb48..849fca54a8 100644 --- a/packages/rrweb/src/record/mutation.ts +++ b/packages/rrweb/src/record/mutation.ts @@ -562,6 +562,7 @@ export default class MutationBuffer { m.attributeName!, value!, this.maskAllText, + this.unmaskTextSelector, this.maskTextFn ); } diff --git a/packages/rrweb/test/__snapshots__/integration.test.ts.snap b/packages/rrweb/test/__snapshots__/integration.test.ts.snap index b508216b30..f0dd3aa94d 100644 --- a/packages/rrweb/test/__snapshots__/integration.test.ts.snap +++ b/packages/rrweb/test/__snapshots__/integration.test.ts.snap @@ -4097,6 +4097,366 @@ exports[`record integration tests can use maskInputOptions to configure which ty ]" `; +exports[`record integration tests correctly masks & unmasks attribute values 1`] = ` +"[ + { + "type": 0, + "data": {} + }, + { + "type": 1, + "data": {} + }, + { + "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": { + "lang": "en" + }, + "childNodes": [ + { + "type": 2, + "tagName": "head", + "attributes": {}, + "childNodes": [ + { + "type": 3, + "textContent": "\\n ", + "id": 5 + }, + { + "type": 2, + "tagName": "meta", + "attributes": { + "charset": "UTF-8" + }, + "childNodes": [], + "id": 6 + }, + { + "type": 3, + "textContent": "\\n ", + "id": 7 + }, + { + "type": 2, + "tagName": "meta", + "attributes": { + "name": "viewport", + "content": "width=device-width, initial-scale=1.0" + }, + "childNodes": [], + "id": 8 + }, + { + "type": 3, + "textContent": "\\n ", + "id": 9 + }, + { + "type": 2, + "tagName": "meta", + "attributes": { + "http-equiv": "X-UA-Compatible", + "content": "ie=edge" + }, + "childNodes": [], + "id": 10 + }, + { + "type": 3, + "textContent": "\\n ", + "id": 11 + }, + { + "type": 2, + "tagName": "title", + "attributes": {}, + "childNodes": [ + { + "type": 3, + "textContent": "********** ****", + "id": 13 + } + ], + "id": 12 + }, + { + "type": 3, + "textContent": "\\n ", + "id": 14 + } + ], + "id": 4 + }, + { + "type": 3, + "textContent": "\\n\\n ", + "id": 15 + }, + { + "type": 2, + "tagName": "body", + "attributes": {}, + "childNodes": [ + { + "type": 3, + "textContent": "\\n ", + "id": 17 + }, + { + "type": 2, + "tagName": "form", + "attributes": {}, + "childNodes": [ + { + "type": 3, + "textContent": "\\n ", + "id": 19 + }, + { + "type": 2, + "tagName": "div", + "attributes": { + "title": "Test title", + "aria-label": "Test aria label", + "class": "rr-unmask" + }, + "childNodes": [ + { + "type": 3, + "textContent": "\\n Test content\\n ", + "id": 21 + } + ], + "id": 20 + }, + { + "type": 3, + "textContent": "\\n\\n ", + "id": 22 + }, + { + "type": 2, + "tagName": "div", + "attributes": { + "title": "**** ***** *", + "aria-label": "**** **** ***** *" + }, + "childNodes": [ + { + "type": 3, + "textContent": "\\n **** ******* *\\n ", + "id": 24 + } + ], + "id": 23 + }, + { + "type": 3, + "textContent": "\\n\\n ", + "id": 25 + }, + { + "type": 2, + "tagName": "input", + "attributes": { + "type": "text", + "placeholder": "Test placeholder 1", + "class": "rr-unmask" + }, + "childNodes": [], + "id": 26 + }, + { + "type": 3, + "textContent": "\\n ", + "id": 27 + }, + { + "type": 2, + "tagName": "input", + "attributes": { + "type": "text", + "placeholder": "**** *********** *", + "class": "" + }, + "childNodes": [], + "id": 28 + }, + { + "type": 3, + "textContent": "\\n\\n ", + "id": 29 + }, + { + "type": 2, + "tagName": "input", + "attributes": { + "type": "submit", + "value": "Submit button 1", + "class": "rr-unmask" + }, + "childNodes": [], + "id": 30 + }, + { + "type": 3, + "textContent": "\\n ", + "id": 31 + }, + { + "type": 2, + "tagName": "input", + "attributes": { + "type": "submit", + "value": "****** ****** *", + "class": "" + }, + "childNodes": [], + "id": 32 + }, + { + "type": 3, + "textContent": "\\n\\n ", + "id": 33 + }, + { + "type": 2, + "tagName": "input", + "attributes": { + "type": "button", + "value": "****** *", + "class": "" + }, + "childNodes": [], + "id": 34 + }, + { + "type": 3, + "textContent": "\\n ", + "id": 35 + } + ], + "id": 18 + }, + { + "type": 3, + "textContent": "\\n \\n ", + "id": 36 + }, + { + "type": 2, + "tagName": "script", + "attributes": {}, + "childNodes": [ + { + "type": 3, + "textContent": "SCRIPT_PLACEHOLDER", + "id": 38 + } + ], + "id": 37 + }, + { + "type": 3, + "textContent": "\\n \\n \\n\\n", + "id": 39 + } + ], + "id": 16 + } + ], + "id": 3 + } + ], + "id": 1 + }, + "initialOffset": { + "left": 0, + "top": 0 + } + } + }, + { + "type": 3, + "data": { + "source": 0, + "texts": [], + "attributes": [ + { + "id": 20, + "attributes": { + "title": "new title", + "aria-label": "new aria label" + } + }, + { + "id": 23, + "attributes": { + "title": "*** *****", + "aria-label": "*** **** *****" + } + }, + { + "id": 26, + "attributes": { + "placeholder": "new placeholder" + } + }, + { + "id": 28, + "attributes": { + "placeholder": "*** ***********" + } + }, + { + "id": 30, + "attributes": { + "value": "new value" + } + }, + { + "id": 32, + "attributes": { + "value": "new value" + } + }, + { + "id": 34, + "attributes": { + "value": "new value" + } + } + ], + "removes": [], + "adds": [] + } + } +]" +`; + exports[`record integration tests handles null attribute values 1`] = ` "[ { diff --git a/packages/rrweb/test/html/attributes-mask.html b/packages/rrweb/test/html/attributes-mask.html new file mode 100644 index 0000000000..cab12e9bf3 --- /dev/null +++ b/packages/rrweb/test/html/attributes-mask.html @@ -0,0 +1,29 @@ + + +
+ + + +