HyperDX 代码规范实战指南:grep-first 复用、Mantine 语义变体与前端工程最佳实践
【免费下载链接】hyperdxResolve production issues, fast. An open source observability platform unifying session replays, logs, metrics, traces and errors powered by ClickHouse and OpenTelemetry.项目地址: https://gitcode.com/gh_mirrors/hy/hyperdx
本文以仓库 agent_docs/code_style.md 为骨架,结合
packages/下的真实源码与 CI 脚本展开。它面向在 HyperDX 仓库中新增类型、Zod schema、辅助函数或 React 组件的开发者(含 AI 编码 Agent),系统讲解"先搜索后实现"的复用纪律、common-utils共享代码地图、Mantine 组件变体约束、语义设计 token、文案规范与重构原则。读完你可以做到:在动手写代码前用几分钟定位到仓库里已有的实现,写出与 HyperDX / ClickStack 双品牌主题完全一致的 UI,并且不踩中已被明令禁止的代码模式。
文档定位:一份给编码 Agent 的"渐进式披露"规范
agent_docs/目录采用渐进式披露设计:详细的领域知识不塞进每个会话都会加载的AGENTS.md,而是按任务按需读取。agent_docs/README.md 明确说明,code_style.md的适用时机是"在任何包中新增类型、Zod schema、辅助函数或组件之前(grep-first 规则 + common-utils 内容地图),以及任何packages/appUI 改动之前"。
开篇有一条重要提示:Pre-commit hooks 会自动处理格式化,因此本文档聚焦于实现模式而非格式细节——代码风格约束的核心是"模式正确",而不是"缩进正确"。
TypeScript:消灭any,拥抱类型推导
HyperDX 的 TypeScript 基调是用类型系统表达契约:
- 避免
any,使用恰当的类型; - 避免
as强制断言,尽可能使用satisfies或类型推导; - 运行时校验统一走Zod schemas;
- 为数据结构定义清晰的 interface;
- 实现正确的错误边界(error boundaries);
- 定义并导入可复用的具名类型,而不是到处重复冗长的类型标注。
"命名类型复用"在仓库中有直接的落地证据。packages/app/src/types.ts通过别名重导出common-utils中的类型,避免在 app 包内重复定义:
// packages/app/src/types.ts import { NumberFormat as _NumberFormat } from '@hyperdx/common-utils/dist/types'; export type NumberFormat = _NumberFormat;这种"别名垫片(alias-shim)"模式会在下文"找到已有实现之后怎么办"一节中详细展开。
代码组织:单一职责、行数上限与强制 DRY
组织规范共四条:
- 单一职责:每个组件/函数只做一件事;
- 文件大小:单文件上限300 行,接近上限就要重构;
- DRY(必选):在新增类型、schema、辅助函数或组件之前,先 grep 是否已有实现——即下一节要讲的 grep-first 规则;
- 上下文学习:实现前先阅读仓库中相似的文件,对齐既有写法。
新增类型 / Schema / 辅助函数之前:先 grep(REQUIRED)
这是本文档唯一的强制流程,值得单独成节。规则原文:在你定义新的类型、Zod schema、辅助函数或 React 组件之前,先搜索是否已有实现。关键技巧是:
搜索的是操作(把列表切分、按时间戳分桶、校验 URL、格式化时长),而不是你正准备给它起的名字。名字各不相同,但操作是相同的。
官方给出的搜索命令(约 20ms 内完成):
# 用两三个词描述操作,作为备选关键词 grep -rniE "export (const|function|type|interface|enum) [a-zA-Z]*(bucket|interval|granular)" \ packages/common-utils/src packages/app/src packages/api/src \ --include='*.ts' --include='*.tsx'搜索顺序:先搜你正在编辑的文件 → 再搜packages/common-utils/src/→ 最后搜你所在包的其余部分。
一个关键的架构事实:common-utils没有根 barrel(root barrel)——没有src/index.ts,package.json也不声明入口点,因此所有导入都是形如@hyperdx/common-utils/dist/...的深路径(deep import)。packages/common-utils/src/types.ts仅这一个文件就有约 2850 行、280+ 个导出(实测当前仓库为 3150 行、314 个 export 行,还在持续增长)。你无法通过读 index 发现已有什么,grep 才是索引。
共享代码已经住在哪里:common-utils 内容地图
仓库中跨包的导入约 90% 集中在九个模块(下表中.../dist均指@hyperdx/common-utils/dist):
| 导入路径 | 承载内容 |
|---|---|
.../dist/types | 所有共享 Zod schema 及其推断类型:sources、alerts、dashboards、tiles、webhooks、chart configs,以及SQLInterval、MetricsDataType、StacktraceFrame等 |
.../dist/core/utils | 时间分桶与粒度计算(toStartOfInterval、timeBucketByGranularity、convertGranularityToSeconds)、字符串拆分(splitAndTrimCSV、splitAndTrimWithBracket、escapeSqlString)、hashCode、parseJSON、formatDate |
.../dist/core/metadata | ClickHouse 表/列内省——Metadata、getMetadata、parseKeyPath、unquoteIdentifier |
.../dist/clickhouse(/node、/browser) | 查询客户端、ChSql/chSql/concatChSql、ClickHouse ↔ JS 类型转换 |
.../dist/core/renderChartConfig | ChartConfig→ SQL |
.../dist/filters | 过滤器状态、序列化与校验(FilterState、filtersToQuery、parseQuery) |
.../dist/guards | Chart-config 与 source 类型守卫(isBuilderChartConfig、isRawSqlChartConfig、isHeatmapCompatibleSource) |
.../dist/validation | isValidUrl、isValidSlackUrl、密码校验器 |
.../dist/variables | Dashboard 变量解析与模板展开 |
时间分桶函数在源码中有完整实现,例如 packages/common-utils/src/core/utils.ts#L599 的convertGranularityToSeconds将SQLInterval粒度换算为秒,#L668 的timeBucketByGranularity在此基础上按起点做分桶迭代——这正是"先 grep 到它、别自己重写"的典型场景。
第二顺位的包内位置:packages/app/src/utils.ts、packages/app/src/types.ts、packages/app/src/ChartUtils.tsx、packages/api/src/utils/。
找到已有实现之后怎么办
文档给出了一张"决策表",共五种情形:
| 情形 | 做法 |
|---|---|
| 存在规范版本且适用 | 直接导入,删掉你正在写的那份拷贝 |
| 存在,但想用本地名 | 别名垫片——如 packages/app/src/types.ts#L9 的export type NumberFormat = _NumberFormat;,绝不重新输入一遍定义 |
| 存在,但形状"几乎合适" | 从它派生(Pick、Omit、.extend()、Alert & { … }),或参数化规范版本,保持单一事实来源 |
| 两个组件间有共享逻辑 | 提炼成兄弟模块——如 packages/app/src/components/sourceSelectUtils.tsx#L51 的useFilteredSortedSourceItems,被SourceSelect和SourceMultiSelect共同使用 |
| 两个版本必须并存 | 用 doc-comment 说明"为什么",并点名孪生版本。典型例子见 packages/common-utils/src/types.ts#L571-L582:Mongoose 的IWebhook(定义于 packages/api/src/models/webhook.ts#L17)与 JSON 序列化用的WebhookSchema成对存在 |
不可接受的做法:出现第二个定义,却没有别名、没有解释孪生关系的 doc-comment、也没有把两者钉在一起的测试。
整文件跨包移植:@source标记
当整个文件必须从其他包复制过来(packages/cli就为"Web Frontend Alignment"清单中的组件做了这件事,见 packages/cli/AGENTS.md),必须在文件头部用@source标签标明来源,一个来源文件一个标签:
/** * Source helper functions. * * @source packages/app/src/source.ts */CI 脚本 scripts/ci/ratchet.mjs 会统计这些标签,让总量在 CI 中保持可见。但注意文档的明确态度:该统计是建议性的,不是门禁——标注移植永远不会让构建失败,而删除标签来"变绿"只是隐藏了拷贝而非移除拷贝。想降低计数,正确做法是把共享代码迁入common-utils,然后运行yarn ratchet:update锁定基线。
React 模式:函数组件 + Hooks + 自定义 Hook
- 使用函数组件 + hooks,不用 class 组件;
- 写小而聚焦的组件;
- 把可复用逻辑提取为自定义 hooks;
- 为 props 定义 TypeScript interface;
- 列表使用正确的 key,昂贵计算使用 memoization。
Mantine UI:自定义变体是唯一正解
项目以Mantine UI为组件层,但通过自定义变体(variants)在品牌主题中统一样式。注意:文档原文写的packages/app/src/theme/mantineTheme.ts在仓库中实际位于品牌子目录——packages/app/src/theme/themes/hyperdx/mantineTheme.ts 与 packages/app/src/theme/themes/clickstack/mantineTheme.ts,两个品牌主题各自构建MantineThemeOverride。
两条总原则:
- 优先用 Mantine 组件,而不是自写样式的元素;
- 优先用 Mantine 单个 style props(如
m='xs'),而不是原始 styles(如style={{ margin: '4px' }})。
Button 与 ActionIcon 变体(REQUIRED)
Button 和 ActionIcon 只能用下表这五种变体,这是硬性约束:
| 变体 | 用途 | 示例 |
|---|---|---|
variant="primary" | 主操作(Submit、Save、Create、Run) | <Button variant="primary">Save</Button> |
variant="secondary" | 次操作(Cancel、Clear、辅助动作) | <Button variant="secondary">Cancel</Button> |
variant="danger" | 破坏性操作(Delete、Remove、Rotate API Key) | <Button variant="danger">Delete</Button> |
variant="link" | 无背景无边框的链接式操作(View Details、导航式 CTA) | <Button variant="link">View Details</Button> |
variant="subtle" | 透明背景 + hover 高亮;工具栏/工具控件(折叠开关、关闭按钮) | <Button variant="subtle">Filter</Button> |
正确用法:
<Button variant="primary">Save</Button> <Button variant="secondary">Cancel</Button> <Button variant="danger">Delete</Button> <Button variant="subtle">Filter</Button> <Button variant="link">View Details</Button> <ActionIcon variant="primary">...</ActionIcon> <ActionIcon variant="secondary">...</ActionIcon> <ActionIcon variant="danger">...</ActionIcon> <ActionIcon variant="link">...</ActionIcon> <ActionIcon variant="subtle">...</ActionIcon>禁止模式(light/outline/filled/default+ 调色板颜色均不可用于 Button/ActionIcon):
<Button variant="light" color="green">Save</Button> <Button variant="outline" color="red">Delete</Button> <Button variant="filled" color="gray">Cancel</Button> <Button variant="default">Cancel</Button> <ActionIcon variant="light" color="red">...</ActionIcon> <ActionIcon variant="filled" color="gray">...</ActionIcon>两个变体的行为细节(在主题源码中均有对应实现):
- link:无背景、无边框、文字用 muted 色(
--color-text-secondary),hover 时文字提亮到全对比度,适合混入周围内容的链接式 CTA(如 "View Details"、"View Full Trace")——见 mantineTheme.ts#L323-L329 的Buttonvars 实现。 - subtle:透明背景 + 标准文字色,hover 出现
--color-bg-hover背景高亮。它是ActionIcon 的默认变体(defaultProps: { variant: 'subtle' }),适合工具栏图标、折叠开关、关闭按钮。与 link 的区别是:subtle 显示 hover 背景,link 改变文字颜色。
注意:variant="filled"对表单输入(Select、TextInput 等)仍然有效,只是不能用于 Button/ActionIcon。
纯图标按钮必须用 ActionIcon
如果 Button 里只有图标、没有文字,就必须换成 ActionIcon:
// ❌ 错误 —— Button 只装了一个图标 <Button variant="secondary" px="xs"> <IconRefresh size={18} /> </Button> // ✅ 正确 —— 纯图标按钮用 ActionIcon <ActionIcon variant="secondary" size="input-sm"> <IconRefresh size={18} /> </ActionIcon>文档特别说明:这条规则无法被 ESLint 强制执行,必须靠人工代码审查。
语义组件变体:Alert / Text / 危险控件
项目为Alert、Text、Button、ActionIcon提供主题化的语义变体,让提示框和状态文字由设计 token 驱动,在 HyperDX 与 ClickStack 两个品牌以及明/暗模式下保持一致。优先使用这些变体,而不是裸的 Mantine 调色板颜色(color="yellow"、color="red"、c="green"等)。
变体 → token 的映射集中定义在 packages/app/src/theme/themes/semanticVariants.ts(两个品牌主题共用的唯一事实来源):SEMANTIC_TEXT_COLORS、SEMANTIC_CONTROL_COLORS、SEMANTIC_ALERT_VARS分别驱动 Text、Button/ActionIcon 与 Alert。
Alert—— 支持info|success|warning|danger,渲染带色调的-subtle背景,标题、图标以及正文都使用语义色 token(Mantine 默认会把 message 强制成黑/白,主题通过styles覆盖让正文跟随--alert-color):
// ✅ token 驱动,两个品牌 + 明暗模式都正确,满足 WCAG AA <Alert variant="warning" title="Heads up">This may take a while.</Alert> <Alert variant="danger" title="Failed">Could not save the alert.</Alert> // ❌ 硬编码 Mantine 调色板 —— 不感知主题、对比度不一致 <Alert color="yellow" title="Heads up">...</Alert> <Alert color="red" title="Failed">...</Alert>Text—— 支持danger|warning|success,用于行内状态/校验文字:
<Text variant="danger">Required field</Text> <Text variant="success">Connection verified</Text> // ❌ 语义状态文字不要用裸调色板 <Text c="red.5">Required field</Text>Button/ActionIcon的variant="danger"是"软"控件:色调化的--color-bg-danger-subtle背景(带 hover)加语义前景色,不是实心红填充。warning/success刻意不开放为控件变体,只用于Text和Alert。
兼容性说明:已有的<Alert color="...">调用点不会被改动,语义变体是 opt-in 的;但新增的提示框应优先用变体,动到附近代码时顺手迁移color="..."的旧写法。
确认对话框:一律使用useConfirm(REQUIRED)
任何"你确定吗?"步骤都必须用useConfirm(@/useConfirm),禁止手搓带 Cancel/Confirm 按钮的<Modal>。Provider 已在 packages/app/pages/_app.tsx 全应用挂载,调用点零配置:
const confirm = useConfirm(); const handleDelete = async () => { if ( await confirm( <> Deleting {name} is <b>not reversible</b>. </>, 'Delete', { variant: 'danger' }, ) ) { await deleteThing.mutateAsync({ id }); } };要点:
- 消息是
ReactNode,可以携带强调与多句话; - 破坏性操作传
{ variant: 'danger' };确认按钮文案默认Confirm; - 它恰好 resolve 一次:源码 packages/app/src/useConfirm.tsx#L44-L63 用 Promise 封装,
onConfirm/onClose各自 resolve 后立即setState(null),所以关闭动画期间双击 Confirm 不会触发两次操作——手搓 Modal 必须自己防重; - 测试 id 是共享且已存在的:
confirm-modal、confirm-confirm-button、confirm-cancel-button。不要为每个流程发明新的 confirm/cancel 测试 id——E2E 页面对象依赖这些共享 id。
已知限制:它不向 Modal 传title,正文按size="sm" opacity={0.7}渲染,且 CSS opacity 作用于整个子树,嵌套的<Text>无法单独恢复全对比度。如果某个流程确实需要标题或全对比度正文,请扩展useConfirm(加可选 prop,作用于所有调用点),而不是另起一个一次性 Modal。
组件测试中要 mock 它——ConfirmProvider依赖next/router,jsdom 里不可用:
jest.mock('@/useConfirm', () => ({ useConfirm: jest.fn() }));对参数做断言(需要检查文案时把消息ReactNode渲染出来);真正的对话框行为交给 E2E 验证。
空状态:一律使用EmptyState组件(REQUIRED)
所有空/无数据状态都用EmptyState(@/components/EmptyState),禁止临时内联的空状态 div。组件实现在 packages/app/src/components/EmptyState.tsx。
| Prop | 类型 | 默认值 | 说明 |
|---|---|---|---|
icon | ReactNode | — | 主题圆形内的图标(不传则隐藏) |
title | string | — | 标题文字(headline 风格,不加句号) |
description | ReactNode | — | 标题下方的说明文字 |
children | ReactNode | — | 说明下方的操作(按钮、链接) |
variant | "default" \| "card" | "default" | "card"时包一层带边框的 Paper |
// ❌ 差 —— 随手内联的空状态 <div className="text-center my-4 fs-8">No data</div> <Text ta="center" c="dimmed">Nothing here</Text> // ✅ 好 —— 使用 EmptyState 组件 <EmptyState icon={<IconBell size={32} />} title="No alerts created yet" description="Create alerts from dashboard charts or saved searches." variant="card" />标题文案:title当作短 headline(类似 UI 里的Title),不要以句号结尾;完整句子放进description,使用正常标点(需要时以句号结尾)。与列表页保持平行措辞(如 dashboards 和 saved searches 使用 "No matching … yet" / "No … yet" 且不带句号)。
代码片段:用 Mantine<Code>与CopySnippet(REQUIRED)
禁止渲染裸<pre>、临时Paper+ 等宽字体、或未加样式的<code>。统一使用既有组件,与 Terraform export、onboarding 及 Storybook 指南保持一致:
| 种类 | 组件 | 使用时机 |
|---|---|---|
| 行内代码 | Mantine<Code> | 散文中的短 token(Session、列名、flag) |
| 围栏 / 多行 / 可复制 | CopySnippet(@/components/ClickStackOnboarding/CopySnippet) | 安装命令、HCL、JSX 示例等用户可能复制的块,内部是<Code block>+ Copy 按钮 |
// ✅ 行内 —— Mantine Code Create a source with <Code>Session</Code> type. // ✅ 围栏 / 可复制 —— CopySnippet(Code + Copy) <CopySnippet label="Import block" snippet={`import { clickstack_dashboard } from "clickhouse/clickstack"`} /> // 周围标题已说明内容时,label 可省略 <CopySnippet snippet={USAGE} /> // ❌ 差 —— 裸 pre / 手写 Paper 外壳 <pre>{snippet}</pre> <Paper bg="var(--color-bg-code)"><Text component="pre" ff="monospace">{snippet}</Text></Paper>CopySnippet的实现在 packages/app/src/components/ClickStackOnboarding/CopySnippet.tsx,它还支持accessKey属性——设置后片段会被脱敏,直到用户显式 Reveal(内部借助RevealSnippet)。SQL 查询预览(渲染高亮的 ClickHouse SQL)仍用SQLPreview/ChartSQLPreview——那些是编辑器,不是片段外壳。
图表卡片:用ChartCard而不是手搓边框
用ChartCard(@/components/charts/ChartCard)包图表卡片,实现见 packages/app/src/components/charts/ChartCard.tsx。它是带边框的表面,标题下方有与自定义 dashboard tile 相同的全宽分隔线,取代了旧的ChartBox;不要手搓带边框的<div>/<Paper>包图表。
ChartCard只渲染卡片外壳。头部分隔线只有在后代渲染了带title(或toolbarItems)的ChartContainer时才会出现——ChartCard提供ChartContainerCardHeaderProvider把头部切换到卡片模式——所以要在里面放一个渲染ChartContainer的图表(DBTimeChart、DBTableChart、DBHeatmapChart、DBListBarChart等)。自带标题的内容(如定制表格卡片)也应让标题走带title的ChartContainer而不是裸Text,从而获得同样的卡片头部(含分隔线与顶部内边距),而不是紧贴顶边框。tile 级控件(全屏、线/柱切换、kebab 菜单)属于 dashboard tile,刻意不属于ChartCard。
| Prop | 类型 | 说明 |
|---|---|---|
children | ReactNode | 图表,通常是DB*Chart(或带标题的ChartContainer) |
style | CSSProperties | 尺寸/溢出覆盖——传固定height,或用flex: 1; height: 100%填满 flex 行(paddingInline被钉住以保持分隔线对齐) |
data-testid | string | 测试钩子 |
// ✅ 好 —— 共享卡片外壳,与 dashboard tile 一致 <ChartCard style={{ height: 350 }}> <DBTimeChart title="Request Latency" config={config} /> </ChartCard> // ❌ 差 —— 手搓卡片,与 dashboard 观感脱节 <Box style={{ border: '1px solid var(--color-border)', borderRadius: 4 }}> <DBTimeChart title="Request Latency" config={config} /> </Box>务必给它一个高度:ChartCard是width: 100%并填满父容器,所以父容器(或style={{ height }})必须定义高度。并排等宽图表(如 RED 行)在Flex内使用style={{ flex: 1, minWidth: 0, minHeight: 0, height: '100%' }}。
UI 文案:一律 sentence case
所有用户可见文本使用句子式大小写(sentence case)——只大写首词以及专有名词/缩写,禁止 Title Case(每个实词都大写)。适用于用户读到的每个字符串:字段标签、按钮、tab/菜单项、标题、节标题、弹窗标题、占位符、tooltip、表格列头、空状态、toast/通知文案。
| Title Case(避免) | Sentence case(使用) |
|---|---|
Data Source | Data source |
Chart Name | Chart name |
Add Series | Add series |
Count of Events | Count of events |
Save Changes | Save changes |
Delete Dashboard | Delete dashboard |
专有名词、产品名、缩写保持原样——sentence case 只改变它们周围的普通词:HyperDX、ClickHouse、ClickStack、OpenTelemetry/OTel、Lucene、SQL、PromQL、MongoDB、Kubernetes、JSON、CSV、URL、ID、API、MCP、CPU、P95。例如:Search your events w/ Lucene、Edit SQL、Copy as cURL、View in ClickHouse。
只有静态 UI 外壳需要 sentence case;永远不要改写动态/用户数据(列名、tag 值、日志/trace 内容、用户输入的资源名)——按原样渲染。
语义设计 token:优先于裸 Mantine 颜色
UI 用 Mantine 组件搭建,但颜色和表面应遵循主题中的语义 CSS 自定义属性(--color-*等),而不是临时 Mantine 调色板值。token 定义在packages/app/src/theme/themes/**/_tokens.scss(hyperdx 与 clickstack 各有 hyperdx/_tokens.scss 和 clickstack/_tokens.scss,另有共享的_base-tokens.scss),对齐 Click UI 风格系统,让 HyperDX 与 ClickStack 视觉一致,是通往统一设计系统的路径。
- Do:布局、组件、间距用 Mantine;主题化背景、文字色、边框、状态用语义 token,如
style={{ color: 'var(--color-text-muted)' }}或style={{ border: '1px solid var(--color-border)' }}; - Do not:存在语义 token 时依赖裸 Mantine 颜色 props 处理应用外壳与内容——如
c="gray.5"、bg="dark.7"、任意color="blue.4"用于应该与产品其余部分一致的表层; - 参考:packages/app/src/theme/semanticColorsGrouped.ts(token 名清单)、
packages/app/src/theme/themes/下的主题 SCSS,以及 Storybook 的SemanticColors等主题故事。
packages/app/src/theme/**中的 Mantine 主题覆盖可能把 Mantine 的色阶映射到项目调色板(例如 mantineTheme.ts 里Button/ActionIcon/Alert的 vars 都指向--color-*token),但这不取代新样式中显式使用var(--color-...)。
图表与可视化颜色是另一套更具体的契约——它们有独立的 categorical、semantic、heatmap 调色板,通过packages/app/src/utils.ts的辅助函数接线。不要硬编码系列颜色,也不要用--color-text-*系列给图表上色。动手渲染任何数据之前,先读 agent_docs/data_viz_colors.md。
重构:直接编辑,不留 v2 副本
- 直接编辑文件——不要创建
component-v2.tsx之类的副本; - 检查受影响区域的重复代码;
- 改动后验证所有调用方与集成点;
- 重构是为了提升清晰度或降低复杂度,不是为了改而改。
文件命名与组织
- 遵循包内约定的清晰、描述性命名;
- 永久文件名中避免"temp"、"refactored"、"improved" 等字眼;
- 相关组件放进同一个目录。
小结:落地清单
写代码前快速过一遍这份清单:新增类型/schema/helper 前先按"操作" grep(顺序:当前文件 →common-utils→ 包内),命中就导入或派生而不是重写;Button/ActionIcon 只用五种变体、纯图标用ActionIcon;确认弹窗走useConfirm、空状态走EmptyState、代码块走<Code>/CopySnippet、图表卡片走ChartCard;新文案用 sentence case 并保留专有名词原样;颜色优先--color-*语义 token,图表颜色另看data_viz_colors.md。把这些纪律内化后,你的改动会在视觉、类型与复用三个维度上都与现有代码库融为一体。
【免费下载链接】hyperdxResolve production issues, fast. An open source observability platform unifying session replays, logs, metrics, traces and errors powered by ClickHouse and OpenTelemetry.项目地址: https://gitcode.com/gh_mirrors/hy/hyperdx
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考