add meta event and fix childList observer, also update related replayer
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "rrweb",
|
||||
"version": "0.3.0",
|
||||
"version": "0.4.0",
|
||||
"description": "record and replay the web",
|
||||
"main": "dist/index.js",
|
||||
"module": "dist/module.js",
|
||||
@@ -40,6 +40,6 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"mitt": "^1.1.3",
|
||||
"rrweb-snapshot": "^0.4.3"
|
||||
"rrweb-snapshot": "^0.5.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ export default [
|
||||
file: './dist/record/module.js',
|
||||
},
|
||||
{
|
||||
name: 'record',
|
||||
name: 'record1',
|
||||
format: 'iife',
|
||||
file: './dist/record/browser.js',
|
||||
},
|
||||
|
||||
@@ -27,106 +27,121 @@ function record(options: recordOptions = {}) {
|
||||
emit(
|
||||
wrapEvent({
|
||||
type: EventType.DomContentLoaded,
|
||||
data: {
|
||||
href: window.location.href,
|
||||
},
|
||||
data: {},
|
||||
}),
|
||||
);
|
||||
});
|
||||
on(
|
||||
'load',
|
||||
() => {
|
||||
emit(
|
||||
wrapEvent({
|
||||
type: EventType.Load,
|
||||
data: {
|
||||
width: getWindowWidth(),
|
||||
height: getWindowHeight(),
|
||||
const init = () => {
|
||||
emit(
|
||||
wrapEvent({
|
||||
type: EventType.Meta,
|
||||
data: {
|
||||
href: window.location.href,
|
||||
width: getWindowWidth(),
|
||||
height: getWindowHeight(),
|
||||
},
|
||||
}),
|
||||
);
|
||||
const [node, idNodeMap] = snapshot(document);
|
||||
if (!node) {
|
||||
return console.warn('Failed to snapshot the document');
|
||||
}
|
||||
mirror.map = idNodeMap;
|
||||
emit(
|
||||
wrapEvent({
|
||||
type: EventType.FullSnapshot,
|
||||
data: {
|
||||
node,
|
||||
initialOffset: {
|
||||
left: document.documentElement.scrollLeft,
|
||||
top: document.documentElement.scrollTop,
|
||||
},
|
||||
}),
|
||||
);
|
||||
const [node, idNodeMap] = snapshot(document);
|
||||
if (!node) {
|
||||
return console.warn('Failed to snapshot the document');
|
||||
}
|
||||
mirror.map = idNodeMap;
|
||||
emit(
|
||||
wrapEvent({
|
||||
type: EventType.FullSnapshot,
|
||||
data: {
|
||||
node,
|
||||
initialOffset: {
|
||||
left: document.documentElement.scrollLeft,
|
||||
top: document.documentElement.scrollTop,
|
||||
},
|
||||
}),
|
||||
);
|
||||
initObservers({
|
||||
mutationCb: m =>
|
||||
emit(
|
||||
wrapEvent({
|
||||
type: EventType.IncrementalSnapshot,
|
||||
data: {
|
||||
source: IncrementalSource.Mutation,
|
||||
...m,
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
initObservers({
|
||||
mutationCb: m =>
|
||||
emit(
|
||||
wrapEvent({
|
||||
type: EventType.IncrementalSnapshot,
|
||||
data: {
|
||||
source: IncrementalSource.Mutation,
|
||||
...m,
|
||||
},
|
||||
}),
|
||||
),
|
||||
mousemoveCb: positions =>
|
||||
emit(
|
||||
wrapEvent({
|
||||
type: EventType.IncrementalSnapshot,
|
||||
data: {
|
||||
source: IncrementalSource.MouseMove,
|
||||
positions,
|
||||
},
|
||||
}),
|
||||
),
|
||||
mouseInteractionCb: d =>
|
||||
emit(
|
||||
wrapEvent({
|
||||
type: EventType.IncrementalSnapshot,
|
||||
data: {
|
||||
source: IncrementalSource.MouseInteraction,
|
||||
...d,
|
||||
},
|
||||
}),
|
||||
),
|
||||
scrollCb: p =>
|
||||
emit(
|
||||
wrapEvent({
|
||||
type: EventType.IncrementalSnapshot,
|
||||
data: {
|
||||
source: IncrementalSource.Scroll,
|
||||
...p,
|
||||
},
|
||||
}),
|
||||
),
|
||||
viewportResizeCb: d =>
|
||||
emit(
|
||||
wrapEvent({
|
||||
type: EventType.IncrementalSnapshot,
|
||||
data: {
|
||||
source: IncrementalSource.ViewportResize,
|
||||
...d,
|
||||
},
|
||||
}),
|
||||
),
|
||||
inputCb: v =>
|
||||
emit(
|
||||
wrapEvent({
|
||||
type: EventType.IncrementalSnapshot,
|
||||
data: {
|
||||
source: IncrementalSource.Input,
|
||||
...v,
|
||||
},
|
||||
}),
|
||||
),
|
||||
});
|
||||
},
|
||||
window,
|
||||
);
|
||||
}),
|
||||
),
|
||||
mousemoveCb: positions =>
|
||||
emit(
|
||||
wrapEvent({
|
||||
type: EventType.IncrementalSnapshot,
|
||||
data: {
|
||||
source: IncrementalSource.MouseMove,
|
||||
positions,
|
||||
},
|
||||
}),
|
||||
),
|
||||
mouseInteractionCb: d =>
|
||||
emit(
|
||||
wrapEvent({
|
||||
type: EventType.IncrementalSnapshot,
|
||||
data: {
|
||||
source: IncrementalSource.MouseInteraction,
|
||||
...d,
|
||||
},
|
||||
}),
|
||||
),
|
||||
scrollCb: p =>
|
||||
emit(
|
||||
wrapEvent({
|
||||
type: EventType.IncrementalSnapshot,
|
||||
data: {
|
||||
source: IncrementalSource.Scroll,
|
||||
...p,
|
||||
},
|
||||
}),
|
||||
),
|
||||
viewportResizeCb: d =>
|
||||
emit(
|
||||
wrapEvent({
|
||||
type: EventType.IncrementalSnapshot,
|
||||
data: {
|
||||
source: IncrementalSource.ViewportResize,
|
||||
...d,
|
||||
},
|
||||
}),
|
||||
),
|
||||
inputCb: v =>
|
||||
emit(
|
||||
wrapEvent({
|
||||
type: EventType.IncrementalSnapshot,
|
||||
data: {
|
||||
source: IncrementalSource.Input,
|
||||
...v,
|
||||
},
|
||||
}),
|
||||
),
|
||||
});
|
||||
};
|
||||
if (
|
||||
document.readyState === 'interactive' ||
|
||||
document.readyState === 'complete'
|
||||
) {
|
||||
init();
|
||||
} else {
|
||||
on(
|
||||
'load',
|
||||
() => {
|
||||
emit(
|
||||
wrapEvent({
|
||||
type: EventType.Load,
|
||||
data: {},
|
||||
}),
|
||||
);
|
||||
init();
|
||||
},
|
||||
window,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
// TODO: handle internal error
|
||||
console.warn(error);
|
||||
|
||||
@@ -74,13 +74,6 @@ function initMutationObserver(cb: mutationCallBack): MutationObserver {
|
||||
item.attributes[attributeName!] = value;
|
||||
}
|
||||
case 'childList': {
|
||||
removedNodes.forEach(n => {
|
||||
removes.push({
|
||||
parentId: id,
|
||||
id: mirror.getId(n as INode),
|
||||
});
|
||||
mirror.removeNodeFromMap(n as INode);
|
||||
});
|
||||
addedNodes.forEach(n => {
|
||||
adds.push({
|
||||
parentId: id,
|
||||
@@ -90,9 +83,15 @@ function initMutationObserver(cb: mutationCallBack): MutationObserver {
|
||||
nextId: !nextSibling
|
||||
? nextSibling
|
||||
: mirror.getId(nextSibling as INode),
|
||||
node: serializeNodeWithId(n, document, mirror.map)!,
|
||||
});
|
||||
});
|
||||
removedNodes.forEach(n => {
|
||||
removes.push({
|
||||
parentId: id,
|
||||
id: mirror.getId(n as INode),
|
||||
});
|
||||
serializeNodeWithId(n as INode, document, mirror.map);
|
||||
mirror.removeNodeFromMap(n as INode);
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { rebuild, serializeNodeWithId } from 'rrweb-snapshot';
|
||||
import { rebuild, buildNodeWithSN } from 'rrweb-snapshot';
|
||||
import * as mittProxy from 'mitt';
|
||||
import { later, clear } from './timer';
|
||||
import {
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
playerMetaData,
|
||||
viewportResizeDimention,
|
||||
} from '../types';
|
||||
import { mirror, getIdNodeMap } from '../utils';
|
||||
import { mirror } from '../utils';
|
||||
|
||||
// https://github.com/rollup/rollup/issues/1267#issuecomment-296395734
|
||||
// tslint:disable-next-line
|
||||
@@ -82,8 +82,9 @@ export class Replayer {
|
||||
let castFn: undefined | (() => void);
|
||||
switch (event.type) {
|
||||
case EventType.DomContentLoaded:
|
||||
break;
|
||||
case EventType.Load:
|
||||
break;
|
||||
case EventType.Meta:
|
||||
castFn = () =>
|
||||
this.emitter.emit('resize', {
|
||||
width: event.data.width,
|
||||
@@ -141,6 +142,7 @@ export class Replayer {
|
||||
this.timerIds.push(id);
|
||||
}
|
||||
|
||||
// TODO: add speed to mouse move timestamp calculation
|
||||
private getDelay(event: eventWithTime): number {
|
||||
// Mouse move events was recorded in a throttle function,
|
||||
// so we need to find the real timestamp by traverse the time offsets.
|
||||
@@ -163,29 +165,35 @@ export class Replayer {
|
||||
}
|
||||
|
||||
private rebuildFullSnapshot(event: fullSnapshotEvent) {
|
||||
const doc = rebuild(event.data.node);
|
||||
if (doc) {
|
||||
this.iframe.contentDocument!.open();
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/Element/innerHTML
|
||||
this.iframe.contentDocument!.write(
|
||||
new XMLSerializer()
|
||||
.serializeToString(doc as Document)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>'),
|
||||
);
|
||||
this.iframe.contentDocument!.close();
|
||||
mirror.map = getIdNodeMap(this.iframe.contentDocument!);
|
||||
// avoid form submit to refresh the iframe
|
||||
this.iframe.contentDocument!.querySelectorAll('form').forEach(form => {
|
||||
form.addEventListener('submit', evt => evt.preventDefault());
|
||||
});
|
||||
}
|
||||
mirror.map = rebuild(event.data.node, this.iframe.contentDocument!)[1];
|
||||
// avoid form submit to refresh the iframe
|
||||
this.iframe.contentDocument!.querySelectorAll('form').forEach(form => {
|
||||
form.addEventListener('submit', evt => evt.preventDefault());
|
||||
});
|
||||
}
|
||||
|
||||
private applyIncremental(d: incrementalData, isSync: boolean) {
|
||||
switch (d.source) {
|
||||
case IncrementalSource.Mutation: {
|
||||
d.adds.forEach(mutation => {
|
||||
const target = buildNodeWithSN(
|
||||
mutation.node,
|
||||
this.iframe.contentDocument!,
|
||||
mirror.map,
|
||||
) as Node;
|
||||
const parent = (mirror.getNode(mutation.parentId) as Node) as Element;
|
||||
if (mutation.nextId) {
|
||||
const next = (mirror.getNode(mutation.nextId) as Node) as Element;
|
||||
parent.insertBefore(target, next);
|
||||
} else if (mutation.previousId) {
|
||||
const previous = (mirror.getNode(
|
||||
mutation.previousId,
|
||||
) as Node) as Element;
|
||||
parent.insertBefore(target, previous.nextSibling);
|
||||
} else {
|
||||
parent.appendChild(target);
|
||||
}
|
||||
});
|
||||
d.texts.forEach(mutation => {
|
||||
const target = (mirror.getNode(mutation.id) as Node) as Text;
|
||||
target.textContent = mutation.value;
|
||||
@@ -209,26 +217,6 @@ export class Replayer {
|
||||
parent.removeChild(target);
|
||||
delete mirror.map[mutation.id];
|
||||
});
|
||||
d.adds.forEach(mutation => {
|
||||
const target = (mirror.getNode(mutation.id) as Node) as Element;
|
||||
const parent = (mirror.getNode(mutation.parentId) as Node) as Element;
|
||||
if (mutation.nextId) {
|
||||
const next = (mirror.getNode(mutation.nextId) as Node) as Element;
|
||||
parent.insertBefore(target, next);
|
||||
} else if (mutation.previousId) {
|
||||
const previous = (mirror.getNode(
|
||||
mutation.previousId,
|
||||
) as Node) as Element;
|
||||
parent.insertBefore(target, previous.nextSibling);
|
||||
} else {
|
||||
parent.appendChild(target);
|
||||
}
|
||||
serializeNodeWithId(
|
||||
mirror.getNode(mutation.id),
|
||||
this.iframe.contentDocument!,
|
||||
mirror.map,
|
||||
);
|
||||
});
|
||||
break;
|
||||
}
|
||||
case IncrementalSource.MouseMove:
|
||||
|
||||
24
src/types.ts
24
src/types.ts
@@ -5,21 +5,17 @@ export enum EventType {
|
||||
Load,
|
||||
FullSnapshot,
|
||||
IncrementalSnapshot,
|
||||
Meta,
|
||||
}
|
||||
|
||||
export type domContentLoadedEvent = {
|
||||
type: EventType.DomContentLoaded;
|
||||
data: {
|
||||
href: string;
|
||||
};
|
||||
data: {};
|
||||
};
|
||||
|
||||
export type loadedEvent = {
|
||||
type: EventType.Load;
|
||||
data: {
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
data: {};
|
||||
};
|
||||
|
||||
export type fullSnapshotEvent = {
|
||||
@@ -38,6 +34,15 @@ export type incrementalSnapshotEvent = {
|
||||
data: incrementalData;
|
||||
};
|
||||
|
||||
export type metaEvent = {
|
||||
type: EventType.Meta;
|
||||
data: {
|
||||
href: string;
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
};
|
||||
|
||||
export enum IncrementalSource {
|
||||
Mutation,
|
||||
MouseMove,
|
||||
@@ -85,7 +90,8 @@ export type event =
|
||||
| domContentLoadedEvent
|
||||
| loadedEvent
|
||||
| fullSnapshotEvent
|
||||
| incrementalSnapshotEvent;
|
||||
| incrementalSnapshotEvent
|
||||
| metaEvent;
|
||||
|
||||
export type eventWithTime = event & {
|
||||
timestamp: number;
|
||||
@@ -125,7 +131,7 @@ export type addedNodeMutation = {
|
||||
parentId: number;
|
||||
previousId: number | null;
|
||||
nextId: number | null;
|
||||
id: number;
|
||||
node: serializedNodeWithId;
|
||||
};
|
||||
|
||||
type mutationCallbackParam = {
|
||||
|
||||
33
src/utils.ts
33
src/utils.ts
@@ -1,4 +1,3 @@
|
||||
import { idNodeMap, NodeType, serializeNodeWithId, resetId } from 'rrweb-snapshot';
|
||||
import {
|
||||
Mirror,
|
||||
throttleOptions,
|
||||
@@ -30,38 +29,6 @@ export const mirror: Mirror = {
|
||||
},
|
||||
};
|
||||
|
||||
// TODO: transform this into the snapshot repo
|
||||
export function getIdNodeMap(doc: Document) {
|
||||
resetId();
|
||||
const map: idNodeMap = {};
|
||||
|
||||
function walk(n: Node) {
|
||||
const node = serializeNodeWithId(n, doc, map);
|
||||
if (!node) {
|
||||
return null;
|
||||
}
|
||||
if (node.type === NodeType.Document || node.type === NodeType.Element) {
|
||||
let dataStr: string | null = null;
|
||||
let extraChildIndexes: number[] = [];
|
||||
if (node.type === NodeType.Element) {
|
||||
dataStr = (n as Element).getAttribute('data-extra-child-index');
|
||||
}
|
||||
if (dataStr) {
|
||||
extraChildIndexes = JSON.parse(dataStr);
|
||||
}
|
||||
n.childNodes.forEach((childNode, index) => {
|
||||
// skip extra DOM created when rebuild
|
||||
if (extraChildIndexes.indexOf(index) < 0) {
|
||||
walk(childNode);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
walk(doc);
|
||||
return map;
|
||||
}
|
||||
|
||||
// copy from underscore and modified
|
||||
export function throttle<T>(
|
||||
func: (arg: T) => void,
|
||||
|
||||
Reference in New Issue
Block a user