# Dino English · 网络 & 三方（#5）规则

| 项 | 值 |
| --- | --- |
| 看板编号 | #5，`DASHBOARDS.md` §6.1.5 |
| 负责人 | @lee.winqi |
| 文档依据 | Lark《关键链路数据 & 维度定义（v0.1 草案）》**2026-08-12 版** §3.5 |
| 实测依据 | BigQuery，量级窗口 2026-07-13 → 08-11（30 天）；字段与分布窗口 **2026-08-05 → 08-11（7 天）** |
| 建立日期 | 2026-08-13 |
| 状态 | **已上线** · [health-network.html](./health-network.html)（数据 2026-08-14，窗口 07-14 → 08-12，三窗口可切）|
| 开发补充 | §3.1 / §3.3 / §3.4 已按客户端源码定稿；开发中又撞到两件事，见 §10 |

**这份文件是「网络 & 三方」这块板的唯一标准。**口径有分歧以本文件为准；与 Lark 文档冲突时先改 Lark 再改这里，不要只改一边。

页面做法见 [`PAGE-STANDARD.md`](./PAGE-STANDARD.md)，代码写法见 [`DEVELOPMENT.md`](./DEVELOPMENT.md)，通用取数纪律见 [`DASHBOARDS.md`](./DASHBOARDS.md)，本文件不重复。

---

## 0. 这块板回答什么

> **App 跟外面说话，通不通、快不快、断在哪一层。**

「外面」有五类，全归这块板：

| 域 | 事件 | 回答 |
| --- | --- | --- |
| 自家后端接口 | `api_result` | 请求成功率、耗时、按接口下钻 |
| 三方 SDK | `sdk_init_result` | SDK 初始化通没通 |
| 会话保持 | `signature_refresh_result` / `auth_refresh_result` / `http_unauthorized_result` | 签名与 token 刷新、401 处理 |
| 深链 | `deeplink_parse_result` / `deeplink_security_result` | 外部链接解析与安全校验 |
| 推送 | `push_token_result` / `push_bind_result` / `push_permission_result` / `push_open_result` / `push_deeplink_result` | 推送能不能送达 |

**先说结论：这块板最大的价值不是「成功率是多少」，而是证明了「一个笼统的成功率不能看」。**下面 §3 的七条发现，每一条都会让天真算法算出一个错得离谱的数。§3.1 / §3.3 / §3.4 已于 2026-08-13 经客户端源码逐条核对，不再是推测。

---

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

### 1.1 管

- 单个接口通不通、多快、失败在哪一层（网络 / 服务端 / 解码 / 业务）
- 三方 SDK 初始化健康度
- 签名刷新、token 刷新、401 的处理链路（**从 #1 登录板迁入**，见 §1.3）
- 深链解析与推送送达

### 1.2 不管（交给谁）

| 不管 | 归属 | 为什么 |
| --- | --- | --- |
| 登录两段成功率 | #1 | `sdk_auth_launch_result` 虽在 `third_party` 域，但属登录链路 |
| 支付三段 | #2 | 支付接口失败会在本板的 `/payment/*` 里出现，但成败判定归 #2 |
| 教室 WebView / RTC | #3 | RTC 不走 HTTP，不进本板 |
| 冷启动 / 首屏耗时 | #4 | 本板只管单次请求耗时，不管端到端 |
| 「请求失败之后用户去了哪」 | 用户路线板 | 本板只管这一次请求，不管后果 |

### 1.3 从 #1 迁入的三个事件

`signature_refresh_result` / `auth_refresh_result` / `http_unauthorized_result` 虽然 `diagnostic_area='auth'`，但按 Lark §3.5 语义属「鉴权刷新与 401 处理」。

**✅ 2026-08-14 已迁移完成。**原先临时挂在 `health-auth.html` **第 6 节**（不是第 2 节，本文件先前写错）。迁移动作：

| 登录板原有的东西 | 处理 |
| --- | --- |
| 第 6 节主体（两张折线图 + 总量表 + 最惨组合表） | **删除**，原位留一个跳转卡片 —— 保留小节而不是直接删掉，老读者按目录找过来要能看到东西去哪了 |
| 「严重 · 请求签名刷不出来」告警 | **删除**，不留降级版 |
| 派生量 `SIG` / `TOK` / `sigRate` / `sessD` / `sessDays` / `win6` | 删除 |
| `ch-sig` / `ch-sigfail` 两张图的绘制调用 | 删除 |
| 坑清单里两条引用 | 改写，指向本板 |
| **第 1 节版本档对照里的「签名刷新」列** | **保留** —— 它不是重复出数，是让「新版登录变差」和「签名修复」能对照（新档登录掉可能是签名修复的取舍）。算法与本板一致，值不会打架 |

