From 61312a0ad0de6105224e3f6dc162e15993fa1266 Mon Sep 17 00:00:00 2001 From: Yanzhen Yu Date: Wed, 1 Apr 2026 12:00:00 +0800 Subject: [PATCH] add mouse movement observer --- src/record/index.ts | 10 ++++++++ src/record/observer.ts | 52 ++++++++++++++++++++++++++++++++++++++---- src/types.ts | 30 ++++++++++++++++++++---- src/utils.ts | 36 ++++++++++++++++++++++++++++- 4 files changed, 117 insertions(+), 11 deletions(-) diff --git a/src/record/index.ts b/src/record/index.ts index 3040ee4b..5d543fcf 100644 --- a/src/record/index.ts +++ b/src/record/index.ts @@ -60,6 +60,16 @@ function record(options: recordOptions) { }, }), ), + mousemoveCb: positions => + emit( + wrapEvent({ + type: EventType.IncrementalSnapshot, + data: { + source: IncrementalSource.MouseMove, + positions, + }, + }), + ), }); }); } catch (error) { diff --git a/src/record/observer.ts b/src/record/observer.ts index 0c851a1b..b0b2482e 100644 --- a/src/record/observer.ts +++ b/src/record/observer.ts @@ -1,5 +1,5 @@ import { INode } from 'rrweb-snapshot'; -import { mirror } from '../utils'; +import { mirror, throttle } from '../utils'; import { mutationCallBack, textMutation, @@ -7,6 +7,8 @@ import { removedNodeMutation, addedNodeMutation, observerParam, + mousemoveCallBack, + mousePosition, } from '../types'; function initMutationObserver(cb: mutationCallBack): MutationObserver { @@ -99,9 +101,49 @@ function initMutationObserver(cb: mutationCallBack): MutationObserver { return observer; } -export default function initObservers(o: observerParam) { - const mutationObserver = initMutationObserver(o.mutationCb); - return { - mutationObserver, +function initMousemoveObserver(cb: mousemoveCallBack): () => void { + let positions: mousePosition[] = []; + let timeBaseline: number | null; + const wrappedCb = throttle(() => { + const totalOffset = Date.now() - timeBaseline!; + cb( + positions.map(p => { + p.timeOffset -= totalOffset; + return p; + }), + ); + positions = []; + timeBaseline = null; + }, 500); + const updatePosition = throttle( + evt => { + const { clientX, clientY } = evt; + if (!timeBaseline) { + timeBaseline = Date.now(); + } + positions.push({ + x: clientX, + y: clientY, + timeOffset: Date.now() - timeBaseline, + }); + wrappedCb(); + }, + 20, + { + trailing: false, + }, + ); + document.addEventListener('mousemove', updatePosition); + return () => { + document.removeEventListener('mousemove', updatePosition); + }; +} + +export default function initObservers(o: observerParam) { + const mutationObserver = initMutationObserver(o.mutationCb); + const mousemoveHandler = initMousemoveObserver(o.mousemoveCb); + return { + mutationObserver, + mousemoveHandler, }; } diff --git a/src/types.ts b/src/types.ts index 949cc5d7..d141eb12 100644 --- a/src/types.ts +++ b/src/types.ts @@ -26,20 +26,26 @@ export type fullSnapshotEvent = { }; }; -export enum IncrementalSource { - Mutation, -} - export type incrementalSnapshotEvent = { type: EventType.IncrementalSnapshot; data: incrementalData; }; +export enum IncrementalSource { + Mutation, + MouseMove, +} + export type mutationData = { source: IncrementalSource.Mutation; } & mutationCallbackParam; -export type incrementalData = mutationData; +export type mousemoveData = { + source: IncrementalSource.MouseMove; + positions: mousePosition[]; +}; + +export type incrementalData = mutationData | mousemoveData; export type event = | domContentLoadedEvent @@ -57,6 +63,7 @@ export type recordOptions = { export type observerParam = { mutationCb: mutationCallBack; + mousemoveCb: mousemoveCallBack; }; export type textMutation = { @@ -92,8 +99,21 @@ type mutationCallbackParam = { export type mutationCallBack = (m: mutationCallbackParam) => void; +export type mousemoveCallBack = (m: mousePosition[]) => void; + +export type mousePosition = { + x: number; + y: number; + timeOffset: number; +}; + export type Mirror = { map: idNodeMap; getId: (n: INode) => number; getNode: (id: number) => INode; }; + +export type throttleOptions = { + leading?: boolean; + trailing?: boolean; +}; diff --git a/src/utils.ts b/src/utils.ts index 64a73b68..487fd087 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,4 +1,4 @@ -import { Mirror } from './types'; +import { Mirror, throttleOptions } from './types'; export const mirror: Mirror = { map: {}, @@ -9,3 +9,37 @@ export const mirror: Mirror = { return mirror.map[id]; }, }; + +// copy from underscore +export function throttle( + func: (arg: T) => void, + wait: number, + options: throttleOptions = {}, +) { + let timeout: number | null = null; + let previous = 0; + // tslint:disable-next-line: only-arrow-functions + return function() { + let now = Date.now(); + if (!previous && options.leading === false) { + previous = now; + } + let remaining = wait - (now - previous); + let context = this; + let args = arguments; + if (remaining <= 0 || remaining > wait) { + if (timeout) { + clearTimeout(timeout); + timeout = null; + } + previous = now; + func.apply(context, args); + } else if (!timeout && options.trailing !== false) { + timeout = setTimeout(() => { + previous = options.leading === false ? 0 : Date.now(); + timeout = null; + func.apply(context, args); + }, remaining); + } + }; +}