Skip to content

Commit

Permalink
add checkout config to recorder
Browse files Browse the repository at this point in the history
  • Loading branch information
Yuyz0112 committed Jan 9, 2019
1 parent b45655e commit dee88a6
Show file tree
Hide file tree
Showing 5 changed files with 274 additions and 38 deletions.
74 changes: 74 additions & 0 deletions guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
99 changes: 63 additions & 36 deletions src/record/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
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);
}
}
emit(e, isCheckout);
};

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: {},
Expand All @@ -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: {
Expand All @@ -76,7 +103,7 @@ function record(options: recordOptions = {}): listenerHandler | undefined {
}),
),
mousemoveCb: positions =>
emit(
wrappedEmit(
wrapEvent({
type: EventType.IncrementalSnapshot,
data: {
Expand All @@ -86,7 +113,7 @@ function record(options: recordOptions = {}): listenerHandler | undefined {
}),
),
mouseInteractionCb: d =>
emit(
wrappedEmit(
wrapEvent({
type: EventType.IncrementalSnapshot,
data: {
Expand All @@ -96,7 +123,7 @@ function record(options: recordOptions = {}): listenerHandler | undefined {
}),
),
scrollCb: p =>
emit(
wrappedEmit(
wrapEvent({
type: EventType.IncrementalSnapshot,
data: {
Expand All @@ -106,7 +133,7 @@ function record(options: recordOptions = {}): listenerHandler | undefined {
}),
),
viewportResizeCb: d =>
emit(
wrappedEmit(
wrapEvent({
type: EventType.IncrementalSnapshot,
data: {
Expand All @@ -116,7 +143,7 @@ function record(options: recordOptions = {}): listenerHandler | undefined {
}),
),
inputCb: v =>
emit(
wrappedEmit(
wrapEvent({
type: EventType.IncrementalSnapshot,
data: {
Expand All @@ -138,7 +165,7 @@ function record(options: recordOptions = {}): listenerHandler | undefined {
on(
'load',
() => {
emit(
wrappedEmit(
wrapEvent({
type: EventType.Load,
data: {},
Expand Down
4 changes: 3 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
133 changes: 133 additions & 0 deletions test/record.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
/* 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';

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('can 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.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(36);
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[11].type).to.equal(EventType.FullSnapshot);
expect(this.events[22].type).to.equal(EventType.FullSnapshot);
expect(this.events[33].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(35);
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[33].type).to.equal(EventType.FullSnapshot);
});
});
2 changes: 1 addition & 1 deletion test/replayer.test.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down

0 comments on commit dee88a6

Please sign in to comment.