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

16 KiB
Raw Blame History

第一迭代埋点与实验规划(身份漏斗)

角色: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. 手机号 / 邮箱全文:失败事件中不得回传用户输入的账号原文;仅允许 identifierTypeusername / phone / email)这类枚举。
  4. 用户名原文:注册失败重复冲突时只报 username_taken 分类,不报具体用户名。
  5. 完整 IP、精确地理位置、设备广告标识(IDFA/GAID)
  6. 原始请求/响应报文、异常堆栈原文:失败只允许上报枚举化的 failureReason + 业务错误码 + HTTP 状态码。

服务端接收器应对属性做白名单校验:schema 之外的字段直接丢弃并计数告警,防止未来有人顺手塞入敏感字段。

1.4 事件清单

注册

事件名 触发时机 专有属性
auth_register_started 用户进入注册页并产生首次输入(首字符),每个注册会话记一次 entryPointlaunch / login_page_link
auth_register_succeeded 客户端收到 POST /api/v1/auth/register 的成功响应(code=0)之后 durationMsstarted → succeeded 耗时)
auth_register_failed 收到失败响应、请求超时或本地校验拦截提交时 failureReasonerrorCode(业务码,可空)、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 已写入安全存储后 identifierTypedurationMs
auth_login_failed 收到失败响应或请求超时 identifierTypefailureReasonerrorCodehttpStatusattemptSeq

auth_login_failed.failureReason 枚举:invalid_credentials(凭证错误,不区分账号不存在与密码错误,与接口防枚举策略一致)、account_locked(登录失败限制触发)、rate_limitedvalidation_errornetwork_errorserver_error

Token 刷新

事件名 触发时机 专有属性
auth_token_refresh_succeeded POST /api/v1/auth/refresh 轮换成功且新 token 落库安全存储后 triggerproactive 预刷新 / on_401 拦截器触发 / restore 启动恢复)
auth_token_refresh_failed 刷新失败 triggerfailureReasonerrorCodehttpStatus

failureReason 枚举:refresh_expiredrefresh_revoked(含轮换重放被拒,是 M1 验收「退出后 refresh token 不可再次使用」的观测点)、network_errorserver_error

退出

事件名 触发时机 专有属性
auth_logout 用户主动退出:本地凭证已清除时上报(不等待服务端撤销结果) serverRevokedboolPOST /api/v1/auth/logout 是否成功)

被动登出(会话被撤销/过期导致跳登录页)不用此事件,由 auth_session_restore_failedauth_token_refresh_failed 覆盖,避免口径混淆。

登录态恢复

事件名 触发时机 专有属性
auth_session_restore_started 冷启动时安全存储中存在 refresh token,开始恢复流程
auth_session_restore_succeeded 恢复完成:拿到有效 access token 且 GET /api/v1/me 成功 durationMsusedRefreshbool,是否经历了刷新)
auth_session_restore_failed 恢复失败,用户被要求重新登录 failureReasonrefresh_expired / refresh_revoked / network_error / server_error)、errorCodehttpStatus

说明:冷启动无存量凭证时不上报任何 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_failedfailureReason 分布;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_credentialsvalidation_erroraccount_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,携带 experimentKeyvarianteventVersion):分析只统计实际曝光用户,杜绝按分配名单算分母。
  • 实验设计文档模板与评审流程:假设、主指标、护栏指标、提前停止规则、多重比较校正约定。
  • 安全机制:护栏指标(崩溃率、登录成功率等)实时监控与一键回滚(可复用后续的配置下发/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 迁移新建表:
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 恢复