feat: 首页登陆事件追踪

This commit is contained in:
zdl
2025-10-28 21:45:06 +08:00
parent cddd0e860e
commit c42a14aa8f
4 changed files with 653 additions and 11 deletions

View File

@@ -37,6 +37,7 @@ import VerificationCodeInput from './VerificationCodeInput';
import WechatRegister from './WechatRegister'; import WechatRegister from './WechatRegister';
import { setCurrentUser } from '../../mocks/data/users'; import { setCurrentUser } from '../../mocks/data/users';
import { logger } from '../../utils/logger'; import { logger } from '../../utils/logger';
import { useAuthEvents } from '../../hooks/useAuthEvents';
// 统一配置对象 // 统一配置对象
const AUTH_CONFIG = { const AUTH_CONFIG = {
@@ -86,6 +87,12 @@ export default function AuthFormContent() {
// 响应式布局配置 // 响应式布局配置
const isMobile = useBreakpointValue({ base: true, md: false }); const isMobile = useBreakpointValue({ base: true, md: false });
// 事件追踪
const authEvents = useAuthEvents({
component: 'AuthFormContent',
isMobile: isMobile
});
const stackDirection = useBreakpointValue({ base: "column", md: "row" }); const stackDirection = useBreakpointValue({ base: "column", md: "row" });
const stackSpacing = useBreakpointValue({ base: 4, md: 2 }); // ✅ 桌面端从32px减至8px更紧凑 const stackSpacing = useBreakpointValue({ base: 4, md: 2 }); // ✅ 桌面端从32px减至8px更紧凑
@@ -107,6 +114,16 @@ export default function AuthFormContent() {
...prev, ...prev,
[name]: value [name]: value
})); }));
// 追踪用户开始填写手机号 (判断用户选择了手机登录方式)
if (name === 'phone' && value.length === 1 && !formData.phone) {
authEvents.trackPhoneLoginInitiated(value);
}
// 追踪验证码输入变化
if (name === 'verificationCode') {
authEvents.trackVerificationCodeInputChanged(value.length);
}
}; };
// 倒计时逻辑 // 倒计时逻辑
@@ -144,6 +161,10 @@ export default function AuthFormContent() {
} }
if (!/^1[3-9]\d{9}$/.test(credential)) { if (!/^1[3-9]\d{9}$/.test(credential)) {
// 追踪手机号验证失败
authEvents.trackPhoneNumberValidated(credential, false, 'invalid_format');
authEvents.trackFormValidationError('phone', 'invalid_format', '请输入有效的手机号');
toast({ toast({
title: "请输入有效的手机号", title: "请输入有效的手机号",
status: "warning", status: "warning",
@@ -152,6 +173,9 @@ export default function AuthFormContent() {
return; return;
} }
// 追踪手机号验证通过
authEvents.trackPhoneNumberValidated(credential, true);
try { try {
setSendingCode(true); setSendingCode(true);
@@ -187,6 +211,14 @@ export default function AuthFormContent() {
} }
if (response.ok && data.success) { if (response.ok && data.success) {
// 追踪验证码发送成功 (或重发)
const isResend = verificationCodeSent;
if (isResend) {
authEvents.trackVerificationCodeResent(credential, countdown > 0 ? 2 : 1);
} else {
authEvents.trackVerificationCodeSent(credential, config.api.purpose);
}
// ❌ 移除成功 toast静默处理 // ❌ 移除成功 toast静默处理
logger.info('AuthFormContent', '验证码发送成功', { logger.info('AuthFormContent', '验证码发送成功', {
credential: credential.substring(0, 3) + '****' + credential.substring(7), credential: credential.substring(0, 3) + '****' + credential.substring(7),
@@ -204,6 +236,13 @@ export default function AuthFormContent() {
throw new Error(data.error || '发送验证码失败'); throw new Error(data.error || '发送验证码失败');
} }
} catch (error) { } catch (error) {
// 追踪验证码发送失败
authEvents.trackVerificationCodeSendFailed(credential, error);
authEvents.trackError('api', error.message || '发送验证码失败', {
endpoint: '/api/auth/send-verification-code',
phone_masked: credential.substring(0, 3) + '****' + credential.substring(7)
});
logger.api.error('POST', '/api/auth/send-verification-code', error, { logger.api.error('POST', '/api/auth/send-verification-code', error, {
credential: credential.substring(0, 3) + '****' + credential.substring(7) credential: credential.substring(0, 3) + '****' + credential.substring(7)
}); });
@@ -256,6 +295,9 @@ export default function AuthFormContent() {
return; return;
} }
// 追踪验证码提交
authEvents.trackVerificationCodeSubmitted(phone);
// 构建请求体 // 构建请求体
const requestBody = { const requestBody = {
credential: phone.trim(), // 添加 trim() 防止空格 credential: phone.trim(), // 添加 trim() 防止空格
@@ -310,6 +352,9 @@ export default function AuthFormContent() {
// 更新session // 更新session
await checkSession(); await checkSession();
// 追踪登录成功并识别用户
authEvents.trackLoginSuccess(data.user, 'phone', data.isNewUser);
// ✅ 保留登录成功 toast关键操作提示 // ✅ 保留登录成功 toast关键操作提示
toast({ toast({
title: data.isNewUser ? '注册成功' : '登录成功', title: data.isNewUser ? '注册成功' : '登录成功',
@@ -329,6 +374,8 @@ export default function AuthFormContent() {
setTimeout(() => { setTimeout(() => {
setCurrentPhone(phone); setCurrentPhone(phone);
setShowNicknamePrompt(true); setShowNicknamePrompt(true);
// 追踪昵称设置引导显示
authEvents.trackNicknamePromptShown(phone);
}, config.features.successDelay); }, config.features.successDelay);
} else { } else {
// 已有用户,直接登录成功 // 已有用户,直接登录成功
@@ -349,6 +396,15 @@ export default function AuthFormContent() {
} }
} catch (error) { } catch (error) {
const { phone, verificationCode } = formData; const { phone, verificationCode } = formData;
// 追踪登录失败
const errorType = error.message.includes('网络') ? 'network' :
error.message.includes('服务器') ? 'api' : 'validation';
authEvents.trackLoginFailed('phone', errorType, error.message, {
phone_masked: phone ? phone.substring(0, 3) + '****' + phone.substring(7) : 'N/A',
has_verification_code: !!verificationCode
});
logger.error('AuthFormContent', 'handleSubmit', error, { logger.error('AuthFormContent', 'handleSubmit', error, {
phone: phone ? phone.substring(0, 3) + '****' + phone.substring(7) : 'N/A', phone: phone ? phone.substring(0, 3) + '****' + phone.substring(7) : 'N/A',
hasVerificationCode: !!verificationCode hasVerificationCode: !!verificationCode
@@ -376,6 +432,9 @@ export default function AuthFormContent() {
// 微信H5登录处理 // 微信H5登录处理
const handleWechatH5Login = async () => { const handleWechatH5Login = async () => {
// 追踪用户选择微信登录
authEvents.trackWechatLoginInitiated('icon_button');
try { try {
// 1. 构建回调URL // 1. 构建回调URL
const redirectUrl = `${window.location.origin}/home/wechat-callback`; const redirectUrl = `${window.location.origin}/home/wechat-callback`;
@@ -396,11 +455,19 @@ export default function AuthFormContent() {
throw new Error('获取授权链接失败'); throw new Error('获取授权链接失败');
} }
// 追踪微信H5跳转
authEvents.trackWechatH5Redirect();
// 4. 延迟跳转,让用户看到提示 // 4. 延迟跳转,让用户看到提示
setTimeout(() => { setTimeout(() => {
window.location.href = response.auth_url; window.location.href = response.auth_url;
}, 500); }, 500);
} catch (error) { } catch (error) {
// 追踪跳转失败
authEvents.trackError('api', error.message || '获取微信授权链接失败', {
context: 'wechat_h5_redirect'
});
logger.error('AuthFormContent', 'handleWechatH5Login', error); logger.error('AuthFormContent', 'handleWechatH5Login', error);
toast({ toast({
title: "跳转失败", title: "跳转失败",
@@ -412,14 +479,17 @@ export default function AuthFormContent() {
} }
}; };
// 组件载时清理 // 组件载时追踪页面浏览
useEffect(() => { useEffect(() => {
isMountedRef.current = true; isMountedRef.current = true;
// 追踪登录页面浏览
authEvents.trackLoginPageViewed();
return () => { return () => {
isMountedRef.current = false; isMountedRef.current = false;
}; };
}, []); }, [authEvents]);
return ( return (
<> <>
@@ -479,6 +549,7 @@ export default function AuthFormContent() {
color="blue.500" color="blue.500"
textDecoration="underline" textDecoration="underline"
_hover={{ color: "blue.600" }} _hover={{ color: "blue.600" }}
onClick={authEvents.trackUserAgreementClicked}
> >
用户协议 用户协议
</ChakraLink> </ChakraLink>
@@ -491,6 +562,7 @@ export default function AuthFormContent() {
color="blue.500" color="blue.500"
textDecoration="underline" textDecoration="underline"
_hover={{ color: "blue.600" }} _hover={{ color: "blue.600" }}
onClick={authEvents.trackPrivacyPolicyClicked}
> >
隐私政策 隐私政策
</ChakraLink> </ChakraLink>
@@ -518,8 +590,30 @@ export default function AuthFormContent() {
<AlertDialogHeader fontSize="lg" fontWeight="bold">完善个人信息</AlertDialogHeader> <AlertDialogHeader fontSize="lg" fontWeight="bold">完善个人信息</AlertDialogHeader>
<AlertDialogBody>您已成功注册是否前往个人资料设置昵称和其他信息</AlertDialogBody> <AlertDialogBody>您已成功注册是否前往个人资料设置昵称和其他信息</AlertDialogBody>
<AlertDialogFooter> <AlertDialogFooter>
<Button ref={cancelRef} onClick={() => { setShowNicknamePrompt(false); handleLoginSuccess({ phone: currentPhone }); }}>稍后再说</Button> <Button
<Button colorScheme="green" onClick={() => { setShowNicknamePrompt(false); handleLoginSuccess({ phone: currentPhone }); setTimeout(() => { navigate('/home/profile'); }, 300); }} ml={3}>去设置</Button> ref={cancelRef}
onClick={() => {
authEvents.trackNicknamePromptSkipped();
setShowNicknamePrompt(false);
handleLoginSuccess({ phone: currentPhone });
}}
>
稍后再说
</Button>
<Button
colorScheme="green"
onClick={() => {
authEvents.trackNicknamePromptAccepted();
setShowNicknamePrompt(false);
handleLoginSuccess({ phone: currentPhone });
setTimeout(() => {
navigate('/home/profile');
}, 300);
}}
ml={3}
>
去设置
</Button>
</AlertDialogFooter> </AlertDialogFooter>
</AlertDialogContent> </AlertDialogContent>
</AlertDialogOverlay> </AlertDialogOverlay>

View File

@@ -18,6 +18,7 @@ import { authService, WECHAT_STATUS, STATUS_MESSAGES } from "../../services/auth
import { useAuthModal } from "../../contexts/AuthModalContext"; import { useAuthModal } from "../../contexts/AuthModalContext";
import { useAuth } from "../../contexts/AuthContext"; import { useAuth } from "../../contexts/AuthContext";
import { logger } from "../../utils/logger"; import { logger } from "../../utils/logger";
import { useAuthEvents } from "../../hooks/useAuthEvents";
// 配置常量 // 配置常量
const POLL_INTERVAL = 2000; // 轮询间隔2秒 const POLL_INTERVAL = 2000; // 轮询间隔2秒
@@ -51,6 +52,12 @@ export default function WechatRegister() {
const { closeModal } = useAuthModal(); const { closeModal } = useAuthModal();
const { refreshSession } = useAuth(); const { refreshSession } = useAuth();
// 事件追踪
const authEvents = useAuthEvents({
component: 'WechatRegister',
isMobile: false // WechatRegister 只在桌面端显示
});
// 状态管理 // 状态管理
const [wechatAuthUrl, setWechatAuthUrl] = useState(""); const [wechatAuthUrl, setWechatAuthUrl] = useState("");
const [wechatSessionId, setWechatSessionId] = useState(""); const [wechatSessionId, setWechatSessionId] = useState("");
@@ -126,6 +133,13 @@ export default function WechatRegister() {
logger.info('WechatRegister', '登录接口返回', { success: response?.success, hasUser: !!response?.user }); logger.info('WechatRegister', '登录接口返回', { success: response?.success, hasUser: !!response?.user });
if (response?.success) { if (response?.success) {
// 追踪微信登录成功
authEvents.trackLoginSuccess(
response.user,
'wechat',
response.isNewUser || false
);
// Session cookie 会自动管理,不需要手动存储 // Session cookie 会自动管理,不需要手动存储
// 如果后端返回了 token可以选择性存储兼容旧方式 // 如果后端返回了 token可以选择性存储兼容旧方式
if (response.token) { if (response.token) {
@@ -148,10 +162,16 @@ export default function WechatRegister() {
throw new Error(response?.error || '登录失败'); throw new Error(response?.error || '登录失败');
} }
} catch (error) { } catch (error) {
// 追踪微信登录失败
authEvents.trackLoginFailed('wechat', 'api', error.message || '登录失败', {
session_id: sessionId?.substring(0, 8) + '...',
status: status
});
logger.error('WechatRegister', 'handleLoginSuccess', error, { sessionId }); logger.error('WechatRegister', 'handleLoginSuccess', error, { sessionId });
showError("登录失败", error.message || "请重试"); showError("登录失败", error.message || "请重试");
} }
}, [showSuccess, showError, closeModal, refreshSession]); }, [showSuccess, showError, closeModal, refreshSession, authEvents]);
/** /**
* 检查微信扫码状态 * 检查微信扫码状态
@@ -191,6 +211,16 @@ export default function WechatRegister() {
// 组件卸载后不再更新状态 // 组件卸载后不再更新状态
if (!isMountedRef.current) return; if (!isMountedRef.current) return;
// 追踪状态变化
if (wechatStatus !== status) {
authEvents.trackWechatStatusChanged(currentSessionId, wechatStatus, status);
// 特别追踪扫码事件
if (status === WECHAT_STATUS.SCANNED) {
authEvents.trackWechatQRScanned(currentSessionId);
}
}
setWechatStatus(status); setWechatStatus(status);
// 处理成功状态 // 处理成功状态
@@ -203,6 +233,9 @@ export default function WechatRegister() {
} }
// 处理过期状态 // 处理过期状态
else if (status === WECHAT_STATUS.EXPIRED) { else if (status === WECHAT_STATUS.EXPIRED) {
// 追踪二维码过期
authEvents.trackWechatQRExpired(currentSessionId, QR_CODE_TIMEOUT / 1000);
clearTimers(); clearTimers();
sessionIdRef.current = null; // 清理 sessionId sessionIdRef.current = null; // 清理 sessionId
if (isMountedRef.current) { if (isMountedRef.current) {
@@ -268,6 +301,16 @@ export default function WechatRegister() {
try { try {
setIsLoading(true); setIsLoading(true);
// 追踪用户选择微信登录(首次或刷新)
const isRefresh = Boolean(wechatSessionId);
if (isRefresh) {
const oldSessionId = wechatSessionId;
authEvents.trackWechatLoginInitiated('qr_refresh');
// 稍后会在成功时追踪刷新事件
} else {
authEvents.trackWechatLoginInitiated('qr_area');
}
// 生产环境:调用真实 API // 生产环境:调用真实 API
const response = await authService.getWechatQRCode(); const response = await authService.getWechatQRCode();
@@ -283,6 +326,13 @@ export default function WechatRegister() {
throw new Error(response.message || '获取二维码失败'); throw new Error(response.message || '获取二维码失败');
} }
// 追踪二维码显示 (首次或刷新)
if (isRefresh) {
authEvents.trackWechatQRRefreshed(wechatSessionId, response.data.session_id);
} else {
authEvents.trackWechatQRDisplayed(response.data.session_id, response.data.auth_url);
}
// 同时更新 ref 和 state确保轮询能立即读取到最新值 // 同时更新 ref 和 state确保轮询能立即读取到最新值
sessionIdRef.current = response.data.session_id; sessionIdRef.current = response.data.session_id;
setWechatAuthUrl(response.data.auth_url); setWechatAuthUrl(response.data.auth_url);
@@ -297,6 +347,11 @@ export default function WechatRegister() {
// 启动轮询检查扫码状态 // 启动轮询检查扫码状态
startPolling(); startPolling();
} catch (error) { } catch (error) {
// 追踪获取二维码失败
authEvents.trackError('api', error.message || '获取二维码失败', {
context: 'get_wechat_qrcode'
});
logger.error('WechatRegister', 'getWechatQRCode', error); logger.error('WechatRegister', 'getWechatQRCode', error);
if (isMountedRef.current) { if (isMountedRef.current) {
showError("获取微信授权失败", error.message || "请稍后重试"); showError("获取微信授权失败", error.message || "请稍后重试");
@@ -306,7 +361,7 @@ export default function WechatRegister() {
setIsLoading(false); setIsLoading(false);
} }
} }
}, [startPolling, showError]); }, [startPolling, showError, wechatSessionId, authEvents]);
/** /**
* 安全的按钮点击处理,确保所有错误都被捕获,防止被 ErrorBoundary 捕获 * 安全的按钮点击处理,确保所有错误都被捕获,防止被 ErrorBoundary 捕获

463
src/hooks/useAuthEvents.js Normal file
View File

@@ -0,0 +1,463 @@
// src/hooks/useAuthEvents.js
// 认证事件追踪 Hook
import { useCallback } from 'react';
import { usePostHogTrack, usePostHogUser } from './usePostHogRedux';
import { ACTIVATION_EVENTS } from '../lib/constants';
import { logger } from '../utils/logger';
/**
* 认证事件追踪 Hook
* 提供登录/注册流程中所有关键节点的事件追踪功能
*
* 用法示例:
*
* ```jsx
* import { useAuthEvents } from 'hooks/useAuthEvents';
*
* function AuthComponent() {
* const {
* trackLoginPageViewed,
* trackPhoneLoginInitiated,
* trackVerificationCodeSent,
* trackLoginSuccess
* } = useAuthEvents();
*
* useEffect(() => {
* trackLoginPageViewed();
* }, [trackLoginPageViewed]);
*
* const handlePhoneFocus = () => {
* trackPhoneLoginInitiated(formData.phone);
* };
* }
* ```
*
* @param {Object} options - 配置选项
* @param {string} options.component - 组件名称 ('AuthFormContent' | 'WechatRegister')
* @param {boolean} options.isMobile - 是否为移动设备
* @returns {Object} 事件追踪处理函数集合
*/
export const useAuthEvents = ({ component = 'AuthFormContent', isMobile = false } = {}) => {
const { track } = usePostHogTrack();
const { identify } = usePostHogUser();
// 通用事件属性
const getBaseProperties = useCallback(() => ({
component,
device: isMobile ? 'mobile' : 'desktop',
timestamp: new Date().toISOString(),
}), [component, isMobile]);
// ==================== 页面浏览事件 ====================
/**
* 追踪登录页面浏览
*/
const trackLoginPageViewed = useCallback(() => {
track(ACTIVATION_EVENTS.LOGIN_PAGE_VIEWED, getBaseProperties());
logger.debug('useAuthEvents', '📄 Login Page Viewed', { component });
}, [track, getBaseProperties, component]);
// ==================== 登录方式选择 ====================
/**
* 追踪用户开始手机号登录
* @param {string} phone - 手机号(可选,用于判断是否已填写)
*/
const trackPhoneLoginInitiated = useCallback((phone = '') => {
track(ACTIVATION_EVENTS.PHONE_LOGIN_INITIATED, {
...getBaseProperties(),
has_phone: Boolean(phone),
});
logger.debug('useAuthEvents', '📱 Phone Login Initiated', { hasPhone: Boolean(phone) });
}, [track, getBaseProperties]);
/**
* 追踪用户选择微信登录
* @param {string} source - 触发来源 ('qr_area' | 'icon_button' | 'h5_redirect')
*/
const trackWechatLoginInitiated = useCallback((source = 'qr_area') => {
track(ACTIVATION_EVENTS.WECHAT_LOGIN_INITIATED, {
...getBaseProperties(),
source,
});
logger.debug('useAuthEvents', '💬 WeChat Login Initiated', { source });
}, [track, getBaseProperties]);
// ==================== 手机验证码流程 ====================
/**
* 追踪验证码发送成功
* @param {string} phone - 手机号
* @param {string} purpose - 发送目的 ('login' | 'register')
*/
const trackVerificationCodeSent = useCallback((phone, purpose = 'login') => {
track(ACTIVATION_EVENTS.VERIFICATION_CODE_SENT, {
...getBaseProperties(),
phone_masked: phone ? phone.substring(0, 3) + '****' + phone.substring(7) : '',
purpose,
});
logger.debug('useAuthEvents', '✉️ Verification Code Sent', { phone: phone?.substring(0, 3) + '****', purpose });
}, [track, getBaseProperties]);
/**
* 追踪验证码发送失败
* @param {string} phone - 手机号
* @param {Error|string} error - 错误对象或错误消息
*/
const trackVerificationCodeSendFailed = useCallback((phone, error) => {
const errorMessage = typeof error === 'string' ? error : error?.message || 'Unknown error';
track(ACTIVATION_EVENTS.VERIFICATION_CODE_SEND_FAILED, {
...getBaseProperties(),
phone_masked: phone ? phone.substring(0, 3) + '****' + phone.substring(7) : '',
error_message: errorMessage,
error_type: 'send_code_failed',
});
logger.debug('useAuthEvents', '❌ Verification Code Send Failed', { error: errorMessage });
}, [track, getBaseProperties]);
/**
* 追踪用户输入验证码
* @param {number} codeLength - 当前输入的验证码长度
*/
const trackVerificationCodeInputChanged = useCallback((codeLength) => {
track(ACTIVATION_EVENTS.VERIFICATION_CODE_INPUT_CHANGED, {
...getBaseProperties(),
code_length: codeLength,
is_complete: codeLength >= 6,
});
logger.debug('useAuthEvents', '⌨️ Verification Code Input Changed', { codeLength });
}, [track, getBaseProperties]);
/**
* 追踪重新发送验证码
* @param {string} phone - 手机号
* @param {number} attemptCount - 第几次重发(可选)
*/
const trackVerificationCodeResent = useCallback((phone, attemptCount = 1) => {
track(ACTIVATION_EVENTS.VERIFICATION_CODE_RESENT, {
...getBaseProperties(),
phone_masked: phone ? phone.substring(0, 3) + '****' + phone.substring(7) : '',
attempt_count: attemptCount,
});
logger.debug('useAuthEvents', '🔄 Verification Code Resent', { attempt: attemptCount });
}, [track, getBaseProperties]);
/**
* 追踪手机号验证结果
* @param {string} phone - 手机号
* @param {boolean} isValid - 是否有效
* @param {string} errorType - 错误类型(可选)
*/
const trackPhoneNumberValidated = useCallback((phone, isValid, errorType = '') => {
track(ACTIVATION_EVENTS.PHONE_NUMBER_VALIDATED, {
...getBaseProperties(),
phone_masked: phone ? phone.substring(0, 3) + '****' + phone.substring(7) : '',
is_valid: isValid,
error_type: errorType,
});
logger.debug('useAuthEvents', '✓ Phone Number Validated', { isValid, errorType });
}, [track, getBaseProperties]);
/**
* 追踪验证码提交
* @param {string} phone - 手机号
*/
const trackVerificationCodeSubmitted = useCallback((phone) => {
track(ACTIVATION_EVENTS.VERIFICATION_CODE_SUBMITTED, {
...getBaseProperties(),
phone_masked: phone ? phone.substring(0, 3) + '****' + phone.substring(7) : '',
});
logger.debug('useAuthEvents', '📤 Verification Code Submitted');
}, [track, getBaseProperties]);
// ==================== 微信登录流程 ====================
/**
* 追踪微信二维码显示
* @param {string} sessionId - 会话ID
* @param {string} authUrl - 授权URL
*/
const trackWechatQRDisplayed = useCallback((sessionId, authUrl = '') => {
track(ACTIVATION_EVENTS.WECHAT_QR_DISPLAYED, {
...getBaseProperties(),
session_id: sessionId?.substring(0, 8) + '...',
has_auth_url: Boolean(authUrl),
});
logger.debug('useAuthEvents', '🔲 WeChat QR Code Displayed', { sessionId: sessionId?.substring(0, 8) });
}, [track, getBaseProperties]);
/**
* 追踪微信二维码被扫描
* @param {string} sessionId - 会话ID
*/
const trackWechatQRScanned = useCallback((sessionId) => {
track(ACTIVATION_EVENTS.WECHAT_QR_SCANNED, {
...getBaseProperties(),
session_id: sessionId?.substring(0, 8) + '...',
});
logger.debug('useAuthEvents', '📱 WeChat QR Code Scanned', { sessionId: sessionId?.substring(0, 8) });
}, [track, getBaseProperties]);
/**
* 追踪微信二维码过期
* @param {string} sessionId - 会话ID
* @param {number} timeElapsed - 经过时间(秒)
*/
const trackWechatQRExpired = useCallback((sessionId, timeElapsed = 0) => {
track(ACTIVATION_EVENTS.WECHAT_QR_EXPIRED, {
...getBaseProperties(),
session_id: sessionId?.substring(0, 8) + '...',
time_elapsed: timeElapsed,
});
logger.debug('useAuthEvents', '⏰ WeChat QR Code Expired', { sessionId: sessionId?.substring(0, 8), timeElapsed });
}, [track, getBaseProperties]);
/**
* 追踪刷新微信二维码
* @param {string} oldSessionId - 旧会话ID
* @param {string} newSessionId - 新会话ID
*/
const trackWechatQRRefreshed = useCallback((oldSessionId, newSessionId) => {
track(ACTIVATION_EVENTS.WECHAT_QR_REFRESHED, {
...getBaseProperties(),
old_session_id: oldSessionId?.substring(0, 8) + '...',
new_session_id: newSessionId?.substring(0, 8) + '...',
});
logger.debug('useAuthEvents', '🔄 WeChat QR Code Refreshed');
}, [track, getBaseProperties]);
/**
* 追踪微信登录状态变化
* @param {string} sessionId - 会话ID
* @param {string} oldStatus - 旧状态
* @param {string} newStatus - 新状态
*/
const trackWechatStatusChanged = useCallback((sessionId, oldStatus, newStatus) => {
track(ACTIVATION_EVENTS.WECHAT_STATUS_CHANGED, {
...getBaseProperties(),
session_id: sessionId?.substring(0, 8) + '...',
old_status: oldStatus,
new_status: newStatus,
});
logger.debug('useAuthEvents', '🔄 WeChat Status Changed', { oldStatus, newStatus });
}, [track, getBaseProperties]);
/**
* 追踪移动端跳转微信H5授权
*/
const trackWechatH5Redirect = useCallback(() => {
track(ACTIVATION_EVENTS.WECHAT_H5_REDIRECT, getBaseProperties());
logger.debug('useAuthEvents', '🔗 WeChat H5 Redirect');
}, [track, getBaseProperties]);
// ==================== 登录/注册结果 ====================
/**
* 追踪登录成功并识别用户
* @param {Object} user - 用户对象
* @param {string} loginMethod - 登录方式 ('wechat' | 'phone')
* @param {boolean} isNewUser - 是否为新注册用户
*/
const trackLoginSuccess = useCallback((user, loginMethod, isNewUser = false) => {
// 追踪登录成功事件
const eventName = isNewUser ? ACTIVATION_EVENTS.USER_SIGNED_UP : ACTIVATION_EVENTS.USER_LOGGED_IN;
track(eventName, {
...getBaseProperties(),
user_id: user.id,
login_method: loginMethod,
is_new_user: isNewUser,
has_nickname: Boolean(user.nickname),
has_email: Boolean(user.email),
has_wechat: Boolean(user.wechat_open_id),
});
// 识别用户(关联 PostHog 用户)
identify(user.id.toString(), {
phone: user.phone,
username: user.username,
nickname: user.nickname,
email: user.email,
login_method: loginMethod,
is_new_user: isNewUser,
registration_date: user.created_at,
last_login: new Date().toISOString(),
has_wechat: Boolean(user.wechat_open_id),
wechat_open_id: user.wechat_open_id,
wechat_union_id: user.wechat_union_id,
});
logger.debug('useAuthEvents', `${isNewUser ? 'User Signed Up' : 'User Logged In'}`, {
userId: user.id,
method: loginMethod,
isNewUser,
});
}, [track, identify, getBaseProperties]);
/**
* 追踪登录失败
* @param {string} loginMethod - 登录方式 ('wechat' | 'phone')
* @param {string} errorType - 错误类型
* @param {string} errorMessage - 错误消息
* @param {Object} context - 额外上下文信息
*/
const trackLoginFailed = useCallback((loginMethod, errorType, errorMessage, context = {}) => {
track(ACTIVATION_EVENTS.LOGIN_FAILED, {
...getBaseProperties(),
login_method: loginMethod,
error_type: errorType,
error_message: errorMessage,
...context,
});
logger.debug('useAuthEvents', '❌ Login Failed', { method: loginMethod, errorType, errorMessage });
}, [track, getBaseProperties]);
// ==================== 用户行为细节 ====================
/**
* 追踪表单字段聚焦
* @param {string} fieldName - 字段名称 ('phone' | 'verificationCode')
*/
const trackFormFocused = useCallback((fieldName) => {
track(ACTIVATION_EVENTS.AUTH_FORM_FOCUSED, {
...getBaseProperties(),
field_name: fieldName,
});
logger.debug('useAuthEvents', '🎯 Form Field Focused', { fieldName });
}, [track, getBaseProperties]);
/**
* 追踪表单验证错误
* @param {string} fieldName - 字段名称
* @param {string} errorType - 错误类型
* @param {string} errorMessage - 错误消息
*/
const trackFormValidationError = useCallback((fieldName, errorType, errorMessage) => {
track(ACTIVATION_EVENTS.AUTH_FORM_VALIDATION_ERROR, {
...getBaseProperties(),
field_name: fieldName,
error_type: errorType,
error_message: errorMessage,
});
logger.debug('useAuthEvents', '⚠️ Form Validation Error', { fieldName, errorType });
}, [track, getBaseProperties]);
/**
* 追踪昵称设置引导弹窗显示
* @param {string} phone - 手机号
*/
const trackNicknamePromptShown = useCallback((phone) => {
track(ACTIVATION_EVENTS.NICKNAME_PROMPT_SHOWN, {
...getBaseProperties(),
phone_masked: phone ? phone.substring(0, 3) + '****' + phone.substring(7) : '',
});
logger.debug('useAuthEvents', '💬 Nickname Prompt Shown');
}, [track, getBaseProperties]);
/**
* 追踪用户接受设置昵称
*/
const trackNicknamePromptAccepted = useCallback(() => {
track(ACTIVATION_EVENTS.NICKNAME_PROMPT_ACCEPTED, getBaseProperties());
logger.debug('useAuthEvents', '✅ Nickname Prompt Accepted');
}, [track, getBaseProperties]);
/**
* 追踪用户跳过设置昵称
*/
const trackNicknamePromptSkipped = useCallback(() => {
track(ACTIVATION_EVENTS.NICKNAME_PROMPT_SKIPPED, getBaseProperties());
logger.debug('useAuthEvents', '⏭️ Nickname Prompt Skipped');
}, [track, getBaseProperties]);
/**
* 追踪用户点击用户协议链接
*/
const trackUserAgreementClicked = useCallback(() => {
track(ACTIVATION_EVENTS.USER_AGREEMENT_LINK_CLICKED, getBaseProperties());
logger.debug('useAuthEvents', '📄 User Agreement Link Clicked');
}, [track, getBaseProperties]);
/**
* 追踪用户点击隐私政策链接
*/
const trackPrivacyPolicyClicked = useCallback(() => {
track(ACTIVATION_EVENTS.PRIVACY_POLICY_LINK_CLICKED, getBaseProperties());
logger.debug('useAuthEvents', '📄 Privacy Policy Link Clicked');
}, [track, getBaseProperties]);
// ==================== 错误追踪 ====================
/**
* 追踪通用错误
* @param {string} errorType - 错误类型 ('network' | 'api' | 'validation' | 'session')
* @param {string} errorMessage - 错误消息
* @param {Object} context - 错误上下文
*/
const trackError = useCallback((errorType, errorMessage, context = {}) => {
const eventMap = {
network: ACTIVATION_EVENTS.NETWORK_ERROR_OCCURRED,
api: ACTIVATION_EVENTS.API_ERROR_OCCURRED,
session: ACTIVATION_EVENTS.SESSION_EXPIRED,
default: ACTIVATION_EVENTS.LOGIN_ERROR_OCCURRED,
};
const eventName = eventMap[errorType] || eventMap.default;
track(eventName, {
...getBaseProperties(),
error_type: errorType,
error_message: errorMessage,
...context,
});
logger.error('useAuthEvents', `${errorType} Error`, { errorMessage, context });
}, [track, getBaseProperties]);
// ==================== 返回接口 ====================
return {
// 页面浏览
trackLoginPageViewed,
// 登录方式选择
trackPhoneLoginInitiated,
trackWechatLoginInitiated,
// 手机验证码流程
trackVerificationCodeSent,
trackVerificationCodeSendFailed,
trackVerificationCodeInputChanged,
trackVerificationCodeResent,
trackPhoneNumberValidated,
trackVerificationCodeSubmitted,
// 微信登录流程
trackWechatQRDisplayed,
trackWechatQRScanned,
trackWechatQRExpired,
trackWechatQRRefreshed,
trackWechatStatusChanged,
trackWechatH5Redirect,
// 登录/注册结果
trackLoginSuccess,
trackLoginFailed,
// 用户行为
trackFormFocused,
trackFormValidationError,
trackNicknamePromptShown,
trackNicknamePromptAccepted,
trackNicknamePromptSkipped,
trackUserAgreementClicked,
trackPrivacyPolicyClicked,
// 错误追踪
trackError,
};
};
export default useAuthEvents;

View File

@@ -34,24 +34,54 @@ export const ACTIVATION_EVENTS = {
LOGIN_PAGE_VIEWED: 'Login Page Viewed', LOGIN_PAGE_VIEWED: 'Login Page Viewed',
SIGNUP_PAGE_VIEWED: 'Signup Page Viewed', SIGNUP_PAGE_VIEWED: 'Signup Page Viewed',
// Login/Signup actions // Login method selection
LOGIN_METHOD_SELECTED: 'Login Method Selected', // wechat, email, phone PHONE_LOGIN_INITIATED: 'Phone Login Initiated', // 用户开始填写手机号
WECHAT_LOGIN_INITIATED: 'WeChat Login Initiated', // 用户选择微信登录
// Phone verification code flow
VERIFICATION_CODE_SENT: 'Verification Code Sent',
VERIFICATION_CODE_SEND_FAILED: 'Verification Code Send Failed',
VERIFICATION_CODE_INPUT_CHANGED: 'Verification Code Input Changed',
VERIFICATION_CODE_RESENT: 'Verification Code Resent',
VERIFICATION_CODE_SUBMITTED: 'Verification Code Submitted',
PHONE_NUMBER_VALIDATED: 'Phone Number Validated',
// WeChat login flow
WECHAT_QR_DISPLAYED: 'WeChat QR Code Displayed', WECHAT_QR_DISPLAYED: 'WeChat QR Code Displayed',
WECHAT_QR_SCANNED: 'WeChat QR Code Scanned', WECHAT_QR_SCANNED: 'WeChat QR Code Scanned',
WECHAT_QR_EXPIRED: 'WeChat QR Code Expired',
WECHAT_QR_REFRESHED: 'WeChat QR Code Refreshed',
WECHAT_STATUS_CHANGED: 'WeChat Status Changed',
WECHAT_H5_REDIRECT: 'WeChat H5 Redirect', // 移动端跳转微信H5
// Login/Signup results
USER_LOGGED_IN: 'User Logged In', USER_LOGGED_IN: 'User Logged In',
USER_SIGNED_UP: 'User Signed Up', USER_SIGNED_UP: 'User Signed Up',
VERIFICATION_CODE_SENT: 'Verification Code Sent',
VERIFICATION_CODE_SUBMITTED: 'Verification Code Submitted',
LOGIN_FAILED: 'Login Failed', LOGIN_FAILED: 'Login Failed',
SIGNUP_FAILED: 'Signup Failed', SIGNUP_FAILED: 'Signup Failed',
// User behavior details
AUTH_FORM_FOCUSED: 'Auth Form Field Focused',
AUTH_FORM_VALIDATION_ERROR: 'Auth Form Validation Error',
NICKNAME_PROMPT_SHOWN: 'Nickname Prompt Shown',
NICKNAME_PROMPT_ACCEPTED: 'Nickname Prompt Accepted',
NICKNAME_PROMPT_SKIPPED: 'Nickname Prompt Skipped',
USER_AGREEMENT_LINK_CLICKED: 'User Agreement Link Clicked',
PRIVACY_POLICY_LINK_CLICKED: 'Privacy Policy Link Clicked',
// Error tracking
LOGIN_ERROR_OCCURRED: 'Login Error Occurred',
NETWORK_ERROR_OCCURRED: 'Network Error Occurred',
SESSION_EXPIRED: 'Session Expired',
API_ERROR_OCCURRED: 'API Error Occurred',
// Onboarding // Onboarding
ONBOARDING_STARTED: 'Onboarding Started', ONBOARDING_STARTED: 'Onboarding Started',
ONBOARDING_STEP_COMPLETED: 'Onboarding Step Completed', ONBOARDING_STEP_COMPLETED: 'Onboarding Step Completed',
ONBOARDING_COMPLETED: 'Onboarding Completed', ONBOARDING_COMPLETED: 'Onboarding Completed',
ONBOARDING_SKIPPED: 'Onboarding Skipped', ONBOARDING_SKIPPED: 'Onboarding Skipped',
// User agreement // User agreement (deprecated, use link clicked events instead)
USER_AGREEMENT_VIEWED: 'User Agreement Viewed', USER_AGREEMENT_VIEWED: 'User Agreement Viewed',
USER_AGREEMENT_ACCEPTED: 'User Agreement Accepted', USER_AGREEMENT_ACCEPTED: 'User Agreement Accepted',
PRIVACY_POLICY_VIEWED: 'Privacy Policy Viewed', PRIVACY_POLICY_VIEWED: 'Privacy Policy Viewed',