mcpField
I
improving-drf-endpoints

improving-drf-endpoints

github.com/posthog开发40.5k201 次安装今天更新
代码仓库

关于 improving-drf-endpoints

用于改进 PostHog Django REST Framework 序列化器和端点的开发技能,帮助排查 OpenAPI 与生成类型问题,也涵盖为 MCP 开放端点的准备工作。

主要功能

  1. 01审查序列化器字段与描述
  2. 02检查生成类型和 API 客户端
  3. 03为 ViewSet 与自定义操作添加注解
  4. 04定义类型化响应与错误
  5. 05注册产品级团队嵌套路由

适用场景

  1. 01修复缺失或错误的生成类型
  2. 02审查序列化器或 ViewSet 变更
  3. 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.md
quick-reference-table.md
serializer-fields.md
viewset-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,逐项完成以下清单。

  1. 每个字段都有 help_text —— 描述用途、格式、约束、有效值
  2. 没有裸 ListField() 或 DictField() —— 始终用带类型的 serializer 或字段指定 child=
  3. 没有裸 JSONField() —— 创建一个自定义字段类,带 @extend_schema_field(TypedSchema)
  4. SerializerMethodField 的 get_* 方法上有 @extend_schema_field
  5. ChoiceField 有显式的 choices=,并列出所有有效值
  6. 把 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)
  7. 当输入结构与输出不同时,读与写的 serializer 要分开
  8. 每个成功响应都由 serializer 支撑 —— 返回原始 dict 或无类型列表意味着下游没有生成的类型

有关模式和示例,参见 serializer-fields.md。

ViewSet 与 action 注解
  1. 每个自定义 @action 都要有 @extend_schema 或 @validated_request——否则 drf-spectacular 会发现零个参数
  2. 普通 ViewSet 方法要有 schema 注解——带 serializer_class 的 ModelViewSet 会被自动发现;普通 ViewSet 不会
  3. @extend_schema 要放在实际方法上(get、post、create、list),而不是辅助函数或类本身上
  4. 错误响应要标注类型——使用 OpenApiResponse(response=ErrorSerializer),而不是 OpenApiTypes.OBJECT
  5. 列表端点要声明分页——对于不分页的自定义 action,用 pagination_class=None 重置
  6. 优先使用 @validated_request,而不是手动 serializer.is_valid() + @extend_schema——它在一个装饰器中同时处理两者
  7. 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 脚手架和前端类型生成器就无法将端点路由到正确的产品
  8. 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 — 查找未解决的冲突并建议覆盖

评论

支持 Markdown