Files
patbond-doc/docs/development/iterations/iteration-1/05-experiment-tracking-plan.md
T
lixi 209021e7d2 docs: 迁入第一迭代过程报告并建立进展看板
- 新增 development/iterations/iteration-1/:15 份角色报告 + 进展看板(已完成/未闭环/下一步),作为双人协作的进度事实来源
- 新增 ADR-006:测试与交付容器化策略(Testcontainers / 交付 Docker 包 / 本机库仅个人联调)
- Git 工作流规范补充:敏感信息只进忽略文件或 sample、测试数据不入库、测试代码限标准测试目录
- 门禁:mkdocs build --strict 通过(零警告)
2026-09-04 10:45:05 +08:00

222 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 第一迭代埋点与实验规划(身份漏斗)
> 角色:Experiment Tracker
> 日期:2026-09-03
> 依据:`patbond-doc/docs/development/development-plan.md` 第 8 节(第一迭代范围)、第 9 节「可观测性与产品验证」、第 11 节(已知风险)
> 范围:仅覆盖「注册 → 登录 → 获取当前用户 → 退出」身份纵切;不涉及社区、AI 创作、本地服务的事件与实验。
---
## 1. 埋点事件规范
### 1.1 设计原则
- 事件名使用 `snake_case`,格式为 `<域>_<对象>_<结果>`,域前缀统一为 `auth`
- 每个事件携带 `eventVersion`(本迭代全部为 `1`);schema 变更时递增版本,消费端按版本解析,禁止原地改语义。
- 事件在**客户端**采集(反映用户实际体验,含网络失败),`serverTs` 由接收端补写,作为统计的权威时间;`clientTs` 仅用于排序和时延诊断。
- 每个事件由客户端生成 `eventId`(UUIDv7),用于服务端幂等去重(配合 at-least-once 上报)。
- JSON 字段 `camelCase`、UUID 字符串、ISO 8601 时间,与第 6.1 节 API 通用规则一致。
### 1.2 公共属性(所有事件必带)
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `eventId` | UUID | 客户端生成(UUIDv7),服务端去重键 |
| `eventName` | string | 见 1.4 事件清单 |
| `eventVersion` | int | 本迭代固定 `1` |
| `anonymousId` | UUID | 设备级匿名标识,首次启动生成,存普通本地存储(非敏感);注册前唯一可用的主体标识 |
| `userId` | UUID / null | 登录后填充;注册开始、登录前事件为 null |
| `sessionId` | UUID | 客户端会话标识:应用冷启动或后台超过 30 分钟后重新生成 |
| `clientTs` | timestamptz | 客户端本地时间(ISO 8601 含时区) |
| `serverTs` | timestamptz | **服务端接收时补写**,客户端不发;统计窗口一律以此为准 |
| `appVersion` | string | 如 `1.0.0+12` |
| `platform` | string | `android` / `ios` |
| `osVersion` | string | 粗粒度主版本,如 `android-14` |
### 1.3 隐私红线(禁止字段,任何事件不得携带)
依据第 9 节「不得包含密码、token 或非必要个人信息」及 6.1 节日志规则:
1. **密码**:明文、哈希、长度、强度分数均禁止。
2. **凭证**access token、refresh token、验证码、上传签名、`Idempotency-Key` 等任何凭证或其片段。
3. **手机号 / 邮箱全文**:失败事件中不得回传用户输入的账号原文;仅允许 `identifierType``username` / `phone` / `email`)这类枚举。
4. **用户名原文**:注册失败重复冲突时只报 `username_taken` 分类,不报具体用户名。
5. **完整 IP、精确地理位置、设备广告标识(IDFA/GAID)**
6. **原始请求/响应报文、异常堆栈原文**:失败只允许上报枚举化的 `failureReason` + 业务错误码 + HTTP 状态码。
服务端接收器应对属性做白名单校验:schema 之外的字段直接丢弃并计数告警,防止未来有人顺手塞入敏感字段。
### 1.4 事件清单
#### 注册
| 事件名 | 触发时机 | 专有属性 |
| --- | --- | --- |
| `auth_register_started` | 用户进入注册页并产生首次输入(首字符),每个注册会话记一次 | `entryPoint``launch` / `login_page_link` |
| `auth_register_succeeded` | 客户端收到 `POST /api/v1/auth/register` 的成功响应(code=0)之后 | `durationMs`started → succeeded 耗时) |
| `auth_register_failed` | 收到失败响应、请求超时或本地校验拦截提交时 | `failureReason``errorCode`(业务码,可空)、`httpStatus`(可空)、`attemptSeq`(本注册会话第几次尝试) |
`auth_register_failed.failureReason` 枚举:
- `validation_error` — 本地或服务端参数校验失败(格式、必填)
- `username_taken` — 用户名已存在
- `phone_taken` — 手机号已存在
- `weak_password` — 密码不满足策略
- `rate_limited` — 触发频控
- `network_error` — 超时 / 无网络 / 连接失败
- `server_error` — 5xx 或未知业务码
#### 登录
| 事件名 | 触发时机 | 专有属性 |
| --- | --- | --- |
| `auth_login_succeeded` | 收到 `POST /api/v1/auth/login` 成功响应且 token 已写入安全存储后 | `identifierType``durationMs` |
| `auth_login_failed` | 收到失败响应或请求超时 | `identifierType``failureReason``errorCode``httpStatus``attemptSeq` |
`auth_login_failed.failureReason` 枚举:`invalid_credentials`(凭证错误,不区分账号不存在与密码错误,与接口防枚举策略一致)、`account_locked`(登录失败限制触发)、`rate_limited``validation_error``network_error``server_error`
#### Token 刷新
| 事件名 | 触发时机 | 专有属性 |
| --- | --- | --- |
| `auth_token_refresh_succeeded` | `POST /api/v1/auth/refresh` 轮换成功且新 token 落库安全存储后 | `trigger``proactive` 预刷新 / `on_401` 拦截器触发 / `restore` 启动恢复) |
| `auth_token_refresh_failed` | 刷新失败 | `trigger``failureReason``errorCode``httpStatus` |
`failureReason` 枚举:`refresh_expired``refresh_revoked`(含轮换重放被拒,是 M1 验收「退出后 refresh token 不可再次使用」的观测点)、`network_error``server_error`
#### 退出
| 事件名 | 触发时机 | 专有属性 |
| --- | --- | --- |
| `auth_logout` | 用户主动退出:本地凭证已清除时上报(不等待服务端撤销结果) | `serverRevoked`bool`POST /api/v1/auth/logout` 是否成功)|
被动登出(会话被撤销/过期导致跳登录页)不用此事件,由 `auth_session_restore_failed``auth_token_refresh_failed` 覆盖,避免口径混淆。
#### 登录态恢复
| 事件名 | 触发时机 | 专有属性 |
| --- | --- | --- |
| `auth_session_restore_started` | 冷启动时安全存储中存在 refresh token,开始恢复流程 | — |
| `auth_session_restore_succeeded` | 恢复完成:拿到有效 access token 且 `GET /api/v1/me` 成功 | `durationMs``usedRefresh`bool,是否经历了刷新) |
| `auth_session_restore_failed` | 恢复失败,用户被要求重新登录 | `failureReason``refresh_expired` / `refresh_revoked` / `network_error` / `server_error`)、`errorCode``httpStatus` |
说明:冷启动无存量凭证时不上报任何 restore 事件(不算失败),保证会话恢复成功率的分母干净。
### 1.5 与文档第 9 节首批漏斗事件的对应
文档建议首批覆盖六个漏斗事件,本迭代范围内落地其中两个:`auth_register_succeeded`(注册完成)、`auth_login_succeeded`(登录成功)。宠物创建、动态发布、AI 任务成功、预约确认分别属于 M2–M5,届时沿用本规范的公共属性与命名规则扩展,不在本轮定义。
---
## 2. 指标口径
统计一律以 `serverTs` 划定窗口(UTC 日界,展示层可换算);主体去重优先 `userId`,注册前用 `anonymousId`
### 2.1 注册转化率
- **分子**:窗口内产生 `auth_register_succeeded` 的去重 `anonymousId` 数。
- **分母**:窗口内产生 `auth_register_started` 的去重 `anonymousId` 数。
- **窗口**:按天统计;归因窗口 24 小时——`started` 落在 D 日的设备,其成功可发生在 `started` 后 24h 内,仍计入 D 日转化(避免跨零点漏算)。
- **辅助指标**`auth_register_failed``failureReason` 分布;`network_error`/`server_error` 占比是技术问题信号,`validation_error`/`weak_password` 占比是产品/文案问题信号。
### 2.2 登录成功率
- **尝试级(主口径)**:分子 = `auth_login_succeeded` 事件数;分母 = `auth_login_succeeded` + `auth_login_failed` 事件数。按天统计,另看 7 天滚动。
- **用户级(辅助口径)**:窗口内最终登录成功(至少一条 succeeded)的去重主体 / 发起过登录尝试的去重主体(`userId` 缺失时用 `anonymousId`),窗口 24 小时。
- **排除项**:主口径不排除 `invalid_credentials`(它反映真实用户体验);另设「系统登录成功率」= 排除 `invalid_credentials``validation_error``account_locked` 后的成功率,用于监控服务健康,目标应接近 100%。
### 2.3 会话恢复成功率
- **分子**`auth_session_restore_succeeded` 事件数。
- **分母**`auth_session_restore_started` 事件数(即冷启动时存在存量凭证并发起恢复的次数)。
- **窗口**:按天统计。无存量凭证的冷启动不进分母;`refresh_expired` 计入失败(是体验事实),但按 `failureReason` 拆分后单独解读——过期占比高提示 token 有效期策略问题(第 12 节待确认事项),`refresh_revoked`/`server_error` 占比高提示实现缺陷。
- 该指标同时是 M1 验收标准「客户端可恢复登录态」的量化观测。
---
## 3. 为什么第一迭代不启动 A/B 实验
文档第 9 节明确规则:**「A/B 实验必须使用稳定分流和独立曝光事件;在基础埋点、指标口径和样本量规则完成前不启动产品实验。」** 当前三个前置条件均不满足:
1. **基础埋点不存在**:三仓尚无任何事件采集链路(第 2 节基线:Flutter 没有网络层,API 无观测能力),本报告定义的事件本身就是待建设项。
2. **指标口径未经数据验证**:口径刚在本报告提出,尚未有真实数据校验事件触发准确性(重复、丢失、时序)——没有可信基线就无法解读实验差异。
3. **样本量规则无从谈起**:第一迭代是首个真实版本,DAU 基线为零。以典型场景估算:若登录成功率基线约 90%,要在 95% 置信度、80% 功效下检出 3 个百分点的绝对提升,每组约需 1,600 个独立用户;当前流量远达不到,实验只会产出噪声结论。
4. **没有分流与曝光基础设施**:无稳定分流(用户 ID 哈希 + 实验盐)组件,无独立曝光事件,无法保证随机化与分析对齐。
5. **也没有值得实验的对照**:第一迭代只有一条登录纵切路径,不存在需要 A/B 裁决的产品分叉;此时的正确动作是把漏斗测准,而不是测变体。
### 未来启动实验前必须就绪的前置条件清单
- [ ] 本报告第 1 节事件已上线,且通过数据质量验收:事件丢失率 < 5%,`eventId` 去重生效,`serverTs` 覆盖率 100%,属性白名单校验无敏感字段泄漏。
- [ ] 第 2 节三个指标已连续稳定产出 ≥ 2 周,形成基线均值与方差,且与服务端日志(如登录接口成功率)交叉核对一致。
- [ ] 样本量规则成文:给定基线率、最小可检测效应(MDE)、95% 置信度、80% 功效的样本量计算方法与查表,并据实际 DAU 判断实验最短运行时长。
- [ ] 稳定分流组件:`hash(userId, experimentSalt) % buckets`,同一用户在实验期内分组不变、跨端一致;登录前实验用 `anonymousId` 并定义登录后归并规则。
- [ ] 独立曝光事件(如 `experiment_exposed`,携带 `experimentKey``variant``eventVersion`):分析只统计实际曝光用户,杜绝按分配名单算分母。
- [ ] 实验设计文档模板与评审流程:假设、主指标、护栏指标、提前停止规则、多重比较校正约定。
- [ ] 安全机制:护栏指标(崩溃率、登录成功率等)实时监控与一键回滚(可复用后续的配置下发/feature flag 能力)。
- [ ] 隐私合规复核:实验分组数据同样遵守 1.3 节红线。
---
## 4. 埋点数据落地建议
约束:与现有技术栈一致(Spring Boot + PostgreSQL 16 + Flutter);**不引入第三方分析 SaaS/SDK**——文档第 11 节第 10 条明示外部供应商均未确定,埋点作为基础设施不应在此时绑定未评审的供应商,自建最小链路即可满足第一迭代验证需求,且数据留在自有库,规避合规不确定性。
### 4.1 客户端(Flutter
- 新增轻量 `analytics` 模块(与 `auth` 等 feature 平级),对外仅暴露 `track(eventName, props)`;事件构造时自动附加公共属性。
- **本地持久化队列**:事件先写本地队列(`sqflite` 表或追加式文件——Flutter 生态内置方案,非第三方分析服务),应用被杀不丢事件。注意:事件属于非敏感数据,存普通本地存储即可,**不占用安全存储**(安全存储按 4.2 节仅放 token)。
- **批量上报**:满 20 条或 30 秒定时触发,冷启动和进入后台时各冲刷一次;单批 ≤ 50 条。
- **重试**:指数退避(5s 起,上限 5 分钟)+ at-least-once;服务端靠 `eventId` 去重,客户端只在收到 2xx 后删除本地记录。4xx(schema 被拒)不重试,丢弃并本地计数。
- **背压**:队列上限 1,000 条,超限丢最旧事件;上报失败不得阻塞或影响任何业务流程(埋点永远是旁路)。
### 4.2 服务端(Spring
- 新增接口 `POST /api/v1/events`(批量数组体)。放在现有服务内(建议 `patbond-user` 或后续网关层)即可,第一迭代不为埋点单起服务。
- **鉴权**:登录后带 access token;注册/登录前的事件允许匿名上报(仅此端点),配合频控与 body 大小限制(如单批 ≤ 64KB)防滥用。
- **处理**:白名单校验事件名与属性 → 剥离/拒绝红线字段 → 补写 `serverTs` → 落库。响应 `202`,不因单条非法事件拒绝整批(返回逐条结果)。
- **存储**:落 `platform` schema(平台能力域,与第 3 节域划分一致),经 Flyway 迁移新建表:
```sql
create table platform.product_events (
event_id uuid primary key, -- 客户端 UUIDv7,天然幂等去重
event_name text not null,
event_version int not null,
anonymous_id uuid not null,
user_id uuid,
session_id uuid not null,
client_ts timestamptz not null,
server_ts timestamptz not null default now(),
app_version text not null,
platform text not null,
props jsonb not null default '{}'::jsonb -- 白名单内的专有属性
);
create index on platform.product_events (event_name, server_ts);
create index on platform.product_events (user_id, server_ts);
```
- 插入用 `on conflict (event_id) do nothing` 实现去重。第一迭代事件量极小,同库 SQL 直接算第 2 节指标即可(可沉淀成参考查询放入 `docs/database/`);将来量大再考虑异步化(RabbitMQ 已在技术栈规划内)或独立分析存储。
- **红线兜底**:该表数据同样受 6.1 节日志规则约束;接收端是最后一道防线,白名单之外字段一律丢弃并打点告警。
### 4.3 数据质量监控(上线即带)
- 服务端技术指标:`/api/v1/events` 请求量、拒绝率、去重命中率(与第 9 节技术指标要求一致)。
- 每日核对:`auth_login_succeeded` 事件数 vs 登录接口成功响应数,偏差 > 5% 告警——这是未来实验前置条件里「交叉核对」的日常化。
---
## 附:事件总览
| # | 事件名 | 版本 | 阶段 |
| --- | --- | --- | --- |
| 1 | `auth_register_started` | 1 | 注册 |
| 2 | `auth_register_succeeded` | 1 | 注册(漏斗事件) |
| 3 | `auth_register_failed` | 1 | 注册 |
| 4 | `auth_login_succeeded` | 1 | 登录(漏斗事件) |
| 5 | `auth_login_failed` | 1 | 登录 |
| 6 | `auth_token_refresh_succeeded` | 1 | 会话维持 |
| 7 | `auth_token_refresh_failed` | 1 | 会话维持 |
| 8 | `auth_logout` | 1 | 退出 |
| 9 | `auth_session_restore_started` | 1 | 恢复 |
| 10 | `auth_session_restore_succeeded` | 1 | 恢复 |
| 11 | `auth_session_restore_failed` | 1 | 恢复 |