add checkout config to recorder (#36)

* add checkout config to recorder

* add test cases for checkout feature and extract assertSnapshot method
This commit is contained in:
yz-yu
2019-01-11 10:52:04 +08:00
committed by GitHub
parent b45655ea3a
commit cedc87c69e
9 changed files with 738 additions and 314 deletions

View File

@@ -110,6 +110,80 @@ You may find some contents on the webpage which are not willing to be recorded,
- An element with the class name `.rr-ignore` will not record its input events.
- `input[type="password"]` will be ignored as default.
#### Checkout
By default, all the emitted events are required to replay a session and if you do not want to store all the events, you can use the checkout config.
**Most of the time you do not need to configure this**. But if you want to do something like capturing just the last N events from when an error has occurred, here is an example:
```js
// We use a two-dimensional array to store multiple events array
const eventsMatrix = [[]];
rrweb.record({
emit(event, isCheckout) {
// isCheckout is a flag to tell you the events has been checkout
if (isCheckout) {
eventsMatrix.push([]);
}
const lastEvents = eventsMatrix[eventsMatrix.length - 1];
lastEvents.push(event);
},
checkoutEveryNth: 200, // checkout every 200 events
});
// send last two events array to the backend
window.onerror = function() {
const len = eventsMatrix.length;
const events = eventsMatrix[len - 2].concat(eventsMatrix[len - 1]);
const body = JSON.stringify({ events });
fetch('http://YOUR_BACKEND_API', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body,
});
};
```
Due to the incremental-snapshot-chain mechanism rrweb used, we can not capture the last N events accurately. With the sample code above, you will finally get the last 200 to 400 events been sent to your backend.
Similarly, you can also configure `checkoutEveryNms` to capture the last N minutes events:
```js
// We use a two-dimensional array to store multiple events array
const eventsMatrix = [[]];
rrweb.record({
emit(event, isCheckout) {
// isCheckout is a flag to tell you the events has been checkout
if (isCheckout) {
eventsMatrix.push([]);
}
const lastEvents = eventsMatrix[eventsMatrix.length - 1];
lastEvents.push(event);
},
checkoutEveryNms: 5 * 60 * 1000, // checkout every 5 minutes
});
// send last two events array to the backend
window.onerror = function() {
const len = eventsMatrix.length;
const events = eventsMatrix[len - 2].concat(eventsMatrix[len - 1]);
const body = JSON.stringify({ events });
fetch('http://YOUR_BACKEND_API', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body,
});
};
```
With the sample code above, you will finally get the last 5 to 10 minutes of events been sent to your backend.
### Replay
You need to include the style sheet before replay:

View File

@@ -18,16 +18,68 @@ function wrapEvent(e: event): eventWithTime {
}
function record(options: recordOptions = {}): listenerHandler | undefined {
const { emit } = options;
const { emit, checkoutEveryNms, checkoutEveryNth } = options;
// runtime checks for user options
if (!emit) {
throw new Error('emit function is required');
}
let lastFullSnapshotEvent: eventWithTime;
let incrementalSnapshotCount = 0;
const wrappedEmit = (e: eventWithTime, isCheckout?: boolean) => {
emit(e, 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,
);
const [node, idNodeMap] = snapshot(document);
if (!node) {
return console.warn('Failed to snapshot the document');
}
mirror.map = idNodeMap;
wrappedEmit(
wrapEvent({
type: EventType.FullSnapshot,
data: {
node,
initialOffset: {
left: document.documentElement!.scrollLeft,
top: document.documentElement!.scrollTop,
},
},
}),
);
}
try {
const handlers: listenerHandler[] = [];
handlers.push(
on('DOMContentLoaded', () => {
emit(
wrappedEmit(
wrapEvent({
type: EventType.DomContentLoaded,
data: {},
@@ -36,37 +88,12 @@ function record(options: recordOptions = {}): listenerHandler | undefined {
}),
);
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,
},
},
}),
);
takeFullSnapshot();
handlers.push(
initObservers({
mutationCb: m =>
emit(
wrappedEmit(
wrapEvent({
type: EventType.IncrementalSnapshot,
data: {
@@ -76,7 +103,7 @@ function record(options: recordOptions = {}): listenerHandler | undefined {
}),
),
mousemoveCb: positions =>
emit(
wrappedEmit(
wrapEvent({
type: EventType.IncrementalSnapshot,
data: {
@@ -86,7 +113,7 @@ function record(options: recordOptions = {}): listenerHandler | undefined {
}),
),
mouseInteractionCb: d =>
emit(
wrappedEmit(
wrapEvent({
type: EventType.IncrementalSnapshot,
data: {
@@ -96,7 +123,7 @@ function record(options: recordOptions = {}): listenerHandler | undefined {
}),
),
scrollCb: p =>
emit(
wrappedEmit(
wrapEvent({
type: EventType.IncrementalSnapshot,
data: {
@@ -106,7 +133,7 @@ function record(options: recordOptions = {}): listenerHandler | undefined {
}),
),
viewportResizeCb: d =>
emit(
wrappedEmit(
wrapEvent({
type: EventType.IncrementalSnapshot,
data: {
@@ -116,7 +143,7 @@ function record(options: recordOptions = {}): listenerHandler | undefined {
}),
),
inputCb: v =>
emit(
wrappedEmit(
wrapEvent({
type: EventType.IncrementalSnapshot,
data: {
@@ -138,7 +165,7 @@ function record(options: recordOptions = {}): listenerHandler | undefined {
on(
'load',
() => {
emit(
wrappedEmit(
wrapEvent({
type: EventType.Load,
data: {},

View File

@@ -99,7 +99,9 @@ export type eventWithTime = event & {
};
export type recordOptions = {
emit?: (e: eventWithTime) => void;
emit?: (e: eventWithTime, isCheckout?: boolean) => void;
checkoutEveryNth?: number;
checkoutEveryNms?: number;
};
export type observerParam = {

View File

@@ -4,13 +4,11 @@ exports[`attributes 1`] = `
"[
{
\\"type\\": 0,
\\"data\\": {},
\\"timestamp\\": 1542268800000
\\"data\\": {}
},
{
\\"type\\": 1,
\\"data\\": {},
\\"timestamp\\": 1542268800000
\\"data\\": {}
},
{
\\"type\\": 4,
@@ -18,8 +16,7 @@ exports[`attributes 1`] = `
\\"href\\": \\"about:blank\\",
\\"width\\": 1920,
\\"height\\": 1080
},
\\"timestamp\\": 1542268800000
}
},
{
\\"type\\": 2,
@@ -128,8 +125,7 @@ exports[`attributes 1`] = `
\\"left\\": 0,
\\"top\\": 0
}
},
\\"timestamp\\": 1542268800000
}
},
{
\\"type\\": 3,
@@ -151,8 +147,7 @@ exports[`attributes 1`] = `
}
],
\\"adds\\": []
},
\\"timestamp\\": 1542268800000
}
}
]"
`;
@@ -161,13 +156,11 @@ exports[`block 1`] = `
"[
{
\\"type\\": 0,
\\"data\\": {},
\\"timestamp\\": 1542268800000
\\"data\\": {}
},
{
\\"type\\": 1,
\\"data\\": {},
\\"timestamp\\": 1542268800000
\\"data\\": {}
},
{
\\"type\\": 4,
@@ -175,8 +168,7 @@ exports[`block 1`] = `
\\"href\\": \\"about:blank\\",
\\"width\\": 1920,
\\"height\\": 1080
},
\\"timestamp\\": 1542268800000
}
},
{
\\"type\\": 2,
@@ -335,8 +327,7 @@ exports[`block 1`] = `
\\"left\\": 0,
\\"top\\": 0
}
},
\\"timestamp\\": 1542268800000
}
}
]"
`;
@@ -345,13 +336,11 @@ exports[`character-data 1`] = `
"[
{
\\"type\\": 0,
\\"data\\": {},
\\"timestamp\\": 1542268800000
\\"data\\": {}
},
{
\\"type\\": 1,
\\"data\\": {},
\\"timestamp\\": 1542268800000
\\"data\\": {}
},
{
\\"type\\": 4,
@@ -359,8 +348,7 @@ exports[`character-data 1`] = `
\\"href\\": \\"about:blank\\",
\\"width\\": 1920,
\\"height\\": 1080
},
\\"timestamp\\": 1542268800000
}
},
{
\\"type\\": 2,
@@ -469,8 +457,7 @@ exports[`character-data 1`] = `
\\"left\\": 0,
\\"top\\": 0
}
},
\\"timestamp\\": 1542268800000
}
},
{
\\"type\\": 3,
@@ -490,8 +477,7 @@ exports[`character-data 1`] = `
}
],
\\"adds\\": []
},
\\"timestamp\\": 1542268800000
}
}
]"
`;
@@ -500,13 +486,11 @@ exports[`child-list 1`] = `
"[
{
\\"type\\": 0,
\\"data\\": {},
\\"timestamp\\": 1542268800000
\\"data\\": {}
},
{
\\"type\\": 1,
\\"data\\": {},
\\"timestamp\\": 1542268800000
\\"data\\": {}
},
{
\\"type\\": 4,
@@ -514,8 +498,7 @@ exports[`child-list 1`] = `
\\"href\\": \\"about:blank\\",
\\"width\\": 1920,
\\"height\\": 1080
},
\\"timestamp\\": 1542268800000
}
},
{
\\"type\\": 2,
@@ -624,8 +607,7 @@ exports[`child-list 1`] = `
\\"left\\": 0,
\\"top\\": 0
}
},
\\"timestamp\\": 1542268800000
}
},
{
\\"type\\": 3,
@@ -653,8 +635,7 @@ exports[`child-list 1`] = `
}
}
]
},
\\"timestamp\\": 1542268800000
}
}
]"
`;
@@ -663,13 +644,11 @@ exports[`form 1`] = `
"[
{
\\"type\\": 0,
\\"data\\": {},
\\"timestamp\\": 1542268800000
\\"data\\": {}
},
{
\\"type\\": 1,
\\"data\\": {},
\\"timestamp\\": 1542268800000
\\"data\\": {}
},
{
\\"type\\": 4,
@@ -677,8 +656,7 @@ exports[`form 1`] = `
\\"href\\": \\"about:blank\\",
\\"width\\": 1920,
\\"height\\": 1080
},
\\"timestamp\\": 1542268800000
}
},
{
\\"type\\": 2,
@@ -1061,8 +1039,7 @@ exports[`form 1`] = `
\\"left\\": 0,
\\"top\\": 0
}
},
\\"timestamp\\": 1542268800000
}
},
{
\\"type\\": 3,
@@ -1070,8 +1047,7 @@ exports[`form 1`] = `
\\"source\\": 2,
\\"type\\": 5,
\\"id\\": 22
},
\\"timestamp\\": 1542268800000
}
},
{
\\"type\\": 3,
@@ -1080,8 +1056,7 @@ exports[`form 1`] = `
\\"text\\": \\"t\\",
\\"isChecked\\": false,
\\"id\\": 22
},
\\"timestamp\\": 1542268800000
}
},
{
\\"type\\": 3,
@@ -1090,8 +1065,7 @@ exports[`form 1`] = `
\\"text\\": \\"te\\",
\\"isChecked\\": false,
\\"id\\": 22
},
\\"timestamp\\": 1542268800000
}
},
{
\\"type\\": 3,
@@ -1100,8 +1074,7 @@ exports[`form 1`] = `
\\"text\\": \\"tes\\",
\\"isChecked\\": false,
\\"id\\": 22
},
\\"timestamp\\": 1542268800000
}
},
{
\\"type\\": 3,
@@ -1110,8 +1083,7 @@ exports[`form 1`] = `
\\"text\\": \\"test\\",
\\"isChecked\\": false,
\\"id\\": 22
},
\\"timestamp\\": 1542268800000
}
},
{
\\"type\\": 3,
@@ -1119,8 +1091,7 @@ exports[`form 1`] = `
\\"source\\": 2,
\\"type\\": 1,
\\"id\\": 27
},
\\"timestamp\\": 1542268800000
}
},
{
\\"type\\": 3,
@@ -1128,8 +1099,7 @@ exports[`form 1`] = `
\\"source\\": 2,
\\"type\\": 6,
\\"id\\": 22
},
\\"timestamp\\": 1542268800000
}
},
{
\\"type\\": 3,
@@ -1137,8 +1107,7 @@ exports[`form 1`] = `
\\"source\\": 2,
\\"type\\": 5,
\\"id\\": 27
},
\\"timestamp\\": 1542268800000
}
},
{
\\"type\\": 3,
@@ -1146,8 +1115,7 @@ exports[`form 1`] = `
\\"source\\": 2,
\\"type\\": 0,
\\"id\\": 27
},
\\"timestamp\\": 1542268800000
}
},
{
\\"type\\": 3,
@@ -1155,8 +1123,7 @@ exports[`form 1`] = `
\\"source\\": 2,
\\"type\\": 2,
\\"id\\": 27
},
\\"timestamp\\": 1542268800000
}
},
{
\\"type\\": 3,
@@ -1165,8 +1132,7 @@ exports[`form 1`] = `
\\"text\\": \\"on\\",
\\"isChecked\\": true,
\\"id\\": 27
},
\\"timestamp\\": 1542268800000
}
},
{
\\"type\\": 3,
@@ -1174,8 +1140,7 @@ exports[`form 1`] = `
\\"source\\": 2,
\\"type\\": 1,
\\"id\\": 32
},
\\"timestamp\\": 1542268800000
}
},
{
\\"type\\": 3,
@@ -1183,8 +1148,7 @@ exports[`form 1`] = `
\\"source\\": 2,
\\"type\\": 6,
\\"id\\": 27
},
\\"timestamp\\": 1542268800000
}
},
{
\\"type\\": 3,
@@ -1192,8 +1156,7 @@ exports[`form 1`] = `
\\"source\\": 2,
\\"type\\": 5,
\\"id\\": 32
},
\\"timestamp\\": 1542268800000
}
},
{
\\"type\\": 3,
@@ -1201,8 +1164,7 @@ exports[`form 1`] = `
\\"source\\": 2,
\\"type\\": 0,
\\"id\\": 32
},
\\"timestamp\\": 1542268800000
}
},
{
\\"type\\": 3,
@@ -1210,8 +1172,7 @@ exports[`form 1`] = `
\\"source\\": 2,
\\"type\\": 2,
\\"id\\": 32
},
\\"timestamp\\": 1542268800000
}
},
{
\\"type\\": 3,
@@ -1220,8 +1181,7 @@ exports[`form 1`] = `
\\"text\\": \\"on\\",
\\"isChecked\\": true,
\\"id\\": 32
},
\\"timestamp\\": 1542268800000
}
},
{
\\"type\\": 3,
@@ -1229,8 +1189,7 @@ exports[`form 1`] = `
\\"source\\": 2,
\\"type\\": 6,
\\"id\\": 32
},
\\"timestamp\\": 1542268800000
}
},
{
\\"type\\": 3,
@@ -1238,8 +1197,7 @@ exports[`form 1`] = `
\\"source\\": 2,
\\"type\\": 5,
\\"id\\": 37
},
\\"timestamp\\": 1542268800000
}
},
{
\\"type\\": 3,
@@ -1248,8 +1206,7 @@ exports[`form 1`] = `
\\"text\\": \\"t\\",
\\"isChecked\\": false,
\\"id\\": 37
},
\\"timestamp\\": 1542268800000
}
},
{
\\"type\\": 3,
@@ -1258,8 +1215,7 @@ exports[`form 1`] = `
\\"text\\": \\"te\\",
\\"isChecked\\": false,
\\"id\\": 37
},
\\"timestamp\\": 1542268800000
}
},
{
\\"type\\": 3,
@@ -1268,8 +1224,7 @@ exports[`form 1`] = `
\\"text\\": \\"tex\\",
\\"isChecked\\": false,
\\"id\\": 37
},
\\"timestamp\\": 1542268800000
}
},
{
\\"type\\": 3,
@@ -1278,8 +1233,7 @@ exports[`form 1`] = `
\\"text\\": \\"text\\",
\\"isChecked\\": false,
\\"id\\": 37
},
\\"timestamp\\": 1542268800000
}
},
{
\\"type\\": 3,
@@ -1288,8 +1242,7 @@ exports[`form 1`] = `
\\"text\\": \\"texta\\",
\\"isChecked\\": false,
\\"id\\": 37
},
\\"timestamp\\": 1542268800000
}
},
{
\\"type\\": 3,
@@ -1298,8 +1251,7 @@ exports[`form 1`] = `
\\"text\\": \\"textar\\",
\\"isChecked\\": false,
\\"id\\": 37
},
\\"timestamp\\": 1542268800000
}
},
{
\\"type\\": 3,
@@ -1308,8 +1260,7 @@ exports[`form 1`] = `
\\"text\\": \\"textare\\",
\\"isChecked\\": false,
\\"id\\": 37
},
\\"timestamp\\": 1542268800000
}
},
{
\\"type\\": 3,
@@ -1318,8 +1269,7 @@ exports[`form 1`] = `
\\"text\\": \\"textarea\\",
\\"isChecked\\": false,
\\"id\\": 37
},
\\"timestamp\\": 1542268800000
}
},
{
\\"type\\": 3,
@@ -1328,8 +1278,7 @@ exports[`form 1`] = `
\\"text\\": \\"textarea \\",
\\"isChecked\\": false,
\\"id\\": 37
},
\\"timestamp\\": 1542268800000
}
},
{
\\"type\\": 3,
@@ -1338,8 +1287,7 @@ exports[`form 1`] = `
\\"text\\": \\"textarea t\\",
\\"isChecked\\": false,
\\"id\\": 37
},
\\"timestamp\\": 1542268800000
}
},
{
\\"type\\": 3,
@@ -1348,8 +1296,7 @@ exports[`form 1`] = `
\\"text\\": \\"textarea te\\",
\\"isChecked\\": false,
\\"id\\": 37
},
\\"timestamp\\": 1542268800000
}
},
{
\\"type\\": 3,
@@ -1358,8 +1305,7 @@ exports[`form 1`] = `
\\"text\\": \\"textarea tes\\",
\\"isChecked\\": false,
\\"id\\": 37
},
\\"timestamp\\": 1542268800000
}
},
{
\\"type\\": 3,
@@ -1368,8 +1314,7 @@ exports[`form 1`] = `
\\"text\\": \\"textarea test\\",
\\"isChecked\\": false,
\\"id\\": 37
},
\\"timestamp\\": 1542268800000
}
},
{
\\"type\\": 3,
@@ -1378,8 +1323,7 @@ exports[`form 1`] = `
\\"text\\": \\"1\\",
\\"isChecked\\": false,
\\"id\\": 42
},
\\"timestamp\\": 1542268800000
}
}
]"
`;
@@ -1388,13 +1332,11 @@ exports[`ignore 1`] = `
"[
{
\\"type\\": 0,
\\"data\\": {},
\\"timestamp\\": 1542268800000
\\"data\\": {}
},
{
\\"type\\": 1,
\\"data\\": {},
\\"timestamp\\": 1542268800000
\\"data\\": {}
},
{
\\"type\\": 4,
@@ -1402,8 +1344,7 @@ exports[`ignore 1`] = `
\\"href\\": \\"about:blank\\",
\\"width\\": 1920,
\\"height\\": 1080
},
\\"timestamp\\": 1542268800000
}
},
{
\\"type\\": 2,
@@ -1633,8 +1574,7 @@ exports[`ignore 1`] = `
\\"left\\": 0,
\\"top\\": 0
}
},
\\"timestamp\\": 1542268800000
}
},
{
\\"type\\": 3,
@@ -1642,8 +1582,7 @@ exports[`ignore 1`] = `
\\"source\\": 2,
\\"type\\": 5,
\\"id\\": 22
},
\\"timestamp\\": 1542268800000
}
},
{
\\"type\\": 3,
@@ -1651,8 +1590,7 @@ exports[`ignore 1`] = `
\\"source\\": 2,
\\"type\\": 6,
\\"id\\": 22
},
\\"timestamp\\": 1542268800000
}
},
{
\\"type\\": 3,
@@ -1660,8 +1598,7 @@ exports[`ignore 1`] = `
\\"source\\": 2,
\\"type\\": 5,
\\"id\\": 27
},
\\"timestamp\\": 1542268800000
}
}
]"
`;
@@ -1670,13 +1607,11 @@ exports[`select2 1`] = `
"[
{
\\"type\\": 0,
\\"data\\": {},
\\"timestamp\\": 1542268800000
\\"data\\": {}
},
{
\\"type\\": 1,
\\"data\\": {},
\\"timestamp\\": 1542268800000
\\"data\\": {}
},
{
\\"type\\": 4,
@@ -1684,8 +1619,7 @@ exports[`select2 1`] = `
\\"href\\": \\"about:blank\\",
\\"width\\": 1920,
\\"height\\": 1080
},
\\"timestamp\\": 1542268800000
}
},
{
\\"type\\": 2,
@@ -2189,8 +2123,7 @@ exports[`select2 1`] = `
\\"left\\": 0,
\\"top\\": 0
}
},
\\"timestamp\\": 1542268800000
}
},
{
\\"type\\": 3,
@@ -2198,8 +2131,7 @@ exports[`select2 1`] = `
\\"source\\": 2,
\\"type\\": 1,
\\"id\\": 26
},
\\"timestamp\\": 1542268800000
}
},
{
\\"type\\": 3,
@@ -2207,8 +2139,7 @@ exports[`select2 1`] = `
\\"source\\": 2,
\\"type\\": 5,
\\"id\\": 42
},
\\"timestamp\\": 1542268800000
}
},
{
\\"type\\": 3,
@@ -2647,8 +2578,7 @@ exports[`select2 1`] = `
}
}
]
},
\\"timestamp\\": 1542268800000
}
},
{
\\"type\\": 3,
@@ -2656,8 +2586,7 @@ exports[`select2 1`] = `
\\"source\\": 2,
\\"type\\": 0,
\\"id\\": 93
},
\\"timestamp\\": 1542268800000
}
},
{
\\"type\\": 3,
@@ -2666,8 +2595,7 @@ exports[`select2 1`] = `
\\"text\\": \\"\\",
\\"isChecked\\": false,
\\"id\\": 81
},
\\"timestamp\\": 1542268800000
}
},
{
\\"type\\": 3,
@@ -2676,8 +2604,7 @@ exports[`select2 1`] = `
\\"text\\": \\"\\",
\\"isChecked\\": false,
\\"id\\": 35
},
\\"timestamp\\": 1542268800000
}
},
{
\\"type\\": 3,
@@ -2685,8 +2612,7 @@ exports[`select2 1`] = `
\\"source\\": 2,
\\"type\\": 1,
\\"id\\": 93
},
\\"timestamp\\": 1542268800000
}
},
{
\\"type\\": 3,
@@ -2694,8 +2620,7 @@ exports[`select2 1`] = `
\\"source\\": 2,
\\"type\\": 6,
\\"id\\": 81
},
\\"timestamp\\": 1542268800000
}
},
{
\\"type\\": 3,
@@ -2703,8 +2628,7 @@ exports[`select2 1`] = `
\\"source\\": 2,
\\"type\\": 5,
\\"id\\": 35
},
\\"timestamp\\": 1542268800000
}
},
{
\\"type\\": 3,
@@ -2775,8 +2699,7 @@ exports[`select2 1`] = `
}
}
]
},
\\"timestamp\\": 1542268800000
}
},
{
\\"type\\": 3,
@@ -2784,8 +2707,7 @@ exports[`select2 1`] = `
\\"source\\": 2,
\\"type\\": 0,
\\"id\\": 67
},
\\"timestamp\\": 1542268800000
}
}
]"
`;

View File

@@ -0,0 +1,247 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`async-checkout 1`] = `
"[
{
\\"type\\": 4,
\\"data\\": {
\\"href\\": \\"about:blank\\",
\\"width\\": 800,
\\"height\\": 600
}
},
{
\\"type\\": 2,
\\"data\\": {
\\"node\\": {
\\"type\\": 0,
\\"childNodes\\": [
{
\\"type\\": 2,
\\"tagName\\": \\"html\\",
\\"attributes\\": {},
\\"childNodes\\": [
{
\\"type\\": 2,
\\"tagName\\": \\"head\\",
\\"attributes\\": {},
\\"childNodes\\": [],
\\"id\\": 3
},
{
\\"type\\": 2,
\\"tagName\\": \\"body\\",
\\"attributes\\": {},
\\"childNodes\\": [
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 5
},
{
\\"type\\": 2,
\\"tagName\\": \\"input\\",
\\"attributes\\": {
\\"type\\": \\"text\\"
},
\\"childNodes\\": [],
\\"id\\": 6
},
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\\\n \\\\n \\",
\\"id\\": 7
}
],
\\"id\\": 4
}
],
\\"id\\": 2
}
],
\\"id\\": 1
},
\\"initialOffset\\": {
\\"left\\": 0,
\\"top\\": 0
}
}
},
{
\\"type\\": 3,
\\"data\\": {
\\"source\\": 0,
\\"texts\\": [],
\\"attributes\\": [],
\\"removes\\": [
{
\\"parentId\\": 4,
\\"id\\": 6
}
],
\\"adds\\": [
{
\\"parentId\\": 4,
\\"previousId\\": 7,
\\"nextId\\": null,
\\"node\\": {
\\"type\\": 2,
\\"tagName\\": \\"p\\",
\\"attributes\\": {},
\\"childNodes\\": [],
\\"id\\": 8
}
},
{
\\"parentId\\": 8,
\\"previousId\\": null,
\\"nextId\\": null,
\\"node\\": {
\\"type\\": 2,
\\"tagName\\": \\"span\\",
\\"attributes\\": {},
\\"childNodes\\": [],
\\"id\\": 9
}
}
]
}
},
{
\\"type\\": 3,
\\"data\\": {
\\"source\\": 0,
\\"texts\\": [],
\\"attributes\\": [],
\\"removes\\": [],
\\"adds\\": [
{
\\"parentId\\": 9,
\\"previousId\\": null,
\\"nextId\\": null,
\\"node\\": {
\\"type\\": 3,
\\"textContent\\": \\"test\\",
\\"id\\": 10
}
}
]
}
},
{
\\"type\\": 4,
\\"data\\": {
\\"href\\": \\"about:blank\\",
\\"width\\": 800,
\\"height\\": 600
}
},
{
\\"type\\": 2,
\\"data\\": {
\\"node\\": {
\\"type\\": 0,
\\"childNodes\\": [
{
\\"type\\": 2,
\\"tagName\\": \\"html\\",
\\"attributes\\": {},
\\"childNodes\\": [
{
\\"type\\": 2,
\\"tagName\\": \\"head\\",
\\"attributes\\": {},
\\"childNodes\\": [],
\\"id\\": 3
},
{
\\"type\\": 2,
\\"tagName\\": \\"body\\",
\\"attributes\\": {},
\\"childNodes\\": [
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 5
},
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\\\n \\\\n \\",
\\"id\\": 6
},
{
\\"type\\": 2,
\\"tagName\\": \\"p\\",
\\"attributes\\": {},
\\"childNodes\\": [
{
\\"type\\": 2,
\\"tagName\\": \\"span\\",
\\"attributes\\": {},
\\"childNodes\\": [
{
\\"type\\": 3,
\\"textContent\\": \\"test\\",
\\"id\\": 9
}
],
\\"id\\": 8
}
],
\\"id\\": 7
}
],
\\"id\\": 4
}
],
\\"id\\": 2
}
],
\\"id\\": 1
},
\\"initialOffset\\": {
\\"left\\": 0,
\\"top\\": 0
}
}
},
{
\\"type\\": 3,
\\"data\\": {
\\"source\\": 0,
\\"texts\\": [],
\\"attributes\\": [],
\\"removes\\": [
{
\\"parentId\\": 7,
\\"id\\": 8
}
],
\\"adds\\": [
{
\\"parentId\\": 4,
\\"previousId\\": 7,
\\"nextId\\": null,
\\"node\\": {
\\"type\\": 2,
\\"tagName\\": \\"span\\",
\\"attributes\\": {},
\\"childNodes\\": [],
\\"id\\": 10
}
},
{
\\"parentId\\": 10,
\\"previousId\\": null,
\\"nextId\\": null,
\\"node\\": {
\\"type\\": 3,
\\"textContent\\": \\"test\\",
\\"id\\": 11
}
}
]
}
}
]"
`;

