feat: Allow to pass errorHandler as record option (#1107)
* feat: Allow to pass `errorHandler` as record option * add docs * Apply formatting changes
This commit is contained in:
5
.changeset/pretty-plums-rescue.md
Normal file
5
.changeset/pretty-plums-rescue.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
'rrweb': minor
|
||||||
|
---
|
||||||
|
|
||||||
|
feat: Allow to pass `errorHandler` as record option
|
||||||
1
guide.md
1
guide.md
@@ -163,6 +163,7 @@ The parameter of `rrweb.record` accepts the following options.
|
|||||||
| collectFonts | false | whether to collect fonts in the website |
|
| collectFonts | false | whether to collect fonts in the website |
|
||||||
| userTriggeredOnInput | false | whether to add `userTriggered` on input events that indicates if this event was triggered directly by the user or not. [What is `userTriggered`?](https://github.com/rrweb-io/rrweb/pull/495) |
|
| userTriggeredOnInput | false | whether to add `userTriggered` on input events that indicates if this event was triggered directly by the user or not. [What is `userTriggered`?](https://github.com/rrweb-io/rrweb/pull/495) |
|
||||||
| plugins | [] | load plugins to provide extended record functions. [What is plugins?](./docs/recipes/plugin.md) |
|
| plugins | [] | load plugins to provide extended record functions. [What is plugins?](./docs/recipes/plugin.md) |
|
||||||
|
| errorHandler | - | A callback that is called if something inside of rrweb throws an error. The callback receives the error as argument. |
|
||||||
|
|
||||||
#### Privacy
|
#### Privacy
|
||||||
|
|
||||||
|
|||||||
36
packages/rrweb/src/record/error-handler.ts
Normal file
36
packages/rrweb/src/record/error-handler.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import type { ErrorHandler } from '../types';
|
||||||
|
|
||||||
|
type Callback = (...args: unknown[]) => unknown;
|
||||||
|
|
||||||
|
let errorHandler: ErrorHandler | undefined;
|
||||||
|
|
||||||
|
export function registerErrorHandler(handler: ErrorHandler | undefined) {
|
||||||
|
errorHandler = handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function unregisterErrorHandler() {
|
||||||
|
errorHandler = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrap callbacks in a wrapper that allows to pass errors to a configured `errorHandler` method.
|
||||||
|
*/
|
||||||
|
export const callbackWrapper = <T extends Callback>(cb: T): T => {
|
||||||
|
if (!errorHandler) {
|
||||||
|
return cb;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rrwebWrapped = ((...rest: unknown[]) => {
|
||||||
|
try {
|
||||||
|
return cb(...rest);
|
||||||
|
} catch (error) {
|
||||||
|
if (errorHandler && errorHandler(error) === true) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}) as unknown as T;
|
||||||
|
|
||||||
|
return rrwebWrapped;
|
||||||
|
};
|
||||||
@@ -36,6 +36,11 @@ import { IframeManager } from './iframe-manager';
|
|||||||
import { ShadowDomManager } from './shadow-dom-manager';
|
import { ShadowDomManager } from './shadow-dom-manager';
|
||||||
import { CanvasManager } from './observers/canvas/canvas-manager';
|
import { CanvasManager } from './observers/canvas/canvas-manager';
|
||||||
import { StylesheetManager } from './stylesheet-manager';
|
import { StylesheetManager } from './stylesheet-manager';
|
||||||
|
import {
|
||||||
|
callbackWrapper,
|
||||||
|
registerErrorHandler,
|
||||||
|
unregisterErrorHandler,
|
||||||
|
} from './error-handler';
|
||||||
|
|
||||||
function wrapEvent(e: event): eventWithTime {
|
function wrapEvent(e: event): eventWithTime {
|
||||||
return {
|
return {
|
||||||
@@ -85,8 +90,11 @@ function record<T = eventWithTime>(
|
|||||||
plugins,
|
plugins,
|
||||||
keepIframeSrcFn = () => false,
|
keepIframeSrcFn = () => false,
|
||||||
ignoreCSSAttributes = new Set([]),
|
ignoreCSSAttributes = new Set([]),
|
||||||
|
errorHandler,
|
||||||
} = options;
|
} = options;
|
||||||
|
|
||||||
|
registerErrorHandler(errorHandler);
|
||||||
|
|
||||||
const inEmittingFrame = recordCrossOriginIframes
|
const inEmittingFrame = recordCrossOriginIframes
|
||||||
? window.parent === window
|
? window.parent === window
|
||||||
: true;
|
: true;
|
||||||
@@ -416,7 +424,7 @@ function record<T = eventWithTime>(
|
|||||||
const handlers: listenerHandler[] = [];
|
const handlers: listenerHandler[] = [];
|
||||||
|
|
||||||
const observe = (doc: Document) => {
|
const observe = (doc: Document) => {
|
||||||
return initObservers(
|
return callbackWrapper(initObservers)(
|
||||||
{
|
{
|
||||||
mutationCb: wrappedMutationEmit,
|
mutationCb: wrappedMutationEmit,
|
||||||
mousemoveCb: (positions, source) =>
|
mousemoveCb: (positions, source) =>
|
||||||
@@ -609,6 +617,7 @@ function record<T = eventWithTime>(
|
|||||||
return () => {
|
return () => {
|
||||||
handlers.forEach((h) => h());
|
handlers.forEach((h) => h());
|
||||||
recording = false;
|
recording = false;
|
||||||
|
unregisterErrorHandler();
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// TODO: handle internal error
|
// TODO: handle internal error
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ import {
|
|||||||
} from '@rrweb/types';
|
} from '@rrweb/types';
|
||||||
import MutationBuffer from './mutation';
|
import MutationBuffer from './mutation';
|
||||||
import ProcessedNodeManager from './processed-node-manager';
|
import ProcessedNodeManager from './processed-node-manager';
|
||||||
|
import { callbackWrapper } from './error-handler';
|
||||||
|
|
||||||
type WindowWithStoredMutationObserver = IWindow & {
|
type WindowWithStoredMutationObserver = IWindow & {
|
||||||
__rrMutationObserver?: MutationObserver;
|
__rrMutationObserver?: MutationObserver;
|
||||||
@@ -110,7 +111,9 @@ export function initMutationObserver(
|
|||||||
}
|
}
|
||||||
const observer = new (mutationObserverCtor as new (
|
const observer = new (mutationObserverCtor as new (
|
||||||
callback: MutationCallback,
|
callback: MutationCallback,
|
||||||
) => MutationObserver)(mutationBuffer.processMutations.bind(mutationBuffer));
|
) => MutationObserver)(
|
||||||
|
callbackWrapper(mutationBuffer.processMutations.bind(mutationBuffer)),
|
||||||
|
);
|
||||||
observer.observe(rootEl, {
|
observer.observe(rootEl, {
|
||||||
attributes: true,
|
attributes: true,
|
||||||
attributeOldValue: true,
|
attributeOldValue: true,
|
||||||
@@ -144,63 +147,67 @@ function initMoveObserver({
|
|||||||
let positions: mousePosition[] = [];
|
let positions: mousePosition[] = [];
|
||||||
let timeBaseline: number | null;
|
let timeBaseline: number | null;
|
||||||
const wrappedCb = throttle(
|
const wrappedCb = throttle(
|
||||||
(
|
callbackWrapper(
|
||||||
source:
|
(
|
||||||
| IncrementalSource.MouseMove
|
source:
|
||||||
| IncrementalSource.TouchMove
|
| IncrementalSource.MouseMove
|
||||||
| IncrementalSource.Drag,
|
| IncrementalSource.TouchMove
|
||||||
) => {
|
| IncrementalSource.Drag,
|
||||||
const totalOffset = Date.now() - timeBaseline!;
|
) => {
|
||||||
mousemoveCb(
|
const totalOffset = Date.now() - timeBaseline!;
|
||||||
positions.map((p) => {
|
mousemoveCb(
|
||||||
p.timeOffset -= totalOffset;
|
positions.map((p) => {
|
||||||
return p;
|
p.timeOffset -= totalOffset;
|
||||||
}),
|
return p;
|
||||||
source,
|
}),
|
||||||
);
|
source,
|
||||||
positions = [];
|
);
|
||||||
timeBaseline = null;
|
positions = [];
|
||||||
},
|
timeBaseline = null;
|
||||||
|
},
|
||||||
|
),
|
||||||
callbackThreshold,
|
callbackThreshold,
|
||||||
);
|
);
|
||||||
const updatePosition = throttle<MouseEvent | TouchEvent | DragEvent>(
|
const updatePosition = callbackWrapper(
|
||||||
(evt) => {
|
throttle<MouseEvent | TouchEvent | DragEvent>(
|
||||||
const target = getEventTarget(evt);
|
callbackWrapper((evt) => {
|
||||||
const { clientX, clientY } = isTouchEvent(evt)
|
const target = getEventTarget(evt);
|
||||||
? evt.changedTouches[0]
|
const { clientX, clientY } = isTouchEvent(evt)
|
||||||
: evt;
|
? evt.changedTouches[0]
|
||||||
if (!timeBaseline) {
|
: evt;
|
||||||
timeBaseline = Date.now();
|
if (!timeBaseline) {
|
||||||
}
|
timeBaseline = Date.now();
|
||||||
positions.push({
|
}
|
||||||
x: clientX,
|
positions.push({
|
||||||
y: clientY,
|
x: clientX,
|
||||||
id: mirror.getId(target as Node),
|
y: clientY,
|
||||||
timeOffset: Date.now() - timeBaseline,
|
id: mirror.getId(target as Node),
|
||||||
});
|
timeOffset: Date.now() - timeBaseline,
|
||||||
// it is possible DragEvent is undefined even on devices
|
});
|
||||||
// that support event 'drag'
|
// it is possible DragEvent is undefined even on devices
|
||||||
wrappedCb(
|
// that support event 'drag'
|
||||||
typeof DragEvent !== 'undefined' && evt instanceof DragEvent
|
wrappedCb(
|
||||||
? IncrementalSource.Drag
|
typeof DragEvent !== 'undefined' && evt instanceof DragEvent
|
||||||
: evt instanceof MouseEvent
|
? IncrementalSource.Drag
|
||||||
? IncrementalSource.MouseMove
|
: evt instanceof MouseEvent
|
||||||
: IncrementalSource.TouchMove,
|
? IncrementalSource.MouseMove
|
||||||
);
|
: IncrementalSource.TouchMove,
|
||||||
},
|
);
|
||||||
threshold,
|
}),
|
||||||
{
|
threshold,
|
||||||
trailing: false,
|
{
|
||||||
},
|
trailing: false,
|
||||||
|
},
|
||||||
|
),
|
||||||
);
|
);
|
||||||
const handlers = [
|
const handlers = [
|
||||||
on('mousemove', updatePosition, doc),
|
on('mousemove', updatePosition, doc),
|
||||||
on('touchmove', updatePosition, doc),
|
on('touchmove', updatePosition, doc),
|
||||||
on('drag', updatePosition, doc),
|
on('drag', updatePosition, doc),
|
||||||
];
|
];
|
||||||
return () => {
|
return callbackWrapper(() => {
|
||||||
handlers.forEach((h) => h());
|
handlers.forEach((h) => h());
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function initMouseInteractionObserver({
|
function initMouseInteractionObserver({
|
||||||
@@ -235,7 +242,7 @@ function initMouseInteractionObserver({
|
|||||||
}
|
}
|
||||||
const id = mirror.getId(target);
|
const id = mirror.getId(target);
|
||||||
const { clientX, clientY } = e;
|
const { clientX, clientY } = e;
|
||||||
mouseInteractionCb({
|
callbackWrapper(mouseInteractionCb)({
|
||||||
type: MouseInteractions[eventKey],
|
type: MouseInteractions[eventKey],
|
||||||
id,
|
id,
|
||||||
x: clientX,
|
x: clientX,
|
||||||
@@ -255,9 +262,9 @@ function initMouseInteractionObserver({
|
|||||||
const handler = getHandler(eventKey);
|
const handler = getHandler(eventKey);
|
||||||
handlers.push(on(eventName, handler, doc));
|
handlers.push(on(eventName, handler, doc));
|
||||||
});
|
});
|
||||||
return () => {
|
return callbackWrapper(() => {
|
||||||
handlers.forEach((h) => h());
|
handlers.forEach((h) => h());
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function initScrollObserver({
|
export function initScrollObserver({
|
||||||
@@ -271,27 +278,35 @@ export function initScrollObserver({
|
|||||||
observerParam,
|
observerParam,
|
||||||
'scrollCb' | 'doc' | 'mirror' | 'blockClass' | 'blockSelector' | 'sampling'
|
'scrollCb' | 'doc' | 'mirror' | 'blockClass' | 'blockSelector' | 'sampling'
|
||||||
>): listenerHandler {
|
>): listenerHandler {
|
||||||
const updatePosition = throttle<UIEvent>((evt) => {
|
const updatePosition = callbackWrapper(
|
||||||
const target = getEventTarget(evt);
|
throttle<UIEvent>(
|
||||||
if (!target || isBlocked(target as Node, blockClass, blockSelector, true)) {
|
callbackWrapper((evt) => {
|
||||||
return;
|
const target = getEventTarget(evt);
|
||||||
}
|
if (
|
||||||
const id = mirror.getId(target as Node);
|
!target ||
|
||||||
if (target === doc && doc.defaultView) {
|
isBlocked(target as Node, blockClass, blockSelector, true)
|
||||||
const scrollLeftTop = getWindowScroll(doc.defaultView);
|
) {
|
||||||
scrollCb({
|
return;
|
||||||
id,
|
}
|
||||||
x: scrollLeftTop.left,
|
const id = mirror.getId(target as Node);
|
||||||
y: scrollLeftTop.top,
|
if (target === doc && doc.defaultView) {
|
||||||
});
|
const scrollLeftTop = getWindowScroll(doc.defaultView);
|
||||||
} else {
|
scrollCb({
|
||||||
scrollCb({
|
id,
|
||||||
id,
|
x: scrollLeftTop.left,
|
||||||
x: (target as HTMLElement).scrollLeft,
|
y: scrollLeftTop.top,
|
||||||
y: (target as HTMLElement).scrollTop,
|
});
|
||||||
});
|
} else {
|
||||||
}
|
scrollCb({
|
||||||
}, sampling.scroll || 100);
|
id,
|
||||||
|
x: (target as HTMLElement).scrollLeft,
|
||||||
|
y: (target as HTMLElement).scrollTop,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
sampling.scroll || 100,
|
||||||
|
),
|
||||||
|
);
|
||||||
return on('scroll', updatePosition, doc);
|
return on('scroll', updatePosition, doc);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -300,18 +315,23 @@ function initViewportResizeObserver({
|
|||||||
}: observerParam): listenerHandler {
|
}: observerParam): listenerHandler {
|
||||||
let lastH = -1;
|
let lastH = -1;
|
||||||
let lastW = -1;
|
let lastW = -1;
|
||||||
const updateDimension = throttle(() => {
|
const updateDimension = callbackWrapper(
|
||||||
const height = getWindowHeight();
|
throttle(
|
||||||
const width = getWindowWidth();
|
callbackWrapper(() => {
|
||||||
if (lastH !== height || lastW !== width) {
|
const height = getWindowHeight();
|
||||||
viewportResizeCb({
|
const width = getWindowWidth();
|
||||||
width: Number(width),
|
if (lastH !== height || lastW !== width) {
|
||||||
height: Number(height),
|
viewportResizeCb({
|
||||||
});
|
width: Number(width),
|
||||||
lastH = height;
|
height: Number(height),
|
||||||
lastW = width;
|
});
|
||||||
}
|
lastH = height;
|
||||||
}, 200);
|
lastW = width;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
200,
|
||||||
|
),
|
||||||
|
);
|
||||||
return on('resize', updateDimension, window);
|
return on('resize', updateDimension, window);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -382,7 +402,7 @@ function initInputObserver({
|
|||||||
}
|
}
|
||||||
cbWithDedup(
|
cbWithDedup(
|
||||||
target,
|
target,
|
||||||
wrapEventWithUserTriggeredFlag(
|
callbackWrapper(wrapEventWithUserTriggeredFlag)(
|
||||||
{ text, isChecked, userTriggered },
|
{ text, isChecked, userTriggered },
|
||||||
userTriggeredOnInput,
|
userTriggeredOnInput,
|
||||||
),
|
),
|
||||||
@@ -397,7 +417,7 @@ function initInputObserver({
|
|||||||
if (el !== target) {
|
if (el !== target) {
|
||||||
cbWithDedup(
|
cbWithDedup(
|
||||||
el,
|
el,
|
||||||
wrapEventWithUserTriggeredFlag(
|
callbackWrapper(wrapEventWithUserTriggeredFlag)(
|
||||||
{
|
{
|
||||||
text: (el as HTMLInputElement).value,
|
text: (el as HTMLInputElement).value,
|
||||||
isChecked: !isChecked,
|
isChecked: !isChecked,
|
||||||
@@ -419,7 +439,7 @@ function initInputObserver({
|
|||||||
) {
|
) {
|
||||||
lastInputValueMap.set(target, v);
|
lastInputValueMap.set(target, v);
|
||||||
const id = mirror.getId(target as Node);
|
const id = mirror.getId(target as Node);
|
||||||
inputCb({
|
callbackWrapper(inputCb)({
|
||||||
...v,
|
...v,
|
||||||
id,
|
id,
|
||||||
});
|
});
|
||||||
@@ -427,7 +447,7 @@ function initInputObserver({
|
|||||||
}
|
}
|
||||||
const events = sampling.input === 'last' ? ['change'] : ['input', 'change'];
|
const events = sampling.input === 'last' ? ['change'] : ['input', 'change'];
|
||||||
const handlers: Array<listenerHandler | hookResetter> = events.map(
|
const handlers: Array<listenerHandler | hookResetter> = events.map(
|
||||||
(eventName) => on(eventName, eventHandler, doc),
|
(eventName) => on(eventName, callbackWrapper(eventHandler), doc),
|
||||||
);
|
);
|
||||||
const currentWindow = doc.defaultView;
|
const currentWindow = doc.defaultView;
|
||||||
if (!currentWindow) {
|
if (!currentWindow) {
|
||||||
@@ -457,7 +477,7 @@ function initInputObserver({
|
|||||||
{
|
{
|
||||||
set() {
|
set() {
|
||||||
// mock to a normal event
|
// mock to a normal event
|
||||||
eventHandler({
|
callbackWrapper(eventHandler)({
|
||||||
target: this as EventTarget,
|
target: this as EventTarget,
|
||||||
isTrusted: false, // userTriggered to false as this could well be programmatic
|
isTrusted: false, // userTriggered to false as this could well be programmatic
|
||||||
} as Event);
|
} as Event);
|
||||||
@@ -469,9 +489,9 @@ function initInputObserver({
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return () => {
|
return callbackWrapper(() => {
|
||||||
handlers.forEach((h) => h());
|
handlers.forEach((h) => h());
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
type GroupingCSSRule =
|
type GroupingCSSRule =
|
||||||
@@ -548,97 +568,125 @@ function initStyleSheetObserver(
|
|||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/unbound-method
|
// eslint-disable-next-line @typescript-eslint/unbound-method
|
||||||
const insertRule = win.CSSStyleSheet.prototype.insertRule;
|
const insertRule = win.CSSStyleSheet.prototype.insertRule;
|
||||||
win.CSSStyleSheet.prototype.insertRule = function (
|
win.CSSStyleSheet.prototype.insertRule = new Proxy(insertRule, {
|
||||||
this: CSSStyleSheet,
|
apply: callbackWrapper(
|
||||||
rule: string,
|
(
|
||||||
index?: number,
|
target: typeof insertRule,
|
||||||
) {
|
thisArg: CSSStyleSheet,
|
||||||
const { id, styleId } = getIdAndStyleId(
|
argumentsList: [string, number | undefined],
|
||||||
this,
|
) => {
|
||||||
mirror,
|
const [rule, index] = argumentsList;
|
||||||
stylesheetManager.styleMirror,
|
|
||||||
);
|
|
||||||
|
|
||||||
if ((id && id !== -1) || (styleId && styleId !== -1)) {
|
const { id, styleId } = getIdAndStyleId(
|
||||||
styleSheetRuleCb({
|
thisArg,
|
||||||
id,
|
mirror,
|
||||||
styleId,
|
stylesheetManager.styleMirror,
|
||||||
adds: [{ rule, index }],
|
);
|
||||||
});
|
|
||||||
}
|
if ((id && id !== -1) || (styleId && styleId !== -1)) {
|
||||||
return insertRule.apply(this, [rule, index]);
|
styleSheetRuleCb({
|
||||||
};
|
id,
|
||||||
|
styleId,
|
||||||
|
adds: [{ rule, index }],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return target.apply(thisArg, argumentsList);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/unbound-method
|
// eslint-disable-next-line @typescript-eslint/unbound-method
|
||||||
const deleteRule = win.CSSStyleSheet.prototype.deleteRule;
|
const deleteRule = win.CSSStyleSheet.prototype.deleteRule;
|
||||||
win.CSSStyleSheet.prototype.deleteRule = function (
|
win.CSSStyleSheet.prototype.deleteRule = new Proxy(deleteRule, {
|
||||||
this: CSSStyleSheet,
|
apply: callbackWrapper(
|
||||||
index: number,
|
(
|
||||||
) {
|
target: typeof deleteRule,
|
||||||
const { id, styleId } = getIdAndStyleId(
|
thisArg: CSSStyleSheet,
|
||||||
this,
|
argumentsList: [number],
|
||||||
mirror,
|
) => {
|
||||||
stylesheetManager.styleMirror,
|
const [index] = argumentsList;
|
||||||
);
|
|
||||||
|
|
||||||
if ((id && id !== -1) || (styleId && styleId !== -1)) {
|
const { id, styleId } = getIdAndStyleId(
|
||||||
styleSheetRuleCb({
|
thisArg,
|
||||||
id,
|
mirror,
|
||||||
styleId,
|
stylesheetManager.styleMirror,
|
||||||
removes: [{ index }],
|
);
|
||||||
});
|
|
||||||
}
|
if ((id && id !== -1) || (styleId && styleId !== -1)) {
|
||||||
return deleteRule.apply(this, [index]);
|
styleSheetRuleCb({
|
||||||
};
|
id,
|
||||||
|
styleId,
|
||||||
|
removes: [{ index }],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return target.apply(thisArg, argumentsList);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
let replace: (text: string) => Promise<CSSStyleSheet>;
|
let replace: (text: string) => Promise<CSSStyleSheet>;
|
||||||
|
|
||||||
if (win.CSSStyleSheet.prototype.replace) {
|
if (win.CSSStyleSheet.prototype.replace) {
|
||||||
// eslint-disable-next-line @typescript-eslint/unbound-method
|
// eslint-disable-next-line @typescript-eslint/unbound-method
|
||||||
replace = win.CSSStyleSheet.prototype.replace;
|
replace = win.CSSStyleSheet.prototype.replace;
|
||||||
win.CSSStyleSheet.prototype.replace = function (
|
win.CSSStyleSheet.prototype.replace = new Proxy(replace, {
|
||||||
this: CSSStyleSheet,
|
apply: callbackWrapper(
|
||||||
text: string,
|
(
|
||||||
) {
|
target: typeof replace,
|
||||||
const { id, styleId } = getIdAndStyleId(
|
thisArg: CSSStyleSheet,
|
||||||
this,
|
argumentsList: [string],
|
||||||
mirror,
|
) => {
|
||||||
stylesheetManager.styleMirror,
|
const [text] = argumentsList;
|
||||||
);
|
|
||||||
|
|
||||||
if ((id && id !== -1) || (styleId && styleId !== -1)) {
|
const { id, styleId } = getIdAndStyleId(
|
||||||
styleSheetRuleCb({
|
thisArg,
|
||||||
id,
|
mirror,
|
||||||
styleId,
|
stylesheetManager.styleMirror,
|
||||||
replace: text,
|
);
|
||||||
});
|
|
||||||
}
|
if ((id && id !== -1) || (styleId && styleId !== -1)) {
|
||||||
return replace.apply(this, [text]);
|
styleSheetRuleCb({
|
||||||
};
|
id,
|
||||||
|
styleId,
|
||||||
|
replace: text,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return target.apply(thisArg, argumentsList);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let replaceSync: (text: string) => void;
|
let replaceSync: (text: string) => void;
|
||||||
if (win.CSSStyleSheet.prototype.replaceSync) {
|
if (win.CSSStyleSheet.prototype.replaceSync) {
|
||||||
// eslint-disable-next-line @typescript-eslint/unbound-method
|
// eslint-disable-next-line @typescript-eslint/unbound-method
|
||||||
replaceSync = win.CSSStyleSheet.prototype.replaceSync;
|
replaceSync = win.CSSStyleSheet.prototype.replaceSync;
|
||||||
win.CSSStyleSheet.prototype.replaceSync = function (
|
win.CSSStyleSheet.prototype.replaceSync = new Proxy(replaceSync, {
|
||||||
this: CSSStyleSheet,
|
apply: callbackWrapper(
|
||||||
text: string,
|
(
|
||||||
) {
|
target: typeof replaceSync,
|
||||||
const { id, styleId } = getIdAndStyleId(
|
thisArg: CSSStyleSheet,
|
||||||
this,
|
argumentsList: [string],
|
||||||
mirror,
|
) => {
|
||||||
stylesheetManager.styleMirror,
|
const [text] = argumentsList;
|
||||||
);
|
|
||||||
|
|
||||||
if ((id && id !== -1) || (styleId && styleId !== -1)) {
|
const { id, styleId } = getIdAndStyleId(
|
||||||
styleSheetRuleCb({
|
thisArg,
|
||||||
id,
|
mirror,
|
||||||
styleId,
|
stylesheetManager.styleMirror,
|
||||||
replaceSync: text,
|
);
|
||||||
});
|
|
||||||
}
|
if ((id && id !== -1) || (styleId && styleId !== -1)) {
|
||||||
return replaceSync.apply(this, [text]);
|
styleSheetRuleCb({
|
||||||
};
|
id,
|
||||||
|
styleId,
|
||||||
|
replaceSync: text,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return target.apply(thisArg, argumentsList);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const supportedNestedCSSRuleTypes: {
|
const supportedNestedCSSRuleTypes: {
|
||||||
@@ -677,59 +725,78 @@ function initStyleSheetObserver(
|
|||||||
deleteRule: type.prototype.deleteRule,
|
deleteRule: type.prototype.deleteRule,
|
||||||
};
|
};
|
||||||
|
|
||||||
type.prototype.insertRule = function (
|
type.prototype.insertRule = new Proxy(
|
||||||
this: CSSGroupingRule,
|
unmodifiedFunctions[typeKey].insertRule,
|
||||||
rule: string,
|
{
|
||||||
index?: number,
|
apply: callbackWrapper(
|
||||||
) {
|
(
|
||||||
const { id, styleId } = getIdAndStyleId(
|
target: typeof insertRule,
|
||||||
this.parentStyleSheet,
|
thisArg: CSSRule,
|
||||||
mirror,
|
argumentsList: [string, number | undefined],
|
||||||
stylesheetManager.styleMirror,
|
) => {
|
||||||
);
|
const [rule, index] = argumentsList;
|
||||||
|
|
||||||
if ((id && id !== -1) || (styleId && styleId !== -1)) {
|
const { id, styleId } = getIdAndStyleId(
|
||||||
styleSheetRuleCb({
|
thisArg.parentStyleSheet,
|
||||||
id,
|
mirror,
|
||||||
styleId,
|
stylesheetManager.styleMirror,
|
||||||
adds: [
|
);
|
||||||
{
|
|
||||||
rule,
|
|
||||||
index: [
|
|
||||||
...getNestedCSSRulePositions(this as CSSRule),
|
|
||||||
index || 0, // defaults to 0
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return unmodifiedFunctions[typeKey].insertRule.apply(this, [rule, index]);
|
|
||||||
};
|
|
||||||
|
|
||||||
type.prototype.deleteRule = function (
|
if ((id && id !== -1) || (styleId && styleId !== -1)) {
|
||||||
this: CSSGroupingRule,
|
styleSheetRuleCb({
|
||||||
index: number,
|
id,
|
||||||
) {
|
styleId,
|
||||||
const { id, styleId } = getIdAndStyleId(
|
adds: [
|
||||||
this.parentStyleSheet,
|
{
|
||||||
mirror,
|
rule,
|
||||||
stylesheetManager.styleMirror,
|
index: [
|
||||||
);
|
...getNestedCSSRulePositions(thisArg),
|
||||||
|
index || 0, // defaults to 0
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return target.apply(thisArg, argumentsList);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
if ((id && id !== -1) || (styleId && styleId !== -1)) {
|
type.prototype.deleteRule = new Proxy(
|
||||||
styleSheetRuleCb({
|
unmodifiedFunctions[typeKey].deleteRule,
|
||||||
id,
|
{
|
||||||
styleId,
|
apply: callbackWrapper(
|
||||||
removes: [
|
(
|
||||||
{ index: [...getNestedCSSRulePositions(this as CSSRule), index] },
|
target: typeof deleteRule,
|
||||||
],
|
thisArg: CSSRule,
|
||||||
});
|
argumentsList: [number],
|
||||||
}
|
) => {
|
||||||
return unmodifiedFunctions[typeKey].deleteRule.apply(this, [index]);
|
const [index] = argumentsList;
|
||||||
};
|
|
||||||
|
const { id, styleId } = getIdAndStyleId(
|
||||||
|
thisArg.parentStyleSheet,
|
||||||
|
mirror,
|
||||||
|
stylesheetManager.styleMirror,
|
||||||
|
);
|
||||||
|
|
||||||
|
if ((id && id !== -1) || (styleId && styleId !== -1)) {
|
||||||
|
styleSheetRuleCb({
|
||||||
|
id,
|
||||||
|
styleId,
|
||||||
|
removes: [
|
||||||
|
{ index: [...getNestedCSSRulePositions(thisArg), index] },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return target.apply(thisArg, argumentsList);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
return () => {
|
return callbackWrapper(() => {
|
||||||
win.CSSStyleSheet.prototype.insertRule = insertRule;
|
win.CSSStyleSheet.prototype.insertRule = insertRule;
|
||||||
win.CSSStyleSheet.prototype.deleteRule = deleteRule;
|
win.CSSStyleSheet.prototype.deleteRule = deleteRule;
|
||||||
replace && (win.CSSStyleSheet.prototype.replace = replace);
|
replace && (win.CSSStyleSheet.prototype.replace = replace);
|
||||||
@@ -738,7 +805,7 @@ function initStyleSheetObserver(
|
|||||||
type.prototype.insertRule = unmodifiedFunctions[typeKey].insertRule;
|
type.prototype.insertRule = unmodifiedFunctions[typeKey].insertRule;
|
||||||
type.prototype.deleteRule = unmodifiedFunctions[typeKey].deleteRule;
|
type.prototype.deleteRule = unmodifiedFunctions[typeKey].deleteRule;
|
||||||
});
|
});
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function initAdoptedStyleSheetObserver(
|
export function initAdoptedStyleSheetObserver(
|
||||||
@@ -792,7 +859,7 @@ export function initAdoptedStyleSheetObserver(
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return () => {
|
return callbackWrapper(() => {
|
||||||
Object.defineProperty(host, 'adoptedStyleSheets', {
|
Object.defineProperty(host, 'adoptedStyleSheets', {
|
||||||
configurable: originalPropertyDescriptor.configurable,
|
configurable: originalPropertyDescriptor.configurable,
|
||||||
enumerable: originalPropertyDescriptor.enumerable,
|
enumerable: originalPropertyDescriptor.enumerable,
|
||||||
@@ -801,7 +868,7 @@ export function initAdoptedStyleSheetObserver(
|
|||||||
// eslint-disable-next-line @typescript-eslint/unbound-method
|
// eslint-disable-next-line @typescript-eslint/unbound-method
|
||||||
set: originalPropertyDescriptor.set,
|
set: originalPropertyDescriptor.set,
|
||||||
});
|
});
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function initStyleDeclarationObserver(
|
function initStyleDeclarationObserver(
|
||||||
@@ -815,70 +882,82 @@ function initStyleDeclarationObserver(
|
|||||||
): listenerHandler {
|
): listenerHandler {
|
||||||
// eslint-disable-next-line @typescript-eslint/unbound-method
|
// eslint-disable-next-line @typescript-eslint/unbound-method
|
||||||
const setProperty = win.CSSStyleDeclaration.prototype.setProperty;
|
const setProperty = win.CSSStyleDeclaration.prototype.setProperty;
|
||||||
win.CSSStyleDeclaration.prototype.setProperty = function (
|
win.CSSStyleDeclaration.prototype.setProperty = new Proxy(setProperty, {
|
||||||
this: CSSStyleDeclaration,
|
apply: callbackWrapper(
|
||||||
property: string,
|
(
|
||||||
value: string,
|
target: typeof setProperty,
|
||||||
priority: string,
|
thisArg: CSSStyleDeclaration,
|
||||||
) {
|
argumentsList: [string, string, string],
|
||||||
// ignore this mutation if we do not care about this css attribute
|
) => {
|
||||||
if (ignoreCSSAttributes.has(property)) {
|
const [property, value, priority] = argumentsList;
|
||||||
return setProperty.apply(this, [property, value, priority]);
|
|
||||||
}
|
// ignore this mutation if we do not care about this css attribute
|
||||||
const { id, styleId } = getIdAndStyleId(
|
if (ignoreCSSAttributes.has(property)) {
|
||||||
this.parentRule?.parentStyleSheet,
|
return setProperty.apply(thisArg, [property, value, priority]);
|
||||||
mirror,
|
}
|
||||||
stylesheetManager.styleMirror,
|
const { id, styleId } = getIdAndStyleId(
|
||||||
);
|
thisArg.parentRule?.parentStyleSheet,
|
||||||
if ((id && id !== -1) || (styleId && styleId !== -1)) {
|
mirror,
|
||||||
styleDeclarationCb({
|
stylesheetManager.styleMirror,
|
||||||
id,
|
);
|
||||||
styleId,
|
if ((id && id !== -1) || (styleId && styleId !== -1)) {
|
||||||
set: {
|
styleDeclarationCb({
|
||||||
property,
|
id,
|
||||||
value,
|
styleId,
|
||||||
priority,
|
set: {
|
||||||
},
|
property,
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
value,
|
||||||
index: getNestedCSSRulePositions(this.parentRule!),
|
priority,
|
||||||
});
|
},
|
||||||
}
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
return setProperty.apply(this, [property, value, priority]);
|
index: getNestedCSSRulePositions(thisArg.parentRule!),
|
||||||
};
|
});
|
||||||
|
}
|
||||||
|
return target.apply(thisArg, argumentsList);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/unbound-method
|
// eslint-disable-next-line @typescript-eslint/unbound-method
|
||||||
const removeProperty = win.CSSStyleDeclaration.prototype.removeProperty;
|
const removeProperty = win.CSSStyleDeclaration.prototype.removeProperty;
|
||||||
win.CSSStyleDeclaration.prototype.removeProperty = function (
|
win.CSSStyleDeclaration.prototype.removeProperty = new Proxy(removeProperty, {
|
||||||
this: CSSStyleDeclaration,
|
apply: callbackWrapper(
|
||||||
property: string,
|
(
|
||||||
) {
|
target: typeof removeProperty,
|
||||||
// ignore this mutation if we do not care about this css attribute
|
thisArg: CSSStyleDeclaration,
|
||||||
if (ignoreCSSAttributes.has(property)) {
|
argumentsList: [string],
|
||||||
return removeProperty.apply(this, [property]);
|
) => {
|
||||||
}
|
const [property] = argumentsList;
|
||||||
const { id, styleId } = getIdAndStyleId(
|
|
||||||
this.parentRule?.parentStyleSheet,
|
|
||||||
mirror,
|
|
||||||
stylesheetManager.styleMirror,
|
|
||||||
);
|
|
||||||
if ((id && id !== -1) || (styleId && styleId !== -1)) {
|
|
||||||
styleDeclarationCb({
|
|
||||||
id,
|
|
||||||
styleId,
|
|
||||||
remove: {
|
|
||||||
property,
|
|
||||||
},
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
||||||
index: getNestedCSSRulePositions(this.parentRule!),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return removeProperty.apply(this, [property]);
|
|
||||||
};
|
|
||||||
|
|
||||||
return () => {
|
// ignore this mutation if we do not care about this css attribute
|
||||||
|
if (ignoreCSSAttributes.has(property)) {
|
||||||
|
return removeProperty.apply(thisArg, [property]);
|
||||||
|
}
|
||||||
|
const { id, styleId } = getIdAndStyleId(
|
||||||
|
thisArg.parentRule?.parentStyleSheet,
|
||||||
|
mirror,
|
||||||
|
stylesheetManager.styleMirror,
|
||||||
|
);
|
||||||
|
if ((id && id !== -1) || (styleId && styleId !== -1)) {
|
||||||
|
styleDeclarationCb({
|
||||||
|
id,
|
||||||
|
styleId,
|
||||||
|
remove: {
|
||||||
|
property,
|
||||||
|
},
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
|
index: getNestedCSSRulePositions(thisArg.parentRule!),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return target.apply(thisArg, argumentsList);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
return callbackWrapper(() => {
|
||||||
win.CSSStyleDeclaration.prototype.setProperty = setProperty;
|
win.CSSStyleDeclaration.prototype.setProperty = setProperty;
|
||||||
win.CSSStyleDeclaration.prototype.removeProperty = removeProperty;
|
win.CSSStyleDeclaration.prototype.removeProperty = removeProperty;
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function initMediaInteractionObserver({
|
function initMediaInteractionObserver({
|
||||||
@@ -888,26 +967,30 @@ function initMediaInteractionObserver({
|
|||||||
mirror,
|
mirror,
|
||||||
sampling,
|
sampling,
|
||||||
}: observerParam): listenerHandler {
|
}: observerParam): listenerHandler {
|
||||||
const handler = (type: MediaInteractions) =>
|
const handler = callbackWrapper((type: MediaInteractions) =>
|
||||||
throttle((event: Event) => {
|
throttle(
|
||||||
const target = getEventTarget(event);
|
callbackWrapper((event: Event) => {
|
||||||
if (
|
const target = getEventTarget(event);
|
||||||
!target ||
|
if (
|
||||||
isBlocked(target as Node, blockClass, blockSelector, true)
|
!target ||
|
||||||
) {
|
isBlocked(target as Node, blockClass, blockSelector, true)
|
||||||
return;
|
) {
|
||||||
}
|
return;
|
||||||
const { currentTime, volume, muted, playbackRate } =
|
}
|
||||||
target as HTMLMediaElement;
|
const { currentTime, volume, muted, playbackRate } =
|
||||||
mediaInteractionCb({
|
target as HTMLMediaElement;
|
||||||
type,
|
mediaInteractionCb({
|
||||||
id: mirror.getId(target as Node),
|
type,
|
||||||
currentTime,
|
id: mirror.getId(target as Node),
|
||||||
volume,
|
currentTime,
|
||||||
muted,
|
volume,
|
||||||
playbackRate,
|
muted,
|
||||||
});
|
playbackRate,
|
||||||
}, sampling.media || 500);
|
});
|
||||||
|
}),
|
||||||
|
sampling.media || 500,
|
||||||
|
),
|
||||||
|
);
|
||||||
const handlers = [
|
const handlers = [
|
||||||
on('play', handler(MediaInteractions.Play)),
|
on('play', handler(MediaInteractions.Play)),
|
||||||
on('pause', handler(MediaInteractions.Pause)),
|
on('pause', handler(MediaInteractions.Pause)),
|
||||||
@@ -915,9 +998,9 @@ function initMediaInteractionObserver({
|
|||||||
on('volumechange', handler(MediaInteractions.VolumeChange)),
|
on('volumechange', handler(MediaInteractions.VolumeChange)),
|
||||||
on('ratechange', handler(MediaInteractions.RateChange)),
|
on('ratechange', handler(MediaInteractions.RateChange)),
|
||||||
];
|
];
|
||||||
return () => {
|
return callbackWrapper(() => {
|
||||||
handlers.forEach((h) => h());
|
handlers.forEach((h) => h());
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function initFontObserver({ fontCb, doc }: observerParam): listenerHandler {
|
function initFontObserver({ fontCb, doc }: observerParam): listenerHandler {
|
||||||
@@ -956,13 +1039,16 @@ function initFontObserver({ fontCb, doc }: observerParam): listenerHandler {
|
|||||||
'add',
|
'add',
|
||||||
function (original: (font: FontFace) => void) {
|
function (original: (font: FontFace) => void) {
|
||||||
return function (this: FontFaceSet, fontFace: FontFace) {
|
return function (this: FontFaceSet, fontFace: FontFace) {
|
||||||
setTimeout(() => {
|
setTimeout(
|
||||||
const p = fontMap.get(fontFace);
|
callbackWrapper(() => {
|
||||||
if (p) {
|
const p = fontMap.get(fontFace);
|
||||||
fontCb(p);
|
if (p) {
|
||||||
fontMap.delete(fontFace);
|
fontCb(p);
|
||||||
}
|
fontMap.delete(fontFace);
|
||||||
}, 0);
|
}
|
||||||
|
}),
|
||||||
|
0,
|
||||||
|
);
|
||||||
return original.apply(this, [fontFace]);
|
return original.apply(this, [fontFace]);
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@@ -973,16 +1059,16 @@ function initFontObserver({ fontCb, doc }: observerParam): listenerHandler {
|
|||||||
});
|
});
|
||||||
handlers.push(restoreHandler);
|
handlers.push(restoreHandler);
|
||||||
|
|
||||||
return () => {
|
return callbackWrapper(() => {
|
||||||
handlers.forEach((h) => h());
|
handlers.forEach((h) => h());
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function initSelectionObserver(param: observerParam): listenerHandler {
|
function initSelectionObserver(param: observerParam): listenerHandler {
|
||||||
const { doc, mirror, blockClass, blockSelector, selectionCb } = param;
|
const { doc, mirror, blockClass, blockSelector, selectionCb } = param;
|
||||||
let collapsed = true;
|
let collapsed = true;
|
||||||
|
|
||||||
const updateSelection = () => {
|
const updateSelection = callbackWrapper(() => {
|
||||||
const selection = doc.getSelection();
|
const selection = doc.getSelection();
|
||||||
|
|
||||||
if (!selection || (collapsed && selection?.isCollapsed)) return;
|
if (!selection || (collapsed && selection?.isCollapsed)) return;
|
||||||
@@ -1012,7 +1098,7 @@ function initSelectionObserver(param: observerParam): listenerHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
selectionCb({ ranges });
|
selectionCb({ ranges });
|
||||||
};
|
});
|
||||||
|
|
||||||
updateSelection();
|
updateSelection();
|
||||||
|
|
||||||
@@ -1148,7 +1234,7 @@ export function initObservers(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return callbackWrapper(() => {
|
||||||
mutationBuffers.forEach((b) => b.reset());
|
mutationBuffers.forEach((b) => b.reset());
|
||||||
mutationObserver.disconnect();
|
mutationObserver.disconnect();
|
||||||
mousemoveHandler();
|
mousemoveHandler();
|
||||||
@@ -1163,7 +1249,7 @@ export function initObservers(
|
|||||||
fontObserver();
|
fontObserver();
|
||||||
selectionObserver();
|
selectionObserver();
|
||||||
pluginHandlers.forEach((h) => h());
|
pluginHandlers.forEach((h) => h());
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
type CSSGroupingProp =
|
type CSSGroupingProp =
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ export type recordOptions<T> = {
|
|||||||
// departed, please use sampling options
|
// departed, please use sampling options
|
||||||
mousemoveWait?: number;
|
mousemoveWait?: number;
|
||||||
keepIframeSrcFn?: KeepIframeSrcFn;
|
keepIframeSrcFn?: KeepIframeSrcFn;
|
||||||
|
errorHandler?: ErrorHandler;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type observerParam = {
|
export type observerParam = {
|
||||||
@@ -211,3 +212,5 @@ export type CrossOriginIframeMessageEventContent<T = eventWithTime> = {
|
|||||||
};
|
};
|
||||||
export type CrossOriginIframeMessageEvent =
|
export type CrossOriginIframeMessageEvent =
|
||||||
MessageEvent<CrossOriginIframeMessageEventContent>;
|
MessageEvent<CrossOriginIframeMessageEventContent>;
|
||||||
|
|
||||||
|
export type ErrorHandler = (error: unknown) => void | boolean;
|
||||||
|
|||||||
468
packages/rrweb/test/record/error-handler.test.ts
Normal file
468
packages/rrweb/test/record/error-handler.test.ts
Normal file
@@ -0,0 +1,468 @@
|
|||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import type * as puppeteer from 'puppeteer';
|
||||||
|
import type { recordOptions } from '../../src/types';
|
||||||
|
import { listenerHandler, eventWithTime, EventType } from '@rrweb/types';
|
||||||
|
import { launchPuppeteer } from '../utils';
|
||||||
|
import {
|
||||||
|
callbackWrapper,
|
||||||
|
registerErrorHandler,
|
||||||
|
unregisterErrorHandler,
|
||||||
|
} from '../../src/record/error-handler';
|
||||||
|
|
||||||
|
interface ISuite {
|
||||||
|
code: string;
|
||||||
|
browser: puppeteer.Browser;
|
||||||
|
page: puppeteer.Page;
|
||||||
|
events: eventWithTime[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IWindow extends Window {
|
||||||
|
rrweb: {
|
||||||
|
record: (
|
||||||
|
options: recordOptions<eventWithTime>,
|
||||||
|
) => listenerHandler | undefined;
|
||||||
|
addCustomEvent<T>(tag: string, payload: T): void;
|
||||||
|
};
|
||||||
|
emit: (e: eventWithTime) => undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const setup = function (
|
||||||
|
this: ISuite,
|
||||||
|
content: string,
|
||||||
|
canvasSample: 'all' | number = 'all',
|
||||||
|
): ISuite {
|
||||||
|
const ctx = {} as ISuite;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
ctx.browser = await launchPuppeteer();
|
||||||
|
|
||||||
|
const bundlePath = path.resolve(__dirname, '../../dist/rrweb.js');
|
||||||
|
ctx.code = fs.readFileSync(bundlePath, 'utf8');
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
ctx.page = await ctx.browser.newPage();
|
||||||
|
await ctx.page.goto('about:blank');
|
||||||
|
await ctx.page.setContent(content);
|
||||||
|
await ctx.page.evaluate(ctx.code);
|
||||||
|
ctx.events = [];
|
||||||
|
await ctx.page.exposeFunction('emit', (e: eventWithTime) => {
|
||||||
|
if (e.type === EventType.DomContentLoaded || e.type === EventType.Load) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ctx.events.push(e);
|
||||||
|
});
|
||||||
|
|
||||||
|
ctx.page.on('console', (msg) => console.log('PAGE LOG:', msg.text()));
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await ctx.page.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await ctx.browser.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
return ctx;
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('error-handler', function (this: ISuite) {
|
||||||
|
jest.setTimeout(100_000);
|
||||||
|
|
||||||
|
const ctx: ISuite = setup.call(
|
||||||
|
this,
|
||||||
|
`
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<style>
|
||||||
|
body { background: red; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id='in'></div>
|
||||||
|
<div id='out'></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`,
|
||||||
|
);
|
||||||
|
|
||||||
|
describe('CSSStyleSheet.prototype', () => {
|
||||||
|
it('triggers for errors from insertRule', async () => {
|
||||||
|
await ctx.page.evaluate(() => {
|
||||||
|
// @ts-ignore rewrite this to something buggy
|
||||||
|
window.CSSStyleSheet.prototype.insertRule = function () {
|
||||||
|
// @ts-ignore
|
||||||
|
window.doSomethingWrong();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
await ctx.page.evaluate(() => {
|
||||||
|
const { record } = (window as unknown as IWindow).rrweb;
|
||||||
|
record({
|
||||||
|
errorHandler: (error) => {
|
||||||
|
document.getElementById('out')!.innerText = `${error}`;
|
||||||
|
},
|
||||||
|
emit: (window as unknown as IWindow).emit,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Trigger buggy style sheet insert
|
||||||
|
setTimeout(() => {
|
||||||
|
// @ts-ignore
|
||||||
|
document.styleSheets[0].insertRule('body { background: blue; }');
|
||||||
|
}, 50);
|
||||||
|
});
|
||||||
|
|
||||||
|
await ctx.page.waitForTimeout(100);
|
||||||
|
|
||||||
|
const element = await ctx.page.$('#out');
|
||||||
|
const text = await element!.evaluate((el) => el.textContent);
|
||||||
|
|
||||||
|
expect(text).toEqual(
|
||||||
|
'TypeError: window.doSomethingWrong is not a function',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('triggers for errors from deleteRule', async () => {
|
||||||
|
await ctx.page.evaluate(() => {
|
||||||
|
// @ts-ignore rewrite this to something buggy
|
||||||
|
window.CSSStyleSheet.prototype.deleteRule = function () {
|
||||||
|
// @ts-ignore
|
||||||
|
window.doSomethingWrong();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
await ctx.page.evaluate(() => {
|
||||||
|
const { record } = (window as unknown as IWindow).rrweb;
|
||||||
|
record({
|
||||||
|
errorHandler: (error) => {
|
||||||
|
document.getElementById('out')!.innerText = `${error}`;
|
||||||
|
},
|
||||||
|
emit: (window as unknown as IWindow).emit,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Trigger buggy style sheet delete
|
||||||
|
setTimeout(() => {
|
||||||
|
document.styleSheets[0].deleteRule(0);
|
||||||
|
}, 50);
|
||||||
|
});
|
||||||
|
|
||||||
|
await ctx.page.waitForTimeout(100);
|
||||||
|
|
||||||
|
const element = await ctx.page.$('#out');
|
||||||
|
const text = await element!.evaluate((el) => el.textContent);
|
||||||
|
|
||||||
|
expect(text).toEqual(
|
||||||
|
'TypeError: window.doSomethingWrong is not a function',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('triggers for errors from replace', async () => {
|
||||||
|
await ctx.page.evaluate(() => {
|
||||||
|
// @ts-ignore rewrite this to something buggy
|
||||||
|
window.CSSStyleSheet.prototype.replace = function () {
|
||||||
|
// @ts-ignore
|
||||||
|
window.doSomethingWrong();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
await ctx.page.evaluate(() => {
|
||||||
|
const { record } = (window as unknown as IWindow).rrweb;
|
||||||
|
record({
|
||||||
|
errorHandler: (error) => {
|
||||||
|
document.getElementById('out')!.innerText = `${error}`;
|
||||||
|
},
|
||||||
|
emit: (window as unknown as IWindow).emit,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Trigger buggy style sheet insert
|
||||||
|
setTimeout(() => {
|
||||||
|
// @ts-ignore
|
||||||
|
document.styleSheets[0].replace('body { background: blue; }');
|
||||||
|
}, 50);
|
||||||
|
});
|
||||||
|
|
||||||
|
await ctx.page.waitForTimeout(100);
|
||||||
|
|
||||||
|
const element = await ctx.page.$('#out');
|
||||||
|
const text = await element!.evaluate((el) => el.textContent);
|
||||||
|
|
||||||
|
expect(text).toEqual(
|
||||||
|
'TypeError: window.doSomethingWrong is not a function',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('triggers for errors from replaceSync', async () => {
|
||||||
|
await ctx.page.evaluate(() => {
|
||||||
|
// @ts-ignore rewrite this to something buggy
|
||||||
|
window.CSSStyleSheet.prototype.replaceSync = function () {
|
||||||
|
// @ts-ignore
|
||||||
|
window.doSomethingWrong();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
await ctx.page.evaluate(() => {
|
||||||
|
const { record } = (window as unknown as IWindow).rrweb;
|
||||||
|
record({
|
||||||
|
errorHandler: (error) => {
|
||||||
|
document.getElementById('out')!.innerText = `${error}`;
|
||||||
|
},
|
||||||
|
emit: (window as unknown as IWindow).emit,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Trigger buggy style sheet insert
|
||||||
|
setTimeout(() => {
|
||||||
|
// @ts-ignore
|
||||||
|
document.styleSheets[0].replaceSync('body { background: blue; }');
|
||||||
|
}, 50);
|
||||||
|
});
|
||||||
|
|
||||||
|
await ctx.page.waitForTimeout(100);
|
||||||
|
|
||||||
|
const element = await ctx.page.$('#out');
|
||||||
|
const text = await element!.evaluate((el) => el.textContent);
|
||||||
|
|
||||||
|
expect(text).toEqual(
|
||||||
|
'TypeError: window.doSomethingWrong is not a function',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('triggers for errors from CSSGroupingRule.insertRule', async () => {
|
||||||
|
await ctx.page.evaluate(() => {
|
||||||
|
// @ts-ignore rewrite this to something buggy
|
||||||
|
window.CSSGroupingRule.prototype.insertRule = function () {
|
||||||
|
// @ts-ignore
|
||||||
|
window.doSomethingWrong();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
await ctx.page.evaluate(() => {
|
||||||
|
const { record } = (window as unknown as IWindow).rrweb;
|
||||||
|
record({
|
||||||
|
errorHandler: (error) => {
|
||||||
|
document.getElementById('out')!.innerText = `${error}`;
|
||||||
|
},
|
||||||
|
emit: (window as unknown as IWindow).emit,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Trigger buggy style sheet insert
|
||||||
|
setTimeout(() => {
|
||||||
|
document.styleSheets[0].insertRule('@media {}');
|
||||||
|
const atMediaRule = document.styleSheets[0]
|
||||||
|
.cssRules[0] as CSSMediaRule;
|
||||||
|
|
||||||
|
const ruleIdx0 = atMediaRule.insertRule(
|
||||||
|
'body { background: #000; }',
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
}, 50);
|
||||||
|
});
|
||||||
|
|
||||||
|
await ctx.page.waitForTimeout(100);
|
||||||
|
|
||||||
|
const element = await ctx.page.$('#out');
|
||||||
|
const text = await element!.evaluate((el) => el.textContent);
|
||||||
|
|
||||||
|
expect(text).toEqual(
|
||||||
|
'TypeError: window.doSomethingWrong is not a function',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('triggers for errors from CSSGroupingRule.deleteRule', async () => {
|
||||||
|
await ctx.page.evaluate(() => {
|
||||||
|
// @ts-ignore rewrite this to something buggy
|
||||||
|
window.CSSGroupingRule.prototype.deleteRule = function () {
|
||||||
|
// @ts-ignore
|
||||||
|
window.doSomethingWrong();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
await ctx.page.evaluate(() => {
|
||||||
|
const { record } = (window as unknown as IWindow).rrweb;
|
||||||
|
record({
|
||||||
|
errorHandler: (error) => {
|
||||||
|
document.getElementById('out')!.innerText = `${error}`;
|
||||||
|
},
|
||||||
|
emit: (window as unknown as IWindow).emit,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Trigger buggy style sheet delete
|
||||||
|
setTimeout(() => {
|
||||||
|
document.styleSheets[0].insertRule('@media {}');
|
||||||
|
const atMediaRule = document.styleSheets[0]
|
||||||
|
.cssRules[0] as CSSMediaRule;
|
||||||
|
|
||||||
|
const ruleIdx0 = atMediaRule.deleteRule(0);
|
||||||
|
}, 50);
|
||||||
|
});
|
||||||
|
|
||||||
|
await ctx.page.waitForTimeout(100);
|
||||||
|
|
||||||
|
const element = await ctx.page.$('#out');
|
||||||
|
const text = await element!.evaluate((el) => el.textContent);
|
||||||
|
|
||||||
|
expect(text).toEqual(
|
||||||
|
'TypeError: window.doSomethingWrong is not a function',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('triggers for errors from CSSStyleDeclaration.setProperty', async () => {
|
||||||
|
await ctx.page.evaluate(() => {
|
||||||
|
// @ts-ignore rewrite this to something buggy
|
||||||
|
window.CSSStyleDeclaration.prototype.setProperty = function () {
|
||||||
|
// @ts-ignore
|
||||||
|
window.doSomethingWrong();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
await ctx.page.evaluate(() => {
|
||||||
|
const { record } = (window as unknown as IWindow).rrweb;
|
||||||
|
record({
|
||||||
|
errorHandler: (error) => {
|
||||||
|
document.getElementById('out')!.innerText = `${error}`;
|
||||||
|
},
|
||||||
|
emit: (window as unknown as IWindow).emit,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Trigger buggy style sheet insert
|
||||||
|
setTimeout(() => {
|
||||||
|
(
|
||||||
|
document.styleSheets[0].cssRules[0] as unknown as {
|
||||||
|
style: CSSStyleDeclaration;
|
||||||
|
}
|
||||||
|
).style.setProperty('background', 'blue');
|
||||||
|
}, 50);
|
||||||
|
});
|
||||||
|
|
||||||
|
await ctx.page.waitForTimeout(100);
|
||||||
|
|
||||||
|
const element = await ctx.page.$('#out');
|
||||||
|
const text = await element!.evaluate((el) => el.textContent);
|
||||||
|
|
||||||
|
expect(text).toEqual(
|
||||||
|
'TypeError: window.doSomethingWrong is not a function',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('triggers for errors from CSSStyleDeclaration.removeProperty', async () => {
|
||||||
|
await ctx.page.evaluate(() => {
|
||||||
|
// @ts-ignore rewrite this to something buggy
|
||||||
|
window.CSSStyleDeclaration.prototype.removeProperty = function () {
|
||||||
|
// @ts-ignore
|
||||||
|
window.doSomethingWrong();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
await ctx.page.evaluate(() => {
|
||||||
|
const { record } = (window as unknown as IWindow).rrweb;
|
||||||
|
record({
|
||||||
|
errorHandler: (error) => {
|
||||||
|
document.getElementById('out')!.innerText = `${error}`;
|
||||||
|
},
|
||||||
|
emit: (window as unknown as IWindow).emit,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Trigger buggy style sheet insert
|
||||||
|
setTimeout(() => {
|
||||||
|
(
|
||||||
|
document.styleSheets[0].cssRules[0] as unknown as {
|
||||||
|
style: CSSStyleDeclaration;
|
||||||
|
}
|
||||||
|
).style.removeProperty('background');
|
||||||
|
}, 50);
|
||||||
|
});
|
||||||
|
|
||||||
|
await ctx.page.waitForTimeout(100);
|
||||||
|
|
||||||
|
const element = await ctx.page.$('#out');
|
||||||
|
const text = await element!.evaluate((el) => el.textContent);
|
||||||
|
|
||||||
|
expect(text).toEqual(
|
||||||
|
'TypeError: window.doSomethingWrong is not a function',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('triggers for errors from mutation observer', async () => {
|
||||||
|
await ctx.page.evaluate(() => {
|
||||||
|
const { record } = (window as unknown as IWindow).rrweb;
|
||||||
|
record({
|
||||||
|
errorHandler: (error) => {
|
||||||
|
document.getElementById('out')!.innerText = `${error}`;
|
||||||
|
},
|
||||||
|
emit: (window as unknown as IWindow).emit,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Trigger buggy mutation observer
|
||||||
|
setTimeout(() => {
|
||||||
|
const el = document.getElementById('in')!;
|
||||||
|
|
||||||
|
// @ts-ignore we want to trigger an error in the mutation observer, which uses this
|
||||||
|
el.getAttribute = undefined;
|
||||||
|
|
||||||
|
el.setAttribute('data-attr', 'new');
|
||||||
|
}, 50);
|
||||||
|
});
|
||||||
|
|
||||||
|
await ctx.page.waitForTimeout(100);
|
||||||
|
|
||||||
|
const element = await ctx.page.$('#out');
|
||||||
|
const text = await element!.evaluate((el) => el.textContent);
|
||||||
|
|
||||||
|
expect(text).toEqual('TypeError: m.target.getAttribute is not a function');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('errorHandler unit', function () {
|
||||||
|
afterEach(function () {
|
||||||
|
unregisterErrorHandler();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not swallow if no errorHandler set', () => {
|
||||||
|
unregisterErrorHandler();
|
||||||
|
|
||||||
|
const wrapped = callbackWrapper(() => {
|
||||||
|
throw new Error('test');
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(() => wrapped()).toThrowError('test');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not swallow if errorHandler returns void', () => {
|
||||||
|
registerErrorHandler(() => {
|
||||||
|
// do nothing
|
||||||
|
});
|
||||||
|
|
||||||
|
const wrapped = callbackWrapper(() => {
|
||||||
|
throw new Error('test');
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(() => wrapped()).toThrowError('test');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not swallow if errorHandler returns false', () => {
|
||||||
|
registerErrorHandler(() => {
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
const wrapped = callbackWrapper(() => {
|
||||||
|
throw new Error('test');
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(() => wrapped()).toThrowError('test');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('swallows if errorHandler returns true', () => {
|
||||||
|
registerErrorHandler(() => {
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
const wrapped = callbackWrapper(() => {
|
||||||
|
throw new Error('test');
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(() => wrapped()).not.toThrowError('test');
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user