# Dino English · 用户路线（打开 App 后的链路）规则

| 项 | 值 |
| --- | --- |
| 链路短码 | `flow = app_open` |
| 负责人 | @lee.winqi（Lark 显示名待填，见 §12 拍板项 ①） |
| 文档依据 | Lark《关键链路数据 & 维度定义（v0.1 草案）》**2026-08-12 版**（`W45UdP6yKoOl4mxS7gajm1POpjh`） |
| 实测依据 | BigQuery，窗口 2026-07-13 → 08-11（30 天）+ 单日 events_20260811 |
| 建立日期 | 2026-08-13 |
| 状态 | **已上线** · [health-user-route.html](./health-user-route.html)（数据 2026-08-13，7 天 / 30 天两窗口；7 月窗口数据不足已丢弃）· 已接入每日自动刷新 |

**这份文件是「用户路线」这条链路的唯一标准。**口径有分歧时以本文件为准；本文件与 Lark 文档冲突时，先改 Lark 再改这里，不要只改一边。

本文件**只管这一条链路**。其余看板的规则仍在 [`DASHBOARDS.md`](./DASHBOARDS.md)，页面做法仍在 [`PAGE-STANDARD.md`](./PAGE-STANDARD.md)，代码写法仍在 [`DEVELOPMENT.md`](./DEVELOPMENT.md)，本文件不重复、不覆盖。

---

## 0. 这条链路回答什么

> **用户打开 App 之后，实际走到了哪里；没走到的，掉在哪一段。**

Lark 已定义的六条链路（登录 / 支付 / 教室 / 崩溃性能 / 网络三方 / 语音）都是**竖着切**的：每条只看自己那一段，各自算各自的成功率。结果是「用户打开 App 到能用」这件事横跨其中五条，**没有任何一块板能完整看见它**。

举个已经发生的例子：Android 的 `signature_refresh_result` 技术失败率 80.2%（30 天 186,687 条里失败 149,727 条）。这个数已于 2026-08-14 迁进 [`health-network.html`](./health-network.html) 第 1 节（口径见 [`NETWORK-THIRDPARTY.md`](./NETWORK-THIRDPARTY.md) §3.7），那边还补了按设备口径 —— 3,836 / 8,583 台设备中过招，每台平均失败 43 次，**失败量是重试放大出来的**。

但**换成哪块板都回答不了同一个问题**：这些失败的用户后来怎么样了？被踢回登录页了？还是静默重试成功、用户根本无感？#5 能告诉你「刷不出来」，登录板能告诉你「有人在登录」，**没有一块板能把这两件事连起来**。

这正是 Lark §2.1 新增第 5 类「路径分布」要解决的问题 —— 前四类标量指标（量 / 成功率 / 耗时 / 失败归因）看不见分叉，**只会表现为「成功率低了一点」，然后无人知道为什么低**。

**本链路是 §2.1 第 5 类在「一次打开 App」这个作用域上的第一个落地场景。**

---

## 1. 边界：管什么、不管什么

划清边界是这条链路的第一要务 —— 它横跨五条现有链路，不划清就会出现同一个数在两块板上有两个值。

### 1.1 管

| 范围 | 说明 |
| --- | --- |
| 冷启动到首屏可用的**全过程** | 进程起来 → SDK 初始化 → 归因 → ID 同步 → 会话保持 → 首帧 → 首页/登录页就绪 → 路由落地 |
| 这段路的**走向** | 走了哪条路径、各路径占比、非成功路径掉在哪一步（§2.1 第 5 类） |
| **交棒点** | 用户从启动链路进入登录 / 进教室 / 支付时，本链路在交棒处结束并登记去向 |

### 1.2 不管（交给谁）

| 不管 | 归属 | 为什么 |
| --- | --- | --- |
| 登录本身两段成功率 | Lark §3.1 / 看板 #1 | 本链路只记「走到了登录页」，进了登录页之后不归它 |
| 进教室 WebView / RTC | Lark §3.3 / 看板 #3 | 同上，只记交棒 |
| 支付三段 | Lark §3.2 / 看板 #2 | 同上 |
| **崩溃 / ANR / 内存绝对值** | Lark §3.4 / 看板 #4 | `cold_start_result` 与 `page_ready_result` 两块板都用，但**口径不同**，见 §1.3 |
| 单个接口成功率、签名刷新本身的健康度 | Lark §3.5 / 看板 #5 | 本链路只关心它失败之后用户被带到了哪里 |

### 1.3 与 #4 崩溃性能的分工（最容易打架的地方）

`cold_start_result` / `page_ready_result` 这两个事件**两块板都要用**，必须说清各取什么：

| | #4 崩溃 / 性能 | 本链路（用户路线） |
| --- | --- | --- |
| 取什么 | `duration_ms` 的 **P50 / P95 分布**、按机型内存档下钻 | 该步骤**有没有发生**、发生后**去了哪一步** |
| 回答 | 「慢不慢」 | 「通没通、掉在哪」 |
| 分母 | 该事件自身量 | 一次打开 App |

**硬规矩：耗时分布只在 #4 出，本链路不出耗时排行榜；路径分布只在本链路出，#4 不画路径图。**两块板同时上线时，`cold_start_result` 的 P95 只有一个出处。

---

## 2. 链路定义

### 2.1 flow 注册（Lark §2.4 要求）

```
flow = app_open
```

低基数枚举，双端共表。与现有 `login` / `subscribe` / `enter_class` / `voice` / `deeplink` 并列。

### 2.2 step 枚举 —— 声明顺序即预期路径

Lark §2.4 要求「注册 step 枚举值，声明顺序即预期路径」。本链路 9 步：

| # | step 短码 | 对应现有 `diagnostic_id` | 备注 |
| --- | --- | --- | --- |
| 1 | `process_start` | 无独立事件 | 由 `cold_start_result` 的起点隐含，不单独埋 |
| 2 | `analytics_setup` | `analytics_setup_result` | `area=analytics`，**建立 session 的那一步，见 §4.2** |
| 3 | `sdk_init` | `sdk_init_result` | `area=third_party` |
| 4 | `first_frame` | `cold_start_result` | TTID，`area=performance` |
| 5 | `session_restore` | `auth_refresh_result` + `signature_refresh_result` | `area=auth`，会话保持 |
| 6 | `context_sync` | `app_context_sync_result` / `_summary` | `area=analytics` |
| 7 | `attribution` | `appsflyer_start_result` | `area=attribution` |
| 8 | `page_ready` | `page_ready_result` | `target=home` / `login` |
| 9 | `route` | `route_navigation_result` / `deeplink_parse_result` / `h5_webview_load_*` | `area=routing` / `deeplink` |
| — | `handoff` | 无 | 终态标记，交棒给其他 flow，见 §2.4 |

#### ⚠️ 这个顺序是实测出来的，不是按逻辑排的（2026-08-13 修订）

本文件初版把顺序排成 `sdk_init → analytics_setup → attribution → context_sync → session_restore → first_frame → page_ready`，那是**按逻辑想当然**排的。跑出路径分布后被推翻 —— iOS 实测 TOP 路径（675 个会话，占 24.9%）是：

