diff --git a/.env.production b/.env.production index d10ff9d9..43c875c1 100644 --- a/.env.production +++ b/.env.production @@ -20,11 +20,16 @@ REACT_APP_API_URL=https://api.valuefrontier.cn # PostHog 分析配置(生产环境) # PostHog API Key(从 PostHog 项目设置中获取) REACT_APP_POSTHOG_KEY=phc_xKlRyG69Bx7hgOdFeCeLUvQWvSjw18ZKFgCwCeYezWF -# PostHog API Host(使用 PostHog Cloud) -REACT_APP_POSTHOG_HOST=https://app.posthog.com +# PostHog API Host(自托管实例) +REACT_APP_POSTHOG_HOST=http://101.43.133.214:7050 # 启用会话录制(Session Recording)用于回放用户操作、排查问题 REACT_APP_ENABLE_SESSION_RECORDING=true +# PostHog Cloud 双写配置(验证自托管有效性 + Cloud 兜底) +REACT_APP_POSTHOG_CLOUD_KEY=phc_xxxxxxx +REACT_APP_POSTHOG_CLOUD_HOST=https://us.posthog.com +REACT_APP_POSTHOG_DUAL_WRITE=true + # React 构建优化配置 # 禁用 source map 生成(生产环境不需要,提升打包速度和安全性) GENERATE_SOURCEMAP=false diff --git a/src/lib/posthog.js b/src/lib/posthog.js index 45d632a2..6da427e9 100644 --- a/src/lib/posthog.js +++ b/src/lib/posthog.js @@ -8,6 +8,7 @@ let isInitialized = false; /** * Initialize PostHog SDK * Should be called once when the app starts + * 支持双写模式:同时发送到自托管和 Cloud */ export const initPostHog = () => { // 开发环境禁用 PostHog(减少日志噪音,仅生产环境启用) @@ -26,6 +27,7 @@ export const initPostHog = () => { const apiKey = process.env.REACT_APP_POSTHOG_KEY; const apiHost = process.env.REACT_APP_POSTHOG_HOST || 'https://app.posthog.com'; + const dualWrite = process.env.REACT_APP_POSTHOG_DUAL_WRITE === 'true'; if (!apiKey) { console.warn('⚠️ PostHog API key not found. Analytics will be disabled.'); @@ -35,6 +37,7 @@ export const initPostHog = () => { isInitializing = true; try { + // 主实例(自托管) posthog.init(apiKey, { api_host: apiHost, @@ -94,6 +97,26 @@ export const initPostHog = () => { }, }); + // Cloud 实例(双写备份) + if (dualWrite) { + const cloudKey = process.env.REACT_APP_POSTHOG_CLOUD_KEY; + const cloudHost = process.env.REACT_APP_POSTHOG_CLOUD_HOST || 'https://us.posthog.com'; + + if (cloudKey && cloudKey !== 'phc_xxxxxxx') { + posthog.init(cloudKey, { + api_host: cloudHost, + capture_pageview: true, + capture_pageleave: true, + autocapture: true, + session_recording: { enabled: false }, // Cloud 不需要录屏,节省流量 + }, 'cloud'); // 第三个参数是实例名称 + + console.log('[PostHog] 双写模式已启用:自托管 + Cloud'); + } else { + console.warn('[PostHog] 双写模式已启用但 Cloud API Key 未配置'); + } + } + isInitialized = true; } catch (error) { // 忽略 AbortError(通常由热重载或快速导航引起) @@ -116,6 +139,7 @@ export const getPostHog = () => { /** * Identify user with PostHog * Call this after successful login/registration + * 双写模式下同时同步到 Cloud * * @param {string} userId - Unique user identifier * @param {object} userProperties - User properties (email, name, subscription_tier, etc.) @@ -126,16 +150,31 @@ export const identifyUser = (userId, userProperties = {}) => { return; } + const properties = { + 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, + }; + 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, - }); + // 主实例 + posthog.identify(userId, properties); + + // 双写到 Cloud + if (process.env.REACT_APP_POSTHOG_DUAL_WRITE === 'true') { + try { + const cloudInstance = posthog.getInstance('cloud'); + if (cloudInstance) { + cloudInstance.identify(userId, properties); + } + } catch (e) { + // 静默失败,不影响主流程 + } + } } catch (error) { console.error('❌ User identification failed:', error); } @@ -157,16 +196,32 @@ export const setUserProperties = (properties) => { /** * Track custom event + * 双写模式下同时发送到 Cloud * * @param {string} eventName - Name of the event * @param {object} properties - Event properties */ export const trackEvent = (eventName, properties = {}) => { + const eventProperties = { + ...properties, + timestamp: new Date().toISOString(), + }; + try { - posthog.capture(eventName, { - ...properties, - timestamp: new Date().toISOString(), - }); + // 主实例 + posthog.capture(eventName, eventProperties); + + // 双写到 Cloud + if (process.env.REACT_APP_POSTHOG_DUAL_WRITE === 'true') { + try { + const cloudInstance = posthog.getInstance('cloud'); + if (cloudInstance) { + cloudInstance.capture(eventName, eventProperties); + } + } catch (e) { + // 静默失败,不影响主流程 + } + } } catch (error) { console.error('❌ Event tracking failed:', error); } @@ -220,10 +275,24 @@ export const trackPageView = (pagePath, properties = {}) => { /** * Reset user session * Call this on logout + * 双写模式下同时重置 Cloud 实例 */ export const resetUser = () => { try { + // 主实例 posthog.reset(); + + // 双写:重置 Cloud 实例 + if (process.env.REACT_APP_POSTHOG_DUAL_WRITE === 'true') { + try { + const cloudInstance = posthog.getInstance('cloud'); + if (cloudInstance) { + cloudInstance.reset(); + } + } catch (e) { + // 静默失败 + } + } } catch (error) { console.error('❌ Session reset failed:', error); }