登录板自检迁移后仍 **36/36 PASS**。

**遗留：**`auth-data.js` 的 `D.session` 字段现在没有任何页面在用（版本档那列取的是 `verGroup` 里的 `sig` 字段，不是它）。没有顺手删 —— 删它要改 `gen-auth-data.py` 并重跑登录板数据，会把登录板的数据日期推到 08-14，与其余板不同步。**建议下次刷登录板数据时一并清理。**

---

## 2. 事件与字段

### 2.1 `api_result` 字段表（7 天实测，Android 85,197 条 / iOS 17,546 条）

| 字段 | 类型 | 覆盖 | 说明 |
| --- | --- | --- | --- |
| `target` | string | 100% | **就是接口路径**，如 `/app/v1/open/language`。Android 112 个、iOS 82 个 |
| `result` | string | 100% | 终态。**两端枚举不同**，见 §4 |
| `failure_reason` | string | 失败时 | 大类：`network` / `server` / `data` / `business` / `unknown` |
| `reason` | string | 部分 | 细分：`offline` / `timeout` / `ssl_error` / `connection_lost` / `network_error` / `http_5xx` / `decode_failed` / `cancelled` / `unauthorized` |
| `status_code` | **int** | Android 38% / iOS 68% | HTTP 码。网络层失败时为空 |
| `biz_code` | **int** | Android 32% / iOS 3.5% | 业务码。Android 成功时恒为 200；iOS 出现 `40001` / `40002` / `40010` / `52001` 等 |
| `duration_ms` | **int** | 100% | 单次请求耗时 |
| `attempt` | **int** | **Android only** | 0 / 1，重试轮次。**iOS 完全没有这个字段** |
| `network_type` | string | 100% | `wifi` / `4g` / `5g` / `3g` / `2g` / `offline` / `null` |
| `trace_id` | string | 100% | 每条一个，已落地 |
| `device_id` / `uid` | string | 100% | 去重用，见 §3.6 |
| `sample_rate` | double | **线上 0%** | 设计上中签的 success 带 `0.1`、失败传 null 被丢弃 → 有此字段即采样样本。**但线上全版本零条**，见 §3.2 |

**`failure_reason` 和 `reason` 是两层，不是同义词。**做归因表要用「大类 → 细分」两级，只取一个会丢一半信息。

### 2.2 其余四域

| 事件 | Android | iOS | 备注 |
| --- | --- | --- | --- |
| `sdk_init_result` | 60,254 成功 / 938 失败（98.5%） | 19,110 成功 / **0 失败** | `target` = SDK 名，Android 9 个 / iOS 7 个 |
| `signature_refresh_result` | 24,857 成功 / **99,611 失败** | 2,574 成功 / 3,443 失败 | 见 §3.5 |
| `auth_refresh_result` | 2,432 成功 / 5,669 失败 / 513 rejected | 602 成功 / 418 失败 | |
| `http_unauthorized_result` | 41 条 | 2 条（30 天窗口） | 量太少，只做附注 |
| `deeplink_parse_result` | **17 条**（30 天） | 1,514 成功 / **0 失败** | Android 基本没埋 |
| `deeplink_security_result` | 无 | 1,514 成功 / **0 失败** | iOS only |
| `push_token_result` | 3,328 成功 / 122 失败 | 5,440 成功 / 115 失败 | |
| `push_bind_result` | 1,667 成功 / 3,431 rejected | 1,507 成功 / 7,657 **skip** / 990 **cancel** | 词表两端完全不同，见 §4 |
| `push_permission_result` | 1,967 授权 / 776 拒绝 | 1,852 授权 / 750 拒绝 / 26 未决 | **全部 `result=success`**，授权与否在 `reason` 里 |

---

## 3. 五条地基级实测发现（本板的核心）

这五条决定了页面怎么切。**任何一条没落实，出来的数都是错的。**

### 3.1 采样：10% 只压 success，失败全量（2026-08-13 按源码确认）

Lark §3.5 的「10% 采样」经客户端源码核对，机制是这样的：

| 事实 | 出处 |
| --- | --- |
| 采样点**只有一处**：不中签**整条事件不发**，不是打标记后发 | `ApiResultConverterFactory.kt:103` `if (!shouldSampleSuccess()) return` |
| 判定 `ThreadLocalRandom.nextDouble() < 0.1`，**逐次独立** | `ApiResultTraceInterceptor.kt:169-170` |
| **失败全量**：`tech_fail`（IOException / 5xx / decode_failed）、`rejected`（4xx / 业务码）、`user_abort` 全都不经过采样 | 同上 |
| 中签的 success 带 `sample_rate=0.1`；失败传 null，而 `analyticsParams.put` 对 null 直接丢 | `AnalyticsParamBuilder.kt:11` |

