diff --git a/.changeset/swift-dancers-rest.md b/.changeset/swift-dancers-rest.md new file mode 100644 index 0000000000..bcedf6c875 --- /dev/null +++ b/.changeset/swift-dancers-rest.md @@ -0,0 +1,6 @@ +--- +'rrweb-snapshot': minor +'rrweb': minor +--- + +Extends maskTextFn to pass the HTMLElement to the deciding function diff --git a/packages/rrweb-snapshot/src/snapshot.ts b/packages/rrweb-snapshot/src/snapshot.ts index 04a7383c16..0c4dd2fb58 100644 --- a/packages/rrweb-snapshot/src/snapshot.ts +++ b/packages/rrweb-snapshot/src/snapshot.ts @@ -714,7 +714,7 @@ function serializeTextNode( if (!isStyle && !isScript && !isTextarea && textContent && forceMask) { textContent = maskTextFn - ? maskTextFn(textContent) + ? maskTextFn(textContent, n.parentElement) : textContent.replace(/[\S]/g, '*'); } if (isTextarea && textContent && (maskInputOptions.textarea || forceMask)) { diff --git a/packages/rrweb-snapshot/src/types.ts b/packages/rrweb-snapshot/src/types.ts index 5405e8ec9c..6b80404b76 100644 --- a/packages/rrweb-snapshot/src/types.ts +++ b/packages/rrweb-snapshot/src/types.ts @@ -157,7 +157,7 @@ export type DataURLOptions = Partial<{ quality: number; }>; -export type MaskTextFn = (text: string) => string; +export type MaskTextFn = (text: string, element: HTMLElement | null) => string; export type MaskInputFn = (text: string, element: HTMLElement) => string; export type MaskAttributeFn = ( attributeName: string, diff --git a/packages/rrweb/src/record/mutation.ts b/packages/rrweb/src/record/mutation.ts index 588a267b58..3afc6e598d 100644 --- a/packages/rrweb/src/record/mutation.ts +++ b/packages/rrweb/src/record/mutation.ts @@ -32,6 +32,7 @@ import { isSerializedStylesheet, inDom, getShadowHost, + closestElementOfNode, } from '../utils'; type DoubleLinkedListNode = { @@ -525,6 +526,7 @@ export default class MutationBuffer { switch (m.type) { case 'characterData': { const value = m.target.textContent; + if ( !isBlocked( m.target, @@ -546,7 +548,7 @@ export default class MutationBuffer { this.maskAllText, ) && value ? this.maskTextFn - ? this.maskTextFn(value) + ? this.maskTextFn(value, closestElementOfNode(m.target)) : value.replace(/[\S]/g, '*') : value, node: m.target, diff --git a/packages/rrweb/src/utils.ts b/packages/rrweb/src/utils.ts index eba0847128..d3b6bebb6d 100644 --- a/packages/rrweb/src/utils.ts +++ b/packages/rrweb/src/utils.ts @@ -219,6 +219,23 @@ export function getWindowWidth(): number { ); } +/** + * Returns the given node as an HTMLElement if it is one, otherwise the parent node as an HTMLElement + * @param node - node to check + * @returns HTMLElement or null + */ + +export function closestElementOfNode(node: Node | null): HTMLElement | null { + if (!node) { + return null; + } + const el: HTMLElement | null = + node.nodeType === node.ELEMENT_NODE + ? (node as HTMLElement) + : node.parentElement; + return el; +} + /** * Checks if the given element set to be blocked by rrweb * @param node - node to check @@ -237,11 +254,11 @@ export function isBlocked( if (!node) { return false; } - const el: HTMLElement | null = - node.nodeType === node.ELEMENT_NODE - ? (node as HTMLElement) - : node.parentElement; - if (!el) return false; + const el = closestElementOfNode(node); + + if (!el) { + return false; + } const blockedPredicate = createMatchPredicate(blockClass, blockSelector); diff --git a/packages/rrweb/test/__snapshots__/integration.test.ts.snap b/packages/rrweb/test/__snapshots__/integration.test.ts.snap index 79919c964f..9564b69d88 100644 --- a/packages/rrweb/test/__snapshots__/integration.test.ts.snap +++ b/packages/rrweb/test/__snapshots__/integration.test.ts.snap @@ -7236,7 +7236,7 @@ exports[`record integration tests mutations should work when blocked class is un \\"attributes\\": { \\"class\\": \\"rr-block\\", \\"rr_width\\": \\"1904px\\", - \\"rr_height\\": \\"21px\\" + \\"rr_height\\": \\"21.5px\\" }, \\"childNodes\\": [], \\"id\\": 84 @@ -8913,7 +8913,7 @@ exports[`record integration tests should mask only inputs 1`] = ` }, { \\"type\\": 3, - \\"textContent\\": \\"\\\\n \\", + \\"textContent\\": \\"\\\\n\\\\n \\", \\"id\\": 35 }, { @@ -8951,9 +8951,29 @@ exports[`record integration tests should mask only inputs 1`] = ` }, { \\"type\\": 3, - \\"textContent\\": \\"\\\\n \\\\n \\", + \\"textContent\\": \\"\\\\n\\\\n \\", \\"id\\": 41 }, + { + \\"type\\": 2, + \\"tagName\\": \\"p\\", + \\"attributes\\": { + \\"data-unmask-example\\": \\"true\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n unmask1\\\\n \\", + \\"id\\": 43 + } + ], + \\"id\\": 42 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\", + \\"id\\": 44 + }, { \\"type\\": 2, \\"tagName\\": \\"script\\", @@ -8962,15 +8982,15 @@ exports[`record integration tests should mask only inputs 1`] = ` { \\"type\\": 3, \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", - \\"id\\": 43 + \\"id\\": 46 } ], - \\"id\\": 42 + \\"id\\": 45 }, { \\"type\\": 3, \\"textContent\\": \\"\\\\n \\\\n \\\\n\\\\n\\", - \\"id\\": 44 + \\"id\\": 47 } ], \\"id\\": 16 @@ -9676,7 +9696,7 @@ exports[`record integration tests should mask texts 1`] = ` }, { \\"type\\": 3, - \\"textContent\\": \\"\\\\n \\", + \\"textContent\\": \\"\\\\n\\\\n \\", \\"id\\": 35 }, { @@ -9714,9 +9734,29 @@ exports[`record integration tests should mask texts 1`] = ` }, { \\"type\\": 3, - \\"textContent\\": \\"\\\\n \\\\n \\", + \\"textContent\\": \\"\\\\n\\\\n \\", \\"id\\": 41 }, + { + \\"type\\": 2, + \\"tagName\\": \\"p\\", + \\"attributes\\": { + \\"data-unmask-example\\": \\"true\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n unmask1\\\\n \\", + \\"id\\": 43 + } + ], + \\"id\\": 42 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\", + \\"id\\": 44 + }, { \\"type\\": 2, \\"tagName\\": \\"script\\", @@ -9725,15 +9765,15 @@ exports[`record integration tests should mask texts 1`] = ` { \\"type\\": 3, \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", - \\"id\\": 43 + \\"id\\": 46 } ], - \\"id\\": 42 + \\"id\\": 45 }, { \\"type\\": 3, \\"textContent\\": \\"\\\\n \\\\n \\\\n\\\\n\\", - \\"id\\": 44 + \\"id\\": 47 } ], \\"id\\": 16 @@ -9992,7 +10032,7 @@ exports[`record integration tests should mask texts using maskAllText 1`] = ` }, { \\"type\\": 3, - \\"textContent\\": \\"\\\\n \\", + \\"textContent\\": \\"\\\\n\\\\n \\", \\"id\\": 35 }, { @@ -10030,9 +10070,29 @@ exports[`record integration tests should mask texts using maskAllText 1`] = ` }, { \\"type\\": 3, - \\"textContent\\": \\"\\\\n \\\\n \\", + \\"textContent\\": \\"\\\\n\\\\n \\", \\"id\\": 41 }, + { + \\"type\\": 2, + \\"tagName\\": \\"p\\", + \\"attributes\\": { + \\"data-unmask-example\\": \\"true\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n *******\\\\n \\", + \\"id\\": 43 + } + ], + \\"id\\": 42 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\", + \\"id\\": 44 + }, { \\"type\\": 2, \\"tagName\\": \\"script\\", @@ -10041,15 +10101,15 @@ exports[`record integration tests should mask texts using maskAllText 1`] = ` { \\"type\\": 3, \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", - \\"id\\": 43 + \\"id\\": 46 } ], - \\"id\\": 42 + \\"id\\": 45 }, { \\"type\\": 3, \\"textContent\\": \\"\\\\n \\\\n \\\\n\\\\n\\", - \\"id\\": 44 + \\"id\\": 47 } ], \\"id\\": 16 @@ -10308,7 +10368,7 @@ exports[`record integration tests should mask texts using maskTextFn 1`] = ` }, { \\"type\\": 3, - \\"textContent\\": \\"\\\\n \\", + \\"textContent\\": \\"\\\\n\\\\n \\", \\"id\\": 35 }, { @@ -10346,9 +10406,29 @@ exports[`record integration tests should mask texts using maskTextFn 1`] = ` }, { \\"type\\": 3, - \\"textContent\\": \\"\\\\n \\\\n \\", + \\"textContent\\": \\"\\\\n\\\\n \\", \\"id\\": 41 }, + { + \\"type\\": 2, + \\"tagName\\": \\"p\\", + \\"attributes\\": { + \\"data-unmask-example\\": \\"true\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n unmask1\\\\n \\", + \\"id\\": 43 + } + ], + \\"id\\": 42 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\", + \\"id\\": 44 + }, { \\"type\\": 2, \\"tagName\\": \\"script\\", @@ -10357,15 +10437,15 @@ exports[`record integration tests should mask texts using maskTextFn 1`] = ` { \\"type\\": 3, \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", - \\"id\\": 43 + \\"id\\": 46 } ], - \\"id\\": 42 + \\"id\\": 45 }, { \\"type\\": 3, \\"textContent\\": \\"\\\\n \\\\n \\\\n\\\\n\\", - \\"id\\": 44 + \\"id\\": 47 } ], \\"id\\": 16 @@ -21975,6 +22055,342 @@ exports[`record integration tests should record webgl canvas mutations 1`] = ` ]" `; +exports[`record integration tests should unmask texts using maskTextFn 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\\": \\"M*** ****\\", + \\"id\\": 13 + } + ], + \\"id\\": 12 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 14 + } + ], + \\"id\\": 4 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 15 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 17 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"p\\", + \\"attributes\\": { + \\"class\\": \\"rr-mask\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"****1\\", + \\"id\\": 19 + } + ], + \\"id\\": 18 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 20 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"div\\", + \\"attributes\\": { + \\"class\\": \\"rr-mask\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 22 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"span\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"****2\\", + \\"id\\": 24 + } + ], + \\"id\\": 23 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 25 + } + ], + \\"id\\": 21 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 26 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"div\\", + \\"attributes\\": { + \\"data-masking\\": \\"true\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 28 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"div\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 30 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"div\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"****3\\", + \\"id\\": 32 + } + ], + \\"id\\": 31 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 33 + } + ], + \\"id\\": 29 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 34 + } + ], + \\"id\\": 27 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n\\\\n \\", + \\"id\\": 35 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"div\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"****4\\", + \\"id\\": 37 + } + ], + \\"id\\": 36 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 38 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"textarea\\", + \\"attributes\\": { + \\"value\\": \\"*****\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"*****\\", + \\"id\\": 40 + } + ], + \\"id\\": 39 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n\\\\n \\", + \\"id\\": 41 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"p\\", + \\"attributes\\": { + \\"data-unmask-example\\": \\"true\\" + }, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n unmask1\\\\n \\", + \\"id\\": 43 + } + ], + \\"id\\": 42 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\", + \\"id\\": 44 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 46 + } + ], + \\"id\\": 45 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\\\n\\\\n\\", + \\"id\\": 47 + } + ], + \\"id\\": 16 + } + ], + \\"id\\": 3 + } + ], + \\"id\\": 1 + }, + \\"initialOffset\\": { + \\"left\\": 0, + \\"top\\": 0 + } + } + } +]" +`; + exports[`record integration tests will serialize node before record 1`] = ` "[ { diff --git a/packages/rrweb/test/html/mask-text.html b/packages/rrweb/test/html/mask-text.html index 3477a03c9f..3e7ac98072 100644 --- a/packages/rrweb/test/html/mask-text.html +++ b/packages/rrweb/test/html/mask-text.html @@ -16,7 +16,12 @@
mask3
+
mask4
+ +

+ unmask1 +

diff --git a/packages/rrweb/test/integration.test.ts b/packages/rrweb/test/integration.test.ts index c20adc2494..55c74664f0 100644 --- a/packages/rrweb/test/integration.test.ts +++ b/packages/rrweb/test/integration.test.ts @@ -1416,6 +1416,26 @@ describe('record integration tests', function (this: ISuite) { assertSnapshot(snapshots); }); + it('should unmask texts using maskTextFn', async () => { + const page: puppeteer.Page = await browser.newPage(); + await page.goto('about:blank'); + await page.setContent( + getHtml.call(this, 'mask-text.html', { + maskTextSelector: '*', + maskTextFn: (t: string, el: HTMLElement) => { + return el.matches('[data-unmask-example="true"]') + ? t + : t.replace(/[a-z]/g, '*'); + }, + }), + ); + + const snapshots = (await page.evaluate( + 'window.snapshots', + )) as eventWithTime[]; + assertSnapshot(snapshots); + }); + it('should mask texts using maskAllText', async () => { const page: puppeteer.Page = await browser.newPage(); await page.goto('about:blank');