首页
/ PostHog Self-driving Inbox 新信号源接入指南:从 SignalSourceProduct 枚举到 Scout Emitter 的三端改造

PostHog Self-driving Inbox 新信号源接入指南:从 SignalSourceProduct 枚举到 Scout Emitter 的三端改造

2026-09-05 20:31:52作者:秋阔奎Evelyn

本文为 PostHog「Self-driving Inbox(自动驾驶收件箱)」的源码级扩展指南:当你需要把一个新的数据仓库来源(Jira、GitLab、Sentry、Intercom 等)接入 PostHog 的信号体系时,需要同时改动后端的信号发射器(scout emitter)、Code 前端的开关与表单接线,以及(可选的)CLI 自助配置向导。读完本文,你能独立完成一个新 inbox 信号源从枚举、迁移、HogQL 取数到 UI 开关卡片的全链路落地,并理解每个环节的底层实现与常见陷阱。

一、Self-driving Inbox 的工作原理

Self-driving Inbox 连接的是 PostHog 的数据仓库源(data-warehouse source):它同步一张「可行动记录」表(issues / tickets / conversations 等),再由 posthog/posthog 后端的一个服务端 signals scout 监听新行,把 findings/reports 发射进 Code Inbox。

关键设计约束是:scout 并不泛化地处理任意表——它由一个以 (source_type, table) 为键的静态注册表驱动。因此新增一个来源必然触碰三个表面(surface):

表面 改动内容
posthog/posthog(本仓库) 新的 scout emitter + 注册表条目 + SignalSourceProduct 枚举值(+ 迁移)+ 契约变体。前提:数据仓库源本身必须已经存在(所有 Tier-1 源均已存在)
posthog/code(独立仓库) 约 8 个 UI/接线文件:source-product 联合类型、开关卡片、设置表单、hook 映射、图标、过滤项(OAuth 源无需额外服务/路由)
PostHog/context-mill(独立仓库,可选) self-driving 技能的 connected-tools 列表,使 npx @posthog/wizard self-driving 主动推荐该来源。可跳过——跳过后来源在其他地方照常工作,只是向导不会主动推荐

前两个表面是强制的(缺了它们来源既无法发射信号,也无法在应用里被开关);第三个表面只决定 CLI 向导是否在 onboarding 时「主动提供」该来源。

部署顺序(不可跳过)

后端 SignalSourceProduct 的选择项必须先于 Code UI 上线。Code 端开关调用 createSignalSourceConfig({ source_product }),而 Django 模型会拒绝不在 choices 中的 source_product,返回 400。因此:先合入 posthog/posthog 的 PR(至少先合入枚举迁移),再合入 posthog/code 的 PR。若两个 PR 同时提交,应在 Code PR 描述中注明依赖关系。

二、后端(posthog/posthog)改造清单

Scout 位于 products/signals/backend/。数据仓库 → 信号的通用管道(fetcher、gate、warehouse hook)都是可复用的,每个来源真正新增的只有 emitter 模块和一条注册表条目,共 7 步:

1. 枚举:SignalSourceProduct

enums.py 中添加 SignalSourceProduct 成员,并同步添加 SIGNAL_SOURCE_PRODUCT_LABELS 条目。该文件的设计意图在源码注释中写得很清楚:这是一个「Django-free 的纯 StrEnum」,为了让 contracts.py、模型层和前端 codegen 都能低成本地导入同一份分类法,StrEnum 成员与字符串值相等,可直接用于 == 比较与 ORM 过滤。

模型侧的 choices 由 signal_source_product_choices() 这个可调用函数生成——从 models.pySignalSourceConfig 结构看,source_product 字段的 choices 直接绑定到该函数,因此新增一个标签项即自动成为模型合法值;源码注释也说明了这样做的目的:让新来源「成为单文件改动」,且 choices 的取值/顺序与 SIGNAL_SOURCE_PRODUCT_LABELS 保持一致,避免产生无意义的 AlterField 迁移噪音。

