# 用户旅程阻塞行为分析 V1

> Review 日期：2026-08-15
> 客户端边界：只读核对 Android/iOS 线上 `main`，不修改客户端
> 数据边界：只展示聚合统计，不输出事件级样本或用户、设备、订单关联键

## 1. 要解决的问题

这页回答五个问题：

1. 用户在注册、支付、进入课堂的哪些行为阶段没有得到预期结果；
2. 失败集中在哪个平台和哪类原因；
3. 最近 7 天相对此前 7 天发生了什么变化；
4. 为什么这样判断，证据来自什么事件、字段和聚合数字；
5. 这是优先调查信号、已确认事实还是数据缺口，下一步应该验证什么。

它是跨旅程的行为分析入口。登录、支付、课堂、网络和性能的运行监控继续由现有专业看板负责。

## 2. V1 范围

V1 只分析三条现有数据能够诚实支持的路径：

| 路径 | Android | iOS | 页面展示 |
| --- | --- | --- | --- |
| 注册 | 服务端 `signup_result` | 服务端 `signup_result` | 结果量、失败量、失败占比、原因分布、日趋势、7 天对比 |
| 支付 | v0.24 `subscription_checkout_start/result` 可按订单局部关联 | start 在购买调用返回后才发 | Android 成熟 start-only 行为；双端客户端返回分类；iOS 只展示已完成结果对 |
| 课堂 | enter / WebView / RTC 阶段事件 | enter / WebView / RTC 阶段事件 | 各阶段独立的事件与结果分布、候选关联键覆盖 |

暂不纳入：

- 把不同路径合成一个总阻塞人数；
- 用户级跨阶段漏斗；
- 支付成交、收入或服务端履约结论；
- 课堂阶段事件量之间的用户转化率；
- Words、Listening 等观察期不足的新行为点；
- 客户端新增或修改埋点。

## 3. 双端代码 Review 结论

Android/iOS 的 `main` 均按当前线上版本解释。每次分析先刷新远程 `main`，再固定到 40 位 commit。独立代码审计 artifact 保留仓库、commit、文件路径和行号作为内部可审计锚点；合并给行为报告与页面时会剥离这些定位信息，只呈现代码逻辑、用户影响、可确认边界和运行数据验证建议，不带源码文本。

这条口径有一个必须保留的版本边界：30 天行为数据可能包含旧 `app_version`。在建立 `app_version → release tag/commit` 映射前，当前 `main` 只能解释当前线上版本的分类逻辑，不能直接替历史旧版本背书。

### 3.1 注册

`signup_result` 是服务端结果事件，Android/iOS 客户端明确不重复发送。移动端终态要兼容 string/int 两种 `is_success`；`result=account_unavailable` 是失败原因补充，不是终态主字段。

2026-07-15～2026-08-13 的 30 个已结算日表中，Android 与 iOS 都有 30/30 天数据。此前短期缺口在后续查询中被回填，因此正式分析不能把未结算日的空白直接解释为客户端断报。

### 3.2 支付

Android v0.24 的 start 与 result 共享 `order_id`，start 在支付面板拉起时发送。30 天实测有 1,259 个相关订单，其中 1,177 个成对、82 个只有 start、没有 result-only。它支持描述成熟 start-only 的数量、占比和趋势，但 start-only 本身不等于支付卡死。

iOS `PurchaseLifecycleService` 在 `gateway.purchase(...)` 返回后才发送 start，随后发送 result；面板拉起异常还可能出现 result-without-start。因此：

- iOS start 不能作为支付等待起点；
- 不能据此计算等待耗时、in-flight、hung 或终态回传率；
- 只展示已经返回的支付结果和 completed pairs。

双端支付能力不同，页面必须分开说明，不能合并成同一漏斗。

此外，`is_success=false` 不是统一的技术失败：Android 的 `result=1` 是 Google Billing `USER_CANCELED`，iOS 的 `result=user_cancel` 也是用户取消。V1 按订单去重后拆成成功类结果、用户取消、其他未完成、未知和冲突；这些都只是客户端面板返回分类，不代表服务端支付成交或履约。

### 3.3 课堂

