在 Sentry 中开发 Seer Embed 组件:从 Zod Schema 到后端代码生成的完整实践指南
2026/9/11 9:33:25 网站建设 项目流程

在 Sentry 中开发 Seer Embed 组件:从 Zod Schema 到后端代码生成的完整实践指南

【免费下载链接】sentryDeveloper-first error tracking and performance monitoring项目地址: https://gitcode.com/GitHub_Trending/sen/sentry

Seer 是 Sentry 中由大模型驱动的智能代理(agent),它在回答用户问题时会产生 Markdown 输出。Seer Embed 是渲染在 Seer Markdown 输出中的富交互组件,采用 Markdoc 风格的标签语法({% name %}{ ... }{% /name %})嵌入文本流,每个 embed 由 Zod Schema、React 组件与注册表条目三部分组成。本文基于 .agents/skills/seer-embed/SKILL.md 展开,结合仓库内真实源码与测试,完整讲解从 schema 定义、组件编写、注册、代码生成到验证、特性开关(feature flag)门控的全流程,读完即可为 Seer 新增一个可被 LLM 生成、可被前端渲染的 embed 组件。

一、Seer Embed 的工作原理

在深入操作步骤前,先理解 embed 在整条链路中的位置。Seer 的 markdown 输出会先被前端做流式逐块重新词法分析与重新渲染,因此:

  • 标签语法:LLM 在输出中写{% myEmbed %}{"someField":"hello"}{% /myEmbed %},标签体内的内容是 JSON 格式的数据载荷;
  • 数据校验:前端用该 embed 对应的 Zod schema 对 JSON 载荷做解析,校验失败时丢弃该组件并告警(生产环境通过 Sentry 上报);
  • 注册表分发:渲染器从注册表按标签名查组件,命中则渲染,未命中则上报 "no renderer for tag" 后返回 null(渲染器实现);
  • 契约双端共享:同一个 Zod schema 通过代码生成脚本导出为 JSON Schema,写入后端文件,后端再将其注入发给 Seer agent 的系统提示词,让 LLM 知道何时该输出哪个标签、标签体该长什么样。

所以一个 embed 实际跨越三层:前端 schema(TS/Zod)→ 生成产物(JSON Schema)→ 后端过滤与注入。这也是为什么新增 embed 需要前端组件、注册表、代码生成三处配合。

二、开始前的准备

动手前先完成三项确认(对应 SKILL.md 的 "Before You Start"):

  1. 阅读 schemas.ts,了解现有 embed 的 schema 写法与命名习惯——当前仓库已内置 20+ 个 embed,覆盖timestampdocsdashboarddsnuserissue/issuesreplayreleasechartautofix/autofixRefalertmonitorsavedIssueViewsavedQuerytraceprofile、各类查询 embed(issuesQueryerrorsQueryspansQuerylogsQueryreplaysQuerymetricsQuery)以及结构化 embedagentWriteApproval
  2. 阅读 index.ts,查看已注册的 embed 列表;
  3. 确认要新增的 embed 名称不存在,避免与既有注册表条目冲突。

SEER_EMBED_SCHEMASSTRUCTURED_SEER_EMBED_SCHEMAS两个对象通过ALL_SEER_EMBED_SCHEMAS合并,SeerEmbedName类型由keyof typeof ALL_SEER_EMBED_SCHEMAS推导,因此 schema 中每新增一个 key,全链路的 TypeScript 类型都会随之收紧(schemas.ts)。

三、Step 1:定义 Zod Schema

在 schemas.ts 的SEER_EMBED_SCHEMAS对象中追加条目:

