// src/utils/trackingHelpers.js // PostHog 追踪性能优化工具 - 使用 requestIdleCallback 延迟非关键事件 import { shouldTrackImmediately } from '../constants/tracking'; /** * requestIdleCallback Polyfill * Safari 和旧浏览器不支持 requestIdleCallback,使用 setTimeout 降级 * * @param {Function} callback - 回调函数 * @param {Object} options - 配置选项 * @param {number} options.timeout - 超时时间(毫秒) * @returns {number} 定时器 ID */ const requestIdleCallbackPolyfill = (callback, options = {}) => { const timeout = options.timeout || 2000; const start = Date.now(); return setTimeout(() => { callback({ didTimeout: false, timeRemaining: () => Math.max(0, 50 - (Date.now() - start)), }); }, 1); }; /** * cancelIdleCallback Polyfill * * @param {number} id - 定时器 ID */ const cancelIdleCallbackPolyfill = (id) => { clearTimeout(id); }; // 使用原生 API 或 polyfill const requestIdleCallbackCompat = typeof window !== 'undefined' && window.requestIdleCallback ? window.requestIdleCallback.bind(window) : requestIdleCallbackPolyfill; const cancelIdleCallbackCompat = typeof window !== 'undefined' && window.cancelIdleCallback ? window.cancelIdleCallback.bind(window) : cancelIdleCallbackPolyfill; // ==================== 待发送事件队列 ==================== /** * 待发送事件队列(用于批量发送优化) * @type {Array<{trackFn: Function, args: Array}>} */ let pendingEvents = []; /** * 已调度的 idle callback ID(防止重复调度) * @type {number|null} */ let scheduledCallbackId = null; /** * 刷新待发送事件队列 * 立即执行所有待发送的追踪事件 */ const flushPendingEvents = () => { if (pendingEvents.length === 0) return; const eventsToFlush = [...pendingEvents]; pendingEvents = []; eventsToFlush.forEach(({ trackFn, args }) => { try { trackFn(...args); } catch (error) { console.error('❌ [trackingHelpers] Failed to flush event:', error); } }); if (process.env.NODE_ENV === 'development') { console.log( `%c✅ [trackingHelpers] Flushed ${eventsToFlush.length} pending event(s)`, 'color: #10B981; font-weight: bold;' ); } }; /** * 处理空闲时执行待发送事件 * * @param {IdleDeadline} deadline - 空闲时间信息 */ const processIdleEvents = (deadline) => { scheduledCallbackId = null; // 如果超时或队列为空,强制刷新 if (deadline.didTimeout || pendingEvents.length === 0) { flushPendingEvents(); return; } // 在空闲时间内尽可能多地处理事件 while (pendingEvents.length > 0 && deadline.timeRemaining() > 0) { const { trackFn, args } = pendingEvents.shift(); try { trackFn(...args); } catch (error) { console.error('❌ [trackingHelpers] Failed to track event:', error); } } // 如果还有未处理的事件,继续调度 if (pendingEvents.length > 0) { scheduledCallbackId = requestIdleCallbackCompat(processIdleEvents, { timeout: 2000, }); } }; // ==================== 公共 API ==================== /** * 在浏览器空闲时追踪事件(非关键事件优化) * * 使用 requestIdleCallback API 延迟事件追踪到浏览器空闲时执行, * 避免阻塞主线程,提升页面交互响应速度。 * * **适用场景**: * - 页面浏览事件(page_viewed) * - 列表查看事件(list_viewed) * - 筛选/排序事件(filter_applied, sorted) * - 低优先级交互事件 * * **不适用场景**: * - 关键业务事件(登录、支付、关注) * - 用户明确操作事件(按钮点击、详情打开) * - 需要实时追踪的事件 * * @param {Function} trackFn - PostHog 追踪函数(如 track, trackPageView) * @param {...any} args - 传递给追踪函数的参数 * * @example * import { trackEventIdle } from '@utils/trackingHelpers'; * import { trackEvent } from '@lib/posthog'; * * // 延迟追踪页面浏览事件 * trackEventIdle(trackEvent, 'page_viewed', { page: '/community' }); * * // 延迟追踪筛选事件 * trackEventIdle(track, 'news_filter_applied', { importance: 'high' }); */ export const trackEventIdle = (trackFn, ...args) => { if (!trackFn || typeof trackFn !== 'function') { console.warn('⚠️ [trackingHelpers] trackFn must be a function'); return; } // 添加到待发送队列 pendingEvents.push({ trackFn, args }); if (process.env.NODE_ENV === 'development') { console.log( `%c⏱️ [trackingHelpers] Event queued for idle execution (queue: ${pendingEvents.length})`, 'color: #8B5CF6; font-weight: bold;', args[0] // 事件名称 ); } // 如果没有已调度的 callback,调度一个新的 if (scheduledCallbackId === null) { scheduledCallbackId = requestIdleCallbackCompat(processIdleEvents, { timeout: 2000, // 2秒超时保护,确保事件不会无限延迟 }); } }; /** * 立即追踪事件(关键事件) * * 同步执行追踪,不延迟。用于需要实时追踪的关键业务事件。 * * **适用场景**: * - 关键业务事件(登录、注册、支付、订阅) * - 用户明确操作(按钮点击、详情打开、搜索提交) * - 高优先级交互事件(关注、分享、评论) * - 需要准确时序的事件 * * @param {Function} trackFn - PostHog 追踪函数 * @param {...any} args - 传递给追踪函数的参数 * * @example * import { trackEventImmediate } from '@utils/trackingHelpers'; * import { trackEvent } from '@lib/posthog'; * * // 立即追踪登录事件 * trackEventImmediate(trackEvent, 'user_logged_in', { method: 'password' }); * * // 立即追踪详情打开事件 * trackEventImmediate(track, 'news_detail_opened', { news_id: 123 }); */ export const trackEventImmediate = (trackFn, ...args) => { if (!trackFn || typeof trackFn !== 'function') { console.warn('⚠️ [trackingHelpers] trackFn must be a function'); return; } try { trackFn(...args); if (process.env.NODE_ENV === 'development') { console.log( `%c⚡ [trackingHelpers] Event tracked immediately`, 'color: #F59E0B; font-weight: bold;', args[0] // 事件名称 ); } } catch (error) { console.error('❌ [trackingHelpers] Failed to track event immediately:', error); } }; /** * 智能追踪包装器 * * 根据事件优先级自动选择立即追踪或空闲时追踪。 * 使用 `shouldTrackImmediately()` 判断事件优先级,简化调用方代码。 * * **适用场景**: * - 业务代码不需要关心事件优先级细节 * - 统一的追踪接口,自动优化性能 * - 易于维护和扩展 * * **优先级规则**(由 `src/constants/tracking.js` 配置): * - CRITICAL / HIGH → 立即追踪(`trackEventImmediate`) * - NORMAL / LOW → 空闲时追踪(`trackEventIdle`) * * @param {Function} trackFn - PostHog 追踪函数(如 `track` from `usePostHogTrack`) * @param {string} eventName - 事件名称(需在 `tracking.js` 中定义优先级) * @param {Object} properties - 事件属性 * * @example * import { smartTrack } from '@/utils/trackingHelpers'; * import { usePostHogTrack } from '@/hooks/usePostHogRedux'; * import { RETENTION_EVENTS } from '@/lib/constants'; * * const { track } = usePostHogTrack(); * * // 自动根据优先级选择追踪方式 * smartTrack(track, RETENTION_EVENTS.NEWS_ARTICLE_CLICKED, { news_id: 123 }); * smartTrack(track, RETENTION_EVENTS.NEWS_LIST_VIEWED, { total_count: 30 }); */ export const smartTrack = (trackFn, eventName, properties = {}) => { if (!trackFn || typeof trackFn !== 'function') { console.warn('⚠️ [trackingHelpers] smartTrack: trackFn must be a function'); return; } if (!eventName || typeof eventName !== 'string') { console.warn('⚠️ [trackingHelpers] smartTrack: eventName must be a string'); return; } // 根据事件优先级选择追踪方式 if (shouldTrackImmediately(eventName)) { // 高优先级事件:立即追踪 trackEventImmediate(trackFn, eventName, properties); } else { // 普通优先级事件:空闲时追踪 trackEventIdle(trackFn, eventName, properties); } }; /** * 页面卸载前刷新所有待发送事件 * * 在 beforeunload 事件中调用,确保页面关闭前发送所有待发送的追踪事件。 * 防止用户快速关闭页面时丢失事件数据。 * * **使用方式**: * ```javascript * import { flushPendingEventsBeforeUnload } from '@utils/trackingHelpers'; * * useEffect(() => { * window.addEventListener('beforeunload', flushPendingEventsBeforeUnload); * return () => { * window.removeEventListener('beforeunload', flushPendingEventsBeforeUnload); * }; * }, []); * ``` */ export const flushPendingEventsBeforeUnload = () => { // 取消已调度的 idle callback if (scheduledCallbackId !== null) { cancelIdleCallbackCompat(scheduledCallbackId); scheduledCallbackId = null; } // 立即刷新所有待发送事件 flushPendingEvents(); if (process.env.NODE_ENV === 'development') { console.log( '%c🔄 [trackingHelpers] Flushed pending events before unload', 'color: #3B82F6; font-weight: bold;' ); } }; /** * 获取当前待发送事件数量(调试用) * * @returns {number} 待发送事件数量 */ export const getPendingEventsCount = () => { return pendingEvents.length; }; /** * 清空待发送事件队列(测试用) */ export const clearPendingEvents = () => { if (scheduledCallbackId !== null) { cancelIdleCallbackCompat(scheduledCallbackId); scheduledCallbackId = null; } pendingEvents = []; }; // ==================== 默认导出 ==================== export default { trackEventIdle, trackEventImmediate, smartTrack, flushPendingEventsBeforeUnload, getPendingEventsCount, clearPendingEvents, };