- 新增 development/iterations/iteration-1/:15 份角色报告 + 进展看板(已完成/未闭环/下一步),作为双人协作的进度事实来源 - 新增 ADR-006:测试与交付容器化策略(Testcontainers / 交付 Docker 包 / 本机库仅个人联调) - Git 工作流规范补充:敏感信息只进忽略文件或 sample、测试数据不入库、测试代码限标准测试目录 - 门禁:mkdocs build --strict 通过(零警告)
16 KiB
第一迭代埋点与实验规划(身份漏斗)
角色: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 节日志规则:
- 密码:明文、哈希、长度、强度分数均禁止。
- 凭证:access token、refresh token、验证码、上传签名、
Idempotency-Key等任何凭证或其片段。 - 手机号 / 邮箱全文:失败事件中不得回传用户输入的账号原文;仅允许
identifierType(username/phone/email)这类枚举。 - 用户名原文:注册失败重复冲突时只报
username_taken分类,不报具体用户名。 - 完整 IP、精确地理位置、设备广告标识(IDFA/GAID)。
- 原始请求/响应报文、异常堆栈原文:失败只允许上报枚举化的
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 实验必须使用稳定分流和独立曝光事件;在基础埋点、指标口径和样本量规则完成前不启动产品实验。」 当前三个前置条件均不满足:
- 基础埋点不存在:三仓尚无任何事件采集链路(第 2 节基线:Flutter 没有网络层,API 无观测能力),本报告定义的事件本身就是待建设项。
- 指标口径未经数据验证:口径刚在本报告提出,尚未有真实数据校验事件触发准确性(重复、丢失、时序)——没有可信基线就无法解读实验差异。
- 样本量规则无从谈起:第一迭代是首个真实版本,DAU 基线为零。以典型场景估算:若登录成功率基线约 90%,要在 95% 置信度、80% 功效下检出 3 个百分点的绝对提升,每组约需 1,600 个独立用户;当前流量远达不到,实验只会产出噪声结论。
- 没有分流与曝光基础设施:无稳定分流(用户 ID 哈希 + 实验盐)组件,无独立曝光事件,无法保证随机化与分析对齐。
- 也没有值得实验的对照:第一迭代只有一条登录纵切路径,不存在需要 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,不因单条非法事件拒绝整批(返回逐条结果)。 - 存储:落
platformschema(平台能力域,与第 3 节域划分一致),经 Flyway 迁移新建表:
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 | 恢复 |