```
analytics_setup > sdk_init > first_frame > session_restore > context_sync > attribution
```

**首帧（TTID）很早就出来了**，会话保持、ID 同步、归因全都排在首帧**之后**异步跑完。这也解释了为什么 `cold_start_result` 在两端都是零技术失败 —— 它测的是「画出第一帧」，那时候业务数据一件都还没就绪。

`analytics_setup` 排在最前，与 §4.2 的 session 观测吻合：它就是建立 session 的那一步，所以它自己有 58% 没赶上。

**Android 的前段时序无法验证**（拼不出路径，§4.2），且缺 session 的事件分布与 iOS 不同，**不要照搬这个顺序去解读 Android**。

### 2.3 预期路径

| 路径 | 走向 | 判定 |
| --- | --- | --- |
| **A · 老用户直达** | `first_frame → session_restore(success) → …→ page_ready(home)` | 预期 |
| **B · 未登录进登录页** | `first_frame → …→ page_ready(login) → handoff:login` | 预期 |
| **C · 深链 / 推送直达** | `first_frame → …→ deeplink_parse → route → handoff:enter_class` | 预期 |
| **D · 新装首启** | `first_open → …→ page_ready(login) → handoff:login` | 预期 |
| **E · 被踢回** ⚠️ | `first_frame → session_restore(tech_fail) → page_ready(login)` | **非预期**，见 §2.4 |
| **F · 没走到首屏就绪** ⚠️ | `first_frame → …（始终没有 page_ready）` | **非预期**，见 §2.5 |

### 2.5 路径 F：两成到三成的打开没走到首屏就绪（实测新增）

本文件初版没有这条路径 —— 它是**跑出路径分布后才发现的**，而且量大到不能不单列：

| | 含冷启动的会话 | 其中始终没有 `page_ready` | 占比 |
| --- | --- | --- | --- |
| **Android** | 1,865 | **407** | **21.8%** |
| **iOS** | 2,710 | **953** | **35.2%** |

iOS 侧最大的一条路径就是它 —— 675 个会话（24.9%）走的是 `analytics_setup>sdk_init>first_frame>session_restore>context_sync>attribution`，**整条路径里没有 `page_ready`**。

**⚠️ 但这个数不能直接读成「两三成的用户没进到首页」。**至少有三种解释，本文件不裁定哪一种：

1. **埋点覆盖不全**（可能性最大）：`page_ready_result` 按 Lark §3.4 **只覆盖 `home` 和 `login` 两个页面**。用户启动后落到别的页面（课程页、教室、活动页…）就没有事件，属于观测盲区，不是故障。
2. **用户在首屏就绪前就离开**：这种情况按 Lark §3.4.1 应报 `tech_fail + rendered=false`，但实测这类记录极少（Android 63 条），说明要么很少发生，要么没报上来。
3. **真的没就绪且没上报**：即故障且漏埋。

**判读纪律：页面必须把这三种解释并列写出，不得只写第三种。**要区分它们，需要 `page_ready_result` 扩展 target 覆盖（Lark §3.4 自己写着「后续按需扩到课程列表等关键页」），已记入 §12 对外推动。

### 2.4 路径 E 是这条链路存在的理由（且当前无法证实）

路径 E = 会话保持失败导致老用户被踢回登录页。它是本链路要抓的头号目标，因为：

- `signature_refresh_result` Android 技术失败率 **80.2%**、`auth_refresh_result` Android **69.6%**（30 天实测，§7.2；均已按 §5.1 规则 2 把 `rejected` 剔出分母）
- 这两个数已经大到不可能只是「静默重试」，但**今天没有任何一块板能证明它导致了用户被踢回**

**当前证不了，原因写在 [`DASHBOARDS.md`](./DASHBOARDS.md) §8 里已登记的一条待推动项：`auth_entry_source` 实际只有 `onboarding` / `welcome` / `welcome_first`，缺文档要求的「深链」与「被踢回」两类。**

**因此本文件把路径 E 定义为「假设」，不是「结论」。**在下列任一条件满足前，看板**不得**输出「N% 的用户被踢回」这类说法：

1. `auth_entry_source` 补上「被踢回」取值，或
2. `session_id` 落地（§4.1），能把 `session_restore(tech_fail)` 与后续 `page_ready(login)` 绑在同一次打开上

在此之前，看板只能并排陈列两个事实（会话保持失败率 / 登录页就绪量），**并显式标注「两者关系未经证实」**。这是硬规则，不是措辞建议。

---

## 3. 关键数据 = 5 类（Lark §2.1）

| # | 类别 | 本链路口径 | 今天能不能出 |
| --- | --- | --- | --- |
| 1 | 量 | 打开次数 = `cold_start_result` 全量（含 `rejected+background_start`，但需剔出后单列） | ✅ |
| 2 | 成功率 | 见 §3.1 | ⚠️ 分母待拍板 |
| 3 | 耗时 | **不出**，归 #4（见 §1.3） | — |
| 4 | 失败归因 | 各 step 的 `reason` 分布 | ✅ |
| 5 | **路径分布** | 见 §3.2 | ❌ 前段不可算，见 §4 |

### 3.1 主指标口径（2026-08-13 定稿 · 第二版，按双端代码核查重写）

#### 硬规则：`page_ready_result` **必须按 `target` 拆开算，禁止合并**

```
主指标 = page_ready_result(target=home) 的技术成功率
       = success ÷ (success + 失败)        -- 失败双取 fail / tech_fail
       两端各算各的，不合并

target=login  →  不进主指标。单列量，中性灰不判档（理由见下）
```

**这条是本文件最硬的一条口径，因为合并算出来的数会被样本结构操纵。**

#### 为什么不能合并：两端的 `target=login` 都记不到失败

双端代码核查结论一致 —— **`target=login` 按实现就不存在失败终态**：

| 端 | 证据 | 结论 |
| --- | --- | --- |
| Android | `LoginViewModel.kt:216-223` 的 `emitPageReady` 里 `failureReason = null` **写死**；配置接口失败走 `cachedConfig ?: fallbackLoginMethodsConfig(...)`（`LoginViewModel.kt:158-172`），照样 `Content` + `reportPageReadyOnce()` | 首帧到了就 `rendered=true` / `result=success`。提前离开只会出 `rendered=false`，`result` **仍是 success** |
| iOS | `PerfDiagnostics.reportRenderedPageReady` 首帧渲染后固定报 `result=.success` + `rendered=true`（`dino-english-ios/Common/App/PerfDiagnostics.swift:196-210`）；`login-methods` 拉失败走本地 fallback 照样渲染照样 success（同文件 128-140 行） | 同上 |

**BigQuery 实测完全吻合（30 天窗口）：**

| | `target=home` | `target=login` |
| --- | --- | --- |
| **Android** | success 1,449 · **tech_fail 63**（`timeout` 38 / `network_error` 23 / `unknown` 2）→ **95.83%** | **1,252 条全部 success，零失败** |
| **iOS** | success 2,297 · **`fail` 62**（reason 全 `unknown`）→ **97.37%** | **0 条**（见 §8 缺口 11） |

