change observed mutations into serializable records
This commit is contained in:
@@ -1,35 +0,0 @@
|
|||||||
import { snapshot } from 'rrweb-snapshot';
|
|
||||||
import { EventType, event } from './types';
|
|
||||||
|
|
||||||
function on(
|
|
||||||
type: string,
|
|
||||||
fn: EventListenerOrEventListenerObject,
|
|
||||||
target = document,
|
|
||||||
) {
|
|
||||||
target.addEventListener(type, fn);
|
|
||||||
}
|
|
||||||
|
|
||||||
function createEvent(type: EventType, data: any): event {
|
|
||||||
return {
|
|
||||||
type,
|
|
||||||
data,
|
|
||||||
timestamp: Date.now(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function emit(e: event) {}
|
|
||||||
|
|
||||||
function record() {
|
|
||||||
on('DOMContentLoaded', () => {
|
|
||||||
emit(
|
|
||||||
createEvent(EventType.DomContentLoaded, { href: window.location.href }),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
on('load', () => {
|
|
||||||
emit(createEvent(EventType.Load, null));
|
|
||||||
const node = snapshot(document);
|
|
||||||
emit(createEvent(EventType.FullSnapshot, { node }));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export default record;
|
|
||||||
70
src/record/index.ts
Normal file
70
src/record/index.ts
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import { snapshot } from 'rrweb-snapshot';
|
||||||
|
import initObservers from './observer';
|
||||||
|
import { mirror } from '../utils';
|
||||||
|
import {
|
||||||
|
EventType,
|
||||||
|
event,
|
||||||
|
eventWithTime,
|
||||||
|
recordOptions,
|
||||||
|
IncrementalSource,
|
||||||
|
} from '../types';
|
||||||
|
|
||||||
|
function on(
|
||||||
|
type: string,
|
||||||
|
fn: EventListenerOrEventListenerObject,
|
||||||
|
target = document,
|
||||||
|
) {
|
||||||
|
target.addEventListener(type, fn, { capture: true, passive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
function wrapEvent(e: event): eventWithTime {
|
||||||
|
return {
|
||||||
|
...e,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function record(options: recordOptions) {
|
||||||
|
try {
|
||||||
|
const { emit } = options;
|
||||||
|
// runtime checks for user options
|
||||||
|
if (!emit) {
|
||||||
|
throw new Error('emit function is required');
|
||||||
|
}
|
||||||
|
on('DOMContentLoaded', () => {
|
||||||
|
emit(
|
||||||
|
wrapEvent({
|
||||||
|
type: EventType.DomContentLoaded,
|
||||||
|
data: {
|
||||||
|
href: window.location.href,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
on('load', () => {
|
||||||
|
emit(wrapEvent({ type: EventType.Load, data: {} }));
|
||||||
|
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 } }));
|
||||||
|
initObservers({
|
||||||
|
mutationCb: m =>
|
||||||
|
emit(
|
||||||
|
wrapEvent({
|
||||||
|
type: EventType.IncrementalSnapshot,
|
||||||
|
data: {
|
||||||
|
source: IncrementalSource.Mutation,
|
||||||
|
...m,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
// TODO: handle internal error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default record;
|
||||||
107
src/record/observer.ts
Normal file
107
src/record/observer.ts
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
import { INode } from 'rrweb-snapshot';
|
||||||
|
import { mirror } from '../utils';
|
||||||
|
import {
|
||||||
|
mutationCallBack,
|
||||||
|
textMutation,
|
||||||
|
attributeMutation,
|
||||||
|
removedNodeMutation,
|
||||||
|
addedNodeMutation,
|
||||||
|
observerParam,
|
||||||
|
} from '../types';
|
||||||
|
|
||||||
|
function initMutationObserver(cb: mutationCallBack): MutationObserver {
|
||||||
|
const observer = new MutationObserver(mutations => {
|
||||||
|
const texts: textMutation[] = [];
|
||||||
|
const attributes: attributeMutation[] = [];
|
||||||
|
const removes: removedNodeMutation[] = [];
|
||||||
|
const adds: addedNodeMutation[] = [];
|
||||||
|
mutations.forEach(mutation => {
|
||||||
|
const {
|
||||||
|
type,
|
||||||
|
target,
|
||||||
|
oldValue,
|
||||||
|
addedNodes,
|
||||||
|
removedNodes,
|
||||||
|
attributeName,
|
||||||
|
nextSibling,
|
||||||
|
previousSibling,
|
||||||
|
} = mutation;
|
||||||
|
const id = mirror.getId(target as INode);
|
||||||
|
switch (type) {
|
||||||
|
case 'characterData': {
|
||||||
|
const value = target.textContent;
|
||||||
|
if (value !== oldValue) {
|
||||||
|
texts.push({
|
||||||
|
id,
|
||||||
|
value,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'attributes': {
|
||||||
|
const value = (target as HTMLElement).getAttribute(attributeName!);
|
||||||
|
if (value === oldValue) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let item: attributeMutation | undefined = attributes.find(
|
||||||
|
a => a.id === id,
|
||||||
|
);
|
||||||
|
if (!item) {
|
||||||
|
item = {
|
||||||
|
id,
|
||||||
|
attributes: {},
|
||||||
|
};
|
||||||
|
attributes.push(item);
|
||||||
|
}
|
||||||
|
// overwrite attribute if the mutations was triggered in same time
|
||||||
|
item.attributes[attributeName!] = value;
|
||||||
|
}
|
||||||
|
case 'childList': {
|
||||||
|
removedNodes.forEach(n => {
|
||||||
|
removes.push({
|
||||||
|
parentId: id,
|
||||||
|
id: mirror.getId(n as INode),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
addedNodes.forEach(n => {
|
||||||
|
adds.push({
|
||||||
|
parentId: id,
|
||||||
|
previousId: !previousSibling
|
||||||
|
? previousSibling
|
||||||
|
: mirror.getId(previousSibling as INode),
|
||||||
|
nextId: !nextSibling
|
||||||
|
? nextSibling
|
||||||
|
: mirror.getId(nextSibling as INode),
|
||||||
|
id: mirror.getId(n as INode),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
cb({
|
||||||
|
texts,
|
||||||
|
attributes,
|
||||||
|
removes,
|
||||||
|
adds,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
observer.observe(document, {
|
||||||
|
attributes: true,
|
||||||
|
attributeOldValue: true,
|
||||||
|
characterData: true,
|
||||||
|
characterDataOldValue: true,
|
||||||
|
childList: true,
|
||||||
|
subtree: true,
|
||||||
|
});
|
||||||
|
return observer;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function initObservers(o: observerParam) {
|
||||||
|
const mutationObserver = initMutationObserver(o.mutationCb);
|
||||||
|
return {
|
||||||
|
mutationObserver,
|
||||||
|
};
|
||||||
|
}
|
||||||
95
src/types.ts
95
src/types.ts
@@ -1,3 +1,5 @@
|
|||||||
|
import { serializedNodeWithId, idNodeMap, INode } from 'rrweb-snapshot';
|
||||||
|
|
||||||
export enum EventType {
|
export enum EventType {
|
||||||
DomContentLoaded,
|
DomContentLoaded,
|
||||||
Load,
|
Load,
|
||||||
@@ -5,8 +7,93 @@ export enum EventType {
|
|||||||
IncrementalSnapshot,
|
IncrementalSnapshot,
|
||||||
}
|
}
|
||||||
|
|
||||||
export type event = {
|
export type domContentLoadedEvent = {
|
||||||
type: EventType;
|
type: EventType.DomContentLoaded;
|
||||||
timestamp: number;
|
data: {
|
||||||
data: any;
|
href: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type loadedEvent = {
|
||||||
|
type: EventType.Load;
|
||||||
|
data: {};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type fullSnapshotEvent = {
|
||||||
|
type: EventType.FullSnapshot;
|
||||||
|
data: {
|
||||||
|
node: serializedNodeWithId;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export enum IncrementalSource {
|
||||||
|
Mutation,
|
||||||
|
}
|
||||||
|
|
||||||
|
export type incrementalSnapshotEvent = {
|
||||||
|
type: EventType.IncrementalSnapshot;
|
||||||
|
data: incrementalData;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type mutationData = {
|
||||||
|
source: IncrementalSource.Mutation;
|
||||||
|
} & mutationCallbackParam;
|
||||||
|
|
||||||
|
export type incrementalData = mutationData;
|
||||||
|
|
||||||
|
export type event =
|
||||||
|
| domContentLoadedEvent
|
||||||
|
| loadedEvent
|
||||||
|
| fullSnapshotEvent
|
||||||
|
| incrementalSnapshotEvent;
|
||||||
|
|
||||||
|
export type eventWithTime = event & {
|
||||||
|
timestamp: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type recordOptions = {
|
||||||
|
emit: (e: eventWithTime) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type observerParam = {
|
||||||
|
mutationCb: mutationCallBack;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type textMutation = {
|
||||||
|
id: number;
|
||||||
|
value: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type attributeMutation = {
|
||||||
|
id: number;
|
||||||
|
attributes: {
|
||||||
|
[key: string]: string | null;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type removedNodeMutation = {
|
||||||
|
parentId: number;
|
||||||
|
id: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type addedNodeMutation = {
|
||||||
|
parentId: number;
|
||||||
|
previousId: number | null;
|
||||||
|
nextId: number | null;
|
||||||
|
id: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type mutationCallbackParam = {
|
||||||
|
texts: textMutation[];
|
||||||
|
attributes: attributeMutation[];
|
||||||
|
removes: removedNodeMutation[];
|
||||||
|
adds: addedNodeMutation[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type mutationCallBack = (m: mutationCallbackParam) => void;
|
||||||
|
|
||||||
|
export type Mirror = {
|
||||||
|
map: idNodeMap;
|
||||||
|
getId: (n: INode) => number;
|
||||||
|
getNode: (id: number) => INode;
|
||||||
};
|
};
|
||||||
|
|||||||
11
src/utils.ts
Normal file
11
src/utils.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { Mirror } from './types';
|
||||||
|
|
||||||
|
export const mirror: Mirror = {
|
||||||
|
map: {},
|
||||||
|
getId(n) {
|
||||||
|
return n.__sn && n.__sn.id;
|
||||||
|
},
|
||||||
|
getNode(id) {
|
||||||
|
return mirror.map[id];
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"module": "commonjs",
|
"module": "commonjs",
|
||||||
"noImplicitAny": true,
|
"noImplicitAny": true,
|
||||||
|
"strictNullChecks": true,
|
||||||
"removeComments": true,
|
"removeComments": true,
|
||||||
"preserveConstEnums": true,
|
"preserveConstEnums": true,
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
|
|||||||
Reference in New Issue
Block a user