impl #23 add custom privacy selectors

This commit is contained in:
Yanzhen Yu
2026-04-01 12:00:00 +08:00
parent 7380e599c8
commit d1b32e6e9d
7 changed files with 60 additions and 32 deletions

View File

@@ -24,8 +24,7 @@
"dist", "dist",
"lib", "lib",
"es", "es",
"index.d.ts", "typings"
"src/types.ts"
], ],
"author": "yanzhen@smartx.com", "author": "yanzhen@smartx.com",
"license": "MIT", "license": "MIT",
@@ -59,7 +58,7 @@
"dependencies": { "dependencies": {
"@types/smoothscroll-polyfill": "^0.3.0", "@types/smoothscroll-polyfill": "^0.3.0",
"mitt": "^1.1.3", "mitt": "^1.1.3",
"rrweb-snapshot": "^0.7.5", "rrweb-snapshot": "^0.7.6",
"smoothscroll-polyfill": "^0.4.3" "smoothscroll-polyfill": "^0.4.3"
} }
} }

View File

@@ -18,7 +18,13 @@ function wrapEvent(e: event): eventWithTime {
} }
function record(options: recordOptions = {}): listenerHandler | undefined { function record(options: recordOptions = {}): listenerHandler | undefined {
const { emit, checkoutEveryNms, checkoutEveryNth } = options; const {
emit,
checkoutEveryNms,
checkoutEveryNth,
blockClass = 'rr-block',
ignoreClass = 'rr-ignore',
} = options;
// runtime checks for user options // runtime checks for user options
if (!emit) { if (!emit) {
throw new Error('emit function is required'); throw new Error('emit function is required');
@@ -56,7 +62,7 @@ function record(options: recordOptions = {}): listenerHandler | undefined {
}), }),
isCheckout, isCheckout,
); );
const [node, idNodeMap] = snapshot(document); const [node, idNodeMap] = snapshot(document, blockClass);
if (!node) { if (!node) {
return console.warn('Failed to snapshot the document'); return console.warn('Failed to snapshot the document');
} }
@@ -152,6 +158,8 @@ function record(options: recordOptions = {}): listenerHandler | undefined {
}, },
}), }),
), ),
blockClass,
ignoreClass,
}), }),
); );
}; };

View File

