diff --git a/packages/rrweb/rollup.config.js b/packages/rrweb/rollup.config.js index 65cfafc4a8..8d6a3c6633 100644 --- a/packages/rrweb/rollup.config.js +++ b/packages/rrweb/rollup.config.js @@ -93,6 +93,16 @@ const baseConfigs = [ name: 'rrwebConsoleReplay', pathFn: toPluginPath('console', 'replay'), }, + { + input: './src/plugins/sequential-id/record/index.ts', + name: 'rrwebSequentialIdRecord', + pathFn: toPluginPath('sequential-id', 'record'), + }, + { + input: './src/plugins/sequential-id/replay/index.ts', + name: 'rrwebSequentialIdReplay', + pathFn: toPluginPath('sequential-id', 'replay'), + }, ]; let configs = []; diff --git a/packages/rrweb/src/plugins/sequential-id/record/index.ts b/packages/rrweb/src/plugins/sequential-id/record/index.ts new file mode 100644 index 0000000000..a439831191 --- /dev/null +++ b/packages/rrweb/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/packages/rrweb/src/plugins/sequential-id/replay/index.ts b/packages/rrweb/src/plugins/sequential-id/replay/index.ts new file mode 100644 index 0000000000..852a02cf36 --- /dev/null +++ b/packages/rrweb/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/packages/rrweb/src/record/index.ts b/packages/rrweb/src/record/index.ts index e9189c1670..d5136b802a 100644 --- a/packages/rrweb/src/record/index.ts +++ b/packages/rrweb/src/record/index.ts @@ -120,6 +120,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() && @@ -134,7 +145,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; @@ -405,20 +416,22 @@ function record( iframeManager, shadowDomManager, 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, + }, + }), + ), + })) || [], }, hooks, ); diff --git a/packages/rrweb/src/types.ts b/packages/rrweb/src/types.ts index 78ed5f8c44..3afe9b561d 100644 --- a/packages/rrweb/src/types.ts +++ b/packages/rrweb/src/types.ts @@ -204,7 +204,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/packages/rrweb/typings/plugins/sequential-id/record/index.d.ts b/packages/rrweb/typings/plugins/sequential-id/record/index.d.ts new file mode 100644 index 0000000000..3311e19b3c --- /dev/null +++ b/packages/rrweb/typings/plugins/sequential-id/record/index.d.ts @@ -0,0 +1,6 @@ +import { RecordPlugin } from '../../../types'; +export declare type SequentialIdOptions = { + key: string; +}; +export declare const PLUGIN_NAME = "rrweb/sequential-id@1"; +export declare const getRecordSequentialIdPlugin: (options?: Partial) => RecordPlugin; diff --git a/packages/rrweb/typings/plugins/sequential-id/replay/index.d.ts b/packages/rrweb/typings/plugins/sequential-id/replay/index.d.ts new file mode 100644 index 0000000000..a1eee69e1b --- /dev/null +++ b/packages/rrweb/typings/plugins/sequential-id/replay/index.d.ts @@ -0,0 +1,7 @@ +import type { SequentialIdOptions } from '../record'; +import { ReplayPlugin } from '../../../types'; +declare type Options = SequentialIdOptions & { + warnOnMissingId: boolean; +}; +export declare const getReplaySequentialIdPlugin: (options?: Partial) => ReplayPlugin; +export {}; diff --git a/packages/rrweb/typings/types.d.ts b/packages/rrweb/typings/types.d.ts index 6cf36aea4a..3cf2953e0a 100644 --- a/packages/rrweb/typings/types.d.ts +++ b/packages/rrweb/typings/types.d.ts @@ -1,3 +1,4 @@ +/// import { serializedNodeWithId, idNodeMap, INode, MaskInputOptions, SlimDOMOptions, MaskInputFn, MaskTextFn } from 'rrweb-snapshot'; import { PackFn, UnpackFn } from './packer/base'; import { IframeManager } from './record/iframe-manager'; @@ -126,7 +127,8 @@ export declare type SamplingStrategy = Partial<{ }>; export declare 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; }; export declare type recordOptions = {