* fix the statement which is getting changed by microbundle * Create chatty-cherries-train.md * fix formatting. * fix position of comment --------- Co-authored-by: Justin Halsall <Juice10@users.noreply.github.com>
648 lines
16 KiB
TypeScript
648 lines
16 KiB
TypeScript
import {
|
|
snapshot,
|
|
MaskInputOptions,
|
|
SlimDOMOptions,
|
|
createMirror,
|
|
} from 'rrweb-snapshot';
|
|
import {
|
|
initObservers,
|
|
mutationBuffers,
|
|
processedNodeManager,
|
|
} from './observer';
|
|
import {
|
|
on,
|
|
getWindowWidth,
|
|
getWindowHeight,
|
|
getWindowScroll,
|
|
polyfill,
|
|
hasShadowRoot,
|
|
isSerializedIframe,
|
|
isSerializedStylesheet,
|
|
} from '../utils';
|
|
import type { recordOptions } from '../types';
|
|
import {
|
|
EventType,
|
|
event,
|
|
eventWithTime,
|
|
IncrementalSource,
|
|
listenerHandler,
|
|
mutationCallbackParam,
|
|
scrollCallback,
|
|
canvasMutationParam,
|
|
adoptedStyleSheetParam,
|
|
} from '@rrweb/types';
|
|
import type { CrossOriginIframeMessageEventContent } from '../types';
|
|
import { IframeManager } from './iframe-manager';
|
|
import { ShadowDomManager } from './shadow-dom-manager';
|
|
import { CanvasManager } from './observers/canvas/canvas-manager';
|
|
import { StylesheetManager } from './stylesheet-manager';
|
|
|
|
function wrapEvent(e: event): eventWithTime {
|
|
return {
|
|
...e,
|
|
timestamp: Date.now(),
|
|
};
|
|
}
|
|
|
|
let wrappedEmit!: (e: eventWithTime, isCheckout?: boolean) => void;
|
|
|
|
let takeFullSnapshot!: (isCheckout?: boolean) => void;
|
|
let canvasManager!: CanvasManager;
|
|
let recording = false;
|
|
|
|
const mirror = createMirror();
|
|
function record<T = eventWithTime>(
|
|
options: recordOptions<T> = {},
|
|
): listenerHandler | undefined {
|
|
const {
|
|
emit,
|
|
checkoutEveryNms,
|
|
checkoutEveryNth,
|
|
blockClass = 'rr-block',
|
|
blockSelector = null,
|
|
ignoreClass = 'rr-ignore',
|
|
maskTextClass = 'rr-mask',
|
|
maskTextSelector = null,
|
|
inlineStylesheet = true,
|
|
maskAllInputs,
|
|
maskInputOptions: _maskInputOptions,
|
|
slimDOMOptions: _slimDOMOptions,
|
|
maskInputFn,
|
|
maskTextFn,
|
|
hooks,
|
|
packFn,
|
|
sampling = {},
|
|
dataURLOptions = {},
|
|
mousemoveWait,
|
|
recordCanvas = false,
|
|
recordCrossOriginIframes = false,
|
|
recordAfter = options.recordAfter === 'DOMContentLoaded'
|
|
? options.recordAfter
|
|
: 'load',
|
|
userTriggeredOnInput = false,
|
|
collectFonts = false,
|
|
inlineImages = false,
|
|
plugins,
|
|
keepIframeSrcFn = () => false,
|
|
ignoreCSSAttributes = new Set([]),
|
|
} = options;
|
|
|
|
const inEmittingFrame = recordCrossOriginIframes
|
|
? window.parent === window
|
|
: true;
|
|
|
|
let passEmitsToParent = false;
|
|
if (!inEmittingFrame) {
|
|
try {
|
|
// throws if parent is cross-origin
|
|
if (window.parent.document) {
|
|
passEmitsToParent = false; // if parent is same origin we collect iframe events from the parent
|
|
}
|
|
} catch (e) {
|
|
passEmitsToParent = true;
|
|
}
|
|
}
|
|
|
|
// runtime checks for user options
|
|
if (inEmittingFrame && !emit) {
|
|
throw new Error('emit function is required');
|
|
}
|
|
// move departed options to new options
|
|
if (mousemoveWait !== undefined && sampling.mousemove === undefined) {
|
|
sampling.mousemove = mousemoveWait;
|
|
}
|
|
|
|
// reset mirror in case `record` this was called earlier
|
|
mirror.reset();
|
|
|
|
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,
|
|
password: true,
|
|
}
|
|
: _maskInputOptions !== undefined
|
|
? _maskInputOptions
|
|
: { password: true };
|
|
|
|
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
|
|
? _slimDOMOptions
|
|
: {};
|
|
|
|
polyfill();
|
|
|
|
let lastFullSnapshotEvent: eventWithTime;
|
|
let incrementalSnapshotCount = 0;
|
|
|
|
const eventProcessor = (e: eventWithTime): T => {
|
|
for (const plugin of plugins || []) {
|
|
if (plugin.eventProcessor) {
|
|
e = plugin.eventProcessor(e);
|
|
}
|
|
}
|
|
if (
|
|
packFn &&
|
|
// Disable packing events which will be emitted to parent frames.
|
|
!passEmitsToParent
|
|
) {
|
|
e = packFn(e) as unknown as eventWithTime;
|
|
}
|
|
return e as unknown as T;
|
|
};
|
|
wrappedEmit = (e: eventWithTime, isCheckout?: boolean) => {
|
|
if (
|
|
mutationBuffers[0]?.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
|
|
mutationBuffers.forEach((buf) => buf.unfreeze());
|
|
}
|
|
|
|
if (inEmittingFrame) {
|
|
emit?.(eventProcessor(e), isCheckout);
|
|
} else if (passEmitsToParent) {
|
|
const message: CrossOriginIframeMessageEventContent<T> = {
|
|
type: 'rrweb',
|
|
event: eventProcessor(e),
|
|
origin: window.location.origin,
|
|
isCheckout,
|
|
};
|
|
window.parent.postMessage(message, '*');
|
|
}
|
|
|
|
if (e.type === EventType.FullSnapshot) {
|
|
lastFullSnapshotEvent = e;
|
|
incrementalSnapshotCount = 0;
|
|
} else if (e.type === EventType.IncrementalSnapshot) {
|
|
// attach iframe should be considered as full snapshot
|
|
if (
|
|
e.data.source === IncrementalSource.Mutation &&
|
|
e.data.isAttachIframe
|
|
) {
|
|
return;
|
|
}
|
|
|
|
incrementalSnapshotCount++;
|
|
const exceedCount =
|
|
checkoutEveryNth && incrementalSnapshotCount >= checkoutEveryNth;
|
|
const exceedTime =
|
|
checkoutEveryNms &&
|
|
e.timestamp - lastFullSnapshotEvent.timestamp > checkoutEveryNms;
|
|
if (exceedCount || exceedTime) {
|
|
takeFullSnapshot(true);
|
|
}
|
|
}
|
|
};
|
|
|
|
const wrappedMutationEmit = (m: mutationCallbackParam) => {
|
|
wrappedEmit(
|
|
wrapEvent({
|
|
type: EventType.IncrementalSnapshot,
|
|
data: {
|
|
source: IncrementalSource.Mutation,
|
|
...m,
|
|
},
|
|
}),
|
|
);
|
|
};
|
|
const wrappedScrollEmit: scrollCallback = (p) =>
|
|
wrappedEmit(
|
|
wrapEvent({
|
|
type: EventType.IncrementalSnapshot,
|
|
data: {
|
|
source: IncrementalSource.Scroll,
|
|
...p,
|
|
},
|
|
}),
|
|
);
|
|
const wrappedCanvasMutationEmit = (p: canvasMutationParam) =>
|
|
wrappedEmit(
|
|
wrapEvent({
|
|
type: EventType.IncrementalSnapshot,
|
|
data: {
|
|
source: IncrementalSource.CanvasMutation,
|
|
...p,
|
|
},
|
|
}),
|
|
);
|
|
|
|
const wrappedAdoptedStyleSheetEmit = (a: adoptedStyleSheetParam) =>
|
|
wrappedEmit(
|
|
wrapEvent({
|
|
type: EventType.IncrementalSnapshot,
|
|
data: {
|
|
source: IncrementalSource.AdoptedStyleSheet,
|
|
...a,
|
|
},
|
|
}),
|
|
);
|
|
|
|
const stylesheetManager = new StylesheetManager({
|
|
mutationCb: wrappedMutationEmit,
|
|
adoptedStyleSheetCb: wrappedAdoptedStyleSheetEmit,
|
|
});
|
|
|
|
const iframeManager = new IframeManager({
|
|
mirror,
|
|
mutationCb: wrappedMutationEmit,
|
|
stylesheetManager: stylesheetManager,
|
|
recordCrossOriginIframes,
|
|
wrappedEmit,
|
|
});
|
|
|
|
/**
|
|
* Exposes mirror to the plugins
|
|
*/
|
|
for (const plugin of plugins || []) {
|
|
if (plugin.getMirror)
|
|
plugin.getMirror({
|
|
nodeMirror: mirror,
|
|
crossOriginIframeMirror: iframeManager.crossOriginIframeMirror,
|
|
crossOriginIframeStyleMirror:
|
|
iframeManager.crossOriginIframeStyleMirror,
|
|
});
|
|
}
|
|
|
|
canvasManager = new CanvasManager({
|
|
recordCanvas,
|
|
mutationCb: wrappedCanvasMutationEmit,
|
|
win: window,
|
|
blockClass,
|
|
blockSelector,
|
|
mirror,
|
|
sampling: sampling.canvas,
|
|
dataURLOptions,
|
|
});
|
|
|
|
const shadowDomManager = new ShadowDomManager({
|
|
mutationCb: wrappedMutationEmit,
|
|
scrollCb: wrappedScrollEmit,
|
|
bypassOptions: {
|
|
blockClass,
|
|
blockSelector,
|
|
maskTextClass,
|
|
maskTextSelector,
|
|
inlineStylesheet,
|
|
maskInputOptions,
|
|
dataURLOptions,
|
|
maskTextFn,
|
|
maskInputFn,
|
|
recordCanvas,
|
|
inlineImages,
|
|
sampling,
|
|
slimDOMOptions,
|
|
iframeManager,
|
|
stylesheetManager,
|
|
canvasManager,
|
|
keepIframeSrcFn,
|
|
processedNodeManager,
|
|
},
|
|
mirror,
|
|
});
|
|
|
|
takeFullSnapshot = (isCheckout = false) => {
|
|
wrappedEmit(
|
|
wrapEvent({
|
|
type: EventType.Meta,
|
|
data: {
|
|
href: window.location.href,
|
|
width: getWindowWidth(),
|
|
height: getWindowHeight(),
|
|
},
|
|
}),
|
|
isCheckout,
|
|
);
|
|
|
|
// When we take a full snapshot, old tracked StyleSheets need to be removed.
|
|
stylesheetManager.reset();
|
|
|
|
shadowDomManager.init();
|
|
|
|
mutationBuffers.forEach((buf) => buf.lock()); // don't allow any mirror modifications during snapshotting
|
|
const node = snapshot(document, {
|
|
mirror,
|
|
blockClass,
|
|
blockSelector,
|
|
maskTextClass,
|
|
maskTextSelector,
|
|
inlineStylesheet,
|
|
maskAllInputs: maskInputOptions,
|
|
maskTextFn,
|
|
slimDOM: slimDOMOptions,
|
|
dataURLOptions,
|
|
recordCanvas,
|
|
inlineImages,
|
|
onSerialize: (n) => {
|
|
if (isSerializedIframe(n, mirror)) {
|
|
iframeManager.addIframe(n as HTMLIFrameElement);
|
|
}
|
|
if (isSerializedStylesheet(n, mirror)) {
|
|
stylesheetManager.trackLinkElement(n as HTMLLinkElement);
|
|
}
|
|
if (hasShadowRoot(n)) {
|
|
shadowDomManager.addShadowRoot(n.shadowRoot, document);
|
|
}
|
|
},
|
|
onIframeLoad: (iframe, childSn) => {
|
|
iframeManager.attachIframe(iframe, childSn);
|
|
shadowDomManager.observeAttachShadow(iframe);
|
|
},
|
|
onStylesheetLoad: (linkEl, childSn) => {
|
|
stylesheetManager.attachLinkElement(linkEl, childSn);
|
|
},
|
|
keepIframeSrcFn,
|
|
});
|
|
|
|
if (!node) {
|
|
return console.warn('Failed to snapshot the document');
|
|
}
|
|
|
|
wrappedEmit(
|
|
wrapEvent({
|
|
type: EventType.FullSnapshot,
|
|
data: {
|
|
node,
|
|
initialOffset: getWindowScroll(window),
|
|
},
|
|
}),
|
|
isCheckout,
|
|
);
|
|
mutationBuffers.forEach((buf) => buf.unlock()); // generate & emit any mutations that happened during snapshotting, as can now apply against the newly built mirror
|
|
|
|
// Some old browsers don't support adoptedStyleSheets.
|
|
if (document.adoptedStyleSheets && document.adoptedStyleSheets.length > 0)
|
|
stylesheetManager.adoptStyleSheets(
|
|
document.adoptedStyleSheets,
|
|
mirror.getId(document),
|
|
);
|
|
};
|
|
|
|
try {
|
|
const handlers: listenerHandler[] = [];
|
|
|
|
const observe = (doc: Document) => {
|
|
return initObservers(
|
|
{
|
|
mutationCb: wrappedMutationEmit,
|
|
mousemoveCb: (positions, source) =>
|
|
wrappedEmit(
|
|
wrapEvent({
|
|
type: EventType.IncrementalSnapshot,
|
|
data: {
|
|
source,
|
|
positions,
|
|
},
|
|
}),
|
|
),
|
|
mouseInteractionCb: (d) =>
|
|
wrappedEmit(
|
|
wrapEvent({
|
|
type: EventType.IncrementalSnapshot,
|
|
data: {
|
|
source: IncrementalSource.MouseInteraction,
|
|
...d,
|
|
},
|
|
}),
|
|
),
|
|
scrollCb: wrappedScrollEmit,
|
|
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,
|
|
},
|
|
}),
|
|
),
|
|
styleDeclarationCb: (r) =>
|
|
wrappedEmit(
|
|
wrapEvent({
|
|
type: EventType.IncrementalSnapshot,
|
|
data: {
|
|
source: IncrementalSource.StyleDeclaration,
|
|
...r,
|
|
},
|
|
}),
|
|
),
|
|
canvasMutationCb: wrappedCanvasMutationEmit,
|
|
fontCb: (p) =>
|
|
wrappedEmit(
|
|
wrapEvent({
|
|
type: EventType.IncrementalSnapshot,
|
|
data: {
|
|
source: IncrementalSource.Font,
|
|
...p,
|
|
},
|
|
}),
|
|
),
|
|
selectionCb: (p) => {
|
|
wrappedEmit(
|
|
wrapEvent({
|
|
type: EventType.IncrementalSnapshot,
|
|
data: {
|
|
source: IncrementalSource.Selection,
|
|
...p,
|
|
},
|
|
}),
|
|
);
|
|
},
|
|
blockClass,
|
|
ignoreClass,
|
|
maskTextClass,
|
|
maskTextSelector,
|
|
maskInputOptions,
|
|
inlineStylesheet,
|
|
sampling,
|
|
recordCanvas,
|
|
inlineImages,
|
|
userTriggeredOnInput,
|
|
collectFonts,
|
|
doc,
|
|
maskInputFn,
|
|
maskTextFn,
|
|
keepIframeSrcFn,
|
|
blockSelector,
|
|
slimDOMOptions,
|
|
dataURLOptions,
|
|
mirror,
|
|
iframeManager,
|
|
stylesheetManager,
|
|
shadowDomManager,
|
|
processedNodeManager,
|
|
canvasManager,
|
|
ignoreCSSAttributes,
|
|
plugins:
|
|
plugins
|
|
?.filter((p) => p.observer)
|
|
?.map((p) => ({
|
|
observer: p.observer!,
|
|
options: p.options,
|
|
callback: (payload: object) =>
|
|
wrappedEmit(
|
|
wrapEvent({
|
|
type: EventType.Plugin,
|
|
data: {
|
|
plugin: p.name,
|
|
payload,
|
|
},
|
|
}),
|
|
),
|
|
})) || [],
|
|
},
|
|
hooks,
|
|
);
|
|
};
|
|
|
|
iframeManager.addLoadListener((iframeEl) => {
|
|
try {
|
|
handlers.push(observe(iframeEl.contentDocument!));
|
|
} catch (error) {
|
|
// TODO: handle internal error
|
|
console.warn(error);
|
|
}
|
|
});
|
|
|
|
const init = () => {
|
|
takeFullSnapshot();
|
|
handlers.push(observe(document));
|
|
recording = true;
|
|
};
|
|
if (
|
|
document.readyState === 'interactive' ||
|
|
document.readyState === 'complete'
|
|
) {
|
|
init();
|
|
} else {
|
|
handlers.push(
|
|
on('DOMContentLoaded', () => {
|
|
wrappedEmit(
|
|
wrapEvent({
|
|
type: EventType.DomContentLoaded,
|
|
data: {},
|
|
}),
|
|
);
|
|
if (recordAfter === 'DOMContentLoaded') init();
|
|
}),
|
|
);
|
|
handlers.push(
|
|
on(
|
|
'load',
|
|
() => {
|
|
wrappedEmit(
|
|
wrapEvent({
|
|
type: EventType.Load,
|
|
data: {},
|
|
}),
|
|
);
|
|
if (recordAfter === 'load') init();
|
|
},
|
|
window,
|
|
),
|
|
);
|
|
}
|
|
return () => {
|
|
handlers.forEach((h) => h());
|
|
recording = false;
|
|
};
|
|
} catch (error) {
|
|
// TODO: handle internal error
|
|
console.warn(error);
|
|
}
|
|
}
|
|
|
|
record.addCustomEvent = <T>(tag: string, payload: T) => {
|
|
if (!recording) {
|
|
throw new Error('please add custom event after start recording');
|
|
}
|
|
wrappedEmit(
|
|
wrapEvent({
|
|
type: EventType.Custom,
|
|
data: {
|
|
tag,
|
|
payload,
|
|
},
|
|
}),
|
|
);
|
|
};
|
|
|
|
record.freezePage = () => {
|
|
mutationBuffers.forEach((buf) => buf.freeze());
|
|
};
|
|
|
|
record.takeFullSnapshot = (isCheckout?: boolean) => {
|
|
if (!recording) {
|
|
throw new Error('please take full snapshot after start recording');
|
|
}
|
|
takeFullSnapshot(isCheckout);
|
|
};
|
|
|
|
record.mirror = mirror;
|
|
|
|
export default record;
|