feat(PostHog): 添加双写模式支持自托管 + Cloud 并行
- .env.production: 主实例改为自托管 101.43.133.214:7050 - .env.production: 添加 Cloud 双写配置(DUAL_WRITE=true) - posthog.js: initPostHog() 支持初始化 Cloud 第二实例 - posthog.js: trackEvent() 事件同时发送到两个实例 - posthog.js: identifyUser() 用户身份同步到两个实例 - posthog.js: resetUser() 退出时重置两个实例 目的:验证自托管数据采集有效性 + Cloud 作为兜底策略 ⚠️ 部署前需将 REACT_APP_POSTHOG_CLOUD_KEY 替换为真实 Key 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -20,11 +20,16 @@ REACT_APP_API_URL=https://api.valuefrontier.cn
|
|||||||
# PostHog 分析配置(生产环境)
|
# PostHog 分析配置(生产环境)
|
||||||
# PostHog API Key(从 PostHog 项目设置中获取)
|
# PostHog API Key(从 PostHog 项目设置中获取)
|
||||||
REACT_APP_POSTHOG_KEY=phc_xKlRyG69Bx7hgOdFeCeLUvQWvSjw18ZKFgCwCeYezWF
|
REACT_APP_POSTHOG_KEY=phc_xKlRyG69Bx7hgOdFeCeLUvQWvSjw18ZKFgCwCeYezWF
|
||||||
# PostHog API Host(使用 PostHog Cloud)
|
# PostHog API Host(自托管实例)
|
||||||
REACT_APP_POSTHOG_HOST=https://app.posthog.com
|
REACT_APP_POSTHOG_HOST=http://101.43.133.214:7050
|
||||||
# 启用会话录制(Session Recording)用于回放用户操作、排查问题
|
# 启用会话录制(Session Recording)用于回放用户操作、排查问题
|
||||||
REACT_APP_ENABLE_SESSION_RECORDING=true
|
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 构建优化配置
|
# React 构建优化配置
|
||||||
# 禁用 source map 生成(生产环境不需要,提升打包速度和安全性)
|
# 禁用 source map 生成(生产环境不需要,提升打包速度和安全性)
|
||||||
GENERATE_SOURCEMAP=false
|
GENERATE_SOURCEMAP=false
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ let isInitialized = false;
|
|||||||
/**
|
/**
|
||||||
* Initialize PostHog SDK
|
* Initialize PostHog SDK
|
||||||
* Should be called once when the app starts
|
* Should be called once when the app starts
|
||||||
|
* 支持双写模式:同时发送到自托管和 Cloud
|
||||||
*/
|
*/
|
||||||
export const initPostHog = () => {
|
export const initPostHog = () => {
|
||||||
// 开发环境禁用 PostHog(减少日志噪音,仅生产环境启用)
|
// 开发环境禁用 PostHog(减少日志噪音,仅生产环境启用)
|
||||||
@@ -26,6 +27,7 @@ export const initPostHog = () => {
|
|||||||
|
|
||||||
const apiKey = process.env.REACT_APP_POSTHOG_KEY;
|
const apiKey = process.env.REACT_APP_POSTHOG_KEY;
|
||||||
const apiHost = process.env.REACT_APP_POSTHOG_HOST || 'https://app.posthog.com';
|
const apiHost = process.env.REACT_APP_POSTHOG_HOST || 'https://app.posthog.com';
|
||||||
|
const dualWrite = process.env.REACT_APP_POSTHOG_DUAL_WRITE === 'true';
|
||||||
|
|
||||||
if (!apiKey) {
|
if (!apiKey) {
|
||||||
console.warn('⚠️ PostHog API key not found. Analytics will be disabled.');
|
console.warn('⚠️ PostHog API key not found. Analytics will be disabled.');
|
||||||
@@ -35,6 +37,7 @@ export const initPostHog = () => {
|
|||||||
isInitializing = true;
|
isInitializing = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// 主实例(自托管)
|
||||||
posthog.init(apiKey, {
|
posthog.init(apiKey, {
|
||||||
api_host: apiHost,
|
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;
|
isInitialized = true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// 忽略 AbortError(通常由热重载或快速导航引起)
|
// 忽略 AbortError(通常由热重载或快速导航引起)
|
||||||
@@ -116,6 +139,7 @@ export const getPostHog = () => {
|
|||||||
/**
|
/**
|
||||||
* Identify user with PostHog
|
* Identify user with PostHog
|
||||||
* Call this after successful login/registration
|
* Call this after successful login/registration
|
||||||
|
* 双写模式下同时同步到 Cloud
|
||||||
*
|
*
|
||||||
* @param {string} userId - Unique user identifier
|
* @param {string} userId - Unique user identifier
|
||||||
* @param {object} userProperties - User properties (email, name, subscription_tier, etc.)
|
* @param {object} userProperties - User properties (email, name, subscription_tier, etc.)
|
||||||
@@ -126,8 +150,7 @@ export const identifyUser = (userId, userProperties = {}) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
const properties = {
|
||||||
posthog.identify(userId, {
|
|
||||||
email: userProperties.email,
|
email: userProperties.email,
|
||||||
username: userProperties.username,
|
username: userProperties.username,
|
||||||
subscription_tier: userProperties.subscription_tier || 'free',
|
subscription_tier: userProperties.subscription_tier || 'free',
|
||||||
@@ -135,7 +158,23 @@ export const identifyUser = (userId, userProperties = {}) => {
|
|||||||
registration_date: userProperties.registration_date,
|
registration_date: userProperties.registration_date,
|
||||||
last_login: new Date().toISOString(),
|
last_login: new Date().toISOString(),
|
||||||
...userProperties,
|
...userProperties,
|
||||||
});
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 主实例
|
||||||
|
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) {
|
} catch (error) {
|
||||||
console.error('❌ User identification failed:', error);
|
console.error('❌ User identification failed:', error);
|
||||||
}
|
}
|
||||||
@@ -157,16 +196,32 @@ export const setUserProperties = (properties) => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Track custom event
|
* Track custom event
|
||||||
|
* 双写模式下同时发送到 Cloud
|
||||||
*
|
*
|
||||||
* @param {string} eventName - Name of the event
|
* @param {string} eventName - Name of the event
|
||||||
* @param {object} properties - Event properties
|
* @param {object} properties - Event properties
|
||||||
*/
|
*/
|
||||||
export const trackEvent = (eventName, properties = {}) => {
|
export const trackEvent = (eventName, properties = {}) => {
|
||||||
try {
|
const eventProperties = {
|
||||||
posthog.capture(eventName, {
|
|
||||||
...properties,
|
...properties,
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
});
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 主实例
|
||||||
|
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) {
|
} catch (error) {
|
||||||
console.error('❌ Event tracking failed:', error);
|
console.error('❌ Event tracking failed:', error);
|
||||||
}
|
}
|
||||||
@@ -220,10 +275,24 @@ export const trackPageView = (pagePath, properties = {}) => {
|
|||||||
/**
|
/**
|
||||||
* Reset user session
|
* Reset user session
|
||||||
* Call this on logout
|
* Call this on logout
|
||||||
|
* 双写模式下同时重置 Cloud 实例
|
||||||
*/
|
*/
|
||||||
export const resetUser = () => {
|
export const resetUser = () => {
|
||||||
try {
|
try {
|
||||||
|
// 主实例
|
||||||
posthog.reset();
|
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) {
|
} catch (error) {
|
||||||
console.error('❌ Session reset failed:', error);
|
console.error('❌ Session reset failed:', error);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user