feat: 首页登陆事件追踪
This commit is contained in:
@@ -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}
|
||||
>
|
||||
《用户协议》
|
||||
</ChakraLink>
|
||||
@@ -491,6 +562,7 @@ export default function AuthFormContent() {
|
||||
color="blue.500"
|
||||
textDecoration="underline"
|
||||
_hover={{ color: "blue.600" }}
|
||||
onClick={authEvents.trackPrivacyPolicyClicked}
|
||||
>
|
||||
《隐私政策》
|
||||
</ChakraLink>
|
||||
@@ -518,8 +590,30 @@ export default function AuthFormContent() {
|
||||
<AlertDialogHeader fontSize="lg" fontWeight="bold">完善个人信息</AlertDialogHeader>
|
||||
<AlertDialogBody>您已成功注册!是否前往个人资料设置昵称和其他信息?</AlertDialogBody>
|
||||
<AlertDialogFooter>
|
||||
<Button ref={cancelRef} onClick={() => { setShowNicknamePrompt(false); handleLoginSuccess({ phone: currentPhone }); }}>稍后再说</Button>
|
||||
<Button colorScheme="green" onClick={() => { setShowNicknamePrompt(false); handleLoginSuccess({ phone: currentPhone }); setTimeout(() => { navigate('/home/profile'); }, 300); }} ml={3}>去设置</Button>
|
||||
<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>
|
||||
</AlertDialogContent>
|
||||
</AlertDialogOverlay>
|
||||
|
||||
@@ -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 捕获
|
||||
|
||||
Reference in New Issue
Block a user