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:
zdl
2025-11-18 21:29:33 +08:00
parent c9419d3c14
commit 17c04211bb
3 changed files with 168 additions and 1 deletions

View File

@@ -9,8 +9,9 @@
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Visionware. 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 { useDispatch } from 'react-redux';
import { useLocation } from 'react-router-dom';
// Routes // Routes
import AppRoutes from './routes'; import AppRoutes from './routes';
@@ -30,12 +31,24 @@ import { initializePostHog } from './store/slices/posthogSlice';
// Utils // Utils
import { logger } from './utils/logger'; import { logger } from './utils/logger';
// PostHog 追踪
import { trackEvent, trackEventAsync } from '@lib/posthog';
// Contexts
import { useAuth } from '@contexts/AuthContext';
/** /**
* AppContent - 应用核心内容 * AppContent - 应用核心内容
* 负责 PostHog 初始化和渲染路由 * 负责 PostHog 初始化和渲染路由
*/ */
function AppContent() { function AppContent() {
const dispatch = useDispatch(); const dispatch = useDispatch();
const location = useLocation();
const { isAuthenticated } = useAuth();
// ✅ 使用 Ref 存储页面进入时间和路径(避免闭包问题)
const pageEnterTimeRef = useRef(Date.now());
const currentPathRef = useRef(location.pathname);
// 🎯 PostHog Redux 初始化 // 🎯 PostHog Redux 初始化
useEffect(() => { useEffect(() => {
@@ -43,6 +56,67 @@ function AppContent() {
logger.info('App', 'PostHog Redux 初始化已触发'); logger.info('App', 'PostHog Redux 初始化已触发');
}, [dispatch]); }, [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 />; return <AppRoutes />;
} }

View File

@@ -4,6 +4,7 @@ import { useNavigate } from 'react-router-dom';
import { useToast } from '@chakra-ui/react'; import { useToast } from '@chakra-ui/react';
import { logger } from '../utils/logger'; import { logger } from '../utils/logger';
import { useNotification } from '../contexts/NotificationContext'; import { useNotification } from '../contexts/NotificationContext';
import { identifyUser, resetUser, trackEvent } from '@lib/posthog';
// 创建认证上下文 // 创建认证上下文
const AuthContext = createContext(); const AuthContext = createContext();
@@ -90,6 +91,16 @@ export const AuthProvider = ({ children }) => {
if (prevUser && prevUser.id === data.user.id) { if (prevUser && prevUser.id === data.user.id) {
return prevUser; 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; return data.user;
}); });
setIsAuthenticated((prev) => prev === true ? prev : true); setIsAuthenticated((prev) => prev === true ? prev : true);
@@ -209,6 +220,26 @@ export const AuthProvider = ({ children }) => {
setUser(data.user); setUser(data.user);
setIsAuthenticated(true); 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让调用者处理UI反馈避免并发更新冲突
// toast({ // toast({
// title: "登录成功", // title: "登录成功",
@@ -263,6 +294,21 @@ export const AuthProvider = ({ children }) => {
setUser(data.user); setUser(data.user);
setIsAuthenticated(true); 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({ toast({
title: "注册成功", title: "注册成功",
description: "欢迎加入价值前沿!", description: "欢迎加入价值前沿!",
@@ -315,6 +361,21 @@ export const AuthProvider = ({ children }) => {
setUser(data.user); setUser(data.user);
setIsAuthenticated(true); 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({ toast({
title: "注册成功", title: "注册成功",
description: "欢迎加入价值前沿!", description: "欢迎加入价值前沿!",
@@ -405,6 +466,14 @@ export const AuthProvider = ({ children }) => {
credentials: 'include' credentials: 'include'
}); });
// ✅ 追踪登出事件(必须在 resetUser() 之前,否则会丢失用户身份)
trackEvent('user_logged_out', {
timestamp: new Date().toISOString()
});
// ✅ 重置 PostHog 用户会话
resetUser();
// 清除本地状态 // 清除本地状态
setUser(null); setUser(null);
setIsAuthenticated(false); setIsAuthenticated(false);

View File

@@ -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 * Track page view
* *