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.
*/
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 />;
}

View File

@@ -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);

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
*