export const SEER_EMBED_SCHEMAS = { // ...existing entries myEmbed: { description: "One sentence describing what this embed does—this passes through directly to the LLM's system prompt.", level: ['inline'], // 'inline', 'block', or both schema: z.object({ // Define the data shape the LLM will produce someField: z.string(), optionalField: z.number().optional(), }), examples: [{label: 'Basic', data: {someField: 'hello'}}], // featureFlag: 'organizations:seer-explorer-my-embed', // optional }, } as const satisfies Record<string, SeerEmbedSchema>;

SeerEmbedSchema接口(schemas.ts)由五个字段组成:descriptionlevelschema、可选的examples与可选的featureFlag

关键决策点

  • description:写给 LLM 看。它会被原样注入 LLM 的系统提示词,LLM 依据它决定何时输出该 embed。描述要具体说明使用场景,并明确"唯一方式"类约束。例如timestamp的 description 明确要求"所有 datetime 值都必须用本 embed,绝不允许输出裸时间文本";issue则声明"引用 Sentry issue 的唯一方式,禁止出现在 Markdown 表格或列表中"。这类约束直接决定了 LLM 行为的正确性。
  • level:inline 与 block 两种形态
    • ['inline']:流式内嵌在文字中的小部件(时间戳、徽标、紧凑链接);
    • ['block']:独占一行的富卡片(图表、表格、数据预览);
    • 两者都列:embed 具备自适应的双形态,组件render会收到第二参数level来分支渲染。
    • 注意 examples 里可用level覆盖单个示例的形态,只有与 schema 默认(level数组首项)不同时才需要显式设置。
  • schema:用 Zod,保持扁平简单。LLM 必须产出合法 JSON,因此字段越简单、约束越明确,LLM 越不容易出错。仓库中的成熟实践包括:
    • .default()为可选字段提供合理默认值,例如format: z.enum(['absolute', 'relative']).default('absolute')mode: z.enum(['samples', 'aggregate']).default('samples')
    • .enum()约束字符串取值,例如chartvisualization: z.enum(['line', 'area', 'bar'])x_axis: z.enum(['time', 'category'])y_axis_unit: z.enum(['number', 'percentage', 'duration', 'bytes'])
    • .describe()给关键字段补充给 LLM 的说明,例如yAxes字段的'Aggregate functions to chart, e.g. "count()" or "p95(span.duration)"'
    • 需要跨字段约束时用superRefine,如chart中"category 轴仅支持 bar 图""time 轴数值必须是带偏移的 ISO 8601 时间戳"(schemas.ts);
    • 查询类 embed 通过对象展开复用公共字段,pageFilterFields(projects/environments/statsPeriod/start/end)被所有查询 embed 共享,exploreQueryFields进一步叠加 query/mode/groupBy/yAxes 等(schemas.ts)。statsPeriod用正则^\d+[smhdw]$约束相对时间写法,如"24h""7d"
    • 一个容易踩的坑:agents 经常输出裸数字作为 ID,所以idString保留为z.union([z.string(), z.number()])不要加.transform(),否则代码生成脚本无法导出 JSON Schema(schemas.ts)。该设计有测试覆盖:schemas.spec.ts断言spansQuery接受数字型 project ID,且导出的 JSON Schema 中projectsitemsanyOf: [string, number](schemas.spec.ts)。
  • examples:few-shot 示例。每个data必须能通过 schema 校验。它们会被放入发送给 LLM 的生成 JSON 中作为少样本示例;在 stories 页面中,同一 embed 的所有 examples 会被组合进一个 Markdown 块,通过单个<SeerMarkdown>渲染——inline 示例包裹在散文文本中,block 示例追加在末尾。用多个 examples 展示不同的属性组合或 block/inline 形态差异。
  • featureFlag:可选门控。设置后,后端在该 flag 关闭时会把这个 embed 从发给 LLM 的 schema 中过滤掉(详见第七节)。

四、Step 2:创建组件

在 embeds/components/ 下新建<name>.tsx

import {defineSeerEmbed} from 'sentry/components/seer/markdown/embeds/utils'; export const MyEmbed = defineSeerEmbed({ name: 'myEmbed', // must match the key in SEER_EMBED_SCHEMAS render({someField, optionalField}) { // Props are typed from the Zod schema — already validated return <span>{someField}</span>; }, });

defineSeerEmbed为你做了什么

查看 utils.tsx 的源码实现:

  1. 按名称查找 Zod schema:从ALL_SEER_EMBED_SCHEMAS[name]取到 schema;
  2. safeParse校验dataprop:解析失败返回null,绝不渲染脏数据;
  3. 开发环境警告 / 生产环境 Sentry 上报:由于 markdown 在每个流式 chunk 都会重解析重渲染,非法 props 理论上每 chunk 报一次——reportInvalidEmbedreportedInvalidEmbedsSet 按name:code@path去重,保证每个不同失败每页只上报一次;开发环境console.warn,生产环境以seer_embed.name为 tag、seer-embed-invalid-props为 fingerprint 上报captureException(utils.tsx);
  4. 设置displayNameEmbed.displayName = name,注册表以它为 key。

编写规则

  • name必须与SEER_EMBED_SCHEMAS中的 key 完全一致;
  • render第一参数是Zod 输出类型EmbedOutput<N>),即已经过解析与校验、带默认值的 props,天然获得完整类型推导;
  • 若 schema 的level同时含'inline''block'render收到第二参数level'inline' | 'block')用于分支;
  • 保持组件简单:优先复用现成 Sentry 组件(DateTimeTimeSinceLink等),而不是从零手写;
  • 组件拿不到任何上下文:它不知道自己在页面哪里出现,只拿到标签体内的数据。这也是 embeds/README.md 强调的约束来源——embed 的交互必须自包含:不要把 host 路由的location/navigate传入 widget,legend 选择、排序、列宽等 UI 状态要存在 embed 内部,widget 动作要么本地处理要么禁用。

常见组件形态参考

仓库中已注册的组件覆盖了几种典型形态,可作为参考实现:

  • 纯内联轻组件timestamp.tsxdocs.tsxuser.tsxdsn.tsx等单文件组件;
  • inline/block 双形态issuereplayreleasedashboardalertmonitor等;
  • 含 block 预览的查询类errorsQueryspansQuerylogsQueryissuesQueryreplaysQuerymetricsQuery会拉取数据并渲染图表/表格(如 errorsQuery 的 block 形态在聚合图上叠加最多前五行匹配记录);
  • 结构化 embedagentWriteApproval.tsx用于浏览器会话中请求 Sentry API 写权限授权,schema 中requiredScopes直接取用API_ACCESS_SCOPES常量枚举(schemas.ts)。

五、Step 2b:组件长成一个目录时如何拆分

链接型 embed 保持单文件即可。一旦 embed 要渲染 block 预览——需要拉数据、懒加载重型视图或按子类型分支——就把它升级为目录,让 reviewer 一次只读一个关注点。仓库中monitoralertdashboard均已采用该模式。以monitor为例(monitor/):

components/monitor/ monitor.tsx # defineSeerEmbed only: inline link vs lazily imported block monitorLink.tsx # the inline level monitorBlock.tsx # default export: fetch, card chrome, dispatch monitorTypes/ # one file per subtype, when the embed has subtypes cron.tsx uptime.tsx monitor.spec.tsx # colocated, not in resourceEmbeds.spec.tsx

入口<name>.tsx只负责按 level 挑选渲染哪个形态:

const LazyMonitorBlock = lazy(() => import('./monitorBlock')); export const Monitor = defineSeerEmbed({ name: 'monitor', render(props, level) { if (level === 'block') { return <LazyLoad LazyComponent={LazyMonitorBlock} {...props} />; } return <MonitorLink {...props} />; }, });

该模式的关键约定:

  • 目录没有index.tsx:入口文件按 embed 命名(monitor/monitor.tsx),并在 embeds/index.ts 中显式导入;
  • 入口只放defineSeerEmbed+ level 分发:block 需要的一切都放在lazy(() => import('./<name>Block'))后面、以default导出(lazy()的要求)。这样文本中内联提到资源时,不会把沉重的 block 拉进 bundle——dashboardmonitor都遵循此约定;
  • 子类型按变化轴建目录:当 block 按子类型分支(detector 类型、widget 类型)时,每个分支一个文件,放在以变化轴命名的兄弟目录(monitorTypes/,而不是容易读成 TypeScript 类型的types/),block 里用单个switch分发。新增子类型 = 新文件 + 一个 case,而不是在两个散落的长 switch 里改来改去;
  • 公共条件在 block 里推导一次、以 props 下传:不要在每个变体文件里重复推导,旧单文件里的两处 switch 难以同步正是该约定要解决的问题;
  • 测试就近放置:规格测试写成<name>.spec.tsx并与组件同目录,使用embeds/testUtils.tsx里共享的renderEmbed/hrefFor辅助函数。resourceEmbeds.spec.tsx只放链接级 embed——它被所有 embed 共享,block embed 往里加 case 会造成持续冲突。

六、Step 3:注册组件

在 embeds/index.ts 中导入组件并加入embeds数组:

import {MyEmbed} from './components/myEmbed'; import {Timestamp} from './components/timestamp'; import {SeerEmbedRegistry} from './registry'; const embeds = [Timestamp, MyEmbed]; for (const embed of embeds) { SeerEmbedRegistry.register(embed.displayName, embed); }

注册表本身是一个模块级Map<string, RegisteredEmbed>,提供register/get/list三个方法,key 即defineSeerEmbed设置的displayName(registry.tsx)。渲染端在 SeerMarkdown 中通过SeerEmbedRegistry.get(name)查组件:命中则渲染(block 形态包一层带间距的Container);未命中则调用reportUnhandledTag上报后返回 null,而不会像普通 Markdown 那样把未知标签当纯文本回显——未知标签的告警同样按名去重、每页只报一次(index.tsx)。

七、Step 4:重新生成后端 Schema

运行代码生成脚本,把前端 Zod schema 同步为后端发给 Seer agent 的 JSON Schema:

pnpm gen:embed-widgets

该脚本实现在 scripts/genEmbedWidgets.ts,核心逻辑是调用seerEmbedsToJsonSchemas()——遍历SEER_EMBED_SCHEMAS,把每个条目的descriptionlevelexamplesfeatureFlagz.toJSONSchema(def.schema)导出的body组装为 widget 定义(schemas.ts)——写入src/sentry/seer/agent/embed_widgets.generated.json,并用pnpm oxfmt格式化保证字节级稳定,让 CI 用git diff就能校验新鲜度。

必须提交这个生成文件,它被版本管理,不在 gitignore 中。生成样例(timestamp条目)可见 embed_widgets.generated.json:body是标准的 JSON Schema(含$schematypepropertiesrequired),LLM 靠它学会输出合法载荷。

后端侧,embed_widgets.py 在进程启动时加载该生成文件,get_embed_widgets(organization, actor)负责按 feature flag 过滤:entry 带featureFlag且组织未开启该 flag 时会被剔除,无 flag 的 widget 恒包含;过滤逻辑内部通过features.has(w["featureFlag"], organization, actor=actor)判定(embed_widgets.py)。

八、Step 5:验证

新增 embed 后按顺序验证:

  1. Lint:对新文件运行pnpm run lint:js
  2. 类型:运行pnpm run typecheck,确认 schema 类型在组件、注册、渲染链路上正确流转——render参数由 Zod 输出类型推导,schema 改动会即时暴露类型错误;
  3. 手动测试:在 Seer Explorer 中触发会使用该 embed 的响应;或直接用SeerMarkdown渲染原始标签做本地验证:
<SeerMarkdown raw={`{% myEmbed %}{"someField":"hello"}{% /myEmbed %}`} />

此外仓库还提供两层自动化保障可参考:

  • schema 契约测试:schemas.spec.ts 断言seerEmbedsToJsonSchemas()的输出(如 replay 的时间戳偏移要求在 agent 契约中可见、数字 project ID 在 JSON Schema 中以anyOf导出)——新增 embed 时可仿照补充此类断言;
  • 组件级测试:目录化 embed 使用共享的renderEmbed/hrefFor辅助函数写*.spec.tsx,例如 monitor.spec.tsx、alert.spec.tsx,并可通过stories下的 story 页面直观检查渲染效果。

九、可选:用 Feature Flag 门控新 embed

如果需要逐步灰度而非直接全量上线:

  1. 在 schema 条目上加featureFlag: 'organizations:seer-explorer-<name>'
  2. 在 src/sentry/features/temporary.py 注册该 flag——仓库中同族 flag 均以OrganizationFeature+FeatureHandlerStrategy.FLAGPOLE注册(如organizations:seer-explorerorganizations:seer-explorer-embedsorganizations:seer-agent-autofix等);
  3. 后端 embed_widgets.py 会自动通过features.has()过滤带 flag 的 widget,flag 关闭时该 embed 不会出现在发给 LLM 的 schema 中——前端组件可以保留注册,因为 LLM 根本不会生成对应标签。

注意区分:autofixautofixRef两个 embed 共用organizations:seer-agent-autofixflag,其余 embed 的 flag 命名遵循organizations:seer-explorer-<name>惯例。

十、文件清单速查

文件职责
static/app/components/seer/markdown/embeds/schemas.ts新增 Zod schema 条目
static/app/components/seer/markdown/embeds/components/创建defineSeerEmbed组件;渲染 block 时改用目录结构
static/app/components/seer/markdown/embeds/index.ts导入并注册
src/sentry/seer/agent/embed_widgets.generated.jsonpnpm gen:embed-widgets重新生成并提交
scripts/genEmbedWidgets.ts前端 → 后端的代码生成脚本
src/sentry/seer/agent/embed_widgets.py后端按 feature flag 过滤 widget 定义

结语

Seer Embed 的整套机制可以概括为一条"单点定义、双端生效"的契约链:在 schemas.ts 里用 Zod 定义一次数据形状,前端组件据此获得强类型 props 与运行时校验,后端通过代码生成拿到 JSON Schema 注入 LLM 提示词,feature flag 再为上线节奏提供灰度能力。新增 embed 时只需遵循本文的五个步骤(加 schema、写组件、注册、重生成、验证),必要时辅以目录化拆分与 flag 门控,即可让 Seer 的答案从"一段文字"升级为"一个可交互的富组件"。

【免费下载链接】sentryDeveloper-first error tracking and performance monitoring项目地址: https://gitcode.com/GitHub_Trending/sen/sentry

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询