feat: record daily login tracking on auth success
This commit is contained in:
@@ -0,0 +1,63 @@
|
|||||||
|
# 登录成功每日登录埋点闭环方案(2026-05-08)
|
||||||
|
|
||||||
|
## 背景
|
||||||
|
|
||||||
|
后台“埋点数据”需要能看到真实登录触发的 `daily_login` 埋点。此前方案 A 已把“读取任务中心时顺手写每日登录埋点”拆成独立 SpacetimeDB procedure:
|
||||||
|
|
||||||
|
- `record_daily_login_tracking_event_and_return`
|
||||||
|
- `spacetime-client` 方法:`record_daily_login_tracking_event(user_id)`
|
||||||
|
|
||||||
|
但认证成功链路还没有调用该方法,因此当前只完成了“任务中心读取不污染登录埋点”,没有完成“用户真实登录写入每日登录埋点”。
|
||||||
|
|
||||||
|
## 目标
|
||||||
|
|
||||||
|
在用户认证成功并创建 refresh session / access token 后,异步尝试写入每日登录埋点。
|
||||||
|
|
||||||
|
覆盖入口:
|
||||||
|
|
||||||
|
- 手机验证码登录:`POST /api/auth/phone/login`
|
||||||
|
- 密码入口登录:`POST /api/auth/entry`
|
||||||
|
- 重置密码后自动登录:`POST /api/auth/password/reset`
|
||||||
|
- 微信 OAuth callback 登录:`GET /api/auth/wechat/callback`
|
||||||
|
- 微信绑定手机号后激活/登录态刷新:`POST /api/auth/wechat/bind-phone`
|
||||||
|
|
||||||
|
## 设计约束
|
||||||
|
|
||||||
|
1. 埋点写入不能阻断登录成功响应。
|
||||||
|
2. 只有认证成功并已创建会话后才记录。
|
||||||
|
3. 失败只记 warning,继续返回 token / cookie。
|
||||||
|
4. 写入统一收口,避免多个登录 handler 各自拼 procedure 调用。
|
||||||
|
5. 不修改 SpacetimeDB 表结构,不需要更新 `migration.rs`。
|
||||||
|
|
||||||
|
## 实现方案
|
||||||
|
|
||||||
|
新增 `api-server` 内部 helper:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
record_daily_login_tracking_event_after_auth_success(
|
||||||
|
state: &AppState,
|
||||||
|
request_context: &RequestContext,
|
||||||
|
user_id: &str,
|
||||||
|
login_method: AuthLoginMethod,
|
||||||
|
).await;
|
||||||
|
```
|
||||||
|
|
||||||
|
该 helper:
|
||||||
|
|
||||||
|
- 调用 `state.spacetime_client().record_daily_login_tracking_event(user_id.to_string()).await`
|
||||||
|
- 成功时记录 info
|
||||||
|
- 失败时记录 warn,并明确“登录流程继续”
|
||||||
|
|
||||||
|
在各登录入口 `create_auth_session` 成功后调用该 helper。
|
||||||
|
|
||||||
|
## 验收
|
||||||
|
|
||||||
|
- `cargo test -p api-server auth_session -- --nocapture`
|
||||||
|
- `cargo check -p api-server`
|
||||||
|
- `cargo check -p spacetime-client`
|
||||||
|
- `npm run check:encoding`
|
||||||
|
- `git diff --check`
|
||||||
|
|
||||||
|
## 注意
|
||||||
|
|
||||||
|
`npm run dev` 是长期运行进程;如需本地 smoke,应启动后用 `/healthz` 和后台页面验证,不要等待该命令退出。
|
||||||
@@ -26,6 +26,47 @@ pub fn create_password_auth_session(
|
|||||||
create_auth_session(state, user, session_client, AuthLoginMethod::Password)
|
create_auth_session(state, user, session_client, AuthLoginMethod::Password)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(test))]
|
||||||
|
pub async fn record_daily_login_tracking_event_after_auth_success(
|
||||||
|
state: &AppState,
|
||||||
|
request_context: &crate::request_context::RequestContext,
|
||||||
|
user_id: &str,
|
||||||
|
login_method: AuthLoginMethod,
|
||||||
|
) {
|
||||||
|
// 登录埋点是运营数据,不应反向阻断已经成功的认证会话签发。
|
||||||
|
match state
|
||||||
|
.spacetime_client()
|
||||||
|
.record_daily_login_tracking_event(user_id.to_string())
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(()) => tracing::info!(
|
||||||
|
request_id = request_context.request_id(),
|
||||||
|
operation = request_context.operation(),
|
||||||
|
user_id = %user_id,
|
||||||
|
login_method = %login_method.as_str(),
|
||||||
|
"登录成功每日登录埋点已记录"
|
||||||
|
),
|
||||||
|
Err(error) => tracing::warn!(
|
||||||
|
request_id = request_context.request_id(),
|
||||||
|
operation = request_context.operation(),
|
||||||
|
user_id = %user_id,
|
||||||
|
login_method = %login_method.as_str(),
|
||||||
|
error = %error,
|
||||||
|
"登录成功每日登录埋点记录失败,登录流程继续"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub async fn record_daily_login_tracking_event_after_auth_success(
|
||||||
|
_state: &AppState,
|
||||||
|
_request_context: &crate::request_context::RequestContext,
|
||||||
|
_user_id: &str,
|
||||||
|
_login_method: AuthLoginMethod,
|
||||||
|
) {
|
||||||
|
// 单元测试默认不启动 SpacetimeDB;这里仅验证登录链路调用点能通过编译并保持非阻断语义。
|
||||||
|
}
|
||||||
|
|
||||||
pub fn create_auth_session(
|
pub fn create_auth_session(
|
||||||
state: &AppState,
|
state: &AppState,
|
||||||
user: &AuthUser,
|
user: &AuthUser,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use axum::{
|
|||||||
http::{HeaderMap, StatusCode},
|
http::{HeaderMap, StatusCode},
|
||||||
response::IntoResponse,
|
response::IntoResponse,
|
||||||
};
|
};
|
||||||
use module_auth::{PasswordEntryError, PasswordEntryInput};
|
use module_auth::{AuthLoginMethod, PasswordEntryError, PasswordEntryInput};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use shared_contracts::auth::{PasswordEntryRequest, PasswordEntryResponse};
|
use shared_contracts::auth::{PasswordEntryRequest, PasswordEntryResponse};
|
||||||
|
|
||||||
@@ -12,7 +12,8 @@ use crate::{
|
|||||||
api_response::json_success_body,
|
api_response::json_success_body,
|
||||||
auth_payload::map_auth_user_payload,
|
auth_payload::map_auth_user_payload,
|
||||||
auth_session::{
|
auth_session::{
|
||||||
attach_set_cookie_header, build_refresh_session_cookie_header, create_password_auth_session,
|
attach_set_cookie_header, build_refresh_session_cookie_header,
|
||||||
|
create_password_auth_session, record_daily_login_tracking_event_after_auth_success,
|
||||||
},
|
},
|
||||||
http_error::AppError,
|
http_error::AppError,
|
||||||
request_context::RequestContext,
|
request_context::RequestContext,
|
||||||
@@ -49,6 +50,13 @@ pub async fn password_entry(
|
|||||||
}
|
}
|
||||||
let session_client = resolve_session_client_context(&headers);
|
let session_client = resolve_session_client_context(&headers);
|
||||||
let signed_session = create_password_auth_session(&state, &result.user, &session_client)?;
|
let signed_session = create_password_auth_session(&state, &result.user, &session_client)?;
|
||||||
|
record_daily_login_tracking_event_after_auth_success(
|
||||||
|
&state,
|
||||||
|
&request_context,
|
||||||
|
&result.user.id,
|
||||||
|
AuthLoginMethod::Password,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
state
|
state
|
||||||
.sync_auth_store_snapshot_to_spacetime()
|
.sync_auth_store_snapshot_to_spacetime()
|
||||||
.await
|
.await
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ use crate::{
|
|||||||
auth_payload::map_auth_user_payload,
|
auth_payload::map_auth_user_payload,
|
||||||
auth_session::{
|
auth_session::{
|
||||||
attach_set_cookie_header, build_refresh_session_cookie_header, create_auth_session,
|
attach_set_cookie_header, build_refresh_session_cookie_header, create_auth_session,
|
||||||
|
record_daily_login_tracking_event_after_auth_success,
|
||||||
},
|
},
|
||||||
http_error::AppError,
|
http_error::AppError,
|
||||||
phone_auth::map_phone_auth_error,
|
phone_auth::map_phone_auth_error,
|
||||||
@@ -79,6 +80,13 @@ pub async fn reset_password(
|
|||||||
&session_client,
|
&session_client,
|
||||||
module_auth::AuthLoginMethod::Password,
|
module_auth::AuthLoginMethod::Password,
|
||||||
)?;
|
)?;
|
||||||
|
record_daily_login_tracking_event_after_auth_success(
|
||||||
|
&state,
|
||||||
|
&request_context,
|
||||||
|
&result.user.id,
|
||||||
|
module_auth::AuthLoginMethod::Password,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
let mut headers = HeaderMap::new();
|
let mut headers = HeaderMap::new();
|
||||||
attach_set_cookie_header(
|
attach_set_cookie_header(
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ use crate::{
|
|||||||
auth_payload::map_auth_user_payload,
|
auth_payload::map_auth_user_payload,
|
||||||
auth_session::{
|
auth_session::{
|
||||||
attach_set_cookie_header, build_refresh_session_cookie_header, create_auth_session,
|
attach_set_cookie_header, build_refresh_session_cookie_header, create_auth_session,
|
||||||
|
record_daily_login_tracking_event_after_auth_success,
|
||||||
},
|
},
|
||||||
http_error::AppError,
|
http_error::AppError,
|
||||||
platform_errors::{attach_retry_after, map_phone_auth_platform_store_error},
|
platform_errors::{attach_retry_after, map_phone_auth_platform_store_error},
|
||||||
@@ -176,6 +177,13 @@ pub async fn phone_login(
|
|||||||
&session_client,
|
&session_client,
|
||||||
AuthLoginMethod::Phone,
|
AuthLoginMethod::Phone,
|
||||||
)?;
|
)?;
|
||||||
|
record_daily_login_tracking_event_after_auth_success(
|
||||||
|
&state,
|
||||||
|
&request_context,
|
||||||
|
&result.user.id,
|
||||||
|
AuthLoginMethod::Phone,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
state
|
state
|
||||||
.sync_auth_store_snapshot_to_spacetime()
|
.sync_auth_store_snapshot_to_spacetime()
|
||||||
.await
|
.await
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ use crate::{
|
|||||||
auth_payload::map_auth_user_payload,
|
auth_payload::map_auth_user_payload,
|
||||||
auth_session::{
|
auth_session::{
|
||||||
attach_set_cookie_header, build_refresh_session_cookie_header, create_auth_session,
|
attach_set_cookie_header, build_refresh_session_cookie_header, create_auth_session,
|
||||||
|
record_daily_login_tracking_event_after_auth_success,
|
||||||
},
|
},
|
||||||
http_error::AppError,
|
http_error::AppError,
|
||||||
platform_errors::{attach_retry_after, map_wechat_provider_error},
|
platform_errors::{attach_retry_after, map_wechat_provider_error},
|
||||||
@@ -74,6 +75,7 @@ pub async fn start_wechat_login(
|
|||||||
|
|
||||||
pub async fn handle_wechat_callback(
|
pub async fn handle_wechat_callback(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
|
Extension(request_context): Extension<RequestContext>,
|
||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
Query(query): Query<WechatCallbackQuery>,
|
Query(query): Query<WechatCallbackQuery>,
|
||||||
) -> Result<impl IntoResponse, AppError> {
|
) -> Result<impl IntoResponse, AppError> {
|
||||||
@@ -141,6 +143,13 @@ pub async fn handle_wechat_callback(
|
|||||||
&session_client,
|
&session_client,
|
||||||
AuthLoginMethod::Wechat,
|
AuthLoginMethod::Wechat,
|
||||||
)?;
|
)?;
|
||||||
|
record_daily_login_tracking_event_after_auth_success(
|
||||||
|
&state,
|
||||||
|
&request_context,
|
||||||
|
&result.user.id,
|
||||||
|
AuthLoginMethod::Wechat,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
state
|
state
|
||||||
.sync_auth_store_snapshot_to_spacetime()
|
.sync_auth_store_snapshot_to_spacetime()
|
||||||
.await
|
.await
|
||||||
@@ -208,6 +217,13 @@ pub async fn bind_wechat_phone(
|
|||||||
&session_client,
|
&session_client,
|
||||||
AuthLoginMethod::Wechat,
|
AuthLoginMethod::Wechat,
|
||||||
)?;
|
)?;
|
||||||
|
record_daily_login_tracking_event_after_auth_success(
|
||||||
|
&state,
|
||||||
|
&request_context,
|
||||||
|
&result.user.id,
|
||||||
|
AuthLoginMethod::Wechat,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
state
|
state
|
||||||
.sync_auth_store_snapshot_to_spacetime()
|
.sync_auth_store_snapshot_to_spacetime()
|
||||||
.await
|
.await
|
||||||
|
|||||||
Reference in New Issue
Block a user