当前枚举已覆盖 Tier-1 客服/工单类(FRESHDESKFRESHSERVICEFRONTGORGIASKUSTOMERDIXAPLAIN)、issue 跟踪类(GITLABGITEASHORTCUT)、错误跟踪类(SENTRYROLLBARBUGSNAGHONEYBADGERRAYGUN)、Tier-2 安全扫描器(SNYKSONARQUBESEMGREPRAPID7_INSIGHTVM)、Tier-3 反馈/评论类以及 OAuth 客服源(INTERCOMHUBSPOT)等——可见注册表机制已在生产中被大规模复用。

2. SourceType(通常无需改动)

models.pySignalSourceConfig.SourceType 只有在记录类型不是已有的 ISSUE / TICKET 时才需要新增成员(例如 scanner 类来源的 SCANNER_FINDING、反馈类的 FEEDBACK/REVIEW)。源码注释特别强调:emission 注册表能发射的每个 source_type 都必须出现在这里,否则启用该来源时会 400

3. 迁移

执行 python manage.py makemigrations signals 生成 NNNN_alter_signalsourceconfig_source_product一次接入多个来源时,把所有新枚举值集中进同一个迁移——多个并行 PR 各自往该模型加迁移会在合并队列中冲突。

4. Emitter:每来源一个新模块

新建 products/signals/backend/emission/<source>_<table>.py,导出一份 SignalSourceTableConfig。其字段定义见 registry.py

  • source_product / source_type:必须与 SignalSourceConfig 的 choices 一致;
  • emitteremitter(row) -> SignalEmitterOutput(source_product, source_type, source_id, description, weight, extra) 的发射函数;
  • record_fetcher:取数函数,数据仓库源统一用 data_warehouse_record_fetcher
  • partition_field:增量游标列;partition_field_is_datetime_string:来源以字符串存时间时置 True;
  • fields:需要 SELECT 的列(只取 emitter 和 extra 元数据用到的);可选 where_clause
  • 可选的 LLM actionability_prompt(判定记录是否可行动,prompt 必须包含 {description} 占位符,由 pydantic 校验器强制)、summarization_prompt + description_summarization_threshold_chars(长描述摘要,二者必须同时设置);
  • 其他防御性字段:max_records(默认 1000)、first_sync_lookback_days(首次同步回看天数,默认 7)、unloggable_fields(日志脱敏列)。

最可靠的抄写模板是 github_issues.py,但必须先看目标表的真实列结构——阅读 products/warehouse_sources/backend/temporal/data_imports/sources/<name>/ 下的 settings.py / canonical_descriptions.py 确认精确列名。

⚠️ 并非所有来源都是扁平列。 GitHub/Linear 把 title/body/created_at 暴露为顶层列,通用 fetcher 的 SELECT {fields} FROM {table} WHERE {partition_field} > cursor 直接可用。但 Jira 的 issues 表只有 idkeyselffields(嵌套 JSON blob)、expand——summary/description/status 全部藏在 fields 里。对这类来源必须在 emitter 里 SELECT fields 并用 JSONExtractString(fields, '…') 提取,partition_field 也要写成 JSON 表达式。registry.pySignalSourceTableConfig 的注释明确指出,partition_field 会被直接插值进 HogQL 的 WHERE,因此 JSON 表达式形式是被支持的——但文档仍强调:这是「最容易微妙出错的一点」,克隆配置后必须实际跑一次同步来验证,而不能只靠读代码。

jira_issues.py 为实证:该来源对状态/优先级/指派人用 JSONExtractString(JSONExtractRaw(fields, 'status'), 'name') 之类的嵌套提取(见第 57–69 行 FIELDS),where_clause 过滤掉 Done/Closed/Resolved 终态工单;由于 created 被 source 提升为顶层列但其存储类型随同步而异,partition_field 写作 "toString(created)" 并配合 partition_field_is_datetime_string=True(源码注释建议首次启用时对照真实同步验证)。另外 Jira 描述是 Atlassian Document Format 富文本 JSON,emitter 里专门实现了 _adf_to_text() 做尽力而为的纯文本提取。

