From 73983e7e789111356f819b27f41517978710570a Mon Sep 17 00:00:00 2001 From: Yanzhen Yu Date: Wed, 1 Apr 2026 12:00:00 +0800 Subject: [PATCH] add skipInactive option Skip inactive time is an important and useful feature. We consider user interaction events as active, and check next user interaction event after apply incremental snapshot. If next user interaction event has a time gap larger than the threshold, we will set a dynamic speed value which will skip the inactive time interval in about 5 seconds. --- src/replay/index.ts | 57 +++++++++++++++++++++++++++++++++++++++++++-- src/replay/timer.ts | 8 +++---- src/types.ts | 2 ++ 3 files changed, 61 insertions(+), 6 deletions(-) diff --git a/src/replay/index.ts b/src/replay/index.ts index 316f525d..2a10aa18 100644 --- a/src/replay/index.ts +++ b/src/replay/index.ts @@ -21,6 +21,9 @@ import { mirror } from '../utils'; import injectStyleRules from './styles/inject-style'; import './styles/style.css'; +const SKIP_TIME_THRESHOLD = 10 * 1000; +const SKIP_TIME_INTERVAL = 5 * 1000; + smoothscroll.polyfill(); // https://github.com/rollup/rollup/issues/1267#issuecomment-296395734 @@ -31,6 +34,7 @@ const defaultConfig: playerConfig = { speed: 1, root: document.body, loadTimeout: 0, + skipInactive: false, }; export class Replayer { @@ -48,6 +52,9 @@ export class Replayer { // record last played event timestamp when paused private lastPlayedEvent: eventWithTime; + private nextUserInteractionEvent: eventWithTime | null; + private noramlSpeed: number; + private timer: Timer; private missingNodeRetryMap: missingNodeMap = {}; @@ -167,8 +174,10 @@ export class Replayer { const firstOffset = event.data.positions[0].timeOffset; // timeOffset is a negative offset to event.timestamp const firstTimestamp = event.timestamp + firstOffset; + event.delay = firstTimestamp - this.baselineTime; return firstTimestamp - this.baselineTime; } + event.delay = event.timestamp - this.baselineTime; return event.timestamp - this.baselineTime; } @@ -194,6 +203,36 @@ export class Replayer { case EventType.IncrementalSnapshot: castFn = () => { this.applyIncremental(event, isSync); + if (event === this.nextUserInteractionEvent) { + this.nextUserInteractionEvent = null; + this.setConfig({ speed: this.noramlSpeed }); + this.emitter.emit('skip-end'); + } + if (this.config.skipInactive && !this.nextUserInteractionEvent) { + for (const _event of this.events) { + if (_event.delay! <= event.delay!) { + continue; + } + if (this.isUserInteraction(_event)) { + if ( + _event.delay! - event.delay! > + SKIP_TIME_THRESHOLD * this.config.speed + ) { + this.nextUserInteractionEvent = _event; + } + break; + } + } + if (this.nextUserInteractionEvent) { + this.noramlSpeed = this.config.speed; + const skipTime = + this.nextUserInteractionEvent.delay! - event.delay!; + this.setConfig({ + speed: Math.round(skipTime / SKIP_TIME_INTERVAL), + }); + this.emitter.emit('skip-start'); + } + } }; break; default: @@ -457,8 +496,12 @@ export class Replayer { const target: HTMLInputElement = (mirror.getNode( d.id, ) as Node) as HTMLInputElement; - target.checked = d.isChecked; - target.value = d.text; + try { + target.checked = d.isChecked; + target.value = d.text; + } catch (error) { + // for safe + } break; } default: @@ -506,4 +549,14 @@ export class Replayer { currentEl = currentEl.parentElement; } } + + private isUserInteraction(event: eventWithTime): boolean { + if (event.type !== EventType.IncrementalSnapshot) { + return false; + } + return ( + event.data.source > IncrementalSource.Mutation && + event.data.source <= IncrementalSource.Input + ); + } } diff --git a/src/replay/timer.ts b/src/replay/timer.ts index 2255bb4d..be4434d0 100644 --- a/src/replay/timer.ts +++ b/src/replay/timer.ts @@ -28,15 +28,15 @@ export default class Timer { public start() { this.actions.sort((a1, a2) => a1.delay - a2.delay); let delayed = 0; - const start = performance.now(); + let lastTimestamp = performance.now(); const { actions, config } = this; const self = this; function check(time: number) { - delayed = time - start; + delayed += (time - lastTimestamp) * config.speed; + lastTimestamp = time; while (actions.length) { const action = actions[0]; - const delayNeeded = action.delay / config.speed; - if (delayed >= delayNeeded) { + if (delayed >= action.delay) { actions.shift(); action.doAction(); } else { diff --git a/src/types.ts b/src/types.ts index ea28e2fe..86ffb8f0 100644 --- a/src/types.ts +++ b/src/types.ts @@ -95,6 +95,7 @@ export type event = export type eventWithTime = event & { timestamp: number; + delay?: number; }; export type recordOptions = { @@ -227,6 +228,7 @@ export type playerConfig = { speed: number; root: Element; loadTimeout: number; + skipInactive: Boolean; }; export type playerMetaData = {