feat: Added support maskInputFn with HTMLElement (#1188)

This commit is contained in:
Ben White
2026-04-01 12:00:00 +08:00
committed by GitHub
parent 23388f2d00
commit ec5e536891
11 changed files with 917 additions and 9 deletions

View File

@@ -0,0 +1,6 @@
---
'rrweb-snapshot': minor
'rrweb': minor
---
feat: Extends maskInputFn to pass the HTMLElement to the deciding function

View File

@@ -685,6 +685,7 @@ function serializeElementNode(
) {
const type = getInputType(n);
attributes.value = maskInputValue({
element: n,
type,
tagName,
value,

View File

@@ -154,7 +154,7 @@ export type DataURLOptions = Partial<{
}>;
export type MaskTextFn = (text: string) => string;
export type MaskInputFn = (text: string) => string;
export type MaskInputFn = (text: string, element: HTMLElement) => string;
export type KeepIframeSrcFn = (src: string) => boolean;

View File

@@ -154,12 +154,14 @@ export function createMirror(): Mirror {
}
export function maskInputValue({
element,
maskInputOptions,
tagName,
type,
value,
maskInputFn,
}: {
element: HTMLElement;
maskInputOptions: MaskInputOptions;
tagName: string;
type: string | null;
@@ -174,7 +176,7 @@ export function maskInputValue({
(actualType && maskInputOptions[actualType as keyof MaskInputOptions])
) {
if (maskInputFn) {
text = maskInputFn(text);
text = maskInputFn(text, element);
} else {
text = '*'.repeat(text.length);
}

View File

@@ -494,6 +494,7 @@ export default class MutationBuffer {
const type = getInputType(target);
value = maskInputValue({
element: target,
maskInputOptions: this.maskInputOptions,
tagName: target.tagName,
type,

View File

@@ -444,6 +444,7 @@ function initInputObserver({
maskInputOptions[type as keyof MaskInputOptions]
) {
text = maskInputValue({
element: target,
maskInputOptions,
tagName,
type,

View File

@@ -1910,7 +1910,8 @@ exports[`record integration tests can record form interactions 1`] = `
\\"name\\": \\"\\",
\\"id\\": \\"\\",
\\"cols\\": \\"30\\",
\\"rows\\": \\"10\\"
\\"rows\\": \\"10\\",
\\"data-unmask-example\\": \\"true\\"
},
\\"childNodes\\": [],
\\"id\\": 42
@@ -3696,7 +3697,8 @@ exports[`record integration tests can use maskInputOptions to configure which ty
\\"name\\": \\"\\",
\\"id\\": \\"\\",
\\"cols\\": \\"30\\",
\\"rows\\": \\"10\\"
\\"rows\\": \\"10\\",
\\"data-unmask-example\\": \\"true\\"
},
\\"childNodes\\": [],
\\"id\\": 42
@@ -5410,6 +5412,866 @@ exports[`record integration tests should handle recursive console messages 1`] =
]"
`;
exports[`record integration tests should mask inputs via function call 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\\": \\"form fields\\",
\\"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\\",
\\"data-unmask-example\\": \\"true\\"
},
\\"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\\": \\"*\\"
},
\\"childNodes\\": [
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 48
},
{
\\"type\\": 2,
\\"tagName\\": \\"option\\",
\\"attributes\\": {
\\"value\\": \\"1\\"
},
\\"childNodes\\": [
{
\\"type\\": 3,
\\"textContent\\": \\"1\\",
\\"id\\": 50
}
],
\\"id\\": 49
},
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 51
},
{
\\"type\\": 2,
\\"tagName\\": \\"option\\",
\\"attributes\\": {
\\"value\\": \\"2\\"
},
\\"childNodes\\": [
{
\\"type\\": 3,
\\"textContent\\": \\"2\\",
\\"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\\": \\"label\\",
\\"attributes\\": {
\\"for\\": \\"password\\"
},
\\"childNodes\\": [
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 58
},
{
\\"type\\": 2,
\\"tagName\\": \\"input\\",
\\"attributes\\": {
\\"type\\": \\"password\\"
},
\\"childNodes\\": [],
\\"id\\": 59
},
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 60
}
],
\\"id\\": 57
},
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 61
}
],
\\"id\\": 18
},
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\\\n \\",
\\"id\\": 62
},
{
\\"type\\": 2,
\\"tagName\\": \\"script\\",
\\"attributes\\": {},
\\"childNodes\\": [
{
\\"type\\": 3,
\\"textContent\\": \\"SCRIPT_PLACEHOLDER\\",
\\"id\\": 64
}
],
\\"id\\": 63
},
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\\\n \\\\n\\\\n\\",
\\"id\\": 65
}
],
\\"id\\": 16
}
],
\\"id\\": 3
}
],
\\"id\\": 1
},
\\"initialOffset\\": {
\\"left\\": 0,
\\"top\\": 0
}
}
},
{
\\"type\\": 3,
\\"data\\": {
\\"source\\": 2,
\\"type\\": 5,
\\"id\\": 22
}
},
{
\\"type\\": 3,
\\"data\\": {
\\"source\\": 5,
\\"text\\": \\"*\\",
\\"isChecked\\": false,
\\"id\\": 22
}
},
{
\\"type\\": 3,
\\"data\\": {
\\"source\\": 5,
\\"text\\": \\"**\\",
\\"isChecked\\": false,
\\"id\\": 22
}
},
{
\\"type\\": 3,
\\"data\\": {
\\"source\\": 5,
\\"text\\": \\"***\\",
\\"isChecked\\": false,
\\"id\\": 22
}
},
{
\\"type\\": 3,
\\"data\\": {
\\"source\\": 5,
\\"text\\": \\"****\\",
\\"isChecked\\": false,
\\"id\\": 22
}
},
{
\\"type\\": 3,
\\"data\\": {
\\"source\\": 2,
\\"type\\": 1,
\\"id\\": 27,
\\"pointerType\\": 0
}
},
{
\\"type\\": 3,
\\"data\\": {
\\"source\\": 2,
\\"type\\": 6,
\\"id\\": 22
}
},
{
\\"type\\": 3,
\\"data\\": {
\\"source\\": 2,
\\"type\\": 5,
\\"id\\": 27
}
},
{
\\"type\\": 3,
\\"data\\": {
\\"source\\": 2,
\\"type\\": 0,
\\"id\\": 27,
\\"pointerType\\": 0
}
},
{
\\"type\\": 3,
\\"data\\": {
\\"source\\": 2,
\\"type\\": 2,
\\"id\\": 27,
\\"pointerType\\": 0
}
},
{
\\"type\\": 3,
\\"data\\": {
\\"source\\": 5,
\\"text\\": \\"on\\",
\\"isChecked\\": true,
\\"id\\": 27
}
},
{
\\"type\\": 3,
\\"data\\": {
\\"source\\": 5,
\\"text\\": \\"off\\",
\\"isChecked\\": false,
\\"id\\": 32
}
},
{
\\"type\\": 3,
\\"data\\": {
\\"source\\": 2,
\\"type\\": 1,
\\"id\\": 37,
\\"pointerType\\": 0
}
},
{
\\"type\\": 3,
\\"data\\": {
\\"source\\": 2,
\\"type\\": 6,
\\"id\\": 27
}
},
{
\\"type\\": 3,
\\"data\\": {
\\"source\\": 2,
\\"type\\": 5,
\\"id\\": 37
}
},
{
\\"type\\": 3,
\\"data\\": {
\\"source\\": 2,
\\"type\\": 0,
\\"id\\": 37,
\\"pointerType\\": 0
}
},
{
\\"type\\": 3,
\\"data\\": {
\\"source\\": 2,
\\"type\\": 2,
\\"id\\": 37,
\\"pointerType\\": 0
}
},
{
\\"type\\": 3,
\\"data\\": {
\\"source\\": 5,
\\"text\\": \\"on\\",
\\"isChecked\\": true,
\\"id\\": 37
}
},
{
\\"type\\": 3,
\\"data\\": {
\\"source\\": 2,
\\"type\\": 6,
\\"id\\": 37
}
},
{
\\"type\\": 3,
\\"data\\": {
\\"source\\": 2,
\\"type\\": 5,
\\"id\\": 59
}
},
{
\\"type\\": 3,
\\"data\\": {
\\"source\\": 5,
\\"text\\": \\"*\\",
\\"isChecked\\": false,
\\"id\\": 59
}
},
{
\\"type\\": 3,
\\"data\\": {
\\"source\\": 5,
\\"text\\": \\"**\\",
\\"isChecked\\": false,
\\"id\\": 59
}
},
{
\\"type\\": 3,
\\"data\\": {
\\"source\\": 5,
\\"text\\": \\"***\\",
\\"isChecked\\": false,
\\"id\\": 59
}
},
{
\\"type\\": 3,
\\"data\\": {
\\"source\\": 5,
\\"text\\": \\"****\\",
\\"isChecked\\": false,
\\"id\\": 59
}
},
{
\\"type\\": 3,
\\"data\\": {
\\"source\\": 5,
\\"text\\": \\"*****\\",
\\"isChecked\\": false,
\\"id\\": 59
}
},
{
\\"type\\": 3,
\\"data\\": {
\\"source\\": 5,
\\"text\\": \\"******\\",
\\"isChecked\\": false,
\\"id\\": 59
}
},
{
\\"type\\": 3,
\\"data\\": {
\\"source\\": 5,
\\"text\\": \\"*******\\",
\\"isChecked\\": false,
\\"id\\": 59
}
},
{
\\"type\\": 3,
\\"data\\": {
\\"source\\": 5,
\\"text\\": \\"********\\",
\\"isChecked\\": false,
\\"id\\": 59
}
},
{
\\"type\\": 3,
\\"data\\": {
\\"source\\": 2,
\\"type\\": 6,
\\"id\\": 59
}
},
{
\\"type\\": 3,
\\"data\\": {
\\"source\\": 2,
\\"type\\": 5,
\\"id\\": 42
}
},
{
\\"type\\": 3,
\\"data\\": {
\\"source\\": 5,
\\"text\\": \\"t\\",
\\"isChecked\\": false,
\\"id\\": 42
}
},
{
\\"type\\": 3,
\\"data\\": {
\\"source\\": 5,
\\"text\\": \\"te\\",
\\"isChecked\\": false,
\\"id\\": 42
}
},
{
\\"type\\": 3,
\\"data\\": {
\\"source\\": 5,
\\"text\\": \\"tex\\",
\\"isChecked\\": false,
\\"id\\": 42
}
},
{
\\"type\\": 3,
\\"data\\": {
\\"source\\": 5,
\\"text\\": \\"text\\",
\\"isChecked\\": false,
\\"id\\": 42
}
},
{
\\"type\\": 3,
\\"data\\": {
\\"source\\": 5,
\\"text\\": \\"texta\\",
\\"isChecked\\": false,
\\"id\\": 42
}
},
{
\\"type\\": 3,
\\"data\\": {
\\"source\\": 5,
\\"text\\": \\"textar\\",
\\"isChecked\\": false,
\\"id\\": 42
}
},
{
\\"type\\": 3,
\\"data\\": {
\\"source\\": 5,
\\"text\\": \\"textare\\",
\\"isChecked\\": false,
\\"id\\": 42
}
},
{
\\"type\\": 3,
\\"data\\": {
\\"source\\": 5,
\\"text\\": \\"textarea\\",
\\"isChecked\\": false,
\\"id\\": 42
}
},
{
\\"type\\": 3,
\\"data\\": {
\\"source\\": 5,
\\"text\\": \\"textarea \\",
\\"isChecked\\": false,
\\"id\\": 42
}
},
{
\\"type\\": 3,
\\"data\\": {
\\"source\\": 5,
\\"text\\": \\"textarea t\\",
\\"isChecked\\": false,
\\"id\\": 42
}
},
{
\\"type\\": 3,
\\"data\\": {
\\"source\\": 5,
\\"text\\": \\"textarea te\\",
\\"isChecked\\": false,
\\"id\\": 42
}
},
{
\\"type\\": 3,
\\"data\\": {
\\"source\\": 5,
\\"text\\": \\"textarea tes\\",
\\"isChecked\\": false,
\\"id\\": 42
}
},
{
\\"type\\": 3,
\\"data\\": {
\\"source\\": 5,
\\"text\\": \\"textarea test\\",
\\"isChecked\\": false,
\\"id\\": 42
}
},
{
\\"type\\": 3,
\\"data\\": {
\\"source\\": 5,
\\"text\\": \\"*\\",
\\"isChecked\\": false,
\\"id\\": 47
}
}
]"
`;
exports[`record integration tests should mask password value attribute with maskInputOptions 1`] = `
"[
{
@@ -8313,7 +9175,8 @@ exports[`record integration tests should not record input values if maskAllInput
\\"name\\": \\"\\",
\\"id\\": \\"\\",
\\"cols\\": \\"30\\",
\\"rows\\": \\"10\\"
\\"rows\\": \\"10\\",
\\"data-unmask-example\\": \\"true\\"
},
\\"childNodes\\": [],
\\"id\\": 42
@@ -12026,7 +12889,8 @@ exports[`record integration tests should record input userTriggered values if us
\\"name\\": \\"\\",
\\"id\\": \\"\\",
\\"cols\\": \\"30\\",
\\"rows\\": \\"10\\"
\\"rows\\": \\"10\\",
\\"data-unmask-example\\": \\"true\\"
},
\\"childNodes\\": [],
\\"id\\": 42

View File

@@ -22,7 +22,7 @@
<input type="checkbox" />
</label>
<label for="textarea">
<textarea name="" id="" cols="30" rows="10"></textarea>
<textarea name="" id="" cols="30" rows="10" data-unmask-example="true"></textarea>
</label>
<label for="select">
<select name="" id="">

View File

@@ -328,6 +328,36 @@ describe('record integration tests', function (this: ISuite) {
assertSnapshot(snapshots);
});
it('should mask inputs via function call', async () => {
const page: puppeteer.Page = await browser.newPage();
await page.goto('about:blank');
await page.setContent(
getHtml.call(this, 'form.html', {
maskAllInputs: true,
maskInputFn: (text: string, element: HTMLElement) => {
// If the element has the attribute "data-unmask-example", we don't mask it
if (element.hasAttribute('data-unmask-example')) {
return text;
}
return '*'.repeat(text.length);
},
}),
);
await page.type('input[type="text"]', 'test');
await page.click('input[type="radio"]');
await page.click('input[type="checkbox"]');
await page.type('input[type="password"]', 'password');
await page.type('textarea', 'textarea test');
await page.select('select', '1');
const snapshots = (await page.evaluate(
'window.snapshots',
)) as eventWithTime[];
assertSnapshot(snapshots);
});
it('should record input userTriggered values if userTriggeredOnInput is enabled', async () => {
const page: puppeteer.Page = await browser.newPage();
await page.goto('about:blank');

View File

@@ -1433,7 +1433,8 @@ exports[`cross origin iframes form.html should map input events correctly 1`] =
\\"name\\": \\"\\",
\\"id\\": \\"\\",
\\"cols\\": \\"30\\",
\\"rows\\": \\"10\\"
\\"rows\\": \\"10\\",
\\"data-unmask-example\\": \\"true\\"
},
\\"childNodes\\": [],
\\"rootId\\": 11,
@@ -2433,7 +2434,8 @@ exports[`cross origin iframes form.html should map scroll events correctly 1`] =
\\"name\\": \\"\\",
\\"id\\": \\"\\",
\\"cols\\": \\"30\\",
\\"rows\\": \\"10\\"
\\"rows\\": \\"10\\",
\\"data-unmask-example\\": \\"true\\"
},
\\"childNodes\\": [],
\\"rootId\\": 11,

View File

@@ -599,6 +599,7 @@ export function generateRecordSnippet(options: recordOptions<eventWithTime>) {
maskInputOptions: ${JSON.stringify(options.maskAllInputs)},
userTriggeredOnInput: ${options.userTriggeredOnInput},
maskTextFn: ${options.maskTextFn},
maskInputFn: ${options.maskInputFn},
recordCanvas: ${options.recordCanvas},
recordAfter: '${options.recordAfter || 'load'}',
inlineImages: ${options.inlineImages},