CodeGraph 匿名遥测系统全解析:Schema 白名单、本地日聚合与 Cloudflare Worker 全链路
CodeGraph 是一个本地优先的代码知识图谱工具,其核心卖点是"你的代码永远不离开你的机器"。要在坚持这一承诺的前提下回答"哪些 Agent、哪些语言、哪些命令真正驱动了使用"这类问题,遥测系统就必须被设计成一句可被证明的话:只采集一小段可审计的匿名计数器列表、逐字段公开文档、随时可关、且无法悄悄扩张。本文以 docs/design/telemetry.md 这份工程契约为主体,结合 src/telemetry/index.ts、telemetry-worker/src/index.ts、telemetry-worker/migrations/0001_init.sql 与 telemetry-worker/src/rollup.ts 的实际实现,完整讲解从客户端内存计数、本地日聚合、fire-and-forget 发送,到 Worker 端白名单校验、D1 存储、夜间 rollup 与 90 天保留期清理的全链路设计。
一、设计目标与非目标:先划清"什么绝不采集"
设计文档首先给出要匿名、聚合地回答的问题:多少台机器在日常使用(日活/周活及其变化)、哪些 Agent 在驱动使用(通过 MCP clientInfo 识别 Claude Code、Cursor、Codex、opencode 等)、用户选择哪些安装目标(local vs global、fresh vs upgrade)、哪些 MCP 工具与 CLI 命令被使用及错误率、哪些语言被索引(用于决定提取器/框架工作的优先级)、以及版本普及速度与 OS/架构/Node 构成。文档特别注明:SQLite 后端现在始终是内置的 node:sqlite,已不存在 native-vs-wasm 之分可测。
"绝不采集"列表同样明确,这构成整个系统的负面契约:
- 永远不采源代码:无文件路径、文件名、仓库名、符号名、查询串、搜索词,以及任何从被索引项目内容中派生的东西;
- 无 IP 地址:在边缘(edge)从不读取,且下游不存在任何能看到 IP 的后端;
- 无第三方分析厂商:事件只存入自有数据库,ingest Worker 不发出任何出站请求;
- 无硬件指纹:机器 ID 是随机 UUID,不派生自任何硬件信息;
- 无按键/逐调用事件流:使用数据先在本地聚合成日 rollup,之后才可能发送;
- 私有
codegraph-profork 不发遥测(见第六节)。
四条设计原则贯穿实现:
- Schema 即白名单。客户端只发契约列出的事件;ingest Worker 按同一白名单校验并丢弃其余。新增一个字段 = 同时修改设计文档、面向用户的
TELEMETRY.md与 Worker 白名单的同一个 PR; - 遥测永远不许让用户付出代价:MCP 工具调用热路径上零额外延迟(这是该仓库的核心不变量)、零新增 npm 依赖(全局
fetch,Node ≥18)、stdout 零字节输出(stdio 是 MCP 协议通道)、零重试、零错误噪音。每一种失败模式都是静默; - 关闭即真关闭:禁用后任何进程都不打开指向遥测端点的套接字——连"已退出"ping 都没有;
- 第一方端点。客户端永远只与
telemetry.getcodegraph.com通信。发布到 npm 的版本把该域名烧录进去,域名必须归项目所有,其后端可以随时更换而无需发客户端新版本。
二、事件契约:信封 + 四种事件
每个批次共享一个公共信封(每进程计算一次):
| 字段 | 示例 | 说明 |
|---|---|---|
machine_id |
b3a8…(UUIDv4) |
随机生成,首次运行时铸出,存于全局配置 |
codegraph_version |
0.9.12 |
来自 package.json |
os / arch |
darwin / arm64 |
process.platform / process.arch |
node_major |
22 |
仅主版本号 |
ci |
false |
环境变量 CI 是否存在 |
schema_version |
2 |
Schema 变更时递增(v2 移除了 index.sqlite_backend) |
在客户端源码 src/telemetry/index.ts 中可以看到这些常量的实际定义:TELEMETRY_ENDPOINT 指向 https://telemetry.getcodegraph.com/v1/events,SCHEMA_VERSION = 2,缓冲区硬上限 MAX_BUFFER_BYTES = 256 * 1024,单请求最多 MAX_EVENTS_PER_REQUEST = 100 个事件,默认超时 DEFAULT_FLUSH_TIMEOUT_MS = 1500,崩溃发送方留下的 claim 文件 STALE_CLAIM_MS(1 小时)后合并回队列。
四种事件类型:
install— 每次安装器运行一条。属性:targets(如["claude","cursor"])、scope(local/global)、kind(fresh/upgrade/reinstall)。在 src/installer/index.ts 中通过getTelemetry().recordLifecycle('install', …)记录。index— 每次完整索引(init/index,而不是每次sync)一条。属性:languages(仅语言名)、file_count_bucket(<100、100-1k、1k-10k、10k+)、duration_bucket(<10s、10-60s、1-5m、5m+)。注意精确计数被刻意不采集——分桶函数bucketFileCount/bucketDuration就在 src/telemetry/index.ts,recordIndexEvent只提取filesByLanguage的键名,绝不携带路径、文件名或精确数量。usage_rollup— 主力事件。每台机器每个(day, kind, name)一条,本地聚合。属性:kind(mcp_tool/cli_command)、name(如codegraph_explore、affected)、count、error_count;MCP 场景另有client_name/client_version,取自initialize握手(src/mcp/session.ts),并在每次recordUsage调用中透传。uninstall— 每次uninstall/uninit运行一条(流失信号)。属性:targets。
一个值得展开的细节:Claude Code 提示词钩子会把它的"闸门决策"也聚合成 cli_command 计数器,命名为 prompt-hook-gate-<outcome>,outcome 取值固定为 high-keyword / high-token / medium-segment / nudge-projects / noop-shape / noop-no-index / noop-unverified / noop-explore-keyword / noop-explore-token / noop-vocab-empty——只记录决策名,永远不记录提示词内容。这正是该闸门实测召回率/精确率漏斗:noop-* 相对 high/medium 档位占比上升,就是关键词表或分段匹配漏掉真实问题的信号;high-* 表示上下文确实被注入——若对应的 codegraph_explore 出错或返回空,则改记 noop-explore-<trigger>;MEDIUM 候选命中尚未回填的分段词表则记 noop-vocab-empty。客户端源码 src/bin/codegraph.ts 中 recordUsage('cli_command', \prompt-hook-gate-${outcome}`, true)` 印证了这一机制。
此外有一个遗留字段:sqlite_backend(native/wasm)在 install 和 index 上仍被接受(schema v2 之前的旧客户端会发送),但当前客户端不再发送。它永远不是 required,待旧客户端占比可忽略后即可从 Worker 移除。
量级数学:由于是 rollup,月事件量 ≈ 活跃机器数 × 活跃天数 × 使用的工具种类(个位数)——设计上就没有逐调用事件。文档给出的运算是:约 97k 接受的 POST/天 ≈ 30M 行 D1 写入/月,对 Workers Paid 包含的 50M;保留期清理达到稳态后删除与插入同价,约升至 48M。真正的约束是存储而非写入:原始事件约 74 MB/天增长,90 天窗口 ≈ 6.7 GB,撞 D1 单库 10 GB 上限——这就是保留窗口定为 90 天的原因。完整算术写在 telemetry-worker/migrations/0001_init.sql 的尾部注释中。
系统里不存在任何"人"的画像可退出:machine_id 是唯一标识符,由客户端铸出的随机 UUID,唯一机器数直接在 SQL 中从它计算。
三、同意与控制:优先级、配置面与 CLI
解析顺序(首个命中生效),在 src/telemetry/index.ts 的 getStatus() 中逐条实现:
DO_NOT_TRACK=1(社区标准,永远优先)→ 关;CODEGRAPH_TELEMETRY=0|1→ 对该进程强制关/开(注意实现中0/false视为关,其余非空值视为开);- 全局配置
~/.codegraph/telemetry.json→ 存储的用户选择; - 默认:开,由首次运行通知把关。
三个交互面:
- 安装器(交互式):现有提示流中一个可见的 clack 开关——"Share anonymous usage data? (no code, paths, or names — see TELEMETRY.md)"——默认 yes。选择以
consent_source: "installer"持久化;重跑/升级尊重已存选择,不再追问。 - 无头路径(
npx codegraph init、MCP server——无 TTY,绝不弹窗):在第一次真正发送之前(记录只是本地缓冲、保持静默,因此安装器的显式开关永远先于任何通知出现)向 stderr 打印一行,并记录first_run_notice_shown:codegraph collects anonymous usage stats (no code, paths, or names) — "codegraph telemetry off" or CODEGRAPH_TELEMETRY=0 disables. Details: TELEMETRY.md - CLI:
codegraph telemetry status|on|off(status 打印机器 ID、当前状态及决策来源)。删除~/.codegraph/telemetry.json可重置一切,包括机器 ID。
配置文件结构(面向用户文档 TELEMETRY.md 与实现一致):
{
"enabled": true,
"machine_id": "uuid-v4",
"consent_source": "installer | default-notice | cli",
"first_run_notice_shown": true,
"updated_at": "2026-06-12T00:00:00Z"
}
~/.codegraph/ 目录是遥测引入的新全局目录(此前不存在任何全局数据);若用户索引 $HOME 本身,它与按项目存放在 <project>/.codegraph/ 的数据因文件名不同而互不冲突。
从用户视角的操作方式(见 TELEMETRY.md):codegraph telemetry off 存储选择并删除所有未发送数据;export CODEGRAPH_TELEMETRY=0 做按 shell/按 CI 覆盖;export DO_NOT_TRACK=1 是跨工具标准,永远生效。codegraph telemetry status 显示当前状态、决策来源与机器 ID。"关就是关":禁用后不记录、不连接、不发任何"已退出"ping。另外独立于遥测,MCP server 每天至多一次在后台检查 GitHub 上的新版本号(只取版本号,不发送任何机器信息);DO_NOT_TRACK=1 也会禁用它,仅关更新检查可用 CODEGRAPH_NO_UPDATE_CHECK=1。
四、客户端架构:内存计数器 + JSONL 缓冲 + 机会性 flush
新模块 src/telemetry/index.ts 是单一小模块、零依赖,围绕四条不变量(文件头注释逐条列出:零热路径开销、零 stdout、关即真关、失败静默)实现:
1. 内存计数器。 记录一次工具调用/CLI 命令就是内存自增。热路径上什么都不落盘、不碰网络。MCP 工具处理器调用 telemetry.count('mcp_tool', name, ok) 后继续。对应源码中 src/mcp/session.ts 的 recordUsage('mcp_tool', toolName, !result.isError, this.clientInfo) 与 CLI 侧 src/bin/codegraph.ts 的 recordUsage('cli_command', name, true)。
2. 缓冲。 计数器防抖异步持久化到 ~/.codegraph/telemetry-queue.jsonl,每行是一个压缩字段的小 JSON(CountLine:d 日、k 类型、n 名称、c 次数、e 错误数、cn/cv 客户端名/版本;或 EventLine 生命周期事件)。硬上限约 256 KB,溢出时丢最旧行(appendLines 中先截断再丢弃不完整的半行);缓冲损坏 → 截断,绝不抛错。
3. Flush 策略——为 process.exit() 而设计。 很多 CLI 动作以 process.exit() 结束,此时 beforeExit 永不触发、异步发送必死。因此设计是:在 process.on('exit') 上做一次微小的同步追加(persistSync(),可存活于 process.exit),真正的网络发送机会性发生——长运行命令(init/index/sync/uninit/upgrade)启动时、MCP server/daemon 上 unref 的 6 小时间隔(startInterval,src/mcp/index.ts 中启动)、以及 install/init/index/uninit 结尾处"有上限的 await"(一秒不可见之处)。发送 POST 已完成 UTC 天的 rollup + 生命周期事件到 https://telemetry.getcodegraph.com/v1/events,带 AbortSignal.timeout(1500),fire-and-forget:任何响应(或没有响应)都是终局——不重试、不冒泡错误。实现上 flushNow() 只发送"今天之前"的计数行与全部生命周期事件,当天的行留在队列;队列通过原子 rename 抢占(telemetry-queue.sending.<pid>.jsonl),并发进程不会重复发送;崩溃的发送方留下的 claim 在 1 小时后被 recoverStaleClaims 合并回队列。开发调试可用 CODEGRAPH_TELEMETRY_DEBUG=1 把 payload 回显到 stderr。
4. 离线/隔离网环境。 flush 静默失败,缓冲保持在容量内,稳态是一个有界文件 + 零噪音。
同意闸门的一个精妙之处(firstRunNotice() 上方注释):一次性通知严格先于离开机器的第一个字节,并在那里铸出机器 ID。由于记录只是本地缓冲,保持静默——这保证安装器能先展示其显式同意开关,而不是让 preAction 使用计数抢先触发通知;显式的安装器/CLI 选择会置 first_run_notice_shown 并永久抑制该通知。
五、Ingest Worker:白名单校验、无出站请求、fail-silent 写入
telemetry.getcodegraph.com 背后是仓库内的 telemetry-worker/——刻意公开,让任何人都能审计端点到底存了什么。它不随 npm 包分发(被 files 白名单排除),部署配置见 telemetry-worker/wrangler.jsonc:路由绑定 telemetry.getcodegraph.com 自定义域名(workers_dev: false,唯一公开面就是文档里那个域名),D1 数据库 codegraph-telemetry 绑定为 env.DB,cron 触发器 30 0 * * *(00:30 UTC),RETENTION_DAYS: 90,以及每分钟每 machine_id 6 次的 MACHINE_RATE_LIMITER("合法客户端每天只 flush 几次;6/min 吸收 install+index 突发,同时压住滥用")。
POST /v1/events 的处理流程(telemetry-worker/src/index.ts):
- 白名单校验:
EVENTS表是"那份白名单",与设计文档逐条镜像——未知事件整体丢弃,未知属性剥离;每个属性带 sanitizer:tokenArray(targets/languages,条目数与字符集受限)、oneOf(scope/kind/分桶枚举)、nonNegInt(count ≤ 1,000,000)、label(客户端名/版本,可含空格与/)。时间戳经过clampTimestamp钳制:未来超过 10 分钟、过去超过 30 天的一律拒绝(离线缓冲会迟到,但不接受不可信时间)。 - 边界:
machine_id必须匹配 UUIDv4 正则;body 上限 64 KB、单批最多 100 事件;按machine_id做最佳努力限流(限流器不可用时 fail-open——丢数据点优于丢可用性)。 - 永不读取客户端 IP——源码中不存在任何读取 request IP 的路径。
- 写入在响应路径之外:
ctx.waitUntil(writeToD1(...)),一次batch()= 一个隐式事务 = 一次往返。每条事件一行events,外加machine_days(machine × day 活动矩阵,prod列用max语义:只有当天该机器所有事件都带ci=1才为 0)与machine_first_seen(离线迟到缓冲可把首见日推早,min语义,绝不推晚)。刻意 fail-silent:D1 错误只以计数记日志(绝不记 payload),客户端照样收到 204。因为客户端从不重试,丢一个数据点优于牺牲可用性。 - 响应:接受即 204(包括"全部事件被白名单丢弃"的情况),格式错误/超大/限流返回诚实的 4xx。客户端把任何响应视为终局,从不重试。
存储 Schema 全量公开在 telemetry-worker/migrations/0001_init.sql,每列带注释。核心表:
events:原始事件。刻意不加CHECK (event IN (...))约束——Worker 的白名单才是唯一真相来源,而写入路径按设计 fail-silent(被拒绝的 INSERT 会静默丢数据而非显式报错)。索引只有(day, event)与(machine_id, day)两个,注释明确警告 D1 按"每触碰一个索引多计一次行写入"计费,所以每个索引约等于 97k 写入/天,加第三个前必须重查量级注释;daily_machines/daily_event_counts/daily_dim_counts:夜间 cron 写入、dashboard 读取的 rollup 表,永久保留。daily_dim_counts是通用(dim, value)表——新增一个 breakdown 是改一行 cron,而不是迁移。注释列出了每个dim服务的图表(os、arch、version、language、target、scope、kind、name、client_name 等);machine_days/machine_first_seen:永不清理——留存队列需要完整历史,且比原始行小两个数量级。
夜间 cron(telemetry-worker/src/rollup.ts,00:30 UTC 由 wrangler.jsonc 触发)做两件事:
- Roll up:把刚完成的 UTC 日(以及前两天的——离线客户端会迟到地发送已完成天的 rollup)roll 进
daily_machines、daily_event_counts、daily_dim_counts。全部聚合是 D1 内部的INSERT … SELECT … ON CONFLICT DO UPDATE——没有事件行跨网络传输,重跑某天是幂等 no-op 而非双计。一个关键细节:usage_rollup行本身是客户端预聚合的计数器,所以count维度必须SUM(json_extract(props,'$.count'))而不是数行——数行会把量级低估一个数量级; - Purge:删除超过
RETENTION_DAYS(90 天)的原始events,按 5000 行/批、单晚最多 60 批的有界 keyset 删除推进。Rollup 与machine_days/machine_first_seen永久保留,所以缩短窗口只损失 ad-hoc 回溯,从不损失图表。
此外还有 POST /admin/rollup 回填补丁口:仅在 ADMIN_TOKEN secret 配置时存在(否则直接 404),用 SHA-256 摘要 + timingSafeEqual 做常量时间 token 比较,支持 ?day=YYYY-MM-DD[&days=N][&reset=1](最多 31 天)。reset=1 会先删除当日事件派生的 rollup 行再重算(用于维度列表变更时的修复),但越过保留窗口的日期会忽略 reset——那会是这个文件唯一不可逆的错误操作。部署、迁移、cron 与 D1 配额算术的运维细节在 telemetry-worker/README.md。
六、Admin Dashboard 与 codegraph-pro 规则
stats.getcodegraph.com 是第二个 Worker(telemetry-dashboard/),读侧,同样是公开源码——"接触遥测的代码应能被采集它的人读懂"。要点:
- 同一 D1 数据库,只读:从不迁移、从不写;两个 Worker 是独立部署,仅按约定共享一份维度名清单——这正是 telemetry-worker/scripts/smoke-cutover.sh 存在的原因:那边不匹配是静默的,表现为某个面板永远读零而非报错;
- 读 rollup 而非原始事件,所以对原始行已被清掉的日期图表依然正确。唯一例外是
/api/activation——"这台机器是否运行过 index"不是日聚合,因此读原始events,受保留窗口约束,并以raw_events_from报告这一点; - 认证是共享密码 + 签名 cookie,规模按"恰好两个人"设计:
ADMIN_PASSWORD+SESSION_SECRET作为 Worker secrets,常量时间比较、HMAC 签名 cookie、无 session 存储、除/login与robots.txt外全部拦截。轮换密码即全员登出——这就是撤销机制; - 这个 Worker 会读客户端 IP,唯一用途是登录限流 key,从不存储、从不记录——与 ingest Worker(根本不读 IP)的唯一刻意差异。
codegraph-pro 规则("不要在上游合并中丢失这一条"):私有 codegraph-pro fork 随客户容器分发,其保证是"什么都不离开盒子"——包括遥测。在 fork 中遥测必须默认关闭且安装器无法开启(编译期常量或剥离模块),容器另设 CODEGRAPH_TELEMETRY=0 双保险。该规则写进 fork 的 CLAUDE.md,且必须存活于每次上游合并。
七、灰度路径与测试基线
文档给出的上线顺序:(1) 设计文档 + 仓库根 TELEMETRY.md(逐字段用户列表)+ README 章节;(2) Worker + DNS 先上线(保证第一个发货的客户端永不 404),随后 dashboard Worker 接同一 D1:周活机器、按目标的安装、按工具 × 客户端的使用、版本普及、被索引语言;(3) 客户端模块 + 配置 + codegraph telemetry 子命令 + MCP clientInfo 管道;(4) 安装器开关 + 首次运行通知,CHANGELOG 在 [Unreleased] 下公告遥测、默认值与每一个关闭开关,发布。
测试约定(不 mock 数据库;fetch 在 globalThis.fetch 处 mock)覆盖:同意优先级(env > config > default)、off ⇒ 零 fetch 调用、跨天 rollup 聚合、缓冲上限 + 损坏缓冲恢复、MCP 传输下无 stdout 不变量、flush abort 遵守超时、安装器开关持久化 + 重跑不追问。仓库内对应 tests/telemetry.test.ts——其文件头直接声明"固定 docs/design/telemetry.md 的四条不变量:零 stdout、关即真关(无 socket 无文件)、失败静默、仅发送已完成天的本地 rollup",全部注入缝(dir、fetch、时钟、env、stderr),不触网、不碰真实 home 目录;另按仓库惯例,安装器开关持久化测试在 tests/installer-targets.test.ts。
八、遗留问题
文档末尾保留了三个开放问题,可视为该契约的演进方向:安装器文案/通知措辞的最终定稿(发布前维护者拍板);uninstall 事件去留(诚实的流失信号 vs. "临走还要 ping 一下"的观感);CI 事件保留(ci: true 打标,因为"引擎跑在 CI 里"是真实使用模式——若其占比主导量级则重议)。
小结:这套设计可复用的几条原则
以 docs/design/telemetry.md 为契约、以 src/telemetry/index.ts 与 telemetry-worker/ 为两端实现,CodeGraph 的遥测给出了一个"隐私友好遥测"的完整参考样本:
- 白名单而不是黑名单:schema 同时是客户端契约与服务端校验器,新增字段必须一次 PR 改三处;
- 本地预聚合:逐调用事件在客户端就坍缩为
(day, kind, name)计数器,量级从"调用数"降到"机器数 × 天数 × 工具数"; - 为
process.exit()设计持久化:同步小写在exit钩子、机会性发送、原子 rename 抢占、陈旧 claim 回并——离线与并发都不出错; - 端到端 fail-silent:客户端任何失败是静默,Worker 写入失败只记计数,客户端从不重试——"丢数据点优于丢可用性"是显式取舍;
- 可审计 > 承诺:Worker 源码、存储 Schema、rollup 与清理逻辑全部在仓库内公开,"保留什么"由 telemetry-worker/migrations/0001_init.sql 逐列注释回答,而不是靠口头保证;
- 让保留期由数学决定:74 MB/天 × 90 天 ≈ 6.7 GB 撞上 D1 的 10 GB 单库上限,于是 90 天窗口被写下;rollup 永久保留使缩短窗口只损失回溯、不损失图表。
读者可以按 TELEMETRY.md 用 codegraph telemetry status|off 验证自己机器上的状态,也可以直接阅读 ingest Worker 与迁移 SQL,逐字段核对"到底存了什么"——这正是该文档要求系统做到的"可证明"。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00