访问计数:在「不追踪」的地基上数人头
目标
站主的原话是「能不能加一个后台,看到多少人来、哪些地方的、哪些公司的」。核对现状: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 个真问题,全部修掉:
- 休眠时端点仍把原始 IP 放进限流 Map——「未配置=全链路休眠」不成立。修:
storeConfigured()短路挪到第一行,休眠时连限流都不碰。 - 服务端不看
DNT/Sec-GPC请求头——「一次也不计」只靠客户端 JS 自觉,直接 POST 可绕过。修:服务端补第二道防线,承诺不再依赖客户端代码被执行。 - 「独立访客 · 7天」标签撒谎(最典型)——指纹按天轮换是隐私设计,代价就是跨天无法去重:7 个日 HLL 求并集≈各日相加,不是「7 天内来过的不同人数」。修:标签改「按日去重」,盐未配置时显示「—」而非误导性的 0。隐私设计的代价要在指标语义上诚实付账,不能两头占。
- 端点可被脚本污染——空 UA、不存在的路径都收,垃圾会灌进城市/页面榜。修:真实路由前缀白名单(
/wp-login.php、//admin都进不来)+ 空 UA 拒收 + 廉价过滤挪到限流前(垃圾不烧真实访客的配额)。 - pipeline 子命令失败被吞——
HINCRBY成功而EXPIRE失败会留永生键,90 天承诺失效。修:失败计数入日志;同日下一次写入天然重发EXPIRE自愈;pruneOld()每日滚动 DEL 保留期外 30 天窗口兜底。 - 趋势图两个空数据错误——仅第 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.tsimport 常量插值,改代码必然改文案。 - 聊天告知行按 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 结论:唯一诚实的来源是留资表单,后台「留资线索」区就是答案。