From c42a14aa8f887f606d14baf983b1bae125019824 Mon Sep 17 00:00:00 2001 From: zdl <3489966805@qq.com> Date: Tue, 28 Oct 2025 21:45:06 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E9=A6=96=E9=A1=B5=E7=99=BB=E9=99=86?= =?UTF-8?q?=E4=BA=8B=E4=BB=B6=E8=BF=BD=E8=B8=AA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Auth/AuthFormContent.js | 102 +++++- src/components/Auth/WechatRegister.js | 59 +++- src/hooks/useAuthEvents.js | 463 +++++++++++++++++++++++++ src/lib/constants.js | 40 ++- 4 files changed, 653 insertions(+), 11 deletions(-) create mode 100644 src/hooks/useAuthEvents.js diff --git a/src/components/Auth/AuthFormContent.js b/src/components/Auth/AuthFormContent.js index 793f2293..5618e64a 100644 --- a/src/components/Auth/AuthFormContent.js +++ b/src/components/Auth/AuthFormContent.js @@ -37,6 +37,7 @@ import VerificationCodeInput from './VerificationCodeInput'; import WechatRegister from './WechatRegister'; import { setCurrentUser } from '../../mocks/data/users'; import { logger } from '../../utils/logger'; +import { useAuthEvents } from '../../hooks/useAuthEvents'; // 统一配置对象 const AUTH_CONFIG = { @@ -86,6 +87,12 @@ export default function AuthFormContent() { // 响应式布局配置 const isMobile = useBreakpointValue({ base: true, md: false }); + + // 事件追踪 + const authEvents = useAuthEvents({ + component: 'AuthFormContent', + isMobile: isMobile + }); const stackDirection = useBreakpointValue({ base: "column", md: "row" }); const stackSpacing = useBreakpointValue({ base: 4, md: 2 }); // ✅ 桌面端从32px减至8px,更紧凑 @@ -107,6 +114,16 @@ export default function AuthFormContent() { ...prev, [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)) { + // 追踪手机号验证失败 + authEvents.trackPhoneNumberValidated(credential, false, 'invalid_format'); + authEvents.trackFormValidationError('phone', 'invalid_format', '请输入有效的手机号'); + toast({ title: "请输入有效的手机号", status: "warning", @@ -152,6 +173,9 @@ export default function AuthFormContent() { return; } + // 追踪手机号验证通过 + authEvents.trackPhoneNumberValidated(credential, true); + try { setSendingCode(true); @@ -187,6 +211,14 @@ export default function AuthFormContent() { } 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,静默处理 logger.info('AuthFormContent', '验证码发送成功', { credential: credential.substring(0, 3) + '****' + credential.substring(7), @@ -204,6 +236,13 @@ export default function AuthFormContent() { throw new Error(data.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, { credential: credential.substring(0, 3) + '****' + credential.substring(7) }); @@ -256,6 +295,9 @@ export default function AuthFormContent() { return; } + // 追踪验证码提交 + authEvents.trackVerificationCodeSubmitted(phone); + // 构建请求体 const requestBody = { credential: phone.trim(), // 添加 trim() 防止空格 @@ -310,6 +352,9 @@ export default function AuthFormContent() { // 更新session await checkSession(); + // 追踪登录成功并识别用户 + authEvents.trackLoginSuccess(data.user, 'phone', data.isNewUser); + // ✅ 保留登录成功 toast(关键操作提示) toast({ title: data.isNewUser ? '注册成功' : '登录成功', @@ -329,6 +374,8 @@ export default function AuthFormContent() { setTimeout(() => { setCurrentPhone(phone); setShowNicknamePrompt(true); + // 追踪昵称设置引导显示 + authEvents.trackNicknamePromptShown(phone); }, config.features.successDelay); } else { // 已有用户,直接登录成功 @@ -349,6 +396,15 @@ export default function AuthFormContent() { } } catch (error) { 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, { phone: phone ? phone.substring(0, 3) + '****' + phone.substring(7) : 'N/A', hasVerificationCode: !!verificationCode @@ -376,6 +432,9 @@ export default function AuthFormContent() { // 微信H5登录处理 const handleWechatH5Login = async () => { + // 追踪用户选择微信登录 + authEvents.trackWechatLoginInitiated('icon_button'); + try { // 1. 构建回调URL const redirectUrl = `${window.location.origin}/home/wechat-callback`; @@ -396,11 +455,19 @@ export default function AuthFormContent() { throw new Error('获取授权链接失败'); } + // 追踪微信H5跳转 + authEvents.trackWechatH5Redirect(); + // 4. 延迟跳转,让用户看到提示 setTimeout(() => { window.location.href = response.auth_url; }, 500); } catch (error) { + // 追踪跳转失败 + authEvents.trackError('api', error.message || '获取微信授权链接失败', { + context: 'wechat_h5_redirect' + }); + logger.error('AuthFormContent', 'handleWechatH5Login', error); toast({ title: "跳转失败", @@ -412,14 +479,17 @@ export default function AuthFormContent() { } }; - // 组件卸载时清理 + // 组件挂载时追踪页面浏览 useEffect(() => { isMountedRef.current = true; + // 追踪登录页面浏览 + authEvents.trackLoginPageViewed(); + return () => { isMountedRef.current = false; }; - }, []); + }, [authEvents]); return ( <> @@ -479,6 +549,7 @@ export default function AuthFormContent() { color="blue.500" textDecoration="underline" _hover={{ color: "blue.600" }} + onClick={authEvents.trackUserAgreementClicked} > 《用户协议》 @@ -491,6 +562,7 @@ export default function AuthFormContent() { color="blue.500" textDecoration="underline" _hover={{ color: "blue.600" }} + onClick={authEvents.trackPrivacyPolicyClicked} > 《隐私政策》 @@ -518,8 +590,30 @@ export default function AuthFormContent() { 完善个人信息 您已成功注册!是否前往个人资料设置昵称和其他信息? - - + + diff --git a/src/components/Auth/WechatRegister.js b/src/components/Auth/WechatRegister.js index 9a01abc9..a3a85bc9 100644 --- a/src/components/Auth/WechatRegister.js +++ b/src/components/Auth/WechatRegister.js @@ -18,6 +18,7 @@ import { authService, WECHAT_STATUS, STATUS_MESSAGES } from "../../services/auth import { useAuthModal } from "../../contexts/AuthModalContext"; import { useAuth } from "../../contexts/AuthContext"; import { logger } from "../../utils/logger"; +import { useAuthEvents } from "../../hooks/useAuthEvents"; // 配置常量 const POLL_INTERVAL = 2000; // 轮询间隔:2秒 @@ -51,6 +52,12 @@ export default function WechatRegister() { const { closeModal } = useAuthModal(); const { refreshSession } = useAuth(); + // 事件追踪 + const authEvents = useAuthEvents({ + component: 'WechatRegister', + isMobile: false // WechatRegister 只在桌面端显示 + }); + // 状态管理 const [wechatAuthUrl, setWechatAuthUrl] = useState(""); const [wechatSessionId, setWechatSessionId] = useState(""); @@ -126,6 +133,13 @@ export default function WechatRegister() { logger.info('WechatRegister', '登录接口返回', { success: response?.success, hasUser: !!response?.user }); if (response?.success) { + // 追踪微信登录成功 + authEvents.trackLoginSuccess( + response.user, + 'wechat', + response.isNewUser || false + ); + // Session cookie 会自动管理,不需要手动存储 // 如果后端返回了 token,可以选择性存储(兼容旧方式) if (response.token) { @@ -148,10 +162,16 @@ export default function WechatRegister() { throw new Error(response?.error || '登录失败'); } } catch (error) { + // 追踪微信登录失败 + authEvents.trackLoginFailed('wechat', 'api', error.message || '登录失败', { + session_id: sessionId?.substring(0, 8) + '...', + status: status + }); + logger.error('WechatRegister', 'handleLoginSuccess', error, { sessionId }); 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 (wechatStatus !== status) { + authEvents.trackWechatStatusChanged(currentSessionId, wechatStatus, status); + + // 特别追踪扫码事件 + if (status === WECHAT_STATUS.SCANNED) { + authEvents.trackWechatQRScanned(currentSessionId); + } + } + setWechatStatus(status); // 处理成功状态 @@ -203,6 +233,9 @@ export default function WechatRegister() { } // 处理过期状态 else if (status === WECHAT_STATUS.EXPIRED) { + // 追踪二维码过期 + authEvents.trackWechatQRExpired(currentSessionId, QR_CODE_TIMEOUT / 1000); + clearTimers(); sessionIdRef.current = null; // 清理 sessionId if (isMountedRef.current) { @@ -268,6 +301,16 @@ export default function WechatRegister() { try { setIsLoading(true); + // 追踪用户选择微信登录(首次或刷新) + const isRefresh = Boolean(wechatSessionId); + if (isRefresh) { + const oldSessionId = wechatSessionId; + authEvents.trackWechatLoginInitiated('qr_refresh'); + // 稍后会在成功时追踪刷新事件 + } else { + authEvents.trackWechatLoginInitiated('qr_area'); + } + // 生产环境:调用真实 API const response = await authService.getWechatQRCode(); @@ -283,6 +326,13 @@ export default function WechatRegister() { 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,确保轮询能立即读取到最新值 sessionIdRef.current = response.data.session_id; setWechatAuthUrl(response.data.auth_url); @@ -297,6 +347,11 @@ export default function WechatRegister() { // 启动轮询检查扫码状态 startPolling(); } catch (error) { + // 追踪获取二维码失败 + authEvents.trackError('api', error.message || '获取二维码失败', { + context: 'get_wechat_qrcode' + }); + logger.error('WechatRegister', 'getWechatQRCode', error); if (isMountedRef.current) { showError("获取微信授权失败", error.message || "请稍后重试"); @@ -306,7 +361,7 @@ export default function WechatRegister() { setIsLoading(false); } } - }, [startPolling, showError]); + }, [startPolling, showError, wechatSessionId, authEvents]); /** * 安全的按钮点击处理,确保所有错误都被捕获,防止被 ErrorBoundary 捕获 diff --git a/src/hooks/useAuthEvents.js b/src/hooks/useAuthEvents.js new file mode 100644 index 00000000..db7a4bfc --- /dev/null +++ b/src/hooks/useAuthEvents.js @@ -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; diff --git a/src/lib/constants.js b/src/lib/constants.js index ec47bd03..a5886a8b 100644 --- a/src/lib/constants.js +++ b/src/lib/constants.js @@ -34,24 +34,54 @@ export const ACTIVATION_EVENTS = { LOGIN_PAGE_VIEWED: 'Login Page Viewed', SIGNUP_PAGE_VIEWED: 'Signup Page Viewed', - // Login/Signup actions - LOGIN_METHOD_SELECTED: 'Login Method Selected', // wechat, email, phone + // Login method selection + 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_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_SIGNED_UP: 'User Signed Up', - VERIFICATION_CODE_SENT: 'Verification Code Sent', - VERIFICATION_CODE_SUBMITTED: 'Verification Code Submitted', LOGIN_FAILED: 'Login 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_STARTED: 'Onboarding Started', ONBOARDING_STEP_COMPLETED: 'Onboarding Step Completed', ONBOARDING_COMPLETED: 'Onboarding Completed', ONBOARDING_SKIPPED: 'Onboarding Skipped', - // User agreement + // User agreement (deprecated, use link clicked events instead) USER_AGREEMENT_VIEWED: 'User Agreement Viewed', USER_AGREEMENT_ACCEPTED: 'User Agreement Accepted', PRIVACY_POLICY_VIEWED: 'Privacy Policy Viewed',