5. 注册:registry

registry.py_register_all_emitters() 中添加 import 并注册,当前仓库的实际 API 是:

register_signal_source(
    ExternalDataSourceType.<YOURSOURCE>,   # 来自 products/warehouse_sources/backend/facade/types.py
    "<table>",                             # 如 "issues" / "tickets" / "conversations"
    <CONFIG>,
)

注册键是 (source_type.value, table)。注意源码中一个特殊约定:GitHub 的 schema 行是带定界符的 owner/repo.issues,因此 _registry_key() 对 GITHUB 会先做 github_split_schema_name 拆出裸 endpoint 再查表(见 registry.py 第 114–119 行)。

6. 契约变体

contracts.py 中为该来源添加一个 Literal[SignalSourceProduct.X] 的 payload 变体——该文件里每个来源一个带字面量限定 source_product 的 pydantic 模型(Jira 的变体在第 209 行附近),用于约束信号 payload 的形状。

7. 通用管道零改动

通用 fetcher(data_warehouse.py)、gate(emission/gate.py)与 warehouse hook 接线都不需要任何修改。从 fetcher 源码可以看清增量语义:有 last_synced_at 时按 partition_expr > last_synced_at 连续同步;首次同步则回看 first_sync_lookback_days 天;若 partition_field_is_datetime_string 为真,分区表达式会被包进 parseDateTimeBestEffort(...)。查询通过 execute_hogql_query(..., query_type="EmitSignalsNewRecords", bypass_warehouse_access_control=True) 执行,属于内部数据导入信号通道(无用户上下文)。

验证

  • 迁移能干净地 apply,之后 python manage.py makemigrations --check 无输出;
  • ExternalDataSourceType 对应成员存在(registry 中从 products/warehouse_sources/backend/facade/types.py 导入,可对照注册表文件核对)。

三、Code 前端(posthog/code 仓库)改造清单

Product key 是小写的 source_product(如 "jira")。在 packages/ 下不区分大小写地 grep "zendesk"——每个与来源列表相关的命中点都要加上新 product。标准清单:

类型门槛(每个来源必改)

  1. packages/shared/src/inbox-types.ts — 在 SourceProduct 联合类型中加入 "jira"
  2. packages/api-client/src/posthog-client.ts — 加入 SignalSourceConfig.source_product 联合类型;只有当记录类型不是已有的 issue/ticket 时才新增 source_type 值。

UI 实路径(每个来源必改)

  1. packages/ui/src/features/inbox/hooks/useSignalSourceToggles.tsSetupSourceProductSOURCE_TYPE_MAPSOURCE_LABELSDATA_WAREHOUSE_SOURCES{ dwSourceType, requiredTable })、ALL_SOURCE_PRODUCTScomputeValues 初始化对象;
  2. packages/ui/src/features/inbox/components/SignalSourceToggles.tsxSignalSourceValues 字段、toggleX/setupX 回调,以及「External connections」列中的一张 <SignalSourceToggleCard>(icon/label/description + requiresSetup/onSetup/loading/syncStatus);
  3. packages/ui/src/features/inbox/components/DataSourceSetup.tsxDataSourceTypeREQUIRED_SCHEMAS 和 switch 分支。凭据类来源直接把分支路由到 <DynamicSourceSetup sourceType="Jira" title="Connect Jira" schemas={schemasPayload("jira")} … />——不新建任何表单组件。只有 OAuth/资源选择类来源才需要定制 XSetup
  4. packages/ui/src/features/inbox/components/utils/source-product-icons.tsxSOURCE_PRODUCT_META 条目(Iconcolorlabel);没有合适的 Phosphor 图标时内联一个 SVG 图标文件(可抄 PgAnalyzeIcon.tsx);
  5. packages/ui/src/features/inbox/filterOptions.tsxINBOX_SOURCE_OPTIONS 条目(来源过滤下拉)。

