黄一航
← 全部日志

访问计数:在「不追踪」的地基上数人头

2026-07-27约 2.5 小时隐私架构访客记录Redis交叉审查

目标

站主的原话是「能不能加一个后台,看到多少人来、哪些地方的、哪些公司的」。核对现状:014 的休眠后台已经能答「哪些地方」(Vercel 粗城市)与「谁」(自愿留资),「哪些公司」已被 014 的调研证伪(国内 IP 反查公司命中率≈0,方案收敛为留资)。唯一真缺口:纯浏览不计数——014 只记提问和下载。本篇补访问计数,且必须不推翻「无 Cookie、无第三方、数据最小化」的既有承诺。

关键决策:聚合而非日志

访问计数有两条路:逐条访问日志(每次访问一行,事后聚合)或写入时即聚合(只存「当天多少次、来自哪些城市、看了哪些页、几个去重访客」四类数字)。选后者:

  • 逐条日志会把 2000 条事件上限吃光,且「浏览了什么页面序列」本身就是行为画像——正是隐私页承诺不做的事。
  • 聚合数字里没有任何标识:城市分布是 HINCRBY 计数、去重访客是 HyperLogLog 基数草图(PFADD 进去的按天轮换哈希装不回来,草图只能回答「大约几个」)。
  • 保留期不再依赖 cron 记得跑:每个日键写入时都重发 EXPIRE(90 天),cron 的 pruneOld() 只做滚动 DEL 兜底。总访问次数是纯数字,与简历下载计数同类,永久保留。

链路:VisitBeacon(约 25 行客户端组件,sendBeacon 发纯文本路径)→ /api/pv(过滤 + 限流)→ recordPageview()(一条 Upstash REST pipeline,一次往返写完全部聚合)。白名单铁律因此多一个成员,本段就是论证:它不写 Cookie、不碰存储、有 DNT/GPC 就一个字节不发,服务端未配置 Upstash 时整条链路静默休眠。

Codex 交叉审查:6 个真问题

功能自测全绿后,把 diff 交给 Codex 盲审(它没写这段代码,盲区与作者不重叠),抓出 6 个真问题,全部修掉:

  1. 休眠时端点仍把原始 IP 放进限流 Map——「未配置=全链路休眠」不成立。修:storeConfigured() 短路挪到第一行,休眠时连限流都不碰。
  2. 服务端不看 DNT/Sec-GPC 请求头——「一次也不计」只靠客户端 JS 自觉,直接 POST 可绕过。修:服务端补第二道防线,承诺不再依赖客户端代码被执行。
  3. 「独立访客 · 7天」标签撒谎(最典型)——指纹按天轮换是隐私设计,代价就是跨天无法去重:7 个日 HLL 求并集≈各日相加,不是「7 天内来过的不同人数」。修:标签改「按日去重」,盐未配置时显示「—」而非误导性的 0。隐私设计的代价要在指标语义上诚实付账,不能两头占。
  4. 端点可被脚本污染——空 UA、不存在的路径都收,垃圾会灌进城市/页面榜。修:真实路由前缀白名单(/wp-login.php//admin 都进不来)+ 空 UA 拒收 + 廉价过滤挪到限流前(垃圾不烧真实访客的配额)。
  5. pipeline 子命令失败被吞——HINCRBY 成功而 EXPIRE 失败会留永生键,90 天承诺失效。修:失败计数入日志;同日下一次写入天然重发 EXPIRE 自愈;pruneOld() 每日滚动 DEL 保留期外 30 天窗口兜底。
  6. 趋势图两个空数据错误——仅第 8–14 天有数据时整张图消失;零访问日画 3% 假柱。修:门槛改 14 天有无数据;零访问日高度 0,底线用边框画。

结果

  • 后台 /admin 新增:访问今天/7天、访客今天/7天(按日去重)、累计五张卡 + 14 天趋势柱 + 「访客来自」「看得最多的页面」两张榜。
  • 合规联动同一次提交(014 的教训):隐私页中英各加「你打开页面时」段落(聚合、90 天、DNT/GPC);聊天浅色卡把 016 删掉的「访客记录告知」行加回(岛与卡都有告知)。
  • 验证:mock Upstash(单命令 + pipeline 两协议)实测全链路——beacon 入库、/en 与尾斜杠归一、DNT/GPC/空 UA/跨站/垃圾路径/限流逐分支断言;聊天场景①(演示模式)复测无恙;typecheck/lint/build/check:content 全绿。
  • 依旧休眠:与 014 同一组 env(Upstash/盐/口令),设好即全部生效,一个都不设则一切如旧。

第二道把关:多法域合规复审

Codex 之外又跑了一遍 014 同款的对抗式合规审查(GDPR/ePrivacy + PIPL),逐条核对文案与代码。代码侧全过(90 天 EXPIRE 四键无遗漏、原始 IP 未落库未外发、无盐不生成指纹、访问计数不进飞书/Resend 链路),但抓出的问题集中在一根轴上——文案比代码更绝对

  • 「里面没有任何能指向你的东西」是可证伪的绝对断言:HLL 虽装不回哈希,但持库者可拿提问日志里的同日指纹做成员性验证。改为机制描述:「去重数进的是基数草图,只能数出大约几个人,拿不回也还原不了你的 IP」。
  • 隐私页第 3 节只说「提问记录存在 Upstash」,访问计数同进 Upstash 却没披露——补上。
  • 保留期数字(30/90 天)在文案里是裸字面量,与代码常量无机械绑定——正是 014 那类雷的孵化条件。改为从 store.ts import 常量插值,改代码必然改文案。
  • 聊天告知行按 WP260 分层告知原则补上最「令人意外」的一项:提问「会推到我手机」。

审查结论原话:「改完这两句,这套披露的诚实度高于我见过的绝大多数商业站点隐私政策。」

踩坑与纠正

  • 预览环境三连坑(全是已知坑复发):无头标签页 rAF 挂起 → 剧场引擎入场动画永不完成,hero 胶囊组停在 opacity:0; pointer-events:none,坐标点击全部落空;最后用 el.click()pointer-events 只拦命中测试、不拦程序化 click)触发展开,下一拍断言 DOM。滚动态截图全白照旧,视觉验证一律 DOM 断言。
  • dev 的 StrictMode 会让 beacon 双跑(effect 双调用),本地计数偏高是正常现象,生产无此事。
  • 差点又犯 014 的「同部署」错:admin 面板先写完、隐私页还没改时就想跑验证——想起 014 的教训把文案补进同一批改动再验。

用时与备注

约 2.5 小时(含 mock Upstash 编写、Codex 交叉审查与 6 处修复、多法域合规复核)。零新依赖。「哪些公司」维持 014 结论:唯一诚实的来源是留资表单,后台「留资线索」区就是答案。