diff --git a/rollup.config.js b/rollup.config.js index 3381a518..25bd0dff 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -33,6 +33,13 @@ function toAllPath(path) { return path.replace('rrweb', 'rrweb-all'); } +function toPluginPath(pluginName, stage) { + return (path) => + path + .replace(/^([\w]+)\//, '$1/plugins/') + .replace('rrweb', `${pluginName}-${stage}`); +} + function toMinPath(path) { return path.replace(/\.js$/, '.min.js'); } @@ -74,6 +81,16 @@ const baseConfigs = [ name: 'rrweb', pathFn: toAllPath, }, + { + input: './src/plugins/console/record/index.ts', + name: 'rrwebConsoleRecord', + pathFn: toPluginPath('console', 'record'), + }, + { + input: './src/plugins/console/replay/index.ts', + name: 'rrwebConsoleReplay', + pathFn: toPluginPath('console', 'replay'), + }, ]; let configs = []; diff --git a/src/entries/all.ts b/src/entries/all.ts index e5080d39..d67ff924 100644 --- a/src/entries/all.ts +++ b/src/entries/all.ts @@ -1,2 +1,4 @@ export * from '../index'; export * from '../packer'; +export * from '../plugins/console/record'; +export * from '../plugins/console/replay'; diff --git a/src/record/error-stack-parser.ts b/src/plugins/console/record/error-stack-parser.ts similarity index 99% rename from src/record/error-stack-parser.ts rename to src/plugins/console/record/error-stack-parser.ts index 311f07a7..da323a4f 100644 --- a/src/record/error-stack-parser.ts +++ b/src/plugins/console/record/error-stack-parser.ts @@ -1,3 +1,4 @@ +// tslint:disable /** * Class StackFrame is a fork of https://github.com/stacktracejs/stackframe/blob/master/stackframe.js * I fork it because: diff --git a/src/plugins/console/record/index.ts b/src/plugins/console/record/index.ts new file mode 100644 index 00000000..250ffd2b --- /dev/null +++ b/src/plugins/console/record/index.ts @@ -0,0 +1,203 @@ +import { listenerHandler, RecordPlugin } from '../../../types'; +import { stringify } from './stringify'; +import { StackFrame, ErrorStackParser } from './error-stack-parser'; +import { patch } from '../../../utils'; + +export type StringifyOptions = { + // limit of string length + stringLengthLimit?: number; + /** + * limit of number of keys in an object + * if an object contains more keys than this limit, we would call its toString function directly + */ + numOfKeysLimit: number; +}; + +type LogRecordOptions = { + level?: LogLevel[] | undefined; + lengthThreshold?: number; + stringifyOptions?: StringifyOptions; + logger?: Logger; +}; + +const defaultLogOptions: LogRecordOptions = { + level: [ + 'assert', + 'clear', + 'count', + 'countReset', + 'debug', + 'dir', + 'dirxml', + 'error', + 'group', + 'groupCollapsed', + 'groupEnd', + 'info', + 'log', + 'table', + 'time', + 'timeEnd', + 'timeLog', + 'trace', + 'warn', + ], + lengthThreshold: 1000, + logger: console, +}; + +export type LogData = { + level: LogLevel; + trace: string[]; + payload: string[]; +}; + +type logCallback = (p: LogData) => void; + +export type LogLevel = + | 'assert' + | 'clear' + | 'count' + | 'countReset' + | 'debug' + | 'dir' + | 'dirxml' + | 'error' + | 'group' + | 'groupCollapsed' + | 'groupEnd' + | 'info' + | 'log' + | 'table' + | 'time' + | 'timeEnd' + | 'timeLog' + | 'trace' + | 'warn'; + +/* fork from interface Console */ +// all kinds of console functions +export type Logger = { + assert?: typeof console.assert; + clear?: typeof console.clear; + count?: typeof console.count; + countReset?: typeof console.countReset; + debug?: typeof console.debug; + dir?: typeof console.dir; + dirxml?: typeof console.dirxml; + error?: typeof console.error; + group?: typeof console.group; + groupCollapsed?: typeof console.groupCollapsed; + groupEnd?: () => void; + info?: typeof console.info; + log?: typeof console.log; + table?: typeof console.table; + time?: typeof console.time; + timeEnd?: typeof console.timeEnd; + timeLog?: typeof console.timeLog; + trace?: typeof console.trace; + warn?: typeof console.warn; +}; + +function initLogObserver( + cb: logCallback, + logOptions: LogRecordOptions, +): listenerHandler { + const logger = logOptions.logger; + if (!logger) { + return () => {}; + } + let logCount = 0; + const cancelHandlers: listenerHandler[] = []; + // add listener to thrown errors + if (logOptions.level!.includes('error')) { + if (window) { + const originalOnError = window.onerror; + window.onerror = ( + msg: Event | string, + file: string, + line: number, + col: number, + error: Error, + ) => { + if (originalOnError) { + originalOnError.apply(this, [msg, file, line, col, error]); + } + const trace: string[] = ErrorStackParser.parse( + error, + ).map((stackFrame: StackFrame) => stackFrame.toString()); + const payload = [stringify(msg, logOptions.stringifyOptions)]; + cb({ + level: 'error', + trace, + payload, + }); + }; + cancelHandlers.push(() => { + window.onerror = originalOnError; + }); + } + } + for (const levelType of logOptions.level!) { + cancelHandlers.push(replace(logger, levelType)); + } + return () => { + cancelHandlers.forEach((h) => h()); + }; + + /** + * replace the original console function and record logs + * @param logger the logger object such as Console + * @param level the name of log function to be replaced + */ + function replace(_logger: Logger, level: LogLevel) { + if (!_logger[level]) { + return () => {}; + } + // replace the logger.{level}. return a restore function + return patch(_logger, level, (original) => { + return (...args: Array) => { + original.apply(this, args); + try { + const trace = ErrorStackParser.parse(new Error()) + .map((stackFrame: StackFrame) => stackFrame.toString()) + .splice(1); // splice(1) to omit the hijacked log function + const payload = args.map((s) => + stringify(s, logOptions.stringifyOptions), + ); + logCount++; + if (logCount < logOptions.lengthThreshold!) { + cb({ + level, + trace, + payload, + }); + } else if (logCount === logOptions.lengthThreshold) { + // notify the user + cb({ + level: 'warn', + trace: [], + payload: [ + stringify('The number of log records reached the threshold.'), + ], + }); + } + } catch (error) { + original('rrweb logger error:', error, ...args); + } + }; + }); + } +} + +export const PLUGIN_NAME = 'rrweb/console@1'; + +export const getRecordConsolePlugin: ( + options?: LogRecordOptions, +) => RecordPlugin = (options) => ({ + name: PLUGIN_NAME, + observer: initLogObserver, + options: options + ? Object.assign({}, defaultLogOptions, options) + : defaultLogOptions, +}); diff --git a/src/record/stringify.ts b/src/plugins/console/record/stringify.ts similarity index 98% rename from src/record/stringify.ts rename to src/plugins/console/record/stringify.ts index b1c69b81..e67c35a9 100644 --- a/src/record/stringify.ts +++ b/src/plugins/console/record/stringify.ts @@ -4,7 +4,7 @@ * */ -import { StringifyOptions } from '../types'; +import { StringifyOptions } from './index'; /** * transfer the node path in Event to string diff --git a/src/plugins/console/replay/index.ts b/src/plugins/console/replay/index.ts new file mode 100644 index 00000000..d54c5e11 --- /dev/null +++ b/src/plugins/console/replay/index.ts @@ -0,0 +1,144 @@ +import { LogLevel, LogData, PLUGIN_NAME } from '../record'; +import { + eventWithTime, + EventType, + IncrementalSource, + ReplayPlugin, +} from '../../../types'; + +/** + * define an interface to replay log records + * (data: logData) => void> function to display the log data + */ +type ReplayLogger = Partial void>>; + +type LogReplayConfig = { + level?: LogLevel[] | undefined; + replayLogger: ReplayLogger | undefined; +}; + +const ORIGINAL_ATTRIBUTE_NAME = '__rrweb_original__'; +type PatchedConsoleLog = { + [ORIGINAL_ATTRIBUTE_NAME]: typeof console.log; +}; + +const defaultLogConfig: LogReplayConfig = { + level: [ + 'assert', + 'clear', + 'count', + 'countReset', + 'debug', + 'dir', + 'dirxml', + 'error', + 'group', + 'groupCollapsed', + 'groupEnd', + 'info', + 'log', + 'table', + 'time', + 'timeEnd', + 'timeLog', + 'trace', + 'warn', + ], + replayLogger: undefined, +}; + +class LogReplayPlugin { + private config: LogReplayConfig; + + constructor(config?: LogReplayConfig) { + this.config = Object.assign(defaultLogConfig, config); + } + + /** + * generate a console log replayer which implement the interface ReplayLogger + */ + public getConsoleLogger(): ReplayLogger { + const replayLogger: ReplayLogger = {}; + for (const level of this.config.level!) { + if (level === 'trace') { + replayLogger[level] = (data: LogData) => { + const logger = ((console.log as unknown) as PatchedConsoleLog)[ + ORIGINAL_ATTRIBUTE_NAME + ] + ? ((console.log as unknown) as PatchedConsoleLog)[ + ORIGINAL_ATTRIBUTE_NAME + ] + : console.log; + logger( + ...data.payload.map((s) => JSON.parse(s)), + this.formatMessage(data), + ); + }; + } else { + replayLogger[level] = (data: LogData) => { + const logger = ((console[level] as unknown) as PatchedConsoleLog)[ + ORIGINAL_ATTRIBUTE_NAME + ] + ? ((console[level] as unknown) as PatchedConsoleLog)[ + ORIGINAL_ATTRIBUTE_NAME + ] + : console[level]; + logger( + ...data.payload.map((s) => JSON.parse(s)), + this.formatMessage(data), + ); + }; + } + } + return replayLogger; + } + + /** + * format the trace data to a string + * @param data the log data + */ + private formatMessage(data: LogData): string { + if (data.trace.length === 0) { + return ''; + } + const stackPrefix = '\n\tat '; + let result = stackPrefix; + result += data.trace.join(stackPrefix); + return result; + } +} + +export const getLogReplayPlugin: (options?: LogReplayConfig) => ReplayPlugin = ( + options, +) => { + const replayLogger = + options?.replayLogger || new LogReplayPlugin(options).getConsoleLogger(); + + return { + handler(event: eventWithTime, _isSync, context) { + let logData: LogData | null = null; + if ( + event.type === EventType.IncrementalSnapshot && + event.data.source === (IncrementalSource.Log as IncrementalSource) + ) { + logData = (event.data as unknown) as LogData; + } else if ( + event.type === EventType.Plugin && + event.data.plugin === PLUGIN_NAME + ) { + logData = event.data.payload as LogData; + } + if (logData) { + try { + if (typeof replayLogger[logData.level] === 'function') { + replayLogger[logData.level]!(logData); + } + } catch (error) { + if (context.replayer.config.showWarning) { + console.warn(error); + } + } + } + }, + }; +}; diff --git a/src/record/index.ts b/src/record/index.ts index 68f2625c..69b350f2 100644 --- a/src/record/index.ts +++ b/src/record/index.ts @@ -16,7 +16,6 @@ import { recordOptions, IncrementalSource, listenerHandler, - LogRecordOptions, mutationCallbackParam, scrollCallback, } from '../types'; @@ -59,7 +58,7 @@ function record( mousemoveWait, recordCanvas = false, collectFonts = false, - recordLog = false, + plugins, } = options; // runtime checks for user options if (!emit) { @@ -113,37 +112,6 @@ function record( : _slimDOMOptions ? _slimDOMOptions : {}; - const defaultLogOptions: LogRecordOptions = { - level: [ - 'assert', - 'clear', - 'count', - 'countReset', - 'debug', - 'dir', - 'dirxml', - 'error', - 'group', - 'groupCollapsed', - 'groupEnd', - 'info', - 'log', - 'table', - 'time', - 'timeEnd', - 'timeLog', - 'trace', - 'warn', - ], - lengthThreshold: 1000, - logger: console, - }; - - const logOptions: LogRecordOptions = recordLog - ? recordLog === true - ? defaultLogOptions - : Object.assign({}, defaultLogOptions, recordLog) - : {}; polyfill(); @@ -400,16 +368,6 @@ function record( }, }), ), - logCb: (p) => - wrappedEmit( - wrapEvent({ - type: EventType.IncrementalSnapshot, - data: { - source: IncrementalSource.Log, - ...p, - }, - }), - ), blockClass, ignoreClass, maskTextClass, @@ -422,12 +380,26 @@ function record( doc, maskInputFn, maskTextFn, - logOptions, blockSelector, slimDOMOptions, mirror, 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, + }, + }), + ), + })) || [], }, hooks, ); diff --git a/src/record/observer.ts b/src/record/observer.ts index c3563973..6665162a 100644 --- a/src/record/observer.ts +++ b/src/record/observer.ts @@ -37,17 +37,11 @@ import { fontParam, MaskInputFn, MaskTextFn, - logCallback, - LogRecordOptions, - Logger, - LogLevel, Mirror, } from '../types'; import MutationBuffer from './mutation'; -import { stringify } from './stringify'; import { IframeManager } from './iframe-manager'; import { ShadowDomManager } from './shadow-dom-manager'; -import { StackFrame, ErrorStackParser } from './error-stack-parser'; type WindowWithStoredMutationObserver = Window & { __rrMutationObserver?: MutationObserver; @@ -535,11 +529,16 @@ function initCanvasMutationObserver( recordArgs[0] && recordArgs[0] instanceof HTMLCanvasElement ) { - const canvas = recordArgs[0] - const ctx = canvas.getContext('2d') - let imgd = ctx?.getImageData(0, 0, canvas.width, canvas.height) + const canvas = recordArgs[0]; + const ctx = canvas.getContext('2d'); + let imgd = ctx?.getImageData( + 0, + 0, + canvas.width, + canvas.height, + ); let pix = imgd?.data; - recordArgs[0] = JSON.stringify(pix) + recordArgs[0] = JSON.stringify(pix); } } cb({ @@ -627,97 +626,6 @@ function initFontObserver(cb: fontCallback): listenerHandler { }; } -function initLogObserver( - cb: logCallback, - logOptions: LogRecordOptions, -): listenerHandler { - const logger = logOptions.logger; - if (!logger) { - return () => {}; - } - let logCount = 0; - const cancelHandlers: listenerHandler[] = []; - // add listener to thrown errors - if (logOptions.level!.includes('error')) { - if (window) { - const originalOnError = window.onerror; - window.onerror = ( - msg: Event | string, - file: string, - line: number, - col: number, - error: Error, - ) => { - if (originalOnError) { - originalOnError.apply(this, [msg, file, line, col, error]); - } - const trace: string[] = ErrorStackParser.parse( - error, - ).map((stackFrame: StackFrame) => stackFrame.toString()); - const payload = [stringify(msg, logOptions.stringifyOptions)]; - cb({ - level: 'error', - trace, - payload, - }); - }; - cancelHandlers.push(() => { - window.onerror = originalOnError; - }); - } - } - for (const levelType of logOptions.level!) { - cancelHandlers.push(replace(logger, levelType)); - } - return () => { - cancelHandlers.forEach((h) => h()); - }; - - /** - * replace the original console function and record logs - * @param logger the logger object such as Console - * @param level the name of log function to be replaced - */ - function replace(_logger: Logger, level: LogLevel) { - if (!_logger[level]) { - return () => {}; - } - // replace the logger.{level}. return a restore function - return patch(_logger, level, (original) => { - return (...args: unknown[]) => { - original.apply(this, args); - try { - const trace = ErrorStackParser.parse(new Error()) - .map((stackFrame: StackFrame) => stackFrame.toString()) - .splice(1); // splice(1) to omit the hijacked log function - const payload = args.map((s) => - stringify(s, logOptions.stringifyOptions), - ); - logCount++; - if (logCount < logOptions.lengthThreshold!) { - cb({ - level, - trace, - payload, - }); - } else if (logCount === logOptions.lengthThreshold) { - // notify the user - cb({ - level: 'warn', - trace: [], - payload: [ - stringify('The number of log records reached the threshold.'), - ], - }); - } - } catch (error) { - original('rrweb logger error:', error, ...args); - } - }; - }); - } -} - function mergeHooks(o: observerParam, hooks: hooksParam) { const { mutationCb, @@ -730,7 +638,6 @@ function mergeHooks(o: observerParam, hooks: hooksParam) { styleSheetRuleCb, canvasMutationCb, fontCb, - logCb, } = o; o.mutationCb = (...p: Arguments) => { if (hooks.mutation) { @@ -792,12 +699,6 @@ function mergeHooks(o: observerParam, hooks: hooksParam) { } fontCb(...p); }; - o.logCb = (...p: Arguments) => { - if (hooks.log) { - hooks.log(...p); - } - logCb(...p); - }; } export function initObservers( @@ -866,9 +767,11 @@ export function initObservers( ? initCanvasMutationObserver(o.canvasMutationCb, o.blockClass, o.mirror) : () => {}; const fontObserver = o.collectFonts ? initFontObserver(o.fontCb) : () => {}; - const logObserver = o.logOptions - ? initLogObserver(o.logCb, o.logOptions) - : () => {}; + // plugins + const pluginHandlers: listenerHandler[] = []; + for (const plugin of o.plugins) { + pluginHandlers.push(plugin.observer(plugin.callback, plugin.options)); + } return () => { mutationObserver.disconnect(); @@ -881,6 +784,6 @@ export function initObservers( styleSheetObserver(); canvasMutationObserver(); fontObserver(); - logObserver(); + pluginHandlers.forEach((h) => h()); }; } diff --git a/src/replay/index.ts b/src/replay/index.ts index 0d46d0ee..0bdd6442 100644 --- a/src/replay/index.ts +++ b/src/replay/index.ts @@ -28,9 +28,6 @@ import { canvasMutationData, Mirror, ElementState, - LogReplayConfig, - logData, - ReplayLogger, } from '../types'; import { createMirror, @@ -54,11 +51,6 @@ const SKIP_TIME_INTERVAL = 5 * 1000; const mitt = (mittProxy as any).default || mittProxy; const REPLAY_CONSOLE_PREFIX = '[replayer]'; -const ORIGINAL_ATTRIBUTE_NAME = '__rrweb_original__'; - -type PatchedConsoleLog = { - [ORIGINAL_ATTRIBUTE_NAME]: typeof console.log; -}; const defaultMouseTailConfig = { duration: 500, @@ -67,31 +59,6 @@ const defaultMouseTailConfig = { strokeStyle: 'red', } as const; -const defaultLogConfig: LogReplayConfig = { - level: [ - 'assert', - 'clear', - 'count', - 'countReset', - 'debug', - 'dir', - 'dirxml', - 'error', - 'group', - 'groupCollapsed', - 'groupEnd', - 'info', - 'log', - 'table', - 'time', - 'timeEnd', - 'timeLog', - 'trace', - 'warn', - ], - replayLogger: undefined, -}; - export class Replayer { public wrapper: HTMLDivElement; public iframe: HTMLIFrameElement; @@ -149,12 +116,8 @@ export class Replayer { UNSAFE_replayCanvas: false, pauseAnimation: true, mouseTail: defaultMouseTailConfig, - logConfig: defaultLogConfig, }; this.config = Object.assign({}, defaultConfig, config); - if (!this.config.logConfig.replayLogger) { - this.config.logConfig.replayLogger = this.getConsoleLogger(); - } this.handleResize = this.handleResize.bind(this); this.getCastFn = this.getCastFn.bind(this); @@ -522,6 +485,11 @@ export class Replayer { if (castFn) { castFn(); } + + for (const plugin of this.config.plugins || []) { + plugin.handler(event, isSync, { replayer: this }); + } + this.service.send({ type: 'CAST_EVENT', payload: { event } }); // events are kept sorted by timestamp, check if this is the last event @@ -1046,19 +1014,6 @@ export class Replayer { } break; } - case IncrementalSource.Log: { - try { - const logData = e.data as logData; - const replayLogger = this.config.logConfig.replayLogger!; - if (typeof replayLogger[logData.level] === 'function') { - replayLogger[logData.level]!(logData); - } - } catch (error) { - if (this.config.showWarning) { - console.warn(error); - } - } - } default: } } @@ -1378,59 +1333,6 @@ export class Replayer { } } - /** - * format the trace data to a string - * @param data the log data - */ - private formatMessage(data: logData): string { - if (data.trace.length === 0) { - return ''; - } - const stackPrefix = '\n\tat '; - let result = stackPrefix; - result += data.trace.join(stackPrefix); - return result; - } - - /** - * generate a console log replayer which implement the interface ReplayLogger - */ - private getConsoleLogger(): ReplayLogger { - const replayLogger: ReplayLogger = {}; - for (const level of this.config.logConfig.level!) { - if (level === 'trace') { - replayLogger[level] = (data: logData) => { - const logger = ((console.log as unknown) as PatchedConsoleLog)[ - ORIGINAL_ATTRIBUTE_NAME - ] - ? ((console.log as unknown) as PatchedConsoleLog)[ - ORIGINAL_ATTRIBUTE_NAME - ] - : console.log; - logger( - ...data.payload.map((s) => JSON.parse(s)), - this.formatMessage(data), - ); - }; - } else { - replayLogger[level] = (data: logData) => { - const logger = ((console[level] as unknown) as PatchedConsoleLog)[ - ORIGINAL_ATTRIBUTE_NAME - ] - ? ((console[level] as unknown) as PatchedConsoleLog)[ - ORIGINAL_ATTRIBUTE_NAME - ] - : console[level]; - logger( - ...data.payload.map((s) => JSON.parse(s)), - this.formatMessage(data), - ); - }; - } - } - return replayLogger; - } - private legacy_resolveMissingNode( map: missingNodeMap, parent: Node, diff --git a/src/types.ts b/src/types.ts index 88a8fe4e..0acb0a16 100644 --- a/src/types.ts +++ b/src/types.ts @@ -9,6 +9,7 @@ import { PackFn, UnpackFn } from './packer/base'; import { FontFaceDescriptors } from 'css-font-loading-module'; import { IframeManager } from './record/iframe-manager'; import { ShadowDomManager } from './record/shadow-dom-manager'; +import type { Replayer } from './replay'; export enum EventType { DomContentLoaded, @@ -17,6 +18,7 @@ export enum EventType { IncrementalSnapshot, Meta, Custom, + Plugin, } export type domContentLoadedEvent = { @@ -54,11 +56,6 @@ export type metaEvent = { }; }; -export type logEvent = { - type: EventType.IncrementalSnapshot; - data: incrementalData; -}; - export type customEvent = { type: EventType.Custom; data: { @@ -67,6 +64,14 @@ export type customEvent = { }; }; +export type pluginEvent = { + type: EventType.Plugin; + data: { + plugin: string; + payload: T; + }; +}; + export type styleSheetEvent = {}; export enum IncrementalSource { @@ -130,10 +135,6 @@ export type fontData = { source: IncrementalSource.Font; } & fontParam; -export type logData = { - source: IncrementalSource.Log; -} & LogParam; - export type incrementalData = | mutationData | mousemoveData @@ -144,8 +145,7 @@ export type incrementalData = | mediaInteractionData | styleSheetRuleData | canvasMutationData - | fontData - | logData; + | fontData; export type event = | domContentLoadedEvent @@ -153,8 +153,8 @@ export type event = | fullSnapshotEvent | incrementalSnapshotEvent | metaEvent - | logEvent - | customEvent; + | customEvent + | pluginEvent; export type eventWithTime = event & { timestamp: number; @@ -191,6 +191,12 @@ export type SamplingStrategy = Partial<{ input: 'all' | 'last'; }>; +export type RecordPlugin = { + name: string; + observer: (cb: Function, options: TOptions) => listenerHandler; + options: TOptions; +}; + export type recordOptions = { emit?: (e: T, isCheckout?: boolean) => void; checkoutEveryNth?: number; @@ -211,9 +217,9 @@ export type recordOptions = { sampling?: SamplingStrategy; recordCanvas?: boolean; collectFonts?: boolean; + plugins?: RecordPlugin[]; // departed, please use sampling options mousemoveWait?: number; - recordLog?: boolean | LogRecordOptions; }; export type observerParam = { @@ -236,8 +242,6 @@ export type observerParam = { styleSheetRuleCb: styleSheetRuleCallback; canvasMutationCb: canvasMutationCallback; fontCb: fontCallback; - logCb: logCallback; - logOptions: LogRecordOptions; sampling: SamplingStrategy; recordCanvas: boolean; collectFonts: boolean; @@ -246,6 +250,11 @@ export type observerParam = { mirror: Mirror; iframeManager: IframeManager; shadowDomManager: ShadowDomManager; + plugins: Array<{ + observer: Function; + callback: Function; + options: unknown; + }>; }; export type hooksParam = { @@ -259,7 +268,6 @@ export type hooksParam = { styleSheetRule?: styleSheetRuleCallback; canvasMutation?: canvasMutationCallback; font?: fontCallback; - log?: logCallback; }; // https://dom.spec.whatwg.org/#interface-mutationrecord @@ -396,67 +404,8 @@ export type fontParam = { descriptors?: FontFaceDescriptors; }; -export type LogLevel = - | 'assert' - | 'clear' - | 'count' - | 'countReset' - | 'debug' - | 'dir' - | 'dirxml' - | 'error' - | 'group' - | 'groupCollapsed' - | 'groupEnd' - | 'info' - | 'log' - | 'table' - | 'time' - | 'timeEnd' - | 'timeLog' - | 'trace' - | 'warn'; - -/* fork from interface Console */ -// all kinds of console functions -export type Logger = { - assert?: typeof console.assert; - clear?: typeof console.clear; - count?: typeof console.count; - countReset?: typeof console.countReset; - debug?: typeof console.debug; - dir?: typeof console.dir; - dirxml?: typeof console.dirxml; - error?: typeof console.error; - group?: typeof console.group; - groupCollapsed?: typeof console.groupCollapsed; - groupEnd?: () => void; - info?: typeof console.info; - log?: typeof console.log; - table?: typeof console.table; - time?: typeof console.time; - timeEnd?: typeof console.timeEnd; - timeLog?: typeof console.timeLog; - trace?: typeof console.trace; - warn?: typeof console.warn; -}; - -/** - * define an interface to replay log records - * (data: logData) => void> function to display the log data - */ -export type ReplayLogger = Partial void>>; - -export type LogParam = { - level: LogLevel; - trace: string[]; - payload: string[]; -}; - export type fontCallback = (p: fontParam) => void; -export type logCallback = (p: LogParam) => void; - export type viewportResizeDimension = { width: number; height: number; @@ -480,7 +429,7 @@ export const enum MediaInteractions { export type mediaInteractionParam = { type: MediaInteractions; id: number; - currentTime?: number + currentTime?: number; }; export type mediaInteractionCallback = (p: mediaInteractionParam) => void; @@ -511,6 +460,13 @@ export type throttleOptions = { export type listenerHandler = () => void; export type hookResetter = () => void; +export type ReplayPlugin = { + handler: ( + event: eventWithTime, + isSync: boolean, + context: { replayer: Replayer }, + ) => void; +}; export type playerConfig = { speed: number; maxSpeed: number; @@ -534,12 +490,7 @@ export type playerConfig = { strokeStyle?: string; }; unpackFn?: UnpackFn; - logConfig: LogReplayConfig; -}; - -export type LogReplayConfig = { - level?: LogLevel[] | undefined; - replayLogger: ReplayLogger | undefined; + plugins?: ReplayPlugin[]; }; export type playerMetaData = { @@ -601,20 +552,3 @@ export type ElementState = { // [scrollLeft,scrollTop] scroll?: [number, number]; }; - -export type StringifyOptions = { - // limit of string length - stringLengthLimit?: number; - /** - * limit of number of keys in an object - * if an object contains more keys than this limit, we would call its toString function directly - */ - numOfKeysLimit: number; -}; - -export type LogRecordOptions = { - level?: LogLevel[] | undefined; - lengthThreshold?: number; - stringifyOptions?: StringifyOptions; - logger?: Logger; -}; diff --git a/src/utils.ts b/src/utils.ts index 59ee18e9..9161afb0 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -53,7 +53,7 @@ export function createMirror(): Mirror { delete this.map[id]; if (n.childNodes) { n.childNodes.forEach((child) => - this.removeNodeFromMap((child as Node) as INode), + this.removeNodeFromMap(child as Node as INode), ); } }, @@ -275,7 +275,7 @@ export function isAncestorRemoved(target: INode, mirror: Mirror): boolean { if (!target.parentNode) { return true; } - return isAncestorRemoved((target.parentNode as unknown) as INode, mirror); + return isAncestorRemoved(target.parentNode as unknown as INode, mirror); } export function isTouchEvent( @@ -286,13 +286,13 @@ export function isTouchEvent( export function polyfill(win = window) { if ('NodeList' in win && !win.NodeList.prototype.forEach) { - win.NodeList.prototype.forEach = (Array.prototype - .forEach as unknown) as NodeList['forEach']; + win.NodeList.prototype.forEach = Array.prototype + .forEach as unknown as NodeList['forEach']; } if ('DOMTokenList' in win && !win.DOMTokenList.prototype.forEach) { - win.DOMTokenList.prototype.forEach = (Array.prototype - .forEach as unknown) as DOMTokenList['forEach']; + win.DOMTokenList.prototype.forEach = Array.prototype + .forEach as unknown as DOMTokenList['forEach']; } // https://github.com/Financial-Times/polyfill-service/pull/183 @@ -322,6 +322,7 @@ export function needCastInSyncMode(event: eventWithTime): boolean { return false; case EventType.FullSnapshot: case EventType.Meta: + case EventType.Plugin: return true; default: break; @@ -395,7 +396,7 @@ export class TreeIndex { const node = mirror.getNode(id); node?.childNodes.forEach((childNode) => { if ('__sn' in childNode) { - deepRemoveFromMirror(((childNode as unknown) as INode).__sn.id); + deepRemoveFromMirror((childNode as unknown as INode).__sn.id); } }); }; @@ -459,12 +460,8 @@ export class TreeIndex { scrollMap: TreeIndex['scrollMap']; inputMap: TreeIndex['inputMap']; } { - const { - tree, - removeNodeMutations, - textMutations, - attributeMutations, - } = this; + const { tree, removeNodeMutations, textMutations, attributeMutations } = + this; const batchMutationData: mutationData = { source: IncrementalSource.Mutation, @@ -653,5 +650,5 @@ export function getBaseDimension( export function hasShadowRoot( n: T, ): n is T & { shadowRoot: ShadowRoot } { - return Boolean(((n as unknown) as Element)?.shadowRoot); + return Boolean((n as unknown as Element)?.shadowRoot); } diff --git a/test/__snapshots__/integration.test.ts.snap b/test/__snapshots__/integration.test.ts.snap index e74967e4..b033fee9 100644 --- a/test/__snapshots__/integration.test.ts.snap +++ b/test/__snapshots__/integration.test.ts.snap @@ -2855,213 +2855,247 @@ exports[`log 1`] = ` } }, { - \\"type\\": 3, + \\"type\\": 6, \\"data\\": { - \\"source\\": 11, - \\"level\\": \\"assert\\", - \\"trace\\": [ - \\"__puppeteer_evaluation_script__:2:37\\" - ], - \\"payload\\": [ - \\"true\\", - \\"\\\\\\"assert\\\\\\"\\" - ] + \\"plugin\\": \\"rrweb/console@1\\", + \\"payload\\": { + \\"level\\": \\"assert\\", + \\"trace\\": [ + \\"__puppeteer_evaluation_script__:2:37\\" + ], + \\"payload\\": [ + \\"true\\", + \\"\\\\\\"assert\\\\\\"\\" + ] + } } }, { - \\"type\\": 3, + \\"type\\": 6, \\"data\\": { - \\"source\\": 11, - \\"level\\": \\"count\\", - \\"trace\\": [ - \\"__puppeteer_evaluation_script__:3:37\\" - ], - \\"payload\\": [ - \\"\\\\\\"count\\\\\\"\\" - ] + \\"plugin\\": \\"rrweb/console@1\\", + \\"payload\\": { + \\"level\\": \\"count\\", + \\"trace\\": [ + \\"__puppeteer_evaluation_script__:3:37\\" + ], + \\"payload\\": [ + \\"\\\\\\"count\\\\\\"\\" + ] + } } }, { - \\"type\\": 3, + \\"type\\": 6, \\"data\\": { - \\"source\\": 11, - \\"level\\": \\"countReset\\", - \\"trace\\": [ - \\"__puppeteer_evaluation_script__:4:37\\" - ], - \\"payload\\": [ - \\"\\\\\\"count\\\\\\"\\" - ] + \\"plugin\\": \\"rrweb/console@1\\", + \\"payload\\": { + \\"level\\": \\"countReset\\", + \\"trace\\": [ + \\"__puppeteer_evaluation_script__:4:37\\" + ], + \\"payload\\": [ + \\"\\\\\\"count\\\\\\"\\" + ] + } } }, { - \\"type\\": 3, + \\"type\\": 6, \\"data\\": { - \\"source\\": 11, - \\"level\\": \\"debug\\", - \\"trace\\": [ - \\"__puppeteer_evaluation_script__:5:37\\" - ], - \\"payload\\": [ - \\"\\\\\\"debug\\\\\\"\\" - ] + \\"plugin\\": \\"rrweb/console@1\\", + \\"payload\\": { + \\"level\\": \\"debug\\", + \\"trace\\": [ + \\"__puppeteer_evaluation_script__:5:37\\" + ], + \\"payload\\": [ + \\"\\\\\\"debug\\\\\\"\\" + ] + } } }, { - \\"type\\": 3, + \\"type\\": 6, \\"data\\": { - \\"source\\": 11, - \\"level\\": \\"dir\\", - \\"trace\\": [ - \\"__puppeteer_evaluation_script__:6:37\\" - ], - \\"payload\\": [ - \\"\\\\\\"dir\\\\\\"\\" - ] + \\"plugin\\": \\"rrweb/console@1\\", + \\"payload\\": { + \\"level\\": \\"dir\\", + \\"trace\\": [ + \\"__puppeteer_evaluation_script__:6:37\\" + ], + \\"payload\\": [ + \\"\\\\\\"dir\\\\\\"\\" + ] + } } }, { - \\"type\\": 3, + \\"type\\": 6, \\"data\\": { - \\"source\\": 11, - \\"level\\": \\"dirxml\\", - \\"trace\\": [ - \\"__puppeteer_evaluation_script__:7:37\\" - ], - \\"payload\\": [ - \\"\\\\\\"dirxml\\\\\\"\\" - ] + \\"plugin\\": \\"rrweb/console@1\\", + \\"payload\\": { + \\"level\\": \\"dirxml\\", + \\"trace\\": [ + \\"__puppeteer_evaluation_script__:7:37\\" + ], + \\"payload\\": [ + \\"\\\\\\"dirxml\\\\\\"\\" + ] + } } }, { - \\"type\\": 3, + \\"type\\": 6, \\"data\\": { - \\"source\\": 11, - \\"level\\": \\"group\\", - \\"trace\\": [ - \\"__puppeteer_evaluation_script__:8:37\\" - ], - \\"payload\\": [] + \\"plugin\\": \\"rrweb/console@1\\", + \\"payload\\": { + \\"level\\": \\"group\\", + \\"trace\\": [ + \\"__puppeteer_evaluation_script__:8:37\\" + ], + \\"payload\\": [] + } } }, { - \\"type\\": 3, + \\"type\\": 6, \\"data\\": { - \\"source\\": 11, - \\"level\\": \\"groupCollapsed\\", - \\"trace\\": [ - \\"__puppeteer_evaluation_script__:9:37\\" - ], - \\"payload\\": [] + \\"plugin\\": \\"rrweb/console@1\\", + \\"payload\\": { + \\"level\\": \\"groupCollapsed\\", + \\"trace\\": [ + \\"__puppeteer_evaluation_script__:9:37\\" + ], + \\"payload\\": [] + } } }, { - \\"type\\": 3, + \\"type\\": 6, \\"data\\": { - \\"source\\": 11, - \\"level\\": \\"info\\", - \\"trace\\": [ - \\"__puppeteer_evaluation_script__:10:37\\" - ], - \\"payload\\": [ - \\"\\\\\\"info\\\\\\"\\" - ] + \\"plugin\\": \\"rrweb/console@1\\", + \\"payload\\": { + \\"level\\": \\"info\\", + \\"trace\\": [ + \\"__puppeteer_evaluation_script__:10:37\\" + ], + \\"payload\\": [ + \\"\\\\\\"info\\\\\\"\\" + ] + } } }, { - \\"type\\": 3, + \\"type\\": 6, \\"data\\": { - \\"source\\": 11, - \\"level\\": \\"log\\", - \\"trace\\": [ - \\"__puppeteer_evaluation_script__:11:37\\" - ], - \\"payload\\": [ - \\"\\\\\\"log\\\\\\"\\" - ] + \\"plugin\\": \\"rrweb/console@1\\", + \\"payload\\": { + \\"level\\": \\"log\\", + \\"trace\\": [ + \\"__puppeteer_evaluation_script__:11:37\\" + ], + \\"payload\\": [ + \\"\\\\\\"log\\\\\\"\\" + ] + } } }, { - \\"type\\": 3, + \\"type\\": 6, \\"data\\": { - \\"source\\": 11, - \\"level\\": \\"table\\", - \\"trace\\": [ - \\"__puppeteer_evaluation_script__:12:37\\" - ], - \\"payload\\": [ - \\"\\\\\\"table\\\\\\"\\" - ] + \\"plugin\\": \\"rrweb/console@1\\", + \\"payload\\": { + \\"level\\": \\"table\\", + \\"trace\\": [ + \\"__puppeteer_evaluation_script__:12:37\\" + ], + \\"payload\\": [ + \\"\\\\\\"table\\\\\\"\\" + ] + } } }, { - \\"type\\": 3, + \\"type\\": 6, \\"data\\": { - \\"source\\": 11, - \\"level\\": \\"time\\", - \\"trace\\": [ - \\"__puppeteer_evaluation_script__:13:37\\" - ], - \\"payload\\": [] + \\"plugin\\": \\"rrweb/console@1\\", + \\"payload\\": { + \\"level\\": \\"time\\", + \\"trace\\": [ + \\"__puppeteer_evaluation_script__:13:37\\" + ], + \\"payload\\": [] + } } }, { - \\"type\\": 3, + \\"type\\": 6, \\"data\\": { - \\"source\\": 11, - \\"level\\": \\"timeEnd\\", - \\"trace\\": [ - \\"__puppeteer_evaluation_script__:14:37\\" - ], - \\"payload\\": [] + \\"plugin\\": \\"rrweb/console@1\\", + \\"payload\\": { + \\"level\\": \\"timeEnd\\", + \\"trace\\": [ + \\"__puppeteer_evaluation_script__:14:37\\" + ], + \\"payload\\": [] + } } }, { - \\"type\\": 3, + \\"type\\": 6, \\"data\\": { - \\"source\\": 11, - \\"level\\": \\"timeLog\\", - \\"trace\\": [ - \\"__puppeteer_evaluation_script__:15:37\\" - ], - \\"payload\\": [] + \\"plugin\\": \\"rrweb/console@1\\", + \\"payload\\": { + \\"level\\": \\"timeLog\\", + \\"trace\\": [ + \\"__puppeteer_evaluation_script__:15:37\\" + ], + \\"payload\\": [] + } } }, { - \\"type\\": 3, + \\"type\\": 6, \\"data\\": { - \\"source\\": 11, - \\"level\\": \\"trace\\", - \\"trace\\": [ - \\"__puppeteer_evaluation_script__:16:37\\" - ], - \\"payload\\": [ - \\"\\\\\\"trace\\\\\\"\\" - ] + \\"plugin\\": \\"rrweb/console@1\\", + \\"payload\\": { + \\"level\\": \\"trace\\", + \\"trace\\": [ + \\"__puppeteer_evaluation_script__:16:37\\" + ], + \\"payload\\": [ + \\"\\\\\\"trace\\\\\\"\\" + ] + } } }, { - \\"type\\": 3, + \\"type\\": 6, \\"data\\": { - \\"source\\": 11, - \\"level\\": \\"warn\\", - \\"trace\\": [ - \\"__puppeteer_evaluation_script__:17:37\\" - ], - \\"payload\\": [ - \\"\\\\\\"warn\\\\\\"\\" - ] + \\"plugin\\": \\"rrweb/console@1\\", + \\"payload\\": { + \\"level\\": \\"warn\\", + \\"trace\\": [ + \\"__puppeteer_evaluation_script__:17:37\\" + ], + \\"payload\\": [ + \\"\\\\\\"warn\\\\\\"\\" + ] + } } }, { - \\"type\\": 3, + \\"type\\": 6, \\"data\\": { - \\"source\\": 11, - \\"level\\": \\"clear\\", - \\"trace\\": [ - \\"__puppeteer_evaluation_script__:18:37\\" - ], - \\"payload\\": [] + \\"plugin\\": \\"rrweb/console@1\\", + \\"payload\\": { + \\"level\\": \\"clear\\", + \\"trace\\": [ + \\"__puppeteer_evaluation_script__:18:37\\" + ], + \\"payload\\": [] + } } } ]" diff --git a/test/integration.test.ts b/test/integration.test.ts index 01bef429..29168e29 100644 --- a/test/integration.test.ts +++ b/test/integration.test.ts @@ -77,7 +77,7 @@ describe('record integration tests', function (this: ISuite) { maskInputOptions: ${JSON.stringify(options.maskAllInputs)}, maskTextFn: ${options.maskTextFn}, recordCanvas: ${options.recordCanvas}, - recordLog: ${options.recordLog}, + plugins: ${options.plugins} }); @@ -90,7 +90,12 @@ describe('record integration tests', function (this: ISuite) { this.browser = await launchPuppeteer(); const bundlePath = path.resolve(__dirname, '../dist/rrweb.min.js'); - this.code = fs.readFileSync(bundlePath, 'utf8'); + const pluginsCode = [ + path.resolve(__dirname, '../dist/plugins/console-record.min.js'), + ] + .map((path) => fs.readFileSync(path, 'utf8')) + .join(); + this.code = fs.readFileSync(bundlePath, 'utf8') + pluginsCode; }); after(async () => { @@ -385,7 +390,7 @@ describe('record integration tests', function (this: ISuite) { await page.goto('about:blank'); await page.setContent( getHtml.call(this, 'log.html', { - recordLog: true, + plugins: '[rrwebConsoleRecord.getRecordConsolePlugin()]', }), ); diff --git a/typings/entries/all.d.ts b/typings/entries/all.d.ts index e5080d39..d67ff924 100644 --- a/typings/entries/all.d.ts +++ b/typings/entries/all.d.ts @@ -1,2 +1,4 @@ export * from '../index'; export * from '../packer'; +export * from '../plugins/console/record'; +export * from '../plugins/console/replay'; diff --git a/typings/plugins/console/record/error-stack-parser.d.ts b/typings/plugins/console/record/error-stack-parser.d.ts new file mode 100644 index 00000000..86a961da --- /dev/null +++ b/typings/plugins/console/record/error-stack-parser.d.ts @@ -0,0 +1,37 @@ +export declare class StackFrame { + private fileName; + private functionName; + private lineNumber?; + private columnNumber?; + constructor(obj: { + fileName?: string; + functionName?: string; + lineNumber?: number; + columnNumber?: number; + }); + toString(): string; +} +export declare const ErrorStackParser: { + parse: (error: Error) => StackFrame[]; + extractLocation: (urlLike: string) => (string | undefined)[]; + parseV8OrIE: (error: { + stack: string; + }) => StackFrame[]; + parseFFOrSafari: (error: { + stack: string; + }) => StackFrame[]; + parseOpera: (e: { + stacktrace?: string; + message: string; + stack?: string; + }) => StackFrame[]; + parseOpera9: (e: { + message: string; + }) => StackFrame[]; + parseOpera10: (e: { + stacktrace: string; + }) => StackFrame[]; + parseOpera11: (error: { + stack: string; + }) => StackFrame[]; +}; diff --git a/typings/plugins/console/record/index.d.ts b/typings/plugins/console/record/index.d.ts new file mode 100644 index 00000000..9461dd1d --- /dev/null +++ b/typings/plugins/console/record/index.d.ts @@ -0,0 +1,41 @@ +import { RecordPlugin } from '../../../types'; +export declare type StringifyOptions = { + stringLengthLimit?: number; + numOfKeysLimit: number; +}; +declare type LogRecordOptions = { + level?: LogLevel[] | undefined; + lengthThreshold?: number; + stringifyOptions?: StringifyOptions; + logger?: Logger; +}; +export declare type LogData = { + level: LogLevel; + trace: string[]; + payload: string[]; +}; +export declare type LogLevel = 'assert' | 'clear' | 'count' | 'countReset' | 'debug' | 'dir' | 'dirxml' | 'error' | 'group' | 'groupCollapsed' | 'groupEnd' | 'info' | 'log' | 'table' | 'time' | 'timeEnd' | 'timeLog' | 'trace' | 'warn'; +export declare type Logger = { + assert?: typeof console.assert; + clear?: typeof console.clear; + count?: typeof console.count; + countReset?: typeof console.countReset; + debug?: typeof console.debug; + dir?: typeof console.dir; + dirxml?: typeof console.dirxml; + error?: typeof console.error; + group?: typeof console.group; + groupCollapsed?: typeof console.groupCollapsed; + groupEnd?: () => void; + info?: typeof console.info; + log?: typeof console.log; + table?: typeof console.table; + time?: typeof console.time; + timeEnd?: typeof console.timeEnd; + timeLog?: typeof console.timeLog; + trace?: typeof console.trace; + warn?: typeof console.warn; +}; +export declare const PLUGIN_NAME = "rrweb/console@1"; +export declare const getRecordConsolePlugin: (options?: LogRecordOptions) => RecordPlugin; +export {}; diff --git a/typings/plugins/console/record/stringify.d.ts b/typings/plugins/console/record/stringify.d.ts new file mode 100644 index 00000000..213bbf35 --- /dev/null +++ b/typings/plugins/console/record/stringify.d.ts @@ -0,0 +1,2 @@ +import { StringifyOptions } from './index'; +export declare function stringify(obj: any, stringifyOptions?: StringifyOptions): string; diff --git a/typings/plugins/console/replay/index.d.ts b/typings/plugins/console/replay/index.d.ts new file mode 100644 index 00000000..8d0fc9c0 --- /dev/null +++ b/typings/plugins/console/replay/index.d.ts @@ -0,0 +1,9 @@ +import { LogLevel, LogData } from '../record'; +import { ReplayPlugin } from '../../../types'; +declare type ReplayLogger = Partial void>>; +declare type LogReplayConfig = { + level?: LogLevel[] | undefined; + replayLogger: ReplayLogger | undefined; +}; +export declare const getLogReplayPlugin: (options?: LogReplayConfig) => ReplayPlugin; +export {}; diff --git a/typings/replay/index.d.ts b/typings/replay/index.d.ts index 0a2af1c8..fc930209 100644 --- a/typings/replay/index.d.ts +++ b/typings/replay/index.d.ts @@ -50,8 +50,6 @@ export declare class Replayer { private applyMutation; private applyScroll; private applyInput; - private formatMessage; - private getConsoleLogger; private legacy_resolveMissingNode; private moveAndHover; private drawMouseTail; diff --git a/typings/types.d.ts b/typings/types.d.ts index 735071b1..a80334e4 100644 --- a/typings/types.d.ts +++ b/typings/types.d.ts @@ -3,13 +3,15 @@ import { PackFn, UnpackFn } from './packer/base'; import { FontFaceDescriptors } from 'css-font-loading-module'; import { IframeManager } from './record/iframe-manager'; import { ShadowDomManager } from './record/shadow-dom-manager'; +import type { Replayer } from './replay'; export declare enum EventType { DomContentLoaded = 0, Load = 1, FullSnapshot = 2, IncrementalSnapshot = 3, Meta = 4, - Custom = 5 + Custom = 5, + Plugin = 6 } export declare type domContentLoadedEvent = { type: EventType.DomContentLoaded; @@ -41,10 +43,6 @@ export declare type metaEvent = { height: number; }; }; -export declare type logEvent = { - type: EventType.IncrementalSnapshot; - data: incrementalData; -}; export declare type customEvent = { type: EventType.Custom; data: { @@ -52,6 +50,13 @@ export declare type customEvent = { payload: T; }; }; +export declare type pluginEvent = { + type: EventType.Plugin; + data: { + plugin: string; + payload: T; + }; +}; export declare type styleSheetEvent = {}; export declare enum IncrementalSource { Mutation = 0, @@ -100,11 +105,8 @@ export declare type canvasMutationData = { export declare type fontData = { source: IncrementalSource.Font; } & fontParam; -export declare type logData = { - source: IncrementalSource.Log; -} & LogParam; -export declare type incrementalData = mutationData | mousemoveData | mouseInteractionData | scrollData | viewportResizeData | inputData | mediaInteractionData | styleSheetRuleData | canvasMutationData | fontData | logData; -export declare type event = domContentLoadedEvent | loadedEvent | fullSnapshotEvent | incrementalSnapshotEvent | metaEvent | logEvent | customEvent; +export declare type incrementalData = mutationData | mousemoveData | mouseInteractionData | scrollData | viewportResizeData | inputData | mediaInteractionData | styleSheetRuleData | canvasMutationData | fontData; +export declare type event = domContentLoadedEvent | loadedEvent | fullSnapshotEvent | incrementalSnapshotEvent | metaEvent | customEvent | pluginEvent; export declare type eventWithTime = event & { timestamp: number; delay?: number; @@ -118,6 +120,11 @@ export declare type SamplingStrategy = Partial<{ scroll: number; input: 'all' | 'last'; }>; +export declare type RecordPlugin = { + name: string; + observer: (cb: Function, options: TOptions) => listenerHandler; + options: TOptions; +}; export declare type recordOptions = { emit?: (e: T, isCheckout?: boolean) => void; checkoutEveryNth?: number; @@ -138,8 +145,8 @@ export declare type recordOptions = { sampling?: SamplingStrategy; recordCanvas?: boolean; collectFonts?: boolean; + plugins?: RecordPlugin[]; mousemoveWait?: number; - recordLog?: boolean | LogRecordOptions; }; export declare type observerParam = { mutationCb: mutationCallBack; @@ -161,8 +168,6 @@ export declare type observerParam = { styleSheetRuleCb: styleSheetRuleCallback; canvasMutationCb: canvasMutationCallback; fontCb: fontCallback; - logCb: logCallback; - logOptions: LogRecordOptions; sampling: SamplingStrategy; recordCanvas: boolean; collectFonts: boolean; @@ -171,6 +176,11 @@ export declare type observerParam = { mirror: Mirror; iframeManager: IframeManager; shadowDomManager: ShadowDomManager; + plugins: Array<{ + observer: Function; + callback: Function; + options: unknown; + }>; }; export declare type hooksParam = { mutation?: mutationCallBack; @@ -183,7 +193,6 @@ export declare type hooksParam = { styleSheetRule?: styleSheetRuleCallback; canvasMutation?: canvasMutationCallback; font?: fontCallback; - log?: logCallback; }; export declare type mutationRecord = { type: string; @@ -290,36 +299,7 @@ export declare type fontParam = { buffer: boolean; descriptors?: FontFaceDescriptors; }; -export declare type LogLevel = 'assert' | 'clear' | 'count' | 'countReset' | 'debug' | 'dir' | 'dirxml' | 'error' | 'group' | 'groupCollapsed' | 'groupEnd' | 'info' | 'log' | 'table' | 'time' | 'timeEnd' | 'timeLog' | 'trace' | 'warn'; -export declare type Logger = { - assert?: typeof console.assert; - clear?: typeof console.clear; - count?: typeof console.count; - countReset?: typeof console.countReset; - debug?: typeof console.debug; - dir?: typeof console.dir; - dirxml?: typeof console.dirxml; - error?: typeof console.error; - group?: typeof console.group; - groupCollapsed?: typeof console.groupCollapsed; - groupEnd?: () => void; - info?: typeof console.info; - log?: typeof console.log; - table?: typeof console.table; - time?: typeof console.time; - timeEnd?: typeof console.timeEnd; - timeLog?: typeof console.timeLog; - trace?: typeof console.trace; - warn?: typeof console.warn; -}; -export declare type ReplayLogger = Partial void>>; -export declare type LogParam = { - level: LogLevel; - trace: string[]; - payload: string[]; -}; export declare type fontCallback = (p: fontParam) => void; -export declare type logCallback = (p: LogParam) => void; export declare type viewportResizeDimension = { width: number; height: number; @@ -363,6 +343,11 @@ export declare type throttleOptions = { }; export declare type listenerHandler = () => void; export declare type hookResetter = () => void; +export declare type ReplayPlugin = { + handler: (event: eventWithTime, isSync: boolean, context: { + replayer: Replayer; + }) => void; +}; export declare type playerConfig = { speed: number; maxSpeed: number; @@ -384,11 +369,7 @@ export declare type playerConfig = { strokeStyle?: string; }; unpackFn?: UnpackFn; - logConfig: LogReplayConfig; -}; -export declare type LogReplayConfig = { - level?: LogLevel[] | undefined; - replayLogger: ReplayLogger | undefined; + plugins?: ReplayPlugin[]; }; export declare type playerMetaData = { startTime: number; @@ -436,14 +417,4 @@ export declare type MaskTextFn = (text: string) => string; export declare type ElementState = { scroll?: [number, number]; }; -export declare type StringifyOptions = { - stringLengthLimit?: number; - numOfKeysLimit: number; -}; -export declare type LogRecordOptions = { - level?: LogLevel[] | undefined; - lengthThreshold?: number; - stringifyOptions?: StringifyOptions; - logger?: Logger; -}; export {};