Core 镜像层(保持同步)

  1. packages/core/src/inbox/signalSourceService.ts — 镜像 SOURCE_TYPE_MAPDATA_WAREHOUSE_SOURCESALL_SOURCE_PRODUCTScomputeSourceValues 初始化,外加 WarehouseSourceProduct/SignalSourceValues
  2. packages/core/src/inbox/dataSourceService.tsDataSourceTypeREQUIRED_SCHEMAScreateXDataSource 方法。

这两处今天不在 UI 实路径上,但 SignalSourceService/DataSourceService 镜像着相同映射,要保持一致。

OAuth 管线——已不再需要每来源定制

现在存在通用的、按 kind 参数化的集成流程:IntegrationServicepackages/core/src/integrations/integration.ts)+ integration tRPC 路由(packages/host-router/src/routers/integration.router.ts)。DynamicSourceSetup 通过字段的 kind 走这条通用路由启动任意 OAuth 流程。所以新的 OAuth 来源不需要新的 per-kind 服务、symbol 或路由——不要按来源去克隆 linear.ts / linear-integration.router.ts(旧的 linear/slack/github 路由仍服务于其他调用方,保留即可)。

验证(Code 仓库)

  • 动了 inbox-types.ts 之后跑 pnpm --filter @posthog/shared build(它是发布型类型包);
  • pnpm typecheck(联合类型被跨包消费,需全仓类型检查);
  • biome lint packages/core packages/ui — 零 noRestrictedImports,导入有序。

四、设置表单:用动态渲染器,不要硬编码

不要手写每来源的设置表单。 PostHog Cloud 通过 HTTP 下发每个来源连接表单的字段 schema,应用用 DynamicSourceSetuppackages/ui/src/features/inbox/components/DynamicSourceSetup.tsx)通用渲染。新的凭据类来源零表单代码——只需把 DataSourceSetup 的 switch 分支路由到 DynamicSourceSetup,传入大写 sourceType 和要同步的 schemas

  • 端点:GET /api/environments/{projectId}/external_data_sources/wizard/?source_type=<Type>Record<string, SourceConfig>。客户端方法 PostHogAPIClient.getExternalDataSourceConfigs,hook 为 useSourceConfig(sourceType)
  • SourceConfig.fields 是联合类型:input(text/email/password/url/number…)、selectswitch-groupoauthoauth-account-selectssh-tunnelfile-uploadDynamicSourceSetup 对 input/select/switch-group 以及 oauth/oauth-account-select 通用渲染,并按字段的 name 构建 createExternalDataSource payload。后端是字段名/标签/必填/secret 的唯一事实来源,表单永不漂移。
  • 字段 name 就是 payload 键——你不再手工维护它们(Jira → subdomainemailapi_token,除 token 外全部 secret:false)。

OAuth 来源同样无需定制表单。 DynamicSourceSetup 通用渲染 oauth 字段(一个「连接」按钮:按 kind 启动流程,轮询 getIntegrationsForProject 等待新集成出现,并把其 id 写入 payload[<name>])与 oauth-account-select 字段(对集成资源做服务端搜索的选择器,覆盖 OAuth 之后才需选资源的场景,如 GitHub 的 repo picker)。因此 OAuth 来源(Intercom、HubSpot、Salesforce…)与凭据来源一样只是一次注册表条目改动。支持的 OAuth kind 列表见 posthog/models/integration/oauth.pyOauthIntegration.supported_kinds(含 slack, salesforce, hubspot, google-ads, …, intercom, linear, clickup, jira, …, stripe,另加经 App install 的 github)。不在该列表中的来源必须走 API-key 表单——其仓库连接器直接吃凭据,与 OAuth 集成列表无关(注意:Jira 的仓库源用的也是 API token 而非 OAuth kind=jira)。

