From ae71cf106a5f9d9ec402a95debea493e2c3db9b0 Mon Sep 17 00:00:00 2001 From: Yanzhen Yu Date: Wed, 1 Apr 2026 12:00:00 +0800 Subject: [PATCH] tweak the code of getting last session, without splice events array --- src/replay/machine.ts | 43 ++++++++---- test/machine.test.ts | 19 +++++ test/replayer.test.ts | 160 +++++++----------------------------------- test/utils.ts | 109 ++++++++++++++++++++++++++-- 4 files changed, 179 insertions(+), 152 deletions(-) create mode 100644 test/machine.test.ts diff --git a/src/replay/machine.ts b/src/replay/machine.ts index af9cc852..a30a9a55 100644 --- a/src/replay/machine.ts +++ b/src/replay/machine.ts @@ -75,6 +75,33 @@ export type PlayerState = 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 = { emitter: Emitter; getCastFn(event: eventWithTime, isSync: boolean): () => void; @@ -171,22 +198,10 @@ export function createPlayerService( play(ctx) { const { timer, events, baselineTime, lastPlayedEvent } = ctx; timer.clear(); - const needed_events = new Array(); - 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 neededEvents = getLastSession(events); const actions = new Array(); - for (const event of needed_events) { + for (const event of neededEvents) { if ( lastPlayedEvent && (event.timestamp <= lastPlayedEvent.timestamp || diff --git a/test/machine.test.ts b/test/machine.test.ts new file mode 100644 index 00000000..261a9a06 --- /dev/null +++ b/test/machine.test.ts @@ -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); + }); +}); diff --git a/test/replayer.test.ts b/test/replayer.test.ts index fe221921..896cda61 100644 --- a/test/replayer.test.ts +++ b/test/replayer.test.ts @@ -5,115 +5,7 @@ import * as path from 'path'; import * as puppeteer from 'puppeteer'; import { expect } from 'chai'; import { Suite } from 'mocha'; -import { - 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; - }; -} +import { launchPuppeteer, sampleEvents as events } from './utils'; interface ISuite extends Suite { code: string; @@ -121,7 +13,7 @@ interface ISuite extends Suite { page: puppeteer.Page; } -describe('replayer', function(this: ISuite) { +describe('replayer', function (this: ISuite) { before(async () => { this.browser = await launchPuppeteer(); @@ -136,7 +28,7 @@ describe('replayer', function(this: ISuite) { await page.evaluate(`const events = ${JSON.stringify(events)}`); 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 () => { @@ -148,60 +40,62 @@ describe('replayer', function(this: ISuite) { }); it('can get meta data', async () => { - const meta = await this.page.evaluate(() => { - const { Replayer } = ((window as unknown) as IWindow).rrweb; + const meta = await this.page.evaluate(` + const { Replayer } = rrweb; const replayer = new Replayer(events); - return replayer.getMetaData(); - }); + replayer.getMetaData(); + `); expect(meta).to.deep.equal({ + startTime: events[0].timestamp, + endTime: events[events.length - 1].timestamp, totalTime: events[events.length - 1].timestamp - events[0].timestamp, }); }); it('will start actions when play', async () => { - const actionLength = await this.page.evaluate(() => { - const { Replayer } = ((window as unknown) as IWindow).rrweb; + const actionLength = await this.page.evaluate(` + const { Replayer } = rrweb; const replayer = new Replayer(events); replayer.play(); - return replayer['timer']['actions'].length; - }); + replayer['timer']['actions'].length; + `); expect(actionLength).to.equal(events.length); }); it('will clean actions when pause', async () => { - const actionLength = await this.page.evaluate(() => { - const { Replayer } = ((window as unknown) as IWindow).rrweb; + const actionLength = await this.page.evaluate(` + const { Replayer } = rrweb; const replayer = new Replayer(events); replayer.play(); replayer.pause(); - return replayer['timer']['actions'].length; - }); + replayer['timer']['actions'].length; + `); expect(actionLength).to.equal(0); }); it('can play at any time offset', async () => { - const actionLength = await this.page.evaluate(() => { - const { Replayer } = ((window as unknown) as IWindow).rrweb; + const actionLength = await this.page.evaluate(` + const { Replayer } = rrweb; const replayer = new Replayer(events); replayer.play(1500); - return replayer['timer']['actions'].length; - }); + replayer['timer']['actions'].length; + `); 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 () => { - const actionLength = await this.page.evaluate(() => { - const { Replayer } = ((window as unknown) as IWindow).rrweb; + const actionLength = await this.page.evaluate(` + const { Replayer } = rrweb; const replayer = new Replayer(events); replayer.play(1500); replayer.pause(); replayer.resume(1500); - return replayer['timer']['actions'].length; - }); + replayer['timer']['actions'].length; + `); expect(actionLength).to.equal( - events.filter(e => e.timestamp - events[0].timestamp >= 1500).length, + events.filter((e) => e.timestamp - events[0].timestamp >= 1500).length, ); }); }); diff --git a/test/utils.ts b/test/utils.ts index 524b22e5..2372e2ba 100644 --- a/test/utils.ts +++ b/test/utils.ts @@ -1,7 +1,12 @@ import { SnapshotState, toMatchSnapshot } from 'jest-snapshot'; import { NodeType } from 'rrweb-snapshot'; import { assert } from 'chai'; -import { EventType, IncrementalSource, eventWithTime } from '../src/types'; +import { + EventType, + IncrementalSource, + eventWithTime, + MouseInteractions, +} from '../src/types'; import * as puppeteer from 'puppeteer'; export async function launchPuppeteer() { @@ -42,7 +47,7 @@ export function matchSnapshot( function stringifySnapshots(snapshots: eventWithTime[]): string { return JSON.stringify( snapshots - .filter(s => { + .filter((s) => { if ( s.type === EventType.IncrementalSnapshot && s.data.source === IncrementalSource.MouseMove @@ -51,7 +56,7 @@ function stringifySnapshots(snapshots: eventWithTime[]): string { } return true; }) - .map(s => { + .map((s) => { if (s.type === EventType.Meta) { s.data.href = 'about:blank'; } @@ -68,7 +73,7 @@ function stringifySnapshots(snapshots: eventWithTime[]): string { s.type === EventType.IncrementalSnapshot && s.data.source === IncrementalSource.Mutation ) { - s.data.attributes.forEach(a => { + s.data.attributes.forEach((a) => { if ( 'style' in a.attributes && coordinatesReg.test(a.attributes.style!) @@ -76,7 +81,7 @@ function stringifySnapshots(snapshots: eventWithTime[]): string { delete a.attributes.style; } }); - s.data.adds.forEach(add => { + s.data.adds.forEach((add) => { if ( add.node.type === NodeType.Element && 'style' in add.node.attributes && @@ -103,3 +108,97 @@ export function assertSnapshot( const result = matchSnapshot(stringifySnapshots(snapshots), filename, name); 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, + }, +];