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:
@@ -36,8 +36,13 @@ import {
|
||||
fontCallback,
|
||||
fontParam,
|
||||
MaskInputFn,
|
||||
logCallback,
|
||||
LogRecordOptions,
|
||||
Logger,
|
||||
LogLevel,
|
||||
} from '../types';
|
||||
import MutationBuffer from './mutation';
|
||||
import { stringify } from './stringify';
|
||||
|
||||
export const mutationBuffer = new MutationBuffer();
|
||||
|
||||
@@ -499,6 +504,100 @@ function initFontObserver(cb: fontCallback): listenerHandler {
|
||||
};
|
||||
}
|
||||
|
||||
function initLogObserver(
|
||||
cb: logCallback,
|
||||
logOptions: LogRecordOptions,
|
||||
): listenerHandler {
|
||||
const logger = logOptions.logger;
|
||||
if (!logger) return () => {};
|
||||
let logCount = 0;
|
||||
const cancelHandlers: any[] = [];
|
||||
// add listener to thrown errors
|
||||
if (logOptions.level!.includes('error')) {
|
||||
if (window) {
|
||||
const originalOnError = window.onerror;
|
||||
window.onerror = (...args: any[]) => {
|
||||
originalOnError && originalOnError.apply(this, args);
|
||||
let stack: Array<string> = [];
|
||||
if (args[args.length - 1] instanceof Error)
|
||||
// 0(the second parameter) tells parseStack that every stack in Error is useful
|
||||
stack = parseStack(args[args.length - 1].stack, 0);
|
||||
const payload = [stringify(args[0], logOptions.stringifyOptions)];
|
||||
cb({
|
||||
level: 'error',
|
||||
trace: stack,
|
||||
payload: 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: any[]) => {
|
||||
original.apply(this, args);
|
||||
try {
|
||||
const stack = parseStack(new Error().stack);
|
||||
const payload = args.map((s) =>
|
||||
stringify(s, logOptions.stringifyOptions),
|
||||
);
|
||||
logCount++;
|
||||
if (logCount < logOptions.lengthThreshold!)
|
||||
cb({
|
||||
level: level,
|
||||
trace: stack,
|
||||
payload: 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);
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
/**
|
||||
* parse single stack message to an stack array.
|
||||
* @param stack the stack message to be parsed
|
||||
* @param omitDepth omit specific depth of useless stack. omit hijacked log function by default
|
||||
*/
|
||||
function parseStack(
|
||||
stack: string | undefined,
|
||||
omitDepth: number = 1,
|
||||
): Array<string> {
|
||||
let stacks: string[] = [];
|
||||
if (stack) {
|
||||
stacks = stack
|
||||
.split('at')
|
||||
.splice(1 + omitDepth)
|
||||
.map((s) => s.trim());
|
||||
}
|
||||
return stacks;
|
||||
}
|
||||
}
|
||||
|
||||
function mergeHooks(o: observerParam, hooks: hooksParam) {
|
||||
const {
|
||||
mutationCb,
|
||||
@@ -511,6 +610,7 @@ function mergeHooks(o: observerParam, hooks: hooksParam) {
|
||||
styleSheetRuleCb,
|
||||
canvasMutationCb,
|
||||
fontCb,
|
||||
logCb,
|
||||
} = o;
|
||||
o.mutationCb = (...p: Arguments<mutationCallBack>) => {
|
||||
if (hooks.mutation) {
|
||||
@@ -572,6 +672,12 @@ function mergeHooks(o: observerParam, hooks: hooksParam) {
|
||||
}
|
||||
fontCb(...p);
|
||||
};
|
||||
o.logCb = (...p: Arguments<logCallback>) => {
|
||||
if (hooks.log) {
|
||||
hooks.log(...p);
|
||||
}
|
||||
logCb(...p);
|
||||
};
|
||||
}
|
||||
|
||||
export function initObservers(
|
||||
@@ -617,6 +723,9 @@ export function initObservers(
|
||||
? initCanvasMutationObserver(o.canvasMutationCb, o.blockClass)
|
||||
: () => {};
|
||||
const fontObserver = o.collectFonts ? initFontObserver(o.fontCb) : () => {};
|
||||
const logObserver = o.logOptions
|
||||
? initLogObserver(o.logCb, o.logOptions)
|
||||
: () => {};
|
||||
|
||||
return () => {
|
||||
mutationObserver.disconnect();
|
||||
@@ -629,5 +738,6 @@ export function initObservers(
|
||||
styleSheetObserver();
|
||||
canvasMutationObserver();
|
||||
fontObserver();
|
||||
logObserver();
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user