Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@highlight-run/rrweb",
"version": "0.9.29",
"version": "0.10.0",
"description": "record and replay the web",
"scripts": {
"test": "npm run bundle:browser && cross-env TS_NODE_CACHE=false TS_NODE_FILES=true mocha -r ts-node/register test/**/*.test.ts",
Expand Down
3 changes: 3 additions & 0 deletions src/record/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ function record<T = eventWithTime>(
collectFonts = false,
recordLog = false,
debug,
isStrictPrivacy = false,
} = options;
// runtime checks for user options
if (!emit) {
Expand Down Expand Up @@ -191,6 +192,7 @@ function record<T = eventWithTime>(
maskAllInputs: maskInputOptions,
slimDOM: slimDOMOptions,
recordCanvas,
isStrictPrivacy,
});

if (!node) {
Expand Down Expand Up @@ -366,6 +368,7 @@ function record<T = eventWithTime>(
collectFonts,
slimDOMOptions,
logOptions,
isStrictPrivacy,
},
hooks,
),
Expand Down
4 changes: 4 additions & 0 deletions src/record/mutation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ export default class MutationBuffer {
private inlineStylesheet: boolean;
private maskInputOptions: MaskInputOptions;
private recordCanvas: boolean;
private isStrictPrivacy: boolean;
private slimDOMOptions: SlimDOMOptions;

public init(
Expand All @@ -157,13 +158,15 @@ export default class MutationBuffer {
maskInputOptions: MaskInputOptions,
recordCanvas: boolean,
slimDOMOptions: SlimDOMOptions,
isStrictPrivacy: boolean,
) {
this.blockClass = blockClass;
this.blockSelector = blockSelector;
this.inlineStylesheet = inlineStylesheet;
this.maskInputOptions = maskInputOptions;
this.recordCanvas = recordCanvas;
this.slimDOMOptions = slimDOMOptions;
this.isStrictPrivacy = isStrictPrivacy;
this.emissionCallback = cb;
}

Expand Down Expand Up @@ -228,6 +231,7 @@ export default class MutationBuffer {
maskInputOptions: this.maskInputOptions,
slimDOMOptions: this.slimDOMOptions,
recordCanvas: this.recordCanvas,
isStrictPrivacy: this.isStrictPrivacy,
});
if (sn) {
adds.push({
Expand Down
3 changes: 3 additions & 0 deletions src/record/observer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ function initMutationObserver(
maskInputOptions: MaskInputOptions,
recordCanvas: boolean,
slimDOMOptions: SlimDOMOptions,
isStrictPrivacy: boolean,
): MutationObserver {
// see mutation.ts for details
mutationBuffer.init(
Expand All @@ -70,6 +71,7 @@ function initMutationObserver(
maskInputOptions,
recordCanvas,
slimDOMOptions,
isStrictPrivacy,
);
let mutationBufferCtor = window.MutationObserver;
const angularZoneSymbol = (window as WindowWithAngularZone)?.Zone?.__symbol__?.(
Expand Down Expand Up @@ -720,6 +722,7 @@ export function initObservers(
o.maskInputOptions,
o.recordCanvas,
o.slimDOMOptions,
o.isStrictPrivacy,
);
const mousemoveHandler = initMoveObserver(o.mousemoveCb, o.sampling);
const mouseInteractionHandler = initMouseInteractionObserver(
Expand Down
7 changes: 7 additions & 0 deletions src/snapshot/rebuild.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,13 @@ function buildNode(
if (name === 'rr_height') {
(node as HTMLElement).style.height = value;
}
/** Highlight Code Start */
// rr_width and rr_height are only set if the node is blocked/censored.
if (name === 'rr_width' || name === 'rr_height') {
(node as HTMLElement).style.backgroundColor = '#000000';
(node as HTMLElement).style.borderRadius = '5px';
}
/** Highlight Code End */
if (name === 'rr_mediaState') {
switch (value) {
case 'played':
Expand Down
49 changes: 47 additions & 2 deletions src/snapshot/snapshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@ function serializeNode(
inlineStylesheet: boolean;
maskInputOptions: MaskInputOptions;
recordCanvas: boolean;
isStrictPrivacy: boolean;
},
): serializedNode | false {
const {
Expand All @@ -214,7 +215,9 @@ function serializeNode(
inlineStylesheet,
maskInputOptions = {},
recordCanvas,
isStrictPrivacy,
} = options;

switch (n.nodeType) {
case n.DOCUMENT_NODE:
return {
Expand All @@ -229,7 +232,7 @@ function serializeNode(
systemId: (n as DocumentType).systemId,
};
case n.ELEMENT_NODE:
const needBlock = _isBlockedElement(
let needBlock = _isBlockedElement(
n as HTMLElement,
blockClass,
blockSelector,
Expand Down Expand Up @@ -318,13 +321,14 @@ function serializeNode(
if ((n as HTMLElement).scrollTop) {
attributes.rr_scrollTop = (n as HTMLElement).scrollTop;
}
if (needBlock) {
if (needBlock || (tagName === 'img' && isStrictPrivacy)) {
const { width, height } = (n as HTMLElement).getBoundingClientRect();
attributes = {
class: attributes.class,
rr_width: `${width}px`,
rr_height: `${height}px`,
};
needBlock = true;
}
return {
type: NodeType.Element,
Expand All @@ -341,11 +345,35 @@ function serializeNode(
n.parentNode && (n.parentNode as HTMLElement).tagName;
let textContent = (n as Text).textContent;
const isStyle = parentTagName === 'STYLE' ? true : undefined;
/** Determines if this node has been handled already. */
let textContentHandled = false;
if (isStyle && textContent) {
textContent = absoluteToStylesheet(textContent, getHref());
textContentHandled = true;
}
if (parentTagName === 'SCRIPT') {
textContent = 'SCRIPT_PLACEHOLDER';
textContentHandled = true;
}

// Randomizes the text content to a string of the same length.
if (isStrictPrivacy && !textContentHandled && parentTagName) {
const IGNORE_TAG_NAMES = new Set([
'HEAD',
'TITLE',
'STYLE',
'SCRIPT',
'HTML',
'BODY',
'NOSCRIPT',
]);
if (!IGNORE_TAG_NAMES.has(parentTagName)) {
textContent =
textContent
?.split(' ')
.map((word) => Math.random().toString(20).substr(2, word.length))
.join(' ') || '';
}
}
return {
type: NodeType.Text,
Expand Down Expand Up @@ -472,6 +500,7 @@ export function serializeNodeWithId(
slimDOMOptions: SlimDOMOptions;
recordCanvas?: boolean;
preserveWhiteSpace?: boolean;
isStrictPrivacy: boolean;
},
): serializedNodeWithId | null {
const {
Expand All @@ -484,6 +513,7 @@ export function serializeNodeWithId(
maskInputOptions = {},
slimDOMOptions,
recordCanvas = false,
isStrictPrivacy,
} = options;
let { preserveWhiteSpace = true } = options;
const _serializedNode = serializeNode(n, {
Expand All @@ -493,6 +523,7 @@ export function serializeNodeWithId(
inlineStylesheet,
maskInputOptions,
recordCanvas,
isStrictPrivacy,
});
if (!_serializedNode) {
// TODO: dev only
Expand Down Expand Up @@ -524,6 +555,16 @@ export function serializeNodeWithId(
let recordChild = !skipChild;
if (serializedNode.type === NodeType.Element) {
recordChild = recordChild && !serializedNode.needBlock;

/** Highlight Code Begin */
// Remove the image's src if isStrictPrivacy.
if (serializedNode.needBlock && serializedNode.tagName === 'img') {
const clone = n.cloneNode();
((clone as unknown) as HTMLImageElement).src = '';
map[id] = clone as INode;
}
/** Highlight Code End */

// this property was not needed in replay side
delete serializedNode.needBlock;
}
Expand Down Expand Up @@ -552,6 +593,7 @@ export function serializeNodeWithId(
slimDOMOptions,
recordCanvas,
preserveWhiteSpace,
isStrictPrivacy,
});
if (serializedChildNode) {
serializedNode.childNodes.push(serializedChildNode);
Expand All @@ -570,6 +612,7 @@ function snapshot(
slimDOM?: boolean | SlimDOMOptions;
recordCanvas?: boolean;
blockSelector?: string | null;
isStrictPrivacy: boolean;
},
): [serializedNodeWithId | null, idNodeMap] {
const {
Expand All @@ -579,6 +622,7 @@ function snapshot(
blockSelector = null,
maskAllInputs = false,
slimDOM = false,
isStrictPrivacy = false,
} = options || {};
const idNodeMap: idNodeMap = {};
const maskInputOptions: MaskInputOptions =
Expand Down Expand Up @@ -632,6 +676,7 @@ function snapshot(
maskInputOptions,
slimDOMOptions,
recordCanvas,
isStrictPrivacy,
}),
idNodeMap,
];
Expand Down
6 changes: 6 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,11 @@ export type recordOptions<T> = {
mousemoveWait?: number;
recordLog?: boolean | LogRecordOptions;
debug?: boolean;
/**
* Enabling this will disable recording of text data on the page. This is useful if you do not want to record personally identifiable information.
* Text will be randomized. Instead of seeing "Hello World" in a recording, you will see "1fds1 j59a0".
*/
isStrictPrivacy?: boolean;
};

export type observerParam = {
Expand All @@ -232,6 +237,7 @@ export type observerParam = {
recordCanvas: boolean;
collectFonts: boolean;
slimDOMOptions: SlimDOMOptions;
isStrictPrivacy: boolean;
};

export type hooksParam = {
Expand Down
3 changes: 2 additions & 1 deletion typings/record/mutation.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@ export default class MutationBuffer {
private inlineStylesheet;
private maskInputOptions;
private recordCanvas;
private isStrictPrivacy;
private slimDOMOptions;
init(cb: mutationCallBack, blockClass: blockClass, blockSelector: string | null, inlineStylesheet: boolean, maskInputOptions: MaskInputOptions, recordCanvas: boolean, slimDOMOptions: SlimDOMOptions): void;
init(cb: mutationCallBack, blockClass: blockClass, blockSelector: string | null, inlineStylesheet: boolean, maskInputOptions: MaskInputOptions, recordCanvas: boolean, slimDOMOptions: SlimDOMOptions, isStrictPrivacy: boolean): void;
freeze(): void;
unfreeze(): void;
isFrozen(): boolean;
Expand Down
2 changes: 1 addition & 1 deletion typings/replay/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,13 @@ export declare class Replayer {
private mouseTail;
private tailPositions;
private emitter;
private activityIntervals;
private inactiveEndTimestamp;
private legacy_missingNodeRetryMap;
private treeIndex;
private fragmentParentMap;
private elementStateMap;
private imageMap;
private activityIntervals;
constructor(events: Array<eventWithTime | string>, config?: Partial<playerConfig>);
on(event: string, handler: Handler): this;
setConfig(config: Partial<playerConfig>): void;
Expand Down
2 changes: 0 additions & 2 deletions typings/snapshot/rebuild.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,10 @@ export declare function buildNodeWithSN(n: serializedNodeWithId, options: {
map: idNodeMap;
skipChild?: boolean;
hackCss: boolean;
afterAppend?: (n: INode) => unknown;
}): INode | null;
declare function rebuild(n: serializedNodeWithId, options: {
doc: Document;
onVisit?: (node: INode) => unknown;
hackCss?: boolean;
afterAppend?: (n: INode) => unknown;
}): [Node | null, idNodeMap];
export default rebuild;
11 changes: 2 additions & 9 deletions typings/snapshot/snapshot.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,7 @@ export declare function serializeNodeWithId(n: Node | INode, options: {
slimDOMOptions: SlimDOMOptions;
recordCanvas?: boolean;
preserveWhiteSpace?: boolean;
onSerialize?: (n: INode) => unknown;
onIframeLoad?: (iframeINode: INode, node: serializedNodeWithId) => unknown;
iframeLoadTimeout?: number;
debug?: boolean;
isStrictPrivacy: boolean;
}): serializedNodeWithId | null;
declare function snapshot(n: Document, options?: {
blockClass?: string | RegExp;
Expand All @@ -27,11 +24,7 @@ declare function snapshot(n: Document, options?: {
slimDOM?: boolean | SlimDOMOptions;
recordCanvas?: boolean;
blockSelector?: string | null;
preserveWhiteSpace?: boolean;
onSerialize?: (n: INode) => unknown;
onIframeLoad?: (iframeINode: INode, node: serializedNodeWithId) => unknown;
iframeLoadTimeout?: number;
debug?: boolean;
isStrictPrivacy: boolean;
}): [serializedNodeWithId | null, idNodeMap];
export declare function visitSnapshot(node: serializedNodeWithId, onVisit: (node: serializedNodeWithId) => unknown): void;
export declare function cleanupSnapshot(): void;
Expand Down
4 changes: 4 additions & 0 deletions typings/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ export declare type recordOptions<T> = {
mousemoveWait?: number;
recordLog?: boolean | LogRecordOptions;
debug?: boolean;
isStrictPrivacy?: boolean;
};
export declare type observerParam = {
mutationCb: mutationCallBack;
Expand All @@ -164,6 +165,7 @@ export declare type observerParam = {
recordCanvas: boolean;
collectFonts: boolean;
slimDOMOptions: SlimDOMOptions;
isStrictPrivacy: boolean;
};
export declare type hooksParam = {
mutation?: mutationCallBack;
Expand Down Expand Up @@ -367,6 +369,8 @@ export declare type playerConfig = {
unpackFn?: UnpackFn;
logConfig: LogReplayConfig;
inactiveThreshold: number;
inactiveSkipTime: number;
maxSkipSpeed: number;
};
export declare type LogReplayConfig = {
level?: Array<LogLevel> | undefined;
Expand Down