PostHog Proto 变更全流程指南:同步更新 Python、Node.js 与 Rust 下游消费者
本文基于 PostHog 仓库内 .claude/rules/proto-changes.md 规则文件展开,系统讲解在 PostHog 多语言 monorepo 中安全地新增或修改 RPC / message 类型的完整协议:从编辑 .proto 定义、重新生成各语言绑定,到同步更新手写的 wrapper、测试桩与 Rust 实现,最后通过 CI 的 lint、breaking 与 staleness 三重门禁。读完本文,你将掌握 PostHog 仓库内 proto 变更的标准操作流,并能对照清单独立完成一次跨 Python、Node.js、Rust 的协议升级。
一、规则文件的定位:一条强制的原子变更约束
仓库根目录的 .claude/rules/proto-changes.md 是一条作用于 proto/** 路径的规则:
- 适用范围:所有对
proto/目录下 RPC 或 message 类型的新增与修改; - 硬性要求:所有下游消费者必须在同一次变更中同步更新,不允许把协议改动与各语言适配拆成多个 PR;
- 执行依据:完整操作协议见 proto/AGENTS.md(仓库同时提供内容相同的 proto/CLAUDE.md)。
这条规则的本质是:PostHog 的 gRPC 服务同时被 Python(Django 后端)、Node.js(摄取 worker 等)和 Rust(personhog 系列服务)三个语言栈消费,任何 .proto 改动若不一次性铺平所有绑定与实现,都会导致编译失败或运行时协议不匹配。
二、proto 目录全景:PostHog 的协议定义分布
在动手改协议前,先了解 proto/README.md 勾勒的目录结构:
proto/
├── buf.yaml # buf lint / breaking 配置
├── cymbal/ # Cymbal 内部服务
│ └── resolution/v1/ # 异常级符号解析
├── ingestion/ # 事件摄取
│ └── worker/v1/ # Consumer → worker 流式摄取传输
├── kafka_assigner/ # Kafka 分区分配
├── personhog/ # Person 数据服务(变更最频繁)
│ ├── types/v1/ # cohort / common / feature_flag / group / person
│ ├── identity/v1/
│ ├── leader/v1/
│ ├── lifecycle/v1/
│ ├── replica/v1/
│ └── service/v1/ # PersonHogService 公开 API
├── prometheus/ # Prometheus remote write
│ └── v1/
└── usage_ingestion/ # 用量摄取服务
└── v1/
2.1 各 proto 的跨语言消费者矩阵
README 中用一张表明确了每个 proto 目录与三种语言消费方的关系,这是判断"这次改动要动哪些下游"的依据:
| Proto | Rust | Python | Node.js |
|---|---|---|---|
cymbal/ |
rust/cymbal-proto(tonic 自动) |
— | — |
ingestion/ |
rust/ingestion-worker-proto(tonic 自动) |
— | nodejs/src/common/generated/ingestion-worker(入库) |
personhog/ |
rust/personhog-proto(tonic 自动) |
posthog/personhog_client/proto/generated/(入库) |
nodejs/src/common/generated/personhog(入库) |
kafka_assigner/ |
rust/kafka-assigner-proto(tonic 自动) |
— | — |
prometheus/ |
rust/prometheus-rw-proto(tonic 自动) |
— | — |
usage_ingestion/ |
rust/usage-ingestion-proto(tonic 自动) |
— | nodejs/src/common/generated/usage-ingestion(入库) |
规律很清晰:Rust 侧全部由 tonic 在 cargo build 时自动生成,无需手动 codegen;Python 只有 personhog 需要生成;Node.js 则把生成结果提交入库——这正是后面三套操作流差异的来源。
2.2 buf 配置:lint 与 breaking 的规则基调
proto/buf.yaml 采用 version: v2,两个关键配置块:
- lint:基于
STANDARD规则集,但针对内部服务放宽了服务/消息命名与注释要求,例如关闭SERVICE_SUFFIX、RPC_RESPONSE_STANDARD_NAME、COMMENT_*系列; - breaking:使用
FILE级别的向后兼容检查,并存在若干"临时豁免",例如允许删除已退役 RPC(RPC_NO_DELETE豁免personhog/replica/v1/replica.proto等)、允许在ingestion/worker/v1/worker.proto上原地修改字段。注意这些豁免均标注"Temporary",等对应改动合入 master 后应移除。
三、Personhog 协议变更:三种语言的下游同步
当你在 proto/personhog/ 下新增或修改 RPC / message 类型时,必须按下述顺序把 Python、Node.js、Rust 三个下游一次铺平。当前 PersonHogService 定义在 proto/personhog/service/v1/service.proto,涵盖 person 查询、distinct ID 操作、hash key override、cohort 成员关系、group 读写与映射、person 属性更新、生命周期 fence、删除与 split 等约 40 个 RPC,是仓库内最大的 gRPC 服务面。
3.1 Python:生成 stub + 手动补齐三处
第一步,运行生成脚本:
bin/generate_personhog_proto.sh
该脚本(bin/generate_personhog_proto.sh)的实际逻辑:
- 校验
grpc_tools与protoletariat两个 Python 包是否可用,缺失时报错提示uv sync(即需先同步 Python 依赖); - 清空并重建输出目录
posthog/personhog_client/proto/generated; - 用
python -m grpc_tools.protoc对proto/personhog/service与proto/personhog/types下的所有.proto生成_pb2.py/_pb2.pyi/_pb2_grpc.py; - 再以
protol --create-package --in-place方式重写包结构,得到posthog/personhog_client/proto/generated/personhog/...下按命名空间分层的可导入模块; - 最后用
ruff check --fix与ruff format对生成代码做 lint 与格式化。
第二步,同步三处手写代码(这是最容易被遗漏的部分):
- posthog/personhog_client/proto/init.py——新增/删除 message 类型时,更新 re-export。注意该文件顶部有一条关键约定:
common_pb2必须先于依赖它的cohort、group、person导入(它们的序列化 descriptor 引用了 common.proto 的定义); - posthog/personhog_client/client.py——新增/删除 RPC 时,添加/移除对应的 wrapper 方法;
- posthog/personhog_client/fake_client.py——为测试场景实现新方法的 fake 版本。
仓库在 posthog/personhog_client/ 下还配套了 test_converters.py、test_fake_client.py、test_interceptor.py、test_retry_interceptor.py 等测试,新增 RPC 时同样建议按既有测试模式补充覆盖。
3.2 Node.js:buf 生成 + 测试桩默认值
第一步,生成 Node.js 绑定:
cd nodejs && pnpm run generate:personhog-proto
该命令(定义于 nodejs/package.json)实际执行:
cd ../proto && buf generate --template ../nodejs/buf.gen.yaml \
--path personhog/service --path personhog/identity \
--path personhog/lifecycle --path personhog/types . \
&& cd ../nodejs && eslint --fix src/common/generated/personhog \
&& prettier --write src/common/generated/personhog
即先用 buf generate 按 nodejs/buf.gen.yaml 模板生成到 nodejs/src/common/generated/personhog,再对生成结果跑 eslint 与 prettier 格式化(保证入库代码风格统一)。
第二步,同步测试桩:
新增 RPC 后,必须在 nodejs/src/common/personhog/client.test.ts 的 SERVICE_DEFAULTS 对象(见 client.test.ts 中 ServiceImpl<typeof PersonHogService> 类型定义处)中为每个新 RPC 添加默认 stub。SERVICE_DEFAULTS 是 Service mock 的基线实现,后续测试通过 ...SERVICE_DEFAULTS 展开它再按需覆盖单个方法,缺少默认桩会导致 mock 校验失败。
3.3 Rust:无需 codegen,但必须实现与接线
Rust 侧没有 codegen 步骤——tonic 在 cargo build 时自动重新生成 rust/personhog-proto 绑定,但新增 RPC 后必须完成三件事(参考 proto/AGENTS.md):
- 在 rust/personhog-replica/ 实现 RPC:包含存储层(storage)与服务 handler(service handler)两部分,即先在存储层实现数据访问逻辑,再把它接到 gRPC handler 上;
- 在 rust/personhog-router/ 完成接线:router 是 personhog 的入口与转发层,需要打通 backend、router、service 三层——backend 层建立到后端的连接、router 层做路由选择、service 层暴露 gRPC 服务;
- 补充测试:遵循 rust/personhog-replica/AGENTS.md 中的 Rust 测试约定(该文件同时是仓库内 Rust 代码测试规范的核心参考)。
四、Ingestion worker 协议:独立的双端流程
proto/ingestion/worker/v1/worker.proto 定义的是 Rust 摄取 consumer 与 Node.js ingestion-api worker 之间的双向流式摄取通道,其语义细节非常丰富,可作为"读懂一个 proto 如何承载强顺序保证"的范例:
IngestStream是唯一的双向流 RPC:consumer 与 worker 之间按(进程, worker pod)建立一条流,SubBatch按发送顺序投递、按读取顺序进入流水线,从而在"同一 routing key 固定在同一个 worker"(由 consumer 的 dispatcher pin 保证)的前提下维持 per-key 顺序;- 首帧必须是
StreamHello(携带consumer_id与stream_epoch),worker 以此重置 per-key 基线——consumer 重启会重放未提交的 offset,因此必须重新基线而非误报乱序; SubBatch携带seq(流内单调递增,worker 拒绝空洞与回退)、batch_id、KafkaMessage列表、replay(重连重放 / 延迟 flush 改道时为 true)、assignment_epoch(分区再平衡时合法重放);- worker 通过
SubBatchStatus枚举回执:OK(提交屏障,等价于现在的 HTTP 200)、FAILED(流必须判死)、BUSY(瞬时背压,consumer 按序重放);consumer 对任何不认识的 status 都按 BUSY 处理,因此未来新增非致命 status 可保持向后兼容。
针对该 proto 的变更流程(同样来自 proto/AGENTS.md):
cd nodejs && pnpm run generate:ingestion-worker-proto
实际命令(nodejs/package.json)为:
cd ../proto && buf generate --template ../nodejs/buf.gen.ingestion-worker.yaml --path ingestion/worker . \
&& cd ../nodejs && eslint --fix src/common/generated/ingestion-worker \
&& prettier --write src/common/generated/ingestion-worker
生成的 Node.js stubs 必须提交入库(CI 会拒绝过期的 stubs);Rust 侧 rust/ingestion-worker-proto 同样在 cargo build 时自动重新生成。KafkaMessage 与 rust/ingestion-consumer/src/types.rs、nodejs/src/ingestion/api/types.ts 中的 SerializedKafkaMessage 保持镜像对应,改动时需一并核对这两个手写类型。
五、CI 门禁:改动是否"完成"的裁决标准
.github/workflows/ci-proto.yml 是 proto 变更的自动化验收线。它通过 paths-filter 监听 proto/**、posthog/personhog_client/proto/generated/**、bin/generate_personhog_proto.sh、nodejs/src/common/generated/**、nodejs/buf.gen*.yaml、nodejs/package.json 等路径,命中后依次跑四个 job:
- lint:
buf lint proto/,检查风格与命名(受 proto/buf.yaml 的except放宽项影响); - breaking:
buf breaking proto/ --against '...posthog.git#branch=master,subdir=proto',以 master 为基准做 FILE 级向后兼容检查; - python-codegen:在 Python 3.13 环境安装
grpcio-tools protoletariat ruff后重跑bash bin/generate_personhog_proto.sh,再用git add -N+git diff --exit-code对比posthog/personhog_client/proto/generated/,有任何差异即判失败并提示"Run 'bash bin/generate_personhog_proto.sh' and commit the result"; - node-codegen:先
pnpm --dir nodejs run generate:personhog-proto与generate:ingestion-worker-proto,再对nodejs/src/common/generated/personhog/与ingestion-worker/做同样的 diff 检查。
最后一个聚合 job proto_checks 汇总四个结果,任一失败整体红掉。这解释了规则文件中"所有下游必须同一次变更完成"的工程动机:CI 会直接拒绝任何"只改了 .proto 却忘了重新生成/提交 stubs"的 PR。
六、实操检查清单:一次完整的 proto 变更
把以上内容收敛为可执行清单(适用 personhog 类协议;ingestion 类跳过 Python 步骤):
- 改定义:编辑
proto/personhog/**/*.proto,新增/修改 RPC 或 message;改动前注意 proto/buf.yaml 的 breaking 豁免项是否覆盖你触碰的文件,未覆盖的破坏性改动会被 CI 拦截; - Python:运行
bash bin/generate_personhog_proto.sh,提交posthog/personhog_client/proto/generated/;message 变化时更新 proto/init.py 的 re-export(保持common_pb2先导入),RPC 变化时更新 client.py 与 fake_client.py; - Node.js:运行
cd nodejs && pnpm run generate:personhog-proto,提交nodejs/src/common/generated/personhog/,并在 client.test.ts 的SERVICE_DEFAULTS中为每个新 RPC 补默认 stub; - Rust:等待
cargo build自动重生成绑定后,在 rust/personhog-replica/ 实现存储层与 handler,在 rust/personhog-router/ 打通 backend/router/service 三层,并按 rust/personhog-replica/AGENTS.md 的约定补测试; - 提交流:将
.proto、全部生成文件与手写适配代码放在同一次提交/同一 PR 中,等待 CI 的 lint、breaking 与两路 staleness 检查通过。
遵循这套流程,你就能在 PostHog 的多语言 monorepo 中完成一次"协议改动不落地不罢休"的原子变更——既满足 .claude/rules/proto-changes.md 的强制规则,也通过 proto/AGENTS.md 与 .github/workflows/ci-proto.yml 的自动门禁,确保 Python、Node.js、Rust 三端始终与协议定义保持同步。
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 StartedRust0631
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
video-shotcraftAI宣传片skill,使用 Remotion 制作电影级产品视频:提供106 张镜头配方卡和可复用的视频魔板。适用于 Claude Code 与 Codex以及所有其他智能体Markdown00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python09
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00