Mask value attribute changes for elements in maskInputOptions (#602)

* mask value attribute changes for elements in maskInputOptions

* refactor initInputObserver to use maskInputValue

* add todo

* Fix typo

* upgrade rrweb-snapshot to 1.1.6

* move maskInputValue to rrweb-snapshot
This commit is contained in:
Justin Halsall
2026-04-01 12:00:00 +08:00
committed by GitHub
parent 38a9e36b8f
commit 4dcd674e34
15 changed files with 469 additions and 47 deletions

View File

@@ -194,6 +194,7 @@ function record<T = eventWithTime>(
inlineStylesheet,
maskInputOptions,
maskTextFn,
maskInputFn,
recordCanvas,
sampling,
slimDOMOptions,

View File

@@ -7,6 +7,9 @@ import {
IGNORED_NODE,
isShadowRoot,
needMaskingText,
maskInputValue,
MaskTextFn,
MaskInputFn,
} from 'rrweb-snapshot';
import {
mutationRecord,
@@ -17,7 +20,6 @@ import {
attributeCursor,
removedNodeMutation,
addedNodeMutation,
MaskTextFn,
Mirror,
} from '../types';
import {
@@ -167,6 +169,7 @@ export default class MutationBuffer {
private inlineStylesheet: boolean;
private maskInputOptions: MaskInputOptions;
private maskTextFn: MaskTextFn | undefined;
private maskInputFn: MaskInputFn | undefined;
private recordCanvas: boolean;
private slimDOMOptions: SlimDOMOptions;
private doc: Document;
@@ -184,6 +187,7 @@ export default class MutationBuffer {
inlineStylesheet: boolean,
maskInputOptions: MaskInputOptions,
maskTextFn: MaskTextFn | undefined,
maskInputFn: MaskInputFn | undefined,
recordCanvas: boolean,
slimDOMOptions: SlimDOMOptions,
doc: Document,
@@ -198,6 +202,7 @@ export default class MutationBuffer {
this.inlineStylesheet = inlineStylesheet;
this.maskInputOptions = maskInputOptions;
this.maskTextFn = maskTextFn;
this.maskInputFn = maskInputFn;
this.recordCanvas = recordCanvas;
this.slimDOMOptions = slimDOMOptions;
this.emissionCallback = cb;
@@ -287,6 +292,7 @@ export default class MutationBuffer {
inlineStylesheet: this.inlineStylesheet,
maskInputOptions: this.maskInputOptions,
maskTextFn: this.maskTextFn,
maskInputFn: this.maskInputFn,
slimDOMOptions: this.slimDOMOptions,
recordCanvas: this.recordCanvas,
onSerialize: (currentN) => {
@@ -443,7 +449,16 @@ export default class MutationBuffer {
break;
}
case 'attributes': {
const value = (m.target as HTMLElement).getAttribute(m.attributeName!);
let value = (m.target as HTMLElement).getAttribute(m.attributeName!);
if (m.attributeName === 'value') {
value = maskInputValue({
maskInputOptions: this.maskInputOptions,
tagName: (m.target as HTMLElement).tagName,
type: (m.target as HTMLElement).getAttribute('type'),
value,
maskInputFn: this.maskInputFn,
});
}
if (isBlocked(m.target, this.blockClass) || value === m.oldValue) {
return;
}

View File

@@ -1,4 +1,11 @@
import { INode, MaskInputOptions, SlimDOMOptions } from 'rrweb-snapshot';
import {
INode,
MaskInputOptions,
SlimDOMOptions,
maskInputValue,
MaskInputFn,
MaskTextFn,
} from 'rrweb-snapshot';
import { FontFaceDescriptors, FontFaceSet } from 'css-font-loading-module';
import {
throttle,
@@ -35,8 +42,6 @@ import {
canvasMutationCallback,
fontCallback,
fontParam,
MaskInputFn,
MaskTextFn,
Mirror,
} from '../types';
import MutationBuffer from './mutation';
@@ -83,6 +88,7 @@ export function initMutationObserver(
inlineStylesheet: boolean,
maskInputOptions: MaskInputOptions,
maskTextFn: MaskTextFn | undefined,
maskInputFn: MaskInputFn | undefined,
recordCanvas: boolean,
slimDOMOptions: SlimDOMOptions,
mirror: Mirror,
@@ -102,6 +108,7 @@ export function initMutationObserver(
inlineStylesheet,
maskInputOptions,
maskTextFn,
maskInputFn,
recordCanvas,
slimDOMOptions,
doc,
@@ -366,11 +373,13 @@ function initInputObserver(
] ||
maskInputOptions[type as keyof MaskInputOptions]
) {
if (maskInputFn) {
text = maskInputFn(text);
} else {
text = '*'.repeat(text.length);
}
text = maskInputValue({
maskInputOptions,
tagName: (target as HTMLElement).tagName,
type,
value: text,
maskInputFn,
});
}
cbWithDedup(target, { text, isChecked });
// if a radio was checked
@@ -476,7 +485,7 @@ function initMediaInteractionObserver(
blockClass: blockClass,
mirror: Mirror,
): listenerHandler {
const handler = (type: MediaInteractions ) => (event: Event) => {
const handler = (type: MediaInteractions) => (event: Event) => {
const target = getEventTarget(event);
if (!target || isBlocked(target as Node, blockClass)) {
return;
@@ -484,13 +493,13 @@ function initMediaInteractionObserver(
mediaInteractionCb({
type,
id: mirror.getId(target as INode),
currentTime: (target as HTMLMediaElement).currentTime
currentTime: (target as HTMLMediaElement).currentTime,
});
};
const handlers = [
on('play', handler(MediaInteractions.Play)),
on('pause', handler(MediaInteractions.Pause)),
on('seeked', handler(MediaInteractions.Seeked))
on('play', handler(MediaInteractions.Play)),
on('pause', handler(MediaInteractions.Pause)),
on('seeked', handler(MediaInteractions.Seeked)),
];
return () => {
handlers.forEach((h) => h());
@@ -716,6 +725,7 @@ export function initObservers(
o.inlineStylesheet,
o.maskInputOptions,
o.maskTextFn,
o.maskInputFn,
o.recordCanvas,
o.slimDOMOptions,
o.mirror,

View File

@@ -2,12 +2,16 @@ import {
mutationCallBack,
blockClass,
maskTextClass,
MaskTextFn,
Mirror,
scrollCallback,
SamplingStrategy,
} from '../types';
import { MaskInputOptions, SlimDOMOptions } from 'rrweb-snapshot';
import {
MaskInputOptions,
SlimDOMOptions,
MaskTextFn,
MaskInputFn,
} from 'rrweb-snapshot';
import { IframeManager } from './iframe-manager';
import { initMutationObserver, initScrollObserver } from './observer';
@@ -19,6 +23,7 @@ type BypassOptions = {
inlineStylesheet: boolean;
maskInputOptions: MaskInputOptions;
maskTextFn: MaskTextFn | undefined;
maskInputFn: MaskInputFn | undefined;
recordCanvas: boolean;
sampling: SamplingStrategy;
slimDOMOptions: SlimDOMOptions;
@@ -54,6 +59,7 @@ export class ShadowDomManager {
this.bypassOptions.inlineStylesheet,
this.bypassOptions.maskInputOptions,
this.bypassOptions.maskTextFn,
this.bypassOptions.maskInputFn,
this.bypassOptions.recordCanvas,
this.bypassOptions.slimDOMOptions,
this.mirror,

View File

@@ -4,6 +4,8 @@ import {
INode,
MaskInputOptions,
SlimDOMOptions,
MaskInputFn,
MaskTextFn,
} from 'rrweb-snapshot';
import { PackFn, UnpackFn } from './packer/base';
import { FontFaceDescriptors } from 'css-font-loading-module';
@@ -544,10 +546,6 @@ export enum ReplayerEvents {
PlayBack = 'play-back',
}
export type MaskInputFn = (text: string) => string;
export type MaskTextFn = (text: string) => string;
// store the state that would be changed during the process(unmount from dom and mount again)
export type ElementState = {
// [scrollLeft,scrollTop]

View File

@@ -53,7 +53,7 @@ export function createMirror(): Mirror {
delete this.map[id];
if (n.childNodes) {
n.childNodes.forEach((child) =>
this.removeNodeFromMap(child as Node as INode),
this.removeNodeFromMap((child as Node) as INode),
);
}
},
@@ -275,7 +275,7 @@ export function isAncestorRemoved(target: INode, mirror: Mirror): boolean {
if (!target.parentNode) {
return true;
}
return isAncestorRemoved(target.parentNode as unknown as INode, mirror);
return isAncestorRemoved((target.parentNode as unknown) as INode, mirror);
}
export function isTouchEvent(
@@ -286,13 +286,13 @@ export function isTouchEvent(
export function polyfill(win = window) {
if ('NodeList' in win && !win.NodeList.prototype.forEach) {
win.NodeList.prototype.forEach = Array.prototype
.forEach as unknown as NodeList['forEach'];
win.NodeList.prototype.forEach = (Array.prototype
.forEach as unknown) as NodeList['forEach'];
}
if ('DOMTokenList' in win && !win.DOMTokenList.prototype.forEach) {
win.DOMTokenList.prototype.forEach = Array.prototype
.forEach as unknown as DOMTokenList['forEach'];
win.DOMTokenList.prototype.forEach = (Array.prototype
.forEach as unknown) as DOMTokenList['forEach'];
}
// https://github.com/Financial-Times/polyfill-service/pull/183
@@ -396,7 +396,7 @@ export class TreeIndex {
const node = mirror.getNode(id);
node?.childNodes.forEach((childNode) => {
if ('__sn' in childNode) {
deepRemoveFromMirror((childNode as unknown as INode).__sn.id);
deepRemoveFromMirror(((childNode as unknown) as INode).__sn.id);
}
});
};
@@ -460,8 +460,12 @@ export class TreeIndex {
scrollMap: TreeIndex['scrollMap'];
inputMap: TreeIndex['inputMap'];
} {
const { tree, removeNodeMutations, textMutations, attributeMutations } =
this;
const {
tree,
removeNodeMutations,
textMutations,
attributeMutations,
} = this;
const batchMutationData: mutationData = {
source: IncrementalSource.Mutation,
@@ -650,5 +654,5 @@ export function getBaseDimension(
export function hasShadowRoot<T extends Node>(
n: T,
): n is T & { shadowRoot: ShadowRoot } {
return Boolean((n as unknown as Element)?.shadowRoot);
return Boolean(((n as unknown) as Element)?.shadowRoot);
}