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
4.3 KiB
4.3 KiB
生时校正匿名统计:隐私说明
任务书: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,失败吞掉。