feat: 完善 PostHog 用户生命周期追踪 + 性能优化
新增功能:
1. 首次访问追踪 (first_visit)
- 记录用户来源(referrer、UTM参数)
- 记录落地页
- 使用 localStorage 永久标记
2. 首次登录追踪 (first_login)
- 区分首次登录和后续登录
- 按用户 ID 独立标记
- 用于计算新用户激活率
3. 登录/登出事件追踪
- 登录成功追踪 (user_logged_in)
- 登出事件追踪 (user_logged_out,必须在 resetUser 之前)
- 注册事件追踪 (user_registered)
4. 页面浏览时长追踪 (page_view_duration)
- 路由切换时自动计算停留时长
- 页面关闭时发送最终时长
- 过滤停留时间 < 1秒的快速跳转
性能优化:
1. 新增 trackEventAsync 函数
- 使用 requestIdleCallback 在浏览器空闲时发送非关键事件
- Safari 等旧浏览器降级到 setTimeout
- 超时保护(最多延迟 2秒)
2. 异步追踪非关键事件
- first_visit - 不阻塞首屏渲染
- page_view_duration - 不阻塞页面切换
3. 关键事件保持同步
- user_registered、user_logged_in、first_login、user_logged_out
- 确保数据准确性和完整性
分析能力提升:
- ✅ 营销渠道 ROI 分析(UTM 参数追踪)
- ✅ 新用户激活率分析(首次登录标记)
- ✅ 用户留存率分析(注册→首次登录→后续登录)
- ✅ 页面热度分析(停留时长统计)
- ✅ 流失用户识别(7天未登录,需后端支持)
This commit is contained in:
76
src/App.js
76
src/App.js
@@ -9,8 +9,9 @@
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Visionware.
|
||||
*/
|
||||
|
||||
import React, { useEffect } from "react";
|
||||
import React, { useEffect, useRef } from "react";
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
// Routes
|
||||
import AppRoutes from './routes';
|
||||
@@ -30,12 +31,24 @@ import { initializePostHog } from './store/slices/posthogSlice';
|
||||
// Utils
|
||||
import { logger } from './utils/logger';
|
||||
|
||||
// PostHog 追踪
|
||||
import { trackEvent, trackEventAsync } from '@lib/posthog';
|
||||
|
||||
// Contexts
|
||||
import { useAuth } from '@contexts/AuthContext';
|
||||
|
||||
/**
|
||||
* AppContent - 应用核心内容
|
||||
* 负责 PostHog 初始化和渲染路由
|
||||
*/
|
||||
function AppContent() {
|
||||
const dispatch = useDispatch();
|
||||
const location = useLocation();
|
||||
const { isAuthenticated } = useAuth();
|
||||
|
||||
// ✅ 使用 Ref 存储页面进入时间和路径(避免闭包问题)
|
||||
const pageEnterTimeRef = useRef(Date.now());
|
||||
const currentPathRef = useRef(location.pathname);
|
||||
|
||||
// 🎯 PostHog Redux 初始化
|
||||
useEffect(() => {
|
||||
@@ -43,6 +56,67 @@ function AppContent() {
|
||||
logger.info('App', 'PostHog Redux 初始化已触发');
|
||||
}, [dispatch]);
|
||||
|
||||
// ✅ 首次访问追踪
|
||||
useEffect(() => {
|
||||
const hasVisited = localStorage.getItem('has_visited');
|
||||
|
||||
if (!hasVisited) {
|
||||
const urlParams = new URLSearchParams(location.search);
|
||||
|
||||
// ⚡ 使用异步追踪,不阻塞页面渲染
|
||||
trackEventAsync('first_visit', {
|
||||
referrer: document.referrer || 'direct',
|
||||
utm_source: urlParams.get('utm_source'),
|
||||
utm_medium: urlParams.get('utm_medium'),
|
||||
utm_campaign: urlParams.get('utm_campaign'),
|
||||
landing_page: location.pathname,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
localStorage.setItem('has_visited', 'true');
|
||||
}
|
||||
}, [location.search, location.pathname]);
|
||||
|
||||
// ✅ 页面浏览时长追踪
|
||||
useEffect(() => {
|
||||
// 计算上一个页面的停留时长
|
||||
const calculateAndTrackDuration = () => {
|
||||
const exitTime = Date.now();
|
||||
const duration = Math.round((exitTime - pageEnterTimeRef.current) / 1000); // 秒
|
||||
|
||||
// 只追踪停留时间 > 1 秒的页面(过滤快速跳转)
|
||||
if (duration > 1) {
|
||||
// ⚡ 使用异步追踪,不阻塞页面切换
|
||||
trackEventAsync('page_view_duration', {
|
||||
path: currentPathRef.current,
|
||||
duration_seconds: duration,
|
||||
is_authenticated: isAuthenticated,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 路由切换时追踪上一个页面的时长
|
||||
if (currentPathRef.current !== location.pathname) {
|
||||
calculateAndTrackDuration();
|
||||
|
||||
// 更新为新页面
|
||||
currentPathRef.current = location.pathname;
|
||||
pageEnterTimeRef.current = Date.now();
|
||||
}
|
||||
|
||||
// 页面关闭/刷新时追踪时长
|
||||
const handleBeforeUnload = () => {
|
||||
calculateAndTrackDuration();
|
||||
};
|
||||
|
||||
window.addEventListener('beforeunload', handleBeforeUnload);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('beforeunload', handleBeforeUnload);
|
||||
};
|
||||
}, [location.pathname, isAuthenticated]);
|
||||
|
||||
return <AppRoutes />;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useNavigate } from 'react-router-dom';
|
||||
import { useToast } from '@chakra-ui/react';
|
||||
import { logger } from '../utils/logger';
|
||||
import { useNotification } from '../contexts/NotificationContext';
|
||||
import { identifyUser, resetUser, trackEvent } from '@lib/posthog';
|
||||
|
||||
// 创建认证上下文
|
||||
const AuthContext = createContext();
|
||||
@@ -90,6 +91,16 @@ export const AuthProvider = ({ children }) => {
|
||||
if (prevUser && prevUser.id === data.user.id) {
|
||||
return prevUser;
|
||||
}
|
||||
|
||||
// ✅ 识别用户身份到 PostHog
|
||||
identifyUser(data.user.id, {
|
||||
email: data.user.email,
|
||||
username: data.user.username,
|
||||
subscription_tier: data.user.subscription_tier,
|
||||
role: data.user.role,
|
||||
registration_date: data.user.created_at
|
||||
});
|
||||
|
||||
return data.user;
|
||||
});
|
||||
setIsAuthenticated((prev) => prev === true ? prev : true);
|
||||
@@ -209,6 +220,26 @@ export const AuthProvider = ({ children }) => {
|
||||
setUser(data.user);
|
||||
setIsAuthenticated(true);
|
||||
|
||||
// ✅ 追踪登录事件
|
||||
trackEvent('user_logged_in', {
|
||||
loginType,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
// ✅ 首次登录追踪
|
||||
const firstLoginKey = `first_login_${data.user.id}`;
|
||||
const hasLoggedInBefore = localStorage.getItem(firstLoginKey);
|
||||
|
||||
if (!hasLoggedInBefore) {
|
||||
trackEvent('first_login', {
|
||||
user_id: data.user.id,
|
||||
login_type: loginType,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
localStorage.setItem(firstLoginKey, 'true');
|
||||
}
|
||||
|
||||
// ⚡ 移除toast,让调用者处理UI反馈,避免并发更新冲突
|
||||
// toast({
|
||||
// title: "登录成功",
|
||||
@@ -263,6 +294,21 @@ export const AuthProvider = ({ children }) => {
|
||||
setUser(data.user);
|
||||
setIsAuthenticated(true);
|
||||
|
||||
// ✅ 识别用户身份到 PostHog
|
||||
identifyUser(data.user.id, {
|
||||
email: data.user.email,
|
||||
username: data.user.username,
|
||||
subscription_tier: data.user.subscription_tier,
|
||||
role: data.user.role,
|
||||
registration_date: data.user.created_at
|
||||
});
|
||||
|
||||
// ✅ 追踪注册事件
|
||||
trackEvent('user_registered', {
|
||||
method: 'phone',
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
toast({
|
||||
title: "注册成功",
|
||||
description: "欢迎加入价值前沿!",
|
||||
@@ -315,6 +361,21 @@ export const AuthProvider = ({ children }) => {
|
||||
setUser(data.user);
|
||||
setIsAuthenticated(true);
|
||||
|
||||
// ✅ 识别用户身份到 PostHog
|
||||
identifyUser(data.user.id, {
|
||||
email: data.user.email,
|
||||
username: data.user.username,
|
||||
subscription_tier: data.user.subscription_tier,
|
||||
role: data.user.role,
|
||||
registration_date: data.user.created_at
|
||||
});
|
||||
|
||||
// ✅ 追踪注册事件
|
||||
trackEvent('user_registered', {
|
||||
method: 'email',
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
toast({
|
||||
title: "注册成功",
|
||||
description: "欢迎加入价值前沿!",
|
||||
@@ -405,6 +466,14 @@ export const AuthProvider = ({ children }) => {
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
// ✅ 追踪登出事件(必须在 resetUser() 之前,否则会丢失用户身份)
|
||||
trackEvent('user_logged_out', {
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
// ✅ 重置 PostHog 用户会话
|
||||
resetUser();
|
||||
|
||||
// 清除本地状态
|
||||
setUser(null);
|
||||
setIsAuthenticated(false);
|
||||
|
||||
@@ -185,6 +185,30 @@ export const trackEvent = (eventName, properties = {}) => {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 异步追踪事件(不阻塞主线程)
|
||||
* 使用 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
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user