Files
vf_react/src/lib/posthog.js
zdl a0d1790469 fix: 修复 PostHog AbortError 和重复初始化问题
## 问题
控制台报错:
```
[PostHog.js] AbortError: The user aborted a request.
```

## 根本原因

### 1. 热重载导致重复初始化
- 开发环境频繁热重载
- App.js 每次重载都调用 initPostHog()
- 之前的网络请求被新请求中断 → AbortError

### 2. 缺少初始化状态管理
- 没有防止重复初始化的锁
- 每次组件更新都可能触发新的初始化

## 解决方案

### 方案A: 防止重复初始化
添加初始化状态锁:
```javascript
let isInitializing = false;
let isInitialized = false;

export const initPostHog = () => {
  // 防止重复初始化
  if (isInitializing || isInitialized) {
    console.log('📊 PostHog 已初始化,跳过重复调用');
    return;
  }

  isInitializing = true;

  try {
    posthog.init(apiKey, {...});
    isInitialized = true;  // 成功后标记为已初始化
  } finally {
    isInitializing = false;  // 无论成功失败都重置标志
  }
};
```

### 方案B: 捕获并忽略 AbortError
在 catch 块中特殊处理:
```javascript
} catch (error) {
  // 忽略 AbortError(通常由热重载引起)
  if (error.name === 'AbortError') {
    console.log('⚠️ PostHog 初始化请求被中断(热重载)');
    return;  // 静默处理,不报错
  }
  console.error(' PostHog initialization failed:', error);
}
```

## 影响
-  修复 AbortError 警告
-  防止热重载时重复初始化
-  提升开发体验
-  不影响生产环境

## 验证
重启开发服务器后:
-  不再有 AbortError
-  PostHog 只初始化一次
-  热重载正常工作

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-30 18:49:03 +08:00

292 lines
7.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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 = () => {
// 防止重复初始化
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,
// Pageview tracking - manual control for better accuracy
capture_pageview: false, // We'll manually capture with custom properties
capture_pageleave: true, // Auto-capture when user leaves page
// Session Recording Configuration
session_recording: {
enabled: process.env.REACT_APP_ENABLE_SESSION_RECORDING === 'true',
// Privacy: Mask sensitive input fields
maskInputOptions: {
password: true,
email: true,
phone: true,
'data-sensitive': true, // Custom attribute for sensitive fields
},
// Record canvas for charts/graphs
recordCanvas: true,
// Network payload capture (useful for debugging API issues)
networkPayloadCapture: {
recordHeaders: true,
recordBody: true,
// Don't record sensitive endpoints
urlBlocklist: [
'/api/auth/session',
'/api/auth/login',
'/api/auth/register',
'/api/payment',
],
},
},
// Performance optimization
batch_size: 10, // Send events in batches of 10
batch_interval_ms: 3000, // Or every 3 seconds
// Privacy settings
respect_dnt: true, // Respect Do Not Track browser setting
persistence: 'localStorage+cookie', // Use both for reliability
// Feature flags (for A/B testing)
bootstrap: {
featureFlags: {},
},
// Autocapture settings
autocapture: {
// Automatically capture clicks on buttons, links, etc.
dom_event_allowlist: ['click', 'submit', 'change'],
// Capture additional element properties
capture_copied_text: false, // Don't capture copied text (privacy)
},
// Development debugging
loaded: (posthogInstance) => {
if (process.env.NODE_ENV === 'development') {
console.log('✅ PostHog initialized successfully');
posthogInstance.debug(); // Enable debug mode in development
}
},
});
isInitialized = true;
console.log('📊 PostHog Analytics initialized');
} catch (error) {
// 忽略 AbortError通常由热重载或快速导航引起
if (error.name === 'AbortError') {
console.log('⚠️ PostHog 初始化请求被中断(可能是热重载),这是正常的');
return;
}
console.error('❌ PostHog initialization failed:', error);
} 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,
});
console.log('👤 User identified:', userId);
} 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);
console.log('📝 User properties updated');
} 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(),
});
if (process.env.NODE_ENV === 'development') {
console.log('📍 Event tracked:', eventName, properties);
}
} catch (error) {
console.error('❌ Event tracking failed:', error);
}
};
/**
* 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,
});
if (process.env.NODE_ENV === 'development') {
console.log('📄 Page view tracked:', pagePath);
}
} catch (error) {
console.error('❌ Page view tracking failed:', error);
}
};
/**
* Reset user session
* Call this on logout
*/
export const resetUser = () => {
try {
posthog.reset();
console.log('🔄 User session reset');
} catch (error) {
console.error('❌ Session reset failed:', error);
}
};
/**
* User opt-out from tracking
*/
export const optOut = () => {
try {
posthog.opt_out_capturing();
console.log('🚫 User opted out of tracking');
} catch (error) {
console.error('❌ Opt-out failed:', error);
}
};
/**
* User opt-in to tracking
*/
export const optIn = () => {
try {
posthog.opt_in_capturing();
console.log('✅ User opted in to tracking');
} 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;
}
};
export default posthog;