`device_id + lesson_id + ga_session_id` 在不同点位覆盖不一致且存在同阶段重复，不能作为唯一 attempt。课堂 V1 固定为阶段聚合：分别展示进入意图、进入失败、WebView 和 RTC 的观察量与结果分布。

候选键覆盖只用于说明数据可关联程度，不用于把各阶段拼成用户级路径。

## 4. 数据口径

### 4.1 统计单位

| 路径 | 统计单位 | 说明 |
| --- | --- | --- |
| 注册 | 事件 | 服务端注册结果事件；成功、失败、未知之和必须等于观察量 |
| 支付 | 业务尝试/订单 | 关联键只在 SQL 内部使用，输出只保留聚合计数 |
| 课堂 | 阶段事件 | 每个阶段独立统计，不跨阶段相除 |

任何页面摘要都不能把这三种单位相加。

### 4.2 注册失败原因

V1 输出三个互斥桶：

- `account_unavailable`：已明确观察到的账号不可用；
- `unclassified`：结果为失败但当前字段不能进一步分类；
- `other`：其余已识别失败原因。

三个失败原因桶之和必须等于失败事件量。

### 4.3 支付观察口径

Android 只使用 `trigger + event_id=subscription_checkout_*` 的 v0.24 代际，旧同名事件不混入。最近 7 天和此前 7 天都使用当时可见的 result，避免把后来到达的终态倒灌进历史窗口。

iOS 不输出 mature start-only 比率，只输出已完成结果的成功类、用户取消、其他未完成、未知、冲突和配对质量。

### 4.4 课堂观察口径

每个平台、每个阶段输出：

- 观察事件、意图事件、成功事件、失败事件；
- 对同时存在成功/失败终态的阶段输出 `失败 / (成功 + 失败)`；`classroom_enter_failure` 本身就是失败信号，只展示次数，不展示天然为 100% 的比例；
- 候选键覆盖；
- 缺失 `event_uuid` 的聚合计数与重复事件行数。

这些字段用于理解阶段行为和数据质量，不表示同一批用户从上一步走到下一步。

## 5. 页面信息结构

页面按三级组织，不在浏览器重复计算口径：

1. `health-user-blocker.html` 一级总览：严格按源文档 §4 固定展示冷启动、注册与登录、进入课堂、课中互动与完课、订阅支付、首页与课程资源加载六条用户旅程；全局稳定性指标不放入该旅程总览。每条只占一行，包含本期观察、Android/iOS 当前证据、首要线索或缺口，以及一个二级页入口；
2. 六个 `health-user-blocker-*.html` 二级旅程页：全部按“本期重点 → 证据 → 边界与下一步”排列。注册、支付、课堂的行为聚合只嵌入对应旅程；生产行为数据未发布时，技术证据仍正常展示，行为区明确显示接入状态，不填模拟数字；
3. `health-auth.html`、`health-purchase.html`、`health-classroom.html`、`health-performance.html` 等现有技术看板作为三级专业诊断，从对应二级旅程页进入并返回。

首页不配置健康阈值，也不输出 `HEALTHY / WATCH / BREACH`。`OBSERVED_FACT`、`CORRELATED_SIGNAL`、`DATA_GAP`、`NOT_READY` 只说明证据角色或数据状态，不是跨旅程优先级。注册与登录、客户端支付结果与服务端最终确认、WebView 阶段与 Click → Ready 端到端结果必须分开解释。ASR/TTS 在源文档旅程表与管理建议中存在范围冲突，确认前不纳入六条正式旅程。

底层原因证据仍分三类：

- `CONFIRMED_REASON`：事件结果字段直接给出的原因，例如账号不可用、用户取消；
- `CORRELATED_SIGNAL`：已观察到需要追查的信号，但当前数据不能证明因果，例如成熟 start-only、课堂阶段失败；
- `UNKNOWN_REASON`：失败终态明确，但现有字段不足以解释主要原因。

每条结论都必须包含 `plainLanguage`、`reason`、至少一条聚合 `evidence`、`reasonEvidence`、`sampleSupport`、`reviewOrderReason`、`limitations` 和 `nextVerification`。