由最后一条推出一个好用的判据：**「有没有 `sample_rate` 字段」= 「是不是采样样本」**，不用再猜。（但线上目前还用不了，见 §3.2。）

**主指标公式（成功侧必须先还原）：**

```
成功率 = (count(success) / 0.1) ÷ ( count(success) / 0.1 + count(全部失败) )
```

直接写 `success ÷ (success + fail)` 会把成功率算成个位数到几十的**假暴跌** —— 这就是主指标出不出得来的关键。

耗时同理，见 §3.5。

### 3.2 但线上一条 `sample_rate` 都没有 —— 还原系数暂时不能自证

实测（窗口 08-05 → 08-11，`api_result` 全量按平台 × 版本 × result 扫）：

| 平台 | 版本 | 带 `sample_rate` 的条数 |
| --- | --- | --- |
| Android | 1.5.2 / 1.5.1 / 1.5.0 / 1.4.2 | **0** |
| iOS | 1.5.1 / 1.5.0 / 1.4.2 | **0** |

采样逻辑和 `sample_rate` 出自同一处改动，所以线上零条最可能是**这段代码还没发版**。两种情况后果完全不同：

| | Android 失败率 | iOS 失败率 |
| --- | --- | --- |
| 若线上**已在采样**（成功 ÷0.1 还原） | 13.5% | 4.7% |
| 若线上**还没采样**（success 即全量） | **49.1%** | **32.8%** |

**裁定：**

1. 页面**必须**按 §3.1 公式还原，但还原系数写成常量 `SUCCESS_SAMPLE_RATE`，并在页面「这页有哪些坑」里注明「按 0.1 固定还原，线上尚无 `sample_rate` 字段可自证，见 §8 拍板项 ①」。
2. 带 `sample_rate` 的版本一旦上线，**立刻改成按字段实测还原**，并复核历史数据是否需要重算。
3. 在 ① 有答案之前，主指标位同时给出两种口径的数（还原前 / 还原后），不允许只给一个。

### 3.3 `offline` 不是「没网」，是 DNS 解析失败

`reason='offline'` 是 Android 头号失败原因，7 天 41,807 条。按 `network_type` 拆开，**68% 发生在有网的时候**：

| `network_type` | 条数 | 占比 |
| --- | --- | --- |
| **wifi** | 19,286 | 46.1% |
| **4g** | 8,882 | 21.2% |
| `offline` | 13,165 | 31.5% |
| **5g** | 250 | 0.6% |
| 其他 | 224 | 0.5% |

这不矛盾 —— **是这个词起错了名**。`offline` 只有一个来源：

```kotlin
is UnknownHostException -> IoMapped(FAILURE_NETWORK, REASON_OFFLINE)
// ApiResultTraceInterceptor.kt:143-147
```

`UnknownHostException` = **DNS 解析不出来**，跟有没有网没关系。而 `network_type` 是埋点公共属性，事件发出那一刻从 `NetworkTypeProvider` 缓存读，只有 `onLost`（默认网络丢失）才写 `offline`。**两个字段口径完全独立**：一个是 DNS 结果，一个是当下 transport。

**所以那 68% 恰恰是这份数据最有价值的部分** —— `network_type` 是唯一能把两类故障切开的办法：

| `network_type` | 实际含义 |
| --- | --- |
| `offline`（31.5%） | 真断网（无网时 DNS 同样抛 `UnknownHostException`） |
| **wifi / 4g / 5g（68%）** | **有连接但域名解析不了**：运营商 DNS 抖动、Wi-Fi portal 未认证、VPN 切换、冷启动 DNS 未就绪 |

**裁定：**

- 归因表第一行写「**DNS 解析失败**」，**不写「无网络」**，也不写「连不上」。
- 这一行必须**按 `network_type` 下钻成两行**：「真断网」与「有网但解析失败」。这是本板最有信息量的一张表。
- `network_type` 本身有缓存滞后（回调线程写、事件线程读），方向上只会**低估** `offline`，造不出 68%，不影响结论。此事写进页面坑位说明。

### 3.4 分母 = 业务调用数；「启动时序」假说已被 `attempt` 证伪

**分母先定死。**拦截器顺序是 `ApiResultTraceInterceptor` 包住 `RetryInterceptor`（`AppNetworkModule.kt:116-119`），因此：

