From d1b2aea4a48d71349eb52c91f73c857256b663ad Mon Sep 17 00:00:00 2001 From: Linghong Date: Wed, 2 Sep 2026 13:47:06 +0000 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8DIssue225=E4=BB=85=E8=AE=B0?= =?UTF-8?q?=E5=BD=95AGC=E8=AF=B7=E6=B1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增AGC-only route tracking门禁 保持既有route和手工资产事件统计语义 清理TrackingEventDraft冗余marker状态 补充定向测试并同步Issue225文档 --- ...Issue225-AGC主站请求标记埋点统计-2026-09-02.md | 19 +- ...e225-AGC主站请求标记埋点分阶段验收-2026-09-02.md | 7 +- ...】Issue225阶段5定向测试与安全边界-2026-09-02.md | 14 +- ...】Issue225阶段6最终门禁与交接收口-2026-09-02.md | 4 +- server-rs/crates/api-server/src/assets.rs | 1 - server-rs/crates/api-server/src/tracking.rs | 258 +++++++++++++----- 6 files changed, 230 insertions(+), 73 deletions(-) diff --git a/local-docs/【实施方案】Issue225-AGC主站请求标记埋点统计-2026-09-02.md b/local-docs/【实施方案】Issue225-AGC主站请求标记埋点统计-2026-09-02.md index 5694c7db5..5339f6f62 100644 --- a/local-docs/【实施方案】Issue225-AGC主站请求标记埋点统计-2026-09-02.md +++ b/local-docs/【实施方案】Issue225-AGC主站请求标记埋点统计-2026-09-02.md @@ -10,7 +10,7 @@ ## 1. 一句话交付结果 -主站 `api-server` 能识别 AGC 发来的 `X-Genarrative-Client: agc`,并在现有成功路由埋点写入 `tracking_event.metadata_json` 的 `client: "agc"`,同时按真实登录用户或 External API Key 的 `owner_user_id` 归属,后台可以通过现有 tracking 事件查询看到这类请求;不改变鉴权、计费、幂等、响应和现有统计口径。 +主站 `api-server` 能识别 AGC 发来的 `X-Genarrative-Client: agc`,并在本期新增的 AGC 专用成功路由埋点写入 `tracking_event.metadata_json` 的 `client: "agc"`,同时按真实登录用户或 External API Key 的 `owner_user_id` 归属,后台可以通过现有 tracking 事件查询看到这类请求;既有 route tracking 和手工资产事件保持原有全客户端统计口径,不改变鉴权、计费、幂等和响应语义。 ## 2. Issue 边界 @@ -165,10 +165,13 @@ X-Genarrative-Client: agc - `app.rs` 在消费 request 之前解析 marker,并从 response extensions 取两类主体。 - `tracking.rs` 提供小型白名单解析函数和主体归属逻辑。 +- `RouteTrackingSpec` 明确区分既有全客户端 route 与本期新增的 AGC-only route;新增 AGC route 只有在 marker 有效时才允许落库。 - `build_route_tracking_metadata` 只在有效 marker 时追加 `client`,不重写既有字段。 - `record_route_tracking_event_after_success` 继续负责 route spec、outbox 和失败日志策略。 - 不把 marker 放进 `RequestContext`,除非阶段 1 证明同一请求的其他统一 tracking 入口确实需要它;避免扩大公共上下文结构。 +既有 route spec 默认保持全客户端语义;本期新增、仅为 AGC 调用清单补齐的 route spec 使用 AGC-only 策略。已有 `handled_by_existing_event` 的详细资产事件不改记录范围,只在有效 marker 时追加 `client`。 + ## 6. Route 覆盖范围 ### 6.1 必须覆盖的账号态路径 @@ -270,21 +273,28 @@ TrackingEventDraft.metadata - 请求体伪造 owner 或 marker 不改变主体归属。 - metadata 原有 route/method/status/operation 和资产嵌套字段仍存在。 -### 9.3 Route 覆盖 +### 9.3 AGC-only 记录门禁 + +- 本期新增的账号态和 External v1 AGC route:带有效 marker 的 2xx 响应才记录。 +- 同一批新增 route:缺失、非法或未知 marker 时不记录,也不拒绝业务请求。 +- 既有 route spec 和已有详细资产事件保持原有全客户端记录语义。 +- 4xx/5xx 即使带有效 marker 也不新增成功 route event。 + +### 9.4 Route 覆盖 - 当前 AGC 调用清单中的账号态路径全部能解析到 route spec。 - 当前 AGC 使用的 External v1 业务路径全部能解析到 route spec。 - `/api/external/v1` 发现、Skill 和 MCP 路径不被普通业务 spec 误收录。 - dynamic project/asset/operation ID 仍按现有 normalize 规则归一化。 -### 9.4 持久化与后台读取 +### 9.5 持久化与后台读取 - 构造 tracking event input 后,`metadata_json` 是合法 JSON object 且含 `client`。 - outbox 入队/回退直写路径不丢失 `client`。 - tracking_event 读回及后台 tracking API 解析不丢失 metadata。 - 现有 daily stat、event id 幂等和失败不阻断语义保持不变。 -### 9.5 状态码语义 +### 9.6 状态码语义 至少保留一组回归: @@ -311,6 +321,7 @@ TrackingEventDraft.metadata - 已有 route、method、status、operation、资产 metadata 和 event key 语义不丢失。 - 登录账号态按真实用户归属,External API Key 态按 `owner_user_id` 归属。 - 当前 AGC 实际使用的账号态和 External v1 业务路径均有 tracking spec。 +- 本期新增的 AGC route spec 未携带有效 marker 时不产生 route tracking event;既有 route spec 和手工资产事件保持原有全客户端统计语义。 - External v1 discovery/Skill/MCP、OSS、签名下载、Provider、搜索、loopback、更新下载不被误纳入普通 AGC 业务统计。 - outbox、SpacetimeDB 写入、daily stat、幂等和失败不阻断语义无回归。 - 后台现有 tracking 查询可以看到 `metadata_json.client`,无需新增 schema 或页面。 diff --git a/local-docs/【实施计划】Issue225-AGC主站请求标记埋点分阶段验收-2026-09-02.md b/local-docs/【实施计划】Issue225-AGC主站请求标记埋点分阶段验收-2026-09-02.md index e6484b71f..431a4fe4c 100644 --- a/local-docs/【实施计划】Issue225-AGC主站请求标记埋点分阶段验收-2026-09-02.md +++ b/local-docs/【实施计划】Issue225-AGC主站请求标记埋点分阶段验收-2026-09-02.md @@ -171,6 +171,8 @@ X-Genarrative-Client: agc - 生成提交、轮询、项目读取/资源登记、素材库、上传凭证、对象确认、换签读取等当前已确认调用。 - 每个新增 spec 固定 event key、module key、scope kind 和动态路径归一化方式。 +本阶段新增的 route spec 统一使用 AGC-only 记录策略:只有带有效 marker 的 2xx 请求才写入;既有 route spec 保持原有全客户端语义,上传票据/对象确认等 `handled_by_existing_event` 路径继续由手工资产事件负责。 + 同时明确不加入: - `/api/external/v1/openapi.json`。 @@ -185,6 +187,7 @@ X-Genarrative-Client: agc - 发现/MCP 路径不会误进入普通业务 route tracking。 - project/asset/operation 动态 ID 仍按现有规则归一化。 - 原有 route spec 的 event key、scope 和统计口径不变。 +- 新增 AGC route 在未携带有效 marker 时不产生 route tracking event,且不影响业务响应。 ### 7.3 阶段完成判据 @@ -289,8 +292,9 @@ CI 已覆盖且与本改动无直接关系的全量测试可交给 CI;本阶 ### 9.4 阶段 5 实现与验证记录 -- `tracking.rs` 抽出 `should_record_route_tracking` 判定,明确只有 2xx 成功响应且不是已有详细资产事件的路由才进入统一成功 route tracking;4xx/5xx 和上传确认等复用事件均不新增重复成功事件。 +- `tracking.rs` 抽出 `should_record_route_tracking` 判定,明确新增 AGC-only route 只有在带有效 marker 的 2xx 响应下才进入统一成功 route tracking;既有全客户端 route 保持原语义,4xx/5xx 和上传确认等复用事件均不新增重复成功事件。 - 新增 2xx、4xx/5xx 状态矩阵测试,覆盖 `OK / CREATED / ACCEPTED / NO_CONTENT` 与认证、权限、客户端、服务端失败状态。 +- 新增 AGC-only route 正反向门禁测试:有效 marker 记录,缺失或无效 marker 不记录;既有 route 和手工资产事件策略保持不变。 - 新增 `app.rs` 路由回归:带有效 `X-Genarrative-Client: agc` 的未认证业务请求仍返回 `401`,marker 不绕过鉴权,也不改变失败状态。 - 新增 route metadata 安全边界测试,确认统一 route metadata 只包含既有 route/method/status/operation 和可选 `client`/资产字段,不出现 authorization、token、API Key、Cookie、签名 URL 或 request body 字段。 - 阶段 4 的账号态、External API Key、outbox、后台 readback 和 mapper 测试全部复跑;SpacetimeDB 既有 event-id 幂等回归也通过。未修改鉴权、请求体、异步轮询、event id、daily stat 或失败回退实现。 @@ -367,6 +371,7 @@ cargo test --locked --manifest-path server-rs/Cargo.toml -p spacetime-module dup ### 10.5 阶段 6 交接结论 - 主站只对白名单值 `X-Genarrative-Client: agc` 追加 `metadata_json.client = "agc"`;缺失、空值、`AGC` 和未知值按未标记处理,不拒绝请求。 +- 本期新增的 AGC route spec 只有在带有效 marker 的 2xx 响应下才记录;既有 route spec 与手工资产事件保持原有全客户端统计语义。 - 账号态沿用真实 access token 用户;External API Key 态使用 `ExternalApiPrincipal.owner_user_id()`,不伪造 `user_id`。 - 当前 AGC 实际账号态和 External v1 业务路径均有显式 route spec;动态 ID 继续归一化,External v1 保留实际外部 path。 - OSS、签名下载、Provider、受控搜索、loopback、更新下载、公开 discovery/Skill/MCP 不进入普通 AGC 业务 route tracking。 diff --git a/local-docs/【阶段验收】Issue225阶段5定向测试与安全边界-2026-09-02.md b/local-docs/【阶段验收】Issue225阶段5定向测试与安全边界-2026-09-02.md index ce603b55f..a2e7d1003 100644 --- a/local-docs/【阶段验收】Issue225阶段5定向测试与安全边界-2026-09-02.md +++ b/local-docs/【阶段验收】Issue225阶段5定向测试与安全边界-2026-09-02.md @@ -17,7 +17,15 @@ - `400 / 401 / 403 / 404 / 500 / 502` 即使带 AGC marker,也不会新增成功 route event; - 已由详细资产事件处理的上传票据和对象确认不重复生成统一 route event。 -### 2.2 鉴权边界 +### 2.2 AGC-only 路由门禁 + +本期新增、仅为 AGC 调用清单补齐的账号态和 External v1 route spec 均要求有效 `X-Genarrative-Client: agc`: + +- 带有效 marker 的 2xx 响应才写入 route tracking; +- 缺失、非法、未知或大小写不符合契约的 marker 不写入这些新增 route event,也不拒绝业务请求; +- 既有 route spec 和已有详细资产事件保持原有全客户端记录语义。 + +### 2.3 鉴权边界 `app.rs` 通过真实 router 发起未认证的: @@ -28,7 +36,7 @@ X-Genarrative-Client: agc 响应仍为 `401 Unauthorized`。marker 不会获得权限,也不会改变失败状态。 -### 2.3 metadata 安全边界 +### 2.4 metadata 安全边界 route metadata 测试确认不会写入: @@ -45,7 +53,7 @@ requestBody 有效 marker 只产生固定的 `client: "agc"`,并保留既有 route/method/status/operation 与资产嵌套字段。 -### 2.4 既有链路复回归 +### 2.5 既有链路复回归 阶段 4 的以下路径在阶段 5 再次顺序执行: diff --git a/local-docs/【阶段验收】Issue225阶段6最终门禁与交接收口-2026-09-02.md b/local-docs/【阶段验收】Issue225阶段6最终门禁与交接收口-2026-09-02.md index 2b586b3be..5521e9da7 100644 --- a/local-docs/【阶段验收】Issue225阶段6最终门禁与交接收口-2026-09-02.md +++ b/local-docs/【阶段验收】Issue225阶段6最终门禁与交接收口-2026-09-02.md @@ -73,6 +73,8 @@ X-Genarrative-Client: agc 不写入 Header 原文、token、API Key、Cookie、签名 URL、请求体或项目绝对路径。 +本期新增的 AGC route spec 只有在请求带有效 marker 时才记录;既有 route spec 与手工资产事件沿用原有全客户端记录策略。 + ### 3.2 主体归属 - 登录账号态:使用已验证 access token 的真实用户,保留既有 `user_id`、`owner_user_id` 和 scope 语义。 @@ -137,7 +139,7 @@ worker 使用既有 `record_tracking_events_and_return` 批量 procedure 写入 ## 6. 可直接粘贴到 Issue #225 的交付评论 -> `#225` 主站侧已完成并收口:统一识别 `X-Genarrative-Client: agc`,并在现有成功 route tracking 的 `tracking_event.metadata_json` 中追加 `client: "agc"`。账号态按已验证 access token 的真实用户归属,External API Key 态按 `ExternalApiPrincipal.owner_user_id()` 归属,不伪造 `user_id`,不记录 token/API Key/Cookie/签名 URL。当前 AGC 实际使用的账号态 `/api/auth/*`、`/api/profile/*`、`/api/assets/*`、`/api/editor/*`、`/api/runtime/*` 与 External v1 业务路径均有显式 route spec;动态 ID 继续归一化,External v1 保留实际外部 path。OSS/签名下载、Provider、受控搜索、loopback、更新下载、公开 discovery/Skill/MCP 不进入普通 AGC 业务统计。记录最终落在现有 `tracking_event.metadata_json.client`,复用既有 outbox、SpacetimeDB procedure、daily stat、event-id 幂等和后台 `GET /admin/api/tracking/events` 读取,不新增 schema、页面或独立事件体系。阶段 1~5 的定向实现和回归已提交,阶段 6 最终门禁通过:api-server tracking/资产/后台/outbox 43 个、鉴权回归 1 个、module-runtime 2 个、spacetime-client 1 个、spacetime-module 1 个定向测试全部通过,Rust 编译/格式、编码和 diff 检查通过;完整工作区测试按 CI 执行。`#226` 客户端标记、origin-safe redirect 和第三方边界契约无需回改。` +> `#225` 主站侧已完成并收口:统一识别 `X-Genarrative-Client: agc`,本期新增的 AGC-only 成功 route 只有在带有效 marker 时才写入 `tracking_event.metadata_json.client = "agc"`;既有 route tracking 和手工资产事件保持原有全客户端统计语义。账号态按已验证 access token 的真实用户归属,External API Key 态按 `ExternalApiPrincipal.owner_user_id()` 归属,不伪造 `user_id`,不记录 token/API Key/Cookie/签名 URL。当前 AGC 实际使用的账号态 `/api/auth/*`、`/api/profile/*`、`/api/assets/*`、`/api/editor/*`、`/api/runtime/*` 与 External v1 业务路径均有显式 route spec;动态 ID 继续归一化,External v1 保留实际外部 path。OSS/签名下载、Provider、受控搜索、loopback、更新下载、公开 discovery/Skill/MCP 不进入普通 AGC 业务统计。记录最终复用既有 outbox、SpacetimeDB procedure、daily stat、event-id 幂等和后台 `GET /admin/api/tracking/events` 读取,不新增 schema、页面或独立事件体系。阶段 1~5 的定向实现和回归已提交,阶段 6 最终门禁通过:api-server tracking/资产/后台/outbox 43 个、鉴权回归 1 个、module-runtime 2 个、spacetime-client 1 个、spacetime-module 1 个定向测试全部通过,Rust 编译/格式、编码和 diff 检查通过;完整工作区测试按 CI 执行。`#226` 客户端标记、origin-safe redirect 和第三方边界契约无需回改。` ## 7. 后续发布前事项 diff --git a/server-rs/crates/api-server/src/assets.rs b/server-rs/crates/api-server/src/assets.rs index bc79e7935..e4e7f035e 100644 --- a/server-rs/crates/api-server/src/assets.rs +++ b/server-rs/crates/api-server/src/assets.rs @@ -655,7 +655,6 @@ fn build_asset_tracking_event_draft( 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) diff --git a/server-rs/crates/api-server/src/tracking.rs b/server-rs/crates/api-server/src/tracking.rs index c6bbf7789..53907f157 100644 --- a/server-rs/crates/api-server/src/tracking.rs +++ b/server-rs/crates/api-server/src/tracking.rs @@ -38,7 +38,6 @@ pub struct TrackingEventDraft { pub profile_id: Option, pub module_key: Option<&'static str>, pub metadata: Value, - pub(crate) client_marker: Option, } #[derive(Clone, Debug, Default, Eq, PartialEq)] @@ -58,7 +57,6 @@ impl TrackingEventDraft { profile_id: None, module_key: Some(module_key), metadata: json!({}), - client_marker: None, } } @@ -80,6 +78,7 @@ struct RouteTrackingSpec { scope_kind: RuntimeTrackingScopeKind, scope_id: &'static str, handled_by_existing_event: bool, + requires_agc_marker: bool, } pub async fn record_external_generation_run_after_success( @@ -144,7 +143,7 @@ pub async fn record_route_tracking_event_after_success( let Some(spec) = resolve_route_tracking_spec(method, path) else { return; }; - if !should_record_route_tracking(status, &spec) { + if !should_record_route_tracking(status, &spec, client_marker) { return; } @@ -155,21 +154,20 @@ pub async fn record_route_tracking_event_after_success( draft.scope_id = scope_id; draft.user_id = identity.user_id; draft.owner_user_id = identity.owner_user_id; - draft.client_marker = client_marker; - draft.metadata = build_route_tracking_metadata( - &spec, - request_context, - method, - path, - status, - draft.client_marker, - ); + draft.metadata = + build_route_tracking_metadata(&spec, request_context, method, path, status, client_marker); record_route_tracking_event_via_outbox_after_success(state, request_context, draft).await; } -fn should_record_route_tracking(status: StatusCode, spec: &RouteTrackingSpec) -> bool { - status.is_success() && !spec.handled_by_existing_event +fn should_record_route_tracking( + status: StatusCode, + spec: &RouteTrackingSpec, + client_marker: Option, +) -> bool { + status.is_success() + && !spec.handled_by_existing_event + && (!spec.requires_agc_marker || matches!(client_marker, Some(TrackingClientMarker::Agc))) } fn resolve_tracking_identity( @@ -226,7 +224,7 @@ fn resolve_route_tracking_spec(method: &Method, path: &str) -> Option Some(route_spec( + ("POST", "/api/auth/entry") => Some(agc_route_spec( "auth_password_login_success", "auth", User, @@ -264,7 +262,7 @@ fn resolve_route_tracking_spec(method: &Method, path: &str) -> Option Some(route_spec( + ("POST", "/api/profile/api-keys") => Some(agc_route_spec( "profile_api_key_create", "profile", User, @@ -288,7 +286,7 @@ fn resolve_route_tracking_spec(method: &Method, path: &str) -> Option Some(route_spec( + ("POST", "/api/profile/recharge/orders/{id}/wechat/confirm") => Some(agc_route_spec( "recharge_order_wechat_confirm", "profile", User, @@ -412,13 +410,13 @@ fn resolve_route_tracking_spec(method: &Method, path: &str) -> Option { Some(manual_asset_route_spec("asset_upload_confirm")) } - ("GET", "/api/assets/read-url") => Some(route_spec( + ("GET", "/api/assets/read-url") => Some(agc_route_spec( "asset_read_url_view", "asset", User, "anonymous", )), - ("GET", "/api/assets/read-bytes") => Some(route_spec( + ("GET", "/api/assets/read-bytes") => Some(agc_route_spec( "asset_read_bytes_view", "asset", User, @@ -483,85 +481,85 @@ fn resolve_route_tracking_spec(method: &Method, path: &str) -> Option Some(route_spec( + ("GET", "/api/editor/projects") => Some(agc_route_spec( "editor_projects_view", "editor", User, "anonymous", )), - ("POST", "/api/editor/projects") => Some(route_spec( + ("POST", "/api/editor/projects") => Some(agc_route_spec( "editor_project_create", "editor", User, "anonymous", )), - ("GET", "/api/editor/projects/{id}") => Some(route_spec( + ("GET", "/api/editor/projects/{id}") => Some(agc_route_spec( "editor_project_view", "editor", User, "anonymous", )), - ("POST", "/api/editor/projects/{id}/resources") => Some(route_spec( + ("POST", "/api/editor/projects/{id}/resources") => Some(agc_route_spec( "editor_project_resource_create", "editor", User, "anonymous", )), - ("GET", "/api/editor/assets/library") => Some(route_spec( + ("GET", "/api/editor/assets/library") => Some(agc_route_spec( "editor_asset_library_view", "editor", User, "anonymous", )), - ("POST", "/api/editor/assets/folders") => Some(route_spec( + ("POST", "/api/editor/assets/folders") => Some(agc_route_spec( "editor_asset_folder_create", "editor", User, "anonymous", )), - ("POST", "/api/editor/images/generations") => Some(route_spec( + ("POST", "/api/editor/images/generations") => Some(agc_route_spec( "editor_image_generation_submit", "editor", User, "anonymous", )), - ("POST", "/api/editor/images/edits") => Some(route_spec( + ("POST", "/api/editor/images/edits") => Some(agc_route_spec( "editor_image_edit_submit", "editor", User, "anonymous", )), - ("POST", "/api/editor/images/background-removals") => Some(route_spec( + ("POST", "/api/editor/images/background-removals") => Some(agc_route_spec( "editor_image_background_removal_submit", "editor", User, "anonymous", )), - ("POST", "/api/editor/icon-spritesheets/generations") => Some(route_spec( + ("POST", "/api/editor/icon-spritesheets/generations") => Some(agc_route_spec( "editor_icon_spritesheet_generation_submit", "editor", User, "anonymous", )), - ("POST", "/api/editor/character-animations/generations") => Some(route_spec( + ("POST", "/api/editor/character-animations/generations") => Some(agc_route_spec( "editor_character_animation_generation_submit", "editor", User, "anonymous", )), - ("POST", "/api/editor/videos/generations") => Some(route_spec( + ("POST", "/api/editor/videos/generations") => Some(agc_route_spec( "editor_video_generation_submit", "editor", User, "anonymous", )), - ("POST", "/api/editor/audios/sound-effects/generations") => Some(route_spec( + ("POST", "/api/editor/audios/sound-effects/generations") => Some(agc_route_spec( "editor_sound_effect_generation_submit", "editor", User, "anonymous", )), - ("POST", "/api/editor/audios/background-music/generations") => Some(route_spec( + ("POST", "/api/editor/audios/background-music/generations") => Some(agc_route_spec( "editor_background_music_generation_submit", "editor", User, @@ -600,7 +598,7 @@ fn resolve_route_tracking_spec(method: &Method, path: &str) -> Option Some(route_spec( + ("GET", "/api/runtime/external-generation/jobs/{id}") => Some(agc_route_spec( "external_generation_job_view", "runtime", User, @@ -612,99 +610,103 @@ fn resolve_route_tracking_spec(method: &Method, path: &str) -> Option { Some(manual_asset_route_spec("asset_upload_confirm")) } - ("GET", "/api/external/v1/assets/read-url") => Some(route_spec( + ("GET", "/api/external/v1/assets/read-url") => Some(agc_route_spec( "asset_read_url_view", "asset", User, "anonymous", )), - ("GET", "/api/external/v1/editor/projects") => Some(route_spec( + ("GET", "/api/external/v1/editor/projects") => Some(agc_route_spec( "editor_projects_view", "editor", User, "anonymous", )), - ("POST", "/api/external/v1/editor/projects") => Some(route_spec( + ("POST", "/api/external/v1/editor/projects") => Some(agc_route_spec( "editor_project_create", "editor", User, "anonymous", )), - ("GET", "/api/external/v1/editor/projects/{id}") => Some(route_spec( + ("GET", "/api/external/v1/editor/projects/{id}") => Some(agc_route_spec( "editor_project_view", "editor", User, "anonymous", )), - ("POST", "/api/external/v1/editor/projects/{id}/resources") => Some(route_spec( + ("POST", "/api/external/v1/editor/projects/{id}/resources") => Some(agc_route_spec( "editor_project_resource_create", "editor", User, "anonymous", )), - ("GET", "/api/external/v1/editor/assets/library") => Some(route_spec( + ("GET", "/api/external/v1/editor/assets/library") => Some(agc_route_spec( "editor_asset_library_view", "editor", User, "anonymous", )), - ("POST", "/api/external/v1/editor/assets/folders") => Some(route_spec( + ("POST", "/api/external/v1/editor/assets/folders") => Some(agc_route_spec( "editor_asset_folder_create", "editor", User, "anonymous", )), - ("POST", "/api/external/v1/editor/images/generations") => Some(route_spec( + ("POST", "/api/external/v1/editor/images/generations") => Some(agc_route_spec( "editor_image_generation_submit", "editor", User, "anonymous", )), - ("POST", "/api/external/v1/editor/images/edits") => Some(route_spec( + ("POST", "/api/external/v1/editor/images/edits") => Some(agc_route_spec( "editor_image_edit_submit", "editor", User, "anonymous", )), - ("POST", "/api/external/v1/editor/images/background-removals") => Some(route_spec( + ("POST", "/api/external/v1/editor/images/background-removals") => Some(agc_route_spec( "editor_image_background_removal_submit", "editor", User, "anonymous", )), - ("POST", "/api/external/v1/editor/icon-spritesheets/generations") => Some(route_spec( + ("POST", "/api/external/v1/editor/icon-spritesheets/generations") => Some(agc_route_spec( "editor_icon_spritesheet_generation_submit", "editor", User, "anonymous", )), - ("POST", "/api/external/v1/editor/character-animations/generations") => Some(route_spec( - "editor_character_animation_generation_submit", - "editor", - User, - "anonymous", - )), - ("POST", "/api/external/v1/editor/videos/generations") => Some(route_spec( + ("POST", "/api/external/v1/editor/character-animations/generations") => { + Some(agc_route_spec( + "editor_character_animation_generation_submit", + "editor", + User, + "anonymous", + )) + } + ("POST", "/api/external/v1/editor/videos/generations") => Some(agc_route_spec( "editor_video_generation_submit", "editor", User, "anonymous", )), - ("POST", "/api/external/v1/editor/audios/sound-effects/generations") => Some(route_spec( - "editor_sound_effect_generation_submit", - "editor", - User, - "anonymous", - )), + ("POST", "/api/external/v1/editor/audios/sound-effects/generations") => { + Some(agc_route_spec( + "editor_sound_effect_generation_submit", + "editor", + User, + "anonymous", + )) + } ("POST", "/api/external/v1/editor/audios/background-music/generations") => { - Some(route_spec( + Some(agc_route_spec( "editor_background_music_generation_submit", "editor", User, "anonymous", )) } - ("GET", "/api/external/v1/generations/{id}") => Some(route_spec( + ("GET", "/api/external/v1/generations/{id}") => Some(agc_route_spec( "external_generation_job_view", "runtime", User, @@ -783,6 +785,19 @@ fn route_spec( scope_kind, scope_id, handled_by_existing_event: false, + requires_agc_marker: false, + } +} + +fn agc_route_spec( + event_key: &'static str, + module_key: &'static str, + scope_kind: RuntimeTrackingScopeKind, + scope_id: &'static str, +) -> RouteTrackingSpec { + RouteTrackingSpec { + requires_agc_marker: true, + ..route_spec(event_key, module_key, scope_kind, scope_id) } } @@ -1440,13 +1455,14 @@ mod tests { axum::http::StatusCode::NO_CONTENT, ] { assert!( - should_record_route_tracking(status, &business_spec), + should_record_route_tracking(status, &business_spec, None), "{status} should record a successful business route" ); } assert!(!should_record_route_tracking( axum::http::StatusCode::OK, - &handled_spec + &handled_spec, + Some(TrackingClientMarker::Agc), )); } @@ -1457,6 +1473,22 @@ mod tests { "/api/external/v1/editor/images/generations", ) .expect("External v1 business route should resolve"); + assert!( + !should_record_route_tracking(axum::http::StatusCode::ACCEPTED, &business_spec, None,), + "an AGC-only route must not record an unmarked request" + ); + assert!(should_record_route_tracking( + axum::http::StatusCode::ACCEPTED, + &business_spec, + Some(TrackingClientMarker::Agc), + )); + let existing_spec = resolve_route_tracking_spec(&Method::GET, "/api/auth/me") + .expect("existing auth route should resolve"); + assert!(should_record_route_tracking( + axum::http::StatusCode::OK, + &existing_spec, + None, + )); for status in [ axum::http::StatusCode::BAD_REQUEST, axum::http::StatusCode::UNAUTHORIZED, @@ -1466,7 +1498,11 @@ mod tests { axum::http::StatusCode::BAD_GATEWAY, ] { assert!( - !should_record_route_tracking(status, &business_spec), + !should_record_route_tracking( + status, + &business_spec, + Some(TrackingClientMarker::Agc), + ), "{status} must not create a successful route event" ); } @@ -1807,6 +1843,102 @@ mod tests { ); } + #[test] + fn newly_added_agc_routes_require_the_agc_marker() { + for (method, path) in [ + (Method::POST, "/api/auth/entry"), + (Method::POST, "/api/profile/api-keys"), + ( + Method::POST, + "/api/profile/recharge/orders/order-225/wechat/confirm", + ), + (Method::GET, "/api/assets/read-url"), + (Method::GET, "/api/assets/read-bytes"), + (Method::GET, "/api/editor/projects"), + (Method::POST, "/api/editor/projects"), + (Method::GET, "/api/editor/projects/project-225"), + (Method::POST, "/api/editor/projects/project-225/resources"), + (Method::GET, "/api/editor/assets/library"), + (Method::POST, "/api/editor/assets/folders"), + (Method::POST, "/api/editor/images/generations"), + (Method::POST, "/api/editor/images/edits"), + (Method::POST, "/api/editor/images/background-removals"), + (Method::POST, "/api/editor/icon-spritesheets/generations"), + (Method::POST, "/api/editor/character-animations/generations"), + (Method::POST, "/api/editor/videos/generations"), + (Method::POST, "/api/editor/audios/sound-effects/generations"), + ( + Method::POST, + "/api/editor/audios/background-music/generations", + ), + (Method::GET, "/api/runtime/external-generation/jobs/job-225"), + (Method::GET, "/api/external/v1/assets/read-url"), + (Method::GET, "/api/external/v1/editor/projects"), + (Method::POST, "/api/external/v1/editor/projects"), + (Method::GET, "/api/external/v1/editor/projects/project-225"), + ( + Method::POST, + "/api/external/v1/editor/projects/project-225/resources", + ), + (Method::GET, "/api/external/v1/editor/assets/library"), + (Method::POST, "/api/external/v1/editor/assets/folders"), + (Method::POST, "/api/external/v1/editor/images/generations"), + (Method::POST, "/api/external/v1/editor/images/edits"), + ( + Method::POST, + "/api/external/v1/editor/images/background-removals", + ), + ( + Method::POST, + "/api/external/v1/editor/icon-spritesheets/generations", + ), + ( + Method::POST, + "/api/external/v1/editor/character-animations/generations", + ), + (Method::POST, "/api/external/v1/editor/videos/generations"), + ( + Method::POST, + "/api/external/v1/editor/audios/sound-effects/generations", + ), + ( + Method::POST, + "/api/external/v1/editor/audios/background-music/generations", + ), + (Method::GET, "/api/external/v1/generations/task-225"), + ] { + let spec = resolve_route_tracking_spec(&method, path) + .unwrap_or_else(|| panic!("missing AGC route tracking spec: {method} {path}")); + assert!( + spec.requires_agc_marker, + "{method} {path} must require the AGC marker" + ); + } + + for (method, path) in [ + (Method::GET, "/api/auth/me"), + (Method::POST, "/api/auth/phone/login"), + (Method::POST, "/api/auth/logout"), + (Method::GET, "/api/profile/dashboard"), + (Method::POST, "/api/auth/refresh"), + (Method::POST, "/api/auth/phone/send-code"), + (Method::POST, "/api/assets/direct-upload-tickets"), + (Method::POST, "/api/assets/objects/confirm"), + ( + Method::POST, + "/api/external/v1/assets/direct-upload-tickets", + ), + (Method::POST, "/api/external/v1/assets/objects/confirm"), + ] { + let spec = resolve_route_tracking_spec(&method, path) + .unwrap_or_else(|| panic!("missing existing route tracking spec: {method} {path}")); + assert!( + !spec.requires_agc_marker, + "{method} {path} must preserve its existing tracking policy" + ); + } + } + #[test] fn agc_external_routes_keep_external_paths_and_specs() { use module_runtime::RuntimeTrackingScopeKind::User;