# 24 · 事件白名单 v2 扩充(T2-17 后端半边) > 依据:`06-experiment-tracking-plan.md` §1.4/§1.5(事件字典 v2 增量)、§5.2(page_viewed 转正稿)、§6.4(值级巡检);ADR-013(health_record_action 移除,dev@58576f8) > > 交付:`patbond-api` dev@`64c9b72`(`patbond-user` 模块 analytics 包,3 文件,+216/−9) ## 1. 结论速览 | 项 | 结果 | | --- | --- | | 新增白名单事件 | 10 个(pet 域 3 + health_record 域 7),props 键集与 06 号 §1.5 可直抄块逐条一致 | | page_viewed 转正核对 | **一致,零修正**:现行白名单已是 `Set.of("pageName", "referrer")`,与 v2 正稿键集相同;仅更新注释标注正稿地位与 pageName 枚举(含 §1.6 修订的 `pet_form`) | | health_record_action | 保持移除(ADR-013),新增集成测试锁定其仍被 `unknown_event_name` 拒绝 | | 测试数 | 182 → **191**(+9:字典边界 5 + 接收端集成 4),`mvnw clean test` 全绿 | | 契约变更 | **无需**:`openapi.yaml` 的 events 契约对事件名开放(字符串 + 后端字典校验),本次未触碰 | ## 2. 新增事件与 props 对照(vs 06 号 §1.4/§1.5) `EventDictionary.java`(`patbond-user/src/main/java/com/patbond/patbond/user/analytics/`)`WHITELIST` 增量,逐条对照字典 v2: ### 2.1 pet 域(3 事件) | 事件名 | 白名单 props | 与 06 号 §1.5 | | --- | --- | --- | | `pet_create_started` | `entryPoint` | 一致 | | `pet_create_succeeded` | `durationMs`、`species`、`petIndex` | 一致 | | `pet_create_failed` | `failureReason`、`errorCode`、`httpStatus`、`attemptSeq` | 一致 | ### 2.2 health_record 域(7 事件) | 事件名 | 白名单 props | 与 06 号 §1.5 | | --- | --- | --- | | `health_record_create_started` | `recordType`、`entryPoint` | 一致 | | `health_record_create_succeeded` | `recordType`、`durationMs`、`photoCount` | 一致 | | `health_record_create_failed` | `recordType`、`failureReason`、`errorCode`、`httpStatus`、`attemptSeq` | 一致 | | `health_record_viewed` | `recordType`、`source` | 一致 | | `health_record_edit_succeeded` | `recordType`、`fieldCount` | 一致 | | `health_record_edit_failed` | `recordType`、`failureReason`、`errorCode`、`httpStatus`(无 `attemptSeq`,正稿如此) | 一致 | | `health_record_deleted` | `recordType` | 一致 | ### 2.3 page_viewed 转正核对 现行条目 `Map.entry("page_viewed", Set.of("pageName", "referrer"))` 与 v2 正稿(§5.2)键集**完全一致,无需修正**。差异只在语义层:v2 要求 pageName 为编译期枚举(`login/register/home/profile/pet_list/pet_detail/pet_form/record_form/record_detail`)——这是客户端约束(T2-17 Flutter 半边)+ §6.4 值级巡检的职责,后端键级白名单结构不承载值枚举(见 §3)。已将枚举全集写入 `EventDictionary` 类注释作字典说明。 ## 3. 枚举值的校验边界(设计决策,沿用现行架构) 当前 `EventDictionary` 是**键级白名单**(白名单外键剥离、红线键拒绝、未知事件名拒绝),不做值级枚举校验。v2 的 `recordType`(`weight/vaccine/health_event/reminder`)、失败枚举(含 `permission_denied/conflict/not_found`)、`pageName` 枚举维持同一分层: 1. **客户端编译期枚举**是第一道约束(06 号 §5.2 明确 pageName 为「编译期枚举」;recordType 同理); 2. **接收端只校验键**——枚举外的值(如 `recordType: "grooming"`)**过 ingest 不拒绝**,由 §6.4 值级巡检 SQL 兜底发现。06 号 §1.5 的「可直抄」Java 块本身就是纯键集,本实现与其逐字一致,未擅自加严接收契约(加严会使客户端枚举漂移时整条事件丢失,与 §5.2 第 4 条「宁可不上报、不要报错名」的防洪水思路相悖)。 此边界已用集成测试 `enumOutRecordTypeValuePassesIngestForOfflinePatrol` 显式锁定为文档化行为,避免后人误当漏洞「修复」。 ## 4. 测试增量(182 → 191,全绿) `JAVA_HOME=/usr/lib/jvm/java-17-openjdk ./mvnw clean test`:总计 191,failures 0,errors 0。 **`EventDictionaryTest`(3 → 8,+5)**: | 测试 | 边界 | | --- | --- | | `v2PetDomainEventsMatchDictionary` | pet 域 3 事件 props 键集 `containsExactlyInAnyOrder` 全矩阵 | | `v2HealthRecordCreateFunnelMatchesDictionary` | 创建漏斗 3 事件键集全矩阵 | | `v2HealthRecordLifecycleEventsMatchDictionary` | viewed/edit/deleted 4 事件键集(含锁定 edit_failed 无 attemptSeq) | | `pageViewedFormalizedPropsAreExactlyPageNameAndReferrer` | 正稿键集恰为 pageName+referrer | | `deliberatelyAbsentEventsStayUnknown` | §1.4 刻意不设的 `pet_viewed`/`health_record_edit_started`/`health_record_delete_failed` 保持 unknown | **`AnalyticsIntegrationTest`(7 → 11,+4)**: | 测试 | 边界 | | --- | --- | | `acceptsV2HealthRecordFunnelEvent` | v2 事件(合法 recordType)端到端 accepted 且落库 | | `stripsPropsOutsideV2Whitelist` | v2 事件白名单外键(内容型 `recordTitle`)被剥离,`recordType` 保留 | | `enumOutRecordTypeValuePassesIngestForOfflinePatrol` | 枚举外 recordType 值过 ingest(§3 决策的锁定) | | `retiredHealthRecordActionStaysRejected` | 废弃事件带 v2 同名 props 上报仍整条 rejected(`unknown_event_name`) | ## 5. 未尽事项 - `entryPoint` 枚举(`profile_empty_state/pet_list/post_register_guide` 等)06 号标注「待 UI 定稿收敛」——键已入白名单,枚举收敛属 Flutter 半边与 UI 定稿,后端无阻塞。 - `pet_create_failed.failureReason` 的 `pet_limit_reached` 待拍板(无上限则删)——纯值级枚举,不影响本次键级白名单。 - T2-17 Flutter 半边(细分事件挂接、pageName 编译期枚举、RouteObserver)不在本工单范围。