| 规则 | 说明 |
| --- | --- |
| **一次业务调用 = 一条 `api_result`** | 分母是**业务调用数**，不是 HTTP 尝试数。**不用去重** |
| `duration_ms` 含所有重试 + `Thread.sleep` 退避（1s 起，`RetryInterceptor.kt:88-90`） | 失败样本耗时被系统性拉长 1s+ |
| `attempt` = 最终那次的 **0 基序号** | `attempt≥1` 的失败 = 重试过仍失败 |
| 401 触发 `TokenAuthenticator` 重发也在内层 | 同样只报一条 |

**再说「App 在网络栈就绪前发请求」这个假说 —— 实测证伪。**

代码上它只成立一半：`DinoEnglishApp.onCreate` 确实同步启了一串网络活儿（Firebase RC fetch、push token 上报、payStartup 对账、Splash 的 device-context），全挤在进程起来那几百毫秒，DNS 缓存和连接池都是冷的，且没有任何「等网络可用」的门控。但 `UnknownHostException` 命中 `isConnectionFailure`，GET/POST 都会重试（`RetryInterceptor.kt:79-84`），间隔 1s —— **能上报出来的 `offline` 已经是「1s 后重试仍失败」**，纯时序竞态早该被这次重试吃掉。

按 `attempt` 分桶实测（Android，7 天）：

| `reason` | `attempt=0` | `attempt=1` | 判读 |
| --- | --- | --- | --- |
| **`offline`** | **0** | **41,807** | **100% 重试过仍失败** |
| `connection_lost` | 0 | 166 | 同上 |
| `ssl_error` | 82 | 218 | 混合 |
| `network_error` | 82 | 179 | 混合 |
| `timeout` | 132 | 17 | 多数未重试（超时不重试） |
| `http_5xx` | 193 | 2 | 多数未重试 |
| `success` | 26,703 | **139** | **重试后成功仅占 0.5%** |

三条结论：

1. **`offline` 零条 `attempt=0`** —— 重试逻辑没有漏掉的代码路径（原本担心的「大量 `attempt=0` 说明有路径没走到」没有发生）。
2. **启动时序不是主因。**这批失败是 DNS / 网络本身解析不了，重试也救不回来。
3. **重试的实际收益极低** —— 进了重试的请求里只有 139 条最终成功（0.5%）。「重试值不值」应单列一个指标，见 §7。

**一个硬限制：`api_result` 里没有任何字段标记「启动期」**（没有 `source`、没有 `phase`）。目前只能靠 `target` 白名单近似切，**切不干净**。所以：

- 页面**仍然分「启动期 / 业务期」两组展示**（§3.4.1 的分布差异是真实的，合并数会互相稀释）
- 但**必须注明「按接口路径近似划分，非埋点字段」**，不得把它当成因果结论
- 想要真结论，需要客户端补 `source` / `phase` 字段，见 §8 拍板项 ③

#### 3.4.1 两组接口的失败率差一个数量级（7 天，`bad` = `tech_fail` + `fail`，未还原采样）

| 接口 | 平台 | 成功 | 失败 | 失败率 |
| --- | --- | --- | --- | --- |
| `/app/v1/open/bootstrap` | iOS | 251 | 3,373 | **93.1%** |
| `/app/v1/open/onboarding/flow` | Android | **1** | 1,034 | **99.9%** |
| `/app/v1/auth/refresh` | Android | 240 | 5,379 | **95.7%** |
| `/app/v1/open/clientConfig` | Android | 1,713 | 8,720 | 83.6% |
| `/app/v1/open/language` | Android | 1,840 | 9,207 | 83.3% |
| `/app/v1/lottie/manifest` | Android | 1,831 | 7,133 | 79.6% |
| `/app/v1/membership/current` | Android | 2,323 | 4,633 | 66.6% |
| — 以下是业务期接口 — | | | | |
| `/app/v1/ai-class/lesson/weekly-plan` | Android | 1,506 | 132 | 8.1% |
| `/app/v1/chat/messages` | Android | 1,450 | 202 | 12.2% |
| `/app/v1/promo/state` | Android | 916 | 123 | 11.8% |
| `/app/v1/user/profile` | iOS | 1,164 | 201 | 14.7% |

**注意这张表的失败率是「未还原采样」的**，若 §3.2 确认线上在采样，两组的绝对值都要 ÷ 还原后重算，但**两组之间的差距不变**（失败侧全量，比例关系不受采样影响）。

`/app/v1/open/onboarding/flow` 的 1 成功 / 1,034 失败**单独列为严重问题** —— 采样口径怎么定它都是坏的（还原后也才 10 : 1,034）。

### 3.5 耗时：成功与失败是两个不可比的分布，不许画进同一张图

