Files
patbond-doc/docs/development/iterations/iteration-3/06-experiment-tracking-plan.md
T
lixi d2867826d3
CI / docs-build (push) Successful in 55s
docs: M3 开工分析 8 份报告入档 + ADR-016~021 拍板决策
- iteration-3 报告 01-08(PM 拆解/后端/Flutter/RC 首个 CERTIFIED/UI/埋点/证据基线/Git)
- ADR-016 自托管 MinIO 起步预留迁云(用户确认现无云存储)、ADR-017 patbond-community
  :8084 + media 归 user + 作者信息跨 schema 只读、ADR-018 范围裁剪(话题剪出/单层评论)、
  ADR-019 幂等按域(PUT/DELETE + request_hash)、ADR-020 聚合 feed_viewed/字典 v3/
  北极星不变/队列三项升第一波、ADR-021 Git 修订(main 更正/PR 情形触发/首发布重建 main/
  防泄漏 grep 先行)
- mkdocs 挂「第三迭代」导航,build --strict 通过

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-08 16:02:53 +08:00

468 lines
39 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 第三迭代埋点与实验规划(社区)
> 角色:Experiment Tracker
> 日期:2026-09-08
> 前序:iteration-2 `06-experiment-tracking-plan.md`(字典 v2、北极星定义式、H1~H4、A/B 八项前置)、`15-analytics-persistent-queue.md`(分段持久化队列实况)、`24-event-whitelist-v2.md`、`29-m2-summary.md` §4(遗留与 M3 方向)、`30-device-verification-checklist.md`(真机补验挂起)
> 依据:`development-plan.md` 第 4 节 community 域、第 7 节 M3 验收、第 9 节「可观测性与产品验证」;`patbond-api` `EventDictionary.java` 现行白名单(v221 事件);`patbond-flutter` `lib/analytics/` 现状(SessionTracker / RouteObserver / 分段队列均已落地)
> 范围:M3 社区纵切(Feed、帖子、草稿/发布、媒体、评论、点赞、收藏、关注、话题);AI 创作、本地服务不在本轮定义
> 性质:纯规划文档,供 M3 开发工单直接引用;不含任何代码改动
**速览(五个核心结论)**
1. 事件字典 v3 增量 **19 个新事件**post 域 8 + feed 域 2 + 互动 8 + 实验基建 `experiment_exposed` 1),命名沿 v1/v2 惯例,结果编码进事件名,见 §1。
2. **Feed 曝光采用「浏览段聚合」设计,逐卡曝光事件被本角色否决**——量级重测表明:逐卡设计下 M2 的「零扩容」结论**不再成立**1,000 DAU 约 7~14 个月击穿 5,000 万行分区阈值,且接收端限流实际未实现、快速滑动会形成无背压直写),聚合设计下**零扩容结论继续成立**,见 §2。
3. 新增 **4 条可证伪假设 H5~H8**(发布渗透率 / 发布漏斗完成率 / 社区-记录协同 / Feed 消费深度),H1~H4 出数日历与责任人落定(判定日 10-06 / 10-13 / 10-20),见 §3。
4. 北极星**建议 M3 保持「7 日回访记录率」不变**,社区复合指标不在本迭代引入;复评点设在 M3 收官、以 H7 读数为依据(**待拍板**),见 §4。
5. A/B 八项前置的 M3 推进计划:**6 项本迭代变绿 + 1 项部分变绿**(#7 的 feature flag 回滚随社区发布开关顺带落地、监控留 M4),维持「M3 末基本全绿、M4 首实验」路线,见 §5。
---
## 0. 基线现状(开工前核对)
M2 收官把 v2 规划的绝大部分落成了现实,本节只记与 M3 规划直接相关的事实。
| 项 | 现状 | 出处 |
| --- | --- | --- |
| 后端字典 | v2 共 21 事件:auth 11 + `page_viewed` 正稿 + pet 3 + health_record 6`health_record_action` 已移除(ADR-013 | `EventDictionary.java` 实读 |
| 接收端 | `POST /api/v1/events` 批量 150、202 逐条、eventId 幂等、白名单剥离、红线拒绝;**64KB 上限与 429 限流均未实现**(契约据实未写,09 号 §出入 1/2) | 09 号报告 |
| 存储 | `platform.product_events` 不分区,分区阈值约 5,000 万行 | v1 报告 13 §2.3 |
| 客户端会话 | `session_tracker.dart` 已落地(冷启动/30 分钟规则);`analytics_route_observer.dart` + `page_viewed` 已挂全 | 15 号 / 24 号报告 |
| 客户端队列 | 分段持久化(500 条 / 25 段、at-least-once、批 ≤50);冲刷触发点 3/4:满 20 条、退后台、冷启动恢复——**30 秒定时器未做** | 15 号 §2.3/§4 |
| 队列遗留三项 | 30s 定时冲刷、退避/429(依赖后端先有限流)、anonymousId 持久化 | 29 号 §4 遗留 3 |
| 真机补验 | Android 落库观察 + SessionTracker 30min 手测挂起(0.5 天清单在 30 号报告)——**这是 A/B 前置 #1「数据质量验收」的拦路项** | 30 号报告 |
| pageName 实况 | 客户端枚举 13 个:字典 v2 初始 9 个 + 客户端自行补充 4 个(`create`/`pet_archive`/`services`/`post_detail`),后者**尚未同步进字典正稿** | `analytics_page_name.dart` 实读 |
| 假设与北极星 | H1~H4 判定线已冻结(观察窗自 2026-09-08 起算);北极星 SQL 与 §6 对账 SQL 已入档待巡检 | M2 06 号 §2/§3 |
**开工前必须知道的一件事**:M2 06 号 §7.1 曾以「限流 60 请求/5 分钟余量十几倍」论证零扩容,但 09 号契约回填核实该限流**从未实现**——接收端目前对客户端写入没有任何背压。这不改变 M2 量级下的结论(量太小),但 M3 引入首个高频事件后,**保护必须内建在事件设计里而不能指望限流兜底**,这是 §2 裁定聚合方案的硬前提之一。
---
## 1. 事件字典 v3 增量(community 域族)
### 1.1 沿用原则与域划分
命名 `<域>_<动作>_<结果>` snake_case、结果编码进事件名(`_succeeded`/`_failed`)、单义事件不设结果后缀(沿 `health_record_viewed`/`health_record_deleted` 先例)、`eventVersion` 起始 1、公共属性十项全带、属性 camelCase。v3 新增域前缀:
- **`post`**:帖子生命周期(创建、媒体、草稿、发布、删除)
- **`feed`**:Feed 消费(曝光聚合、加载失败)
- **`comment`**:评论
- **`user`**:关注关系(`user_followed`——关注的对象是用户,域按实体归 user;话题关注见 §1.6 缺口 3)
- 点赞/收藏归 **`post`** 域(作用对象是帖子)
设计纪律沿 v2 §1.2 的教训:**不设** `community_action(actionType)` 式多路复用事件——like/unlike/favorite/unfavorite 是四个语义独立的动作,各自独立成名,任何一个的枚举扩充不污染其他指标口径。
### 1.2 核心裁定:Feed 曝光用「浏览段聚合」,不做逐卡事件
这是 v3 最重要的一个设计决策,先给结论再给依据(量级数字在 §2 展开):
**`feed_viewed` 定义为「一个 Feed 浏览段」的聚合事件**:用户进入 Feed 页起累计计数,**离开时(路由跳走 / 退后台)发一条**,携带该段的曝光卡片数、翻页数、刷新数与停留时长。卡片「曝光」的客户端判定:卡片可见面积 ≥ 50% 且持续 ≥ 500ms,**同一浏览段内按 postId 去重**(postId 只在客户端内存里做去重键,**绝不上报**,上报的只有计数)。
否决逐卡方案(每张卡片可见发一条 `post_impression(postId)`)的四条理由:
1. **存储击穿**(§2.2):逐卡设计使「零扩容」结论失效,M3 就要启动分区改造——为一个当前没有消费方的数据形态提前付基建成本,不成立。
2. **无背压直写**:接收端限流未实现(§0),快速滑动可产生 5–10 卡/秒,客户端满 20 条即冲刷 ≈ 每 2–4 秒一个 HTTP 请求,无任何机制拦截这种放大。
3. **队列容量反噬**:500 条队列按 M2 量级可容两周离线积压,逐卡设计下缩水到 2~4 天,离线场景开始真实丢数据(丢最旧整段),反而伤害其他低频高价值事件。
4. **当前无消费方**:逐卡曝光的唯一刚需是「按帖子算曝光-点击率」供推荐排序实验用。M3 的 Feed 是游标分页的时序流、没有排序算法;等 M4+ 真做排序实验时,逐帖曝光的正确采集点是**服务端 Feed 下发日志**(server-side,天然全量、无客户端丢失率问题),而不是客户端埋点。此路线记入 backlog(§8 拍板 6),届时按需再评估分区与采样。
聚合方案的代价是丢失「单帖曝光→点进」归因,保留的是本迭代真正要回答的问题:**人们刷不刷、刷多深、刷完动不动手**(H8、H5 的数据源)——按需采集,不为想象中的分析囤数据。
### 1.3 隐私红线增量(社区内容是重灾区,在 v2 五条之上追加)
社区域的埋点只记**行为**不记**内容**,且社区首次引入「用户生成内容 + 用户间关系」,红线从严:
1. **帖子/评论正文**:任何自由文本禁止上报;文本规模用 `textLengthBucket` 枚举(`empty` / `short`(≤50) / `medium`(51500) / `long`(>500)),不报精确字数。
2. **内容 ID 与用户 ID**postId、commentId、topicId、被关注/被赞用户的 userId 一律不进 props(公共属性里的 userId 是**行为主体**自己,这是既有契约;**行为客体**的任何标识不上报)。逐卡曝光被否决后,v3 全部事件无一需要内容 ID。
3. **话题名**:话题是公开分类词但仍不上报名称(自建话题可能含用户自由文本),只报 `topicCount`;话题维度的内容分析走服务端事实表。
4. **媒体线索**:文件名、本地路径、URL 禁止;只允许 `mediaType` 枚举与 `sizeBucket` 枚举(`lt_1mb` / `mb_1_5` / `mb_5_20` / `gte_20mb`),不报精确字节数。
5. **pageName 归一化**(红线 5 延伸):`post_detail``topic_detail``user_profile` 等带参数路由,参数一律剥离,UUID 出现在 pageName/referrer 即验收失败。
红线正则本轮仍不扩(理由同 v2 §1.3);值级巡检(v2 §6.4 长度 >64 扫描)天然覆盖「正文塞进合法字段」的泄漏形态,继续每日跑。
### 1.4 新事件清单
失败枚举基底(v2 七项之上按 M3 验收新增):
- `content_rejected` — 内容审核/敏感词拒绝(**待拍板**:M3 是否有审核环节,无则删)
- `media_too_large` / `unsupported_format` — 媒体上传专用
- `not_found` 复用 — 目标帖子/评论已被删除(对应验收「删除内容不可继续出现」的客户端时序窗口)
#### 发布漏斗(post 域)
| 事件名 | 触发时机 | 专有属性 |
| --- | --- | --- |
| `post_create_started` | 进入发帖编辑器并产生**首次输入**(含首次选媒体),每次进入记一次 | `entryPoint``create_tab` / `feed` / `topic_detail` / `pet_detail`,待 UI 定稿收敛) |
| `post_draft_saved` | 草稿保存成功响应后;**仅手动保存与离开时保存**,若产品做打字自动保存,自动保存不埋(防高频) | `trigger``manual` / `on_exit`)、`mediaCount` |
| `post_publish_succeeded` | 发布接口成功响应后(**漏斗事件**,H5/H6 核心数据源) | `durationMs`started→publish)、`mediaCount``topicCount``textLengthBucket``fromDraft`bool |
| `post_publish_failed` | 发布失败 / 超时 / 本地校验拦截 | `failureReason``errorCode``httpStatus``attemptSeq` |
| `post_deleted` | 删帖成功响应后(单事件风格,失败靠服务端错误率观测) | 无专有属性 |
`post_publish_failed.failureReason``validation_error``content_rejected`(待拍板)、`media_upload_incomplete`(有媒体未传完即点发布)、`rate_limited``network_error``server_error`
#### 媒体上传漏斗(post 域,逐文件)
| 事件名 | 触发时机 | 专有属性 |
| --- | --- | --- |
| `post_media_upload_started` | 单个媒体文件开始上传 | `mediaType``image` / `video`)、`sizeBucket` |
| `post_media_upload_succeeded` | 单文件上传成功 | `mediaType``sizeBucket``durationMs` |
| `post_media_upload_failed` | 单文件失败 / 超时 / 用户取消 | `mediaType``sizeBucket``failureReason``errorCode``httpStatus``attemptSeq` |
逐文件(而非逐帖聚合)的理由:上传是发布漏斗预判的最大流失段(H6),失败归因需要文件粒度的 `sizeBucket × mediaType × failureReason` 交叉;量级无忧——单帖媒体数有产品上限(九宫格类,≤9),非高频。`failureReason``media_too_large``unsupported_format``network_error``server_error``cancelled`
#### Feed 消费(feed 域)
| 事件名 | 触发时机 | 专有属性 |
| --- | --- | --- |
| `feed_viewed` | **离开 Feed**(路由跳走 / 退后台)时发一条,聚合本浏览段(§1.2 裁定) | `feedTab``home` / `topic` / `user_posts` / `favorites`,待 UI 定稿收敛)、`durationMs``impressionCount`(≥50% 可见 ≥500ms、段内按帖去重)、`loadMoreCount`(翻页次数)、`refreshCount`(下拉刷新次数) |
| `feed_load_failed` | 刷新或翻页请求失败(M3 验收「分页不丢失不重复」的客户端观测点) | `feedTab``loadType``refresh` / `load_more`)、`failureReason``errorCode``httpStatus` |
实现注意:`impressionCount` 去重集合只存活于浏览段内存中,段结束即弃;`durationMs` 用前台时长(退后台暂停计时),上限截断 30 分钟(防止挂机污染 H8)。
帖子详情**浏览**不设 `post_viewed`——由 `page_viewed(pageName=post_detail)` 覆盖(沿 v2 `pet_viewed` 不设的同一先例,防双事件重复计数)。
#### 互动(post / comment / user 域)
| 事件名 | 触发时机 | 专有属性 |
| --- | --- | --- |
| `post_liked` | 点赞成功响应后 | `source``feed` / `post_detail` |
| `post_unliked` | 取消点赞成功响应后 | `source` |
| `post_favorited` | 收藏成功响应后 | `source` |
| `post_unfavorited` | 取消收藏成功响应后 | `source` |
| `comment_create_succeeded` | 评论提交成功响应后 | `durationMs``isReply`bool,楼中楼)、`textLengthBucket` |
| `comment_create_failed` | 评论提交失败 | `failureReason``errorCode``httpStatus``attemptSeq` |
| `user_followed` | 关注成功响应后 | `source``post_detail` / `feed` / `user_profile` / `follow_list` |
| `user_unfollowed` | 取关成功响应后 | `source` |
取舍说明(与 v2 同款自觉取舍,复活条件注明):
- **点赞/收藏/关注不埋失败**:幂等写入、单点交互,失败率靠服务端接口错误率观测(`health_record_deleted` 先例)。若乐观更新回滚率成为问题,届时以 eventVersion=2 增补 `_failed`
- **评论不设 `comment_create_started`**:短表单,沿 v2「编辑不设 started」先例;评论放弃率若成为问题再增补。
- **like/unlike 分立而非 `action` 属性**v2 §1.2 废弃 `health_record_action` 的同一逻辑——H5 的「互动用户」分母定义只引用语义单一的事件名。
#### 实验基建(platform 域,A/B 前置 #5 提前进字典)
| 事件名 | 触发时机 | 专有属性 |
| --- | --- | --- |
| `experiment_exposed` | 用户**实际到达**实验触点时(渲染了变体 UI),非分配时 | `experimentKey`(实验注册表枚举)、`variant` |
M4 首实验才启用,但字典与白名单**本迭代一次进**:M3 后端反正要动 `EventDictionary`,避免 M4 为一个事件再开一轮字典工单;客户端强类型封装同批出(可先无调用方)。这直接把 A/B 前置 #5 在 M3 变绿(§5)。
### 1.5 v3 增量总览(19 个新事件)
| # | 事件名 | 版本 | 性质 |
| --- | --- | --- | --- |
| 22 | `post_create_started` | 1 | 新增 |
| 23 | `post_draft_saved` | 1 | 新增 |
| 24 | `post_publish_succeeded` | 1 | 新增(漏斗事件) |
| 25 | `post_publish_failed` | 1 | 新增 |
| 26 | `post_deleted` | 1 | 新增 |
| 27 | `post_media_upload_started` | 1 | 新增 |
| 28 | `post_media_upload_succeeded` | 1 | 新增(漏斗事件) |
| 29 | `post_media_upload_failed` | 1 | 新增 |
| 30 | `feed_viewed` | 1 | 新增(聚合曝光,首个高频事件) |
| 31 | `feed_load_failed` | 1 | 新增 |
| 32 | `post_liked` | 1 | 新增 |
| 33 | `post_unliked` | 1 | 新增 |
| 34 | `post_favorited` | 1 | 新增 |
| 35 | `post_unfavorited` | 1 | 新增 |
| 36 | `comment_create_succeeded` | 1 | 新增 |
| 37 | `comment_create_failed` | 1 | 新增 |
| 38 | `user_followed` | 1 | 新增 |
| 39 | `user_unfollowed` | 1 | 新增 |
| 40 | `experiment_exposed` | 1 | 新增(M4 启用,字典先行) |
后端 `EventDictionary` 白名单增量(工单可直接抄):
```java
// v3 增量 post 域(iteration-3 报告 06 §1.4
Map.entry("post_create_started", Set.of("entryPoint")),
Map.entry("post_draft_saved", Set.of("trigger", "mediaCount")),
Map.entry("post_publish_succeeded",
Set.of("durationMs", "mediaCount", "topicCount", "textLengthBucket", "fromDraft")),
Map.entry("post_publish_failed",
Set.of("failureReason", "errorCode", "httpStatus", "attemptSeq")),
Map.entry("post_deleted", Set.of()),
Map.entry("post_media_upload_started", Set.of("mediaType", "sizeBucket")),
Map.entry("post_media_upload_succeeded", Set.of("mediaType", "sizeBucket", "durationMs")),
Map.entry("post_media_upload_failed",
Set.of("mediaType", "sizeBucket", "failureReason", "errorCode", "httpStatus", "attemptSeq")),
// v3 增量 feed 域(聚合曝光设计,§1.2 裁定)
Map.entry("feed_viewed",
Set.of("feedTab", "durationMs", "impressionCount", "loadMoreCount", "refreshCount")),
Map.entry("feed_load_failed",
Set.of("feedTab", "loadType", "failureReason", "errorCode", "httpStatus")),
// v3 增量互动
Map.entry("post_liked", Set.of("source")),
Map.entry("post_unliked", Set.of("source")),
Map.entry("post_favorited", Set.of("source")),
Map.entry("post_unfavorited", Set.of("source")),
Map.entry("comment_create_succeeded", Set.of("durationMs", "isReply", "textLengthBucket")),
Map.entry("comment_create_failed",
Set.of("failureReason", "errorCode", "httpStatus", "attemptSeq")),
Map.entry("user_followed", Set.of("source")),
Map.entry("user_unfollowed", Set.of("source")),
// A/B 前置 #5:曝光事件字典先行,M4 启用(§1.4)
Map.entry("experiment_exposed", Set.of("experimentKey", "variant"))
```
Flutter 侧沿用强类型封装惯例:新建 `post_analytics.dart` / `feed_analytics.dart` / `community_interaction_analytics.dart`,枚举编译期锁死。
### 1.6 漏斗闭环与维度够用性复核
复核方法同 v2 §1.6:以 §3 假设与 M3 验收逐条反推数据源。
**闭环成立**:发布漏斗四段 `page_viewed(post_form) → post_create_started → post_publish_succeeded/failed`(媒体上传子漏斗嵌套其中,started→succeeded/failed 配对完整);Feed 消费闭环 `feed_viewed`(曝光量)→ `page_viewed(post_detail)`(点进)→ 互动事件。**发布漏斗的「到达→动笔」段由 pageName 新增 `post_form` 承接**(§6),与 v2 修订 1 的 `pet_form` 同构——这次在设计期就补上,不留缺口。
**缺口 1(接受不埋)**:逐帖曝光-点击归因——§1.2 已论证,M4+ 走服务端日志路线,backlog 登记。
**缺口 2(接受不埋)**:评论/帖子的浏览深度(评论区滚动)——`page_viewed(post_detail)` 足够回答「点进率」,评论区消费深度在排序实验之前无消费方。
**缺口 3(待拍板)**:话题关注——若 M3 UI 有「关注话题」按钮,需增补 `topic_followed/unfollowed(source)`(不报话题名,红线 3);UI 定稿前挂起(§8 拍板 5)。
**维度够用性**:H5 需互动/发布事件按 userId 去重(有);H6 需发布漏斗配对 + 媒体子漏斗(有);H7 需互动事件与 `health_record_create_succeeded` 的 userId + serverTs(有,跨域 join);H8 需 `feed_viewed.impressionCount/loadMoreCount`(有)。**全部假设可由 v3 字典 + community 事实表回答,判定通过。**
---
## 2. Feed 曝光量级评估与「零扩容」结论复核
### 2.1 v3 上线后的单用户日事件量重估
| 来源 | 条/DAU/日 | 说明 |
| --- | --- | --- |
| M2 存量(auth + page_viewed + pet/health_record | 1530 | v2 §7.1 估算,实测待巡检校准 |
| `page_viewed` 社区页面增量 | +510 | post_detail 点进是主要来源 |
| `feed_viewed`(聚合) | +3–8 | 每浏览段一条 |
| 互动(like/favorite/comment/follow 及 un-* | +310 | 活跃互动者 |
| 发布漏斗 + 媒体 + 草稿 | +0.5–3 | 发布是低频动作(H5 预估 ≤10% 用户/周) |
| **合计** | **2761** | **约 M2 的 2 倍** |
### 2.2 「零扩容」结论复核:聚合设计下成立,逐卡设计下不成立
**聚合设计(本方案)**
- 接收端:61 条/日、满 20 条冲刷 ≈ 3–4 请求/日/用户,即便未来补 60 请求/5 分钟限流也有百倍余量。契约、批上限 50、接收逻辑**均不动**。
- 存储:1,000 DAU × 60 条 × 365 天 ≈ **2,200 万行/年**,距 5,000 万分区阈值仍有约 2 年余量。**不分区决策继续有效。**
- 队列:500 条 ≈ 8 天以上离线积压(vs M2 两周,可接受),**上限不调**。
- **结论:零改动,「零扩容」结论继续成立。**
**逐卡设计(被否决方案,留数字供复议)**
- 活跃刷 Feed 用户 2–4 段/日 × 2060 卡 ≈ 40–240 条曝光/日,总量升至 100250 条/DAU/日。
- 存储:1,000 DAU 中位 ≈ 4,400 万行/年、上沿 ≈ 9,100 万行/年——**7~14 个月击穿分区阈值**,M3 就得启动分区 + 保留策略改造。
- 突发:快速滑动 5–10 卡/秒 → 每 2–4 秒满 20 条冲刷一次 → 单用户可达 75–150 请求/5 分钟;限流未实现(§0),这是对接收端和数据库的无背压直写。
- 队列:500 条仅容 2–4 天离线积压,挤压其他事件的 at-least-once 保障。
- **结论:逐卡设计使 M2「零扩容」结论失效**——这就是 §1.2 裁定的量化依据。
### 2.3 队列遗留三项的 M3 处置(优先级重排)
| 遗留项(29 号 §4) | M3 处置 | 理由 |
| --- | --- | --- |
| 30s 定时冲刷 | **本迭代第一波做**(P1) | 社区场景出现「长前台会话」(刷 Feed 半小时不切页),现有三触发点在这种会话里最多积压 19 条不上传;定时器同时改善当日监控的数据新鲜度。实现按 15 号 §4 既定方案。 |
| 退避 + 429 处理 | **客户端退避本迭代做**(5xx/网络错误指数退避 + 抖动);429 分支随后端限流落地一并做 | 后端限流是 09 号出入 5 项排期评估的一部分(后端侧决策);客户端 5xx 退避不依赖它,社区量级翻倍后重试风暴的伤害面变大,先行。 |
| anonymousId 持久化 | **本迭代做**(P2,一行级改动) | 现状每次冷启动新生成(`analytics_service.dart` 构造器 `Uuid().v4()`),登录前事件无法跨启动归并。A/B 前置 #4 的「登录前实验 anonymousId 分流」硬依赖持久化——M3 不做,M4 首实验若涉及注册/登录前触点就被卡住。 |
以上三项均为 `lib/analytics/` 内改动,与社区功能开发无耦合,建议与字典 v3 后端工单同批排入第一波。
---
## 3. 产品假设:H5~H8 新增 + H1~H4 出数日历
### 3.0 方法约定(沿 v2 §3,两点强调)
判定线上线前登记并 PM 会签冻结,届满出「支持 / 证伪 / 数据不足」三态判定。**H5~H8 的观察窗自社区功能对用户可用之日(下称 T0,随 M3 发布日落定)起算**,T0 后第 1 周为尝鲜噪声期,除 H6 外剔除。社区上线会扰动 M2 假设的在途窗口,处理纪律见 §3.2。
### H5:社区消费者远多于生产者,但生产者渗透率决定内容池成活(发布渗透假设)
- **陈述**:稳定期内,周活跃用户中当周产生 ≥1 条 `post_publish_succeeded` 的比例 ≥ 5%。
- **判定指标**:周去重 `userId`(post_publish_succeeded) / 周去重 userId(任意事件);辅助读数:互动渗透率(≥1 条 like/favorite/comment/follow 的占比)。
- **判定线**:支持 = ≥ 5%;证伪 = 连续 3 周 < 2%25% 顺延。
- **窗口**T0 后第 25 周。
- **行动**:证伪 → 发布门槛过高或动机不足,「从健康记录一键生成帖子」类降门槛引导进 A/B 候选池;不在 Feed 排序上浪费资源(内容池不成活时排序无意义)。支持 → 内容池自生长成立,资源投向消费侧(H8)。
### H6:媒体上传是发布漏斗的最大流失段(漏斗诊断假设)
- **陈述**:发布漏斗完成率(`post_publish_succeeded` / `post_create_started`,24h 归因窗)≥ 60%,且流失集中在含媒体的发布(含媒体发布的完成率比纯文字低 ≥ 15pp)。
- **判定指标**:漏斗配对 + `post_media_upload_failed``sizeBucket × failureReason` 分布交叉定位。
- **判定线**:支持 = 完成率 ≥ 60% 且媒体差 ≥ 15pp;证伪 = 完成率 < 40%(漏斗整体坏,另找原因)或媒体差 < 5pp(流失不在媒体段);其余顺延。
- **窗口**:T0 后第 1–4 周(**含噪声周**——漏斗诊断恰恰要看首批用户的失败形态,沿 H4 先例)。
- **行动**:支持 → 上传压缩/断点续传优化排 M4 前置;证伪且完成率低 → 按 failureReason 分布重新归因(validation_error 高则查表单/文案)。
### H7:社区活跃提升记录回访(社区-记录协同假设,北极星拍板的数据依据)
- **陈述**:首记后 7 日内产生过 ≥1 次社区互动(like/favorite/comment/follow/publish 任一成功事件)的用户,其 7 日回访记录率比无互动者高 ≥ 8pp。
- **判定指标**:北极星 SQL(M2 06 号 §2.1)按「窗口内是否有社区互动事件」分两群比较;回访事件仍只算 `health_record_create_succeeded`(社区行为只做分群、不充当回访,无循环)。
- **判定线**:支持 = 差值 ≥ 8pp 且两群各 ≥ 100 人;证伪 = 差值 < 3pp 或倒挂;38pp 顺延。
- **窗口**:T0 后 6 周(需 ≥ 2 个成熟队列)。
- **方法论警示**:观察性对照,只证相关(活跃用户本来什么都多做,自选择偏差与 H3 同款)。支持的正确用法是把「记录完成页引导分享到社区」列为 A/B 候选,用随机化坐实;同时它是 §4 北极星复评的核心输入——**若证伪(社区与记录是两个不相干场景),复合北极星的动议应就地终结**。
- **行动**:支持 → A/B 候选池 + 北极星复评启动;证伪 → 社区按独立场景运营,北极星保持记录型不再复议。
### H8:Feed 首屏之外仍有消费需求(内容供给/消费深度假设)
- **陈述**:稳定期内,≥ 40% 的 Feed 浏览段发生翻页(`feed_viewed.loadMoreCount` ≥ 1)。
- **判定指标**:翻页浏览段占比;辅助读数:浏览段 `impressionCount` 中位数、`durationMs` 分布(截断 30min,§1.4)。
- **判定线**:支持 = ≥ 40%;证伪 = 连续 3 周 < 20%2040% 顺延。
- **窗口**T0 后第 25 周。
- **行动**:证伪 → 首屏即耗尽兴趣,指向内容供给不足(结合 H5 判定:若 H5 也证伪则是供给问题,运营/官方内容或 M4 AI 创作「一键发帖」提前;若 H5 支持则是分发问题);支持 → 游标分页体验(预加载、去重)投入合理,排序实验(M4+)有消费基础。
### 3.2 H1~H4 与北极星在 M3 期间的出数安排
M2 冻结的窗口自 2026-09-08 起算,判定日历与责任人如下(周节奏:**每周一**数据侧跑 M2 06 号 §6 全部对账 SQL + §2.1 北极星 SQL,本角色复核读数并记入巡检记录):
| 项 | 窗口 | 关键日期 | 跑数责任 | 判定责任 |
| --- | --- | --- | --- | --- |
| 北极星首个成熟周队列 | W37 队列(09-07~09-13 首记)+8 天成熟 | **2026-09-21(周一)首次出数**,此后每周一滚动 | 数据侧 | 本角色发布(Wilson 95% CI<50 人周合并) |
| H4 激活链路 | 上线后 4 周(含第 1 周) | **2026-10-06 判定** | 数据侧 | 本角色 + PM 会签 |
| H2 多宠 | 上线后 4 周末读数 | **2026-10-06 判定**`pet.pets` 真值侧) | 数据侧 | 同上 |
| H1 记录类型分布 | 第 25 周(09-15~10-12 | **2026-10-13 判定** | 数据侧(§6.2 SQL 即读数) | 同上 |
| H3 提醒-回访 | 6 周(≥2 成熟队列) | **2026-10-20 判定** | 数据侧 | 同上 |
**社区上线对在途窗口的污染纪律**:若 T0(社区发布日)落在 H1/H3 窗口内,判定线**不改**(冻结纪律),但读数发布时必须按 T0 前/后拆周标注;H3 若前后两段方向不一致,判定记「数据不足-顺延」并注明混杂因素,不得挑一段下结论。H7 的对照组恰好提供了交叉检验。
**前置风险**:H1~H4 与北极星的一切读数都以真实事件流入库为前提。真机补验(30 号清单)未完成前,Android 端数据可信度未验证——**补验必须在 09-21 首次北极星出数前完成**,否则首批读数只能标「未验收数据,仅供方向参考」。
---
## 4. 北极星:M3 保持「7 日回访记录率」,不引入社区复合指标(待拍板)
社区上线后「北极星要不要变」是必答题。本角色立场:**M3 全程保持现北极星不变**,理由三条:
1. **基线刚建立,换指标即断线**。北极星 09-21 才出第一个成熟队列读数,M3 期间总共只会积累 4~6 个可比周。此时切换或掺入社区成分,等于永远失去「社区上线前后」这组最有价值的对照——北极星的首要职责是跨迭代可比。
2. **新功能光环效应会系统性高估社区成分**。任何复合指标(如「7 日回访有效行为率 = 记录或发帖或互动」)在社区上线后前几周必然被尝鲜流量冲高,读数好看但不可解释,恰好违背 v2 选 A 弃 B 的原始理由(拒绝易被一次性行为冲高的指标)。
3. **「社区是否服务于留存」本身是待验假设,不是前提**。这正是 H7 的问题。把社区写进北极星等于未经验证就宣布答案。正确顺序:H7 出数(T0+6 周)→ 若支持且 A/B 坐实,M4 起再评估复合式(候选形态:分子扩为「记录 或 发布」,互动类行为因信号太弱不入分子);若 H7 证伪,动议终结。
**落定为拍板项**(§8 拍板 1):M3 保持不变,复评点 = M3 收官会 + H7 读数;PM 保留否决权,否决须给出替代定义式与断线代价的处置方案。社区侧的健康度用**辅助指标层**观测(不升格):周发布渗透率(H5 口径)、周互动渗透率、Feed 翻页率(H8 口径)——三者随 §7 巡检周报发布。
---
## 5. A/B 八项前置的 M3 推进计划
M2 06 号 §4.2 立的路线是「M3 末全绿、M4 首实验」。逐项落定 M3 的动作与责任侧:
| # | 前置条件 | M3 动作 | 责任侧 | M3 末预期 |
| --- | --- | --- | --- | --- |
| 1 | 数据质量验收 | 真机补验(30 号清单,0.5 天)→ M2 字典 v2 事件 2 周巡检达标(丢失 <5%、对账偏差 <5%、去重 <10%、serverTs 100%、无红线泄漏) | 数据 + 真机执行人 | **绿**(拦路项是真机,见 §3.2 风险) |
| 2 | 指标基线 | 北极星 + M2 漏斗连续 ≥2 周稳定产出(09-21 起自然达成),留档均值与方差 | 数据 | **绿** |
| 3 | 样本量规则成文 | 基线率 × MDE × α=0.05 × 功效 80% 的计算方法 + 查表 + 按实测 DAU 换算最短运行时长;以 §3 实测基线代入(不再用 M2 的假设值) | 本角色 | **绿**M3 中交付) |
| 4 | 稳定分流组件 | `hash(userId, experimentSalt) % buckets` 后端组件 + anonymousId 持久化(§2.3,登录前分流的前提)+ 登录后归并规则成文 | 后端(归并规则:本角色) | **绿** |
| 5 | 曝光事件 | `experiment_exposed` 已随 v3 进字典(§1.4);Flutter 强类型封装同批出 | 后端 + Flutter | **绿**(本报告已完成设计) |
| 6 | 实验设计模板与评审流程 | 模板(假设/主指标/护栏/提前停止规则/多重比较约定)+ 评审流程成文;与 #3 同一文档交付 | 本角色 | **绿** |
| 7 | 护栏监控与回滚 | feature flag 开关机制随「社区功能发布开关」顺带落地(社区本就该有开关灰度);护栏**准实时监控**留 M4(依赖监控设施选型) | 后端/DevOps | **部分绿**(回滚绿、监控 M4 |
| 8 | 隐私合规复核 | 每实验一次,常态项 | 每实验 | 常态 |
**结论:M3 末 6 项全绿 + #7 部分绿,M4 初补齐监控即可启动首实验。** 首实验候选池按判定结果动态排序:H3 支持 →「默认引导创建提醒」;H4 证伪 →「建宠成功页引导首条记录」;H7 支持 →「记录完成页引导分享社区」;H5 证伪 →「记录一键生成帖子」。届时按 #3 的样本量规则做可行性检验(运行 >8 周即判不可行,退回观察,止损线预登记——沿 M2 §4.3 纪律)。
---
## 6. pageName 枚举增量与 page_viewed 覆盖检查
### 6.1 增量清单
现状(§0):字典正稿 9 个 + 客户端已自行补充 4 个未同步正稿。v3 一次收编 + 社区族增量:
| pageName | 性质 | 说明 |
| --- | --- | --- |
| `create` / `pet_archive` / `services` / `post_detail` | **收编转正**(客户端已存在) | 补进字典说明,消除枚举双源 |
| `post_form` | 新增 | 发帖编辑器——发布漏斗「到达段」承接者(§1.6),对应 v2 的 `pet_form` 教训,设计期即补 |
| `topic_list` | 新增 | 话题列表/广场 |
| `topic_detail` | 新增 | 话题详情(含话题内 Feed;话题 ID 剥离) |
| `user_profile` | 新增 | **他人**主页(自己的主页仍是 `profile`,两者语义不同不合并;用户 ID 剥离) |
| `follower_list` / `following_list` | 新增 | 粉丝/关注列表分立(关注关系的两个方向是不同页面) |
| `favorite_list` | 新增 | 我的收藏 |
| `draft_list` | 新增 | 草稿箱 |
**9 个新增 + 4 个收编**。Feed 本体不新增 pageName:首页 Tab 即 Feed,沿用 `home`pageName 保持导航语义,Feed 消费的度量职责已由 `feed_viewed` 承担,避免一次改名断掉 M2 以来的 `home` 时序)。终稿在社区 UI 定稿后由 UI + 本角色对齐一次(§8 拍板 4)。
后端零改动提示:`page_viewed` 白名单只校验 props **键**pageName/referrer),值级枚举由客户端编译期锁死 + 离线巡检兜底——pageName 增量**不需要动 EventDictionary**,只改 `analytics_page_name.dart` 与字典文档。
### 6.2 覆盖检查(社区页面族接线的验收 sanity)
沿 v2 §5.2/§6.3.2 框架,社区族新增三条关系式(数据侧入每日巡检):
1. **互动必有承载页**:产生过互动事件(like/favorite/comment/follow)的 session 必有 ≥1 条 `page_viewed`pageName ∈ {home, post_detail, topic_detail, user_profile})。偏差 >5% = 社区页面路由漏挂。
2. **曝光先于点进**:日 `feed_viewed.impressionCount` 总和 ≥ 日 `page_viewed(pageName=post_detail)` 条数(点进的帖子必先曝光;深链/推送入口出现前该式恒成立,破式即 `feed_viewed` 聚合逻辑漏计)。
3. **发布必经编辑器**:日 `post_publish_succeeded + post_publish_failed` ≤ 日 `page_viewed(pageName=post_form)`(发布尝试必先到达编辑器)。
---
## 7. 对账 SQL v3 增量(真值:community 事实表)
v1/v2 巡检全部继续。表名以 M3 后端 DDL 定稿为准,下文假定 `community` schema`community.posts``community.comments``community.post_likes``community.post_favorites``community.follows`),命名不同替换即可。
### 7.1 发布对账(H5 真值侧)
`post_publish_succeeded` 事件数 vs `community.posts` 当日新建行数(排除草稿态),UTC 日界,偏差 >5% 告警——结构同 v2 §6.1,替换事件名与表名即可,不重抄。评论对账同构(`comment_create_succeeded` vs `community.comments`)。
### 7.2 互动净值对账(点赞/收藏的 toggle 语义专用)
like/unlike 是幂等 toggle,事实表存的是**净状态**,逐日计数对账不成立,改对**净增量**:
```sql
-- 日 (post_liked - post_unliked) 事件净值 vs community.post_likes 当日净增行数
WITH evt AS (
SELECT date_trunc('day', server_ts AT TIME ZONE 'UTC') AS day,
count(*) FILTER (WHERE event_name = 'post_liked')
- count(*) FILTER (WHERE event_name = 'post_unliked') AS evt_net
FROM platform.product_events
WHERE event_name IN ('post_liked', 'post_unliked')
GROUP BY 1
)
SELECT e.day, e.evt_net, a.api_net,
abs(e.evt_net - a.api_net) AS diff_abs -- 相对偏差对净值无意义,看绝对差趋势
FROM evt e
JOIN (SELECT date_trunc('day', created_at AT TIME ZONE 'UTC') AS day,
count(*) AS api_net -- 若删行实现取消,需改为审计表/净增视图,DDL 定稿后校准
FROM community.post_likes GROUP BY 1) a USING (day)
ORDER BY e.day;
```
(若后端用删行实现取消点赞,`api_net` 须改从审计日志或快照差分取数——DDL 定稿后由数据侧校准,此处登记口径意图。收藏、关注同构。)
### 7.3 feed_viewed 自洽巡检(聚合事件的质量门)
聚合事件一旦逻辑有 bug,坏的是整段计数,须专设 sanity:
```sql
SELECT date_trunc('day', server_ts AT TIME ZONE 'UTC') AS day,
count(*) AS segments,
count(*) FILTER (WHERE (props->>'impressionCount')::int = 0
AND (props->>'durationMs')::int > 10000) AS zero_imp_long_stay,
-- 停留超 10s 却零曝光 = 曝光判定逻辑失效,>1% 告警
count(*) FILTER (WHERE (props->>'durationMs')::int > 1800000) AS over_cap
-- durationMs 超 30min 截断上限 = 计时暂停逻辑失效,期望恒 0
FROM platform.product_events
WHERE event_name = 'feed_viewed'
GROUP BY 1 ORDER BY 1;
```
### 7.4 巡检节奏汇总
- **每日**v1/v2 既有全部 + §7.1~7.3 + 值级泄漏扫描(v2 §6.4,社区正文是新的高风险源)。
- **每周一**:北极星 + H 假设读数(§3.2 日历);辅助指标层三项(§4)随周报发布。
---
## 8. 待拍板清单(汇总)
| # | 事项 | 选项 | 本角色裁定/建议 |
| --- | --- | --- | --- |
| 1 | 北极星是否随社区调整 | 保持 7 日回访记录率 / 引入社区复合指标 | **建议保持**,复评点 = M3 收官 + H7 读数(论证 §4);PM 否决须给替代定义式与断线处置 |
| 2 | H5~H8 判定线 | §3 各阈值 | T0 前 PM 会签一次,会签后冻结(同 H1~H4 纪律) |
| 3 | `content_rejected` 枚举 | M3 是否有内容审核环节 | 有则保留,无则从枚举删(发布/评论两处) |
| 4 | `entryPoint`/`feedTab`/pageName 终稿 | 待社区 UI 定稿收敛 | 埋点工单开工前 UI + 本角色对齐一次(含拍板 5) |
| 5 | 话题关注事件 | UI 有「关注话题」则增补 `topic_followed/unfollowed` | 按 UI 定稿定(§1.6 缺口 3) |
| 6 | 逐帖曝光路线 | 本迭代不做(§1.2 裁定);M4+ 排序实验若立项,走服务端 Feed 下发日志 | backlog 登记,届时同评分区与采样 |
| 7 | 后端限流排期 | 09 号出入 5 项之一 | 建议 M3 排入(客户端 429 分支在等它,§2.3);非本角色职权,仅登记依赖 |
| 8 | 真机补验时限 | 30 号清单 0.5 天 | **建议 09-21 前完成**(北极星首次出数的数据可信前提,§3.2 风险) |
---
## 附:M3 埋点工单拆分建议(按依赖排序)
1. **真机补验**(30 号清单,独立于开发,越早越好——拍板 8)。
2. **Flutter 队列三小修**(§2.330s 定时器、5xx 退避、anonymousId 持久化)——`lib/analytics/` 内闭环,先于社区新事件。
3. **后端**`EventDictionary` v3 增量 19 事件(§1.5 代码块可直抄,含 `experiment_exposed`+ 集成测试;可与 2 并行。
4. **Flutter**pageName 增量与收编(§6.1`analytics_page_name.dart`+ 社区页面族路由挂接。
5. **Flutter**:社区功能开发时按 §1.4 挂接(强类型封装先行;`feed_viewed` 的浏览段聚合器建议独立类 + 单测覆盖曝光判定/去重/计时暂停/30min 截断)。
6. **数据**:§7 对账 SQL 入巡检(7.2 口径待 DDL 定稿校准);§6.2 三条覆盖 sanity 随社区页面上线启用。
7. **后端**:分流组件 + 归并规则(A/B 前置 #4,§5)。
8. **本角色**:H5~H8 判定线 T0 前会签冻结(拍板 2);样本量规则 + 实验设计模板文档(前置 #3/#6)M3 中交付;每周一北极星/假设读数复核(§3.2 日历)。