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