import { LogLevel, LogData, PLUGIN_NAME } from '../record'; import type { eventWithTime } from '@rrweb/types'; import { EventType, IncrementalSource } from '@rrweb/types'; import type { 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[]; replayLogger?: ReplayLogger; }; 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) as object), 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) as object), 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 getReplayConsolePlugin: ( 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); } } } }, }; };