diff --git a/.changeset/nice-pugs-reply.md b/.changeset/nice-pugs-reply.md new file mode 100644 index 0000000000..04c655a67e --- /dev/null +++ b/.changeset/nice-pugs-reply.md @@ -0,0 +1,5 @@ +--- +'rrweb': patch +--- + +fix: Ensure getting the type of inputs works diff --git a/packages/rrweb-snapshot/src/snapshot.ts b/packages/rrweb-snapshot/src/snapshot.ts index 8c3fda1a2e..364506fd6a 100644 --- a/packages/rrweb-snapshot/src/snapshot.ts +++ b/packages/rrweb-snapshot/src/snapshot.ts @@ -20,6 +20,7 @@ import { maskInputValue, isNativeShadowDom, getCssRulesString, + getInputType, } from './utils'; let _id = 1; @@ -682,11 +683,7 @@ function serializeElementNode( attributes.type !== 'button' && value ) { - const type: string | null = n.hasAttribute('data-rr-is-password') - ? 'password' - : typeof attributes.type === 'string' - ? attributes.type.toLowerCase() - : null; + const type = getInputType(n); attributes.value = maskInputValue({ type, tagName, diff --git a/packages/rrweb-snapshot/src/utils.ts b/packages/rrweb-snapshot/src/utils.ts index 35b6d8075e..a443cb96c1 100644 --- a/packages/rrweb-snapshot/src/utils.ts +++ b/packages/rrweb-snapshot/src/utils.ts @@ -248,3 +248,20 @@ export function isNodeMetaEqual(a: serializedNode, b: serializedNode): boolean { ); return false; } + +/** + * Get the type of an input element. + * This takes care of the case where a password input is changed to a text input. + * In this case, we continue to consider this of type password, in order to avoid leaking sensitive data + * where passwords should be masked. + */ +export function getInputType(element: HTMLElement): Lowercase | null { + const type = (element as HTMLInputElement).type; + + return element.hasAttribute('data-rr-is-password') + ? 'password' + : type + ? // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion + (type.toLowerCase() as Lowercase) + : null; +} diff --git a/packages/rrweb/src/record/mutation.ts b/packages/rrweb/src/record/mutation.ts index aa351fee62..ad97f79915 100644 --- a/packages/rrweb/src/record/mutation.ts +++ b/packages/rrweb/src/record/mutation.ts @@ -8,6 +8,7 @@ import { maskInputValue, Mirror, isNativeShadowDom, + getInputType, } from 'rrweb-snapshot'; import type { observerParam, MutationBufferParam } from '../types'; import type { @@ -29,7 +30,6 @@ import { isSerializedStylesheet, inDom, getShadowHost, - getInputType, } from '../utils'; type DoubleLinkedListNode = { diff --git a/packages/rrweb/src/record/observer.ts b/packages/rrweb/src/record/observer.ts index d2852b970a..bb04dd0300 100644 --- a/packages/rrweb/src/record/observer.ts +++ b/packages/rrweb/src/record/observer.ts @@ -1,10 +1,14 @@ -import { MaskInputOptions, maskInputValue, Mirror } from 'rrweb-snapshot'; +import { + MaskInputOptions, + maskInputValue, + Mirror, + getInputType, +} from 'rrweb-snapshot'; import type { FontFaceSet } from 'css-font-loading-module'; import { throttle, on, hookSetter, - getInputType, getWindowScroll, getWindowHeight, getWindowWidth, diff --git a/packages/rrweb/src/utils.ts b/packages/rrweb/src/utils.ts index 1626e3734c..26dc63888e 100644 --- a/packages/rrweb/src/utils.ts +++ b/packages/rrweb/src/utils.ts @@ -563,18 +563,3 @@ export function inDom(n: Node): boolean { if (!doc) return false; return doc.contains(n) || shadowHostInDom(n); } - -/** - * Get the type of an input element. - * This takes care of the case where a password input is changed to a text input. - * In this case, we continue to consider this of type password, in order to avoid leaking sensitive data - * where passwords should be masked. - */ -export function getInputType(element: HTMLElement): Lowercase | null { - return element.hasAttribute('data-rr-is-password') - ? 'password' - : element.hasAttribute('type') - ? // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion, @typescript-eslint/no-non-null-assertion - (element.getAttribute('type')!.toLowerCase() as Lowercase) - : null; -} diff --git a/packages/rrweb/test/__snapshots__/integration.test.ts.snap b/packages/rrweb/test/__snapshots__/integration.test.ts.snap index 7c6fdfbfda..32b56a6019 100644 --- a/packages/rrweb/test/__snapshots__/integration.test.ts.snap +++ b/packages/rrweb/test/__snapshots__/integration.test.ts.snap @@ -7473,6 +7473,238 @@ exports[`record integration tests should not record input events on ignored elem ]" `; +exports[`record integration tests should not record input values if dynamically added and maskAllInputs is true 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\\": \\"title\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"Empty\\", + \\"id\\": 11 + } + ], + \\"id\\": 10 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 12 + } + ], + \\"id\\": 4 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 13 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"body\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 15 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"div\\", + \\"attributes\\": { + \\"id\\": \\"one\\" + }, + \\"childNodes\\": [], + \\"id\\": 16 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\", + \\"id\\": 17 + }, + { + \\"type\\": 2, + \\"tagName\\": \\"script\\", + \\"attributes\\": {}, + \\"childNodes\\": [ + { + \\"type\\": 3, + \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\", + \\"id\\": 19 + } + ], + \\"id\\": 18 + }, + { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\\\n \\\\n\\\\n\\", + \\"id\\": 20 + } + ], + \\"id\\": 14 + } + ], + \\"id\\": 3 + } + ], + \\"id\\": 1 + }, + \\"initialOffset\\": { + \\"left\\": 0, + \\"top\\": 0 + } + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [], + \\"removes\\": [], + \\"adds\\": [ + { + \\"parentId\\": 14, + \\"nextId\\": 16, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"input\\", + \\"attributes\\": { + \\"id\\": \\"input\\", + \\"value\\": \\"**********************\\" + }, + \\"childNodes\\": [], + \\"id\\": 21 + } + } + ] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"**********************\\", + \\"isChecked\\": false, + \\"id\\": 21 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 2, + \\"type\\": 5, + \\"id\\": 21 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"***********************\\", + \\"isChecked\\": false, + \\"id\\": 21 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"************************\\", + \\"isChecked\\": false, + \\"id\\": 21 + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 5, + \\"text\\": \\"*************************\\", + \\"isChecked\\": false, + \\"id\\": 21 + } + } +]" +`; + exports[`record integration tests should not record input values if maskAllInputs is enabled 1`] = ` "[ { diff --git a/packages/rrweb/test/html/empty.html b/packages/rrweb/test/html/empty.html new file mode 100644 index 0000000000..6ceadbba12 --- /dev/null +++ b/packages/rrweb/test/html/empty.html @@ -0,0 +1,11 @@ + + + + + + Empty + + +
+ + diff --git a/packages/rrweb/test/integration.test.ts b/packages/rrweb/test/integration.test.ts index fec8c36902..aa564d3177 100644 --- a/packages/rrweb/test/integration.test.ts +++ b/packages/rrweb/test/integration.test.ts @@ -455,6 +455,30 @@ describe('record integration tests', function (this: ISuite) { assertSnapshot(snapshots); }); + it('should not record input values if dynamically added and maskAllInputs is true', async () => { + const page: puppeteer.Page = await browser.newPage(); + await page.goto('about:blank'); + await page.setContent( + getHtml.call(this, 'empty.html', { maskAllInputs: true }), + ); + + await page.evaluate(() => { + const el = document.createElement('input'); + el.id = 'input'; + el.value = 'input should be masked'; + + const nextElement = document.querySelector('#one')!; + nextElement.parentNode!.insertBefore(el, nextElement); + }); + + await page.type('#input', 'moo'); + + const snapshots = (await page.evaluate( + 'window.snapshots', + )) as eventWithTime[]; + assertSnapshot(snapshots); + }); + it('should record webgl canvas mutations', async () => { const page: puppeteer.Page = await browser.newPage(); await page.goto('about:blank');