阶段3补齐主站路由埋点覆盖
补齐账号态和 External v1 路由 tracking 规范 复用资产详细事件并传递 AGC 标记,避免重复记录 修正 External API Key 主体归属和动态路由归一化
This commit is contained in:
@@ -889,6 +889,8 @@ pub async fn admin_create_editor_showcase_campaign_image_upload_ticket(
|
||||
&state,
|
||||
&request_context,
|
||||
admin.session().subject.as_str(),
|
||||
Some(admin.session().subject.as_str()),
|
||||
None,
|
||||
CreateDirectUploadTicketRequest {
|
||||
legacy_prefix: "generated-character-drafts".to_string(),
|
||||
path_segments: vec![
|
||||
@@ -944,6 +946,8 @@ pub async fn admin_confirm_editor_showcase_campaign_image_upload(
|
||||
&state,
|
||||
&request_context,
|
||||
admin.session().subject.as_str(),
|
||||
Some(admin.session().subject.as_str()),
|
||||
None,
|
||||
confirm_request,
|
||||
)
|
||||
.await
|
||||
|
||||
@@ -237,12 +237,15 @@ async fn spacetime_unavailable_handler(
|
||||
async fn record_api_tracking_after_success(
|
||||
axum::extract::State(state): axum::extract::State<AppState>,
|
||||
Extension(request_context): Extension<RequestContext>,
|
||||
request: Request<Body>,
|
||||
mut request: Request<Body>,
|
||||
next: middleware::Next,
|
||||
) -> Response {
|
||||
let method = request.method().clone();
|
||||
let path = request.uri().path().to_string();
|
||||
let client_marker = resolve_tracking_client_marker(request.headers());
|
||||
if let Some(client_marker) = client_marker {
|
||||
request.extensions_mut().insert(client_marker);
|
||||
}
|
||||
let response = next.run(request).await;
|
||||
let authenticated = response
|
||||
.extensions()
|
||||
|
||||
@@ -32,7 +32,7 @@ use crate::{
|
||||
platform_errors::map_oss_error,
|
||||
request_context::RequestContext,
|
||||
state::AppState,
|
||||
tracking::{TrackingEventDraft, record_tracking_event_after_success},
|
||||
tracking::{TrackingClientMarker, TrackingEventDraft, record_tracking_event_after_success},
|
||||
};
|
||||
|
||||
// 历史素材类型需要与 SpacetimeDB 侧白名单保持同一口径,避免新增素材类型时 HTTP 门面漏同步。
|
||||
@@ -75,12 +75,15 @@ pub async fn create_direct_upload_ticket(
|
||||
State(state): State<AppState>,
|
||||
Extension(request_context): Extension<RequestContext>,
|
||||
Extension(authenticated): Extension<AuthenticatedAccessToken>,
|
||||
client_marker: Option<Extension<TrackingClientMarker>>,
|
||||
Json(payload): Json<CreateDirectUploadTicketRequest>,
|
||||
) -> Result<Json<Value>, AppError> {
|
||||
create_direct_upload_ticket_for_owner(
|
||||
&state,
|
||||
&request_context,
|
||||
authenticated.claims().user_id(),
|
||||
Some(authenticated.claims().user_id()),
|
||||
client_marker.map(|Extension(marker)| marker),
|
||||
payload,
|
||||
)
|
||||
.await
|
||||
@@ -90,6 +93,8 @@ pub(crate) async fn create_direct_upload_ticket_for_owner(
|
||||
state: &AppState,
|
||||
request_context: &RequestContext,
|
||||
owner_user_id: &str,
|
||||
tracking_user_id: Option<&str>,
|
||||
client_marker: Option<TrackingClientMarker>,
|
||||
payload: CreateDirectUploadTicketRequest,
|
||||
) -> Result<Json<Value>, AppError> {
|
||||
let oss_client = state.oss_client().ok_or_else(|| {
|
||||
@@ -140,8 +145,10 @@ pub(crate) async fn create_direct_upload_ticket_for_owner(
|
||||
record_asset_tracking_event(
|
||||
state,
|
||||
request_context,
|
||||
tracking_user_id,
|
||||
owner_user_id,
|
||||
"asset_upload_ticket_create",
|
||||
client_marker,
|
||||
json!({
|
||||
"asset": {
|
||||
"operation": "asset_upload_ticket_create",
|
||||
@@ -454,12 +461,15 @@ pub async fn confirm_asset_object(
|
||||
State(state): State<AppState>,
|
||||
Extension(request_context): Extension<RequestContext>,
|
||||
Extension(authenticated): Extension<AuthenticatedAccessToken>,
|
||||
client_marker: Option<Extension<TrackingClientMarker>>,
|
||||
Json(payload): Json<ConfirmAssetObjectRequest>,
|
||||
) -> Result<Json<Value>, AppError> {
|
||||
confirm_asset_object_for_owner(
|
||||
&state,
|
||||
&request_context,
|
||||
authenticated.claims().user_id(),
|
||||
Some(authenticated.claims().user_id()),
|
||||
client_marker.map(|Extension(marker)| marker),
|
||||
payload,
|
||||
)
|
||||
.await
|
||||
@@ -469,6 +479,8 @@ pub(crate) async fn confirm_asset_object_for_owner(
|
||||
state: &AppState,
|
||||
request_context: &RequestContext,
|
||||
owner_user_id: &str,
|
||||
tracking_user_id: Option<&str>,
|
||||
client_marker: Option<TrackingClientMarker>,
|
||||
payload: ConfirmAssetObjectRequest,
|
||||
) -> Result<Json<Value>, AppError> {
|
||||
let oss_client = state.oss_client().ok_or_else(|| {
|
||||
@@ -509,8 +521,10 @@ pub(crate) async fn confirm_asset_object_for_owner(
|
||||
record_asset_tracking_event(
|
||||
state,
|
||||
request_context,
|
||||
tracking_user_id,
|
||||
owner_user_id,
|
||||
"asset_upload_confirm",
|
||||
client_marker,
|
||||
json!({
|
||||
"asset": {
|
||||
"operation": "asset_upload_confirm",
|
||||
@@ -542,6 +556,7 @@ pub async fn bind_asset_object_to_entity(
|
||||
State(state): State<AppState>,
|
||||
Extension(request_context): Extension<RequestContext>,
|
||||
Extension(authenticated): Extension<AuthenticatedAccessToken>,
|
||||
client_marker: Option<Extension<TrackingClientMarker>>,
|
||||
Json(payload): Json<BindAssetObjectRequest>,
|
||||
) -> Result<Json<Value>, AppError> {
|
||||
let now_micros = current_utc_micros();
|
||||
@@ -580,8 +595,10 @@ pub async fn bind_asset_object_to_entity(
|
||||
record_asset_tracking_event(
|
||||
&state,
|
||||
&request_context,
|
||||
Some(authenticated.claims().user_id()),
|
||||
authenticated.claims().user_id(),
|
||||
"asset_bind",
|
||||
client_marker.map(|Extension(marker)| marker),
|
||||
json!({
|
||||
"asset": {
|
||||
"operation": "asset_bind",
|
||||
@@ -608,15 +625,84 @@ pub async fn bind_asset_object_to_entity(
|
||||
async fn record_asset_tracking_event(
|
||||
state: &AppState,
|
||||
request_context: &RequestContext,
|
||||
user_id: &str,
|
||||
tracking_user_id: Option<&str>,
|
||||
owner_user_id: &str,
|
||||
event_key: &'static str,
|
||||
client_marker: Option<TrackingClientMarker>,
|
||||
metadata: Value,
|
||||
) {
|
||||
let mut draft = TrackingEventDraft::user(event_key, "asset", user_id);
|
||||
draft.metadata = metadata;
|
||||
let draft = build_asset_tracking_event_draft(
|
||||
request_context,
|
||||
tracking_user_id,
|
||||
owner_user_id,
|
||||
event_key,
|
||||
client_marker,
|
||||
metadata,
|
||||
);
|
||||
record_tracking_event_after_success(state, request_context, draft).await;
|
||||
}
|
||||
|
||||
fn build_asset_tracking_event_draft(
|
||||
request_context: &RequestContext,
|
||||
tracking_user_id: Option<&str>,
|
||||
owner_user_id: &str,
|
||||
event_key: &'static str,
|
||||
client_marker: Option<TrackingClientMarker>,
|
||||
metadata: Value,
|
||||
) -> TrackingEventDraft {
|
||||
let mut draft = TrackingEventDraft::new(event_key, "asset");
|
||||
draft.scope_kind = module_runtime::RuntimeTrackingScopeKind::User;
|
||||
draft.scope_id = owner_user_id.trim().to_string();
|
||||
draft.user_id = tracking_user_id.map(|user_id| user_id.trim().to_string());
|
||||
draft.owner_user_id = Some(owner_user_id.trim().to_string());
|
||||
draft.client_marker = client_marker;
|
||||
let metadata = apply_asset_tracking_client_marker(metadata, client_marker);
|
||||
draft.metadata = if client_marker.is_some() {
|
||||
apply_asset_tracking_route_metadata(metadata, request_context)
|
||||
} else {
|
||||
metadata
|
||||
};
|
||||
draft
|
||||
}
|
||||
|
||||
fn apply_asset_tracking_client_marker(
|
||||
mut metadata: Value,
|
||||
client_marker: Option<TrackingClientMarker>,
|
||||
) -> Value {
|
||||
if matches!(client_marker, Some(TrackingClientMarker::Agc))
|
||||
&& let Some(object) = metadata.as_object_mut()
|
||||
{
|
||||
object.insert("client".to_string(), json!("agc"));
|
||||
}
|
||||
metadata
|
||||
}
|
||||
|
||||
fn apply_asset_tracking_route_metadata(
|
||||
mut metadata: Value,
|
||||
request_context: &RequestContext,
|
||||
) -> Value {
|
||||
let Some(object) = metadata.as_object_mut() else {
|
||||
return metadata;
|
||||
};
|
||||
let Some((method, request_uri)) = request_context.operation().split_once(' ') else {
|
||||
return metadata;
|
||||
};
|
||||
let route = request_uri.split('?').next().unwrap_or(request_uri);
|
||||
object
|
||||
.entry("route".to_string())
|
||||
.or_insert_with(|| json!(route));
|
||||
object
|
||||
.entry("method".to_string())
|
||||
.or_insert_with(|| json!(method));
|
||||
object
|
||||
.entry("status".to_string())
|
||||
.or_insert_with(|| json!(200));
|
||||
object
|
||||
.entry("operation".to_string())
|
||||
.or_insert_with(|| json!(request_context.operation()));
|
||||
metadata
|
||||
}
|
||||
|
||||
fn resolve_asset_read_target(query: &GetReadUrlQuery) -> Result<AssetReadTarget, AppError> {
|
||||
if let Some(object_key) = query
|
||||
.object_key
|
||||
@@ -1023,10 +1109,69 @@ mod tests {
|
||||
use time::OffsetDateTime;
|
||||
use tower::ServiceExt;
|
||||
|
||||
use crate::tracking::TrackingClientMarker;
|
||||
use crate::{app::build_router, config::AppConfig, state::AppState};
|
||||
|
||||
type HmacSha256 = Hmac<Sha256>;
|
||||
|
||||
#[test]
|
||||
fn asset_tracking_metadata_receives_only_valid_agc_marker() {
|
||||
let request_context = crate::request_context::RequestContext::new(
|
||||
"request-225-asset".to_string(),
|
||||
"POST /api/external/v1/assets/objects/confirm?x=1".to_string(),
|
||||
std::time::Duration::ZERO,
|
||||
false,
|
||||
);
|
||||
let marked = super::apply_asset_tracking_client_marker(
|
||||
json!({"asset": {"operation": "asset_upload_confirm"}}),
|
||||
Some(TrackingClientMarker::Agc),
|
||||
);
|
||||
assert_eq!(marked["client"], "agc");
|
||||
assert_eq!(marked["asset"]["operation"], "asset_upload_confirm");
|
||||
|
||||
let marked = super::apply_asset_tracking_route_metadata(marked, &request_context);
|
||||
assert_eq!(marked["route"], "/api/external/v1/assets/objects/confirm");
|
||||
assert_eq!(marked["method"], "POST");
|
||||
assert_eq!(marked["status"], 200);
|
||||
assert_eq!(
|
||||
marked["operation"],
|
||||
"POST /api/external/v1/assets/objects/confirm?x=1"
|
||||
);
|
||||
|
||||
let unmarked = super::apply_asset_tracking_client_marker(
|
||||
json!({"asset": {"operation": "asset_upload_confirm"}}),
|
||||
None,
|
||||
);
|
||||
assert!(unmarked.get("client").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn external_asset_tracking_keeps_owner_without_forging_user() {
|
||||
let request_context = crate::request_context::RequestContext::new(
|
||||
"request-225-owner".to_string(),
|
||||
"POST /api/external/v1/assets/objects/confirm".to_string(),
|
||||
std::time::Duration::ZERO,
|
||||
false,
|
||||
);
|
||||
let draft = super::build_asset_tracking_event_draft(
|
||||
&request_context,
|
||||
None,
|
||||
"owner-225",
|
||||
"asset_upload_confirm",
|
||||
Some(TrackingClientMarker::Agc),
|
||||
json!({"asset": {"operation": "asset_upload_confirm"}}),
|
||||
);
|
||||
|
||||
assert_eq!(draft.user_id, None);
|
||||
assert_eq!(draft.owner_user_id.as_deref(), Some("owner-225"));
|
||||
assert_eq!(
|
||||
draft.scope_kind,
|
||||
module_runtime::RuntimeTrackingScopeKind::User
|
||||
);
|
||||
assert_eq!(draft.scope_id, "owner-225");
|
||||
assert_eq!(draft.metadata["client"], "agc");
|
||||
}
|
||||
|
||||
fn asset_object_record(
|
||||
access_policy: module_assets::AssetObjectAccessPolicy,
|
||||
owner_user_id: Option<&str>,
|
||||
|
||||
@@ -17,6 +17,7 @@ use crate::{
|
||||
http_error::AppError,
|
||||
request_context::RequestContext,
|
||||
state::AppState,
|
||||
tracking::TrackingClientMarker,
|
||||
};
|
||||
|
||||
const SCOPE_EDITOR_ASSET: &str = "editor:asset";
|
||||
@@ -26,6 +27,7 @@ pub async fn create_external_direct_upload_ticket(
|
||||
State(state): State<AppState>,
|
||||
Extension(request_context): Extension<RequestContext>,
|
||||
Extension(principal): Extension<ExternalApiPrincipal>,
|
||||
client_marker: Option<Extension<TrackingClientMarker>>,
|
||||
Json(payload): Json<CreateDirectUploadTicketRequest>,
|
||||
) -> Result<Json<Value>, AppError> {
|
||||
require_scope(&principal, SCOPE_EDITOR_ASSET)?;
|
||||
@@ -33,6 +35,8 @@ pub async fn create_external_direct_upload_ticket(
|
||||
&state,
|
||||
&request_context,
|
||||
principal.owner_user_id(),
|
||||
None,
|
||||
client_marker.map(|Extension(marker)| marker),
|
||||
payload,
|
||||
)
|
||||
.await
|
||||
@@ -42,12 +46,20 @@ pub async fn confirm_external_asset_object(
|
||||
State(state): State<AppState>,
|
||||
Extension(request_context): Extension<RequestContext>,
|
||||
Extension(principal): Extension<ExternalApiPrincipal>,
|
||||
client_marker: Option<Extension<TrackingClientMarker>>,
|
||||
Json(mut payload): Json<ConfirmAssetObjectRequest>,
|
||||
) -> Result<Json<Value>, AppError> {
|
||||
require_scope(&principal, SCOPE_EDITOR_ASSET)?;
|
||||
payload.owner_user_id = Some(principal.owner_user_id().to_string());
|
||||
confirm_asset_object_for_owner(&state, &request_context, principal.owner_user_id(), payload)
|
||||
.await
|
||||
confirm_asset_object_for_owner(
|
||||
&state,
|
||||
&request_context,
|
||||
principal.owner_user_id(),
|
||||
None,
|
||||
client_marker.map(|Extension(marker)| marker),
|
||||
payload,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_external_asset_read_url(
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user