于是合并会同时犯两个错：

1. **稀释**：Android 合并算 = 2,701 ÷ 2,764 = **97.72%**，只看 home = **95.83%** —— 凭空高 **1.89pp**。而且稀释幅度**随未登录用户占比浮动**：新装用户一多，主指标自动变好看，与技术健康毫无关系。
2. **两端不可比**：Android 的分母里 **45% 是恒 100% 的 login**，iOS 是 **0%**。两个都叫「首屏渲染达成率」的数字，分母构成根本不是同一个东西，并排展示即误导。

按 [`DASHBOARDS.md`](./DASHBOARDS.md) §5.4 规则 1 与 [`PAGE-STANDARD.md`](./PAGE-STANDARD.md) §3.2，`target=login` 属**「按事件定义就记不到失败」**，与 `sdk_auth_launch_result` 同型 → **中性灰不判档，只出量**。涂绿就是虚假安全感。

#### `target=home` 才是有判别力的那个（且两端语义一致）

Android `target=home` 没有 fallback 这一说：主数据 `getUserGrowth()` 失败即 `ChatHomeState.Error` → **`tech_fail` + `rendered=false`**（`HomeViewModel.kt:785-812`）。task guide 的 `FallbackToLocalGreeting` 是另一条流，**不参与 `page_ready`**。实测 63 条失败带 `timeout` / `network_error` 归因，佐证这条路径确实会失败。

#### 名字必须叫「渲染达成率」，不能叫「可用率」

即便只看 `target=home`，这个指标也只证明「页面画出来了」，不证明「内容完整」。Lark §3.4 明写「接口失败但本地 fallback 已正常展示不算页面失败，接口健康度由 `api_result` 分析」。叫「可用率」「成功打开率」都是过度承诺，**看板文案一并适用本条**。

#### 不出「链路贯通率」（P0 阶段）

本可以做一个「打开 App → 有没有走到首屏」的贯通率，分母取 `cold_start_result`。**P0 不做**，因为分母对不上：Android 30 天 `cold_start` 2,423，而 `page_ready` 合计 2,764（home 1,512 + login 1,252），**分子比分母多 14%**。原因是 `page_ready` 会重复上报（Lark §3.4.2 iOS 栏明写「首页完成后可重新开启测量，需复核『一次页面进入只报一次』」），且登出回登录页等非冷启动路径也会触发。

**按 session 去重才能算，因此归 P1**（等 `session_id`）。P0 期间与现有三块技术健康板做法一致 —— **各 step 分段各算各的，不强行合成一个总成功率**（同 [`DASHBOARDS.md`](./DASHBOARDS.md) §6.1.1 登录板两段分算）。

#### 后台预拉起

`rejected + background_start` 不带 `duration_ms`（Lark §3.4），任何用到 `cold_start_result` 做分母的地方都必须先剔除并单列，否则预拉起多的版本会显得「打开量高、成功率低」。

### 3.1.1 上游仍未收口

该口径在 iOS 侧文档 `docs/iOS性能诊断v0.2-双端协作确认.md:39-47` **仍挂着 P0 未收口**：iOS 推荐 `page_ready_result` 只衡量页面可见就绪、接口失败健康度由 `api_result` 单独看，**但要等 Android / 数据侧确认**。

**本节的「按 target 拆开」不受收口结果影响** —— 无论上游怎么收口，`target=login` 记不到失败都是实现事实，合并算都会稀释。若收口后改变了 `target=home` 的失败判定，需同步改的只有主指标分子的 `result` 判定一处，**不影响** §4 的路径拼接与 §7 的取数结构。

**两个必须先解决的分母陷阱：**

1. **`page_ready_result` 可能一次打开报多条。**Lark §3.4.2 iOS 栏明写「首页完成后可重新开启测量，需复核『一次页面进入只报一次』」。实测 Android 单日 `page_ready_result` 1,167 条 > `cold_start_result` 992 条 —— **分子大于分母，直接相除得到 >100% 的成功率**。取数必须先按（session, target）去重，或等复核结论。
2. **后台预拉起**报 `rejected + background_start` 且不带 `duration_ms`（Lark §3.4），必须剔出分母并单列，否则预拉起多的版本会显得「打开量高、成功率低」。

### 3.2 路径分布产出方式（Lark §2.4）

Lark §2.4 已定死架构：**GA4 界面做不到，必须经 BigQuery 落一层聚合**。宽表列按 §2.4 原文，不在此重复。本链路补两条约定：

- `path` 的**正式**写法是 `STRING_AGG(step, '>' ORDER BY step_seq)`。`step_seq` 待建，降级期只能退而按 `event_timestamp` 排序 —— Lark §2.4 明写时钟偏差与同毫秒并发会给出错乱路径，代价与适用范围见 §4.4。**降级排序不得固化进宽表定义**，宽表按 `step_seq` 写，落地前该列留空。
- 路径长尾按 §2.4 取 **TOP 20 + 其余归「其他」**，且**必须显式标注被折叠的比例**。

---

## 4. ID 与作用域 —— 本链路最大的约束

**这一节是本文件的核心。做开发前必须读完。**

### 4.1 正式口径要的两个 ID，线上都还没有

| ID | Lark 定义 | 现状 |
| --- | --- | --- |
| `session_id` | 一次打开 App；冷启动生成，退后台超 N 秒回前台换新（N 建议 30 秒） | **待建**（Lark §2.3） |
| `trace_id` | 一次业务链路执行 | **待建**（Lark §2.4）；语音链路已有实现，作用域限于一次 voice turn |

实测 events_20260811：`app_diagnostic` 带 `trace_id` 的只有 Android 15,393 / iOS 3,361 条 —— 与 Lark §2.5 记载的「`trace_id` 已在约 1.5 万条事件上携带（语音链路）」吻合。**启动链路的 9 个事件没有一个携带 `trace_id`。**

**结论：本链路的正式口径（第 5 类路径分布）在 `session_id` 落地前做不了。**

### 4.2 降级口径：能不能用 `ga_session_id` 顶一下？—— Android 不能

唯一现成的会话 ID 是 Firebase 自动采集的 `ga_session_id`。实测结果是**Android 不能用，iOS 可以有条件地用**。

**实测（events_20260811，`app_diagnostic` 事件）：**

| | 事件数 | 带 `ga_session_id` | 覆盖率 |
| --- | --- | --- | --- |
| Android | 72,112 | 45,143 | **62.6%** |
| iOS | 20,735 | 20,043 | **96.7%** |

丢的不是随机的一批，**恰恰是启动最早的那几步**：

