Skip to content

Commit 924b22e

Browse files
authored
john/hig-231-block-recording-on-all-text-nodes (#30)
* Adds text randomization * Update blocked styles * Add privacy mode * Don't record images * v0.10.0 * Rename flag to enableStrictPrivacy * Set redacted style on replayer side
1 parent d6a0238 commit 924b22e

File tree

14 files changed

+84
-21
lines changed

14 files changed

+84
-21
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@highlight-run/rrweb",
3-
"version": "0.9.29",
3+
"version": "0.10.0",
44
"description": "record and replay the web",
55
"scripts": {
66
"test": "npm run bundle:browser && cross-env TS_NODE_CACHE=false TS_NODE_FILES=true mocha -r ts-node/register test/**/*.test.ts",

src/record/index.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,9 @@ function record<T = eventWithTime>(
3333
emit,
3434
checkoutEveryNms,
3535
checkoutEveryNth,
36-
blockClass = 'rr-block',
36+
blockClass = 'highlight-block',
3737
blockSelector = null,
38-
ignoreClass = 'rr-ignore',
38+
ignoreClass = 'highlight-ignore',
3939
inlineStylesheet = true,
4040
maskAllInputs,
4141
maskInputOptions: _maskInputOptions,
@@ -49,6 +49,7 @@ function record<T = eventWithTime>(
4949
collectFonts = false,
5050
recordLog = false,
5151
debug,
52+
enableStrictPrivacy = false,
5253
} = options;
5354
// runtime checks for user options
5455
if (!emit) {
@@ -191,6 +192,7 @@ function record<T = eventWithTime>(
191192
maskAllInputs: maskInputOptions,
192193
slimDOM: slimDOMOptions,
193194
recordCanvas,
195+
enableStrictPrivacy,
194196
});
195197

196198
if (!node) {
@@ -366,6 +368,7 @@ function record<T = eventWithTime>(
366368
collectFonts,
367369
slimDOMOptions,
368370
logOptions,
371+
enableStrictPrivacy,
369372
},
370373
hooks,
371374
),

src/record/mutation.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@ export default class MutationBuffer {
147147
private inlineStylesheet: boolean;
148148
private maskInputOptions: MaskInputOptions;
149149
private recordCanvas: boolean;
150+
private enableStrictPrivacy: boolean;
150151
private slimDOMOptions: SlimDOMOptions;
151152

152153
public init(
@@ -157,13 +158,15 @@ export default class MutationBuffer {
157158
maskInputOptions: MaskInputOptions,
158159
recordCanvas: boolean,
159160
slimDOMOptions: SlimDOMOptions,
161+
enableStrictPrivacy: boolean,
160162
) {
161163
this.blockClass = blockClass;
162164
this.blockSelector = blockSelector;
163165
this.inlineStylesheet = inlineStylesheet;
164166
this.maskInputOptions = maskInputOptions;
165167
this.recordCanvas = recordCanvas;
166168
this.slimDOMOptions = slimDOMOptions;
169+
this.enableStrictPrivacy = enableStrictPrivacy;
167170
this.emissionCallback = cb;
168171
}
169172

@@ -228,6 +231,7 @@ export default class MutationBuffer {
228231
maskInputOptions: this.maskInputOptions,
229232
slimDOMOptions: this.slimDOMOptions,
230233
recordCanvas: this.recordCanvas,
234+
enableStrictPrivacy: this.enableStrictPrivacy,
231235
});
232236
if (sn) {
233237
adds.push({

src/record/observer.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ function initMutationObserver(
6060
maskInputOptions: MaskInputOptions,
6161
recordCanvas: boolean,
6262
slimDOMOptions: SlimDOMOptions,
63+
enableStrictPrivacy: boolean,
6364
): MutationObserver {
6465
// see mutation.ts for details
6566
mutationBuffer.init(
@@ -70,6 +71,7 @@ function initMutationObserver(
7071
maskInputOptions,
7172
recordCanvas,
7273
slimDOMOptions,
74+
enableStrictPrivacy,
7375
);
7476
let mutationBufferCtor = window.MutationObserver;
7577
const angularZoneSymbol = (window as WindowWithAngularZone)?.Zone?.__symbol__?.(
@@ -720,6 +722,7 @@ export function initObservers(
720722
o.maskInputOptions,
721723
o.recordCanvas,
722724
o.slimDOMOptions,
725+
o.enableStrictPrivacy,
723726
);
724727
const mousemoveHandler = initMoveObserver(o.mousemoveCb, o.sampling);
725728
const mouseInteractionHandler = initMouseInteractionObserver(

src/replay/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ export class Replayer {
127127
skipInactive: false,
128128
showWarning: true,
129129
showDebug: false,
130-
blockClass: 'rr-block',
130+
blockClass: 'highlight-block',
131131
liveMode: false,
132132
insertStyleRules: [],
133133
triggerFocus: true,
@@ -641,6 +641,7 @@ export class Replayer {
641641
const styleEl = document.createElement('style');
642642
const { documentElement, head } = this.iframe.contentDocument;
643643
documentElement!.insertBefore(styleEl, head);
644+
console.log(this.config.blockClass);
644645
const injectStylesRules = getInjectStyleRules(
645646
this.config.blockClass,
646647
).concat(this.config.insertStyleRules);

src/replay/styles/inject-style.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
const rules: (blockClass: string) => string[] = (blockClass: string) => [
22
`iframe, .${blockClass} { background: #ccc }`,
33
'noscript { display: none !important; }',
4+
`.${blockClass} { background: black; border-radius: 5px; }`,
5+
`.${blockClass}:hover::after {content: 'Redacted'; color: white; text-align: center; width: 100%; display: block;}`,
46
];
57

68
export default rules;

src/snapshot/snapshot.ts

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,7 @@ function serializeNode(
205205
inlineStylesheet: boolean;
206206
maskInputOptions: MaskInputOptions;
207207
recordCanvas: boolean;
208+
enableStrictPrivacy: boolean;
208209
},
209210
): serializedNode | false {
210211
const {
@@ -214,7 +215,9 @@ function serializeNode(
214215
inlineStylesheet,
215216
maskInputOptions = {},
216217
recordCanvas,
218+
enableStrictPrivacy,
217219
} = options;
220+
218221
switch (n.nodeType) {
219222
case n.DOCUMENT_NODE:
220223
return {
@@ -229,7 +232,7 @@ function serializeNode(
229232
systemId: (n as DocumentType).systemId,
230233
};
231234
case n.ELEMENT_NODE:
232-
const needBlock = _isBlockedElement(
235+
let needBlock = _isBlockedElement(
233236
n as HTMLElement,
234237
blockClass,
235238
blockSelector,
@@ -318,13 +321,14 @@ function serializeNode(
318321
if ((n as HTMLElement).scrollTop) {
319322
attributes.rr_scrollTop = (n as HTMLElement).scrollTop;
320323
}
321-
if (needBlock) {
324+
if (needBlock || (tagName === 'img' && enableStrictPrivacy)) {
322325
const { width, height } = (n as HTMLElement).getBoundingClientRect();
323326
attributes = {
324327
class: attributes.class,
325328
rr_width: `${width}px`,
326329
rr_height: `${height}px`,
327330
};
331+
needBlock = true;
328332
}
329333
return {
330334
type: NodeType.Element,
@@ -341,11 +345,38 @@ function serializeNode(
341345
n.parentNode && (n.parentNode as HTMLElement).tagName;
342346
let textContent = (n as Text).textContent;
343347
const isStyle = parentTagName === 'STYLE' ? true : undefined;
348+
/** Determines if this node has been handled already. */
349+
let textContentHandled = false;
344350
if (isStyle && textContent) {
345351
textContent = absoluteToStylesheet(textContent, getHref());
352+
textContentHandled = true;
346353
}
347354
if (parentTagName === 'SCRIPT') {
348355
textContent = 'SCRIPT_PLACEHOLDER';
356+
textContentHandled = true;
357+
} else if (parentTagName === 'NOSCRIPT') {
358+
textContent = '';
359+
textContentHandled = true;
360+
}
361+
362+
// Randomizes the text content to a string of the same length.
363+
if (enableStrictPrivacy && !textContentHandled && parentTagName) {
364+
const IGNORE_TAG_NAMES = new Set([
365+
'HEAD',
366+
'TITLE',
367+
'STYLE',
368+
'SCRIPT',
369+
'HTML',
370+
'BODY',
371+
'NOSCRIPT',
372+
]);
373+
if (!IGNORE_TAG_NAMES.has(parentTagName)) {
374+
textContent =
375+
textContent
376+
?.split(' ')
377+
.map((word) => Math.random().toString(20).substr(2, word.length))
378+
.join(' ') || '';
379+
}
349380
}
350381
return {
351382
type: NodeType.Text,
@@ -472,6 +503,7 @@ export function serializeNodeWithId(
472503
slimDOMOptions: SlimDOMOptions;
473504
recordCanvas?: boolean;
474505
preserveWhiteSpace?: boolean;
506+
enableStrictPrivacy: boolean;
475507
},
476508
): serializedNodeWithId | null {
477509
const {
@@ -484,6 +516,7 @@ export function serializeNodeWithId(
484516
maskInputOptions = {},
485517
slimDOMOptions,
486518
recordCanvas = false,
519+
enableStrictPrivacy,
487520
} = options;
488521
let { preserveWhiteSpace = true } = options;
489522
const _serializedNode = serializeNode(n, {
@@ -493,6 +526,7 @@ export function serializeNodeWithId(
493526
inlineStylesheet,
494527
maskInputOptions,
495528
recordCanvas,
529+
enableStrictPrivacy,
496530
});
497531
if (!_serializedNode) {
498532
// TODO: dev only
@@ -524,6 +558,16 @@ export function serializeNodeWithId(
524558
let recordChild = !skipChild;
525559
if (serializedNode.type === NodeType.Element) {
526560
recordChild = recordChild && !serializedNode.needBlock;
561+
562+
/** Highlight Code Begin */
563+
// Remove the image's src if enableStrictPrivacy.
564+
if (serializedNode.needBlock && serializedNode.tagName === 'img') {
565+
const clone = n.cloneNode();
566+
((clone as unknown) as HTMLImageElement).src = '';
567+
map[id] = clone as INode;
568+
}
569+
/** Highlight Code End */
570+
527571
// this property was not needed in replay side
528572
delete serializedNode.needBlock;
529573
}
@@ -552,6 +596,7 @@ export function serializeNodeWithId(
552596
slimDOMOptions,
553597
recordCanvas,
554598
preserveWhiteSpace,
599+
enableStrictPrivacy,
555600
});
556601
if (serializedChildNode) {
557602
serializedNode.childNodes.push(serializedChildNode);
@@ -570,15 +615,17 @@ function snapshot(
570615
slimDOM?: boolean | SlimDOMOptions;
571616
recordCanvas?: boolean;
572617
blockSelector?: string | null;
618+
enableStrictPrivacy: boolean;
573619
},
574620
): [serializedNodeWithId | null, idNodeMap] {
575621
const {
576-
blockClass = 'rr-block',
622+
blockClass = 'highlight-block',
577623
inlineStylesheet = true,
578624
recordCanvas = false,
579625
blockSelector = null,
580626
maskAllInputs = false,
581627
slimDOM = false,
628+
enableStrictPrivacy = false,
582629
} = options || {};
583630
const idNodeMap: idNodeMap = {};
584631
const maskInputOptions: MaskInputOptions =
@@ -632,6 +679,7 @@ function snapshot(
632679
maskInputOptions,
633680
slimDOMOptions,
634681
recordCanvas,
682+
enableStrictPrivacy,
635683
}),
636684
idNodeMap,
637685
];

src/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,11 @@ export type recordOptions<T> = {
207207
mousemoveWait?: number;
208208
recordLog?: boolean | LogRecordOptions;
209209
debug?: boolean;
210+
/**
211+
* Enabling this will disable recording of text data on the page. This is useful if you do not want to record personally identifiable information.
212+
* Text will be randomized. Instead of seeing "Hello World" in a recording, you will see "1fds1 j59a0".
213+
*/
214+
enableStrictPrivacy?: boolean;
210215
};
211216

212217
export type observerParam = {
@@ -232,6 +237,7 @@ export type observerParam = {
232237
recordCanvas: boolean;
233238
collectFonts: boolean;
234239
slimDOMOptions: SlimDOMOptions;
240+
enableStrictPrivacy: boolean;
235241
};
236242

237243
export type hooksParam = {

test/html/block.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
<title>Block record</title>
88
</head>
99
<body>
10-
<div class="rr-block" style="width: 50px; height: 50px;">
10+
<div class="highlight-block" style="width: 50px; height: 50px">
1111
<input type="text" /> <span id="text"></span>
1212
</div>
1313
</body>

typings/record/mutation.d.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,9 @@ export default class MutationBuffer {
1616
private inlineStylesheet;
1717
private maskInputOptions;
1818
private recordCanvas;
19+
private enableStrictPrivacy;
1920
private slimDOMOptions;
20-
init(cb: mutationCallBack, blockClass: blockClass, blockSelector: string | null, inlineStylesheet: boolean, maskInputOptions: MaskInputOptions, recordCanvas: boolean, slimDOMOptions: SlimDOMOptions): void;
21+
init(cb: mutationCallBack, blockClass: blockClass, blockSelector: string | null, inlineStylesheet: boolean, maskInputOptions: MaskInputOptions, recordCanvas: boolean, slimDOMOptions: SlimDOMOptions, enableStrictPrivacy: boolean): void;
2122
freeze(): void;
2223
unfreeze(): void;
2324
isFrozen(): boolean;

0 commit comments

Comments
 (0)