只有两种字段类型仍缺通用渲染器(此时禁用提交):ssh-tunnelfile-upload,需路由到定制表单。旧的 GitHubSetup/ZendeskSetup/PgAnalyzeSetup 硬编码表单仅为历史原因保留——可以留,也可以顺手迁到 DynamicSourceSetup,但不要再新增。

full_refresh 细节:issues 类来源(github/linear/jira)会在 useSignalSourceToggles.tsensureRequiredTableSyncing 中把 issues 强制为 full_refresh——因为 issue 会被编辑/关闭,增量 append 会漏掉更新。若你的新来源也同步 issues 表,需把它加入该条件;ticket/conversation 类来源只需强制 should_sync=true

五、来源目录(Tier-1 参考表)

source_type 是传给 createExternalDataSource 的大写 DWH 类型串。实现时应对照 external_data_sources serializer / 该来源的 source.py 精确核对 source_typepayload 键名:

Product source_type Table Auth 设置模板 payload
Jira Jira issues API token DynamicSourceSetup subdomain, email, api_token
GitLab GitLab issues API token DynamicSourceSetup gitlab_host, personal_access_token, project
Sentry Sentry issues API token DynamicSourceSetup auth_token, organization_slug, api_base_url?
Freshdesk Freshdesk tickets API key DynamicSourceSetup subdomain, api_key
Front Front conversations API token DynamicSourceSetup api_token
Gorgias Gorgias tickets API key DynamicSourceSetup gorgias_domain, email, api_key
Intercom Intercom conversations OAuth (kind=intercom) DynamicSourceSetup intercom_integration_id

(Zendesk tickets、GitHub issues、Linear issues、pganalyze issues+servers、Jira issues 已经上线——照抄即可,不要重复添加。Jira 行作为 payload 键与 JSON-blob emitter 陷阱的规范范例保留。)

对照当前仓库的 registry.py,上表中的 GitLab issues、Sentry issues、Freshdesk tickets、Front conversations、Gorgias tickets、Intercom conversations 均已注册,说明该目录表与生产注册表一致。

六、self-driving 向导表面(PostHog/context-mill,可选)

前两个表面让来源在应用里工作,但不会让 CLI npx @posthog/wizard self-driving 的 onboarding 主动推荐它。该流程是由 PostHog/context-mill 仓库里一个技能驱动的 AI agent,其 connected-tool 列表是硬编码的——不动态读取向导端点。因此在把它加进去之前,新来源对 self-driving onboarding 不可见。

需要编辑 context/skills/self-driving/references/5-connected-tools.md(以及 4-sources.md2-read-context.mddescription.md 中涉及来源列表的段落):

  1. 把工具加入 step-5 的 wizard_ask 多选选项({ label, value });
  2. 添加 responder 映射——与 posthog emitter 使用同一 source_product / source_type 对(如 Jira → jira / issue);
  3. 把其 DWH source_type 加入「已连接」检测列表;
  4. 按运行方式分类:
    • 凭据类来源(API key/token —— Jira、Zendesk、pganalyze):运行中无法收集凭据,所以永不把用户送去做 UI 流程。归入 Zendesk 组——arm 休眠的 responder 并记录 follow-up。无 connector 文件。
    • 一键 OAuth 来源(Linear 风格,kind=<source> 集成):新增一个专用 connector 引用(5X-<source>.md,克隆 5b-linear.md),移交授权链接并从集成 id 创建来源。

dist/ 是 gitignored 的(CI 里构建),但建议运行一次 node scripts/build.js(Node ≥20)确认技能能打包——构建会校验结构。

七、created_via 归因