| Android 事件 | 缺 `ga_session_id` | 所属 step |
| --- | --- | --- |
| `auth_refresh_result` | **68.3%** | 6 session_restore |
| `signature_refresh_result` | **64.2%** | 6 session_restore |
| `appsflyer_start_result` | **58.4%** | 4 attribution_start |
| `analytics_setup_result` | **58.3%** | 3 analytics_setup |
| `sdk_init_result` | **47.7%** | 2 sdk_init |
| `api_result` | 37.9% | （跨段） |
| ⎯⎯ **分界线** ⎯⎯ | | |
| `app_context_sync_summary` | 2.7% | 5 context_sync |
| `app_context_sync_result` | 0.2% | 5 context_sync |
| `cold_start_result` | **0%** | 7 first_frame |
| `page_ready_result` | **0%** | 8 page_ready |
| `mem_snapshot` / `h5_webview_load_success` / `route_navigation_result` / `deeplink_parse_result` | **0%** | 7–9 |

#### 判据是「哪个事件」，不是「哪个时间段」（2026-08-13 修订）

初版把这件事解释成一条时间分界线（「在 `analytics_setup` 跑完之前上报的事件拿不到 session」）。**跑出完整数据后这个解释站不住**：`context_sync` 在时序上排得很后（step 6）却几乎不缺（0.2%），`session_restore` 同样排在首帧之后却缺 64–68%。缺失与时间先后没有对应关系。

**数据支持的说法是：特定的四个事件大量不带 session。**

| Android | 缺 `ga_session_id` | 能拼路径？ |
| --- | --- | --- |
| `first_frame` / `page_ready` / `route` | **0%** | ✅ |
| `context_sync` | 0.2%（`_summary` 2.7%） | ✅ |
| `sdk_init` | 47.7% | ❌ |
| `analytics_setup` | 58.3% | ❌ |
| `attribution` | 58.4% | ❌ |
| `session_restore` | 64.2% / 68.3% | ❌ |

**为什么是这四个：它们根本不只在启动时触发。**把量跟冷启动次数一比就看出来了（30 天，Android）：

| 事件 | 量 | ÷ 冷启动 2,423 次 |
| --- | --- | --- |
| `signature_refresh_result` | 186,687 | **77 倍** |
| `sdk_init_result` | 89,238 | **37 倍** |
| `appsflyer_start_result` | 36,377 | 15 倍 |
| `analytics_setup_result` | 36,179 | 15 倍 |

一次打开 App 不可能刷 77 次签名。这些事件大量发生在**没有前台会话的时候**（后台任务、请求驱动的重试等），自然带不到 `ga_session_id`。**具体机制需 Android 侧确认**（§12 已登记）—— 本文件只写数据能支持的部分。

**硬约束（取数脚本按此实现）：**

> **Android 只能用 `first_frame` / `context_sync` / `page_ready` / `route` 四步拼路径，另四步拼不了。**
> **iOS 全段可拼。**

### 4.3 两端的失真模式不同，危险程度也不同

这一点决定了降级期看板能说什么话：

| | 失真模式 | 后果 | 危险度 |
| --- | --- | --- | --- |
| **iOS** | 整条链路一起缺（各事件缺失率齐刷刷 1.6–5.8%，是「某些启动整体没有 session」） | 丢掉的是**完整的会话**，拼出来的路径本身是完整的 | 低 —— 分母偏小，路径可信 |
| **Android** | 链路**缺四步**（那四个事件缺 48–68%，另四步 0%） | 拼出来的路径是**残的** —— 实测只有 24 种，而 iOS 有 139 种 | **高 —— 正是 Lark §2.4 明确警告的「大量并不存在的短路径」** |

**降级期硬规则（三条，不得违反）：**

1. **Android 侧禁止输出含那四个缺 session 事件的路径串。**只能出 `first_frame` / `context_sync` / `page_ready` / `route` 四步的路径，并在图上标明「本图缺 SDK 初始化、分析初始化、会话保持、归因四步，它们在 Android 上不带 session」。
2. **两端的路径分布不得放进同一个分母。**这与项目一贯的「一页两列、各算各的、绝不相加」一致（[`README.md`](./README.md)）。Android 24 种 vs iOS 139 种，差异主要来自可拼步数不同，**不是行为差异**。
3. **缺 session 的四步在两端都只出标量指标**（量 / 成功率 / 失败归因，即第 1、2、4 类），不出路径。标量不需要 session，按事件名聚合即可，不受本节约束。

### 4.3.1 路径分母：一次「含冷启动的会话」

拼路径要先定分母，否则数字没有意义。**本链路的路径分母 = 含 `first_frame` 的 GA 会话**，不含冷启动的会话一律排除。

这条不是洁癖，是被数据逼出来的：Android 侧若不排除，**有 27,474 个会话只含一条 `context_sync`**，占比七成，真正的启动路径会被整个淹掉。那些会话是前台恢复、或 App 已经开着时的后台活动，**本来就不是「一次打开 App」**。

被排除的数量记在数据文件的 `paths.<平台>.noOpen` 里，**页面必须显示，不许闷掉**。

**另外两条判读纪律：**

- **会话数 ≠ 冷启动次数。**实测 Android 1,865 个会话对应 2,423 次冷启动（1.30 次/会话），iOS 2,710 对 3,416（1.26）。因为 GA4 按「30 分钟无活动」切会话（Lark §2.3），**切后台五分钟再回来仍算同一个会话**，期间的多次冷启动会并进同一条路径。这正是 `session_id` 待建的原因，路径口径因此只能算降级。
- **长尾折叠比例必须显示。**实测 TOP 20 覆盖 Android 99.8%、iOS 90.2% —— iOS 有近一成落在长尾里，不标出来会被读成「已覆盖全部路径」（Lark §2.4 明令）。

### 4.4 降级期用什么排序

`step_seq` 待建。降级期在 **iOS + Android 后段**内按 `event_timestamp` 排序，但必须接受两个已知失真并在页面标注：

- 同毫秒并发事件定序不稳定
- 客户端时钟偏差（跨设备聚合时不影响，因为只在单个 session 内排序）

**这是降级期的临时做法，`step_seq` 落地后立即废弃。**不要把它固化进宽表定义。

---

## 5. 终态四分类与折算（Lark §2.2）

本链路各 step 的终态一律折算为四分类 `success` / `tech_fail` / `user_abort` / `rejected`。

### 5.1 词表哨兵实测（2026-08-13，窗口 07-13 → 08-11）

**这张表是本链路取数的依据，写 SQL 前必须对照。**结论与 [`DASHBOARDS.md`](./DASHBOARDS.md) §3.3 一致：**Android 基本已迁到四分类，iOS 大面积仍报旧词 `fail`**。

