feat: enable rrweb to record and replay log messages in console (#424)
* wip: working on rrweb logger * wip: can record and replay some simple log * wip: can record and replay log's stack * wip: try to serialize object * wip: record and replay console logger hijack all of the console functions. add listener to thrown errors * wip: record and replay console logger add limit to the max number of log records * feat: enable rrweb to record and replay log messages in console this is the implementation of new feature request(issue #234) here are a few points of description. 1. users need to set recordLog option in rrweb.record's parameter to record log messages. The log recorder is off by default. 2. support recording and replaying all kinds of console functions. But the reliability of them should be tested more 3. the stringify function in stringify.ts needs improvement. e.g. robustness, handler for cyclical structures and better support for more kinds of object 4. we can replay the log messages in a simulated html console like LogRocket by implementing the interface "ReplayLogger" in the future * improve: the stringify function 1. handle cyclical structures 2. add stringify option to limit the length of result 3. handle function type * refactor: simplify the type definition of ReplayLogger
This commit is contained in:
@@ -27,6 +27,9 @@ import {
|
||||
inputData,
|
||||
canvasMutationData,
|
||||
ElementState,
|
||||
LogReplayConfig,
|
||||
logData,
|
||||
ReplayLogger,
|
||||
} from '../types';
|
||||
import {
|
||||
mirror,
|
||||
@@ -54,6 +57,31 @@ 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;
|
||||
@@ -104,8 +132,11 @@ 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);
|
||||
@@ -904,6 +935,18 @@ 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:
|
||||
}
|
||||
}
|
||||
@@ -1159,6 +1202,48 @@ 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 rrwebOriginal = '__rrweb_original__';
|
||||
const replayLogger: ReplayLogger = {};
|
||||
for (const level of this.config.logConfig.level!)
|
||||
if (level === 'trace')
|
||||
replayLogger[level] = (data: logData) => {
|
||||
const logger = (console.log as any)[rrwebOriginal]
|
||||
? (console.log as any)[rrwebOriginal]
|
||||
: console.log;
|
||||
logger(
|
||||
...data.payload.map((s) => JSON.parse(s)),
|
||||
this.formatMessage(data),
|
||||
);
|
||||
};
|
||||
else
|
||||
replayLogger[level] = (data: logData) => {
|
||||
const logger = (console[level] as any)[rrwebOriginal]
|
||||
? (console[level] as any)[rrwebOriginal]
|
||||
: console[level];
|
||||
logger(
|
||||
...data.payload.map((s) => JSON.parse(s)),
|
||||
this.formatMessage(data),
|
||||
);
|
||||
};
|
||||
return replayLogger;
|
||||
}
|
||||
|
||||
private legacy_resolveMissingNode(
|
||||
map: missingNodeMap,
|
||||
parent: Node,
|
||||
|
||||
@@ -165,6 +165,7 @@ export function createPlayerService(
|
||||
};
|
||||
}),
|
||||
play(ctx) {
|
||||
console.warn('play');
|
||||
const { timer, events, baselineTime, lastPlayedEvent } = ctx;
|
||||
timer.clear();
|
||||
for (const event of events) {
|
||||
|
||||
Reference in New Issue
Block a user