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:
yz-yu
2021-06-28 00:09:09 +08:00
committed by GitHub
parent 4e025c84ce
commit 5fc6c193fd
20 changed files with 753 additions and 579 deletions

View File

@@ -33,6 +33,13 @@ function toAllPath(path) {
return path.replace('rrweb', 'rrweb-all'); return path.replace('rrweb', 'rrweb-all');
} }
function toPluginPath(pluginName, stage) {
return (path) =>
path
.replace(/^([\w]+)\//, '$1/plugins/')
.replace('rrweb', `${pluginName}-${stage}`);
}
function toMinPath(path) { function toMinPath(path) {
return path.replace(/\.js$/, '.min.js'); return path.replace(/\.js$/, '.min.js');
} }
@@ -74,6 +81,16 @@ const baseConfigs = [
name: 'rrweb', name: 'rrweb',
pathFn: toAllPath, 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 = []; let configs = [];

View File

@@ -1,2 +1,4 @@
export * from '../index'; export * from '../index';
export * from '../packer'; export * from '../packer';
export * from '../plugins/console/record';
export * from '../plugins/console/replay';

View File

@@ -1,3 +1,4 @@
// tslint:disable
/** /**
* Class StackFrame is a fork of https://github.com/stacktracejs/stackframe/blob/master/stackframe.js * Class StackFrame is a fork of https://github.com/stacktracejs/stackframe/blob/master/stackframe.js
* I fork it because: * I fork it because:

View 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,
});

View File

@@ -4,7 +4,7 @@
* *
*/ */
import { StringifyOptions } from '../types'; import { StringifyOptions } from './index';
/** /**
* transfer the node path in Event to string * transfer the node path in Event to string

View 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);
}
}
}
},
};
};

View File

@@ -16,7 +16,6 @@ import {
recordOptions, recordOptions,
IncrementalSource, IncrementalSource,
listenerHandler, listenerHandler,
LogRecordOptions,
mutationCallbackParam, mutationCallbackParam,
scrollCallback, scrollCallback,
} from '../types'; } from '../types';
@@ -59,7 +58,7 @@ function record<T = eventWithTime>(
mousemoveWait, mousemoveWait,
recordCanvas = false, recordCanvas = false,
collectFonts = false, collectFonts = false,
recordLog = false, plugins,
} = options; } = options;
// runtime checks for user options // runtime checks for user options
if (!emit) { if (!emit) {
@@ -113,37 +112,6 @@ function record<T = eventWithTime>(
: _slimDOMOptions : _slimDOMOptions
? _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(); polyfill();
@@ -400,16 +368,6 @@ function record<T = eventWithTime>(
}, },
}), }),
), ),
logCb: (p) =>
wrappedEmit(
wrapEvent({
type: EventType.IncrementalSnapshot,
data: {
source: IncrementalSource.Log,
...p,
},
}),
),
blockClass, blockClass,
ignoreClass, ignoreClass,
maskTextClass, maskTextClass,
@@ -422,12 +380,26 @@ function record<T = eventWithTime>(
doc, doc,
maskInputFn, maskInputFn,
maskTextFn, maskTextFn,
logOptions,
blockSelector, blockSelector,
slimDOMOptions, slimDOMOptions,
mirror, mirror,
iframeManager, iframeManager,
shadowDomManager, 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, hooks,
); );

View File

@@ -37,17 +37,11 @@ import {
fontParam, fontParam,
MaskInputFn, MaskInputFn,
MaskTextFn, MaskTextFn,
logCallback,
LogRecordOptions,
Logger,
LogLevel,
Mirror, Mirror,
} from '../types'; } from '../types';
import MutationBuffer from './mutation'; import MutationBuffer from './mutation';
import { stringify } from './stringify';
import { IframeManager } from './iframe-manager'; import { IframeManager } from './iframe-manager';
import { ShadowDomManager } from './shadow-dom-manager'; import { ShadowDomManager } from './shadow-dom-manager';
import { StackFrame, ErrorStackParser } from './error-stack-parser';
type WindowWithStoredMutationObserver = Window & { type WindowWithStoredMutationObserver = Window & {
__rrMutationObserver?: MutationObserver; __rrMutationObserver?: MutationObserver;
@@ -535,11 +529,16 @@ function initCanvasMutationObserver(
recordArgs[0] && recordArgs[0] &&
recordArgs[0] instanceof HTMLCanvasElement recordArgs[0] instanceof HTMLCanvasElement
) { ) {
const canvas = recordArgs[0] const canvas = recordArgs[0];
const ctx = canvas.getContext('2d') const ctx = canvas.getContext('2d');
let imgd = ctx?.getImageData(0, 0, canvas.width, canvas.height) let imgd = ctx?.getImageData(
0,
0,
canvas.width,
canvas.height,
);
let pix = imgd?.data; let pix = imgd?.data;
recordArgs[0] = JSON.stringify(pix) recordArgs[0] = JSON.stringify(pix);
} }
} }
cb({ 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) { function mergeHooks(o: observerParam, hooks: hooksParam) {
const { const {
mutationCb, mutationCb,
@@ -730,7 +638,6 @@ function mergeHooks(o: observerParam, hooks: hooksParam) {
styleSheetRuleCb, styleSheetRuleCb,
canvasMutationCb, canvasMutationCb,
fontCb, fontCb,
logCb,
} = o; } = o;
o.mutationCb = (...p: Arguments<mutationCallBack>) => { o.mutationCb = (...p: Arguments<mutationCallBack>) => {
if (hooks.mutation) { if (hooks.mutation) {
@@ -792,12 +699,6 @@ function mergeHooks(o: observerParam, hooks: hooksParam) {
} }
fontCb(...p); fontCb(...p);
}; };
o.logCb = (...p: Arguments<logCallback>) => {
if (hooks.log) {
hooks.log(...p);
}
logCb(...p);
};
} }
export function initObservers( export function initObservers(
@@ -866,9 +767,11 @@ export function initObservers(
? initCanvasMutationObserver(o.canvasMutationCb, o.blockClass, o.mirror) ? initCanvasMutationObserver(o.canvasMutationCb, o.blockClass, o.mirror)
: () => {}; : () => {};
const fontObserver = o.collectFonts ? initFontObserver(o.fontCb) : () => {}; const fontObserver = o.collectFonts ? initFontObserver(o.fontCb) : () => {};
const logObserver = o.logOptions // plugins
? initLogObserver(o.logCb, o.logOptions) const pluginHandlers: listenerHandler[] = [];
: () => {}; for (const plugin of o.plugins) {
pluginHandlers.push(plugin.observer(plugin.callback, plugin.options));
}
return () => { return () => {
mutationObserver.disconnect(); mutationObserver.disconnect();
@@ -881,6 +784,6 @@ export function initObservers(
styleSheetObserver(); styleSheetObserver();
canvasMutationObserver(); canvasMutationObserver();
fontObserver(); fontObserver();
logObserver(); pluginHandlers.forEach((h) => h());
}; };
} }

View File

@@ -28,9 +28,6 @@ import {
canvasMutationData, canvasMutationData,
Mirror, Mirror,
ElementState, ElementState,
LogReplayConfig,
logData,
ReplayLogger,
} from '../types'; } from '../types';
import { import {
createMirror, createMirror,
@@ -54,11 +51,6 @@ const SKIP_TIME_INTERVAL = 5 * 1000;
const mitt = (mittProxy as any).default || mittProxy; const mitt = (mittProxy as any).default || mittProxy;
const REPLAY_CONSOLE_PREFIX = '[replayer]'; const REPLAY_CONSOLE_PREFIX = '[replayer]';
const ORIGINAL_ATTRIBUTE_NAME = '__rrweb_original__';
type PatchedConsoleLog = {
[ORIGINAL_ATTRIBUTE_NAME]: typeof console.log;
};
const defaultMouseTailConfig = { const defaultMouseTailConfig = {
duration: 500, duration: 500,
@@ -67,31 +59,6 @@ const defaultMouseTailConfig = {
strokeStyle: 'red', strokeStyle: 'red',
} as const; } 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 { export class Replayer {
public wrapper: HTMLDivElement; public wrapper: HTMLDivElement;
public iframe: HTMLIFrameElement; public iframe: HTMLIFrameElement;
@@ -149,12 +116,8 @@ export class Replayer {
UNSAFE_replayCanvas: false, UNSAFE_replayCanvas: false,
pauseAnimation: true, pauseAnimation: true,
mouseTail: defaultMouseTailConfig, mouseTail: defaultMouseTailConfig,
logConfig: defaultLogConfig,
}; };
this.config = Object.assign({}, defaultConfig, config); this.config = Object.assign({}, defaultConfig, config);
if (!this.config.logConfig.replayLogger) {
this.config.logConfig.replayLogger = this.getConsoleLogger();
}
this.handleResize = this.handleResize.bind(this); this.handleResize = this.handleResize.bind(this);
this.getCastFn = this.getCastFn.bind(this); this.getCastFn = this.getCastFn.bind(this);
@@ -522,6 +485,11 @@ export class Replayer {
if (castFn) { if (castFn) {
castFn(); castFn();
} }
for (const plugin of this.config.plugins || []) {
plugin.handler(event, isSync, { replayer: this });
}
this.service.send({ type: 'CAST_EVENT', payload: { event } }); this.service.send({ type: 'CAST_EVENT', payload: { event } });
// events are kept sorted by timestamp, check if this is the last event // events are kept sorted by timestamp, check if this is the last event
@@ -1046,19 +1014,6 @@ export class Replayer {
} }
break; 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: 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( private legacy_resolveMissingNode(
map: missingNodeMap, map: missingNodeMap,
parent: Node, parent: Node,

View File

@@ -9,6 +9,7 @@ import { PackFn, UnpackFn } from './packer/base';
import { FontFaceDescriptors } from 'css-font-loading-module'; import { FontFaceDescriptors } from 'css-font-loading-module';
import { IframeManager } from './record/iframe-manager'; import { IframeManager } from './record/iframe-manager';
import { ShadowDomManager } from './record/shadow-dom-manager'; import { ShadowDomManager } from './record/shadow-dom-manager';
import type { Replayer } from './replay';
export enum EventType { export enum EventType {
DomContentLoaded, DomContentLoaded,
@@ -17,6 +18,7 @@ export enum EventType {
IncrementalSnapshot, IncrementalSnapshot,
Meta, Meta,
Custom, Custom,
Plugin,
} }
export type domContentLoadedEvent = { export type domContentLoadedEvent = {
@@ -54,11 +56,6 @@ export type metaEvent = {
}; };
}; };
export type logEvent = {
type: EventType.IncrementalSnapshot;
data: incrementalData;
};
export type customEvent<T = unknown> = { export type customEvent<T = unknown> = {
type: EventType.Custom; type: EventType.Custom;
data: { 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 type styleSheetEvent = {};
export enum IncrementalSource { export enum IncrementalSource {
@@ -130,10 +135,6 @@ export type fontData = {
source: IncrementalSource.Font; source: IncrementalSource.Font;
} & fontParam; } & fontParam;
export type logData = {
source: IncrementalSource.Log;
} & LogParam;
export type incrementalData = export type incrementalData =
| mutationData | mutationData
| mousemoveData | mousemoveData
@@ -144,8 +145,7 @@ export type incrementalData =
| mediaInteractionData | mediaInteractionData
| styleSheetRuleData | styleSheetRuleData
| canvasMutationData | canvasMutationData
| fontData | fontData;
| logData;
export type event = export type event =
| domContentLoadedEvent | domContentLoadedEvent
@@ -153,8 +153,8 @@ export type event =
| fullSnapshotEvent | fullSnapshotEvent
| incrementalSnapshotEvent | incrementalSnapshotEvent
| metaEvent | metaEvent
| logEvent | customEvent
| customEvent; | pluginEvent;
export type eventWithTime = event & { export type eventWithTime = event & {
timestamp: number; timestamp: number;
@@ -191,6 +191,12 @@ export type SamplingStrategy = Partial<{
input: 'all' | 'last'; input: 'all' | 'last';
}>; }>;
export type RecordPlugin<TOptions = unknown> = {
name: string;
observer: (cb: Function, options: TOptions) => listenerHandler;
options: TOptions;
};
export type recordOptions<T> = { export type recordOptions<T> = {
emit?: (e: T, isCheckout?: boolean) => void; emit?: (e: T, isCheckout?: boolean) => void;
checkoutEveryNth?: number; checkoutEveryNth?: number;
@@ -211,9 +217,9 @@ export type recordOptions<T> = {
sampling?: SamplingStrategy; sampling?: SamplingStrategy;
recordCanvas?: boolean; recordCanvas?: boolean;
collectFonts?: boolean; collectFonts?: boolean;
plugins?: RecordPlugin[];
// departed, please use sampling options // departed, please use sampling options
mousemoveWait?: number; mousemoveWait?: number;
recordLog?: boolean | LogRecordOptions;
}; };
export type observerParam = { export type observerParam = {
@@ -236,8 +242,6 @@ export type observerParam = {
styleSheetRuleCb: styleSheetRuleCallback; styleSheetRuleCb: styleSheetRuleCallback;
canvasMutationCb: canvasMutationCallback; canvasMutationCb: canvasMutationCallback;
fontCb: fontCallback; fontCb: fontCallback;
logCb: logCallback;
logOptions: LogRecordOptions;
sampling: SamplingStrategy; sampling: SamplingStrategy;
recordCanvas: boolean; recordCanvas: boolean;
collectFonts: boolean; collectFonts: boolean;
@@ -246,6 +250,11 @@ export type observerParam = {
mirror: Mirror; mirror: Mirror;
iframeManager: IframeManager; iframeManager: IframeManager;
shadowDomManager: ShadowDomManager; shadowDomManager: ShadowDomManager;
plugins: Array<{
observer: Function;
callback: Function;
options: unknown;
}>;
}; };
export type hooksParam = { export type hooksParam = {
@@ -259,7 +268,6 @@ export type hooksParam = {
styleSheetRule?: styleSheetRuleCallback; styleSheetRule?: styleSheetRuleCallback;
canvasMutation?: canvasMutationCallback; canvasMutation?: canvasMutationCallback;
font?: fontCallback; font?: fontCallback;
log?: logCallback;
}; };
// https://dom.spec.whatwg.org/#interface-mutationrecord // https://dom.spec.whatwg.org/#interface-mutationrecord
@@ -396,67 +404,8 @@ export type fontParam = {
descriptors?: FontFaceDescriptors; 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 fontCallback = (p: fontParam) => void;
export type logCallback = (p: LogParam) => void;
export type viewportResizeDimension = { export type viewportResizeDimension = {
width: number; width: number;
height: number; height: number;
@@ -480,7 +429,7 @@ export const enum MediaInteractions {
export type mediaInteractionParam = { export type mediaInteractionParam = {
type: MediaInteractions; type: MediaInteractions;
id: number; id: number;
currentTime?: number currentTime?: number;
}; };
export type mediaInteractionCallback = (p: mediaInteractionParam) => void; export type mediaInteractionCallback = (p: mediaInteractionParam) => void;
@@ -511,6 +460,13 @@ export type throttleOptions = {
export type listenerHandler = () => void; export type listenerHandler = () => void;
export type hookResetter = () => void; export type hookResetter = () => void;
export type ReplayPlugin = {
handler: (
event: eventWithTime,
isSync: boolean,
context: { replayer: Replayer },
) => void;
};
export type playerConfig = { export type playerConfig = {
speed: number; speed: number;
maxSpeed: number; maxSpeed: number;
@@ -534,12 +490,7 @@ export type playerConfig = {
strokeStyle?: string; strokeStyle?: string;
}; };
unpackFn?: UnpackFn; unpackFn?: UnpackFn;
logConfig: LogReplayConfig; plugins?: ReplayPlugin[];
};
export type LogReplayConfig = {
level?: LogLevel[] | undefined;
replayLogger: ReplayLogger | undefined;
}; };
export type playerMetaData = { export type playerMetaData = {
@@ -601,20 +552,3 @@ export type ElementState = {
// [scrollLeft,scrollTop] // [scrollLeft,scrollTop]
scroll?: [number, number]; 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;
};

View File

@@ -53,7 +53,7 @@ export function createMirror(): Mirror {
delete this.map[id]; delete this.map[id];
if (n.childNodes) { if (n.childNodes) {
n.childNodes.forEach((child) => 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) { if (!target.parentNode) {
return true; return true;
} }
return isAncestorRemoved((target.parentNode as unknown) as INode, mirror); return isAncestorRemoved(target.parentNode as unknown as INode, mirror);
} }
export function isTouchEvent( export function isTouchEvent(
@@ -286,13 +286,13 @@ export function isTouchEvent(
export function polyfill(win = window) { export function polyfill(win = window) {
if ('NodeList' in win && !win.NodeList.prototype.forEach) { if ('NodeList' in win && !win.NodeList.prototype.forEach) {
win.NodeList.prototype.forEach = (Array.prototype win.NodeList.prototype.forEach = Array.prototype
.forEach as unknown) as NodeList['forEach']; .forEach as unknown as NodeList['forEach'];
} }
if ('DOMTokenList' in win && !win.DOMTokenList.prototype.forEach) { if ('DOMTokenList' in win && !win.DOMTokenList.prototype.forEach) {
win.DOMTokenList.prototype.forEach = (Array.prototype win.DOMTokenList.prototype.forEach = Array.prototype
.forEach as unknown) as DOMTokenList['forEach']; .forEach as unknown as DOMTokenList['forEach'];
} }
// https://github.com/Financial-Times/polyfill-service/pull/183 // https://github.com/Financial-Times/polyfill-service/pull/183
@@ -322,6 +322,7 @@ export function needCastInSyncMode(event: eventWithTime): boolean {
return false; return false;
case EventType.FullSnapshot: case EventType.FullSnapshot:
case EventType.Meta: case EventType.Meta:
case EventType.Plugin:
return true; return true;
default: default:
break; break;
@@ -395,7 +396,7 @@ export class TreeIndex {
const node = mirror.getNode(id); const node = mirror.getNode(id);
node?.childNodes.forEach((childNode) => { node?.childNodes.forEach((childNode) => {
if ('__sn' in 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']; scrollMap: TreeIndex['scrollMap'];
inputMap: TreeIndex['inputMap']; inputMap: TreeIndex['inputMap'];
} { } {
const { const { tree, removeNodeMutations, textMutations, attributeMutations } =
tree, this;
removeNodeMutations,
textMutations,
attributeMutations,
} = this;
const batchMutationData: mutationData = { const batchMutationData: mutationData = {
source: IncrementalSource.Mutation, source: IncrementalSource.Mutation,
@@ -653,5 +650,5 @@ export function getBaseDimension(
export function hasShadowRoot<T extends Node>( export function hasShadowRoot<T extends Node>(
n: T, n: T,
): n is T & { shadowRoot: ShadowRoot } { ): n is T & { shadowRoot: ShadowRoot } {
return Boolean(((n as unknown) as Element)?.shadowRoot); return Boolean((n as unknown as Element)?.shadowRoot);
} }

View File

@@ -2855,213 +2855,247 @@ exports[`log 1`] = `
} }
}, },
{ {
\\"type\\": 3, \\"type\\": 6,
\\"data\\": { \\"data\\": {
\\"source\\": 11, \\"plugin\\": \\"rrweb/console@1\\",
\\"level\\": \\"assert\\", \\"payload\\": {
\\"trace\\": [ \\"level\\": \\"assert\\",
\\"__puppeteer_evaluation_script__:2:37\\" \\"trace\\": [
], \\"__puppeteer_evaluation_script__:2:37\\"
\\"payload\\": [ ],
\\"true\\", \\"payload\\": [
\\"\\\\\\"assert\\\\\\"\\" \\"true\\",
] \\"\\\\\\"assert\\\\\\"\\"
]
}
} }
}, },
{ {
\\"type\\": 3, \\"type\\": 6,
\\"data\\": { \\"data\\": {
\\"source\\": 11, \\"plugin\\": \\"rrweb/console@1\\",
\\"level\\": \\"count\\", \\"payload\\": {
\\"trace\\": [ \\"level\\": \\"count\\",
\\"__puppeteer_evaluation_script__:3:37\\" \\"trace\\": [
], \\"__puppeteer_evaluation_script__:3:37\\"
\\"payload\\": [ ],
\\"\\\\\\"count\\\\\\"\\" \\"payload\\": [
] \\"\\\\\\"count\\\\\\"\\"
]
}
} }
}, },
{ {
\\"type\\": 3, \\"type\\": 6,
\\"data\\": { \\"data\\": {
\\"source\\": 11, \\"plugin\\": \\"rrweb/console@1\\",
\\"level\\": \\"countReset\\", \\"payload\\": {
\\"trace\\": [ \\"level\\": \\"countReset\\",
\\"__puppeteer_evaluation_script__:4:37\\" \\"trace\\": [
], \\"__puppeteer_evaluation_script__:4:37\\"
\\"payload\\": [ ],
\\"\\\\\\"count\\\\\\"\\" \\"payload\\": [
] \\"\\\\\\"count\\\\\\"\\"
]
}
} }
}, },
{ {
\\"type\\": 3, \\"type\\": 6,
\\"data\\": { \\"data\\": {
\\"source\\": 11, \\"plugin\\": \\"rrweb/console@1\\",
\\"level\\": \\"debug\\", \\"payload\\": {
\\"trace\\": [ \\"level\\": \\"debug\\",
\\"__puppeteer_evaluation_script__:5:37\\" \\"trace\\": [
], \\"__puppeteer_evaluation_script__:5:37\\"
\\"payload\\": [ ],
\\"\\\\\\"debug\\\\\\"\\" \\"payload\\": [
] \\"\\\\\\"debug\\\\\\"\\"
]
}
} }
}, },
{ {
\\"type\\": 3, \\"type\\": 6,
\\"data\\": { \\"data\\": {
\\"source\\": 11, \\"plugin\\": \\"rrweb/console@1\\",
\\"level\\": \\"dir\\", \\"payload\\": {
\\"trace\\": [ \\"level\\": \\"dir\\",
\\"__puppeteer_evaluation_script__:6:37\\" \\"trace\\": [
], \\"__puppeteer_evaluation_script__:6:37\\"
\\"payload\\": [ ],
\\"\\\\\\"dir\\\\\\"\\" \\"payload\\": [
] \\"\\\\\\"dir\\\\\\"\\"
]
}
} }
}, },
{ {
\\"type\\": 3, \\"type\\": 6,
\\"data\\": { \\"data\\": {
\\"source\\": 11, \\"plugin\\": \\"rrweb/console@1\\",
\\"level\\": \\"dirxml\\", \\"payload\\": {
\\"trace\\": [ \\"level\\": \\"dirxml\\",
\\"__puppeteer_evaluation_script__:7:37\\" \\"trace\\": [
], \\"__puppeteer_evaluation_script__:7:37\\"
\\"payload\\": [ ],
\\"\\\\\\"dirxml\\\\\\"\\" \\"payload\\": [
] \\"\\\\\\"dirxml\\\\\\"\\"
]
}
} }
}, },
{ {
\\"type\\": 3, \\"type\\": 6,
\\"data\\": { \\"data\\": {
\\"source\\": 11, \\"plugin\\": \\"rrweb/console@1\\",
\\"level\\": \\"group\\", \\"payload\\": {
\\"trace\\": [ \\"level\\": \\"group\\",
\\"__puppeteer_evaluation_script__:8:37\\" \\"trace\\": [
], \\"__puppeteer_evaluation_script__:8:37\\"
\\"payload\\": [] ],
\\"payload\\": []
}
} }
}, },
{ {
\\"type\\": 3, \\"type\\": 6,
\\"data\\": { \\"data\\": {
\\"source\\": 11, \\"plugin\\": \\"rrweb/console@1\\",
\\"level\\": \\"groupCollapsed\\", \\"payload\\": {
\\"trace\\": [ \\"level\\": \\"groupCollapsed\\",
\\"__puppeteer_evaluation_script__:9:37\\" \\"trace\\": [
], \\"__puppeteer_evaluation_script__:9:37\\"
\\"payload\\": [] ],
\\"payload\\": []
}
} }
}, },
{ {
\\"type\\": 3, \\"type\\": 6,
\\"data\\": { \\"data\\": {
\\"source\\": 11, \\"plugin\\": \\"rrweb/console@1\\",
\\"level\\": \\"info\\", \\"payload\\": {
\\"trace\\": [ \\"level\\": \\"info\\",
\\"__puppeteer_evaluation_script__:10:37\\" \\"trace\\": [
], \\"__puppeteer_evaluation_script__:10:37\\"
\\"payload\\": [ ],
\\"\\\\\\"info\\\\\\"\\" \\"payload\\": [
] \\"\\\\\\"info\\\\\\"\\"
]
}
} }
}, },
{ {
\\"type\\": 3, \\"type\\": 6,
\\"data\\": { \\"data\\": {
\\"source\\": 11, \\"plugin\\": \\"rrweb/console@1\\",
\\"level\\": \\"log\\", \\"payload\\": {
\\"trace\\": [ \\"level\\": \\"log\\",
\\"__puppeteer_evaluation_script__:11:37\\" \\"trace\\": [
], \\"__puppeteer_evaluation_script__:11:37\\"
\\"payload\\": [ ],
\\"\\\\\\"log\\\\\\"\\" \\"payload\\": [
] \\"\\\\\\"log\\\\\\"\\"
]
}
} }
}, },
{ {
\\"type\\": 3, \\"type\\": 6,
\\"data\\": { \\"data\\": {
\\"source\\": 11, \\"plugin\\": \\"rrweb/console@1\\",
\\"level\\": \\"table\\", \\"payload\\": {
\\"trace\\": [ \\"level\\": \\"table\\",
\\"__puppeteer_evaluation_script__:12:37\\" \\"trace\\": [
], \\"__puppeteer_evaluation_script__:12:37\\"
\\"payload\\": [ ],
\\"\\\\\\"table\\\\\\"\\" \\"payload\\": [
] \\"\\\\\\"table\\\\\\"\\"
]
}
} }
}, },
{ {
\\"type\\": 3, \\"type\\": 6,
\\"data\\": { \\"data\\": {
\\"source\\": 11, \\"plugin\\": \\"rrweb/console@1\\",
\\"level\\": \\"time\\", \\"payload\\": {
\\"trace\\": [ \\"level\\": \\"time\\",
\\"__puppeteer_evaluation_script__:13:37\\" \\"trace\\": [
], \\"__puppeteer_evaluation_script__:13:37\\"
\\"payload\\": [] ],
\\"payload\\": []
}
} }
}, },
{ {
\\"type\\": 3, \\"type\\": 6,
\\"data\\": { \\"data\\": {
\\"source\\": 11, \\"plugin\\": \\"rrweb/console@1\\",
\\"level\\": \\"timeEnd\\", \\"payload\\": {
\\"trace\\": [ \\"level\\": \\"timeEnd\\",
\\"__puppeteer_evaluation_script__:14:37\\" \\"trace\\": [
], \\"__puppeteer_evaluation_script__:14:37\\"
\\"payload\\": [] ],
\\"payload\\": []
}
} }
}, },
{ {
\\"type\\": 3, \\"type\\": 6,
\\"data\\": { \\"data\\": {
\\"source\\": 11, \\"plugin\\": \\"rrweb/console@1\\",
\\"level\\": \\"timeLog\\", \\"payload\\": {
\\"trace\\": [ \\"level\\": \\"timeLog\\",
\\"__puppeteer_evaluation_script__:15:37\\" \\"trace\\": [
], \\"__puppeteer_evaluation_script__:15:37\\"
\\"payload\\": [] ],
\\"payload\\": []
}
} }
}, },
{ {
\\"type\\": 3, \\"type\\": 6,
\\"data\\": { \\"data\\": {
\\"source\\": 11, \\"plugin\\": \\"rrweb/console@1\\",
\\"level\\": \\"trace\\", \\"payload\\": {
\\"trace\\": [ \\"level\\": \\"trace\\",
\\"__puppeteer_evaluation_script__:16:37\\" \\"trace\\": [
], \\"__puppeteer_evaluation_script__:16:37\\"
\\"payload\\": [ ],
\\"\\\\\\"trace\\\\\\"\\" \\"payload\\": [
] \\"\\\\\\"trace\\\\\\"\\"
]
}
} }
}, },
{ {
\\"type\\": 3, \\"type\\": 6,
\\"data\\": { \\"data\\": {
\\"source\\": 11, \\"plugin\\": \\"rrweb/console@1\\",
\\"level\\": \\"warn\\", \\"payload\\": {
\\"trace\\": [ \\"level\\": \\"warn\\",
\\"__puppeteer_evaluation_script__:17:37\\" \\"trace\\": [
], \\"__puppeteer_evaluation_script__:17:37\\"
\\"payload\\": [ ],
\\"\\\\\\"warn\\\\\\"\\" \\"payload\\": [
] \\"\\\\\\"warn\\\\\\"\\"
]
}
} }
}, },
{ {
\\"type\\": 3, \\"type\\": 6,
\\"data\\": { \\"data\\": {
\\"source\\": 11, \\"plugin\\": \\"rrweb/console@1\\",
\\"level\\": \\"clear\\", \\"payload\\": {
\\"trace\\": [ \\"level\\": \\"clear\\",
\\"__puppeteer_evaluation_script__:18:37\\" \\"trace\\": [
], \\"__puppeteer_evaluation_script__:18:37\\"
\\"payload\\": [] ],
\\"payload\\": []
}
} }
} }
]" ]"

View File

@@ -77,7 +77,7 @@ describe('record integration tests', function (this: ISuite) {
maskInputOptions: ${JSON.stringify(options.maskAllInputs)}, maskInputOptions: ${JSON.stringify(options.maskAllInputs)},
maskTextFn: ${options.maskTextFn}, maskTextFn: ${options.maskTextFn},
recordCanvas: ${options.recordCanvas}, recordCanvas: ${options.recordCanvas},
recordLog: ${options.recordLog}, plugins: ${options.plugins}
}); });
</script> </script>
</body> </body>
@@ -90,7 +90,12 @@ describe('record integration tests', function (this: ISuite) {
this.browser = await launchPuppeteer(); this.browser = await launchPuppeteer();
const bundlePath = path.resolve(__dirname, '../dist/rrweb.min.js'); 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 () => { after(async () => {
@@ -385,7 +390,7 @@ describe('record integration tests', function (this: ISuite) {
await page.goto('about:blank'); await page.goto('about:blank');
await page.setContent( await page.setContent(
getHtml.call(this, 'log.html', { getHtml.call(this, 'log.html', {
recordLog: true, plugins: '[rrwebConsoleRecord.getRecordConsolePlugin()]',
}), }),
); );

View File

@@ -1,2 +1,4 @@
export * from '../index'; export * from '../index';
export * from '../packer'; export * from '../packer';
export * from '../plugins/console/record';
export * from '../plugins/console/replay';

View 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[];
};

View 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 {};

View File

@@ -0,0 +1,2 @@
import { StringifyOptions } from './index';
export declare function stringify(obj: any, stringifyOptions?: StringifyOptions): string;

View 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 {};

View File

@@ -50,8 +50,6 @@ export declare class Replayer {
private applyMutation; private applyMutation;
private applyScroll; private applyScroll;
private applyInput; private applyInput;
private formatMessage;
private getConsoleLogger;
private legacy_resolveMissingNode; private legacy_resolveMissingNode;
private moveAndHover; private moveAndHover;
private drawMouseTail; private drawMouseTail;

87
typings/types.d.ts vendored
View File

@@ -3,13 +3,15 @@ import { PackFn, UnpackFn } from './packer/base';
import { FontFaceDescriptors } from 'css-font-loading-module'; import { FontFaceDescriptors } from 'css-font-loading-module';
import { IframeManager } from './record/iframe-manager'; import { IframeManager } from './record/iframe-manager';
import { ShadowDomManager } from './record/shadow-dom-manager'; import { ShadowDomManager } from './record/shadow-dom-manager';
import type { Replayer } from './replay';
export declare enum EventType { export declare enum EventType {
DomContentLoaded = 0, DomContentLoaded = 0,
Load = 1, Load = 1,
FullSnapshot = 2, FullSnapshot = 2,
IncrementalSnapshot = 3, IncrementalSnapshot = 3,
Meta = 4, Meta = 4,
Custom = 5 Custom = 5,
Plugin = 6
} }
export declare type domContentLoadedEvent = { export declare type domContentLoadedEvent = {
type: EventType.DomContentLoaded; type: EventType.DomContentLoaded;
@@ -41,10 +43,6 @@ export declare type metaEvent = {
height: number; height: number;
}; };
}; };
export declare type logEvent = {
type: EventType.IncrementalSnapshot;
data: incrementalData;
};
export declare type customEvent<T = unknown> = { export declare type customEvent<T = unknown> = {
type: EventType.Custom; type: EventType.Custom;
data: { data: {
@@ -52,6 +50,13 @@ export declare type customEvent<T = unknown> = {
payload: T; payload: T;
}; };
}; };
export declare type pluginEvent<T = unknown> = {
type: EventType.Plugin;
data: {
plugin: string;
payload: T;
};
};
export declare type styleSheetEvent = {}; export declare type styleSheetEvent = {};
export declare enum IncrementalSource { export declare enum IncrementalSource {
Mutation = 0, Mutation = 0,
@@ -100,11 +105,8 @@ export declare type canvasMutationData = {
export declare type fontData = { export declare type fontData = {
source: IncrementalSource.Font; source: IncrementalSource.Font;
} & fontParam; } & fontParam;
export declare type logData = { export declare type incrementalData = mutationData | mousemoveData | mouseInteractionData | scrollData | viewportResizeData | inputData | mediaInteractionData | styleSheetRuleData | canvasMutationData | fontData;
source: IncrementalSource.Log; export declare type event = domContentLoadedEvent | loadedEvent | fullSnapshotEvent | incrementalSnapshotEvent | metaEvent | customEvent | pluginEvent;
} & 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 eventWithTime = event & { export declare type eventWithTime = event & {
timestamp: number; timestamp: number;
delay?: number; delay?: number;
@@ -118,6 +120,11 @@ export declare type SamplingStrategy = Partial<{
scroll: number; scroll: number;
input: 'all' | 'last'; input: 'all' | 'last';
}>; }>;
export declare type RecordPlugin<TOptions = unknown> = {
name: string;
observer: (cb: Function, options: TOptions) => listenerHandler;
options: TOptions;
};
export declare type recordOptions<T> = { export declare type recordOptions<T> = {
emit?: (e: T, isCheckout?: boolean) => void; emit?: (e: T, isCheckout?: boolean) => void;
checkoutEveryNth?: number; checkoutEveryNth?: number;
@@ -138,8 +145,8 @@ export declare type recordOptions<T> = {
sampling?: SamplingStrategy; sampling?: SamplingStrategy;
recordCanvas?: boolean; recordCanvas?: boolean;
collectFonts?: boolean; collectFonts?: boolean;
plugins?: RecordPlugin[];
mousemoveWait?: number; mousemoveWait?: number;
recordLog?: boolean | LogRecordOptions;
}; };
export declare type observerParam = { export declare type observerParam = {
mutationCb: mutationCallBack; mutationCb: mutationCallBack;
@@ -161,8 +168,6 @@ export declare type observerParam = {
styleSheetRuleCb: styleSheetRuleCallback; styleSheetRuleCb: styleSheetRuleCallback;
canvasMutationCb: canvasMutationCallback; canvasMutationCb: canvasMutationCallback;
fontCb: fontCallback; fontCb: fontCallback;
logCb: logCallback;
logOptions: LogRecordOptions;
sampling: SamplingStrategy; sampling: SamplingStrategy;
recordCanvas: boolean; recordCanvas: boolean;
collectFonts: boolean; collectFonts: boolean;
@@ -171,6 +176,11 @@ export declare type observerParam = {
mirror: Mirror; mirror: Mirror;
iframeManager: IframeManager; iframeManager: IframeManager;
shadowDomManager: ShadowDomManager; shadowDomManager: ShadowDomManager;
plugins: Array<{
observer: Function;
callback: Function;
options: unknown;
}>;
}; };
export declare type hooksParam = { export declare type hooksParam = {
mutation?: mutationCallBack; mutation?: mutationCallBack;
@@ -183,7 +193,6 @@ export declare type hooksParam = {
styleSheetRule?: styleSheetRuleCallback; styleSheetRule?: styleSheetRuleCallback;
canvasMutation?: canvasMutationCallback; canvasMutation?: canvasMutationCallback;
font?: fontCallback; font?: fontCallback;
log?: logCallback;
}; };
export declare type mutationRecord = { export declare type mutationRecord = {
type: string; type: string;
@@ -290,36 +299,7 @@ export declare type fontParam = {
buffer: boolean; buffer: boolean;
descriptors?: FontFaceDescriptors; 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 fontCallback = (p: fontParam) => void;
export declare type logCallback = (p: LogParam) => void;
export declare type viewportResizeDimension = { export declare type viewportResizeDimension = {
width: number; width: number;
height: number; height: number;
@@ -363,6 +343,11 @@ export declare type throttleOptions = {
}; };
export declare type listenerHandler = () => void; export declare type listenerHandler = () => void;
export declare type hookResetter = () => void; export declare type hookResetter = () => void;
export declare type ReplayPlugin = {
handler: (event: eventWithTime, isSync: boolean, context: {
replayer: Replayer;
}) => void;
};
export declare type playerConfig = { export declare type playerConfig = {
speed: number; speed: number;
maxSpeed: number; maxSpeed: number;
@@ -384,11 +369,7 @@ export declare type playerConfig = {
strokeStyle?: string; strokeStyle?: string;
}; };
unpackFn?: UnpackFn; unpackFn?: UnpackFn;
logConfig: LogReplayConfig; plugins?: ReplayPlugin[];
};
export declare type LogReplayConfig = {
level?: LogLevel[] | undefined;
replayLogger: ReplayLogger | undefined;
}; };
export declare type playerMetaData = { export declare type playerMetaData = {
startTime: number; startTime: number;
@@ -436,14 +417,4 @@ export declare type MaskTextFn = (text: string) => string;
export declare type ElementState = { export declare type ElementState = {
scroll?: [number, number]; 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 {}; export {};