| step | 事件 | Android 的 `result` 取值 | iOS 的 `result` 取值 | 用词一致？ |
| --- | --- | --- | --- | --- |
| 2 | `sdk_init_result` | success 88,300 · **tech_fail** 938 | success 25,578（无失败） | — |
| 3 | `analytics_setup_result` | success 36,179（无失败） | success 6,325（无失败） | — |
| 4 | `appsflyer_start_result` | success 36,377（无失败） | success 21,696 · **`skip` 52** | — |
| 5 | `app_context_sync_result` | success 10,032 · **`fail`** 682 | success 11,522 · **`fail`** 219 | ✅ 两端同旧词 |
| 5 | `app_context_sync_summary` | success 57,367 · **`fail`** 6,955 | success 5,328 · **`fail`** 120 | ✅ 两端同旧词 |
| 6 | `auth_refresh_result` | success 3,689 · **tech_fail** 8,459 · **rejected 1,132** | success 1,394 · **`fail`** 840 | ❌ **不一致** |
| 6 | `signature_refresh_result` | success 36,960 · **tech_fail** 149,715 · **`fail` 12** ⚠️ | success 5,808 · **`fail`** 7,444 | ❌ **不一致，且 Android 自己两套词并存** |
| 7 | `cold_start_result` | success 2,348 · **rejected 75** | success 3,416（无失败） | — |
| 8 | `page_ready_result` | success 2,701 · **tech_fail** 63 | success 2,297 · **`fail`** 62 | ❌ **不一致** |
| 9 | `deeplink_parse_result` | success 15 · **`fail`** 2 | success 3,264 · **`fail`** 4 | ✅ 两端同旧词 |
| 9 | `h5_webview_load_success` | **`result` 全为空** 2,483 | **`result` 全为空** 911 | ✅ 两端同为空 |
| 9 | `h5_webview_load_failed` | **tech_fail** 54 | **tech_fail** 33 | ✅ 两端同新词 |
| 9 | `route_navigation_result` | **`fail`** 29（无 success） | **`fail`** 94（无 success） | ✅ 两端同旧词 |

**由此得出四条取数硬规则：**

1. **失败一律双取 `r IN ('fail','tech_fail')`。**本链路 9 个事件里有 3 个两端用词不一致（`auth_refresh` / `signature_refresh` / `page_ready`），只认一个词会让 iOS 的失败**凭空消失且不报错**。
2. **`rejected` 与 `skip` 不进技术失败率分母**（Lark §2.2）。本链路两处踩这个坑：`auth_refresh_result` Android 有 **1,132 条 rejected**、`cold_start_result` Android 有 **75 条 rejected**、`appsflyer_start_result` iOS 有 **52 条 skip**。用 `失败 ÷ 全量` 会把失败率**算低**。正确写法：`tech_fail ÷ (success + tech_fail)`。
3. **`signature_refresh_result` 的 Android 侧同时存在 `tech_fail` 149,715 与 `fail` 12** —— 同一端两套词并存（迁移期残留）。这恰好证明第 1 条不是防御性写法，而是必需。
4. **`h5_webview_load_*` 的 `result` 为空，判成败只能看事件名。**

### 5.2 存量点位与折算

| step | 现状 | 折算 |
| --- | --- | --- |
| 2 `sdk_init`、6 `auth_refresh`、7 `cold_start`、8 `page_ready`（**仅 Android**） | 已直报四分类 | 直接用 |
| 5 `context_sync`、9 `deeplink_parse`、9 `route_navigation`（两端）、6 与 8 的 **iOS 侧** | 仍报旧值 `fail` | 按 Lark §2.2 折算表 |
| 9 `route` 的 `h5_webview_load_success` / `_failed` | **`result` 字段为空**，成败由 `diagnostic_id` 自身表达 | 看事件名，见下 |
| 9 `route` 的 `route_navigation_result` | **只在失败时上报**，无 `success` 记录 | 无分母，见下 |

**第 9 步这两行是本链路特有的坑，且是两个不同的坑，不要混为一谈：**

- `h5_webview_load_success` / `_failed`：与看板 #3 进教室同型（[`DASHBOARDS.md`](./DASHBOARDS.md) §6.1.3）—— 实测 `result` 全为空，**判成败必须看事件名**。写 `result='success'` 会把这一段整体算成 0。
- `route_navigation_result`：实测 Android 29 条、iOS 94 条**全部是失败态，零成功记录**。这是「分子恒 0」的典型，按 [`DASHBOARDS.md`](./DASHBOARDS.md) §5.4 规则 2 **只出失败量、成功率显示 `—`**，照算会得到「0.00% 危险」，是假的。

### 5.3 `fail` 折算成 `tech_fail` 还是 `rejected`：本链路的裁定

Lark §2.2 规定 `fail` 拆 `tech_fail` / `rejected` 依赖 `failure_reason`，而**该字段 98.8% 集中在 `api_result`**，本链路 9 个事件一个都没有。

**若照字面执行「无 `failure_reason` 一律落未分类」，后果是 iOS 侧整块数据消失** —— iOS 的失败全部报旧词 `fail`（§5.1），落进未分类就等于 `page_ready` 的 97.37%、`auth_refresh` 的 37.6%、`signature_refresh` 的 56.2% 全部算不出来。这不是谨慎，是把一端的数据丢光。

**因此本链路作如下裁定（三档，按证据强度排）：**

| 档 | 适用事件 | 折算 | 依据 |
| --- | --- | --- | --- |
| **A · 有双端对照** | `auth_refresh_result`、`signature_refresh_result`、`page_ready_result` 的 **iOS 侧 `fail`** | **按 `tech_fail` 处理** | 同一 `diagnostic_id` 的 Android 侧报 `tech_fail`，而 Lark §2.6 第一层要求「字段名、枚举取值双端逐字一致」—— 两端语义必须相同，差异只是 iOS 未改报新词 |
| **B · 语义明确但无对照** | `app_context_sync_result` / `_summary`、`deeplink_parse_result`、`route_navigation_result`（**两端**都报 `fail`） | **按 `tech_fail` 处理，但页面标注「推定」** | 这些事件的失败语义本身就是技术错误（同步失败 / 解析失败 / 导航失败），不存在「业务拒绝」的分支 |
| **C · 无法判定** | 出现表外新词，或将来新增带 `failure_reason=business` 的点位 | **落「未分类」，单列不进分母** | Lark §2.2 |

**硬约束：A / B 两档的折算必须在页面「这页有哪些坑」里写明是折算结果，不得显示成原生四分类。**若日后各事件补上 `failure_reason`，本节应整节废弃、回到 Lark §2.2 的自动判定。

#### A 档的补强依据：iOS 侧代码已改，只是未上线

2026-08-13 从 iOS 侧了解到，**`fail` → 四分类的改造已经做完，尚未发版**。这把 A 档从「推定」提升为「已知」：两端语义本来就一致，线上差异纯粹是版本滞后。

### 5.4 ⚠️ iOS 改词上线时，失败率的跳变**不能**读成「修好了」

这是本链路**上线后第一个会踩的坑**，提前写死在这里。

iOS 发版后，同一个 `diagnostic_id` 会出现**新旧版本并存**：旧版本继续报 `fail`，新版本报 `tech_fail`。届时有两种可能，**看板必须能区分**：

| 情况 | 现象 | 该怎么读 |
| --- | --- | --- |
| **只改了词** | iOS 失败率**不变**，只是 `fail` 换成 `tech_fail` | 正常，双取规则（§5.1 规则 1）自动覆盖，无需任何改动 |
| **同时改了归类** ⚠️ | iOS 失败率**突然下降** | **大概率不是修好了，是分母变了** —— 原来记成 `fail` 的一部分被重新归类为 `rejected` / `user_abort`，而这两类按 §5.1 规则 2 不进技术失败率分母 |