| | 成功耗时 | 失败耗时 |
| --- | --- | --- |
| 采样 | **10% 抽样**（若 §3.2 确认） | **全量** |
| 含不含退避 | 不含 | **含 1s 起的 `Thread.sleep` 退避** |
| 实测 p50（Android） | 309ms | `offline` 1,078ms |

两个问题：

1. **低流量接口的成功分位数不稳。**10% 抽样下，7 天只有几十条成功样本的接口，p95 基本是噪声。**裁定：成功耗时分位数只在样本量 ≥ 100 的接口上出，不足的显示 `—`**（按 `DASHBOARDS.md` §5.4「不可信不判档」）。
2. **失败耗时天然更长，不是「失败的请求更慢」，是退避加进去了。**裁定：耗时块**只出 `result='success'` 的分布**（沿用 Lark 口径）；失败耗时若要出，单独成表并注明含退避。

极端值提醒（Android 7 天实测）：`decode_failed` p95 = 1,780,568ms（29.7 分钟）、`timeout` `attempt=1` p95 = 1,088,837ms（18 分钟）、`user_abort` `attempt=0` p50 = 30,018ms（正好卡在 30s 超时阈值）。**画图前必须裁尾**，否则一条 30 分钟的记录会把整张图压平。

### 3.6 一小撮设备在死循环重试，按条数算会被它们决定颜色

| 现象 | 条数 | 设备数 | 每台 |
| --- | --- | --- | --- |
| iOS `network_error` + 真离线 | 4,919 | **69** | **71.3 次** |
| iOS `signature_failed` | 3,443 | **89** | **38.7 次** |
| Android `signature_failed` | 99,611 | 2,634 | 37.8 次 |
| Android `offline`（wifi 下） | 19,286 | 1,640 | 11.8 次 |
| 对照：Android 成功请求 | 26,842 | 2,761 | 7.4 次 |

**69 台设备贡献了 iOS 全部失败的 73%。**只按请求条数算，这块板会被几十台设备染红，而 99% 的用户其实是好的。

**裁定：所有成功率指标必须同时出两个口径 ——「按请求」和「按设备」（该设备 7 天内是否至少遇到一次该失败）。**着色以**按设备**为准，按请求作为对照列。理由同 `DASHBOARDS.md` §5.4 三条强制规则：判据不能被少量极端样本翻转。

### 3.7 签名刷新是全项目当前最大的技术故障

7 天：Android **99,611** 次失败（`reason=signature_failed`）、iOS 3,443 次。30 天 Android 186,687 条里失败约 15 万。

- 按请求：Android 失败率 **80.0%**
- 按设备：2,634 / 7,473 台 = **35.2%** 的活跃设备至少遇到过一次
- 每台遇到的设备平均失败 **37.8 次** → 明显是失败后立即重试的死循环

这个数已经在 `health-auth.html` 第 2 节挂着，也在 `USER-ROUTE.md` §0 被引为「无人能回答后果」的例子。**本板上线后它是 #5 的头号问题，页面第一屏必须放它。**

---

## 4. 终态四分类：两端词表不一致（第三次遇到）

`DASHBOARDS.md` §3.3 已记录过这个坑，本板是它最严重的一次 —— **三个域各错各的**。

### 4.1 `api_result`

| | Android | iOS |
| --- | --- | --- |
| 成功 | `success` | `success` |
| 技术失败 | **`tech_fail`** | **`fail`** |
| 用户取消 | **`user_abort`**（9,324 条） | **无此值**，混在 `fail` 里 |
| 业务拒绝 | **`rejected`**（5,545 条） | **无此值**，混在 `fail` 里 |

**Android 已经迁到四分类，iOS 还停在二分类。**

### 4.2 所以「剔业务 4xx」不能看 `result`

Lark §3.5 主指标写的是「请求成功率（剔业务 4xx）」。两端剔法必须不同：

```sql
-- 正确：用 failure_reason 判业务，不用 result
-- Android: result='rejected' 已经是业务，但 iOS 的业务 4xx 藏在 result='fail' 里
CASE
  WHEN result = 'success' THEN 'success'
  WHEN failure_reason = 'business' THEN 'rejected'      -- 两端统一，剔出分母
  WHEN reason = 'cancelled' OR result = 'user_abort' THEN 'user_abort'
  WHEN result IN ('tech_fail','fail') THEN 'tech_fail'  -- 双端失败词双取
END
```

**只认 `tech_fail` 会静默丢掉整个 iOS；只按 `result` 剔 4xx 会把 iOS 的 403/404/400 算成技术失败。**iOS 的 business 类 7 天有 466+ 条。

### 4.3 `push_bind_result` 更乱

