381 lines
9.8 KiB
TypeScript
381 lines
9.8 KiB
TypeScript
import { snapshot, MaskInputOptions, SlimDOMOptions } from 'rrweb-snapshot';
|
|
import { initObservers, mutationBuffer } from './observer';
|
|
import {
|
|
mirror,
|
|
on,
|
|
getWindowWidth,
|
|
getWindowHeight,
|
|
polyfill,
|
|
} from '../utils';
|
|
import {
|
|
EventType,
|
|
event,
|
|
eventWithTime,
|
|
recordOptions,
|
|
IncrementalSource,
|
|
listenerHandler,
|
|
} from '../types';
|
|
|
|
function wrapEvent(e: event): eventWithTime {
|
|
return {
|
|
...e,
|
|
timestamp: Date.now(),
|
|
};
|
|
}
|
|
|
|
let wrappedEmit!: (e: eventWithTime, isCheckout?: boolean) => void;
|
|
|
|
function record<T = eventWithTime>(
|
|
options: recordOptions<T> = {},
|
|
): listenerHandler | undefined {
|
|
const {
|
|
emit,
|
|
checkoutEveryNms,
|
|
checkoutEveryNth,
|
|
blockClass = 'rr-block',
|
|
blockSelector = null,
|
|
ignoreClass = 'rr-ignore',
|
|
inlineStylesheet = true,
|
|
maskAllInputs,
|
|
maskInputOptions: _maskInputOptions,
|
|
slimDOMOptions: _slimDOMOptions,
|
|
maskInputFn,
|
|
hooks,
|
|
packFn,
|
|
sampling = {},
|
|
mousemoveWait,
|
|
recordCanvas = false,
|
|
collectFonts = false,
|
|
} = options;
|
|
// runtime checks for user options
|
|
if (!emit) {
|
|
throw new Error('emit function is required');
|
|
}
|
|
// move departed options to new options
|
|
if (mousemoveWait !== undefined && sampling.mousemove === undefined) {
|
|
sampling.mousemove = mousemoveWait;
|
|
}
|
|
|
|
const maskInputOptions: MaskInputOptions =
|
|
maskAllInputs === true
|
|
? {
|
|
color: true,
|
|
date: true,
|
|
'datetime-local': true,
|
|
email: true,
|
|
month: true,
|
|
number: true,
|
|
range: true,
|
|
search: true,
|
|
tel: true,
|
|
text: true,
|
|
time: true,
|
|
url: true,
|
|
week: true,
|
|
textarea: true,
|
|
select: true,
|
|
}
|
|
: _maskInputOptions !== undefined
|
|
? _maskInputOptions
|
|
: {};
|
|
|
|
const slimDOMOptions: SlimDOMOptions =
|
|
_slimDOMOptions === true || _slimDOMOptions === 'all'
|
|
? {
|
|
script: true,
|
|
comment: true,
|
|
headFavicon: true,
|
|
headWhitespace: true,
|
|
headMetaSocial: true,
|
|
headMetaRobots: true,
|
|
headMetaHttpEquiv: true,
|
|
headMetaVerification: true,
|
|
// the following are off for slimDOMOptions === true,
|
|
// as they destroy some (hidden) info:
|
|
headMetaAuthorship: _slimDOMOptions === 'all',
|
|
headMetaDescKeywords: _slimDOMOptions === 'all',
|
|
}
|
|
: _slimDOMOptions === false
|
|
? {}
|
|
: _slimDOMOptions;
|
|
|
|
polyfill();
|
|
|
|
let lastFullSnapshotEvent: eventWithTime;
|
|
let incrementalSnapshotCount = 0;
|
|
wrappedEmit = (e: eventWithTime, isCheckout?: boolean) => {
|
|
if (
|
|
mutationBuffer.isFrozen() &&
|
|
e.type !== EventType.FullSnapshot &&
|
|
!(
|
|
e.type === EventType.IncrementalSnapshot &&
|
|
e.data.source === IncrementalSource.Mutation
|
|
)
|
|
) {
|
|
// we've got a user initiated event so first we need to apply
|
|
// all DOM changes that have been buffering during paused state
|
|
mutationBuffer.emit();
|
|
mutationBuffer.unfreeze();
|
|
}
|
|
|
|
emit(((packFn ? packFn(e) : e) as unknown) as T, isCheckout);
|
|
if (e.type === EventType.FullSnapshot) {
|
|
lastFullSnapshotEvent = e;
|
|
incrementalSnapshotCount = 0;
|
|
} else if (e.type === EventType.IncrementalSnapshot) {
|
|
incrementalSnapshotCount++;
|
|
const exceedCount =
|
|
checkoutEveryNth && incrementalSnapshotCount >= checkoutEveryNth;
|
|
const exceedTime =
|
|
checkoutEveryNms &&
|
|
e.timestamp - lastFullSnapshotEvent.timestamp > checkoutEveryNms;
|
|
if (exceedCount || exceedTime) {
|
|
takeFullSnapshot(true);
|
|
}
|
|
}
|
|
};
|
|
|
|
function takeFullSnapshot(isCheckout = false) {
|
|
wrappedEmit(
|
|
wrapEvent({
|
|
type: EventType.Meta,
|
|
data: {
|
|
href: window.location.href,
|
|
width: getWindowWidth(),
|
|
height: getWindowHeight(),
|
|
},
|
|
}),
|
|
isCheckout,
|
|
);
|
|
|
|
let wasFrozen = mutationBuffer.isFrozen();
|
|
mutationBuffer.freeze(); // don't allow any mirror modifications during snapshotting
|
|
const [node, idNodeMap] = snapshot(document, {
|
|
blockClass,
|
|
blockSelector,
|
|
inlineStylesheet,
|
|
maskAllInputs: maskInputOptions,
|
|
slimDOM: slimDOMOptions,
|
|
recordCanvas,
|
|
});
|
|
|
|
if (!node) {
|
|
return console.warn('Failed to snapshot the document');
|
|
}
|
|
|
|
mirror.map = idNodeMap;
|
|
wrappedEmit(
|
|
wrapEvent({
|
|
type: EventType.FullSnapshot,
|
|
data: {
|
|
node,
|
|
initialOffset: {
|
|
left:
|
|
window.pageXOffset !== undefined
|
|
? window.pageXOffset
|
|
: document?.documentElement.scrollLeft ||
|
|
document?.body?.parentElement?.scrollLeft ||
|
|
document?.body.scrollLeft ||
|
|
0,
|
|
top:
|
|
window.pageYOffset !== undefined
|
|
? window.pageYOffset
|
|
: document?.documentElement.scrollTop ||
|
|
document?.body?.parentElement?.scrollTop ||
|
|
document?.body.scrollTop ||
|
|
0,
|
|
},
|
|
},
|
|
}),
|
|
);
|
|
if (!wasFrozen) {
|
|
mutationBuffer.emit(); // emit anything queued up now
|
|
mutationBuffer.unfreeze();
|
|
}
|
|
}
|
|
|
|
try {
|
|
const handlers: listenerHandler[] = [];
|
|
handlers.push(
|
|
on('DOMContentLoaded', () => {
|
|
wrappedEmit(
|
|
wrapEvent({
|
|
type: EventType.DomContentLoaded,
|
|
data: {},
|
|
}),
|
|
);
|
|
}),
|
|
);
|
|
const init = () => {
|
|
takeFullSnapshot();
|
|
|
|
handlers.push(
|
|
initObservers(
|
|
{
|
|
mutationCb: (m) =>
|
|
wrappedEmit(
|
|
wrapEvent({
|
|
type: EventType.IncrementalSnapshot,
|
|
data: {
|
|
source: IncrementalSource.Mutation,
|
|
...m,
|
|
},
|
|
}),
|
|
),
|
|
mousemoveCb: (positions, source) =>
|
|
wrappedEmit(
|
|
wrapEvent({
|
|
type: EventType.IncrementalSnapshot,
|
|
data: {
|
|
source,
|
|
positions,
|
|
},
|
|
}),
|
|
),
|
|
mouseInteractionCb: (d) =>
|
|
wrappedEmit(
|
|
wrapEvent({
|
|
type: EventType.IncrementalSnapshot,
|
|
data: {
|
|
source: IncrementalSource.MouseInteraction,
|
|
...d,
|
|
},
|
|
}),
|
|
),
|
|
scrollCb: (p) =>
|
|
wrappedEmit(
|
|
wrapEvent({
|
|
type: EventType.IncrementalSnapshot,
|
|
data: {
|
|
source: IncrementalSource.Scroll,
|
|
...p,
|
|
},
|
|
}),
|
|
),
|
|
viewportResizeCb: (d) =>
|
|
wrappedEmit(
|
|
wrapEvent({
|
|
type: EventType.IncrementalSnapshot,
|
|
data: {
|
|
source: IncrementalSource.ViewportResize,
|
|
...d,
|
|
},
|
|
}),
|
|
),
|
|
inputCb: (v) =>
|
|
wrappedEmit(
|
|
wrapEvent({
|
|
type: EventType.IncrementalSnapshot,
|
|
data: {
|
|
source: IncrementalSource.Input,
|
|
...v,
|
|
},
|
|
}),
|
|
),
|
|
mediaInteractionCb: (p) =>
|
|
wrappedEmit(
|
|
wrapEvent({
|
|
type: EventType.IncrementalSnapshot,
|
|
data: {
|
|
source: IncrementalSource.MediaInteraction,
|
|
...p,
|
|
},
|
|
}),
|
|
),
|
|
styleSheetRuleCb: (r) =>
|
|
wrappedEmit(
|
|
wrapEvent({
|
|
type: EventType.IncrementalSnapshot,
|
|
data: {
|
|
source: IncrementalSource.StyleSheetRule,
|
|
...r,
|
|
},
|
|
}),
|
|
),
|
|
canvasMutationCb: (p) =>
|
|
wrappedEmit(
|
|
wrapEvent({
|
|
type: EventType.IncrementalSnapshot,
|
|
data: {
|
|
source: IncrementalSource.CanvasMutation,
|
|
...p,
|
|
},
|
|
}),
|
|
),
|
|
fontCb: (p) =>
|
|
wrappedEmit(
|
|
wrapEvent({
|
|
type: EventType.IncrementalSnapshot,
|
|
data: {
|
|
source: IncrementalSource.Font,
|
|
...p,
|
|
},
|
|
}),
|
|
),
|
|
blockClass,
|
|
blockSelector,
|
|
ignoreClass,
|
|
maskInputOptions,
|
|
maskInputFn,
|
|
inlineStylesheet,
|
|
sampling,
|
|
recordCanvas,
|
|
collectFonts,
|
|
slimDOMOptions,
|
|
},
|
|
hooks,
|
|
),
|
|
);
|
|
};
|
|
if (
|
|
document.readyState === 'interactive' ||
|
|
document.readyState === 'complete'
|
|
) {
|
|
init();
|
|
} else {
|
|
handlers.push(
|
|
on(
|
|
'load',
|
|
() => {
|
|
wrappedEmit(
|
|
wrapEvent({
|
|
type: EventType.Load,
|
|
data: {},
|
|
}),
|
|
);
|
|
init();
|
|
},
|
|
window,
|
|
),
|
|
);
|
|
}
|
|
return () => {
|
|
handlers.forEach((h) => h());
|
|
};
|
|
} catch (error) {
|
|
// TODO: handle internal error
|
|
console.warn(error);
|
|
}
|
|
}
|
|
|
|
record.addCustomEvent = <T>(tag: string, payload: T) => {
|
|
if (!wrappedEmit) {
|
|
throw new Error('please add custom event after start recording');
|
|
}
|
|
wrappedEmit(
|
|
wrapEvent({
|
|
type: EventType.Custom,
|
|
data: {
|
|
tag,
|
|
payload,
|
|
},
|
|
}),
|
|
);
|
|
};
|
|
|
|
record.freezePage = () => {
|
|
mutationBuffer.freeze();
|
|
};
|
|
|
|
export default record;
|