**佐证这不是杞人忧天**：`auth_refresh_result` 的 Android 侧有 **1,132 条 `rejected`**（占该事件 8.5%），iOS 侧**一条都没有**。iOS 目前把这些情况归到哪里未知 —— 若上线后它们变成 `rejected`，iOS 的技术失败率会从 37.6% 自动降到更低，**纯属重新分类**。

**三条硬规则：**

1. **iOS 改词上线后的第一个窗口，必须按 `app_info.version` 拆开看**，不得只看汇总（同项目既有做法：技术健康板的「按版本档对照」块，**看修复生效不过滤旧版本，新旧对着看**）。
2. **失败率下降时，先查 `result` 取值分布有没有新增 `rejected` / `user_abort`**，再下「改善」的结论。分布变了就不是改善。
3. **切换窗口内禁止算环比**，理由同 §7.3。

发版前应向 iOS 确认一句：**这次是只换枚举名，还是同时调整了归类？**答案决定上面走哪一行。已记入 §12 对外推动。

另有一条继承自项目既有规则，只登记适用：**失败词双取** `r IN ('fail','tech_fail')`，实测依据见 §5.1（[`DASHBOARDS.md`](./DASHBOARDS.md) §3.3）。

---

## 6. 公共维度

按 Lark §2.3 四层 ID + 公共维度，本链路适用：

| 维度 | 取法 | 注意 |
| --- | --- | --- |
| 平台 | `LOWER(platform)` | 大小写不统一，必须 `LOWER()` |
| 版本 | `app_info.version` | 按版本档对照是项目既有做法 |
| 是否登录 | **`user_id` 标准列** | ⚠️ **不能用 `uid` 参数判 `IS NOT NULL`** —— 未登录时上报字符串 `"null"`（Lark §2.3 实测 27% 为 `"null"`） |
| 内存档 | `mem_tier` | 仅 step 7/8 携带 |
| 冷启动来源 | `source=cold_start` | |

---

## 7. 取数条件与实测量

### 7.1 成本闸门

**完全沿用 [`DASHBOARDS.md`](./DASHBOARDS.md) §4.4，不放宽任何一条**：必带日期条件、单次窗口 ≤365 天、禁 `SELECT *`、不确定先 dry-run（>10 GiB 收窄）。

本链路涉及事件多、跨 5 个 `diagnostic_area`，**取数脚本必须一次查询取全所有 step，不要每段查一次** —— `events_*` 的 `event_params` 是大列，查 9 次就是 9 倍账单。参考实测：30 天全量扫描约 1.4 GB。

### 7.2 各 step 实测量（窗口 2026-07-13 → 08-11，30 天）

| step | 事件 | Android | iOS |
| --- | --- | --- | --- |
| 2 `sdk_init` | `sdk_init_result` | 89,238 · 失败 938 | 25,578 · **零失败** ⚠️ |
| 4 `attribution_start` | `appsflyer_start_result` | 36,377 · **零失败** ⚠️ | 21,696 · **零失败** ⚠️（另 52 条 `skip` 已剔出） |
| 5 `context_sync` | `app_context_sync_result` | 10,714 · 失败 682 | 11,741 · 失败 219 |
| 6 `session_restore` | `auth_refresh_result` | 12,148 · **失败 8,459 → 69.6%**（另 1,132 条 `rejected` 已剔出分母） | 2,234 · 失败 840 → 37.6%（**无 `rejected`，见 §5.4**） |
| 6 `session_restore` | `signature_refresh_result` | 186,687 · **失败 80.2%** | 13,252 · 失败 56.2% |
| 7 `first_frame` | `cold_start_result` | 2,423 · **零技术失败** ⚠️（另 75 条 `rejected`） | 3,416 · **零技术失败** ⚠️ |
| 8 `page_ready` · `target=home` | `page_ready_result` | 1,512 · 失败 63 → **95.83%** | 2,359 · 失败 62 → **97.37%** |
| 8 `page_ready` · `target=login` | `page_ready_result` | 1,252 · **零失败** ⚠️ 不判档 | **0 条** ⚠️ |
| 9 `route` | `deeplink_parse_result` | **17 条** ⚠️ | 3,268 |
| 9 `route` | `route_navigation_result` | 29（全失败） | 94（全失败） |
| 9 `route` | `h5_webview_load_success` | 2,483 | 911 |

step 3 `analytics_setup_result` 未单独取 30 天量（单日 20260811：Android 3,664 / iOS 378）—— 它在本链路里主要作为 §4.2 的 **session 边界证据**使用，不作为独立指标出数。

### 7.3 首报日：本链路凑不满 30 天

按 [`DASHBOARDS.md`](./DASHBOARDS.md) §4.2 的硬规矩，**窗口标事件自己的首报日，不是查询区间**。本链路关键事件首报：

| 事件 | Android 首报 | iOS 首报 |
| --- | --- | --- |
| `cold_start_result` / `page_ready_result` | 07-27 | 07-28 |
| `h5_webview_load_success` | 08-07 | 08-07 |
| `auth_refresh_result` | 07-30 | 07-17 |

**到 2026-08-11 为止，step 7/8 只有 15 天数据，step 9 的 H5 只有 5 天。因此本链路首版禁止算环比**（同 #1 登录板的处理）。

---

## 8. 已知缺口与不可信项

上线首版时**必须**在页面显式列出，不得静默出数：