| 情形 | Android | iOS |
| --- | --- | --- |
| 未登录 | `rejected` + `not_logged_in` | **`skip`** + `not_logged_in` |
| 重复绑定 | `rejected` + `deduped` | **`skip`** + `deduped` |
| 缺 token | `rejected` + `missing_fcm_token` | **`skip`** + `missing_fcm_token` |
| 用户取消 | — | **`cancel`** + `cancelled` |

`skip` / `cancel` 是四分类表外取值（`DASHBOARDS.md` §8 已挂待推动项）。**本板按「语义对齐」处理：`skip` 与 `rejected` 合并成同一档「未执行」，不算成功也不算失败**，并在页面注明两端词不同。

### 4.4 `push_permission_result` 全部是 `success`

授权和拒绝都报 `result=success`，区别只在 `reason`（`authorized` / `denied` / `not_determined`）。**这个事件的 `result` 没有判别力，不能算成功率**，只能按 `reason` 出授权率：Android 71.7%、iOS 70.5%。

---

## 5. 公共维度

按 `DASHBOARDS.md` §5.2 执行，本板专属维度：

| 维度 | 取值 | 用在哪 |
| --- | --- | --- |
| 接口 `target` | Android 112 / iOS 82 个 | 主表下钻；按 §3.3 先分启动期 / 业务期两组 |
| 失败层 `failure_reason` | `network` / `server` / `data` / `business` / `unknown` | 归因表第一级 |
| 失败细分 `reason` | 9 个已实测值 | 归因表第二级 |
| 网络类型 `network_type` | wifi / 4g / 5g / 3g / 2g / offline | §3.2 的证据列，必须出 |
| SDK 名 `target`（`sdk_init_result`） | Android 9 / iOS 7 | 三方 SDK 块 |
| 重试轮次 `attempt` | 0 / 1 | **Android only**，iOS 无此字段，页面须标 |

版本档对照按 `DASHBOARDS.md` 现行做法（三个技术健康板已有「按版本档对照」块），本板照做。

---

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

### 6.1 成本闸门

按 `DASHBOARDS.md` §4.4。本板取数扫描量实测：单条 30 天 `app_diagnostic` 查询约 **1.31 GiB**，7 天约 0.31 GiB。**主表用 30 天、字段分布用 7 天**，不要每条都开 30 天。

### 6.2 各事件量与首报日（窗口 2026-07-13 → 08-11，30 天）

| 事件 | Android | iOS | Android 首报 | iOS 首报 |
| --- | --- | --- | --- | --- |
| `api_result` | 126,362 | 22,791 | 07-20 | **07-28** |
| `sdk_init_result` | 89,238 | 25,578 | 07-20 | **07-28** |
| `signature_refresh_result` | 186,687 | 13,252 | 07-23 | 07-14 |
| `auth_refresh_result` | 13,280 | 2,234 | **07-30** | 07-17 |
| `push_bind_result` | 7,827 | 22,356 | 07-23 | 07-16 |
| `push_token_result` | 5,340 | 12,136 | 07-23 | 07-16 |
| `push_permission_result` | 4,119 | 6,000 | 07-24 | 07-16 |
| `deeplink_parse_result` | **17** | 3,268 | 07-29 | 07-16 |
| `http_unauthorized_result` | 77 | 2 | 07-30 | 08-03 |
| `h5_webview_load_success`（`routing`） | 2,483 | 911 | **08-07** | **08-07** |

**窗口按事件自己的首报日标，不按查询区间**（`DASHBOARDS.md` §4.2 硬规矩）。`api_result` 的 iOS 首报是 07-28，比 Android 晚 8 天，**两端总量没有可比性**。

### 6.3 `DASHBOARDS.md` §4.2 的 area 表已过时，需一并更新

08-03 那版实测漏了这些（30 天实测）：

| area / 事件 | 量 | 状态 |
| --- | --- | --- |
| **`voice`（整个 area）** | `voice_tts_result` 5,061 · `voice_asr_result` 2,722 | 表里完全没有，08-04 才上线 |
| `auth_refresh_result` | 15,514 | 未登记 |
| `http_unauthorized_result` | 79 | 未登记 |
| `app_context_sync_summary` | 69,770 | 未登记 |
| `analytics_setup_result` | 42,504 | 未登记 |
| `posthog_setup_result` | 3,702 | 未登记 |
| `routing` 的 `h5_webview_load_*` | 3,481 | 08-07 新上 |
| `performance` | **17,034**（原记录 363） | 量已够，#4 阻塞解除 |

---

## 7. 页面结构（DoD）

按 `PAGE-STANDARD.md` §6，本板五块：

