关于 improving-drf-endpoints
用于改进 PostHog Django REST Framework 序列化器和端点的开发技能,帮助排查 OpenAPI 与生成类型问题,也涵盖为 MCP 开放端点的准备工作。
主要功能
- 01审查序列化器字段与描述
- 02检查生成类型和 API 客户端
- 03为 ViewSet 与自定义操作添加注解
- 04定义类型化响应与错误
- 05注册产品级团队嵌套路由
适用场景
- 01修复缺失或错误的生成类型
- 02审查序列化器或 ViewSet 变更
- 03准备供 MCP 工具使用的端点
MCP 相关
涵盖会影响生成的 MCP 工具架构的端点和序列化器修改,并指向 implementing-mcp-tools 技能以继续搭建工具。
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ posthog/ posthog --skill improving-drf-endpoints安装后在 Claude Code 中输入/improving-drf-endpoints确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/common-anti-patterns.mdquick-reference-table.mdserializer-fields.mdviewset-annotations.md详细介绍
改进 DRF 端点
在提议基于生成的 OpenAPI schema 进行契约测试之前,先查看 已经尝试过的事项。有六个 PR 采用了这个想法,但都没有合并。
概述
Serializer 字段是 PostHog 整个类型流水线的唯一事实来源:
Django serializer → drf-spectacular → OpenAPI JSON → Orval → Zod schemas → MCP tools
每一个 help_text、每一个字段类型、每一个 @extend_schema 注解都会向下游传递。
缺失 help_text 意味着 Agent 只能猜测参数。
裸 ListField() 意味着生成的 Zod schema 中出现 z.unknown()。
把 serializer 写对,意味着每一个消费方——前端类型、MCP 工具、API 文档——都能自动获得正确的类型和描述。
何时使用
- 编辑或审查任何定义
Serializer或ViewSet的文件 - 修复 OpenAPI spec 警告或生成的类型问题
- 为 MCP 工具暴露准备端点
- 对 API 变更进行代码审查
审查清单
分诊:先检查生成的输出
在深入 Python 之前,先查看已提交的生成类型,了解哪些地方有问题。 找到该端点所属产品对应的生成文件:
- Core API:
frontend/src/generated/core/ - 产品 API:
products/<product>/frontend/generated/
每个都有两个文件:
api.schemas.ts—— 由 serializer 派生出的 TypeScript 接口。搜索 serializer 名称,查找unknown类型(裸ListField/JSONField)、缺失的 JSDoc 描述(缺失help_text),或过于笼统的Record<string, unknown>结构。api.ts—— API 客户端函数。检查该端点的 operation 是否存在——如果缺失,说明 viewset 方法很可能缺少@extend_schema。
这会明确告诉你应优先处理哪些字段和端点。
Serializer 字段
对你接触的每个 serializer 和 viewset,逐项完成以下清单。
- 每个字段都有
help_text—— 描述用途、格式、约束、有效值 - 没有裸
ListField()或DictField()—— 始终用带类型的 serializer 或字段指定child= - 没有裸
JSONField()—— 创建一个自定义字段类,带@extend_schema_field(TypedSchema) SerializerMethodField的get_*方法上有@extend_schema_fieldChoiceField有显式的choices=,并列出所有有效值- 把 choices 定义为一个类 —— 产品的内部模块中用
models.TextChoices,facade 契约文件(backend/facade/contracts.py、backend/facade/enums.py)中用来自posthog/enums.py的LabeledStrEnum或LabeledIntEnum,这些文件不得导入 Django。无论哪种方式,OpenAPI 组件都以类命名(EarlyAccessFeature.Stage->EarlyAccessFeatureStageEnum,通过posthog/openapi/enum_names.py中的ChoicesEnumNameOverrides),因此基于类的 enum 永远不会在format、type、status、kind这类字段名上冲突。把X.choices传给choices=;要在SerializerMethodField上命名带标签的 enum,请使用@extend_schema_field(ChoiceField(choices=X.choices)),而不是返回类型提示。内联choices=[...]列表没有可读取的类,会与现有 choices 冲突,并会在--fail-on-warn下导致 CI 失败;对于没有类能承载的 choice 集合,可在posthog/settings/web.py中显式添加ENUM_NAME_OVERRIDES条目作为兜底(参见 serializer-fields.md) - 当输入结构与输出不同时,读与写的 serializer 要分开
- 每个成功响应都由 serializer 支撑 —— 返回原始 dict 或无类型列表意味着下游没有生成的类型
有关模式和示例,参见 serializer-fields.md。
ViewSet 与 action 注解
- 每个自定义
@action都要有@extend_schema或@validated_request——否则 drf-spectacular 会发现零个参数 - 普通
ViewSet方法要有 schema 注解——带serializer_class的ModelViewSet会被自动发现;普通ViewSet不会 @extend_schema要放在实际方法上(get、post、create、list),而不是辅助函数或类本身上- 错误响应要标注类型——使用
OpenApiResponse(response=ErrorSerializer),而不是OpenApiTypes.OBJECT - 列表端点要声明分页——对于不分页的自定义 action,用
pagination_class=None重置 - 优先使用
@validated_request,而不是手动serializer.is_valid()+@extend_schema——它在一个装饰器中同时处理两者 products/之外的 ViewSet 需要@extend_schema(extensions={"x-product": "<product>"})——位于products/<name>/backend/中的 ViewSet 通过模块路径自动归属;位于posthog/api/或ee/中的 ViewSet 不会自动归属,必须通过x-product扩展显式声明归属。接受普通字符串("product_analytics")或ProductKey.X枚举(kebab 值会被规范化)。不要用tags=["<product>"]来影响代码生成路由——tags仅用于 Swagger UI 显示。没有x-product,MCP 脚手架和前端类型生成器就无法将端点路由到正确的产品partial_update的request=覆盖必须是运行时写入字段的超集——extend_schema(request=CustomSerializer)会替换 drf-spectacular 从serializer_class推断出的内容;即使运行时序列化器仍然接受被省略的字段,它们也会从 OpenAPI、前端类型和 MCP 工具 schema 中消失。更改覆盖后,运行hogli build:openapi并验证生成的 MCP 工具 schema 仍然暴露每一个 OpenAPI body 字段
流式端点: 对于 SSE 或流式响应,使用 @extend_schema(request=InputSerializer, responses={(200, "text/event-stream"): OpenApiTypes.STR}) 来记录请求 schema,即使响应无法完整标注类型。
关于模式和示例,请参阅 viewset-annotations.md。
URL 路由 —— 在哪里注册新的团队嵌套端点
PostHog 曾短暂将项目和环境拆分为两个独立概念,随后又将这一拆分回滚。/api/projects/:team_id/... 是任何团队嵌套端点的规范路径。/api/environments/:team_id/... 是仅为拆分期间针对其集成过的客户端保留的向后兼容别名。
对于新的团队嵌套端点,将其注册在 routers.projects 下。路由位于各产品自己的 products/<name>/backend/routes.py 中,放在 register_routes(routers) 函数里:
# products/<name>/backend/routes.py
from posthog.api.routing import RouterRegistry
def register_routes(routers: RouterRegistry) -> None:
routers.projects.register(r"my_thing", MyThingViewSet, "project_my_thing", ["team_id"])
产品路由是自动发现的 —— posthog/api/__init__.py 会遍历 INSTALLED_APPS,并对每个带有 routes.py 的 products.* 应用调用 register_routes(routers)。新增产品无需修改核心代码:创建 products/<name>/backend/routes.py,并确保该产品位于 PRODUCTS_APPS(posthog/settings/web.py)中。只有核心的、非产品的 viewset 仍直接在 __init__.py 中注册。
为什么由核心发现并调用产品(而不是产品调用核心)。 核心先注册四个父级(root + projects/environments/organizations),然后运行发现循环。产品只嵌套到这些父级上,绝不互相嵌套,因此发现顺序无关紧要。注册保持为急切执行(它在 posthog.api 首次被导入时运行,即首次请求时),并有意地不移入 AppConfig.ready():ready() 在每个进程的 django.setup() 内运行,而注册路由会导入其 viewset,因此那样做会在所有地方把整个 API 拉进 setup() —— 从而破坏让 API 不进入 Celery worker 和管理命令的惰性。完整推理见 RouterRegistry 的文档字符串以及 posthog/api/__init__.py 中的发现循环。
将团队嵌套端点注册在 routers.projects 下,并使用 project_<name> 作为 basename。不存在 environments_router,也没有双路由辅助函数:遗留的 /api/environments/* 接口作为一组已注册路由已被弃用。
仍调用 /api/environments/... 的现有客户端由 EnvironmentsRewriteMiddleware 透明地提供服务,该中间件在进程内将路径重写到等价的 /api/projects/* viewset(不返回 307)。你无需为此注册 env 路由 —— 只需注册在 routers.projects 下,中间件会处理该别名。
门面产品(DataclassSerializer)
对于使用门面模式的产品(例如 visual_review),通过 DataclassSerializer 包装来自 contracts.py 的冻结数据类:
- 字段类型自动从数据类推导 —— 设计上减少了类型问题
- 将枚举字段类型标注为
LabeledStrEnum/LabeledIntEnum(或 Django 的Choices类),而不是普通的StrEnum。posthog/api/documentation.py中的 schema 扩展会用其类标签记录这些字段,因此它们的 OpenAPI 枚举以类命名。普通的StrEnum会回退为基于字段推导的名称。 - 重点关注
help_text(数据类字段不携带它;在序列化器字段覆写上添加) @validated_request已是标准模式 —— 验证响应序列化器已声明@extend_schema的标签和描述仍需在 viewset 方法上设置
决策流程图
digraph audit {
rankdir=TB
node [shape=diamond fontsize=10]
edge [fontsize=9]
start [label="Serializer or\nViewSet file?" shape=box]
is_model [label="ModelViewSet with\nserializer_class?"]
is_plain [label="Plain ViewSet or\ncustom @action?"]
is_facade [label="DataclassSerializer\n(facade product)?"]
check_fields [label="Check fields:\nhelp_text, ListField,\nJSONField, ChoiceField" shape=box]
add_schema [label="Add @validated_request\nor @extend_schema to\nevery method" shape=box]
check_help [label="Focus on help_text\nand response declarations" shape=box]
check_responses [label="Check response types,\npagination, error schemas" shape=box]
start -> is_model
is_model -> check_fields [label="yes"]
is_model -> is_plain [label="no"]
is_plain -> add_schema [label="yes"]
is_plain -> is_facade [label="no"]
is_facade -> check_help [label="yes"]
check_fields -> check_responses
add_schema -> check_fields
check_help -> check_responses
}
快速参考
参见 quick-reference-table.md,获取便于浏览的“我看到 X,就做 Y”查询表。
参见 common-anti-patterns.md,获取前后对比的代码示例。
代码库中的规范示例
- JSONField + @extend_schema_field:
products/alerts/backend/presentation/views/alert.py - @validated_request:
products/tasks/backend/presentation/views/api.py - help_text + 类型化响应:
products/ai_observability/backend/api/summarization.py - 门面产品:
products/visual_review/backend/presentation/views.py
相关
- 下游: 修复序列化器后,使用
implementing-mcp-tools技能来搭建 MCP 工具 - 流水线文档:
docs/published/handbook/engineering/type-system.md - 混入类:
posthog/api/mixins.py(@validated_request源码) - drf-spectacular 配置:
posthog/settings/web.py(SPECTACULAR_SETTINGS) - 枚举冲突诊断:
python manage.py find_enum_collisions— 查找未解决的冲突并建议覆盖