Files
Jyotisha/docs/operations/rectification-telemetry-privacy.md
T
Jesse_ChenandClaude Opus 5.5 d0bfc1fc3d feat(rectification): anonymous aggregate telemetry + admin summary page
One row per rectification Case, written once when the range card is first
delivered (GET /api/rectification/cases/[caseId], fire-and-forget after the
response is built). Numbers and closed enums only: no user / case / session
id, birth data, names, text or timestamps finer than the ISO week. Dedupe via
a separate case_id ledger that cascades with the Case (and account deletion).

Migration 20260926010000 is additive: two RLS tables with no runtime table
grants, SECURITY DEFINER write (service_role), purge (service_role) and
aggregate-only summary (admin_runtime) functions; 180-day retention.

Admin: 「校正统计」 page + GET /api/admin/rectification-telemetry
(admin.customers.read), aggregates only, no per-row view or export.

TASK-rectification-telemetry-20260926. test:db not run locally (no Docker).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017eEAG8HD3mm8gsKXgk8uU8
2026-09-26 15:36:29 +08:00

52 lines
4.3 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.
# 生时校正匿名统计:隐私说明
任务书:`docs/tasks/TASK-rectification-telemetry-20260926.md`。迁移:`frontend/supabase/migrations/20260926010000_rectification_telemetry.sql`。
## 存什么
每个校正会话第一次给出范围卡时记一行,只有下面这些字段(白名单写死在 `frontend/src/lib/rectification-agentic/v9/telemetry.ts` 的 `RECTIFICATION_TELEMETRY_FIELDS`,由 `frontend/tests/rectification-telemetry.test.ts` 钉住;加字段必须改测试):
| 字段 | 含义 | 类型 |
| --- | --- | --- |
| `recorded_week` | 记录所在 ISO 周的周一(UTC) | 日期,只到周 |
| `window_radius_minutes` | 出卡时搜索窗口的一半宽度 | 0–720 |
| `birth_time_source` | `hospital_record` / `approximate` / `period_only` / `unknown` | 枚举 |
| `questions_total` 及五个分类 | 定向 / 引导 / 带年月探针 / 性格 / 开放的提问数(总数含其他类) | 0–1000 |
| `experiences_added` | 用户讲过、仍有效的经历件数 | 0–1000 |
| `range_width_minutes` | 卡上范围宽度 | 0–1440 |
| `candidate_count` | 仍在比较的候选分钟数 | 0–1440 |
| `top_two_gap_points` | 第一名与第二名差几个百分点 | 0–100 |
| `stop_reason` | `converged` / `pool_exhausted` / `user_no_more` / `round_cap` / `user_stopped` / `error` | 枚举 |
| `precision_gate_met` | 精度门槛是否达标 | 布尔 |
| `duration_seconds` | 从第一轮到出卡的秒数 | 0–1 年 |
| `algorithm_version` / `policy_version` / `skill_version` | 版本号(只允许 `[A-Za-z0-9._:+-]`,不合规的记空) | 版本 id |
## 不存什么
- 不存用户、校正记录(Case)、会话编号;不存出生日期、时间、地点、姓名、邮箱;不存用户原话、证据摘要、模型输出;不存 IP;时间不细于周。
- 候选时刻、范围起止时刻(`HH:MM`)也不存,只存宽度。
- 写入失败的日志只有一行 `[rectification-telemetry] write skipped reason=<错误码>`,不带任何字段值、编号或错误原文。
## 去重与账户删除(任务书 D4 的落法)
- 任务书 D4 允许为去重和删除联动存用户 id。本实现更严:统计行里**不存**任何 id。
- 去重用另一张表 `rectification_telemetry_reported_cases`,只有一列 `case_id`,外键指向校正记录并 `on delete cascade`。账户删除 → 身份用户删除 → 校正记录级联删除 → 这张台账的行一并删除。
- 台账没有时间列,也没有指向统计行的列,统计行无法再关联回任何人;账户删除后留下的统计行本来就不含个人信息,所以不需要(也无法)按人删除。
- 同一个校正会话只记一次,以第一次出卡时的状态为准;之后继续补经历、采用、确认都不改这一行。
## 谁能读写
- 两张表都开启 RLS,且不给任何运行角色表权限(`anon` / `authenticated` / `app_runtime` / `admin_runtime` / `service_role` 全部 revoke)。
- 写:只能通过 `record_rectification_telemetry`(SECURITY DEFINER,只授权 `service_role`);函数先核对该 Case 属于该用户,再写台账和统计行,任一 CHECK 不过则整笔回滚。
- 读:只能通过 `rectification_telemetry_summary(weeks)`(SECURITY DEFINER,只授权 `admin_runtime`),返回中位数、分布、按周趋势和版本分布,不返回任何单行。后台接口 `GET /api/admin/rectification-telemetry` 需要 `admin.customers.read` 权限,没有逐条列表、没有导出。
## 保留期
- 180 天。每次写入先删除「所在周的周一早于今天 180 天」的行;汇总函数也不读 180 天以前的行;运维可调用 `purge_expired_rectification_telemetry()`(只授权 `service_role`)手动执行同一删除。
- 没有定时任务:长时间没有新写入时,过期行在库里但汇总看不到,下一次写入即删除。
## 写入时机与性能
- 写入点是 `GET /api/rectification/cases/[caseId]`(每轮结束后前端都会刷新它),只在当前决策为交付结果(`sessionOutcomeAllowsDelivery`)、卡片有候选列、且最近活动在 2 小时内时记录;翻看很久以前的历史不会补记。
- 复用这次响应已经算好的决策和卡片,不再重复计算;写库在响应构建完之后另起(`setImmediate`),不 await,失败吞掉。