60258324e4
CI / docs-build (push) Successful in 2m3s
- 09 契约补录 events(关闭 D-1/放行条件②,另含实现与旧规范 5 处出入记录) - 10 Flutter 埋点修复(接线+三偏差+SessionTracker+page_viewed,34→51 测试,12/12 验收) - 11 后端地基(V3/V4 迁移 8 表+种子、4 条跨 schema FK 剥离、patbond-pet 骨架、ADR-013 执行,82→95 测试) - mkdocs build --strict 通过 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
4.4 KiB
4.4 KiB
09 · POST /api/v1/events 契约补录(D-1 关闭)
角色:API 契约工程师 · 日期:2026-09-07 · 对应:04 号报告 RC-5 / D-1,放行条件②
1. 做了什么
docs/api/openapi.yaml从 5 端点扩为 6 端点:新增POST /api/v1/events(taganalytics,operationIdtrackEvents),info.version 1.0.0 → 1.1.0(纯增量,无既有字段变动)。- 新增组件:
TrackEventsRequest/TrackedEvent/TrackEventsEnvelope/TrackEventsResult/EventResult,错误分支复用既有ErrorEnvelope,鉴权复用既有bearerAuth,风格(camelCase、信封{code,message,data}、examples 写法)与既有 5 端点一致。 docs/api/index.md端点清单同步为 6 端点。- 校验:
python3 yaml.safe_load解析通过;mkdocs build --strict通过(0.60s)。
契约推导以代码实测行为为准(patbond-api/patbond-user analytics 包 + AnalyticsIntegrationTest 7 用例),不照抄 13 号报告草案——草案与实现的出入见 §3。
2. 逐项对照证据(契约条目 ↔ 实现)
| 契约条目 | 实现证据 |
|---|---|
| 批量 1–50,越界整批 400/40000 | TrackEventsRequest.events 上 @Size(min=1,max=50);测试 validationRejects400OnEmptyBatch(空数组 → 400 + code 40000) |
合法批次一律 202 + 信封 {code:0,…} |
Controller ResponseEntity.status(ACCEPTED).body(ApiResponse.success(...));测试 acceptsAnonymousEventBatch(202 + $.code=0) |
逐条结果 {accepted,duplicated,rejected,results[]},results 与请求等长同序 |
TrackEventsResponse 四字段;AnalyticsService.trackEvents 按输入顺序 append |
results[].status ∈ {accepted, duplicate, rejected},reason 仅 rejected 时出现 |
EventResult 三个工厂方法;@JsonInclude(NON_NULL) + record 的 null reason(accepted/duplicate 时 reason=null 不序列化) |
eventId 幂等去重 → duplicate |
repository ON CONFLICT DO NOTHING;测试 deduplicationReturnsDuplicate(同 eventId 二发 → duplicated=1) |
| 匿名可报;带 Bearer 则完整校验,无效 401/40101 | BearerAuthFilter.OPTIONAL_AUTH_PATHS = {"/api/v1/events"}——仅 Authorization 头缺失时放行,头存在则走完整验签;测试 acceptsAnonymousEventBatch 无 Authorization 头成功 |
| 拒绝原因 4 枚举 | unknown_event_name(测试 rejectsBatchWithUnknownEventName)、identity_mismatch(Service 第 2 步,token subject ≠ 事件 userId)、forbidden_field(测试 rejectsEventWithForbiddenFieldPattern,红线正则 password/token/secret/phone/mobile/email/credential/idfa/gaid)、schema_invalid(插入异常兜底) |
| 白名单外 props 剥离但事件保留 | sanitizeProps;测试 stripsPropsOutsideWhitelist(forbiddenExtraField 剥离,事件 accepted 且落库) |
单条事件 10 必填 + 2 可选(userId、props);platform 枚举 android/ios;eventName 正则 ^[a-z][a-z0-9_]{1,63}$;appVersion/osVersion 1–32 |
TrackedEvent 各字段的 @NotNull/@Pattern/@Size 注解逐一对应 |
| 不使用 Idempotency-Key 头 | Controller 无该头参数;13 号报告 §1.1 明文排除 |
3. 实现与草案/规范的不一致(仅记录,不改后端)
- 64KB 请求体上限未实现:13 号报告草案写「body ≤ 64KB 超限 400」,
application.yml无相应 max-size 配置、代码无检查(实际由 servlet 容器默认上限兜底)。契约据实未写 64KB;对应地草案的event_too_large拒绝原因实现中不存在,契约枚举未收录。 - 429 限流未实现:草案有「60 请求/5 分钟」429 + Retry-After,实现无任何限流。契约据实未写 429;后续若加限流属新增错误分支(additive),补契约即可。
- eventId 未强制 UUIDv7:规范要求 v7,服务端仅校验 UUID 格式(客户端实际发 v4,见 06 号报告 §0.1 偏差 2)。契约在 description 注明「规范要求 v7」,schema 层保持
format: uuid与实现一致。 - eventVersion 无
minimum: 1校验:草案 schema 有minimum: 1,实现仅@NotNull(0/负数可通过请求级校验)。契约据实不写 minimum,避免声称不存在的校验。 platform枚举实现为正则^(android|ios)$,与草案枚举等价,契约用 enum 表达。
4. 提交
独立提交(仅 openapi.yaml + index.md)已推送 patbond-doc main;本报告按波末统一提交约定暂不入库。