Skip to content

Commit 9d17a71

Browse files
committed
feat: Allow for masking of attributes via maskAttributesFn
This currently adds a new API, `maskAttributesFn: (key: string, value: string, element: HTMLElement) => string`, that is used as a callback in `transformAttribute`. I prefer this API as it gives more flexibility for users (though it may need to pass the el node for most flexibility), but it is inconsistent with `maskTextFn` and `maskInputFn`. other options: * Rename this to something else (open to ideas) * Change this to pass value, and dom element (similar to MaskInputFn) to customize masking instead of decision maker of when to mask and introduce a simpler declarative API for what attributes to mask * ???
1 parent 759ab0c commit 9d17a71

File tree

13 files changed

+897
-62
lines changed

13 files changed

+897
-62
lines changed

.changeset/twenty-tables-call.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'rrweb-snapshot': patch
3+
'rrweb': patch
4+
---
5+
6+
Add `maskAttributesFn` to be called when transforming an attribute. This is typically used to determine if an attribute should be masked or not.

guide.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ The parameter of `rrweb.record` accepts the following options.
151151
| unmaskTextSelector | null | Use a string to configure which selector should be unmasked, refer to the [privacy](#privacy) chapter |
152152
| maskAllInputs | false | mask all input content as \* |
153153
| maskInputOptions | { password: true } | mask some kinds of input \*<br />refer to the [list](https://github.com/rrweb-io/rrweb/blob/588164aa12f1d94576f89ae0210b98f6e971c895/packages/rrweb-snapshot/src/types.ts#L77-L95) |
154+
| maskAttributeFn | - | callback before transforming attribute. can be used to mask specific attributes |
154155
| maskInputFn | - | customize mask input content recording logic |
155156
| maskTextFn | - | customize mask text content recording logic |
156157
| slimDOMOptions | {} | remove unnecessary parts of the DOM <br />refer to the [list](https://github.com/rrweb-io/rrweb/blob/588164aa12f1d94576f89ae0210b98f6e971c895/packages/rrweb-snapshot/src/types.ts#L97-L108) |

packages/rrweb-snapshot/src/snapshot.ts

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
KeepIframeSrcFn,
1212
ICanvas,
1313
serializedElementNodeWithId,
14+
MaskAttributeFn,
1415
} from './types';
1516
import {
1617
Mirror,
@@ -231,6 +232,8 @@ export function transformAttribute(
231232
tagName: Lowercase<string>,
232233
name: Lowercase<string>,
233234
value: string | null,
235+
element: HTMLElement,
236+
maskAttributeFn: MaskAttributeFn | undefined,
234237
): string | null {
235238
if (!value) {
236239
return value;
@@ -259,6 +262,11 @@ export function transformAttribute(
259262
return absoluteToDoc(doc, value);
260263
}
261264

265+
// Custom attribute masking
266+
if (typeof maskAttributeFn === 'function') {
267+
return maskAttributeFn(name, value, element);
268+
}
269+
262270
return value;
263271
}
264272

@@ -517,6 +525,7 @@ function serializeNode(
517525
blockClass: string | RegExp;
518526
blockSelector: string | null;
519527
maskAllText: boolean;
528+
maskAttributeFn: MaskAttributeFn | undefined;
520529
maskTextClass: string | RegExp;
521530
unmaskTextClass: string | RegExp | null;
522531
maskTextSelector: string | null;
@@ -541,6 +550,7 @@ function serializeNode(
541550
blockClass,
542551
blockSelector,
543552
maskAllText,
553+
maskAttributeFn,
544554
maskTextClass,
545555
unmaskTextClass,
546556
maskTextSelector,
@@ -585,6 +595,7 @@ function serializeNode(
585595
blockClass,
586596
blockSelector,
587597
inlineStylesheet,
598+
maskAttributeFn,
588599
maskInputOptions,
589600
maskInputFn,
590601
dataURLOptions,
@@ -649,7 +660,6 @@ function serializeTextNode(
649660
},
650661
): serializedNode {
651662
const {
652-
<<<<<<< HEAD
653663
maskAllText,
654664
maskTextClass,
655665
unmaskTextClass,
@@ -658,13 +668,6 @@ function serializeTextNode(
658668
maskTextFn,
659669
maskInputOptions,
660670
maskInputFn,
661-
=======
662-
maskTextClass,
663-
maskTextSelector,
664-
maskTextFn,
665-
maskInputFn,
666-
maskInputOptions,
667-
>>>>>>> b310e6f (feat: Better masking of option/radio/checkbox values)
668671
rootId,
669672
} = options;
670673
// The parent node may not be a html element which has a tagName attribute.
@@ -748,6 +751,7 @@ function serializeElementNode(
748751
blockClass: string | RegExp;
749752
blockSelector: string | null;
750753
inlineStylesheet: boolean;
754+
maskAttributeFn: MaskAttributeFn | undefined;
751755
maskInputOptions: MaskInputOptions;
752756
maskInputFn: MaskInputFn | undefined;
753757
dataURLOptions?: DataURLOptions;
@@ -772,6 +776,7 @@ function serializeElementNode(
772776
blockSelector,
773777
inlineStylesheet,
774778
maskInputOptions = {},
779+
maskAttributeFn,
775780
maskInputFn,
776781
dataURLOptions = {},
777782
inlineImages,
@@ -797,6 +802,8 @@ function serializeElementNode(
797802
tagName,
798803
toLowerCase(attr.name),
799804
attr.value,
805+
n,
806+
maskAttributeFn,
800807
);
801808
}
802809
}
@@ -845,10 +852,7 @@ function serializeElementNode(
845852
const type = getInputType(el);
846853
const value = getInputValue(el, toUpperCase(tagName), type);
847854
const checked = (n as HTMLInputElement).checked;
848-
<<<<<<< HEAD
849855
if (
850-
attributes.type !== 'radio' &&
851-
attributes.type !== 'checkbox' &&
852856
attributes.type !== 'submit' &&
853857
attributes.type !== 'button' &&
854858
value
@@ -863,9 +867,6 @@ function serializeElementNode(
863867
maskAllText,
864868
);
865869

866-
=======
867-
if (type !== 'submit' && type !== 'button' && value) {
868-
>>>>>>> b310e6f (feat: Better masking of option/radio/checkbox values)
869870
attributes.value = maskInputValue({
870871
element: el,
871872
type,
@@ -1129,6 +1130,7 @@ export function serializeNodeWithId(
11291130
newlyAddedElement?: boolean;
11301131
maskInputOptions?: MaskInputOptions;
11311132
maskAllText: boolean;
1133+
maskAttributeFn: MaskAttributeFn | undefined;
11321134
maskTextFn: MaskTextFn | undefined;
11331135
maskInputFn: MaskInputFn | undefined;
11341136
slimDOMOptions: SlimDOMOptions;
@@ -1163,6 +1165,7 @@ export function serializeNodeWithId(
11631165
skipChild = false,
11641166
inlineStylesheet = true,
11651167
maskInputOptions = {},
1168+
maskAttributeFn,
11661169
maskTextFn,
11671170
maskInputFn,
11681171
slimDOMOptions,
@@ -1190,6 +1193,7 @@ export function serializeNodeWithId(
11901193
unmaskTextSelector,
11911194
inlineStylesheet,
11921195
maskInputOptions,
1196+
maskAttributeFn,
11931197
maskTextFn,
11941198
maskInputFn,
11951199
dataURLOptions,
@@ -1266,6 +1270,7 @@ export function serializeNodeWithId(
12661270
skipChild,
12671271
inlineStylesheet,
12681272
maskInputOptions,
1273+
maskAttributeFn,
12691274
maskTextFn,
12701275
maskInputFn,
12711276
slimDOMOptions,
@@ -1329,6 +1334,7 @@ export function serializeNodeWithId(
13291334
skipChild: false,
13301335
inlineStylesheet,
13311336
maskInputOptions,
1337+
maskAttributeFn,
13321338
maskTextFn,
13331339
maskInputFn,
13341340
slimDOMOptions,
@@ -1379,6 +1385,7 @@ export function serializeNodeWithId(
13791385
skipChild: false,
13801386
inlineStylesheet,
13811387
maskInputOptions,
1388+
maskAttributeFn,
13821389
maskTextFn,
13831390
maskInputFn,
13841391
slimDOMOptions,
@@ -1422,6 +1429,7 @@ function snapshot(
14221429
unmaskTextSelector?: string | null;
14231430
inlineStylesheet?: boolean;
14241431
maskAllInputs?: boolean | MaskInputOptions;
1432+
maskAttributeFn?: MaskAttributeFn;
14251433
maskTextFn?: MaskTextFn;
14261434
maskInputFn?: MaskInputFn;
14271435
slimDOM?: 'all' | boolean | SlimDOMOptions;
@@ -1456,6 +1464,7 @@ function snapshot(
14561464
inlineImages = false,
14571465
recordCanvas = false,
14581466
maskAllInputs = false,
1467+
maskAttributeFn,
14591468
maskTextFn,
14601469
maskInputFn,
14611470
slimDOM = false,
@@ -1524,6 +1533,7 @@ function snapshot(
15241533
skipChild: false,
15251534
inlineStylesheet,
15261535
maskInputOptions,
1536+
maskAttributeFn,
15271537
maskTextFn,
15281538
maskInputFn,
15291539
slimDOMOptions,

packages/rrweb-snapshot/src/types.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,11 @@ export type DataURLOptions = Partial<{
159159

160160
export type MaskTextFn = (text: string) => string;
161161
export type MaskInputFn = (text: string, element: HTMLElement) => string;
162+
export type MaskAttributeFn = (
163+
attributeName: string,
164+
attributeValue: string,
165+
element: HTMLElement,
166+
) => string;
162167

163168
export type KeepIframeSrcFn = (src: string) => boolean;
164169

packages/rrweb-snapshot/test/snapshot.test.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { JSDOM } from 'jsdom';
55
import {
66
absoluteToStylesheet,
77
serializeNodeWithId,
8+
transformAttribute,
89
_isBlockedElement,
910
needMaskingText,
1011
} from '../src/snapshot';
@@ -111,6 +112,50 @@ describe('absolute url to stylesheet', () => {
111112
});
112113
});
113114

115+
describe('transformAttribute()', () => {
116+
it('handles empty attribute value', () => {
117+
expect(
118+
transformAttribute(
119+
document,
120+
'a',
121+
'data-loading',
122+
null,
123+
document.createElement('span'),
124+
undefined,
125+
),
126+
).toBe(null);
127+
expect(
128+
transformAttribute(
129+
document,
130+
'a',
131+
'data-loading',
132+
'',
133+
document.createElement('span'),
134+
undefined,
135+
),
136+
).toBe('');
137+
});
138+
139+
it('handles custom masking function', () => {
140+
const maskAttributeFn = jest
141+
.fn()
142+
.mockImplementation((_key, value): string => {
143+
return value.split('').reverse().join('');
144+
}) as any;
145+
expect(
146+
transformAttribute(
147+
document,
148+
'a',
149+
'data-loading',
150+
'foo',
151+
document.createElement('span'),
152+
maskAttributeFn,
153+
),
154+
).toBe('oof');
155+
expect(maskAttributeFn).toHaveBeenCalledTimes(1);
156+
});
157+
});
158+
114159
describe('isBlockedElement()', () => {
115160
const subject = (html: string, opt: any = {}) =>
116161
_isBlockedElement(render(html), 'rr-block', opt.blockSelector);
@@ -151,6 +196,7 @@ describe('style elements', () => {
151196
unmaskTextSelector: null,
152197
skipChild: false,
153198
inlineStylesheet: true,
199+
maskAttributeFn: undefined,
154200
maskTextFn: undefined,
155201
maskInputFn: undefined,
156202
slimDOMOptions: {},
@@ -199,6 +245,7 @@ describe('scrollTop/scrollLeft', () => {
199245
unmaskTextSelector: null,
200246
skipChild: false,
201247
inlineStylesheet: true,
248+
maskAttributeFn: undefined,
202249
maskTextFn: undefined,
203250
maskInputFn: undefined,
204251
slimDOMOptions: {},

packages/rrweb/src/record/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ function record<T = eventWithTime>(
7373
maskAllInputs,
7474
maskInputOptions: _maskInputOptions,
7575
slimDOMOptions: _slimDOMOptions,
76+
maskAttributeFn,
7677
maskInputFn,
7778
maskTextFn,
7879
hooks,
@@ -336,6 +337,7 @@ function record<T = eventWithTime>(
336337
inlineStylesheet,
337338
maskInputOptions,
338339
dataURLOptions,
340+
maskAttributeFn,
339341
maskTextFn,
340342
maskInputFn,
341343
recordCanvas,
@@ -381,6 +383,7 @@ function record<T = eventWithTime>(
381383
unmaskTextSelector,
382384
inlineStylesheet,
383385
maskAllInputs: maskInputOptions,
386+
maskAttributeFn,
384387
maskInputFn,
385388
maskTextFn,
386389
slimDOM: slimDOMOptions,
@@ -558,6 +561,7 @@ function record<T = eventWithTime>(
558561
userTriggeredOnInput,
559562
collectFonts,
560563
doc,
564+
maskAttributeFn,
561565
maskInputFn,
562566
maskTextFn,
563567
keepIframeSrcFn,

packages/rrweb/src/record/mutation.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,7 @@ export default class MutationBuffer {
179179
private unmaskTextSelector: observerParam['unmaskTextSelector'];
180180
private inlineStylesheet: observerParam['inlineStylesheet'];
181181
private maskInputOptions: observerParam['maskInputOptions'];
182+
private maskAttributeFn: observerParam['maskAttributeFn'];
182183
private maskTextFn: observerParam['maskTextFn'];
183184
private maskInputFn: observerParam['maskInputFn'];
184185
private keepIframeSrcFn: observerParam['keepIframeSrcFn'];
@@ -207,6 +208,7 @@ export default class MutationBuffer {
207208
'unmaskTextSelector',
208209
'inlineStylesheet',
209210
'maskInputOptions',
211+
'maskAttributeFn',
210212
'maskTextFn',
211213
'maskInputFn',
212214
'keepIframeSrcFn',
@@ -313,6 +315,7 @@ export default class MutationBuffer {
313315
newlyAddedElement: true,
314316
inlineStylesheet: this.inlineStylesheet,
315317
maskInputOptions: this.maskInputOptions,
318+
maskAttributeFn: this.maskAttributeFn,
316319
maskTextFn: this.maskTextFn,
317320
maskInputFn: this.maskInputFn,
318321
slimDOMOptions: this.slimDOMOptions,
@@ -627,6 +630,8 @@ export default class MutationBuffer {
627630
toLowerCase(target.tagName),
628631
toLowerCase(attributeName),
629632
value,
633+
target,
634+
this.maskAttributeFn,
630635
);
631636
}
632637
break;

packages/rrweb/src/types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type {
55
MaskInputFn,
66
MaskTextFn,
77
DataURLOptions,
8+
MaskAttributeFn,
89
} from 'rrweb-snapshot';
910
import type { PackFn, UnpackFn } from './packer/base';
1011
import type { IframeManager } from './record/iframe-manager';
@@ -55,6 +56,7 @@ export type recordOptions<T> = {
5556
unmaskTextSelector?: string;
5657
maskAllInputs?: boolean;
5758
maskInputOptions?: MaskInputOptions;
59+
maskAttributeFn?: MaskAttributeFn;
5860
maskInputFn?: MaskInputFn;
5961
maskTextFn?: MaskTextFn;
6062
slimDOMOptions?: SlimDOMOptions | 'all' | true;
@@ -95,6 +97,7 @@ export type observerParam = {
9597
maskTextSelector: string | null;
9698
unmaskTextSelector: string | null;
9799
maskInputOptions: MaskInputOptions;
100+
maskAttributeFn?: MaskAttributeFn;
98101
maskInputFn?: MaskInputFn;
99102
maskTextFn?: MaskTextFn;
100103
keepIframeSrcFn: KeepIframeSrcFn;
@@ -142,6 +145,7 @@ export type MutationBufferParam = Pick<
142145
| 'unmaskTextSelector'
143146
| 'inlineStylesheet'
144147
| 'maskInputOptions'
148+
| 'maskAttributeFn'
145149
| 'maskTextFn'
146150
| 'maskInputFn'
147151
| 'keepIframeSrcFn'

0 commit comments

Comments
 (0)