| # | 块 | 内容 | 前置 |
| --- | --- | --- | --- |
| 0 | 主指标 | 请求成功率，**按 §3.1 公式还原采样后**出数；同位置并列给出「还原前」灰字对照 | ① 确认发版状态 |
| 1 | 头号问题 | 签名刷新死循环（§3.7），按设备口径出数 | 无 |
| 2 | 接口健康 | **分启动期 / 业务期两组**（注明为路径近似划分），各出按请求 + 按设备双口径 | 白名单定稿 |
| 3 | 失败归因 | `failure_reason` → `reason` 两级；第一行「DNS 解析失败」**必须按 `network_type` 下钻成「真断网 / 有网但解析失败」两行**（§3.3） | 无 |
| 4 | 重试收益 | `attempt≥1` 的量 vs 其中最终成功的量（实测 0.5%）；`offline` 的 `attempt` 分布（§3.4） | 无 |
| 5 | 耗时 | P50 / P95，**只取 `result='success'`**，样本量 < 100 的接口显示 `—`；画图前裁尾（§3.5） | 无 |
| 6 | 三方 SDK / 深链 / 推送 | 各出成功率；iOS 零失败项按 §5.4 中性灰不判档 | 无 |

### 7.1 验收清单

- [ ] 成功侧按 `SUCCESS_SAMPLE_RATE` 常量还原，页面同时给还原前 / 还原后两个数
- [ ] 还原系数在坑位说明里注明「线上暂无 `sample_rate` 可自证」
- [ ] 双端失败词双取（`tech_fail` + `fail`），iOS 行数非零
- [ ] 业务 4xx 按 `failure_reason='business'` 剔除，不是按 `result`
- [ ] 每个成功率都有「按请求 / 按设备」两列，着色取按设备
- [ ] 接口表分启动期 / 业务期，并注明「按路径近似划分，非埋点字段」
- [ ] `offline` 文案写「**DNS 解析失败**」，不写「无网络」；并按 `network_type` 下钻两行
- [ ] 耗时只用成功样本，样本 < 100 显示 `—`，图表已裁尾
- [ ] 零失败项（iOS `sdk_init` / `deeplink`）中性灰，不写 100%
- [ ] `push_permission_result` 不算成功率，只算授权率
- [ ] 窗口标各事件自己的首报日；最新一天标暂定
- [ ] `attempt` 相关图表标注「Android only」
- [ ] `health-auth.html` 第 2 节已删除并留跳转链接

---

## 8. 待拍板

**2026-08-13 已答（客户端源码核对）：**原 ①②③ 三项已由客户端逐条回答并写入 §3.1 / §3.3 / §3.4，不再阻塞开发。剩余项如下。

| # | 事项 | 卡住什么 | 找谁 |
| --- | --- | --- | --- |
| ① | **带 `sample_rate` 的版本发到线上了吗？**线上 1.4.2 / 1.5.0 / 1.5.1 / 1.5.2 全部零条（§3.2） | 决定还原系数用 0.1 还是 1.0 —— 两者差 3.6 倍。开发可先按 0.1 常量走，但**上线前必须有答案** | 客户端 / 发版 |
| ② | `/app/v1/open/onboarding/flow` Android 1 成功 / 1,034 失败 | 采样口径怎么定它都是坏的，需要立刻查 | 后端 / Android |
| ③ | `api_result` 补「启动期」标记（`source` 或 `phase`） | 现在只能按 `target` 白名单近似切，切不干净（§3.4） | 双端 |
| ④ | 重试策略：`offline` 100% 重试仍失败、重试后成功仅 0.5%，这个退避值不值 | 重试把失败量放大了一倍，且几乎没救回请求（§3.4） | Android |
| ⑤ | 签名刷新失败后为什么立即重试、重试上限是多少 | 每台设备平均 37.8 次失败，量是重试放大出来的（§3.7） | 双端 |
| ⑥ | **iOS 何时迁四分类**（`user_abort` / `rejected`） | iOS 目前无法区分用户取消与技术失败（§4.1） | iOS |
| ⑦ | iOS `api_result` 补 `attempt` 字段 | 重试维度只有 Android 能看 | iOS |
| ⑧ | Android 深链 30 天只有 17 条，是没埋还是没人用 | 深链块只能出 iOS 单端（§2.2） | Android |
| ⑨ | `push_bind_result` 的 `skip` / `cancel` 语义 | 表外取值，本板暂按「未执行」处理（§4.3） | 客户端 |

**只有 ① 卡上线**（不卡开发：先按常量 0.1 写，答案回来改一个常量）。②~⑨ 不阻塞，页面按现状标注即可。

---