页面另外用 `findingRole` 统一阅读顺序：`ACTIONABLE_SIGNAL` 显示为“优先调查”，`OBSERVED_FACT` 显示为“已确认事实”，`DATA_GAP` 显示为“数据缺口”。结果字段直接分类只能算已确认事实，不能命名为技术根因或用户动机。

- `reasonEvidence` 只描述原因证据的来源：结果字段直接给出、观察到相关信号或原因分类不足；
- `sampleSupport` 展示实际观察量，并在适用时说明前后两个 7 天窗口是否完整；
- `reviewOrderReason` 用人话解释这条结论为什么现在值得查看，但不把不同旅程的事件、订单和课堂阶段换算成一个全局优先级；
- 课堂所有出现失败的阶段都必须保留在证据表；每个平台只把失败占比和失败量综合后最强的阶段提升为主结论，避免极低频信号与高影响异常同权展示。

页面不显示跨路径业务量总和，也不根据比例自动贴健康或危险标签。结论按路径分组，组内先展示可行动信号，再展示已确认事实和数据缺口；排序只决定阅读顺序。

## 6. 数据流

```text
BigQuery 已结算 GA4 日表
  → sql/user-blocker/*.sql 聚合预览
  → user-blocker-semantic-preview.v1
Android/iOS 线上 main@commit
  → analyze-user-blocker-code-evidence.py
  → user-blocker-code-evidence.v1（无源码片段）
两份证据
  → build-user-blocker-behavior-analysis.py
  → user-blocker-behavior-analysis.v1
  → user-blocker-page.js 展示
```

聚合预览与行为分析都拒绝事件级敏感键。代码证据不会把客户端源码放进 artifact。当前工作流只生成 JSON artifact，不自动提交或发布真实数据。

## 7. 页面数据合同

页面消费 `user-blocker-behavior-analysis.v1`，必须明确：

```text
analysisMode = DESCRIPTIVE_BEHAVIOR
containsEventLevelData = false
```

行为输出还必须包含：

```text
findings[].reasonType = CONFIRMED_REASON | CORRELATED_SIGNAL | UNKNOWN_REASON
findings[].plainLanguage
findings[].reason
findings[].evidence[] = statement + value + basis
findings[].reasonEvidence = DIRECT_RESULT | OBSERVED_SIGNAL | INSUFFICIENT_CLASSIFICATION
findings[].sampleSupport = observed + unit + comparisonComplete（适用时）
findings[].reviewOrderReason
findings[].limitations[]
findings[].nextVerification
findings[].codeEvidenceRefs[]（仅在客户端仓库已接入时存在）
```

可选的 `codeEvidence` 必须满足：

```text
schemaVersion = user-blocker-code-evidence.v1
analysisMode = STATIC_ONLINE_MAIN_CODE_EVIDENCE
containsSourceCode = false
repositories[].requestedRef = main
repositories[].resolvedCommit = 40 位 SHA
versionScope.runtimeMatchRequired = true
```

页面把运行数据证据与客户端代码逻辑结论分栏展示，不显示仓库路径、文件名、行号或 commit。代码结论说明客户端如何分类和处理当前场景、对用户意味着什么，以及还需结合哪些线上数据验证；它不会改变运行数据的 `reasonType`，也不会把静态逻辑升级为线上因果证明。

“已证实”只表示原因分类由结果字段直接给出，不表示样本量天然充分，也不表示已经找到更深层业务动机或技术根因。相关信号和未知原因不得改写成确定因果。

两个连续 7 天只有在各自覆盖完整日期且都有可计算分母时才允许比较。窗口不完整时，结论必须保留当前可用的绝对聚合证据，同时展示 `comparisonLimitation`；缺失日期不得当成零。

生产数据还必须满足：

- 聚合输出为完整数据日；
- `publishableAsUserBlocker=true`；
- 组织访问控制已确认；
- 不包含 `event_uuid`、设备/用户键、`order_id`、request、token、credential 或 signature。

条件未满足时，生产页只显示 `NOT_READY` 接入状态，不显示模拟业务数字。

## 8. 当前真实数据结论

数据窗口为 2026-07-15～2026-08-13，共 30 个已结算日表。

