首页
/ PostHog Proto 变更全流程指南:同步更新 Python、Node.js 与 Rust 下游消费者

PostHog Proto 变更全流程指南:同步更新 Python、Node.js 与 Rust 下游消费者

2026-09-09 22:12:57作者:庞眉杨Will

本文基于 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_SUFFIXRPC_RESPONSE_STANDARD_NAMECOMMENT_* 系列;
  • 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)的实际逻辑:

  1. 校验 grpc_toolsprotoletariat 两个 Python 包是否可用,缺失时报错提示 uv sync(即需先同步 Python 依赖);
  2. 清空并重建输出目录 posthog/personhog_client/proto/generated
  3. python -m grpc_tools.protocproto/personhog/serviceproto/personhog/types 下的所有 .proto 生成 _pb2.py / _pb2.pyi / _pb2_grpc.py
  4. 再以 protol --create-package --in-place 方式重写包结构,得到 posthog/personhog_client/proto/generated/personhog/... 下按命名空间分层的可导入模块;
  5. 最后用 ruff check --fixruff format 对生成代码做 lint 与格式化。

第二步,同步三处手写代码(这是最容易被遗漏的部分):

仓库在 posthog/personhog_client/ 下还配套了 test_converters.pytest_fake_client.pytest_interceptor.pytest_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 generatenodejs/buf.gen.yaml 模板生成到 nodejs/src/common/generated/personhog,再对生成结果跑 eslint 与 prettier 格式化(保证入库代码风格统一)。

第二步,同步测试桩:

新增 RPC 后,必须在 nodejs/src/common/personhog/client.test.tsSERVICE_DEFAULTS 对象(见 client.test.tsServiceImpl<typeof PersonHogService> 类型定义处)中为每个新 RPC 添加默认 stub。SERVICE_DEFAULTSService mock 的基线实现,后续测试通过 ...SERVICE_DEFAULTS 展开它再按需覆盖单个方法,缺少默认桩会导致 mock 校验失败。

3.3 Rust:无需 codegen,但必须实现与接线

Rust 侧没有 codegen 步骤——tonic 在 cargo build 时自动重新生成 rust/personhog-proto 绑定,但新增 RPC 后必须完成三件事(参考 proto/AGENTS.md):

  1. rust/personhog-replica/ 实现 RPC:包含存储层(storage)与服务 handler(service handler)两部分,即先在存储层实现数据访问逻辑,再把它接到 gRPC handler 上;
  2. rust/personhog-router/ 完成接线:router 是 personhog 的入口与转发层,需要打通 backend、router、service 三层——backend 层建立到后端的连接、router 层做路由选择、service 层暴露 gRPC 服务;
  3. 补充测试:遵循 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_idstream_epoch),worker 以此重置 per-key 基线——consumer 重启会重放未提交的 offset,因此必须重新基线而非误报乱序;
  • SubBatch 携带 seq(流内单调递增,worker 拒绝空洞与回退)、batch_idKafkaMessage 列表、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 时自动重新生成。KafkaMessagerust/ingestion-consumer/src/types.rsnodejs/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.shnodejs/src/common/generated/**nodejs/buf.gen*.yamlnodejs/package.json 等路径,命中后依次跑四个 job:

  1. lintbuf lint proto/,检查风格与命名(受 proto/buf.yamlexcept 放宽项影响);
  2. breakingbuf breaking proto/ --against '...posthog.git#branch=master,subdir=proto',以 master 为基准做 FILE 级向后兼容检查;
  3. 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"
  4. node-codegen:先 pnpm --dir nodejs run generate:personhog-protogenerate:ingestion-worker-proto,再对 nodejs/src/common/generated/personhog/ingestion-worker/ 做同样的 diff 检查。

最后一个聚合 job proto_checks 汇总四个结果,任一失败整体红掉。这解释了规则文件中"所有下游必须同一次变更完成"的工程动机:CI 会直接拒绝任何"只改了 .proto 却忘了重新生成/提交 stubs"的 PR。

六、实操检查清单:一次完整的 proto 变更

把以上内容收敛为可执行清单(适用 personhog 类协议;ingestion 类跳过 Python 步骤):

  1. 改定义:编辑 proto/personhog/**/*.proto,新增/修改 RPC 或 message;改动前注意 proto/buf.yaml 的 breaking 豁免项是否覆盖你触碰的文件,未覆盖的破坏性改动会被 CI 拦截;
  2. Python:运行 bash bin/generate_personhog_proto.sh,提交 posthog/personhog_client/proto/generated/;message 变化时更新 proto/init.py 的 re-export(保持 common_pb2 先导入),RPC 变化时更新 client.pyfake_client.py
  3. Node.js:运行 cd nodejs && pnpm run generate:personhog-proto,提交 nodejs/src/common/generated/personhog/,并在 client.test.tsSERVICE_DEFAULTS 中为每个新 RPC 补默认 stub;
  4. Rust:等待 cargo build 自动重生成绑定后,在 rust/personhog-replica/ 实现存储层与 handler,在 rust/personhog-router/ 打通 backend/router/service 三层,并按 rust/personhog-replica/AGENTS.md 的约定补测试;
  5. 提交流:将 .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 三端始终与协议定义保持同步。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
docsdocs
暂无描述
Markdown
899
5.83 K
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.76 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
860
1.35 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
925
1.85 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.84 K
1.02 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
533
601
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.37 K
1.46 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
395
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.04 K
525