UNPKG

user-behavior-analysis

Version:

一个用于追踪和记录用户在网页上各种交互行为的 TypeScript 库

817 lines (742 loc) 29.7 kB
/** * * 这个库用于追踪和记录用户在网页上的各种交互行为,包括: * - 鼠标移动和点击 * - 键盘活动 * - 滚动行为 * - 窗口大小变化 * - 页面可见性变化 * - 表单交互 * - 触摸事件 * - 媒体交互 * - 页面导航 */ /** * 鼠标位置数据类型 [x坐标, y坐标, 时间戳] */ type MousePosition = [number, number, string]; /** * 元素摘要信息 */ interface ElementSummary { /** 标签名 */ tagName: string; /** ID */ id: string; /** 类名 */ className: string; /** name 属性 */ name: string; /** value 属性 */ value: string; /** 文本内容 */ textContent: string; } /** * 点击详情数据类型 [x坐标, y坐标, 元素摘要, 时间戳] */ type ClickDetail = [number, number, ElementSummary, string]; /** * 滚动数据类型 [scrollX, scrollY, 时间戳] */ type ScrollData = [number, number, string]; /** * 窗口尺寸数据类型 [宽度, 高度, 时间戳] */ type WindowSize = [number, number, string]; /** * 可见性变化数据类型 [可见状态, 时间戳] */ type VisibilityChange = [DocumentVisibilityState, string]; /** * 键盘活动数据类型 [按键, 元素摘要, 时间戳] */ type KeyboardActivity = [string, ElementSummary, string]; /** * 导航历史数据类型 [URL, 时间戳] */ type NavigationHistory = [string, string]; /** * 表单交互数据类型 [表单元素摘要, 时间戳] */ type FormInteraction = [ElementSummary, string]; /** * 触摸事件数据类型 [事件类型, x坐标, y坐标, 元素摘要, 时间戳] */ type TouchEventData = [string, number, number, ElementSummary, string]; /** * 媒体交互数据类型 [事件类型, 媒体源, 时间戳] */ type MediaInteraction = [string, string, string]; /** * 用户信息接口 */ interface UserInfo { /** 窗口尺寸 */ windowSize: [number, number]; /** 应用程序代码名称 */ appCodeName: string; /** 应用程序名称 */ appName: string; /** 浏览器供应商 */ vendor: string; /** 操作系统平台 */ platform: string; /** 用户代理字符串 */ userAgent: string; } /** * 时间信息接口 */ interface TimeInfo { /** 开始时间 */ startTime: number | string; /** 当前时间 */ currentTime: number | string; /** 停止时间 */ stopTime: number | string; } /** * 点击信息接口 */ interface ClickInfo { /** 点击次数 */ clickCount: number; /** 点击详情列表 */ clickDetails: ClickDetail[]; } /** * 追踪结果数据接口 */ interface TrackingResults { /** 用户信息 */ userInfo: UserInfo; /** 时间信息 */ time: TimeInfo; /** 点击信息 */ clicks: ClickInfo; /** 鼠标移动轨迹 */ mouseMovements: MousePosition[]; /** 滚动记录 */ mouseScroll: ScrollData[]; /** 键盘活动记录 */ keyboardActivities: KeyboardActivity[]; /** 导航历史 */ navigationHistory: NavigationHistory[]; /** 表单交互记录 */ formInteractions: FormInteraction[]; /** 触摸事件记录 */ touchEvents: TouchEventData[]; /** 媒体交互记录 */ mediaInteractions: MediaInteraction[]; /** 窗口尺寸变化记录 */ windowSizes: WindowSize[]; /** 页面可见性变化记录 */ visibilitychanges: VisibilityChange[]; } /** * 配置选项接口 */ interface UserBehaviourConfig { /** 是否收集用户信息 */ userInfo?: boolean; /** 是否追踪点击事件 */ clicks?: boolean; /** 是否追踪鼠标移动 */ mouseMovement?: boolean; /** 鼠标移动记录间隔(秒) */ mouseMovementInterval?: number; /** 是否追踪鼠标滚动 */ mouseScroll?: boolean; /** 是否启用时间计数 */ timeCount?: boolean; /** 处理后是否清除数据 */ clearAfterProcess?: boolean; /** 数据处理间隔时间(秒) */ processTime?: number; /** 是否追踪窗口大小变化 */ windowResize?: boolean; /** 是否追踪页面可见性变化 */ visibilitychange?: boolean; /** 是否追踪键盘活动 */ keyboardActivity?: boolean; /** 是否追踪页面导航 */ pageNavigation?: boolean; /** 是否追踪表单交互 */ formInteractions?: boolean; /** 是否追踪触摸事件 */ touchEvents?: boolean; /** 是否追踪音视频交互 */ audioVideoInteraction?: boolean; /** 是否启用自定义事件注册 */ customEventRegistration?: boolean; /** 数据处理回调函数 */ processData?: (results: TrackingResults) => void; /** 是否自动发送事件 */ autoSendEvents?: boolean; /** 事件接收后台URL */ sendUrl?: string; } /** * 事件监听器存储接口 */ interface EventListeners { scroll: (() => void) | null; click: ((e: MouseEvent) => void) | null; mouseMovement: ((e: MouseEvent) => void) | null; windowResize: ((e: Event) => void) | null; visibilitychange: ((e: Event) => void) | null; keyboardActivity: ((e: KeyboardEvent) => void) | null; inputActivity: ((e: Event) => void) | null; touchStart: ((e: globalThis.TouchEvent) => void) | null; } /** * 事件处理函数集合接口 */ interface EventFunctions { scroll: () => void; click: (e: MouseEvent) => void; mouseMovement: (e: MouseEvent) => void; windowResize: (e: Event) => void; visibilitychange: (e: Event) => void; keyboardActivity: (e: KeyboardEvent) => void; inputActivity: (e: Event) => void; pageNavigation: () => void; formInteraction: (e: Event) => void; touchStart: (e: globalThis.TouchEvent) => void; mediaInteraction: (e: Event) => void; } /** * 内存管理接口 */ interface MemoryManager { /** 数据处理定时器 */ processInterval: number | null; /** 鼠标移动记录定时器 */ mouseInterval: number | null; /** 当前鼠标位置 */ mousePosition: MousePosition | []; /** 事件监听器引用 */ eventListeners: EventListeners; /** 事件处理函数集合 */ eventsFunctions: EventFunctions; } /** * 用户行为追踪库主类 */ (function (window) { const userBehaviour = (function () { /** * 默认配置选项 */ const defaults: Required<UserBehaviourConfig> = { userInfo: true, clicks: true, mouseMovement: true, mouseMovementInterval: 1, mouseScroll: true, timeCount: true, clearAfterProcess: true, processTime: 15, windowResize: true, visibilitychange: true, keyboardActivity: true, pageNavigation: true, formInteractions: true, touchEvents: true, audioVideoInteraction: true, customEventRegistration: true, processData: function (results: TrackingResults): void { console.log(results); }, autoSendEvents: false, sendUrl: '', }; /** * 用户自定义配置 */ let userConfig: Required<UserBehaviourConfig> = {} as Required<UserBehaviourConfig>; /** * 内存管理对象,存储定时器和事件监听器 */ const mem: MemoryManager = { processInterval: null, mouseInterval: null, mousePosition: [], // [x坐标, y坐标, 时间戳] eventListeners: { scroll: null, click: null, mouseMovement: null, windowResize: null, visibilitychange: null, keyboardActivity: null, inputActivity: null, touchStart: null }, eventsFunctions: { /** * 滚动事件处理函数 * 记录页面滚动位置和时间戳 */ scroll: (): void => { const scrollData: ScrollData = [window.scrollX, window.scrollY, getTimeStamp()]; results.mouseScroll.push(scrollData); sendEventData('scroll', scrollData); }, /** * 点击事件处理函数 * 记录点击位置、DOM路径和时间戳 * @param e 鼠标事件对象 */ click: (e: MouseEvent): void => { results.clicks.clickCount++; const path: string[] = []; let node = ""; // 构建DOM路径 e.composedPath().forEach((el: EventTarget, i: number) => { const element = el as Element; if ((i !== e.composedPath().length - 1) && (i !== e.composedPath().length - 2)) { node = element.localName || ""; // 添加类名 if (element.className && typeof element.className === 'string') { element.classList.forEach((clE: string) => { node += "." + clE; }); } // 添加ID if (element.id) { node += "#" + element.id; } path.push(node); } }); const elementSummary = getElementSummary(e.target); const clickDetail: ClickDetail = [e.clientX, e.clientY, elementSummary, getTimeStamp()]; results.clicks.clickDetails.push(clickDetail); sendEventData('click', clickDetail); }, /** * 鼠标移动事件处理函数 * 更新当前鼠标位置 * @param e 鼠标事件对象 */ mouseMovement: (e: MouseEvent): void => { mem.mousePosition = [e.clientX, e.clientY, getTimeStamp()]; }, /** * 窗口大小变化事件处理函数 * 记录新的窗口尺寸和时间戳 * @param e 事件对象 */ windowResize: (e: Event): void => { const windowSize: WindowSize = [window.innerWidth, window.innerHeight, getTimeStamp()]; results.windowSizes.push(windowSize); sendEventData('windowResize', windowSize); }, /** * 页面可见性变化事件处理函数 * 记录可见性状态变化并处理结果 * @param e 事件对象 */ visibilitychange: (e: Event): void => { const visibilityChange: VisibilityChange = [document.visibilityState, getTimeStamp()]; results.visibilitychanges.push(visibilityChange); sendEventData('visibilitychange', visibilityChange); processResults(); }, /** * 键盘活动事件处理函数 * 记录按键和时间戳 * @param e 键盘事件对象 */ keyboardActivity: (e: KeyboardEvent): void => { const elementSummary = getElementSummary(e.target); const keyboardActivity: KeyboardActivity = [e.key, elementSummary, getTimeStamp()]; results.keyboardActivities.push(keyboardActivity); sendEventData('keyboardActivity', keyboardActivity); }, /** * 输入活动事件处理函数 * 记录输入框的内容变化和时间戳 * @param e 输入事件对象 */ inputActivity: (e: Event): void => { const target = e.target as HTMLInputElement | HTMLTextAreaElement; const elementSummary = getElementSummary(e.target); const inputValue = target.value || ''; const inputActivity: KeyboardActivity = [inputValue, elementSummary, getTimeStamp()]; results.keyboardActivities.push(inputActivity); sendEventData('inputActivity', inputActivity); }, /** * 页面导航事件处理函数 * 记录页面URL变化和时间戳 */ pageNavigation: (): void => { const navigationHistory: NavigationHistory = [location.href, getTimeStamp()]; results.navigationHistory.push(navigationHistory); sendEventData('pageNavigation', navigationHistory); }, /** * 表单交互事件处理函数 * 记录表单提交事件 * @param e 事件对象 */ formInteraction: (e: Event): void => { e.preventDefault(); // 阻止表单正常提交 const elementSummary = getElementSummary(e.target); const formInteraction: FormInteraction = [elementSummary, getTimeStamp()]; results.formInteractions.push(formInteraction); sendEventData('formInteraction', formInteraction); // 可选:在追踪后程序化提交表单 }, /** * 触摸开始事件处理函数 * 记录触摸位置和时间戳 * @param e 触摸事件对象 */ touchStart: (e: globalThis.TouchEvent): void => { if (e.touches && e.touches.length > 0) { const touch = e.touches[0]; const elementSummary = getElementSummary(touch.target); const touchEventData: TouchEventData = ['touchstart', touch.clientX, touch.clientY, elementSummary, getTimeStamp()]; results.touchEvents.push(touchEventData); sendEventData('touchStart', touchEventData); } }, /** * 媒体交互事件处理函数 * 记录媒体播放事件 * @param e 事件对象 */ mediaInteraction: (e: Event): void => { const target = e.target as HTMLMediaElement; const mediaInteraction: MediaInteraction = [e.type, target.currentSrc || '', getTimeStamp()]; results.mediaInteractions.push(mediaInteraction); sendEventData('mediaInteraction', mediaInteraction); } } }; /** * 追踪结果数据存储 */ let results: TrackingResults = {} as TrackingResults; /** * 重置结果数据为初始状态 * 初始化所有追踪数据结构 */ function resetResults(): void { results = { userInfo: { windowSize: [window.innerWidth, window.innerHeight], appCodeName: navigator.appCodeName || '', appName: navigator.appName || '', vendor: navigator.vendor || '', platform: navigator.platform || '', userAgent: navigator.userAgent || '' }, time: { startTime: 0, currentTime: 0, stopTime: 0, }, clicks: { clickCount: 0, clickDetails: [] }, mouseMovements: [], mouseScroll: [], keyboardActivities: [], navigationHistory: [], formInteractions: [], touchEvents: [], mediaInteractions: [], windowSizes: [], visibilitychanges: [], }; } // 初始化结果数据 resetResults(); /** * 获取元素的摘要信息 * @param element HTML元素 * @returns 元素摘要对象 */ function getElementSummary(element: EventTarget | null): ElementSummary { if (!element || !(element instanceof HTMLElement)) { return { tagName: '', id: '', className: '', name: '', value: '', textContent: '' }; } const target = element as HTMLElement; return { tagName: target.tagName || '', id: target.id || '', className: typeof target.className === 'string' ? target.className : '', name: target.getAttribute('name') || '', value: (target as any).value === undefined || (target as any).value === null ? '' : String((target as any).value), textContent: target.textContent?.trim() || '' }; } /** * 发送事件数据到后台 * @param eventType 事件类型 * @param data 事件数据 */ function sendEventData(eventType: string, data: any): void { if (userConfig.autoSendEvents && userConfig.sendUrl) { const payload = { type: eventType, data: data, timestamp: getTimeStamp(), url: location.href, userInfo: results.userInfo // 附加用户信息以便后台分析 }; try { // 使用 sendBeacon 保证数据能可靠发送 if (navigator.sendBeacon) { console.log(`Sending event data to ${userConfig.sendUrl}`, payload); navigator.sendBeacon(userConfig.sendUrl, JSON.stringify(payload)); } } catch (error) { console.error('Failed to send event data:', error); } } } /** * 获取当前时间戳 * @returns 当前时间的毫秒时间戳 */ function getTimeStamp(): string { const now = new Date(); const year = now.getFullYear(); const month = (now.getMonth() + 1).toString().padStart(2, '0'); const day = now.getDate().toString().padStart(2, '0'); const hours = now.getHours().toString().padStart(2, '0'); const minutes = now.getMinutes().toString().padStart(2, '0'); const seconds = now.getSeconds().toString().padStart(2, '0'); return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`; } /** * 配置用户行为追踪选项 * 合并用户配置和默认配置 * @param ob 用户自定义配置对象 */ function config(ob: Partial<UserBehaviourConfig>): void { userConfig = {} as Required<UserBehaviourConfig>; // 遍历默认配置,使用用户配置覆盖默认值 (Object.keys(defaults) as Array<keyof UserBehaviourConfig>).forEach((key) => { const value = key in ob ? ob[key] : defaults[key]; (userConfig as any)[key] = value; }); } /** * 开始用户行为追踪 * 根据配置启用相应的事件监听器和定时器 */ function start(): void { // 如果没有提供配置,使用默认配置 if (Object.keys(userConfig).length !== Object.keys(defaults).length) { console.log("no config provided. using default.."); userConfig = defaults; } // 时间计数设置 if (userConfig.timeCount !== undefined && userConfig.timeCount) { results.time.startTime = getTimeStamp(); } // 鼠标移动追踪 if (userConfig.mouseMovement) { window.addEventListener("mousemove", mem.eventsFunctions.mouseMovement); mem.mouseInterval = window.setInterval(() => { if (mem.mousePosition && mem.mousePosition.length) { // 只有当鼠标位置发生变化时才记录 const lastMovement = results.mouseMovements[results.mouseMovements.length - 1]; if (!results.mouseMovements.length || ((mem.mousePosition[0] !== lastMovement[0]) && (mem.mousePosition[1] !== lastMovement[1]))) { const mousePosition = mem.mousePosition as MousePosition; results.mouseMovements.push(mousePosition); sendEventData('mouseMovement', mousePosition); } } }, defaults.mouseMovementInterval * 1000); } // 点击事件追踪 if (userConfig.clicks) { window.addEventListener("click", mem.eventsFunctions.click); } // 滚动事件追踪 if (userConfig.mouseScroll) { window.addEventListener("scroll", mem.eventsFunctions.scroll); } // 窗口大小变化追踪 if (userConfig.windowResize !== false) { window.addEventListener("resize", mem.eventsFunctions.windowResize); } // 页面可见性变化追踪 if (userConfig.visibilitychange !== false) { window.addEventListener("visibilitychange", mem.eventsFunctions.visibilitychange); } // 键盘活动追踪 if (userConfig.keyboardActivity) { window.addEventListener("keydown", mem.eventsFunctions.keyboardActivity); // 添加输入事件监听,用于捕获输入框的内容变化 document.addEventListener("input", mem.eventsFunctions.inputActivity); } // 页面导航追踪 if (userConfig.pageNavigation) { // 重写 pushState 方法以捕获程序化导航 const originalPushState = window.history.pushState; window.history.pushState = function pushState( data: any, unused: string, url?: string | URL | null ) { const ret = originalPushState.call(this, data, unused, url); window.dispatchEvent(new Event('pushstate')); window.dispatchEvent(new Event('locationchange')); return ret; }; // 监听各种导航事件 window.addEventListener('popstate', mem.eventsFunctions.pageNavigation); window.addEventListener('pushstate', mem.eventsFunctions.pageNavigation); window.addEventListener('locationchange', mem.eventsFunctions.pageNavigation); } // 表单交互追踪 if (userConfig.formInteractions) { document.querySelectorAll('form').forEach(form => form.addEventListener('submit', mem.eventsFunctions.formInteraction) ); } // 触摸事件追踪 if (userConfig.touchEvents) { window.addEventListener("touchstart", mem.eventsFunctions.touchStart); } // 音视频交互追踪 if (userConfig.audioVideoInteraction) { document.querySelectorAll('video, audio').forEach(media => { media.addEventListener('play', mem.eventsFunctions.mediaInteraction); media.addEventListener('pause', mem.eventsFunctions.mediaInteraction); media.addEventListener('ended', mem.eventsFunctions.mediaInteraction); media.addEventListener('timeupdate', mem.eventsFunctions.mediaInteraction); // 可以根据需要添加其他媒体事件 }); } // 设置数据处理定时器 if (typeof userConfig.processTime === 'number' && userConfig.processTime > 0) { mem.processInterval = window.setInterval(() => { processResults(); }, userConfig.processTime * 1000); } } /** * 处理追踪结果 * 调用用户定义的处理函数,并根据配置决定是否清除数据 */ function processResults(): void { userConfig.processData(result()); if (userConfig.clearAfterProcess) { resetResults(); } } /** * 停止用户行为追踪 * 清除所有定时器和事件监听器,并处理最终结果 */ function stop(): void { // 清除定时器 if (mem.processInterval !== null) { clearInterval(mem.processInterval); } if (mem.mouseInterval !== null) { clearInterval(mem.mouseInterval); } // 移除事件监听器 window.removeEventListener("scroll", mem.eventsFunctions.scroll); window.removeEventListener("click", mem.eventsFunctions.click); window.removeEventListener("mousemove", mem.eventsFunctions.mouseMovement); window.removeEventListener("resize", mem.eventsFunctions.windowResize); window.removeEventListener("visibilitychange", mem.eventsFunctions.visibilitychange); window.removeEventListener("keydown", mem.eventsFunctions.keyboardActivity); document.removeEventListener("input", mem.eventsFunctions.inputActivity); window.removeEventListener("touchstart", mem.eventsFunctions.touchStart); // 移除媒体事件监听器 if (userConfig.audioVideoInteraction) { document.querySelectorAll('video, audio').forEach(media => { media.removeEventListener('play', mem.eventsFunctions.mediaInteraction); media.removeEventListener('pause', mem.eventsFunctions.mediaInteraction); media.removeEventListener('ended', mem.eventsFunctions.mediaInteraction); media.removeEventListener('timeupdate', mem.eventsFunctions.mediaInteraction); }); } // 记录停止时间并处理最终结果 results.time.stopTime = getTimeStamp(); processResults(); } /** * 获取当前追踪结果 * 根据配置决定是否包含用户信息和时间信息 * @returns 当前的追踪结果数据 */ function result(): TrackingResults { // 如果配置为不收集用户信息,则删除用户信息 if (userConfig.userInfo === false && results.userInfo !== undefined) { delete (results as any).userInfo; } // 如果启用时间计数,更新当前时间 if (userConfig.timeCount !== undefined && userConfig.timeCount) { results.time.currentTime = getTimeStamp(); } return results; } /** * 显示当前配置 * @returns 当前使用的配置对象 */ function showConfig(): Required<UserBehaviourConfig> { if (Object.keys(userConfig).length !== Object.keys(defaults).length) { return defaults; } else { return userConfig; } } /** * 注册自定义事件 * 允许用户注册自定义事件监听器 * @param eventName 事件名称 * @param callback 事件回调函数 */ function registerCustomEvent(eventName: string, callback: EventListener): void { window.addEventListener(eventName, callback); } // 返回公共API return { /** 显示当前配置 */ showConfig, /** 设置配置选项 */ config, /** 开始追踪 */ start, /** 停止追踪 */ stop, /** 获取追踪结果 */ showResult: result, /** 手动处理结果 */ processResults, /** 注册自定义事件 */ registerCustomEvent, }; })(); // 在浏览器环境中将库挂载到全局对象 if (typeof window !== 'undefined') { (window as any).userBehaviour = userBehaviour; } // 支持模块导出 if (typeof module !== 'undefined' && module.exports) { module.exports = userBehaviour; } })(window);