plugin API (#598)
* temp: plugin API * fix a bug in the replay handler and rename some type names. * update integration test * improve plugin types and handle legacy log data * use different naming in record and replay bundles * delete unreferenced types Co-authored-by: Lucky Feng <294889365@qq.com>
This commit is contained in:
@@ -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 = [];
|
||||
|
||||
@@ -1,2 +1,4 @@
|
||||
export * from '../index';
|
||||
export * from '../packer';
|
||||
export * from '../plugins/console/record';
|
||||
export * from '../plugins/console/replay';
|
||||
|
||||
@@ -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:
|
||||
203
src/plugins/console/record/index.ts
Normal file
203
src/plugins/console/record/index.ts
Normal file
@@ -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<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);
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
});
|
||||
@@ -4,7 +4,7 @@
|
||||
*
|
||||
*/
|
||||
|
||||
import { StringifyOptions } from '../types';
|
||||
import { StringifyOptions } from './index';
|
||||
|
||||
/**
|
||||
* transfer the node path in Event to string
|
||||
144
src/plugins/console/replay/index.ts
Normal file
144
src/plugins/console/replay/index.ts
Normal file
@@ -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<Record<LogLevel, (data: LogData) => 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -16,7 +16,6 @@ import {
|
||||
recordOptions,
|
||||
IncrementalSource,
|
||||
listenerHandler,
|
||||
LogRecordOptions,
|
||||
mutationCallbackParam,
|
||||
scrollCallback,
|
||||
} from '../types';
|
||||
@@ -59,7 +58,7 @@ function record<T = eventWithTime>(
|
||||
mousemoveWait,
|
||||
recordCanvas = false,
|
||||
collectFonts = false,
|
||||
recordLog = false,
|
||||
plugins,
|
||||
} = options;
|
||||
// runtime checks for user options
|
||||
if (!emit) {
|
||||
@@ -113,37 +112,6 @@ function record<T = eventWithTime>(
|
||||
: _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<T = eventWithTime>(
|
||||
},
|
||||
}),
|
||||
),
|
||||
logCb: (p) =>
|
||||
wrappedEmit(
|
||||
wrapEvent({
|
||||
type: EventType.IncrementalSnapshot,
|
||||
data: {
|
||||
source: IncrementalSource.Log,
|
||||
...p,
|
||||
},
|
||||
}),
|
||||
),
|
||||
blockClass,
|
||||
ignoreClass,
|
||||
maskTextClass,
|
||||
@@ -422,12 +380,26 @@ function record<T = eventWithTime>(
|
||||
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,
|
||||
);
|
||||
|
||||
@@ -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<mutationCallBack>) => {
|
||||
if (hooks.mutation) {
|
||||
@@ -792,12 +699,6 @@ function mergeHooks(o: observerParam, hooks: hooksParam) {
|
||||
}
|
||||
fontCb(...p);
|
||||
};
|
||||
o.logCb = (...p: Arguments<logCallback>) => {
|
||||
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());
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
134
src/types.ts
134
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<T = unknown> = {
|
||||
type: EventType.Custom;
|
||||
data: {
|
||||
@@ -67,6 +64,14 @@ export type customEvent<T = unknown> = {
|
||||
};
|
||||
};
|
||||
|
||||
export type pluginEvent<T = unknown> = {
|
||||
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<TOptions = unknown> = {
|
||||
name: string;
|
||||
observer: (cb: Function, options: TOptions) => listenerHandler;
|
||||
options: TOptions;
|
||||
};
|
||||
|
||||
export type recordOptions<T> = {
|
||||
emit?: (e: T, isCheckout?: boolean) => void;
|
||||
checkoutEveryNth?: number;
|
||||
@@ -211,9 +217,9 @@ export type recordOptions<T> = {
|
||||
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<Record<LogLevel, (data: logData) => 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;
|
||||
};
|
||||
|
||||
25
src/utils.ts
25
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<T extends Node>(
|
||||
n: T,
|
||||
): n is T & { shadowRoot: ShadowRoot } {
|
||||
return Boolean(((n as unknown) as Element)?.shadowRoot);
|
||||
return Boolean((n as unknown as Element)?.shadowRoot);
|
||||
}
|
||||
|
||||
@@ -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\\": []
|
||||
}
|
||||
}
|
||||
}
|
||||
]"
|
||||
|
||||
@@ -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}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
@@ -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()]',
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
2
typings/entries/all.d.ts
vendored
2
typings/entries/all.d.ts
vendored
@@ -1,2 +1,4 @@
|
||||
export * from '../index';
|
||||
export * from '../packer';
|
||||
export * from '../plugins/console/record';
|
||||
export * from '../plugins/console/replay';
|
||||
|
||||
37
typings/plugins/console/record/error-stack-parser.d.ts
vendored
Normal file
37
typings/plugins/console/record/error-stack-parser.d.ts
vendored
Normal file
@@ -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[];
|
||||
};
|
||||
41
typings/plugins/console/record/index.d.ts
vendored
Normal file
41
typings/plugins/console/record/index.d.ts
vendored
Normal file
@@ -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 {};
|
||||
2
typings/plugins/console/record/stringify.d.ts
vendored
Normal file
2
typings/plugins/console/record/stringify.d.ts
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
import { StringifyOptions } from './index';
|
||||
export declare function stringify(obj: any, stringifyOptions?: StringifyOptions): string;
|
||||
9
typings/plugins/console/replay/index.d.ts
vendored
Normal file
9
typings/plugins/console/replay/index.d.ts
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
import { LogLevel, LogData } from '../record';
|
||||
import { ReplayPlugin } from '../../../types';
|
||||
declare type ReplayLogger = Partial<Record<LogLevel, (data: LogData) => void>>;
|
||||
declare type LogReplayConfig = {
|
||||
level?: LogLevel[] | undefined;
|
||||
replayLogger: ReplayLogger | undefined;
|
||||
};
|
||||
export declare const getLogReplayPlugin: (options?: LogReplayConfig) => ReplayPlugin;
|
||||
export {};
|
||||
2
typings/replay/index.d.ts
vendored
2
typings/replay/index.d.ts
vendored
@@ -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;
|
||||
|
||||
87
typings/types.d.ts
vendored
87
typings/types.d.ts
vendored
@@ -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<T = unknown> = {
|
||||
type: EventType.Custom;
|
||||
data: {
|
||||
@@ -52,6 +50,13 @@ export declare type customEvent<T = unknown> = {
|
||||
payload: T;
|
||||
};
|
||||
};
|
||||
export declare type pluginEvent<T = unknown> = {
|
||||
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<TOptions = unknown> = {
|
||||
name: string;
|
||||
observer: (cb: Function, options: TOptions) => listenerHandler;
|
||||
options: TOptions;
|
||||
};
|
||||
export declare type recordOptions<T> = {
|
||||
emit?: (e: T, isCheckout?: boolean) => void;
|
||||
checkoutEveryNth?: number;
|
||||
@@ -138,8 +145,8 @@ export declare type recordOptions<T> = {
|
||||
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<Record<LogLevel, (data: logData) => 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 {};
|
||||
|
||||
Reference in New Issue
Block a user