| # | 缺口 | 影响 | 处理 |
| --- | --- | --- | --- |
| 1 | **`session_id` / `step_seq` 未建** | 第 5 类路径分布无正式口径 | 降级方案见 §4，页面标「降级口径」 |
| 2 | **Android 前段无 session** | step 1–6 不可拼路径 | §4.3 三条硬规则 |
| 3 | **`auth_entry_source` 缺「被踢回」** | 路径 E 无法证实 | §2.4，只陈列不下结论 |
| 4 | **零失败的三处**：`cold_start_result` 两端、`appsflyer_start_result` 两端、iOS `sdk_init_result` 均零技术失败 | 无判别力，涂绿是虚假安全感 | **中性灰不判档**；判据用失败率不用「恰好等于 0」（[`PAGE-STANDARD.md`](./PAGE-STANDARD.md) §3.2、[`DASHBOARDS.md`](./DASHBOARDS.md) §5.4 规则 1） |
| 5 | **Android `deeplink_parse_result` 仅 17 条**（iOS 3,268） | 路径 C 在 Android 上基本不可见 | 判为疑似漏埋，页面标注，**不得**解读为「Android 用户不走深链」 |
| 6 | `page_ready_result` 可能一次打开多条 | 分子 > 分母 | §3.1 陷阱 1 |
| 7 | 崩溃 / ANR 在 Crashlytics，未入 BQ | 「打开后崩了」这条路径看不见 | 页面标缺口 |
| 8 | `route_navigation_result` 只在失败时上报 | 无分母，不能算成功率 | 只出失败量，成功率显示 `—`，**不得算成 0.00%** |
| 9 | **fallback 降级路径不可见**：`login-methods` 拉失败、登录页用本地兜底列表时，两端都照报 `success + rendered=true`（§3.1） | 「进了登录页，但登录方式是兜底的」这条**真实存在的降级路径**在路径分布里看不见；主指标也不会因此下降 | 页面必须标注主指标是「渲染达成」不是「可用」；想看这条路径需 join `api_result` 的 `login-methods` 失败量，**属 P1**，P0 不做 |
| 10 | **`target=login` 两端都记不到失败**（Android `failureReason=null` 写死；iOS 首帧即 success），实测 Android 1,252 条零失败 | 合并进主指标会稀释 `target=home` 的失败率，且稀释幅度随未登录占比浮动 | **按 target 拆开算，`login` 中性灰不判档**（§3.1 硬规则） |
| 11 | **iOS 线上没有 `target=login` 数据**：30 天 0 条，而 Lark §3.4.2 写着「两个登录 VC 均已接入」 | 代码已落地 ≠ 线上已生效。iOS 侧登录页就绪完全不可观测 | 页面标「iOS 无数据」，**不得**显示为 0% 或留空；需向 iOS 确认是未触发还是未上报 |
| 12 | **iOS 的 `rendered` 取不到、`reason` 无判别力**：实测 iOS `page_ready_result` 的 `rendered` 全为空，62 条失败的 `reason` **全部是 `unknown`**（Android 能分 `timeout` 38 / `network_error` 23） | iOS 侧「慢在网络还是渲染」答不了，失败归因（第 4 类关键数据）在 iOS 上等于没有 | 归因表按端分开出，iOS 侧显式标「归因缺失」，**不得**把两端 reason 合并成一张分布图 |
| 13 | **`target=login` 口径上游 P0 未收口** | 主指标失败判定可能变 | §3.1.1；「按 target 拆开」这条不受收口影响 |
| 14 | **iOS 的 `fail` → 四分类改造已完成、待发版** | 上线后新旧版本并存；若同时调整了归类，iOS 失败率会自动下降而**并非修复生效** | §5.4 三条硬规则：按版本拆开看、先查 `result` 分布再下结论、切换窗口不算环比 |
| 15 | **路径 F：两三成的打开没走到首屏就绪**（Android 21.8% / iOS 35.2%） | 可能是埋点覆盖不全（`page_ready` 只覆盖 home/login）、也可能是真掉了，**当前分不清** | §2.5 三种解释必须并列写出，**不得只写「故障」那一种** |
| 16 | **路径分母是 GA 会话，不是「一次打开」** | GA4 按 30 分钟无活动切会话，一个会话可能含多次冷启动（实测 1.26~1.30 次/会话） | §4.3.1；页面标「降级口径」，等 `session_id` 落地才准 |
| 17 | **「7 月」窗口整个不可用** | `cold_start_result` 首报 07-27，全月只有 4~5 天数据，Android 仅 12 条 | 取数脚本按 `MIN_CORE=200` 直接丢弃该窗口，页面上不出这个按钮 |

---

## 9. 判读与着色

**分档判据沿用 [`DASHBOARDS.md`](./DASHBOARDS.md) §5.4，页面呈现沿用 [`PAGE-STANDARD.md`](./PAGE-STANDARD.md) §3.2。本链路不新增任何配色规则。**三条重点适用：

> 注：下面的「D §5.4」指 [`DASHBOARDS.md`](./DASHBOARDS.md) 的 §5.4，与本文件的 §5.4（iOS 改词跳变）不是一回事。

- 失败少到不可信 → **中性灰不判档**（D §5.4 规则 1）。判据用**失败率配样本量下限**，不用「失败数恰好等于 0」—— 这条是登录板踩过的坑：一条迟到的失败记录就能把灰色的「不可信」翻成绿色的「健康」（[`PAGE-STANDARD.md`](./PAGE-STANDARD.md) §3.2）
- 分子恒 0 / 无成功记录的段 → 显示 `—`，**不算 0.00%**（D §5.4 规则 2，本链路适用于 `route_navigation_result`）
- **路径占比一律不上健康分档色**（D §5.4 规则 3：只有技术成功率能上分档色）。路径 A 占比高不等于「好」，A/B/C/D 都是预期路径，涂色即误导。路径 E 与「其他」可用**中性警示标记**提示需关注，但不得使用红/琥珀/青三档色

---

## 10. 完成条件（DoD）

分两期。**P0 不等任何埋点改动，今天就能做；P1 等 `session_id`。**

### P0 · 降级版（可立即开发）

1. 前段 step 1–6：两端各出量 / 成功率 / 失败归因（标量，不需 session）
2. 后段 step 7–9：两端各出量 / 成功率 / 失败归因 + **iOS 全段、Android 后段**的路径分布
3. 页面显式标注：降级口径、§8 全部**十七项**缺口、各事件自己的首报日
4. 不出环比（§7.3）、不出耗时排行（§1.3）、不下路径 E 的结论（§2.4）
5. 数据脚本化为 `tools/gen-user-route-data.py`，窗口参数 `from win import args, windows`，与现有五个脚本写法一致
6. 过一份 `user-route_debug.html` 自检页，全 PASS

### P1 · 正式版（等 `session_id` + `step_seq` 落地）

1. 按 Lark §2.4 建 trace 事实宽表，出完整 9 步路径分布与桑基图
2. 路径 E 转为可证实指标
3. 废弃 §4.4 的时间戳排序

### 10.1 验收清单

沿用 [`DASHBOARDS.md`](./DASHBOARDS.md) §7 的九条通用清单，**另加本链路四条**：

- [ ] Android 的路径图**没有**跨越 §4.2 分界线
- [ ] 两端路径分布**没有**放进同一个分母
- [ ] 路径 E 的措辞是「两者关系未经证实」，不是因果句
- [ ] **`page_ready_result` 已按 `target` 拆开**，页面上**没有任何一处**把 `home` 与 `login` 合并计算（§3.1 硬规则）
- [ ] `target=login` 是中性灰、不判档，且 iOS 侧显示「无数据」而非 0%（§8 缺口 10、11）
- [ ] 主指标在页面上叫「首屏**渲染达成**率」，**没有**出现「可用率」「成功打开率」这类过度承诺的写法（§3.1）
- [ ] 失败量已双取 `fail` / `tech_fail`（§5.1）；`rejected` / `skip` 已剔出技术失败率分母（§5.1 规则 2，本链路三处踩坑）
- [ ] `fail` → `tech_fail` 的折算已按 §5.3 三档裁定，且页面坑清单里写明了哪些是**折算结果**而非原生四分类
- [ ] 归因（reason）分布按端分开，**没有**把 iOS 的 62 条 `unknown` 与 Android 的 `timeout`/`network_error` 合并成一张图（§8 缺口 12）
- [ ] P0 **没有**输出「链路贯通率」（分母对不上，归 P1，§3.1）

---

## 11. Lark §2.4「新增链路检查清单」逐条应答

