292 lines
7.7 KiB
JavaScript
292 lines
7.7 KiB
JavaScript
// 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(); // 已关闭:减少控制台日志噪音
|
||
}
|
||
},
|
||
});
|
||
|
||
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;
|