- 注册：按 `event_uuid` 去重后 Android 11,389 条、iOS 2,233 条；两端均覆盖 30/30 天；
- Android v0.24 checkout：1,259 个订单，1,177 成对，82 start-only；
- Android 已返回结果：13 个成功类、1,117 个用户取消、47 个其他未完成；
- iOS 全部已返回结果在可行性验收时为 21 个成功类、496 个用户取消、27 个其他未完成，其中 49 个是 result-only；页面只消费 completed-pair 子集，修正版语义预览需重跑后再冻结其分类数字；
- 课堂候选关联键存在明显缺失和重复，保持阶段聚合；
- 所有结论均是已到仓记录的观察结果，不代表客户端端到端无丢失。

完整查询证据见 [USER-BLOCKER-FEASIBILITY-2026-08-15.md](./USER-BLOCKER-FEASIBILITY-2026-08-15.md)。

## 9. 已实现

- BigQuery 聚合可行性审计：`tools/audit-user-blocker-feasibility.py`；
- 四份只读 SQL：`sql/user-blocker/`；
- 聚合语义预览：`tools/preview-user-blocker-semantics.py`；
- 行为分析生成：`tools/build-user-blocker-behavior-analysis.py`，生成优先调查、已确认事实和数据缺口三类阅读角色，并保留底层原因证据及证据链；
- 线上代码审计：`tools/analyze-user-blocker-code-evidence.py` 只读 Android/iOS `main` 并固定 commit；独立 artifact 保留路径/行号用于内部验证，行为报告和页面只接收注册、支付、WebView/RTC 的代码逻辑结论，不输出定位信息或源码片段；
- 页面分层：`health-user-blocker.html` 只承担 §4 六条用户旅程播报；六个二级旅程页统一采用“重点 → 证据 → 边界与下一步”，注册、支付、课堂行为证据嵌入对应二级页，现有登录、支付、课堂和性能技术板下沉为三级专业诊断；
- 合成数据 debug：`health-user-blocker_debug.html`；
- WIF 聚合验收工作流：`.github/workflows/audit-user-blocker.yml`；每天自动选择最新已结算日表并生成最近 30 天 artifact，也支持手动窗口。artifact 包含 feasibility、semantic preview、code evidence、behavior analysis 四份 JSON，保留 14 天并附 SHA-256 清单，不提交或发布真实数据；
- 自动窗口解析：`tools/resolve-user-blocker-window.py`，只查询 `__TABLES__` 元数据并排除 `events_intraday_*`。

### 9.1 可选接入两个客户端私库

在 Dashboard 仓库 Actions secret 中配置 `CLIENT_REPOS_READ_TOKEN` 后，工作流才会额外检出：

- `prime-future/dino-english-android` 的 `main`；
- `prime-future/dino-english-ios` 的 `main`。

两个仓库在同一组织权限范围内，使用一个能读取这两个仓库的 fine-grained PAT 或等价 GitHub App token。令牌只授予这两个仓库的 `Contents: Read-only`，并完成组织 SSO 授权；不授予写权限。默认 `GITHUB_TOKEN` 不能假设有权读取其他私库。检出步骤使用 `persist-credentials: false`，客户端工作区不上传 artifact。

未配置 secret 时，工作流仍正常生成 BigQuery 三份分析结果，同时输出 `status=NOT_CONFIGURED` 的代码证据文件，不阻塞每日任务。配置存在但任一仓库权限错误时应让任务失败，避免悄悄把“读不到代码”当成“代码没有证据”。

生产 `user-blocker-data.js` 目前仍是安全占位数据。

## 10. 上线前还需要完成

1. 将四份 SQL 部署为稳定的聚合输出，并对已结算数据日做回刷与校验；
2. 让生成器读取该稳定输出，形成可发布的 `user-blocker-behavior-analysis.v1`；
3. 为生产页面接入组织访问控制；
4. 用真实聚合数据跑页面自检并抽样对账；
5. 确认没有事件级字段后再替换 `user-blocker-data.js`。
6. 建立线上 `app_version → release tag/commit` 映射，历史窗口按版本选择对应代码基线，不能永远套用最新 main。

这些工作不要求修改 Android 或 iOS 客户端。
