diff --git a/package.json b/package.json index fbd6d138..e8f5e0b7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@highlight-run/rrweb", - "version": "1.1.11", + "version": "1.1.12", "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 -r ignore-styles -r jsdom-global/register test/**.test.ts", diff --git a/src/plugins/sequential-id/record/index.ts b/src/plugins/sequential-id/record/index.ts new file mode 100644 index 00000000..a4398311 --- /dev/null +++ b/src/plugins/sequential-id/record/index.ts @@ -0,0 +1,31 @@ +import { RecordPlugin } from '../../../types'; + +export type SequentialIdOptions = { + key: string; +}; + +const defaultOptions: SequentialIdOptions = { + key: '_sid', +}; + +export const PLUGIN_NAME = 'rrweb/sequential-id@1'; + +export const getRecordSequentialIdPlugin: ( + options?: Partial, +) => RecordPlugin = (options) => { + const _options = options + ? Object.assign({}, defaultOptions, options) + : defaultOptions; + let id = 0; + + return { + name: PLUGIN_NAME, + eventProcessor(event) { + Object.assign(event, { + [_options.key]: ++id, + }); + return event; + }, + options: _options, + }; +}; diff --git a/src/plugins/sequential-id/replay/index.ts b/src/plugins/sequential-id/replay/index.ts new file mode 100644 index 00000000..852a02cf --- /dev/null +++ b/src/plugins/sequential-id/replay/index.ts @@ -0,0 +1,39 @@ +import type { SequentialIdOptions } from '../record'; +import { ReplayPlugin, eventWithTime } from '../../../types'; + +type Options = SequentialIdOptions & { + warnOnMissingId: boolean; +}; + +const defaultOptions: Options = { + key: '_sid', + warnOnMissingId: true, +}; + +export const getReplaySequentialIdPlugin: ( + options?: Partial, +) => ReplayPlugin = (options) => { + const { key, warnOnMissingId } = options + ? Object.assign({}, defaultOptions, options) + : defaultOptions; + let currentId = 1; + + return { + handler(event: eventWithTime) { + if (key in event) { + const id = ((event as unknown) as Record)[key]; + if (id !== currentId) { + console.error( + `[sequential-id-plugin]: expect to get an id with value "${currentId}", but got "${id}"`, + ); + } else { + currentId++; + } + } else if (warnOnMissingId) { + console.warn( + `[sequential-id-plugin]: failed to get id in key: "${key}"`, + ); + } + }, + }; +}; diff --git a/src/record/index.ts b/src/record/index.ts index 63881230..01a9cb58 100644 --- a/src/record/index.ts +++ b/src/record/index.ts @@ -122,6 +122,17 @@ function record( let lastFullSnapshotEvent: eventWithTime; let incrementalSnapshotCount = 0; + const eventProcessor = (e: eventWithTime): T => { + for (const plugin of plugins || []) { + if (plugin.eventProcessor) { + e = plugin.eventProcessor(e); + } + } + if (packFn) { + e = (packFn(e) as unknown) as eventWithTime; + } + return (e as unknown) as T; + }; wrappedEmit = (e: eventWithTime, isCheckout?: boolean) => { if ( mutationBuffers[0]?.isFrozen() && @@ -136,7 +147,7 @@ function record( mutationBuffers.forEach((buf) => buf.unfreeze()); } - emit(((packFn ? packFn(e) : e) as unknown) as T, isCheckout); + emit(eventProcessor(e), isCheckout); if (e.type === EventType.FullSnapshot) { lastFullSnapshotEvent = e; incrementalSnapshotCount = 0; @@ -417,20 +428,22 @@ function record( shadowDomManager, canvasManager, plugins: - plugins?.map((p) => ({ - observer: p.observer, - options: p.options, - callback: (payload: object) => - wrappedEmit( - wrapEvent({ - type: EventType.Plugin, - data: { - plugin: p.name, - payload, - }, - }), - ), - })) || [], + plugins + ?.filter((p) => p.observer) + ?.map((p) => ({ + observer: p.observer!, + options: p.options, + callback: (payload: object) => + wrappedEmit( + wrapEvent({ + type: EventType.Plugin, + data: { + plugin: p.name, + payload, + }, + }), + ), + })) || [], enableStrictPrivacy, }, hooks, diff --git a/src/types.ts b/src/types.ts index 4f161d94..3cd5af61 100644 --- a/src/types.ts +++ b/src/types.ts @@ -213,7 +213,8 @@ export type SamplingStrategy = Partial<{ export type RecordPlugin = { name: string; - observer: (cb: Function, win: IWindow, options: TOptions) => listenerHandler; + observer?: (cb: Function, win: IWindow, options: TOptions) => listenerHandler; + eventProcessor?: (event: eventWithTime) => eventWithTime & TExtend; options: TOptions; }; diff --git a/src/utils.ts b/src/utils.ts index b94fe97c..ab8d3462 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -232,7 +232,11 @@ export function isBlocked(node: Node | null, blockClass: blockClass): boolean { if (node.nodeType === node.ELEMENT_NODE) { let needBlock = false; if (typeof blockClass === 'string') { - needBlock = (node as HTMLElement).classList.contains(blockClass); + if ((node as HTMLElement).closest !== undefined) { + return (node as HTMLElement).closest('.' + blockClass) !== null; + } else { + needBlock = (node as HTMLElement).classList.contains(blockClass); + } } else { (node as HTMLElement).classList.forEach((className) => { if (blockClass.test(className)) {