tweak the code of getting last session, without splice events array

This commit is contained in:
Yanzhen Yu
2026-04-01 12:00:00 +08:00
committed by yz-yu
parent 19acba745a
commit ae71cf106a
4 changed files with 179 additions and 152 deletions

View File

@@ -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
View 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);
});
});

View File

@@ -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;
@@ -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,
); );
}); });
}); });

View File

@@ -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,
},
];