Files
lixi 60258324e4
CI / docs-build (push) Successful in 2m3s
docs: M2 第一波收口——报告 09/10/11 入档挂导航
- 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>
2026-09-07 14:54:19 +08:00

4.4 KiB
Raw Permalink Blame History

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/eventstag analyticsoperationId trackEvents),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. 逐项对照证据(契约条目 ↔ 实现)

契约条目 实现证据
批量 150,越界整批 400/40000 TrackEventsRequest.events@Size(min=1,max=50);测试 validationRejects400OnEmptyBatch(空数组 → 400 + code 40000
合法批次一律 202 + 信封 {code:0,…} Controller ResponseEntity.status(ACCEPTED).body(ApiResponse.success(...));测试 acceptsAnonymousEventBatch202 + $.code=0
逐条结果 {accepted,duplicated,rejected,results[]}results 与请求等长同序 TrackEventsResponse 四字段;AnalyticsService.trackEvents 按输入顺序 append
results[].status ∈ {accepted, duplicate, rejected}reason 仅 rejected 时出现 EventResult 三个工厂方法;@JsonInclude(NON_NULL) + record 的 null reasonaccepted/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_mismatchService 第 2 步,token subject ≠ 事件 userId)、forbidden_field(测试 rejectsEventWithForbiddenFieldPattern,红线正则 password/token/secret/phone/mobile/email/credential/idfa/gaid)、schema_invalid(插入异常兜底)
白名单外 props 剥离但事件保留 sanitizeProps;测试 stripsPropsOutsideWhitelistforbiddenExtraField 剥离,事件 accepted 且落库)
单条事件 10 必填 + 2 可选(userId、props);platform 枚举 android/ioseventName 正则 ^[a-z][a-z0-9_]{1,63}$appVersion/osVersion 132 TrackedEvent 各字段的 @NotNull/@Pattern/@Size 注解逐一对应
不使用 Idempotency-Key 头 Controller 无该头参数;13 号报告 §1.1 明文排除

3. 实现与草案/规范的不一致(仅记录,不改后端)

  1. 64KB 请求体上限未实现:13 号报告草案写「body ≤ 64KB 超限 400」,application.yml 无相应 max-size 配置、代码无检查(实际由 servlet 容器默认上限兜底)。契约据实未写 64KB;对应地草案的 event_too_large 拒绝原因实现中不存在,契约枚举未收录。
  2. 429 限流未实现:草案有「60 请求/5 分钟」429 + Retry-After,实现无任何限流。契约据实未写 429;后续若加限流属新增错误分支(additive),补契约即可。
  3. eventId 未强制 UUIDv7:规范要求 v7,服务端仅校验 UUID 格式(客户端实际发 v4,见 06 号报告 §0.1 偏差 2)。契约在 description 注明「规范要求 v7」,schema 层保持 format: uuid 与实现一致。
  4. eventVersion 无 minimum: 1 校验:草案 schema 有 minimum: 1,实现仅 @NotNull(0/负数可通过请求级校验)。契约据实不写 minimum,避免声称不存在的校验。
  5. platform 枚举实现为正则 ^(android|ios)$,与草案枚举等价,契约用 enum 表达。

4. 提交

独立提交(仅 openapi.yaml + index.md)已推送 patbond-doc main;本报告按波末统一提交约定暂不入库。