From 2b969804a902a6941ea64c111f9682ad5692ce76 Mon Sep 17 00:00:00 2001 From: yz-yu Date: Wed, 1 Apr 2026 12:00:00 +0800 Subject: [PATCH] impl sequential id plugins (#819) Also introduce a new kind of plugin: event processor --- packages/rrweb/rollup.config.js | 10 +++++ .../src/plugins/sequential-id/record/index.ts | 31 +++++++++++++ .../src/plugins/sequential-id/replay/index.ts | 39 +++++++++++++++++ packages/rrweb/src/record/index.ts | 43 ++++++++++++------- packages/rrweb/src/types.ts | 3 +- .../plugins/sequential-id/record/index.d.ts | 6 +++ .../plugins/sequential-id/replay/index.d.ts | 7 +++ packages/rrweb/typings/types.d.ts | 3 +- 8 files changed, 125 insertions(+), 17 deletions(-) create mode 100644 packages/rrweb/src/plugins/sequential-id/record/index.ts create mode 100644 packages/rrweb/src/plugins/sequential-id/replay/index.ts create mode 100644 packages/rrweb/typings/plugins/sequential-id/record/index.d.ts create mode 100644 packages/rrweb/typings/plugins/sequential-id/replay/index.d.ts diff --git a/packages/rrweb/rollup.config.js b/packages/rrweb/rollup.config.js index 65cfafc4..8d6a3c66 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 00000000..a4398311 --- /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 00000000..852a02cf --- /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 e4c92365..e8d071c7 100644 --- a/packages/rrweb/src/record/index.ts +++ b/packages/rrweb/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, + }, + }), + ), + })) || [], }, hooks, ); diff --git a/packages/rrweb/src/types.ts b/packages/rrweb/src/types.ts index 74f8a52f..545eaaa9 100644 --- a/packages/rrweb/src/types.ts +++ b/packages/rrweb/src/types.ts @@ -205,7 +205,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 00000000..3311e19b --- /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 00000000..a1eee69e --- /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 9c3bd9c3..2beecbc8 100644 --- a/packages/rrweb/typings/types.d.ts +++ b/packages/rrweb/typings/types.d.ts @@ -128,7 +128,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 = {