// src/lib/posthog.js import posthog from 'posthog-js'; // 初始化状态管理(防止重复初始化) let isInitializing = false; let isInitialized = false; /** * Initialize PostHog SDK * Should be called once when the app starts */ export const initPostHog = () => { // 开发环境禁用 PostHog(减少日志噪音,仅生产环境启用) if (process.env.NODE_ENV === 'development') { return; } // 防止重复初始化 if (isInitializing || isInitialized) { console.log('📊 PostHog 已初始化或正在初始化中,跳过重复调用'); return; } // Only run in browser environment if (typeof window === 'undefined') return; const apiKey = process.env.REACT_APP_POSTHOG_KEY; const apiHost = process.env.REACT_APP_POSTHOG_HOST || 'https://app.posthog.com'; if (!apiKey) { console.warn('⚠️ PostHog API key not found. Analytics will be disabled.'); return; } isInitializing = true; try { posthog.init(apiKey, { api_host: apiHost, // 📄 页面浏览追踪 capture_pageview: true, // 自动捕获页面浏览事件 capture_pageleave: true, // 自动捕获用户离开页面事件 // 📹 会话录制配置(Session Recording) session_recording: { enabled: process.env.REACT_APP_ENABLE_SESSION_RECORDING === 'true', // 🔒 隐私保护:遮蔽敏感输入字段(录制时会自动打码) maskInputOptions: { password: true, // 遮蔽密码输入框 email: true, // 遮蔽邮箱输入框 phone: true, // 遮蔽手机号输入框 'data-sensitive': true, // 遮蔽带有 data-sensitive 属性的字段(可在 HTML 中自定义) }, // 📊 录制 Canvas 画布内容(用于记录图表、图形等可视化内容) recordCanvas: true, // 🌐 网络请求数据捕获(用于调试 API 问题) networkPayloadCapture: { recordHeaders: true, // 捕获请求头 recordBody: true, // 捕获请求体 // 🚫 敏感接口黑名单(不记录以下接口的数据) urlBlocklist: [ '/api/auth/session', // 会话接口 '/api/auth/login', // 登录接口 '/api/auth/register', // 注册接口 '/api/payment', // 支付接口 ], }, }, // ⚡ 性能优化:批量发送事件 batch_size: 10, // 每 10 个事件发送一次 batch_interval_ms: 3000, // 或每 3 秒发送一次(两个条件满足其一即发送) // 🔐 隐私设置 respect_dnt: true, // 尊重浏览器的"禁止追踪"(Do Not Track)设置 persistence: 'localStorage+cookie', // 同时使用 localStorage 和 Cookie 存储(提高可靠性) // 🚩 功能开关(Feature Flags)- 用于 A/B 测试和灰度发布 bootstrap: { featureFlags: {}, // 初始功能开关配置(可从服务端动态加载) }, // 🖱️ 自动捕获设置(Autocapture) autocapture: { // 自动捕获用户交互事件(点击、提交、修改等) dom_event_allowlist: ['click', 'submit', 'change'], // 捕获额外的元素属性 capture_copied_text: false, // 不捕获用户复制的文本(隐私保护) }, }); isInitialized = true; } catch (error) { // 忽略 AbortError(通常由热重载或快速导航引起) if (error.name === 'AbortError') { return; } } finally { isInitializing = false; } }; /** * Get PostHog instance * @returns {object} PostHog instance */ export const getPostHog = () => { return posthog; }; /** * Identify user with PostHog * Call this after successful login/registration * * @param {string} userId - Unique user identifier * @param {object} userProperties - User properties (email, name, subscription_tier, etc.) */ export const identifyUser = (userId, userProperties = {}) => { if (!userId) { console.warn('⚠️ Cannot identify user: userId is required'); return; } try { posthog.identify(userId, { email: userProperties.email, username: userProperties.username, subscription_tier: userProperties.subscription_tier || 'free', role: userProperties.role, registration_date: userProperties.registration_date, last_login: new Date().toISOString(), ...userProperties, }); } catch (error) { console.error('❌ User identification failed:', error); } }; /** * Update user properties * Use this to update user attributes without re-identifying * * @param {object} properties - Properties to update */ export const setUserProperties = (properties) => { try { posthog.people.set(properties); } catch (error) { console.error('❌ Failed to update user properties:', error); } }; /** * Track custom event * * @param {string} eventName - Name of the event * @param {object} properties - Event properties */ export const trackEvent = (eventName, properties = {}) => { try { posthog.capture(eventName, { ...properties, timestamp: new Date().toISOString(), }); } catch (error) { console.error('❌ Event tracking failed:', error); } }; /** * 异步追踪事件(不阻塞主线程) * 使用 requestIdleCallback 在浏览器空闲时发送事件 * * @param {string} eventName - 事件名称 * @param {object} properties - 事件属性 */ export const trackEventAsync = (eventName, properties = {}) => { // 浏览器支持 requestIdleCallback 时使用(推荐) if (typeof requestIdleCallback !== 'undefined') { requestIdleCallback( () => { trackEvent(eventName, properties); }, { timeout: 2000 } // 最多延迟 2 秒(防止永远不执行) ); } else { // 降级方案:使用 setTimeout(兼容性更好) setTimeout(() => { trackEvent(eventName, properties); }, 0); } }; /** * Track page view * * @param {string} pagePath - Current page path * @param {object} properties - Additional properties */ export const trackPageView = (pagePath, properties = {}) => { try { posthog.capture('$pageview', { $current_url: window.location.href, page_path: pagePath, page_title: document.title, referrer: document.referrer, ...properties, }); } catch (error) { console.error('❌ Page view tracking failed:', error); } }; /** * Reset user session * Call this on logout */ export const resetUser = () => { try { posthog.reset(); } catch (error) { console.error('❌ Session reset failed:', error); } }; /** * User opt-out from tracking */ export const optOut = () => { try { posthog.opt_out_capturing(); } catch (error) { console.error('❌ Opt-out failed:', error); } }; /** * User opt-in to tracking */ export const optIn = () => { try { posthog.opt_in_capturing(); } catch (error) { console.error('❌ Opt-in failed:', error); } }; /** * Check if user has opted out * @returns {boolean} */ export const hasOptedOut = () => { try { return posthog.has_opted_out_capturing(); } catch (error) { console.error('❌ Failed to check opt-out status:', error); return false; } }; /** * Get feature flag value * @param {string} flagKey - Feature flag key * @param {any} defaultValue - Default value if flag not found * @returns {any} Feature flag value */ export const getFeatureFlag = (flagKey, defaultValue = false) => { try { return posthog.getFeatureFlag(flagKey) || defaultValue; } catch (error) { console.error('❌ Failed to get feature flag:', error); return defaultValue; } }; /** * Check if feature flag is enabled * @param {string} flagKey - Feature flag key * @returns {boolean} */ export const isFeatureEnabled = (flagKey) => { try { return posthog.isFeatureEnabled(flagKey); } catch (error) { console.error('❌ Failed to check feature flag:', error); return false; } }; /** * Report performance metrics to PostHog * @param {object} metrics - Performance metrics object */ export const reportPerformanceMetrics = (metrics) => { // 仅在生产环境上报 if (process.env.NODE_ENV !== 'production') { console.log('📊 [开发环境] 性能指标(未上报到 PostHog):', metrics); return; } try { // 获取浏览器和设备信息 const browserInfo = { userAgent: navigator.userAgent, viewport: `${window.innerWidth}x${window.innerHeight}`, connection: navigator.connection?.effectiveType || 'unknown', deviceMemory: navigator.deviceMemory || 'unknown', hardwareConcurrency: navigator.hardwareConcurrency || 'unknown', }; // 上报性能指标 posthog.capture('Performance Metrics', { // 网络指标 dns_ms: metrics.dns, tcp_ms: metrics.tcp, ttfb_ms: metrics.ttfb, dom_load_ms: metrics.domLoad, resource_load_ms: metrics.resourceLoad, // 渲染指标 fp_ms: metrics.fp, fcp_ms: metrics.fcp, lcp_ms: metrics.lcp, // React 指标 react_init_ms: metrics.reactInit, auth_check_ms: metrics.authCheck, homepage_render_ms: metrics.homepageRender, // 总计 total_white_screen_ms: metrics.totalWhiteScreen, // 性能评分 performance_score: calculatePerformanceScore(metrics), // 浏览器和设备信息 ...browserInfo, // 时间戳 timestamp: new Date().toISOString(), }); console.log('✅ 性能指标已上报到 PostHog'); } catch (error) { console.error('❌ PostHog 性能指标上报失败:', error); } }; /** * Calculate overall performance score (0-100) * @param {object} metrics - Performance metrics * @returns {number} Score from 0 to 100 */ const calculatePerformanceScore = (metrics) => { let score = 100; // 白屏时间评分(权重 40%) if (metrics.totalWhiteScreen) { if (metrics.totalWhiteScreen > 3000) score -= 40; else if (metrics.totalWhiteScreen > 2000) score -= 20; else if (metrics.totalWhiteScreen > 1500) score -= 10; } // TTFB 评分(权重 20%) if (metrics.ttfb) { if (metrics.ttfb > 1000) score -= 20; else if (metrics.ttfb > 500) score -= 10; } // LCP 评分(权重 20%) if (metrics.lcp) { if (metrics.lcp > 4000) score -= 20; else if (metrics.lcp > 2500) score -= 10; } // FCP 评分(权重 10%) if (metrics.fcp) { if (metrics.fcp > 3000) score -= 10; else if (metrics.fcp > 1800) score -= 5; } // 认证检查评分(权重 10%) if (metrics.authCheck) { if (metrics.authCheck > 500) score -= 10; else if (metrics.authCheck > 300) score -= 5; } return Math.max(0, Math.min(100, score)); }; export default posthog;