Lark 文档强制要求接新链路时逐项核对，**本节即本链路的应答**：

| # | 检查项 | 本链路应答 |
| --- | --- | --- |
| 1 | 注册 `flow` 枚举值（双端共表） | `app_open`。**待双端登记**（拍板项 ③） |
| 2 | 注册 `step` 枚举值，声明顺序即预期路径 | §2.2 九步已定义，顺序即路径。**待双端登记** |
| 3 | 专属字段 ≤ 5？ | ✅ **0 个**。本链路完全复用现有事件字段，不新增专属字段 |
| 4 | 估算「日触发次数 × 步骤数」，选宽事件级 / 步骤级 | 实测 **约 1,340 次/天**（20260811：Android 992 + iOS 346 次冷启动）**× 9 步 ≈ 1.2 万**，低于 5 万阈值 → **步骤级**。且事件**已经在报**，本链路是复用而非新增，**事件量净增 0** |
| 5 | 定 TTL 超时阈值 | 建议 **60 秒**（冷启动到首屏就绪的合理上界；实测 `cold_start_result` >15,000ms 已被客户端静默丢弃）。**待拍板**（拍板项 ④） |
| 6 | 定成功采样率 | **全量不采样**。理由同第 4 条：量级只有 1 万/天，采样收益为零、失真风险为实 |
| 7 | 采样按 `trace_id` 哈希，不得逐事件随机 | 不适用（不采样）。若日后放量需采样，按 Lark §2.4 一致性哈希，**禁止逐事件随机** |

**第 4 条值得单独说**：本链路是目前唯一「事件量净增 0」的新链路 —— 它不新埋任何事件，只是把已经在报的 9 类事件按路径重新组织。Lark §2.4 担心的配额触顶问题（21.4 万/天、配额 100 万），本链路不贡献任何增量。这也是它应该优先做的理由。

---

## 12. 待拍板（负责人：@lee.winqi）

| # | 事项 | 卡不卡开发 |
| --- | --- | --- |
| ① | 负责人在 Lark 文档里的显示名（本文件暂写 `@lee.winqi`） | 不卡 |
| ② | ~~主指标分母口径~~ **已定（2026-08-13，双端代码核查后重写）**：`page_ready_result` **按 `target` 拆开算、禁止合并**；`target=home` 为主指标，`target=login` 两端都记不到失败 → 中性灰不判档。指标名为「首屏**渲染达成**率」。详见 §3.1 | ~~卡 P0~~ 已解锁 |
| ③ | `flow=app_open` 与 9 个 `step` 短码，要不要同步到 Lark 双端共表 | 不卡 P0，卡 P1 |
| ④ | TTL 超时阈值（§11 建议 60 秒） | 不卡 P0，卡 P1 |
| ⑤ | 本文件要不要挂进 [`DASHBOARDS.md`](./DASHBOARDS.md) §6 看板清单、[`README.md`](./README.md) 页面表与 [`index.html`](./index.html) 导航（三处各加一行）。**本次未动这三份文件**，等你点头 | 不卡 |

**需要对外推动的两件事**（不属于本看板开发，但决定 P1 能否做）：

| 事项 | 影响 | 找谁 |
| --- | --- | --- |
| **`session_id` 落地**（Lark §2.3「待建」） | 第 5 类路径分布无正式口径；Android 前段永远拼不起来 | 双端 |
| **Android 启动前段拿不到 `ga_session_id`** | 本文件 §4.2 实测：`sdk_init` 48% / `signature_refresh` 64% / `auth_refresh` 68% 无 session | Android |
| **`page_ready_result` 的 `target=login` 口径收口**（iOS 侧 `docs/iOS性能诊断v0.2-双端协作确认.md:39-47` 挂 P0 未收口） | 主指标失败判定可能变（§3.1.1） | Android / 数据侧 |
| **iOS 的 `target=login` 线上 0 条**（30 天） | Lark §3.4.2 写着「两个登录 VC 均已接入」，但线上无数据 —— 是没触发还是没上报？iOS 登录页就绪完全不可观测 | iOS |
| **iOS 的 `page_ready_result` 缺 `rendered`、`reason` 恒 `unknown`** | 「慢在网络还是渲染」在 iOS 上答不了；失败归因等于没有（Android 有 `timeout`/`network_error`） | iOS |
| **`page_ready_result` 扩展 target 覆盖**（Lark §3.4 自己写着「后续按需扩到课程列表等关键页」） | 不扩就永远分不清路径 F（Android 21.8% / iOS 35.2% 没走到首屏）是观测盲区还是真掉了 | 双端 / PM |
| **确认 Android 那四个事件为什么不带 `ga_session_id`**（`sdk_init` 48% / `analytics_setup` 58% / `attribution` 58% / `session_restore` 64~68%） | Android 的路径只能拼四步，另四步永远看不见。实测这些事件量是冷启动的 15~77 倍，**不只在启动时触发** | Android |
| **`target=login` 两端都记不到失败**（Android `failureReason=null` 写死 / iOS 首帧即 success） | 登录页就绪永远 100%，无判别力。若产品需要衡量登录页健康，需补真实失败终态 | 双端 / PM |
| ~~iOS 把失败统一改标 `tech_fail`~~ **已改完，待发版**（2026-08-13 获知） | 上线后新旧版本并存，失败率可能跳变 —— 见 §5.4 三条硬规则 | iOS |
| **发版前问清：这次是只换枚举名，还是同时调整了归类？** | 决定 §5.4 走哪一行。若同时改归类，iOS 失败率会自动下降但**不是修好了**（`auth_refresh` 的 Android 有 1,132 条 `rejected`，iOS 一条没有） | iOS |
| **Android `signature_refresh_result` 两套词并存**（`tech_fail` 149,715 + `fail` 12） | 迁移期残留，量小但说明改报未彻底 | Android |

**已核销**：~~核对 Android 的 `page_ready_result` 在 fallback 时是否同样报 success~~ —— 2026-08-13 已核，两端行为一致（都报 success），且经 BigQuery 实测佐证。

---

## 附：本文件的实测证据

| 证据 | 查询窗口 | 用在哪 |
| --- | --- | --- |
| 各 `diagnostic_id` × platform 量 / 失败数 / 首末报日 | 20260713 → 20260811 | §7.2、§7.3 |
| `ga_session_id` 覆盖率（两端汇总） | events_20260811 | §4.2 |
| `ga_session_id` 缺失率按 `diagnostic_id` 拆分 | events_20260811 | §4.2 分界线 |
| `trace_id` 携带量 | events_20260811 | §4.1 |
| `page_ready_result` 按 `target` × `result` × `rendered` × `reason` 拆分 | 20260713 → 20260811 | §3.1、§8 缺口 10–12 |
| **词表哨兵**：本链路 13 个事件的 `result` 取值 × platform | 20260713 → 20260811 | §5.1 |

全部经 BigQuery 实跑，非文档推断。重跑时注意日表回填（[`DASHBOARDS.md`](./DASHBOARDS.md) §4.2.1），窗口终点为最新日表时数字会小幅变动。
