Skip to content

Commit c60b366

Browse files
committed
fix: Ensure text masking for updated attributes works
We had inconsistent casing for the tagName, leading to some masking slipping through.
1 parent 0eb738a commit c60b366

File tree

5 files changed

+56
-14
lines changed

5 files changed

+56
-14
lines changed

packages/rrweb-snapshot/src/snapshot.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -237,8 +237,8 @@ function getHref() {
237237
export function transformAttribute(
238238
doc: Document,
239239
element: HTMLElement,
240-
tagName: string,
241-
name: string,
240+
_tagName: string,
241+
_name: string,
242242
value: string | null,
243243
maskAllText: boolean,
244244
unmaskTextSelector: string | undefined | null,
@@ -248,6 +248,9 @@ export function transformAttribute(
248248
return value;
249249
}
250250

251+
const name = _name.toLowerCase();
252+
const tagName = _tagName.toLowerCase();
253+
251254
// relative path in attribute
252255
if (name === 'src' || name === 'href') {
253256
return absoluteToDoc(doc, value);

packages/rrweb-snapshot/typings/snapshot.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { serializedNodeWithId, INode, idNodeMap, MaskInputOptions, SlimDOMOption
22
export declare const IGNORED_NODE = -2;
33
export declare function absoluteToStylesheet(cssText: string | null, href: string): string;
44
export declare function absoluteToDoc(doc: Document, attributeValue: string): string;
5-
export declare function transformAttribute(doc: Document, element: HTMLElement, tagName: string, name: string, value: string | null, maskAllText: boolean, unmaskTextSelector: string | undefined | null, maskTextFn: MaskTextFn | undefined): string | null;
5+
export declare function transformAttribute(doc: Document, element: HTMLElement, _tagName: string, _name: string, value: string | null, maskAllText: boolean, unmaskTextSelector: string | undefined | null, maskTextFn: MaskTextFn | undefined): string | null;
66
export declare function _isBlockedElement(element: HTMLElement, blockClass: string | RegExp, blockSelector: string | null, unblockSelector: string | null): boolean;
77
export declare function needMaskingText(node: Node | null, maskTextClass: string | RegExp, maskTextSelector: string | null, unmaskTextSelector: string | null, maskAllText: boolean): boolean;
88
export declare function serializeNodeWithId(n: Node | INode, options: {

packages/rrweb/src/record/observer.ts

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ export function initMutationObserver(
113113
// If this callback returns `false`, we do not want to process the mutations
114114
// This can be used to e.g. do a manual full snapshot when mutations become too large, or similar.
115115
if (options.onMutation && options.onMutation(mutations) === false) {
116-
return;
116+
return;
117117
}
118118
mutationBuffer.processMutations(mutations);
119119
}),
@@ -231,7 +231,9 @@ function initMouseInteractionObserver({
231231
const getHandler = (eventKey: keyof typeof MouseInteractions) => {
232232
return (event: MouseEvent | TouchEvent) => {
233233
const target = getEventTarget(event) as Node;
234-
if (isBlocked(target as Node, blockClass, blockSelector, unblockSelector)) {
234+
if (
235+
isBlocked(target as Node, blockClass, blockSelector, unblockSelector)
236+
) {
235237
return;
236238
}
237239
const e = isTouchEvent(event) ? event.changedTouches[0] : event;
@@ -275,11 +277,20 @@ export function initScrollObserver({
275277
sampling,
276278
}: Pick<
277279
observerParam,
278-
'scrollCb' | 'doc' | 'mirror' | 'blockClass' | 'blockSelector' | 'unblockSelector' | 'sampling'
280+
| 'scrollCb'
281+
| 'doc'
282+
| 'mirror'
283+
| 'blockClass'
284+
| 'blockSelector'
285+
| 'unblockSelector'
286+
| 'sampling'
279287
>): listenerHandler {
280288
const updatePosition = throttle<UIEvent>((evt) => {
281289
const target = getEventTarget(evt);
282-
if (!target || isBlocked(target as Node, blockClass, blockSelector, unblockSelector)) {
290+
if (
291+
!target ||
292+
isBlocked(target as Node, blockClass, blockSelector, unblockSelector)
293+
) {
283294
return;
284295
}
285296
const id = mirror.getId(target as INode);
@@ -350,17 +361,18 @@ function initInputObserver({
350361
}: observerParam): listenerHandler {
351362
function eventHandler(event: Event) {
352363
let target = getEventTarget(event);
364+
const tagName = target && (target as Element).tagName;
365+
353366
const userTriggered = event.isTrusted;
354367
/**
355368
* If a site changes the value 'selected' of an option element, the value of its parent element, usually a select element, will be changed as well.
356369
* We can treat this change as a value change of the select element the current target belongs to.
357370
*/
358-
if (target && (target as Element).tagName === 'OPTION')
359-
target = (target as Element).parentElement;
371+
if (tagName === 'OPTION') target = (target as Element).parentElement;
360372
if (
361373
!target ||
362-
!(target as Element).tagName ||
363-
INPUT_TAGS.indexOf((target as Element).tagName) < 0 ||
374+
!tagName ||
375+
INPUT_TAGS.indexOf(tagName) < 0 ||
364376
isBlocked(target as Node, blockClass, blockSelector, unblockSelector)
365377
) {
366378
return;
@@ -386,7 +398,7 @@ function initInputObserver({
386398
hasInputMaskOptions({
387399
maskInputOptions,
388400
maskInputSelector,
389-
tagName: (target as HTMLElement).tagName,
401+
tagName,
390402
type,
391403
})
392404
) {
@@ -395,7 +407,7 @@ function initInputObserver({
395407
maskInputOptions,
396408
maskInputSelector,
397409
unmaskInputSelector,
398-
tagName: (target as HTMLElement).tagName,
410+
tagName,
399411
type,
400412
value: text,
401413
maskInputFn,
@@ -754,7 +766,10 @@ function initMediaInteractionObserver({
754766
throttle(
755767
callbackWrapper((event: Event) => {
756768
const target = getEventTarget(event);
757-
if (!target || isBlocked(target as Node, blockClass, blockSelector, unblockSelector)) {
769+
if (
770+
!target ||
771+
isBlocked(target as Node, blockClass, blockSelector, unblockSelector)
772+
) {
758773
return;
759774
}
760775
const { currentTime, volume, muted } = target as HTMLMediaElement;

packages/rrweb/test/__snapshots__/integration.test.ts.snap

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6524,6 +6524,23 @@ exports[`record integration tests should mask text in form elements 1`] = `
65246524
"top": 0
65256525
}
65266526
}
6527+
},
6528+
{
6529+
"type": 3,
6530+
"data": {
6531+
"source": 0,
6532+
"texts": [],
6533+
"attributes": [
6534+
{
6535+
"id": 87,
6536+
"attributes": {
6537+
"value": "*** *****"
6538+
}
6539+
}
6540+
],
6541+
"removes": [],
6542+
"adds": []
6543+
}
65276544
}
65286545
]"
65296546
`;

packages/rrweb/test/integration.test.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -598,6 +598,13 @@ describe('record integration tests', function (this: ISuite) {
598598
getHtml.call(this, 'form.html', { maskAllText: true }),
599599
);
600600

601+
// Ensure also masked when we change stuff
602+
await page.evaluate(() => {
603+
document
604+
.querySelector('input[type="submit"]')
605+
?.setAttribute('value', 'new value');
606+
});
607+
601608
const snapshots = await page.evaluate('window.snapshots');
602609
assertSnapshot(snapshots);
603610
});

0 commit comments

Comments
 (0)