457 lines
15 KiB
JavaScript
457 lines
15 KiB
JavaScript
// 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,
|
|
});
|
|
}, [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),
|
|
});
|
|
}, [track, getBaseProperties]);
|
|
|
|
/**
|
|
* 追踪微信二维码被扫描
|
|
* @param {string} sessionId - 会话ID
|
|
*/
|
|
const trackWechatQRScanned = useCallback((sessionId) => {
|
|
track(ACTIVATION_EVENTS.WECHAT_QR_SCANNED, {
|
|
...getBaseProperties(),
|
|
session_id: 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,
|
|
});
|
|
}, [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) + '...',
|
|
});
|
|
}, [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,
|
|
});
|
|
}, [track, getBaseProperties]);
|
|
|
|
/**
|
|
* 追踪移动端跳转微信H5授权
|
|
*/
|
|
const trackWechatH5Redirect = useCallback(() => {
|
|
track(ACTIVATION_EVENTS.WECHAT_H5_REDIRECT, getBaseProperties());
|
|
}, [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;
|