## 9. 与其他文档的改动清单

| 文件 | 改什么 | 状态 |
| --- | --- | --- |
| `DASHBOARDS.md` §4.2 | area 表补 `voice` 等 8 项（§6.3） | ✅ 2026-08-14 |
| `DASHBOARDS.md` §6.1.5 | 状态「待做」→「已完成」，补口径摘要与本文件链接 | ✅ |
| `DASHBOARDS.md` §6.1.4 | #4 去掉「阻塞」标记，`performance` 量改 17,034 | ✅ |
| `DASHBOARDS.md` §8 | 落地顺序表勾掉 #5，#4 标可提前，新增 #11 语音；对外推动表并入本文件拍板项 | ✅ |
| `README.md` / `index.html` | 页面表、文件表、导航卡片 | ✅ |
| **`health-auth.html`** | 删第 6 节（签名刷新）主体，原位留跳转卡片；告警、派生量、两张图一并清理 | ✅ 2026-08-14，详见 §1.3。自检仍 36/36 |
| `USER-ROUTE.md` §0 | 签名刷新的数改引本文件 | ✅ |
| **本板可选增强** | `firebase_performance` 的 `NETWORK_REQUEST`(Android 12 万 / iOS 21 万,含 `response_code`、各阶段时间戳)可作**客户端视角 vs 网络层视角**的交叉验证 —— 但它在 `US`,与 GA4 跨区域不能 JOIN,且只有 6 天。详见 [`PERFORMANCE.md`](./PERFORMANCE.md) §1.2 | ⛔ 待评估 |
| **Lark §3.5** | 回写三条源码确认结论：① 采样只压 success、失败全量、判据是 `sample_rate` 字段；② `offline` 实为 `UnknownHostException`（DNS），术语应改名；③ 分母是业务调用数（拦截器包住重试），`duration_ms` 含退避 | ⛔ 待推动 |

---

## 10. 开发中撞到的两件事（2026-08-14 补）

### 10.1 `result='cancel'` 是 1.4.2 的旧词，归一规则要双取

取数脚本的终态哨兵报出 5 条落进 `other` 桶。查下来是 **Android 1.4.2 的 `result='cancel'`**，全部集中在 2026-07-28 单日，三个不同接口，不带 `reason`。新版本改叫 `user_abort` + `reason=cancelled`。

**已在 `VERDICT` 归一里双取**（`r IN ('user_abort','cancel')`）。量极小不影响任何结论，但记在这里，因为这是本项目**第四次**撞上「同一件事两个词」：iOS 的 `fail`、`signup_method`→`method`、推送的 `skip`/`rejected`，加这一个。

**做新板时的默认动作：终态归一必须带一个 `other` 兜底桶 + stderr 哨兵，不要假设枚举是全的。**

### 10.2 全局 `untrusted()` 管不了「总量太少」，本板在展示层补了一道

`page-kit.js` 的 `untrusted()` 只判「失败少到不像真的」。本板有两处它盖不住：

| 格子 | 判定次数 | 直接判档的结果 |
| --- | --- | --- |
| 深链解析 Android | 24 次（挂 2 次） | 91.67% → **红色「危险」** |
| 401 处理 iOS | 2 次（挂 2 次） | 0.00% → **红色「危险」** |

两处的真实问题都是**没埋点 / 没人走这条路**，不是链路坏了 —— 判成红色会让人去修一条根本没在跑的链路。

本板在渲染层加了 `MIN_JUDGE = 100`：**判定次数低于 100 一律标灰不判档**，hover 里说明原因。

**没有改全局 `untrusted()`** —— 那要同步改 `auth-page.js` 与 `page-kit.js` 两份，影响全部看板，不该由本板顺手做。但这是个**通病**（支付板的购买段 19 次判定、恢复段 4 次也是同一类），值得单独提一次：**建议把「判定次数下限」并进全局判据**，那样四块板的小样本格子会一起变干净。
| **本板可选增强** | `firebase_performance` 的 `NETWORK_REQUEST`(Android 12 万 / iOS 21 万,含 `response_code`、各阶段时间戳)可作**客户端视角 vs 网络层视角**的交叉验证 —— 但它在 `US`,与 GA4 跨区域不能 JOIN,且只有 6 天。详见 [`PERFORMANCE.md`](./PERFORMANCE.md) §1.2 | ⛔ 待评估 |
| **Lark §3.5** | 回写三条源码确认结论：① 采样只压 success、失败全量、判据是 `sample_rate` 字段；② `offline` 实为 `UnknownHostException`（DNS），术语应改名；③ 分母是业务调用数（拦截器包住重试），`duration_ms` 含退避 |