View File

@@ -1,84 +1,7 @@
import * as fs from 'fs';
import * as path from 'path';
import * as puppeteer from 'puppeteer';
import { assert } from 'chai';
import { SnapshotState, toMatchSnapshot } from 'jest-snapshot';
import { EventType, IncrementalSource, eventWithTime } from '../src/types';
import { NodeType } from 'rrweb-snapshot';
function matchSnapshot(actual: string, testFile: string, testTitle: string) {
const snapshotState = new SnapshotState(testFile, {
updateSnapshot: process.env.SNAPSHOT_UPDATE ? 'all' : 'new',
});
const matcher = toMatchSnapshot.bind({
snapshotState,
currentTestName: testTitle,
});
const result = matcher(actual);
snapshotState.save();
return result;
}
/**
* Puppeteer may cast random mouse move which make our tests flaky.
* So we only do snapshot test with filtered events.
* @param snapshots incrementalSnapshotEvent[]
*/
function stringifySnapshots(snapshots: eventWithTime[]): string {
return JSON.stringify(
snapshots
.filter(s => {
if (
s.type === EventType.IncrementalSnapshot &&
s.data.source === IncrementalSource.MouseMove
) {
return false;
}
return true;
})
.map(s => {
if (s.type === EventType.Meta) {
s.data.href = 'about:blank';
}
// FIXME: travis coordinates seems different with my laptop
const coordinatesReg = /(bottom|top|left|right)/;
if (
s.type === EventType.IncrementalSnapshot &&
s.data.source === IncrementalSource.MouseInteraction
) {
delete s.data.x;
delete s.data.y;
}
if (
s.type === EventType.IncrementalSnapshot &&
s.data.source === IncrementalSource.Mutation
) {
s.data.attributes.forEach(a => {
if (
'style' in a.attributes &&
coordinatesReg.test(a.attributes.style!)
) {
delete a.attributes.style;
}
});
s.data.adds.forEach(add => {
if (
add.node.type === NodeType.Element &&
'style' in add.node.attributes &&
typeof add.node.attributes.style === 'string' &&
coordinatesReg.test(add.node.attributes.style)
) {
delete add.node.attributes.style;
}
});
}
return s;
}),
null,
2,
);
}
import { assertSnapshot } from './utils';
describe('record integration tests', () => {
function getHtml(fileName: string): string {
@@ -133,12 +56,7 @@ describe('record integration tests', () => {
await page.select('select', '1');
const snapshots = await page.evaluate('window.snapshots');
const result = matchSnapshot(
stringifySnapshots(snapshots),
__filename,
'form',
);
assert(result.pass, result.pass ? '' : result.report());
assertSnapshot(snapshots, __filename, 'form');
}).timeout(5000);
it('can record childList mutations', async () => {
@@ -156,12 +74,7 @@ describe('record integration tests', () => {
});
const snapshots = await page.evaluate('window.snapshots');
const result = matchSnapshot(
stringifySnapshots(snapshots),
__filename,
'child-list',
);
assert(result.pass, result.pass ? '' : result.report());
assertSnapshot(snapshots, __filename, 'child-list');
}).timeout(5000);
it('can record character data muatations', async () => {
@@ -181,12 +94,7 @@ describe('record integration tests', () => {
});
const snapshots = await page.evaluate('window.snapshots');
const result = matchSnapshot(
stringifySnapshots(snapshots),
__filename,
'character-data',
);
assert(result.pass, result.pass ? '' : result.report());
assertSnapshot(snapshots, __filename, 'character-data');
});
it('can record attribute mutation', async () => {
@@ -204,12 +112,7 @@ describe('record integration tests', () => {
});
const snapshots = await page.evaluate('window.snapshots');
const result = matchSnapshot(
stringifySnapshots(snapshots),
__filename,
'attributes',
);
assert(result.pass, result.pass ? '' : result.report());
assertSnapshot(snapshots, __filename, 'attributes');
});
it('can record node mutations', async () => {
@@ -228,12 +131,7 @@ describe('record integration tests', () => {
await page.click('.select2-container');
const snapshots = await page.evaluate('window.snapshots');
const result = matchSnapshot(
stringifySnapshots(snapshots),
__filename,
'select2',
);
assert(result.pass, result.pass ? '' : result.report());
assertSnapshot(snapshots, __filename, 'select2');
}).timeout(10000);
it('should not record input events on ignored elements', async () => {
@@ -245,12 +143,7 @@ describe('record integration tests', () => {
await page.type('.rr-ignore', 'secret');
const snapshots = await page.evaluate('window.snapshots');
const result = matchSnapshot(
stringifySnapshots(snapshots),
__filename,
'ignore',
);
assert(result.pass, result.pass ? '' : result.report());
assertSnapshot(snapshots, __filename, 'ignore');
});
it('should not record blocked elements and its child nodes', async () => {
@@ -263,11 +156,6 @@ describe('record integration tests', () => {
await page.click('#text');
const snapshots = await page.evaluate('window.snapshots');
const result = matchSnapshot(
stringifySnapshots(snapshots),
__filename,
'block',
);
assert(result.pass, result.pass ? '' : result.report());
assertSnapshot(snapshots, __filename, 'block');
});
});

175
test/record.test.ts Normal file
View File

@@ -0,0 +1,175 @@
/* tslint:disable no-console */
import * as fs from 'fs';
import * as path from 'path';
import * as puppeteer from 'puppeteer';
import { expect } from 'chai';
import {
recordOptions,
listenerHandler,
eventWithTime,
EventType,
} from '../src/types';
import { assertSnapshot } from './utils';
interface IWindow extends Window {
rrweb: {
record: (options: recordOptions) => listenerHandler | undefined;
};
emit: (e: eventWithTime) => undefined;
}
describe('record', () => {
before(async () => {
this.browser = await puppeteer.launch({
headless: false,
args: ['--no-sandbox'],
});
const bundlePath = path.resolve(__dirname, '../dist/rrweb.min.js');
this.code = fs.readFileSync(bundlePath, 'utf8');
});
beforeEach(async () => {
const page: puppeteer.Page = await this.browser.newPage();
await page.goto('about:blank');
await page.setContent(`
<html>
<body>
<input type="text" />
</body>
</html>
`);
await page.evaluate(this.code);
this.page = page;
this.events = [];
await this.page.exposeFunction('emit', (e: eventWithTime) => {
if (e.type === EventType.DomContentLoaded || e.type === EventType.Load) {
return;
}
this.events.push(e);
});
page.on('console', msg => console.log('PAGE LOG:', msg.text()));
});
afterEach(async () => {
await this.page.close();
});
after(async () => {
await this.browser.close();
});
it('will only have one full snapshot without checkout config', async () => {
await this.page.evaluate(() => {
const { record } = (window as IWindow).rrweb;
record({
emit: (window as IWindow).emit,
});
});
let count = 30;
while (count--) {
await this.page.type('input', 'a');
}
await this.page.waitFor(10);
expect(this.events.length).to.equal(33);
expect(
this.events.filter(
(event: eventWithTime) => event.type === EventType.Meta,
).length,
).to.equal(1);
expect(
this.events.filter(
(event: eventWithTime) => event.type === EventType.FullSnapshot,
).length,
).to.equal(1);
});
it('can checkout full snapshot by count', async () => {
await this.page.evaluate(() => {
const { record } = (window as IWindow).rrweb;
record({
emit: (window as IWindow).emit,
checkoutEveryNth: 10,
});
});
let count = 30;
while (count--) {
await this.page.type('input', 'a');
}
await this.page.waitFor(10);
expect(this.events.length).to.equal(39);
expect(
this.events.filter(
(event: eventWithTime) => event.type === EventType.Meta,
).length,
).to.equal(4);
expect(
this.events.filter(
(event: eventWithTime) => event.type === EventType.FullSnapshot,
).length,
).to.equal(4);
expect(this.events[1].type).to.equal(EventType.FullSnapshot);
expect(this.events[13].type).to.equal(EventType.FullSnapshot);
expect(this.events[25].type).to.equal(EventType.FullSnapshot);
expect(this.events[37].type).to.equal(EventType.FullSnapshot);
});
it('can checkout full snapshot by time', async () => {
await this.page.evaluate(() => {
const { record } = (window as IWindow).rrweb;
record({
emit: (window as IWindow).emit,
checkoutEveryNms: 500,
});
});
let count = 30;
while (count--) {
await this.page.type('input', 'a');
}
await this.page.waitFor(500);
expect(this.events.length).to.equal(33);
await this.page.type('input', 'a');
await this.page.waitFor(10);
expect(this.events.length).to.equal(36);
expect(
this.events.filter(
(event: eventWithTime) => event.type === EventType.Meta,
).length,
).to.equal(2);
expect(
this.events.filter(
(event: eventWithTime) => event.type === EventType.FullSnapshot,
).length,
).to.equal(2);
expect(this.events[1].type).to.equal(EventType.FullSnapshot);
expect(this.events[35].type).to.equal(EventType.FullSnapshot);
});
it('is safe to checkout during async callbacks', async () => {
await this.page.evaluate(() => {
const { record } = (window as IWindow).rrweb;
record({
emit: (window as IWindow).emit,
checkoutEveryNth: 2,
});
const p = document.createElement('p');
const span = document.createElement('span');
setTimeout(() => {
document.body.appendChild(p);
p.appendChild(span);
document.body.removeChild(document.querySelector('input')!);
}, 0);
setTimeout(() => {
span.innerText = 'test';
}, 10);
setTimeout(() => {
p.removeChild(span);
document.body.appendChild(span);
}, 10);
});
await this.page.waitFor(50);
assertSnapshot(this.events, __filename, 'async-checkout');
});
});

View File

@@ -1,4 +1,4 @@
/* tslint:disable no-string-literal */
/* tslint:disable no-string-literal no-console */
import * as fs from 'fs';
import * as path from 'path';

89
test/utils.ts Normal file
View File

@@ -0,0 +1,89 @@
import { SnapshotState, toMatchSnapshot } from 'jest-snapshot';
import { NodeType } from 'rrweb-snapshot';
import { assert } from 'chai';
import { EventType, IncrementalSource, eventWithTime } from '../src/types';
function matchSnapshot(actual: string, testFile: string, testTitle: string) {
const snapshotState = new SnapshotState(testFile, {
updateSnapshot: process.env.SNAPSHOT_UPDATE ? 'all' : 'new',
});
const matcher = toMatchSnapshot.bind({
snapshotState,
currentTestName: testTitle,
});
const result = matcher(actual);
snapshotState.save();
return result;
}
/**
* Puppeteer may cast random mouse move which make our tests flaky.
* So we only do snapshot test with filtered events.
* Also remove timestamp from event.
* @param snapshots incrementalSnapshotEvent[]
*/
function stringifySnapshots(snapshots: eventWithTime[]): string {
return JSON.stringify(
snapshots
.filter(s => {
if (
s.type === EventType.IncrementalSnapshot &&
s.data.source === IncrementalSource.MouseMove
) {
return false;
}
return true;
})
.map(s => {
if (s.type === EventType.Meta) {
s.data.href = 'about:blank';
}
// FIXME: travis coordinates seems different with my laptop
const coordinatesReg = /(bottom|top|left|right)/;
if (
s.type === EventType.IncrementalSnapshot &&
s.data.source === IncrementalSource.MouseInteraction
) {
delete s.data.x;
delete s.data.y;
}
if (
s.type === EventType.IncrementalSnapshot &&
s.data.source === IncrementalSource.Mutation
) {
s.data.attributes.forEach(a => {
if (
'style' in a.attributes &&
coordinatesReg.test(a.attributes.style!)
) {
delete a.attributes.style;
}
});
s.data.adds.forEach(add => {
if (
add.node.type === NodeType.Element &&
'style' in add.node.attributes &&
typeof add.node.attributes.style === 'string' &&
coordinatesReg.test(add.node.attributes.style)
) {
delete add.node.attributes.style;
}
});
}
delete s.timestamp;
return s;
}),
null,
2,
);
}
export function assertSnapshot(
snapshots: eventWithTime[],
filename: string,
name: string,
) {
const result = matchSnapshot(stringifySnapshots(snapshots), filename, name);
assert(result.pass, result.pass ? '' : result.report());
}