diff --git a/server-rs/crates/api-server/src/app.rs b/server-rs/crates/api-server/src/app.rs index 0560ab8c9..df0506f49 100644 --- a/server-rs/crates/api-server/src/app.rs +++ b/server-rs/crates/api-server/src/app.rs @@ -24,7 +24,7 @@ use crate::{ response_headers::propagate_request_id_header, state::{AppState, BackpressureState}, telemetry::record_http_observability, - tracking::record_route_tracking_event_after_success, + tracking::{record_route_tracking_event_after_success, resolve_tracking_client_marker}, wechat::pay::{ handle_wechat_pay_notify, handle_wechat_pay_refund_notify, handle_wechat_virtual_payment_message_push_verify, handle_wechat_virtual_payment_notify, @@ -241,6 +241,7 @@ async fn record_api_tracking_after_success( ) -> Response { let method = request.method().clone(); let path = request.uri().path().to_string(); + let client_marker = resolve_tracking_client_marker(request.headers()); let response = next.run(request).await; let authenticated = response .extensions() @@ -253,6 +254,7 @@ async fn record_api_tracking_after_success( &path, response.status(), authenticated.as_ref(), + client_marker, ) .await; response diff --git a/server-rs/crates/api-server/src/tracking.rs b/server-rs/crates/api-server/src/tracking.rs index f7c31bb9a..f60c0d541 100644 --- a/server-rs/crates/api-server/src/tracking.rs +++ b/server-rs/crates/api-server/src/tracking.rs @@ -1,4 +1,4 @@ -use axum::http::{Method, StatusCode}; +use axum::http::{HeaderMap, Method, StatusCode}; #[cfg(not(test))] use module_auth::AuthLoginMethod; use module_runtime::RuntimeTrackingScopeKind; @@ -8,6 +8,22 @@ use uuid::Uuid; use crate::{auth::AuthenticatedAccessToken, request_context::RequestContext, state::AppState}; +const AGC_CLIENT_MARKER_HEADER: &str = "x-genarrative-client"; +const AGC_CLIENT_MARKER_VALUE: &str = "agc"; + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) enum TrackingClientMarker { + Agc, +} + +pub(crate) fn resolve_tracking_client_marker(headers: &HeaderMap) -> Option { + headers + .get(AGC_CLIENT_MARKER_HEADER) + .and_then(|value| value.to_str().ok()) + .filter(|value| value.trim() == AGC_CLIENT_MARKER_VALUE) + .map(|_| TrackingClientMarker::Agc) +} + /// 后端用户行为埋点入口统一走这里:写入失败只记录日志,不反向阻断主业务。 #[derive(Clone, Debug)] pub struct TrackingEventDraft { @@ -19,6 +35,7 @@ pub struct TrackingEventDraft { pub profile_id: Option, pub module_key: Option<&'static str>, pub metadata: Value, + pub(crate) client_marker: Option, } impl TrackingEventDraft { @@ -32,6 +49,7 @@ impl TrackingEventDraft { profile_id: None, module_key: Some(module_key), metadata: json!({}), + client_marker: None, } } @@ -110,6 +128,7 @@ pub async fn record_route_tracking_event_after_success( path: &str, status: StatusCode, authenticated: Option<&AuthenticatedAccessToken>, + client_marker: Option, ) { if !status.is_success() { return; @@ -130,6 +149,7 @@ pub async fn record_route_tracking_event_after_success( draft.scope_kind = spec.scope_kind; draft.scope_id = scope_id; draft.user_id = user_id; + draft.client_marker = client_marker; draft.metadata = build_route_tracking_metadata(&spec, request_context, method, path, status); if draft.user_id.is_some() { draft.owner_user_id = draft.user_id.clone(); @@ -836,9 +856,42 @@ fn runtime_profile_beijing_day_key(occurred_at_micros: i64) -> i64 { #[cfg(test)] mod tests { - use axum::http::Method; + use axum::http::{HeaderMap, HeaderValue, Method}; - use super::{is_route_tracking_excluded, normalize_route_path, resolve_route_tracking_spec}; + use super::{ + TrackingClientMarker, is_route_tracking_excluded, normalize_route_path, + resolve_route_tracking_spec, resolve_tracking_client_marker, + }; + + #[test] + fn tracking_client_marker_accepts_only_trimmed_lowercase_agc() { + let cases = [ + (None, None), + (Some(""), None), + (Some("AGC"), None), + (Some("other"), None), + (Some(" agc "), Some(TrackingClientMarker::Agc)), + (Some("agc"), Some(TrackingClientMarker::Agc)), + ]; + + for (value, expected) in cases { + let mut headers = HeaderMap::new(); + if let Some(value) = value { + headers.insert( + "X-GENARRATIVE-CLIENT", + HeaderValue::from_str(value).expect("marker fixture should be valid"), + ); + } + assert_eq!(resolve_tracking_client_marker(&headers), expected); + } + + let mut invalid_headers = HeaderMap::new(); + invalid_headers.insert( + "x-genarrative-client", + HeaderValue::from_bytes(&[0xff]).expect("invalid text fixture should be accepted"), + ); + assert_eq!(resolve_tracking_client_marker(&invalid_headers), None); + } #[test] fn route_normalization_preserves_static_segments_and_replaces_ids() {