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>
40 lines
4.4 KiB
Markdown
40 lines
4.4 KiB
Markdown
# 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`(tag `analytics`,operationId `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. 逐项对照证据(契约条目 ↔ 实现)
|
||
|
||
| 契约条目 | 实现证据 |
|
||
| --- | --- |
|
||
| 批量 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. 实现与草案/规范的不一致(仅记录,不改后端)
|
||
|
||
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;本报告按波末统一提交约定暂不入库。
|