@@ -46,7 +46,10 @@ import { deepDelete, isParentRemoved, isParentDropped } from './collection';
* which means all the id related calculation should be lazy too. * which means all the id related calculation should be lazy too.
* @param cb mutationCallBack * @param cb mutationCallBack
*/ */
function initMutationObserver(cb: mutationCallBack): MutationObserver { function initMutationObserver(
cb: mutationCallBack,
blockClass: string,
): MutationObserver {
const observer = new MutationObserver(mutations => { const observer = new MutationObserver(mutations => {
const texts: textCursor[] = []; const texts: textCursor[] = [];
const attributes: attributeCursor[] = []; const attributes: attributeCursor[] = [];
@@ -57,7 +60,7 @@ function initMutationObserver(cb: mutationCallBack): MutationObserver {
const droppedSet = new Set<Node>(); const droppedSet = new Set<Node>();
const genAdds = (n: Node) => { const genAdds = (n: Node) => {
if (isBlocked(n)) { if (isBlocked(n, blockClass)) {
return; return;
} }
addsSet.add(n); addsSet.add(n);
@@ -76,7 +79,7 @@ function initMutationObserver(cb: mutationCallBack): MutationObserver {
switch (type) { switch (type) {
case 'characterData': { case 'characterData': {
const value = target.textContent; const value = target.textContent;
if (!isBlocked(target) && value !== oldValue) { if (!isBlocked(target, blockClass) && value !== oldValue) {
texts.push({ texts.push({
value, value,
node: target, node: target,
@@ -86,7 +89,7 @@ function initMutationObserver(cb: mutationCallBack): MutationObserver {
} }
case 'attributes': { case 'attributes': {
const value = (target as HTMLElement).getAttribute(attributeName!); const value = (target as HTMLElement).getAttribute(attributeName!);
if (isBlocked(target) || value === oldValue) { if (isBlocked(target, blockClass) || value === oldValue) {
return; return;
} }
let item: attributeCursor | undefined = attributes.find( let item: attributeCursor | undefined = attributes.find(
@@ -108,7 +111,7 @@ function initMutationObserver(cb: mutationCallBack): MutationObserver {
removedNodes.forEach(n => { removedNodes.forEach(n => {
const nodeId = mirror.getId(n as INode); const nodeId = mirror.getId(n as INode);
const parentId = mirror.getId(target as INode); const parentId = mirror.getId(target as INode);
if (isBlocked(n)) { if (isBlocked(n, blockClass)) {
return; return;
} }
// removed node has not been serialized yet, just remove it from the Set // removed node has not been serialized yet, just remove it from the Set
@@ -154,7 +157,7 @@ function initMutationObserver(cb: mutationCallBack): MutationObserver {
nextId: !n.nextSibling nextId: !n.nextSibling
? n.nextSibling ? n.nextSibling
: mirror.getId(n.nextSibling as INode), : mirror.getId(n.nextSibling as INode),
node: serializeNodeWithId(n, document, mirror.map, true)!, node: serializeNodeWithId(n, document, mirror.map, blockClass, true)!,
}); });
} else { } else {
droppedSet.add(n); droppedSet.add(n);
@@ -239,11 +242,12 @@ function initMousemoveObserver(cb: mousemoveCallBack): listenerHandler {
function initMouseInteractionObserver( function initMouseInteractionObserver(
cb: mouseInteractionCallBack, cb: mouseInteractionCallBack,
blockClass: string,
): listenerHandler { ): listenerHandler {
const handlers: listenerHandler[] = []; const handlers: listenerHandler[] = [];
const getHandler = (eventKey: keyof typeof MouseInteractions) => { const getHandler = (eventKey: keyof typeof MouseInteractions) => {
return (event: MouseEvent) => { return (event: MouseEvent) => {
if (isBlocked(event.target as Node)) { if (isBlocked(event.target as Node, blockClass)) {
return; return;
} }
const id = mirror.getId(event.target as INode); const id = mirror.getId(event.target as INode);
@@ -268,9 +272,12 @@ function initMouseInteractionObserver(
}; };
} }
function initScrollObserver(cb: scrollCallback): listenerHandler { function initScrollObserver(
cb: scrollCallback,
blockClass: string,
): listenerHandler {
const updatePosition = throttle<UIEvent>(evt => { const updatePosition = throttle<UIEvent>(evt => {
if (!evt.target || isBlocked(evt.target as Node)) { if (!evt.target || isBlocked(evt.target as Node, blockClass)) {
return; return;
} }
const id = mirror.getId(evt.target as INode); const id = mirror.getId(evt.target as INode);
@@ -313,23 +320,26 @@ const HOOK_PROPERTIES: Array<[HTMLElement, string]> = [
[HTMLSelectElement.prototype, 'value'], [HTMLSelectElement.prototype, 'value'],
[HTMLTextAreaElement.prototype, 'value'], [HTMLTextAreaElement.prototype, 'value'],
]; ];
const IGNORE_CLASS = 'rr-ignore';
const lastInputValueMap: WeakMap<EventTarget, inputValue> = new WeakMap(); const lastInputValueMap: WeakMap<EventTarget, inputValue> = new WeakMap();
function initInputObserver(cb: inputCallback): listenerHandler { function initInputObserver(
cb: inputCallback,
blockClass: string,
ignoreClass: string,
): listenerHandler {
function eventHandler(event: Event) { function eventHandler(event: Event) {
const { target } = event; const { target } = event;
if ( if (
!target || !target ||
!(target as Element).tagName || !(target as Element).tagName ||
INPUT_TAGS.indexOf((target as Element).tagName) < 0 || INPUT_TAGS.indexOf((target as Element).tagName) < 0 ||
isBlocked(target as Node) isBlocked(target as Node, blockClass)
) { ) {
return; return;
} }
const type: string | undefined = (target as HTMLInputElement).type; const type: string | undefined = (target as HTMLInputElement).type;
if ( if (
type === 'password' || type === 'password' ||
(target as HTMLElement).classList.contains(IGNORE_CLASS) (target as HTMLElement).classList.contains(ignoreClass)
) { ) {
return; return;
} }
@@ -396,14 +406,19 @@ function initInputObserver(cb: inputCallback): listenerHandler {
} }
export default function initObservers(o: observerParam): listenerHandler { export default function initObservers(o: observerParam): listenerHandler {
const mutationObserver = initMutationObserver(o.mutationCb); const mutationObserver = initMutationObserver(o.mutationCb, o.blockClass);
const mousemoveHandler = initMousemoveObserver(o.mousemoveCb); const mousemoveHandler = initMousemoveObserver(o.mousemoveCb);
const mouseInteractionHandler = initMouseInteractionObserver( const mouseInteractionHandler = initMouseInteractionObserver(
o.mouseInteractionCb, o.mouseInteractionCb,
o.blockClass,
); );
const scrollHandler = initScrollObserver(o.scrollCb); const scrollHandler = initScrollObserver(o.scrollCb, o.blockClass);
const viewportResizeHandler = initViewportResizeObserver(o.viewportResizeCb); const viewportResizeHandler = initViewportResizeObserver(o.viewportResizeCb);
const inputHandler = initInputObserver(o.inputCb); const inputHandler = initInputObserver(
o.inputCb,
o.blockClass,
o.ignoreClass,
);
return () => { return () => {
mutationObserver.disconnect(); mutationObserver.disconnect();
mousemoveHandler(); mousemoveHandler();

View File

@@ -20,7 +20,7 @@ import {
ReplayerEvents, ReplayerEvents,
} from '../types'; } from '../types';
import { mirror } from '../utils'; import { mirror } from '../utils';
import injectStyleRules from './styles/inject-style'; import getInjectStyleRules from './styles/inject-style';
import './styles/style.css'; import './styles/style.css';
const SKIP_TIME_THRESHOLD = 10 * 1000; const SKIP_TIME_THRESHOLD = 10 * 1000;
@@ -70,6 +70,7 @@ export class Replayer {
skipInactive: false, skipInactive: false,
showWarning: true, showWarning: true,
showDebug: false, showDebug: false,
blockClass: 'rr-block',
}; };
this.config = Object.assign({}, defaultConfig, config); this.config = Object.assign({}, defaultConfig, config);
@@ -278,6 +279,7 @@ export class Replayer {
const styleEl = document.createElement('style'); const styleEl = document.createElement('style');
const { documentElement, head } = this.iframe.contentDocument!; const { documentElement, head } = this.iframe.contentDocument!;
documentElement!.insertBefore(styleEl, head); documentElement!.insertBefore(styleEl, head);
const injectStyleRules = getInjectStyleRules(this.config.blockClass);
for (let idx = 0; idx < injectStyleRules.length; idx++) { for (let idx = 0; idx < injectStyleRules.length; idx++) {
(styleEl.sheet! as CSSStyleSheet).insertRule(injectStyleRules[idx], idx); (styleEl.sheet! as CSSStyleSheet).insertRule(injectStyleRules[idx], idx);
} }

View File

@@ -1,5 +1,5 @@
const rules: string[] = [ const rules: (blockClass: string) => string[] = (blockClass: string) => [
'iframe, .rr-block { background: #ccc }', `iframe, .${blockClass} { background: #ccc }`,
'noscript { display: none !important; }', 'noscript { display: none !important; }',
]; ];

View File

@@ -102,6 +102,8 @@ export type recordOptions = {
emit?: (e: eventWithTime, isCheckout?: boolean) => void; emit?: (e: eventWithTime, isCheckout?: boolean) => void;
checkoutEveryNth?: number; checkoutEveryNth?: number;
checkoutEveryNms?: number; checkoutEveryNms?: number;
blockClass?: string;
ignoreClass?: string;
}; };
export type observerParam = { export type observerParam = {
@@ -111,6 +113,8 @@ export type observerParam = {
scrollCb: scrollCallback; scrollCb: scrollCallback;
viewportResizeCb: viewportResizeCallback; viewportResizeCb: viewportResizeCallback;
inputCb: inputCallback; inputCb: inputCallback;
blockClass: string;
ignoreClass: string;
}; };
export type textCursor = { export type textCursor = {
@@ -229,9 +233,10 @@ export type playerConfig = {
speed: number; speed: number;
root: Element; root: Element;
loadTimeout: number; loadTimeout: number;
skipInactive: Boolean; skipInactive: boolean;
showWarning: Boolean; showWarning: boolean;
showDebug: Boolean; showDebug: boolean;
blockClass: string;
}; };
export type playerMetaData = { export type playerMetaData = {

View File

@@ -113,18 +113,17 @@ export function getWindowWidth(): number {
); );
} }
const BLOCK_CLASS = 'rr-block'; export function isBlocked(node: Node | null, blockClass: string): boolean {
export function isBlocked(node: Node | null): boolean {
if (!node) { if (!node) {
return false; return false;
} }
if (node.nodeType === node.ELEMENT_NODE) { if (node.nodeType === node.ELEMENT_NODE) {
return ( return (
(node as HTMLElement).classList.contains(BLOCK_CLASS) || (node as HTMLElement).classList.contains(blockClass) ||
isBlocked(node.parentNode) isBlocked(node.parentNode, blockClass)
); );
} }
return isBlocked(node.parentNode); return isBlocked(node.parentNode, blockClass);
} }
export function isAncestorRemoved(target: INode): boolean { export function isAncestorRemoved(target: INode): boolean {