ExternalDataSource.created_via 记录来源以何种方式创建,并且由服务端从请求传输层派生event_usage.py 中的 get_event_source,按 user-agent 判定)——API 调用方不能直接设置它,这是刻意为之(否则任何客户端都能自我贴标签)。新来源自动继承该机制,无每来源接线。机器值 mcp 会按调用方升级:

调用方 传输层 created_via
PostHog Desktop app inbox(posthog/code) EventSource.POSTHOG_CODE self_driving
npx @posthog/wizard(self-driving 程序) EventSource.WIZARD self_driving
其他 MCP 客户端 EventSource.MCP mcp

升级逻辑位于 external_data_source.py_create_external_data_source 中:源码显示 created_via 在创建时写入后不可再变更(PATCH 时被 pop 掉),且当机器注入的值为 mcp 时会用 transport_created_via 映射按 get_event_source(request) 覆写;WIZARD 传输经 is_wizard_self_driving_program(request) 判定后进一步升级为 self_driving

八、Worktree、PR 规范与陷阱

Worktree 设置(后端工作)

# 从本地 posthog/posthog 克隆出发
git fetch origin
git worktree add ../worktrees/inbox-<source> -b <your-branch-prefix>/inbox-<source>-source origin/master
  • 若 agent 沙箱未配置 GPG 签名密钥,对该次提交禁用签名(git -c commit.gpgsign=false commit)以免因缺 key 失败。
  • 不要反射性地加 --no-verify:pre-commit/pre-push 钩子会跑 lint、type-check 与 ci:preflight,跳过它们会让破坏直达 CI。仅在某个钩子在 worktree 里确实无法运行时绕过它,且先手工跑完上文 Verify 步骤。
  • PR 以 ready for review(非 draft)状态提交;语义化提交前缀用 feat(data-warehouse):(凡触碰 warehouse sources / signals)。
  • 合并后清理 worktree:git worktree remove ../worktrees/inbox-<source>

PR 规范

  • 每个来源、每个仓库各一个 PR。标题:feat(data-warehouse): add <Source> as a self-driving inbox source
  • PR 正文基于仓库 PR 模板;后端 PR 先行(或共享枚举迁移先行),Code PR 引用后端 PR 并声明部署顺序依赖。
  • 迁移卫生:若开多个后端 PR,把所有新 SignalSourceProduct 值放进一个 prep PR 的迁移里,各来源 emitter PR 不携带迁移——否则冲突。
  • merging-prs 技能经 Trunk 队列合入各 PR。

陷阱清单

  • source_type(DWH,大写,如 "Jira")≠ source_product(小写,如 "jira")≠ SourceType 记录种类("issue")。三个不同的字段。
  • DATA_WAREHOUSE_SOURCES 通过 source_type.toLowerCase() 匹配已存在的外部来源——所以 dwSourceType 必须等于真实的 DWH 类型串。
  • 精确的 payload 键才是真正的风险点,必须与该 source_type 的 posthog serializer 匹配。上线前对照 source.py 确认。
  • 每个 product 都要覆盖 SOURCE_PRODUCT_METAINBOX_SOURCE_OPTIONS(inbox 的 CLAUDE.md 特别点名),否则 findings 渲染时缺图标/过滤项。
  • 不要与 packages/core/src/onboarding/githubConnectService.ts 混淆——那是仓库访问(clone/branch/PR),与 warehouse source 无关。

小结

PostHog Self-driving Inbox 的扩展模型是「通用管道 + 静态注册」:fetcher、gate、契约校验、动态表单渲染器全部复用,每新增一个来源只写一份 SignalSourceTableConfig 及其 emitter,再按三份清单(后端七步、前端九处、可选向导)完成接线。掌握 (source_type, table) 注册键、source_type/source_product/SourceType 三字段区分、以及 Jira 式 JSON-blob 表的 JSONExtractString 取数与 partition_field 表达式这三点,就能把任意新的 Tier-1 来源安全地接入 inbox 信号体系。

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