docs: 迁入第一迭代过程报告并建立进展看板
- 新增 development/iterations/iteration-1/:15 份角色报告 + 进展看板(已完成/未闭环/下一步),作为双人协作的进度事实来源 - 新增 ADR-006:测试与交付容器化策略(Testcontainers / 交付 Docker 包 / 本机库仅个人联调) - Git 工作流规范补充:敏感信息只进忽略文件或 sample、测试数据不入库、测试代码限标准测试目录 - 门禁:mkdocs build --strict 通过(零警告)
This commit is contained in:
@@ -0,0 +1,221 @@
|
||||
# 第一迭代埋点与实验规划(身份漏斗)
|
||||
|
||||
> 角色: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 | 恢复 |
|
||||
Reference in New Issue
Block a user