# Dino English · 看板规则与条件

本文只写**规则、口径、前置条件**。页面怎么做(文案、颜色、图表、emoji、自检)见 [`PAGE-STANDARD.md`](./PAGE-STANDARD.md)。

**数据源硬规则:只用 BigQuery。不使用 Google Analytics Data API / GA MCP。**

口径依据四份 Lark 文档(见 §2),已于 2026-08-03 全文读取并与 BigQuery 实际数据核对。**核对结论:文档是目标态,BQ 是现状,两者存在系统性偏差(见 §3)。取数前必须先验证事件名与字段,不能照文档写 SQL。**

---

## 1. 每个 HTML 看板必须展示的元信息

页头(或同等醒目位置)**必须**包含:

| 字段 | 规则 | 示例 |
| --- | --- | --- |
| Project | GCP / BigQuery 项目 | `dino-english-497507` |
| Source | 本次查询用到的 dataset(可多个) | `analytics_538991439` / `de_dwd` / `de_ads` |
| Location | BigQuery 区域 | `asia-southeast1` |
| Timezone | 与 SQL 口径一致(事件时间默认 UTC;页面写明) | `UTC` |
| Range | **日历日期**,禁止写 `30daysAgo` / `yesterday` 等相对值 | `2026-07-01 → 2026-07-30` |
| Pulled | 数据拉取完成时间(注明时区) | `2026-08-03 08:00 UTC+8` |

补充:

- Range 起止日 = 本次 SQL 实际过滤的日期(`event_date` / `install_date` / `_TABLE_SUFFIX` 等)
- 刷新数据后必须同时更新 Range 与 Pulled
- 若页面有多个时间窗(如主表 30 天、趋势近 14 天),每个区块各自标明日期范围
- 原始事件时间用 `TIMESTAMP_MICROS(event_timestamp)`;业务宽表用节点日期字段
- **埋点上线日期不同,30 天窗口普遍取不满。做任何看板前先查该事件 / area 的首日,窗口按实际可用范围写,不要硬凑 30 天**

---

## 2. 文档依据

