tweak the code of getting last session, without splice events array
This commit is contained in:
@@ -75,6 +75,33 @@ export type PlayerState =
|
|||||||
context: PlayerContext;
|
context: PlayerContext;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If the array have multiple meta and fullsnapshot events,
|
||||||
|
* return the events from last meta to the end.
|
||||||
|
*/
|
||||||
|
export function getLastSession(events: eventWithTime[]): eventWithTime[] {
|
||||||
|
const lastSession: eventWithTime[] = [];
|
||||||
|
|
||||||
|
let hasFullSnapshot = false;
|
||||||
|
let hasMeta = false;
|
||||||
|
|
||||||
|
for (let idx = events.length - 1; idx >= 0; idx--) {
|
||||||
|
const event = events[idx];
|
||||||
|
lastSession.unshift(event);
|
||||||
|
if (event.type === EventType.FullSnapshot) {
|
||||||
|
hasFullSnapshot = true;
|
||||||
|
}
|
||||||
|
if (event.type === EventType.Meta) {
|
||||||
|
hasMeta = true;
|
||||||
|
}
|
||||||
|
if (hasFullSnapshot && hasMeta) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return lastSession;
|
||||||
|
}
|
||||||
|
|
||||||
type PlayerAssets = {
|
type PlayerAssets = {
|
||||||
emitter: Emitter;
|
emitter: Emitter;
|
||||||
getCastFn(event: eventWithTime, isSync: boolean): () => void;
|
getCastFn(event: eventWithTime, isSync: boolean): () => void;
|
||||||
@@ -171,22 +198,10 @@ export function createPlayerService(
|
|||||||
play(ctx) {
|
play(ctx) {
|
||||||
const { timer, events, baselineTime, lastPlayedEvent } = ctx;
|
const { timer, events, baselineTime, lastPlayedEvent } = ctx;
|
||||||
timer.clear();
|
timer.clear();
|
||||||
const needed_events = new Array<eventWithTime>();
|
const neededEvents = getLastSession(events);
|
||||||
for (const event of events) {
|
|
||||||
if (event.timestamp < baselineTime &&
|
|
||||||
event.type === EventType.FullSnapshot &&
|
|
||||||
needed_events.length > 0 &&
|
|
||||||
needed_events[needed_events.length -1].type === EventType.Meta
|
|
||||||
) {
|
|
||||||
// delete everything before Meta
|
|
||||||
// so that we only rebuild from the latest full snapshot
|
|
||||||
needed_events.splice(0, needed_events.length -1);
|
|
||||||
}
|
|
||||||
needed_events.push(event);
|
|
||||||
}
|
|
||||||
|
|
||||||
const actions = new Array<actionWithDelay>();
|
const actions = new Array<actionWithDelay>();
|
||||||
for (const event of needed_events) {
|
for (const event of neededEvents) {
|
||||||
if (
|
if (
|
||||||
lastPlayedEvent &&
|
lastPlayedEvent &&
|
||||||
(event.timestamp <= lastPlayedEvent.timestamp ||
|
(event.timestamp <= lastPlayedEvent.timestamp ||
|
||||||
|
|||||||
19
test/machine.test.ts
Normal file
19
test/machine.test.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { expect } from 'chai';
|
||||||
|
import { getLastSession } from '../src/replay/machine';
|
||||||
|
import { sampleEvents } from './utils';
|
||||||
|
import { EventType } from '../src/types';
|
||||||
|
|
||||||
|
const events = sampleEvents.filter(
|
||||||
|
(e) => ![EventType.DomContentLoaded, EventType.Load].includes(e.type),
|
||||||
|
);
|
||||||
|
|
||||||
|
describe('get last session', () => {
|
||||||
|
it('will return all the events when there is only one session', () => {
|
||||||
|
expect(getLastSession(events)).to.deep.equal(events);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('will return last session when there is more than one in the events', () => {
|
||||||
|
const multiple = events.concat(events).concat(events);
|
||||||
|
expect(getLastSession(multiple)).to.deep.equal(events);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -5,115 +5,7 @@ import * as path from 'path';
|
|||||||
import * as puppeteer from 'puppeteer';
|
import * as puppeteer from 'puppeteer';
|
||||||
import { expect } from 'chai';
|
import { expect } from 'chai';
|
||||||
import { Suite } from 'mocha';
|
import { Suite } from 'mocha';
|
||||||
import {
|
import { launchPuppeteer, sampleEvents as events } from './utils';
|
||||||
EventType,
|
|
||||||
eventWithTime,
|
|
||||||
IncrementalSource,
|
|
||||||
MouseInteractions,
|
|
||||||
} from '../src/types';
|
|
||||||
import { Replayer } from '../src';
|
|
||||||
import { launchPuppeteer } from './utils';
|
|
||||||
|
|
||||||
const now = Date.now();
|
|
||||||
|
|
||||||
const events: eventWithTime[] = [
|
|
||||||
{
|
|
||||||
type: EventType.DomContentLoaded,
|
|
||||||
data: {},
|
|
||||||
timestamp: now,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: EventType.Load,
|
|
||||||
data: {},
|
|
||||||
timestamp: now + 1000,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: EventType.Meta,
|
|
||||||
data: {
|
|
||||||
href: 'http://localhost',
|
|
||||||
width: 1000,
|
|
||||||
height: 800,
|
|
||||||
},
|
|
||||||
timestamp: now + 1000,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: EventType.FullSnapshot,
|
|
||||||
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: [],
|
|
||||||
id: 4,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
id: 2,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
id: 1,
|
|
||||||
},
|
|
||||||
initialOffset: {
|
|
||||||
top: 0,
|
|
||||||
left: 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
timestamp: now + 1000,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: EventType.IncrementalSnapshot,
|
|
||||||
data: {
|
|
||||||
source: IncrementalSource.MouseInteraction,
|
|
||||||
type: MouseInteractions.Click,
|
|
||||||
id: 1,
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
},
|
|
||||||
timestamp: now + 2000,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: EventType.IncrementalSnapshot,
|
|
||||||
data: {
|
|
||||||
source: IncrementalSource.MouseInteraction,
|
|
||||||
type: MouseInteractions.Click,
|
|
||||||
id: 1,
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
},
|
|
||||||
timestamp: now + 3000,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: EventType.IncrementalSnapshot,
|
|
||||||
data: {
|
|
||||||
source: IncrementalSource.MouseInteraction,
|
|
||||||
type: MouseInteractions.Click,
|
|
||||||
id: 1,
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
},
|
|
||||||
timestamp: now + 4000,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
interface IWindow extends Window {
|
|
||||||
rrweb: {
|
|
||||||
Replayer: typeof Replayer;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ISuite extends Suite {
|
interface ISuite extends Suite {
|
||||||
code: string;
|
code: string;
|
||||||
@@ -121,7 +13,7 @@ interface ISuite extends Suite {
|
|||||||
page: puppeteer.Page;
|
page: puppeteer.Page;
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('replayer', function(this: ISuite) {
|
describe('replayer', function (this: ISuite) {
|
||||||
before(async () => {
|
before(async () => {
|
||||||
this.browser = await launchPuppeteer();
|
this.browser = await launchPuppeteer();
|
||||||
|
|
||||||
@@ -136,7 +28,7 @@ describe('replayer', function(this: ISuite) {
|
|||||||
await page.evaluate(`const events = ${JSON.stringify(events)}`);
|
await page.evaluate(`const events = ${JSON.stringify(events)}`);
|
||||||
this.page = page;
|
this.page = page;
|
||||||
|
|
||||||
page.on('console', msg => console.log('PAGE LOG:', msg.text()));
|
page.on('console', (msg) => console.log('PAGE LOG:', msg.text()));
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
@@ -148,60 +40,62 @@ describe('replayer', function(this: ISuite) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('can get meta data', async () => {
|
it('can get meta data', async () => {
|
||||||
const meta = await this.page.evaluate(() => {
|
const meta = await this.page.evaluate(`
|
||||||
const { Replayer } = ((window as unknown) as IWindow).rrweb;
|
const { Replayer } = rrweb;
|
||||||
const replayer = new Replayer(events);
|
const replayer = new Replayer(events);
|
||||||
return replayer.getMetaData();
|
replayer.getMetaData();
|
||||||
});
|
`);
|
||||||
expect(meta).to.deep.equal({
|
expect(meta).to.deep.equal({
|
||||||
|
startTime: events[0].timestamp,
|
||||||
|
endTime: events[events.length - 1].timestamp,
|
||||||
totalTime: events[events.length - 1].timestamp - events[0].timestamp,
|
totalTime: events[events.length - 1].timestamp - events[0].timestamp,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('will start actions when play', async () => {
|
it('will start actions when play', async () => {
|
||||||
const actionLength = await this.page.evaluate(() => {
|
const actionLength = await this.page.evaluate(`
|
||||||
const { Replayer } = ((window as unknown) as IWindow).rrweb;
|
const { Replayer } = rrweb;
|
||||||
const replayer = new Replayer(events);
|
const replayer = new Replayer(events);
|
||||||
replayer.play();
|
replayer.play();
|
||||||
return replayer['timer']['actions'].length;
|
replayer['timer']['actions'].length;
|
||||||
});
|
`);
|
||||||
expect(actionLength).to.equal(events.length);
|
expect(actionLength).to.equal(events.length);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('will clean actions when pause', async () => {
|
it('will clean actions when pause', async () => {
|
||||||
const actionLength = await this.page.evaluate(() => {
|
const actionLength = await this.page.evaluate(`
|
||||||
const { Replayer } = ((window as unknown) as IWindow).rrweb;
|
const { Replayer } = rrweb;
|
||||||
const replayer = new Replayer(events);
|
const replayer = new Replayer(events);
|
||||||
replayer.play();
|
replayer.play();
|
||||||
replayer.pause();
|
replayer.pause();
|
||||||
return replayer['timer']['actions'].length;
|
replayer['timer']['actions'].length;
|
||||||
});
|
`);
|
||||||
expect(actionLength).to.equal(0);
|
expect(actionLength).to.equal(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('can play at any time offset', async () => {
|
it('can play at any time offset', async () => {
|
||||||
const actionLength = await this.page.evaluate(() => {
|
const actionLength = await this.page.evaluate(`
|
||||||
const { Replayer } = ((window as unknown) as IWindow).rrweb;
|
const { Replayer } = rrweb;
|
||||||
const replayer = new Replayer(events);
|
const replayer = new Replayer(events);
|
||||||
replayer.play(1500);
|
replayer.play(1500);
|
||||||
return replayer['timer']['actions'].length;
|
replayer['timer']['actions'].length;
|
||||||
});
|
`);
|
||||||
expect(actionLength).to.equal(
|
expect(actionLength).to.equal(
|
||||||
events.filter(e => e.timestamp - events[0].timestamp >= 1500).length,
|
events.filter((e) => e.timestamp - events[0].timestamp >= 1500).length,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('can resume at any time offset', async () => {
|
it('can resume at any time offset', async () => {
|
||||||
const actionLength = await this.page.evaluate(() => {
|
const actionLength = await this.page.evaluate(`
|
||||||
const { Replayer } = ((window as unknown) as IWindow).rrweb;
|
const { Replayer } = rrweb;
|
||||||
const replayer = new Replayer(events);
|
const replayer = new Replayer(events);
|
||||||
replayer.play(1500);
|
replayer.play(1500);
|
||||||
replayer.pause();
|
replayer.pause();
|
||||||
replayer.resume(1500);
|
replayer.resume(1500);
|
||||||
return replayer['timer']['actions'].length;
|
replayer['timer']['actions'].length;
|
||||||
});
|
`);
|
||||||
expect(actionLength).to.equal(
|
expect(actionLength).to.equal(
|
||||||
events.filter(e => e.timestamp - events[0].timestamp >= 1500).length,
|
events.filter((e) => e.timestamp - events[0].timestamp >= 1500).length,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
109
test/utils.ts
109
test/utils.ts
@@ -1,7 +1,12 @@
|
|||||||
import { SnapshotState, toMatchSnapshot } from 'jest-snapshot';
|
import { SnapshotState, toMatchSnapshot } from 'jest-snapshot';
|
||||||
import { NodeType } from 'rrweb-snapshot';
|
import { NodeType } from 'rrweb-snapshot';
|
||||||
import { assert } from 'chai';
|
import { assert } from 'chai';
|
||||||
import { EventType, IncrementalSource, eventWithTime } from '../src/types';
|
import {
|
||||||
|
EventType,
|
||||||
|
IncrementalSource,
|
||||||
|
eventWithTime,
|
||||||
|
MouseInteractions,
|
||||||
|
} from '../src/types';
|
||||||
import * as puppeteer from 'puppeteer';
|
import * as puppeteer from 'puppeteer';
|
||||||
|
|
||||||
export async function launchPuppeteer() {
|
export async function launchPuppeteer() {
|
||||||
@@ -42,7 +47,7 @@ export function matchSnapshot(
|
|||||||
function stringifySnapshots(snapshots: eventWithTime[]): string {
|
function stringifySnapshots(snapshots: eventWithTime[]): string {
|
||||||
return JSON.stringify(
|
return JSON.stringify(
|
||||||
snapshots
|
snapshots
|
||||||
.filter(s => {
|
.filter((s) => {
|
||||||
if (
|
if (
|
||||||
s.type === EventType.IncrementalSnapshot &&
|
s.type === EventType.IncrementalSnapshot &&
|
||||||
s.data.source === IncrementalSource.MouseMove
|
s.data.source === IncrementalSource.MouseMove
|
||||||
@@ -51,7 +56,7 @@ function stringifySnapshots(snapshots: eventWithTime[]): string {
|
|||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
})
|
})
|
||||||
.map(s => {
|
.map((s) => {
|
||||||
if (s.type === EventType.Meta) {
|
if (s.type === EventType.Meta) {
|
||||||
s.data.href = 'about:blank';
|
s.data.href = 'about:blank';
|
||||||
}
|
}
|
||||||
@@ -68,7 +73,7 @@ function stringifySnapshots(snapshots: eventWithTime[]): string {
|
|||||||
s.type === EventType.IncrementalSnapshot &&
|
s.type === EventType.IncrementalSnapshot &&
|
||||||
s.data.source === IncrementalSource.Mutation
|
s.data.source === IncrementalSource.Mutation
|
||||||
) {
|
) {
|
||||||
s.data.attributes.forEach(a => {
|
s.data.attributes.forEach((a) => {
|
||||||
if (
|
if (
|
||||||
'style' in a.attributes &&
|
'style' in a.attributes &&
|
||||||
coordinatesReg.test(a.attributes.style!)
|
coordinatesReg.test(a.attributes.style!)
|
||||||
@@ -76,7 +81,7 @@ function stringifySnapshots(snapshots: eventWithTime[]): string {
|
|||||||
delete a.attributes.style;
|
delete a.attributes.style;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
s.data.adds.forEach(add => {
|
s.data.adds.forEach((add) => {
|
||||||
if (
|
if (
|
||||||
add.node.type === NodeType.Element &&
|
add.node.type === NodeType.Element &&
|
||||||
'style' in add.node.attributes &&
|
'style' in add.node.attributes &&
|
||||||
@@ -103,3 +108,97 @@ export function assertSnapshot(
|
|||||||
const result = matchSnapshot(stringifySnapshots(snapshots), filename, name);
|
const result = matchSnapshot(stringifySnapshots(snapshots), filename, name);
|
||||||
assert(result.pass, result.pass ? '' : result.report());
|
assert(result.pass, result.pass ? '' : result.report());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const now = Date.now();
|
||||||
|
export const sampleEvents: eventWithTime[] = [
|
||||||
|
{
|
||||||
|
type: EventType.DomContentLoaded,
|
||||||
|
data: {},
|
||||||
|
timestamp: now,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: EventType.Load,
|
||||||
|
data: {},
|
||||||
|
timestamp: now + 1000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: EventType.Meta,
|
||||||
|
data: {
|
||||||
|
href: 'http://localhost',
|
||||||
|
width: 1000,
|
||||||
|
height: 800,
|
||||||
|
},
|
||||||
|
timestamp: now + 1000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: EventType.FullSnapshot,
|
||||||
|
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: [],
|
||||||
|
id: 4,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
id: 2,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
id: 1,
|
||||||
|
},
|
||||||
|
initialOffset: {
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
timestamp: now + 1000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: EventType.IncrementalSnapshot,
|
||||||
|
data: {
|
||||||
|
source: IncrementalSource.MouseInteraction,
|
||||||
|
type: MouseInteractions.Click,
|
||||||
|
id: 1,
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
},
|
||||||
|
timestamp: now + 2000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: EventType.IncrementalSnapshot,
|
||||||
|
data: {
|
||||||
|
source: IncrementalSource.MouseInteraction,
|
||||||
|
type: MouseInteractions.Click,
|
||||||
|
id: 1,
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
},
|
||||||
|
timestamp: now + 3000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: EventType.IncrementalSnapshot,
|
||||||
|
data: {
|
||||||
|
source: IncrementalSource.MouseInteraction,
|
||||||
|
type: MouseInteractions.Click,
|
||||||
|
id: 1,
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
},
|
||||||
|
timestamp: now + 4000,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|||||||
Reference in New Issue
Block a user