- ✅ 新增 measure(name, startMark, endMark) 方法(支持命名测量) - ✅ 新增 getMarks() - 获取所有性能标记 - ✅ 新增 getMeasures() - 获取所有测量结果 - ✅ 新增 getReport() - 返回完整 JSON 报告 - ✅ 新增 exportJSON() - 导出 JSON 文件 - ✅ 新增 reportToPostHog() - 上报到 PostHog - ✅ 新增全局 API window.__PERFORMANCE__(仅开发环境) - ✅ 彩色控制台使用说明 2️⃣ 添加 PostHog 性能上报 - ✅ 在 posthog.js 中新增 reportPerformanceMetrics() 函数 - ✅ 上报所有关键性能指标(网络、渲染、React) - ✅ 自动计算性能评分(0-100) - ✅ 包含浏览器和设备信息
398 lines
11 KiB
JavaScript
398 lines
11 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 = () => {
|
||
// 开发环境禁用 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;
|