diff --git a/packages/rrweb-snapshot/src/snapshot.ts b/packages/rrweb-snapshot/src/snapshot.ts index f1092f2c25..d779193ce8 100644 --- a/packages/rrweb-snapshot/src/snapshot.ts +++ b/packages/rrweb-snapshot/src/snapshot.ts @@ -236,6 +236,7 @@ function getHref() { export function transformAttribute( doc: Document, + element: HTMLElement, tagName: string, name: string, value: string | null, @@ -265,7 +266,13 @@ export function transformAttribute( return absoluteToDoc(doc, value); } else if ( maskAllText && - ['placeholder', 'title', 'aria-label'].indexOf(name) > -1 + (['placeholder', 'title', 'aria-label'].indexOf(name) > -1 || + (tagName === 'input' && + name === 'value' && + element.getAttribute('type') && + ['submit', 'button'].indexOf( + element.getAttribute('type')!.toLowerCase(), + ) > -1)) ) { return maskTextFn ? maskTextFn(value) : defaultMaskFn(value); } @@ -468,8 +475,8 @@ function serializeNode( } = options; // Only record root id when document object is not the base document let rootId: number | undefined; - if (((doc as unknown) as INode).__sn) { - const docId = ((doc as unknown) as INode).__sn.id; + if ((doc as unknown as INode).__sn) { + const docId = (doc as unknown as INode).__sn.id; rootId = docId === 1 ? undefined : docId; } switch (n.nodeType) { @@ -509,6 +516,7 @@ function serializeNode( if (!skipAttribute(tagName, name, value)) { attributes[name] = transformAttribute( doc, + n as HTMLElement, tagName, name, value, @@ -773,7 +781,9 @@ function serializeNode( } } -function lowerIfExists(maybeAttr: string | number | boolean | null | undefined): string { +function lowerIfExists( + maybeAttr: string | number | boolean | null | undefined, +): string { if (maybeAttr === undefined || maybeAttr === null) { return ''; } else { diff --git a/packages/rrweb-snapshot/typings/snapshot.d.ts b/packages/rrweb-snapshot/typings/snapshot.d.ts index 0f4ec0b1b1..86233a5def 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, 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, 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 fc7ed63131..d60074d40d 100644 --- a/packages/rrweb/src/record/mutation.ts +++ b/packages/rrweb/src/record/mutation.ts @@ -543,9 +543,11 @@ export default class MutationBuffer { } } else { // overwrite attribute if the mutations was triggered in same time + const element = m.target as HTMLElement; item.attributes[m.attributeName!] = transformAttribute( this.doc, - (m.target as HTMLElement).tagName, + element, + element.tagName, m.attributeName!, value!, this.maskAllText, diff --git a/packages/rrweb/test/__snapshots__/integration.test.ts.snap b/packages/rrweb/test/__snapshots__/integration.test.ts.snap index 782918e17f..7a77c4ad85 100644 --- a/packages/rrweb/test/__snapshots__/integration.test.ts.snap +++ b/packages/rrweb/test/__snapshots__/integration.test.ts.snap @@ -1409,8 +1409,23 @@ exports[`record integration tests can record form interactions 1`] = ` }, { "type": 3, - "textContent": "\\n ", + "textContent": "\\n ", "id": 71 + }, + { + "type": 2, + "tagName": "input", + "attributes": { + "type": "submit", + "value": "Submit form" + }, + "childNodes": [], + "id": 72 + }, + { + "type": 3, + "textContent": "\\n ", + "id": 73 } ], "id": 18 @@ -1418,7 +1433,7 @@ exports[`record integration tests can record form interactions 1`] = ` { "type": 3, "textContent": "\\n \\n ", - "id": 72 + "id": 74 }, { "type": 2, @@ -1428,15 +1443,15 @@ exports[`record integration tests can record form interactions 1`] = ` { "type": 3, "textContent": "SCRIPT_PLACEHOLDER", - "id": 74 + "id": 76 } ], - "id": 73 + "id": 75 }, { "type": 3, "textContent": "\\n \\n \\n\\n", - "id": 75 + "id": 77 } ], "id": 16 @@ -3256,8 +3271,23 @@ exports[`record integration tests can use maskInputOptions to configure which ty }, { "type": 3, - "textContent": "\\n ", + "textContent": "\\n ", "id": 71 + }, + { + "type": 2, + "tagName": "input", + "attributes": { + "type": "submit", + "value": "Submit form" + }, + "childNodes": [], + "id": 72 + }, + { + "type": 3, + "textContent": "\\n ", + "id": 73 } ], "id": 18 @@ -3265,7 +3295,7 @@ exports[`record integration tests can use maskInputOptions to configure which ty { "type": 3, "textContent": "\\n \\n ", - "id": 72 + "id": 74 }, { "type": 2, @@ -3275,15 +3305,15 @@ exports[`record integration tests can use maskInputOptions to configure which ty { "type": 3, "textContent": "SCRIPT_PLACEHOLDER", - "id": 74 + "id": 76 } ], - "id": 73 + "id": 75 }, { "type": 3, "textContent": "\\n \\n \\n\\n", - "id": 75 + "id": 77 } ], "id": 16 @@ -4464,176 +4494,733 @@ exports[`record integration tests should mask only inputs 1`] = ` { "type": 3, "textContent": "\\n ", - "id": 28 + "id": 28 + }, + { + "type": 2, + "tagName": "div", + "attributes": {}, + "childNodes": [ + { + "type": 3, + "textContent": "\\n ", + "id": 30 + }, + { + "type": 2, + "tagName": "div", + "attributes": {}, + "childNodes": [ + { + "type": 3, + "textContent": "mask3", + "id": 32 + } + ], + "id": 31 + }, + { + "type": 3, + "textContent": "\\n ", + "id": 33 + } + ], + "id": 29 + }, + { + "type": 3, + "textContent": "\\n ", + "id": 34 + } + ], + "id": 27 + }, + { + "type": 3, + "textContent": "\\n ", + "id": 35 + }, + { + "type": 2, + "tagName": "div", + "attributes": {}, + "childNodes": [ + { + "type": 3, + "textContent": "\\n mask4\\n ", + "id": 37 + } + ], + "id": 36 + }, + { + "type": 3, + "textContent": "\\n ", + "id": 38 + }, + { + "type": 2, + "tagName": "div", + "attributes": { + "class": "rr-unmask" + }, + "childNodes": [ + { + "type": 3, + "textContent": "\\n mask5\\n ", + "id": 40 + } + ], + "id": 39 + }, + { + "type": 3, + "textContent": "\\n ", + "id": 41 + }, + { + "type": 2, + "tagName": "input", + "attributes": { + "placeholder": "mask6" + }, + "childNodes": [], + "id": 42 + }, + { + "type": 3, + "textContent": "\\n ", + "id": 43 + }, + { + "type": 2, + "tagName": "div", + "attributes": { + "title": "mask7" + }, + "childNodes": [ + { + "type": 3, + "textContent": "\\n ", + "id": 45 + }, + { + "type": 2, + "tagName": "button", + "attributes": { + "aria-label": "mask8" + }, + "childNodes": [ + { + "type": 3, + "textContent": "mask9", + "id": 47 + } + ], + "id": 46 + }, + { + "type": 3, + "textContent": "\\n ", + "id": 48 + }, + { + "type": 2, + "tagName": "textarea", + "attributes": { + "value": "******" + }, + "childNodes": [ + { + "type": 3, + "textContent": "", + "id": 50 + } + ], + "id": 49 + }, + { + "type": 3, + "textContent": "\\n \\n ", + "id": 51 + }, + { + "type": 2, + "tagName": "script", + "attributes": {}, + "childNodes": [ + { + "type": 3, + "textContent": "SCRIPT_PLACEHOLDER", + "id": 53 + } + ], + "id": 52 + }, + { + "type": 3, + "textContent": "\\n \\n \\n\\n", + "id": 54 + } + ], + "id": 44 + } + ], + "id": 16 + } + ], + "id": 3 + } + ], + "id": 1 + }, + "initialOffset": { + "left": 0, + "top": 0 + } + } + } +]" +`; + +exports[`record integration tests should mask text in form elements 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": "label", + "attributes": { + "for": "text" + }, + "childNodes": [ + { + "type": 3, + "textContent": "\\n ", + "id": 21 + }, + { + "type": 2, + "tagName": "input", + "attributes": { + "type": "text" + }, + "childNodes": [], + "id": 22 + }, + { + "type": 3, + "textContent": "\\n ", + "id": 23 + } + ], + "id": 20 + }, + { + "type": 3, + "textContent": "\\n ", + "id": 24 + }, + { + "type": 2, + "tagName": "label", + "attributes": {}, + "childNodes": [ + { + "type": 3, + "textContent": "\\n ", + "id": 26 + }, + { + "type": 2, + "tagName": "input", + "attributes": { + "type": "radio", + "name": "toggle", + "value": "on" + }, + "childNodes": [], + "id": 27 + }, + { + "type": 3, + "textContent": "\\n ", + "id": 28 + } + ], + "id": 25 + }, + { + "type": 3, + "textContent": "\\n ", + "id": 29 + }, + { + "type": 2, + "tagName": "label", + "attributes": {}, + "childNodes": [ + { + "type": 3, + "textContent": "\\n ", + "id": 31 + }, + { + "type": 2, + "tagName": "input", + "attributes": { + "type": "radio", + "name": "toggle", + "value": "off", + "checked": true + }, + "childNodes": [], + "id": 32 + }, + { + "type": 3, + "textContent": "\\n ", + "id": 33 + } + ], + "id": 30 + }, + { + "type": 3, + "textContent": "\\n ", + "id": 34 + }, + { + "type": 2, + "tagName": "label", + "attributes": { + "for": "checkbox" + }, + "childNodes": [ + { + "type": 3, + "textContent": "\\n ", + "id": 36 + }, + { + "type": 2, + "tagName": "input", + "attributes": { + "type": "checkbox" + }, + "childNodes": [], + "id": 37 + }, + { + "type": 3, + "textContent": "\\n ", + "id": 38 + } + ], + "id": 35 + }, + { + "type": 3, + "textContent": "\\n ", + "id": 39 + }, + { + "type": 2, + "tagName": "label", + "attributes": { + "for": "textarea" + }, + "childNodes": [ + { + "type": 3, + "textContent": "\\n ", + "id": 41 + }, + { + "type": 2, + "tagName": "textarea", + "attributes": { + "name": "", + "id": "", + "cols": "30", + "rows": "10" + }, + "childNodes": [], + "id": 42 + }, + { + "type": 3, + "textContent": "\\n ", + "id": 43 + } + ], + "id": 40 + }, + { + "type": 3, + "textContent": "\\n ", + "id": 44 + }, + { + "type": 2, + "tagName": "label", + "attributes": { + "for": "select" + }, + "childNodes": [ + { + "type": 3, + "textContent": "\\n ", + "id": 46 + }, + { + "type": 2, + "tagName": "select", + "attributes": { + "name": "", + "id": "", + "value": "1" + }, + "childNodes": [ + { + "type": 3, + "textContent": "\\n ", + "id": 48 + }, + { + "type": 2, + "tagName": "option", + "attributes": { + "value": "1", + "selected": true + }, + "childNodes": [ + { + "type": 3, + "textContent": "*", + "id": 50 + } + ], + "id": 49 + }, + { + "type": 3, + "textContent": "\\n ", + "id": 51 + }, + { + "type": 2, + "tagName": "option", + "attributes": { + "value": "2" + }, + "childNodes": [ + { + "type": 3, + "textContent": "*", + "id": 53 + } + ], + "id": 52 + }, + { + "type": 3, + "textContent": "\\n ", + "id": 54 + } + ], + "id": 47 + }, + { + "type": 3, + "textContent": "\\n ", + "id": 55 + } + ], + "id": 45 + }, + { + "type": 3, + "textContent": "\\n ", + "id": 56 }, { "type": 2, - "tagName": "div", - "attributes": {}, + "tagName": "label", + "attributes": { + "for": "password" + }, "childNodes": [ { "type": 3, "textContent": "\\n ", - "id": 30 + "id": 58 }, { "type": 2, - "tagName": "div", - "attributes": {}, - "childNodes": [ - { - "type": 3, - "textContent": "mask3", - "id": 32 - } - ], - "id": 31 + "tagName": "input", + "attributes": { + "type": "password" + }, + "childNodes": [], + "id": 59 }, { "type": 3, "textContent": "\\n ", - "id": 33 + "id": 60 } ], - "id": 29 + "id": 57 }, { "type": 3, - "textContent": "\\n ", - "id": 34 - } - ], - "id": 27 - }, - { - "type": 3, - "textContent": "\\n ", - "id": 35 - }, - { - "type": 2, - "tagName": "div", - "attributes": {}, - "childNodes": [ - { - "type": 3, - "textContent": "\\n mask4\\n ", - "id": 37 - } - ], - "id": 36 - }, - { - "type": 3, - "textContent": "\\n ", - "id": 38 - }, - { - "type": 2, - "tagName": "div", - "attributes": { - "class": "rr-unmask" - }, - "childNodes": [ - { - "type": 3, - "textContent": "\\n mask5\\n ", - "id": 40 - } - ], - "id": 39 - }, - { - "type": 3, - "textContent": "\\n ", - "id": 41 - }, - { - "type": 2, - "tagName": "input", - "attributes": { - "placeholder": "mask6" - }, - "childNodes": [], - "id": 42 - }, - { - "type": 3, - "textContent": "\\n ", - "id": 43 - }, - { - "type": 2, - "tagName": "div", - "attributes": { - "title": "mask7" - }, - "childNodes": [ - { - "type": 3, - "textContent": "\\n ", - "id": 45 + "textContent": "\\n ", + "id": 61 }, { "type": 2, - "tagName": "button", + "tagName": "label", "attributes": { - "aria-label": "mask8" + "for": "empty" }, "childNodes": [ { "type": 3, - "textContent": "mask9", - "id": 47 + "textContent": "\\n ", + "id": 63 + }, + { + "type": 2, + "tagName": "input", + "attributes": { + "id": "empty" + }, + "childNodes": [], + "id": 64 + }, + { + "type": 3, + "textContent": "\\n ", + "id": 65 } ], - "id": 46 + "id": 62 }, { "type": 3, - "textContent": "\\n ", - "id": 48 + "textContent": "\\n ", + "id": 66 }, { "type": 2, - "tagName": "textarea", + "tagName": "label", "attributes": { - "value": "******" + "for": "unmask" }, "childNodes": [ { "type": 3, - "textContent": "", - "id": 50 + "textContent": "\\n ", + "id": 68 + }, + { + "type": 2, + "tagName": "input", + "attributes": { + "type": "text", + "class": "rr-unmask" + }, + "childNodes": [], + "id": 69 + }, + { + "type": 3, + "textContent": "\\n ", + "id": 70 } ], - "id": 49 + "id": 67 }, { "type": 3, - "textContent": "\\n \\n ", - "id": 51 + "textContent": "\\n ", + "id": 71 }, { "type": 2, - "tagName": "script", - "attributes": {}, - "childNodes": [ - { - "type": 3, - "textContent": "SCRIPT_PLACEHOLDER", - "id": 53 - } - ], - "id": 52 + "tagName": "input", + "attributes": { + "type": "submit", + "value": "****** ****" + }, + "childNodes": [], + "id": 72 }, { "type": 3, - "textContent": "\\n \\n \\n\\n", - "id": 54 + "textContent": "\\n ", + "id": 73 } ], - "id": 44 + "id": 18 + }, + { + "type": 3, + "textContent": "\\n \\n ", + "id": 74 + }, + { + "type": 2, + "tagName": "script", + "attributes": {}, + "childNodes": [ + { + "type": 3, + "textContent": "SCRIPT_PLACEHOLDER", + "id": 76 + } + ], + "id": 75 + }, + { + "type": 3, + "textContent": "\\n \\n \\n\\n", + "id": 77 } ], "id": 16 @@ -8445,8 +9032,23 @@ exports[`record integration tests should not record input values if maskAllInput }, { "type": 3, - "textContent": "\\n ", + "textContent": "\\n ", "id": 71 + }, + { + "type": 2, + "tagName": "input", + "attributes": { + "type": "submit", + "value": "Submit form" + }, + "childNodes": [], + "id": 72 + }, + { + "type": 3, + "textContent": "\\n ", + "id": 73 } ], "id": 18 @@ -8454,7 +9056,7 @@ exports[`record integration tests should not record input values if maskAllInput { "type": 3, "textContent": "\\n \\n ", - "id": 72 + "id": 74 }, { "type": 2, @@ -8464,15 +9066,15 @@ exports[`record integration tests should not record input values if maskAllInput { "type": 3, "textContent": "SCRIPT_PLACEHOLDER", - "id": 74 + "id": 76 } ], - "id": 73 + "id": 75 }, { "type": 3, "textContent": "\\n \\n \\n\\n", - "id": 75 + "id": 77 } ], "id": 16 @@ -12563,8 +13165,23 @@ exports[`record integration tests should record input userTriggered values if us }, { "type": 3, - "textContent": "\\n ", + "textContent": "\\n ", "id": 71 + }, + { + "type": 2, + "tagName": "input", + "attributes": { + "type": "submit", + "value": "Submit form" + }, + "childNodes": [], + "id": 72 + }, + { + "type": 3, + "textContent": "\\n ", + "id": 73 } ], "id": 18 @@ -12572,7 +13189,7 @@ exports[`record integration tests should record input userTriggered values if us { "type": 3, "textContent": "\\n \\n ", - "id": 72 + "id": 74 }, { "type": 2, @@ -12582,15 +13199,15 @@ exports[`record integration tests should record input userTriggered values if us { "type": 3, "textContent": "SCRIPT_PLACEHOLDER", - "id": 74 + "id": 76 } ], - "id": 73 + "id": 75 }, { "type": 3, "textContent": "\\n \\n \\n\\n", - "id": 75 + "id": 77 } ], "id": 16 diff --git a/packages/rrweb/test/html/form.html b/packages/rrweb/test/html/form.html index b8d8e36444..f4f06af750 100644 --- a/packages/rrweb/test/html/form.html +++ b/packages/rrweb/test/html/form.html @@ -39,6 +39,7 @@ + diff --git a/packages/rrweb/test/integration.test.ts b/packages/rrweb/test/integration.test.ts index 9c09082a5a..8deba04da9 100644 --- a/packages/rrweb/test/integration.test.ts +++ b/packages/rrweb/test/integration.test.ts @@ -500,6 +500,17 @@ describe('record integration tests', function (this: ISuite) { assertSnapshot(snapshots); }); + it('should mask text in form elements', async () => { + const page: puppeteer.Page = await browser.newPage(); + await page.goto('about:blank'); + await page.setContent( + getHtml.call(this, 'form.html', { maskAllText: true }), + ); + + const snapshots = await page.evaluate('window.snapshots'); + assertSnapshot(snapshots); + }); + it('should not record blocked elements and its child nodes', async () => { const page: puppeteer.Page = await browser.newPage(); await page.goto('about:blank');