| 文档 | 用途 | 关键内容 |
| --- | --- | --- |
| [关键链路数据 & 维度定义](https://qjphu5vphyf4.jp.larksuite.com/wiki/JWFow89j0i6L0qkxcsHj1CCypBf)(v0.1 草案,**最后编辑 2026-08-12**) | **技术健康看板唯一口径来源** | §2 通用约定(四分类终态、公共维度)、§3.1 登录注册、§3.2 支付订阅(已定稿)、§3.3 进入教室、§3.4 崩溃性能、§3.5 网络三方(**含 10% 成功采样**)、§3.6 ASR/TTS 语音、§7 iOS 进教室四条路径(08-12 新增)。**08-10 第一批改动只动 §2 通用约定层(黄色底纹 = 评审中未定稿)**:fail→四分类折算规则(支付本轮不迁、停在数仓折算阶段,我们的 `fail`+`tech_fail` 双取仍正确)、trace_id 翻转为「作为基建落地」、待确认表回填 4 项(均与我们现行做法一致) |
| [埋点事件明细](https://qjphu5vphyf4.jp.larksuite.com/wiki/U9Piw7KI7i64aFkYYUkjizOppDb)(V0.24,**更新至 2026-08-12**,适用 V1.3.0+V1.4.0+V1.4.2+V1.5.1+V1.5.3) | **产品事件字典** | 五类事件的 event_id 全量登记、属性枚举、上线 / 变更版本;§3 与历史协议的迁移关系(**08-12 版补登了 `subscription_checkout_start/result` → `trigger` 两行,#8 曾因此少算 4 倍,见 §3.1 附记**);新增 `sms_captcha_result`(V1.5.3)与 `trigger/app_launch`(V1.5.3,冷启动/前台恢复) |
| [埋点规范](https://qjphu5vphyf4.jp.larksuite.com/wiki/O1Zuw7Hh1i6BUikpbzgjZfBJpIc)(V0.24,2026-07-28) | 命名、公共属性、上报边界、质量纪律 | §2.1 公共属性表、§2.2 传值规则、§8 自定义属性 25 Key 白名单、§9 用户标识 |
| [转化链路 A/B 实验 PRD](https://qjphu5vphyf4.jp.larksuite.com/wiki/CxFywmcZaifflGkGHbSjR3AMp4g)(V1.4.2) | A/B 漏斗与发布指标 | §7.1 实验标识、§7.4 漏斗对齐锚点、§7.5 指标口径与护栏 |

### 2.1 已知缺失的配套文档(影响看板完成度)

| 文档 | 谁需要 | 缺了会怎样 |
| --- | --- | --- |
| 《转化链路AB实验-读数与统计指南》 | #6 A/B | 样本剔除、分层、显著性、样本量、SRM 判据、取数路径全部未知,A/B 看板无法给结论 |
| 《Dino English 双端页面、类名与路由对照表》 | #7 / #8 产品漏斗 | `page_view` 的 event_id 与 `screen_view` 的 firebase_screen 映射不明 |
| 基线 PRD V1.3.0 / V1.3.2 / V1.4.0 / V1.4.1 | 全部产品看板 | 线上既有转化触点的出处 |
| 「有效学习分钟数」定义 | #9 北极星 | **四份文档中均无此定义**。原 DASHBOARDS 引用的「埋点规范 §1.2」在实际文档中不存在,该指标来源不明,已降级为待确认 |

硬规则:

- 技术健康看板与产品转化看板**分文件、分口径**,不在同一页混读
- A/B 发布主指标 ≠ 产品北极星
- **技术健康只认 `app_diagnostic`。产品事件(click / page_view / trigger / signup_result / subscription_purchase_result)明确不用于技术健康率计算**(关键链路 §3.1 B 节原文)

---

## 3. 两套事件模型(取数前必读)

BigQuery 里同时存在两套完全不同的事件体系,**互不通用**:

| | 技术诊断 | 产品事件 |
| --- | --- | --- |
| 定义出处 | 关键链路文档 | 埋点规范 + 埋点事件明细 |
| `event_name` | 固定 `app_diagnostic` | `click` / `page_view` / `trigger` / `signup_result` / `subscription_purchase_result` |
| 分类字段 | `diagnostic_area` / `diagnostic_id` / `diagnostic_step` | `event_id`(事件字典内全局唯一) |
| 结果字段 | `result` + `reason` | `is_success` + `result` |
| 回答什么 | 掉在哪段、是不是技术错误 | 用户做了什么、转化多少 |

**注意:`app_diagnostic` 不在埋点规范的五类事件里。** 它是关键链路文档单独定义的体系,埋点规范完全没写它。两套文档各管一半,不要拿埋点规范的规则去套技术诊断,反之亦然。

### 3.1 新旧模型并存(2026-08-03 实测,窗口 07-03 → 08-01)

埋点规范要求把旧的独立事件名迁到「五类 event_name + event_id」模型。**迁移只做了一半,旧事件仍是主力**:

| 规范目标 | 旧 event_name(仍在跑) | 量 | 新模型 | 量 | 状态 |
| --- | --- | --- | --- | --- | --- |
| `click` / event_id | `button_click` | 109,616 | `click` | 5,536(07-22 起) | 旧为主 |
| `page_view` / `login` | `login_page_view` | 11,694 | `page_view`+`event_id=login` | 913 | 旧为主 |
| `page_view` / `report` | `report_page_view` | 4,690 | `page_view`+`event_id=report` | 189 | 旧为主 |
| `page_view` / `subscription` | `subscription_page_view` | 1,632 | `page_view`+`event_id=subscription` | 689 | 旧为主 |
| `page_view` / `discount_offer` | `discount_offer_view` | 3,490 | `page_view`+`event_id=discount_offer` | 364 | 旧为主 |
| `trigger` / `login_result` | `login_result` | 14,790(**07-30 停报**) | `trigger`+`event_id=login_result` | 1,185 | 已切换 |
| `trigger` / `class_stage_*` | `class_stage_progress` | 167,257(**07-29 停报**) | `class_stage_start`/`_end` | 3,751 / 3,254 | 已切换 |
| `trigger` / `dino_session_*` | `dino_assistant_progress` | 35,668 | `dino_session_start`/`_end` | 575 / 500 | 旧为主 |
| `trigger` / `step_submitted` | `step_submitted` | 96,739 | `trigger`+`event_id=step_submitted` | 1,922 | 旧为主 |

规范外事件(文档未登记,实际在跑):`business_result` 11,972 · `HA_LOGIN` 431 · `vip-button-click` 361 · `ui_click` 936 · `coupon_page_view` 611 · H5 落地页系列 `landingpageDino*` 约 2 万。

**2026-08-13 全量复测附记(窗口 07-13 → 08-11,系统性扫出 24 对新旧并存事件)。**
起因:埋点明细 08-12 版补登了 `subscription_checkout_start/result` → `trigger` 的迁移关系,
而 #8 只取了旧形态,「结账有结果」段**少算 4 倍**(317 → 修正后 1,280);此前判定的
「iOS 7 天停报」「7 月口径不可比」皆为误判 —— 事件 07-28 起迁到新形态,一直在报。已修为双取。

- **迁移是按版本灰度,不是一刀切**:旧降新升、此消彼长;按 `order_id` 去重(1,535)≈ 条数(1,539),证明同一订单只报一套,**双取不会重复计**。但未升级的旧版本长期存在(`subscription_checkout_result` 旧形态 08-11 仍有),**双取是长期口径,不是过渡措施**
- 本窗口状态翻转(对照上表):`subscription_page_view` 旧止 08-04 → **已切换**;`class_stage_start/end` 新为主(32,672 / 28,446);`checkout` 两条 **已切换**(07-28 灰度起);`discount_offer_view` 旧 3,548 仍为主、新形态≈0,**未迁**
- **假配对提醒**:`event_name='login'`(17,383 条)是 Firebase 标准事件,与 `page_view+event_id=login` **无关**,不是迁移对,别合并
- 五板消费面审计结论:三个技术健康板全走 `app_diagnostic`(不在迁移范围),`signup_result` 与 `subscription_purchase_result` 无孪生形态,#7 走宽表 —— **修完 #8 后五板干净**。其余 19 对(`step_submitted`、`class_*`、`teacher_*` 等)现无看板消费,做 #5/#6/#A 时必须先查本附记与埋点明细 §3

### 3.2 取数纪律(由 §3.1 推出,强制)

1. **写 SQL 前先跑事件名清单**,确认要用的事件在窗口内实际叫什么、有没有停报:
   ```sql
   SELECT event_name, COUNT(*) n, MIN(event_date) d0, MAX(event_date) d1
   FROM `dino-english-497507.analytics_538991439.events_*`
   WHERE _TABLE_SUFFIX BETWEEN 'YYYYMMDD' AND 'YYYYMMDD'
   GROUP BY 1 ORDER BY n DESC
   ```
2. **跨迁移期的指标必须同时取新旧两套并说明**,只取一套会漏一半量
3. 文档里的 `reason` / `source` 枚举是**参考不是约束**,实际值以 BQ 为准(已知实现漂移见 §5 各看板)
4. 停报事件不可延续旧口径,必须换事件重建并注明两套数值不可比
5. **判失败一律双取 `fail` + `tech_fail`**,理由见 §3.3
6. **每次取数先跑一遍 §3.3 的词表哨兵**,出现表外新词就先查清楚再出数

### 3.3 两端把「失败」写成了不同的词(2026-08-07 实测)

Android 于 2026-07-29 起把失败改标 `tech_fail`,**iOS 大部分事件至今仍标 `fail`**。
而且 iOS 自己也不一致 —— 登录已经用新词,会话保持还是旧词。

窗口 07-07 → 08-05 内两端用词不一致的事件:

| `diagnostic_id` | Android | iOS |
| --- | --- | --- |
| `signature_refresh_result` | `tech_fail` | `fail` |
| `auth_refresh_result` | `tech_fail` | `fail` |
| `api_result` | `tech_fail` | `fail` |
| `appsflyer_conversion_result` | `tech_fail` | `fail` |
| `appsflyer_deeplink_result` | `tech_fail` | `fail` |
| `push_token_result` | `fail` / `tech_fail` | `fail` |

**这是最危险的一类坑:只认一个词,会静默丢掉整整一个平台的失败量 —— 查询不报错,页面照样出数,成功率凭空变好看。**

所以:

```sql
-- 对
COUNTIF(r IN ('fail','tech_fail')) AS failed
-- 错:iOS 的失败会全部消失
COUNTIF(r = 'tech_fail') AS failed
```

取数脚本里把它收成一个常量(`tools/gen-auth-data.py` 的 `FAILED`),**不要就地写字面量**。

另外 `result` 还有表外的取值:`skip`(`push_bind_result` 一万条、`appsflyer_deeplink_result` 一千多条)、
`cancel`(`purchase_client_result`、`push_bind_result`)。这些**既不是成功也不是失败**,
做相关看板前必须先问清楚语义,不要想当然归类。

哨兵查询(`tools/gen-auth-data.py` 每次跑都会执行):

```sql
SELECT (SELECT value.string_value FROM UNNEST(event_params) WHERE key='diagnostic_id') did,
       platform, (SELECT value.string_value FROM UNNEST(event_params) WHERE key='result') r,
       COUNT(*) n
FROM `dino-english-497507.analytics_538991439.events_*`
WHERE _TABLE_SUFFIX BETWEEN 'YYYYMMDD' AND 'YYYYMMDD' AND event_name='app_diagnostic'
GROUP BY 1,2,3 HAVING n > 20
```

---

## 4. 环境与取数条件(BigQuery)

| 项 | 值 / 要求 |
| --- | --- |
| GCP Project | `dino-english-497507` |
| Location | `asia-southeast1` |
| 取数方式 | BigQuery SQL;本地用 MCP `bigquery`(Google MCP Toolbox + ADC) |
| 凭证 | ADC:`gcloud auth application-default login` |
| 日表结算 | 查询用 `events_YYYYMMDD`;当日与前一日为 `events_intraday_*`,**不纳入看板** |
| 静态页 | 数据内嵌页面;更新 = 跑 SQL → 改 `DATA` 常量 → 改 Range / Pulled |
| **查询窗口上限** | **单次查询最长 365 天,不得跨更长区间。见 §4.4** |
| **查询准入** | **SQL 必须带日期条件,没有就拒绝执行、退回补齐(元数据查询除外)。见 §4.4 ②** |
| 禁止 | Google Analytics Data API、`user-analytics-mcp`、`run_report` 等 GA 实时报表接口 |

### 4.1 dataset

| Dataset | 用途 | 实测 |
| --- | --- | --- |
| `analytics_538991439` | Firebase / GA4 导出日表 `events_YYYYMMDD` | 日均 4~8 万行 |
| `de_ods` | 业务原始 | `user` 14,733 · `user_profile` 14,733 · `payment_order` 2,316 · `payment_event` 293 · `web_order` 123 · `analytics_event_outbox` 122,223 |
| `de_dwd` | 明细 | `dwd_firebase_event_di` 130 万 · `dwd_user_identity_map_di` 108,414 · `dwd_funnel_user_di` 32,422 · `dwd_appsflyer_installs_di` 32,422 · `dwd_payment_env_resolved_di` 2,871 · `dwd_retention_cohort_user_di` 14,695 |
| `de_dws` | 汇总 | `dws_funnel_cohort_di` 1,340 · `dws_retention_result_di` 20,669 · `dws_ops_metric_di` 1,733 · `dws_portrait_*` |
| `de_ads` | 看板宽表 | `ads_funnel_dashboard_di` 1,340 · `ads_retention_dashboard_di` 20,504 · `ads_tableau_funnel_user_di` 32,422 |
| `apps_flyers` / `google_ads_*` | 归因 / 广告导出 | 按需 |
| **`firebase_crashlytics`** ⚠️ 在 `US` | 崩溃 / ANR / 非致命异常 | 2026-08-13 起导;只有 `_REALTIME` 表,Android 42 行 / iOS 1 行 |
| **`firebase_performance`** ⚠️ 在 `US` | Firebase 自动采集:卡顿率、启动、网络 | 41 万行,08-05 → 08-11;**只用 `SCREEN_TRACE`,`NETWORK_REQUEST` 归 #5** |
| **`firebase_sessions`** / **`firebase_messaging`** ⚠️ 在 `US` | 会话 / 推送 | 未接入任何看板 |

`*_legacy` 后缀表为历史快照,**不要用于看板**。

⚠️ **四个 `firebase_*` dataset 在 `US`,其余在 `asia-southeast1`。**查它们必须显式指定 location
(`tools/bq.py` 的 `query(..., location='US')`),否则报 404。而且 **BigQuery 不允许跨区域查询** ——
它们与 GA4 日表**永远没法写进同一条 SQL**,要关联只能各查一次再在 Python 侧合并。

### 4.2 `app_diagnostic` 各 area 可用量(07-03 → 08-01 实测)

| area | 量 | 首日 | 对应看板 |
| --- | --- | --- | --- |
| `analytics` | 69,958 | 07-15 | #1(`app_context_sync_result`)、A |
| `attribution` | 31,086 | 07-14 | 归因 |
| `auth` | 26,551 | 07-14 | #1、#5 |
| (无 area) | 25,048 | 07-16 | 待归类 |
| `push` | 21,136 | 07-16 | #5 |
| `network` | 14,936 | 07-20 | #5 |
| `third_party` | 11,828 | 07-20 | #1(`sdk_auth_launch_result`)、#5 |
| `classroom` | 8,791 | 07-22 | #3 |
| `purchase` | 3,129 | 07-16 | #2 |
| `deeplink` | 2,706 | 07-16 | #5 |
| `performance` | 363 | 07-27 | #4 |
| `routing` | 10 | 07-25 | — |

**2026-08-13 复查(窗口 07-13 → 08-11):上表已过期,以下是新增或量级大变的。**做新板前按这份查,别照上表估工作量。

| area / 事件 | 30 天量 | 说明 |
| --- | --- | --- |
| **`voice`(整个 area)** | `voice_tts_result` 5,061 · `voice_asr_result` 2,722 | 上表完全没有。**2026-08-04 才上线**,窗口只有 6~8 天,还在灰度爬坡 |
| `performance` | **17,034**(原 363) | #4 的埋点阻塞已解除;崩溃 / ANR 仍在 Crashlytics,未入 BQ |
| `auth` / `auth_refresh_result` | 15,514 | 未登记,归 #5 |
| `auth` / `http_unauthorized_result` | 79 | 未登记,归 #5;量太少只能做存在性记录 |
| `analytics` / `app_context_sync_summary` | 69,770 | 未登记 |
| `analytics` / `analytics_setup_result` | 42,504 | 未登记 |
| `analytics` / `posthog_setup_result` | 3,702 | 未登记 |
| `routing` / `h5_webview_load_*` | 3,481 | **2026-08-07 新上**;原表 `routing` 只有 10 条 |

**首日要按 `diagnostic_id` × platform 查,不能只按 area。**同一个 area 下不同 id 的上线日能差两周,
两个平台也各上各的。登录两段 + ID 同步附注项实测:

| `diagnostic_id` | Android 首报 | iOS 首报 |
| --- | --- | --- |
| `auth_login_result` | 07-28 | 07-27 |
| `sdk_auth_launch_result` | 07-29 | 07-29 |
| `app_context_sync_result` | 07-23 | 07-16 |

```sql
SELECT (SELECT value.string_value FROM UNNEST(event_params) WHERE key='diagnostic_id') did,
       platform, MIN(event_date) first_day, COUNT(*) n
FROM `dino-english-497507.analytics_538991439.events_*`
WHERE _TABLE_SUFFIX BETWEEN 'YYYYMMDD' AND 'YYYYMMDD' AND event_name='app_diagnostic'
GROUP BY 1,2 ORDER BY 1
```

由此推出两条硬规矩:

1. **窗口标事件自己的首报日,不是查询区间。**写「30 天」而实际只有 8 天,读者会把灰度期的爬坡量当成日常水位。
2. **灰度期的总量不是水位。**实测把 30 天窗口只往后挪 3 天,段②总量从 672 涨到 1,545 —— 那是铺量,不是增长。凑不满窗口就**不要算环比**。

### 4.2.1 最新一张日表还会被回填

实测:2026-08-03 12:15 取的 08-01 是 290 条,08-04 09:10 重拉变 301 条(+11),主指标从 95.21% 掉到 94.74%。

所以窗口终点取**最新一张已结算日表**(`events_intraday_*` 一律排除),并且**这一天要在页面上标成暂定** ——
不删数,但不许当定论。做法见 PAGE-STANDARD §8.2。

### 4.3 原始事件 SQL 约定

```sql
FROM `dino-english-497507.analytics_538991439.events_*`
WHERE _TABLE_SUFFIX BETWEEN '20260703' AND '20260801'

TIMESTAMP_MICROS(event_timestamp) AS event_time_utc
event_date   -- STRING YYYYMMDD

(SELECT value.string_value FROM UNNEST(event_params) WHERE key = 'diagnostic_id') AS diagnostic_id
```

缺字段 / 空表时:表不存在、分区无数据、或 `event_params` 无该 key → 页面注明「条件未满足」,**不编造数**。

### 4.4 查询成本闸门(硬规则,防止 BigQuery 超预算)

BigQuery 按**扫描字节数**计费,不按返回行数。一条没写过滤的查询和一条只取一天的查询,返回结果可能一样,账单差两个数量级。以下四条是硬规则,写 SQL 前逐条过。

**① 时间窗口上限 365 天。**任何一次查询的时间跨度不得超过一年,**没有例外**。需要更长回溯必须先人工确认,并拆成按年的多次查询、每次单独看账单。

**② 必须有日期条件,否则拒绝处理。**这条是**准入门槛,不是建议** —— 一条 SQL 没写日期过滤,不要跑、不要「先试一下看看」,直接退回去补日期。人(或 AI)拿到这种 SQL 的正确反应是:**拒绝执行,说明缺什么,补完再跑。**

| 表类型 | 必须写的过滤 | 反例(禁止,见到就退回) |
| --- | --- | --- |
| GA4 日表 `analytics_538991439.events_*` | `WHERE _TABLE_SUFFIX BETWEEN 'YYYYMMDD' AND 'YYYYMMDD'` | `FROM ...events_*` 不带 `_TABLE_SUFFIX` = 扫全部日表 |
| `de_dwd` / `de_dws` / `de_ads` 的 `*_di` 分区表 | 分区日期字段的 `BETWEEN` / `>=` | 无日期条件的 `SELECT ... FROM ..._di` |
| `de_ods` 业务表 | 有时间字段就必须带上 | 有 `created_at` 却不过滤 |
| **`firebase_performance.*`**(分区表) | **`_PARTITIONTIME BETWEEN`** | 不写就扫全表 41 万行 |
| **`firebase_crashlytics.*_REALTIME`** | `event_timestamp` 的范围条件 | — |

多表 JOIN / UNION 时**每一张事实表各自都要有日期条件**,不能只给其中一张写、指望优化器把裁剪传递过去 —— 它不会。

唯一豁免:元数据查询(`__TABLES__`、`INFORMATION_SCHEMA.*`)不扫数据行,不受此条约束。除此之外没有例外,包括「就查个 COUNT(*)」「只要 MIN(event_date)」—— `COUNT(*)` 在通配表上照样要列出全部日表,`MIN(event_date)` 要把该列全读一遍。真要看边界,用 `__TABLES__` 里的 `table_id`。

**③ 禁止 `SELECT *`。**BigQuery 是列存储,只为读到的列付钱。`events_*` 里 `event_params`、`user_properties`、`device` 这几个嵌套列占了绝大部分体积,`SELECT *` 会把它们全读一遍。只写要用的列;取参数一律用 `(SELECT value.string_value FROM UNNEST(event_params) WHERE key='xxx')` 的写法。

**④ 不确定量级时先 dry-run 估字节。**探索新表、写新 SQL、或窗口超过 90 天,先估再跑:

```bash
bq query --dry_run --use_legacy_sql=false --location=asia-southeast1 'SELECT ...'
```

估出来超过 **10 GiB** 就先收窄窗口或收窄列,不要直接跑。

**默认取最小够用窗口,不要因为「上限是一年」就取一年。**看板主表 30 天、趋势 14 天,这是常态;一年是天花板不是默认值。

参考量级(2026-08-10 实测):`events_*` 全量 40 张表 3.38 GiB,日均约 79 MiB;全项目 8 个 dataset 合计约 7.3 GiB 逻辑字节。也就是说,**当前 GA4 只有 37 天数据,一年上限对它还不是绑定约束 —— 现阶段真正烧钱的是 ② 和 ③,不是 ①。**①是给数据攒厚以后兜底的。

---

## 5. 口径总则

### 5.1 终态四分类(关键链路 §2.2,技术健康的地基)

每条链路终态强制分四类,**不要只有成功 / 失败**:

| result | 含义 | 是否进技术分母 |
| --- | --- | --- |
| `success` | 成功 | ✅ 分子 |
| `tech_fail` | 技术失败(网络 / 超时 / 5xx / 崩溃 / SDK 错误) | ✅ 分母,技术健康主指标只盯这个 |
| `user_abort` | 用户主动取消 / 返回 | ❌ 单独切片 |
| `rejected` | 服务端业务拒绝 / 不满足条件 | ❌ 单独切片 |

**技术成功率 = `success` ÷ (`success` + `tech_fail`)**

`reason` 也要预先标「技术类 / 非技术类」,方便只看技术失败。

**例外:支付链路(§3.2)已定稿不迁四分类**,维持 `success` / `fail` / `cancel` / `deferred` / `skip` 五值,公式见 #2。做支付看板时不要套四分类。

### 5.2 公共维度

关键链路 §2.3 要求所有链路支持:版本 · 平台 · 网络类型 · 地区 · 是否会员 · 用户 / 设备标识。

字段来源(埋点规范 §2.1):

| 维度 | 取法 | 注意 |
| --- | --- | --- |
| 版本 | `app_info.version` | 顶层字段 |
| 平台 | `platform` | 顶层,值为 `ANDROID` / `IOS` |
| 地区 | `geo.country` | **顶层字段,不是 event_params**;研发不上传 country 自定义参数 |
| 用户 | `user_id`(= 业务 uid) | **顶层字段,不在 event_params**;`app_diagnostic` 另有 `uid` 参数 |
| 设备 | `device_id`(params) / `user_pseudo_id`(顶层) | 两者不等价 |
| 网络类型 | `network_type`(params) | 枚举 wifi / 2g / 3g / 4g / 5g / offline |
| 是否会员 | `is_premium`(params) | 规范未列为公共属性,但实际在报 |

`trace_id` 尚未落地(关键链路 §2.4 待讨论),因此**无法还原单次链路**,只能靠 area / id / step + result + reason 聚合。客诉排查类需求目前做不到。

**版本对照口径(2026-08-13 起,三个技术健康板)。**看修复是否生效用**对照不用过滤**:
主指标保持全版本;各板加「按版本档对照」块,按大版本分档(1.5.0 / 1.5.1 合成 1.5.x),
该板**分母事件**的用户数不足 50 人的档并进「其他」—— 一两个人的版本会画出 100% / 0% 的假信号
(实测 1.5.2 全网 1 人)。分档合并**必须在 SQL 做**:去重用户数不可加,升级过的用户会同时出现在两个档里。
不追「最新版本号」:新版本自动成档,取数脚本对 `KNOWN_VG` 之外的档在 stderr 提醒(首日数据慎读)。
漏斗板不加 —— 转化掉量不是靠版本证明的事。

### 5.3 读数纪律(埋点规范 §7 + §2.3)

- 分析主键优先 `uid`;登录前后合并见 `de_dwd.dwd_user_identity_map_di`
- **PII 不作为看板展示或筛选来源**;`result` 不上传原始堆栈、用户输入、手机号、邮箱、Token
- **订阅成交以服务端 `subscription_purchase_result` 为准**;客户端 `subscription_checkout_result` 与支付诊断 `success` 均 ≠ 营收
- 注册成功以服务端完成账号创建和绑定为准;三方授权成功但未建号应为 `is_success=false`
- `appsflyer_id` **已于 2026-07-28 从事件属性中删除**,归因改由服务端用户表 `appsflyer_id` ↔ `uid` / `device_id` 映射
- AppsFlyer `purchase` 仅 `success` 且 `initial_purchase` 时回传,续费不回传;收入 / LTV / 续费看 `subscription_purchase_result`
- 不重复计入 Firebase 自动事件(`first_open` / `app_open` / `session_start`)

### 5.4 健康分档与着色标准(所有看板通用)

数值本身不说明好坏,**分档由本节定义,看板只负责按档上色**。首次落地于 [`health-auth.html`](./health-auth.html),后续看板照抄。

#### 分档阈值

作用对象只有**技术成功率**(§5.1 的 `success` ÷ (`success` + `tech_fail`)):

| 档 | 区间 | 浅色 | 深色 | 用在哪 |
| --- | --- | --- | --- | --- |
| 危险 | < 95% | `#e03131` | `#ff6b6b` | 进度条填充、百分比数字 |
| 需关注 | 95% – 99% | `#fab219` | `#fab219` | 同上 |
| 健康 | ≥ 99% | `#0ca5b8` | `#22c3d6` | 同上 |
| 不判档 | 分母无失败样本 | 中性灰 | 中性灰 | 同上 |

小字号(≤ 14px)另用深一档的文字色以保 4.5:1 对比度:危险 `#c92a2a` / 需关注 `#8a5b00` / 健康 `#0b7d8c`;大字号(≥ 24px)用表内色值即可(≥ 3:1)。

#### 三条强制规则

1. **分母里没有失败样本 → 不判档,中性灰。** 100% 只说明窗口内没有失败记录,不等于健康。典型:`sdk_auth_launch_result` 按事件定义就记不到失败(§6.1.1 段①);`app_context_sync_result` 的登录触发只有 iOS 上报(附注项)。这两处涂绿/涂青就是虚假安全感。
2. **分子恒为 0 的段不算成功率,显示 `—`。** `auth_login_result` 的 `authorization` 段只在 `user_abort` / `tech_fail` 时上报,没有 `success` 记录;若照算会得到「0.00% 危险」,是假的。
3. **只有技术成功率能上分档色。** 放弃率、拒绝率、字段覆盖率、耗时 P50/P95 一律不着色 —— 它们不是健康指标,`user_abort` / `rejected` 本来就不进技术分母。

#### 事件结果构成的颜色(图表与归因表)

与分档色共用同一套,不另起一套:

| result | 色 | 说明 |
| --- | --- | --- |
| `success` | 青(同「健康」) | 堆叠柱的成功段 |
| `user_abort` | 琥珀(同「需关注」) | 非技术,单独切片 |
| `tech_fail` | 红(同「危险」) | 技术健康主指标只盯这个 |
| `rejected` | 橙 `#ec835a` | 业务拒绝,非技术 |

#### 呈现纪律

- **页面其余部分只有黑白灰,有颜色的地方一定在说数据。**章节号、导航、边框、表头不许用彩色
- 颜色不得单独承载信息:状态一律「色点 + 文字」,百分比旁边必须有数值
- 看板顶部必须有一条**颜色说明 bar**,写明三档阈值与灰档含义;bar 用连续渐变(红 → 琥珀 → 青),横轴按真实百分点作图,轴起点若不是 0 必须标注
- 浅色 / 深色两套色值都要给,深色不能由浅色自动反转

#### 每个技术健康看板必备的四个组件

文档「整体看板 & 播报」+ §3.x 专属维度推出的硬要求,#1 已落地,后续看板照做:

1. **播报行** —— 一行给出「量 · 技术健康率(环比) · 关键耗时 P95 · Top 技术失败原因」。环比取不到时显式写不可用及原因,不留空。
2. **版本对照** —— 默认按版本切片对比**新版 vs 上一版**(每天 14:30 过版本),关键指标成对给出 + 差值(pp)。不是把版本混在一张维度表里。
3. **漏斗** —— 只对**同一事件、同一分母**的链路画漏斗;跨事件/跨窗口的段落不得叠成漏斗(#1 两段就不构成漏斗,真漏斗在段② 内部)。左对齐条 + 灰色收窄楔形,转化率与流失原因标在楔形上。
4. **归因面板(主指标非「健康」时强制)** —— 见下。

##### 4. 归因面板:主指标只要不是「健康」,就必须当场解释为什么

**规则:主指标落在「需关注」或「危险」档时,页面必须紧跟一块归因面板。**只标红不解释,等于把定位工作丢给读者;present 时第一个问题必然是「为什么」,答不上来这页就白做。

面板固定回答四问,顺序不变:

| # | 回答什么 | 怎么呈现 | #1 的实例 |
| --- | --- | --- | --- |
| 1 | **失败由什么构成** | 全部 `tech_fail` 按 `reason` 归一到 100% 的堆叠条 + 图例带次数 | 30 次 = signature_failed 9 / unavailable 9 / unknown 6 / server_error 3 / sdk_timeout 2 / network_error 1 |
| 2 | **集中在谁身上** | 每个主因一行「谁 · 何时 · 所以」;「谁」必须下钻到**平台 × 方式 × 入口**级别,不能停在 reason | 三个主因各占 30%/30%/20%,分别 100% 落在 Android×google×welcome、Android×google 授权段、iOS×apple 授权段 —— 前三项占 80% |
| 3 | **是不是某一天造成的** | 尖峰日单独拎出:当日成功率 vs 其余天成功率 | 08-01 单日 16 次(53%),当天 92.63%;07-27→07-31 为 96.03% —— **是这一天把窗口拖过分档线** |
| 4 | **跟别的链路有没有同源** | 显式写出跨看板/跨段的因果,并指向对应小节 | 登录段的 signature_failed 与 §5 `signature_refresh` 同日崩到 19.4% **同源**,是签名机制故障外溢 |

硬要求:

- **主因的「谁」必须是可执行的**:能直接说出该找哪端、哪个登录方式、哪个入口。写到 `reason` 就停等于没归因。
- **区分「集中」与「普遍」**:前三因占比 ≥ 70% 就要明说「不是普遍性劣化」,否则读者会默认全线崩。
- **尖峰日必须单独算**:给出剔除尖峰日后的成功率,让人看到基线水位。
- **文档外的 `reason` 在归因面板里也要标 ✳**,与失败归因表口径一致。
- 面板用左侧危险色标条 + 白底,不整块铺红 —— 铺红会和分档色抢注意力。

#### 开放项

`health-auth.html` 现状:危险 46 格、需关注 25 格、灰 16 格、**健康 0 格** —— 凡有失败样本的指标没有一个到 99%。是照实保留(说明现状确实不健康),还是把健康线放宽到 ≥ 98%(`auth_login` 98.28%、`backend_exchange` 小计 98.17% 会转青),**待定**。改动只涉及阈值一处。

---

## 6. 看板清单与完成条件

### 6.1 技术健康(口径来源:关键链路文档)

| # | 看板 | 主指标 | BQ 条件与实测 | 状态 |
| --- | --- | --- | --- | --- |
| A | 总览 + 播报 | 每链路:量 · 技术健康率(环比)· 关键耗时 P95 · Top 失败原因;按版本对比 | 各 area 汇总,须等各分页口径定稿 | 待做 |
| 1 | 登录注册健康 | 两段技术成功率 + ID 同步附注 | 见 6.1.1 | **已完成** · [health-auth.html](./health-auth.html)(数据 2026-08-10,窗口 07-09 → 08-07) |
| 2 | 支付订阅健康 | load / purchase / restore 三段 | `area='purchase'` 6,692 条 | **已完成** · [health-purchase.html](./health-purchase.html)(数据 2026-08-10,窗口 07-09 → 08-07) |
| 3 | 进入教室健康 | 点击 → 就绪 | `area='classroom'` 1.2 万条(另 4 万条未登记 classroom_*) | **已完成** · [health-classroom.html](./health-classroom.html)(数据 2026-08-10,窗口 07-09 → 08-07) |
| 4 | 崩溃 & 性能 | 冷启动 / 首屏就绪 / 内存 / 卡顿 / 崩溃 · ANR | `area='performance'` **17,034 条** + `firebase_performance` 41 万 + `firebase_crashlytics` | **已完成** · [health-performance.html](./health-performance.html)(数据 2026-08-14)。**口径以 [PERFORMANCE.md](./PERFORMANCE.md) 为准** |
| 5 | 网络 & 三方 | 请求成功率(**剔业务 4xx,且成功侧需还原 10% 采样**) | `network` 14.9 万 + `third_party` 11.5 万 + `push` 6.2 万 + `auth` 会话保持 22 万 + `deeplink` 6.5 千 | **已完成** · [health-network.html](./health-network.html)(数据 2026-08-14,窗口 07-14 → 08-12)。**口径以 [NETWORK-THIRDPARTY.md](./NETWORK-THIRDPARTY.md) 为准** |

#### 6.1.1 登录注册健康(#1)

> **2026-08-07 口径修订。**关键链路文档 2026-08-06 改稿,明确写了 `app_context_sync_result`
> 「**这个和登录流程不存在联系**」「**不阻塞任何登录流程**」,它是冷启动同步 Firebase ID / AppsFlyer ID 用的。
> **登录因此从三段改回两段**,该事件降为附注。
> 本项目此前把它当「登录第三段」并作为头号严重问题上报,是照旧稿做的 —— 已在 `health-auth.html` 更正。

**登录本身两段**,分属两个不同的 `diagnostic_area`(这是最容易漏的点):

| 段 | `diagnostic_id` | `area` | `step` |
| --- | --- | --- | --- |
| ① 三方授权 UI 拉起 | `sdk_auth_launch_result` | **`third_party`** | `authorization` |
| ② 登录授权 / 换 session | `auth_login_result` | `auth` | `authorization` / `backend_exchange` |

附注(**不属于登录链路,不进登录成功率**):

| | `diagnostic_id` | `area` | `step` |
| --- | --- | --- | --- |
| ID 同步 | `app_context_sync_result` | **`analytics`** | `context_sync` |

实测(2026-07-07 → 08-05,末日暂定):

| 指标 | Android | iOS |
| --- | --- | --- |
| ① 授权页拉起 | 1,531 条 · 99.93%(**只有 1 次失败,不可信**) | 361 条 · 100%(零失败记录) |
| ② 登录换 session | 1,949 条 · **94.60%** | 619 条 · **89.51%** |
| ② 用户自己放弃 | 445 次(22.8%) | 131 次(21.2%) |
| ② 后端换登录态耗时 | P50 8.79s · P95 26.3s | P50 7.87s · P95 37.6s |
| 附:ID 同步(登录/注册触发) | **0 条** | 1,118 条 |
| 附:ID 同步(冷启动等) | 4,693 条 · 失败 278 | 6,870 条 · 失败 136 |
| 签名刷新(暂挂) | 79,241 条 · **19.0%** | 7,716 条 · 46.4% |

**目前无判别力 / 需澄清的地方:**

- **段① 接近 100%,不可信。** Android 1,531 次里只有 1 次失败,iOS 361 次零失败。按文档判定口径「UI 已呈现即算成功;未呈现但 SDK 成功则不上报」,失败只在「未呈现且 SDK 失败/取消」时才报 —— 但光「用户取消」就不可能只有这么点。文档的三态判定(`presented`/`notPresented`/`unknown`)**只写了 iOS 怎么实现**,Android 侧是否实现要问。
  判据用**失败率**不用「恰好等于 0」:实测冒出一条迟到的失败记录就会让「零失败」判据失效,把灰色的「不可信」翻成绿色的「健康」。见 PAGE-STANDARD §3.2。
- **附注项两端不一致,但文档自己前后打架。** 正文说「和登录流程不存在联系」,**字段表却仍写着 `source=login_success | signup_success`**,示例 JSON 也是这个值。实际 Android 这两个触发点 0 条、iOS 1,118 条。
  **该确认的是「这个触发点还要不要」,不是直接判 Android 漏埋。**
  数据佐证新口径:九成触发来自 `cold_start`(8,570)与 `foreground_resume`(2,993),登录/注册触发只占 9.6%。

口径:

- **拉起成功率** = `success` ÷ (`success` + `tech_fail`),`user_abort` 不进分母。**该事件故意不带 `duration_ms`**(避免把用户停留授权页时间误读为拉起耗时)
- **登录成功率**:`result` 五值 `success` / `tech_fail` / `user_abort` / `rejected` / **`deferred`**(Kakao 补绑手机号 `phone_binding_required`,窗口内为 0)。**文档要求按 `target` 拆段看**,`target` 枚举:`apple_sdk` / `google_sdk` / `kakao_sdk` / `facebook_sdk` / `auth_login` / `sms_login` / `kakao_bind_phone`
- **ID 同步(附注项)**:`result` 只有 `success` / `fail`(不是 `tech_fail`);`source` 文档字段表只写 `login_success` / `signup_success`,**实际主力是 `cold_start` 8,570 条与 `foreground_resume` 2,993 条**。按新口径它不进登录成功率,做看板时按 source 切片单独看
- **注册 `signup_result` 的登录方式字段两端叫法不同**:文档迁移表写「`signup_method` 统一为 `method`」,但实测**只有 `web_h5` 与 `platform` 为空的行用了新名**(2,907 条),Android / iOS 仍是 `signup_method`(13,338 条)。必须 `COALESCE(method, signup_method)` 双取。另外 `platform` 大小写不统一(`android`/`Android`、`ios`/`iOS`),要 `LOWER()` 归一
- **`signup_result` 曾被误判为停报**:08-10 快照只到 iOS 07-31 / Android 08-04；08-15 回看 07-15～08-13 时两端 30/30 天都有数据。该事件由服务端上报，末段空白是晚到/回填而非已证实的客户端断报；没有 FINAL 水位前仍不能直接比较未成熟日。
- `authorization` 段**只在失败 / 放弃时上报,无 `success` 记录**。登录尝试数 = `backend_exchange` 全量 + `authorization` 失败量
- 专属维度:`auth_method`(google / phone / facebook / kakao / apple) × `auth_entry_source`(onboarding / welcome / welcome_first)
- 耗时只取 `backend_exchange` 且 `result='success'` 的 `duration_ms`
- 注册用 `signup_result`(独立 event_name),终态主字段是 `is_success`；`result` 仅在部分失败中补充 `account_unavailable`，另有失败未分类，`reason/status/biz_code/error_code` 均缺失。失败原因必须单列已分类/未分类，不能只读 `result` 反推终态；落地页场景字段缺失仍须单列未上报量
- 产品侧登录漏斗(`page_view`/`login`、`click`/`login_*`、`phone_code_request_result`、`login_otp_submit`、`phone_binding_result`)**不用于技术健康率**,只用于 #7

已知实现漂移:`authorization` 段实际出现 `reason=interrupted` / `unavailable`,均不在文档枚举(文档为 cancelled / token_missing / no_presentation_anchor / nonce_generation_failed / sdk_timeout / provider_not_registered / unknown)。

着色按 §5.4:段① 与 ID 同步附注项的登录触发部分**失败少到不可信 → 中性灰不判档**(判据用失败率,不用「恰好等于 0」);`authorization` 段四个 `*_sdk` 目标**无 `success` 记录 → 成功率显示 `—`**,不得算成 0.00%。

2026-08-04 重拉后新增的实测结论(均已落到 `health-auth.html`):

| 结论 | 证据 | 影响 |
| --- | --- | --- |
| 段① 无判别力**指向 Android** | 604 条中 Android 552 条全 `success`;文档 A1 的 `presented/notPresented/unknown` 三态观测只描述 iOS | 该问 Android 是否实现「未呈现」判定,不是问「分支会不会上报」 |
| **布尔字段类型双端不一致** | `is_new_user` / `membership_seeded` / `has_onboarding_payload`:Android 报字符串 `"true"/"false"`,iOS 报整数 `1/0` | BigQuery 落 `string_value` 与 `int_value` 两列,**取数必须双取**,否则静默丢一端 |
| 入口维度缺两类 | `auth_entry_source` 实际只有 `onboarding`/`welcome`/`welcome_first`;文档 §3.1 要求含**深链、被踢回** | 「签名刷新失败 → 被踢回重登」的影响无法量化 |
| `reason` 枚举大面积未验证 | authorization 段 7 个枚举只出现 3 个;backend_exchange 段 9 个只出现 4 个 | 未出现 ≠ 没发生,与段① 同类问题,需逐条确认可触发性 |
| 新版 1.5.0 登录更差、会话更好 | 段② 86.30%(63/73) vs 1.4.2 95.98%(477/497),低 9.67pp;但 `signature_refresh` 1.5.0/Android 53.03% vs 1.4.2/Android 21.17% | 1.5.0 仅 109 条,放量后复核;需确认是否为已知取舍 |
| **数据会追平** | 08-03 12:15 取数时 08-01 为 290 条,08-04 09:10 重拉为 301 条(+11 迟到事件),主指标 95.21% → **94.74%**,跌破 95% 分档线 | 发版当天/次日的数不作最终结论,隔日复核 |
| **登录段的 `signature_failed` 与 §5 签名刷新同源** | 9 次 `signature_failed` 全部 Android×google×`welcome` 入口,8 次在 08-01,与 `signature_refresh` 当天崩到 19.4% 同日;08-01 单日贡献 16/30 次技术失败,当天 92.63% vs 前五天 96.03% | 主指标跌破 95% 是签名故障外溢的连带结果,**不是登录链路整体退化**;修 #5 的签名问题应同时抬升 #1 |
| `is_premium` 客户端存在但登录事件没带 | 同 `area=auth` 的 `signature_refresh_result` 有 131 条带 `is_premium`,`auth_login_result` 796 条为 0 | 推动时应说「登录事件补带」,不是「客户端补字段」 |

**`signature_refresh` / `token_refresh` / `unauthorized` 虽然 `area='auth'`,但按文档语义属于 §3.5 网络三方(鉴权刷新与 401 处理),归 #5。**
~~当前临时放在 `health-auth.html`~~ —— **2026-08-14 已迁往 [health-network.html](./health-network.html) 第 1 节**,登录板原位只留跳转卡片。
登录板第 1 节的版本档对照**仍保留「签名刷新」一列**作对照(修复验证要两列一起看),算法与 #5 一致,不是重复出数。

#### 6.1.2 支付订阅健康(#2)

关键链路 §3.2 已于 2026-07-29 全部定稿,无开放待确认项。三段:

| 段 | `diagnostic_id` | `step` | 成功率口径 |
| --- | --- | --- | --- |
| 加载 | `paywall_load_result` | `load` | `success` ÷ 全部;每次打开只报 1 条 |
| 购买 | `purchase_client_result` | `storekit_purchase` | `success` ÷ (`success` + `fail`) |
| 恢复 | `restore_client_result` | `restore` | `success` ÷ (`success` + `fail`);`skip` 单独看 |

硬标准:

- `area='purchase'`;`source` **固定 `paywall`**(含 WinBack / 会员页),入口只走 `params.target`
- **不迁四分类**,维持五值 `success` / `fail` / `cancel` / `deferred` / `skip`;技术失败率**只看 `fail`**,`cancel` / `skip` / `deferred` 单独切片
- 绑他号 = `fail` + `reason=bound_other_account`(**不是 `rejected`**)
- 购买段 `target` 用入口短码,与 `subscription_checkout_start.source` 同枚举;下单透传串不得当 target
- `reason` 枚举:load `network_error` / `billing_unavailable` / `empty_products`;purchase `storekit_failed` / `product_unavailable` / `purchase_in_progress` / `verification_unconfirmed` / `bound_other_account`;restore `no_restorable_purchase` / `offline` / `verification_failed`
- 平台差异:iOS load 成功可报 `target=member|purchase`,Android 可只报 `purchase`;`restore` 的 `not_logged_in` 为 Android-only
- 成交信服务端 S2S,客户端诊断 `success` ≠ 营收
- 三段诊断**无 SKU**(SKU 只在产品事件里),入口 × SKU 下钻做不了

#### 6.1.3 进入教室健康(#3)

- `area='classroom'`;**分母 `class_enter_click`**,分子 `class_webview_load_success` / `class_agora_connect_success`
- 失败:`class_enter_failed`(`invalid_url` / `network_error`)、`class_webview_load_failed`(`load_timeout` / `load_fail`)、`class_agora_connect_failed`(`rtc_disconnected` / `rtc_failed` / `rtc_error`);均 `result=tech_fail`,原始错误码放 `description`(如 `error_-1017`)
- 用户主动退出:`class_user_exit`,`result=user_abort`
- 耗时:`duration_ms` 合并在 `class_webview_load_success`,口径 = 点击 → WebView `onPageFinished`,**不再有独立耗时事件**
- **RTC 成功以 `onSessionStarted` / `onLocalUserRegistered` 为准,不是 `joinChannel` 返回 `ERR_OK`**
- `source` 枚举:`home_button` / `course_list` / `learning_path` / `theme_course` / `teacher_select` / `deeplink`;空字符串为兜底,不应出现
- 文档 §6 有 iOS 实测日志,另有表格未登记的 `classroom_enter_result` / `classroom_rtc_result` / `classroom_webview_start_loading`,做看板时需确认是否纳入

#### 6.1.4 崩溃 / 性能(#4)· 阻塞

- `cold_start_result`:`duration_ms` = 进程启动 → 首帧;`target` 为首个可见 Activity(当前 `SplashActivity`),**不含 Splash → MainActivity 路由时间**,端到端需结合 `page_ready_result(target=home)`
- `page_ready_result`:`target=home` / `login`;失败走 `result=tech_fail`;`rendered=false` 时 `duration_ms` 退化为 `data_ready_ms`
- `mem_snapshot`:`total_pss_kb` / `java_heap_kb` / `native_heap_kb` / `mem_tier` / `app_foreground`;`target=home(phase=enter)` / `classroom(phase=enter/exit 配对)`,**系统直接杀进程时 exit 缺失,配对分析要允许缺失**
- 后台预拉起记 `result=rejected` + `reason=background_start`,不带 `duration_ms`
- **崩溃 / ANR 走 Crashlytics,未入 BQ → 页面标缺口**
- 文档明写待确认:SplashActivity 到其他页面是数据空白地带
- ~~阻塞原因:`area='performance'` 窗口内仅 363 条~~ —— **2026-08-13 复查已涨到 17,034 条**(30 天,双端齐全:
  `mem_snapshot` 6,072 · `cold_start_result` 5,839 · `page_ready_result` 5,123),**埋点阻塞解除,可以开做**。
  但**崩溃 / ANR 本身仍不在 BQ**,页面只能覆盖性能部分 —— 建议改名「性能健康(崩溃待接入)」,
  挂着「崩溃」的名字却没有崩溃数据会误导

#### 6.1.5 网络 & 三方(#5)

> **口径唯一来源是 [`NETWORK-THIRDPARTY.md`](./NETWORK-THIRDPARTY.md)**,本节只留摘要。有分歧以那份为准。

- 主指标:请求成功率(**剔业务 4xx**)
- 覆盖:各接口成功率 / 耗时、**鉴权刷新与 401 处理**、深链解析、三方 SDK 初始化
- 数据源:`area='network'` 14,936 + `third_party` 11,828 + `deeplink` 2,706 + `push` 21,136,以及从 #1 迁入的 `auth` 域会话保持三段
- 专属维度:接口(endpoint) × 错误类型 × SDK

**开发时踩到的四件事,别的板也可能撞上:**

1. **成功侧 10% 采样,失败全量**(客户端 `ApiResultConverterFactory` 只对 success 抽样)。
   成功率必须先还原 `success ÷ 0.1` 再算,**直接相除会算出个位数的假暴跌**。
   中签样本带 `sample_rate` 字段 —— 但**线上全版本目前一条都没有**,取数脚本每次跑会探测。
2. **业务 4xx 必须按 `failure_reason='business'` 剔,不能按 `result` 剔。**
   Android 已迁四分类报 `rejected`,**iOS 仍是二分类**,把业务 4xx 和用户取消全塞进 `fail`。
3. **每个成功率出两个口径:按请求、按设备。**少数设备死循环重试(签名刷新每台平均失败 43 次)会让两个口径
   给出**相反的结论**,着色以按设备为准。设备口径是 `1 − 遇到过失败的设备 ÷ 全部设备`,
   **不能写成 `u_ok ÷ (u_ok + u_f)`** —— 同一台设备既有成功又有失败,分母会重复计数。
4. **`reason='offline'` 是 DNS 解析失败**(`UnknownHostException`),**不是「没网」**。
   它和 `network_type` 是两个独立口径,交叉起来才能把「真断网」和「有网但域名解析不了」切开
   (实测 Android 后者占 69.6%,iOS 反过来 91.4% 是真断网)。

### 6.2 产品转化 / A/B

| # | 看板 | 主指标 | BQ 条件与实测 | 状态 |
| --- | --- | --- | --- | --- |
| 6 | A/B 实验总览 | 进组新装 7 日订阅支付成功率(B vs A);SRM;护栏 | **前置条件不满足,见 6.2.1** | 阻塞 |
| 7 | 新手转化漏斗 | PRD §7.4 锚点逐层到达率 | `dwd_funnel_user_di` 34,749 人 | **已完成** · [funnel-onboarding.html](./funnel-onboarding.html)(数据 2026-08-10) |
| 8 | 支付产品漏斗 | 曝光 → checkout → 成功/取消/失败 | 见 6.2.2 | **已完成** · [funnel-payment.html](./funnel-payment.html)(数据 2026-08-10,数据质量问题已标注) |
| 9 | 有效学习 / 北极星 | 有效学习分钟数 / 周 | **定义缺失**,见 §2.1 | 阻塞 |

#### 6.2.1 A/B 实验总览(#6)· 阻塞

PRD §7.5 口径:

- **主指标(唯一发布依据)** = 进组后 **168 小时**内产生 `subscription_purchase_result(result=success)` 的设备数 ÷ 进组设备数。SKU 含免费试用期时以订阅开通为准
- 归属:未登录按 `device_id`,注册 / 登录成功后服务端绑定 `uid`;Firebase 事件通过顶层 `user_id` 关联。**PRD 明确不设任何 Firebase User Properties**
- 护栏(任一显著恶化即暂停放量):登录 / 注册失败率、分组成功率(fallback 占比)、崩溃率、首课完课率、D1 / D7 新装留存、7 日退订退款率
- §7.4 给了完整的 A/B 漏斗对齐锚点表(进组 / 登录页曝光 / 登录方式 / 登录结果 / 注册 / Onboarding 完成 / 计划页 / Paywall / 首课 / 报告 / 发起支付 / 支付取消失败 / 支付成功),分母统一为进组设备

**三条阻塞(2026-08-03 实测):**

1. **唯一真相源不在 BigQuery。** PRD §7.1 规定服务端分组明细表同步 BQ、作为圈样本的唯一名单。实际扫遍 `de_ods` / `de_dwd` / `de_dws` / `de_ads` **没有任何实验 / 分组表**。事件 `trigger`+`event_id=experiment_group_assign` 仅 691 条(channel = a / b / fallback),PRD 明说它只用于校验分组量与 SRM,**不是真相源**
2. **分子样本过小。** `subscription_purchase_result` 窗口内 2,801 条,其中 `result='success'` 仅约 42 条。以 691 进组设备为分母,任何 B vs A 差异都不可能显著
3. **读数指南缺失。** 样本剔除、分层、显著性、样本量、SRM 判据全部未知(见 §2.1)

**结论:在分组明细表进 BQ 且积累足够样本之前,#6 只能做「SRM 与埋点完整性监控页」,不能做发布决策页。**

#### 6.2.2 支付产品漏斗(#8)

- 锚点:`page_view`+`event_id=subscription` → `trigger`+`event_id=subscription_checkout_start` → `subscription_checkout_result` → `subscription_purchase_result`
- **成交只信 `subscription_purchase_result`**;`result` 为 `pending` / `success`,`type` 为 `initial_purchase` / `renewal`,同一 `order_id` 可先 pending 后 success
- 该事件带 `price` / `currency`(USD)/ `cycle` / `channel` / `content_id`(SKU),**可直接算收入与 ARPU**;`transaction_id` / `original_transaction_id` 用于关联续费
- 已知数据质量问题:2,801 条中 **2,426 条 `result` / `type` / `cycle` / `channel` 全为空**(仅 `price` 有值,合计 38,262),疑为旧口径并存。**做看板前必须先查清这 2,426 条属于哪套口径,否则收入会算错**
- `channel` 实际出现 `Apple` / `Google`(旧)与 `apple_iap` / `google_play`(新)两套写法,须归一
- 前端取消 / 失败原因看 `subscription_checkout_result.result`,该字段**不设统一枚举,各端原样上报**(实测有 `user_cancel` / `storekit_unknown` / `1` / `5`),聚合前需清洗

---

## 7. 更新检查清单(每次改数据)

**五个已完成的看板全部脚本化,不再手填数字。**窗口自动取到最新已结算日表,也可以自己圈一段:

```bash
python3 tools/gen-auth-data.py                 # #1  其余四板:gen-purchase / gen-classroom /
python3 tools/gen-funnel-payment-data.py       # #8  gen-funnel-onboarding / gen-funnel-payment
python3 tools/gen-purchase-data.py --from 2026-07-01 --to 2026-07-31   # 自定义窗口,每个脚本都认
```

脚本各自只改写自己那份 `*-data.js`,页面(含告警文案里的数字)全部由它推导。
不给日期参数时一次出**三个窗口**(最近 30 天 / 最近 7 天 / 上个月),页面上可切换;
**数据不完整的窗口会被整个丢掉**(某事件停报、漏斗倒着长),stderr 会说明原因 —— 不填假数。

上面 1–8 条里 **窗口、Pulled、暂定日、词表哨兵、首报日、求和校验**都已经在脚本里做掉了,人只需要:

- **看 stderr 的告警**:表外新词、两端失败词不一致、停报、求和对不上、首报日撞窗口起点 —— 有告警先查清楚再用
- 跑五份自检页(DEVELOPMENT §2.3),全 PASS 才算完
- 同步改本文件的「状态」列与 [`README.md`](./README.md) 页面表

**新做一块看板**(或改口径)时,下面这些仍要人过一遍:

1. 用 BigQuery SQL(或 MCP `bigquery`)重拉,禁止 GA API
2. **先跑 §3.2 的事件名清单**,确认事件在窗口内的实际名称与停报情况
3. **跑 §3.3 的词表哨兵**,确认没有表外新词、两端用词不一致的都已双取
4. **按 `diagnostic_id` × platform 查首报日**(§4.2),窗口标事件自己的首日,不是查询区间
5. 确认 dataset / 表 / 分区与 Range 一致;不用 `*_legacy` 表、不用 `events_intraday_*`
6. 页面 Range 写成日历日期,多窗口区块各自改;**终点是最新那张日表时才标「暂定」**(自选的历史窗口早已结清,不标)
7. 字段缺失则保留「条件未满足」,不填假数
8. 按 §5.4 核对着色:分档是否与数值一致、**失败少到不可信的是否为灰**、分子恒 0 的段是否显示「不适用」、非健康指标是否误着色
9. 把这套查法固化成 `tools/gen-*-data.py`,别留手工步骤 —— 手填的数字迟早和文案对不上

不想开电脑时,GitHub **Actions → 刷新看板数据 → Run workflow** 填日期即可(每天也会自动跑一次),
自检全 PASS 才提交。首次启用要配 Workload Identity,见 DEVELOPMENT §4.1。

---

## 8. 落地顺序(按实证可行性重排)

| 顺序 | 任务 | 为什么排这 | 前置 |
| --- | --- | --- | --- |
| ~~1~~ | ~~**#1 登录注册**~~ ✅ 2026-08-07 按 08-06 改稿修订:三段改两段 + ID 同步降为附注;含失败词双取、暂定日标记、停报探测 | — | — |
| 1 | **#3 进入教室健康** | 8,791 条量足;文档 §3.3 口径最完整(含分母分子、reason 枚举、iOS 实测样例) | 无 |
| 2 | **#2 支付订阅健康** | 口径已定稿零开放项;3,129 条偏少但结构清晰 | 无 |
| ~~3~~ | ~~**#5 网络 & 三方**~~ ✅ 2026-08-14 完成:成功侧 10% 采样还原、按请求 / 按设备双口径、启动期 vs 业务期分组、`offline` 正名为 DNS 解析失败。规则见 [NETWORK-THIRDPARTY.md](./NETWORK-THIRDPARTY.md);`signature_refresh` 等三个事件**页面已迁入,`health-auth.html` 第 2 节待删** | — |
| 4 | **A 总览 + 播报** | 需要 #1 #2 #3 #5 的口径都定稿才能汇总;注意「环比」目前做不到(埋点窗口不足) | 上面三项 |
| 5 | **#8 支付产品漏斗** | 先解决 2,426 条空字段的归属问题 | 查清旧口径 |
| 6 | **#7 新手转化漏斗** | 宽表已就绪,但新旧事件并存需按 §3.2 双取 | 无 |
| ~~7~~ | ~~**#4 崩溃 / 性能**~~ ✅ 2026-08-14 完成:耗时按内存档三拆、三源边界划清、**新增「埋点体检」块**(把哪端没埋显式列给双端同事)。规则见 [PERFORMANCE.md](./PERFORMANCE.md) | — |
| 新增 | **#11 ASR/TTS 语音**(Lark §3.6,原清单没登记) | `area='voice'` 7,783 条,双端都在报;但 **08-04 才上线,只有 6~8 天**,还在灰度爬坡 —— 能做,但只能做短窗口 + 标首报日,不算环比 | 再攒两周数据更稳 |
| 8 | **#6 A/B** | 先做 SRM 与埋点完整性监控页;发布决策页等分组明细表 | 分组表进 BQ + 读数指南 |
| 9 | **#9 有效学习** | 定义都没有 | 补北极星定义文档 |

需要对外推动的事(不属于看板开发,但决定看板能否做):

| 事项 | 阻塞 | 找谁 |
| --- | --- | --- |
| 服务端 A/B 分组明细表同步到 BigQuery | #6 无法圈样本 | 数据侧 |
| 《转化链路AB实验-读数与统计指南》 | #6 判据未知 | 李双 |
| 「有效学习分钟数」定义 | #9 做不了 | 待定 |
| **iOS 把失败统一改标 `tech_fail`**(见 §3.3) | 所有技术健康看板:只认一个词就静默丢掉整个 iOS,且不报错 | iOS |
| **带 `sample_rate` 的版本发到线上了吗**(#5 §3.2) | 线上全版本零条。若采样代码其实没发版,#5 所有成功率要按「还原前」读 —— **两种口径差 48 个百分点** | 客户端 / 发版 |
| **`/app/v1/open/onboarding/flow` Android 1 成功 / 1,034 失败**(#5 §3.4.1) | 采样口径怎么定它都是坏的 | 后端 / Android |
| **签名刷新失败后立刻重试、上限是多少**(#5 §3.7) | 每台中招设备平均失败 43 次,量是重试放大出来的;`offline` 100% 重试仍失败、重试后成功仅 0.3% | 双端 |
| **iOS 迁四分类**(`user_abort` / `rejected`)(#5 §4.1) | iOS 目前无法区分用户取消与技术失败,#5 的「用户取消」列 iOS 恒为 0 | iOS |
| **`api_result` 补「启动期」标记**(`source` / `phase`)(#5 §3.4) | 现在只能按接口路径近似切启动期 / 业务期,切不干净 | 双端 |
| **`PlaceholderView.showContentViews` 崩溃**(#4):`dino-media-preload` 线程碰 UI,13 台机型全中 | 现成的可修 bug,不卡看板 | Android |
| **iOS 补 `mem_tier` / `phase` / `rendered` / `data_ready_ms`**(#4 埋点体检) | 按内存档下钻、内存配对、渲染判定,iOS 侧整块做不了 | iOS |
| **Crashlytics 的每日批表何时有、iOS 为何几乎无数据**(#4 §2.2) | 决定崩溃能否算 crash-free 率 | 客户端 / Firebase 配置 |
| **确认 `app_context_sync_result` 的 `source=login_success / signup_success` 还要不要** | 关键链路正文说「和登录流程不存在联系」,**字段表和示例 JSON 却仍写着这两个值**。实际 Android 0 条、iOS 1,118 条。文档前后不一致,不先澄清就没法判断 Android 是漏埋还是本来就不用埋 | PM / 客户端 |
| **`signup_result` 水位未定义（旧“断报”结论已被回填推翻）** | 08-10 快照只到 iOS 07-31 / Android 08-04；08-15 回看时 07-15～08-13 两端 30/30 天均有数据。#1 第 5 节、#7 漏斗和用户阻塞回放必须只消费 FINAL 日 | 服务端 / 数据 |
| **`signup_method` → `method` 改名只做了一半** | 文档迁移表写「统一为 `method`」,实测只有 `web_h5` 与 `platform` 为空的行改了,双端仍用旧名。取数必须 `COALESCE` 双取 | 双端 / 数据侧 |
| 确认 **Android** 的 `sdk_auth_launch_result` 是否实现「未呈现」三态判定(文档只写了 iOS) | #1 段① 无判别力:1,531 次里只有 1 次失败(iOS 361 次零失败),光「用户取消」就不可能这么少 | Android |
| **用户取消时也上报「已经等了多久」** | 现在耗时只统计成功的登录,放弃的人没进统计 —— 无法验证「因为太慢所以放弃」(Android 每 4.4 人跑掉 1 个,后端换登录态中位耗时 8.79s) | 双端 |
| `result` 表外取值 `skip` / `cancel` 的语义(`push_bind_result` 一万条 `skip`) | 既非成功也非失败,相关看板无法分类 | 客户端 |
| 布尔字段类型双端统一(`is_new_user` / `membership_seeded` / `has_onboarding_payload`) | 取数须双取 string/int,易静默丢一端 | 双端 |
| `auth_entry_source` 补「深链」与「被踢回」两类入口 | #1 无法量化被动登出后的重登健康度 | 双端 |
| 逐条确认 `reason` 未出现枚举的可触发性(authorization 4 个 / backend 5 个) | 埋点未验证,不能证明分支能上报 | 客户端 |
| `is_premium` 未随 `auth_login_result` 上报 | 公共维度「是否会员」缺失 | 客户端 |
| `reason` 文档外枚举 `interrupted` / `unavailable` 的语义 | 归因表口径 | 客户端 |
