Langfuse chart-view-prototype 深度解析:用「设计即构建」的方式打磨 v4 事件的「任意视图皆图表」体验
【免费下载链接】langfuse🪢 Open source AI engineering platform: LLM evals, observability, metrics, prompt management, playground, datasets. Integrates with OpenTelemetry, LangChain, OpenAI SDK, LiteLLM, and more. 🍊YC W23项目地址: https://gitcode.com/GitHub_Trending/la/langfuse
本指南以 chart-view-prototype 的 README 为骨架,系统拆解 Langfuse 前端团队如何通过一个「可丢弃的设计原型」(throwaway design prototype,阶段 EXP-CHART-PROTOTYPE / phase 0)预先打磨 v4 events/traces 视图的「table → chart」可视化体验。读完本文,你将理解原型的数据层如何用纯函数模拟未来服务端聚合接口、两种配置 UI 形态(内联条 vs 侧边面板)的设计取舍,以及原型如何为后续 tRPCevents.aggregate接真数据铺路。
一、原型的定位:为什么要在接真实数据前先做这个
按 README 的界定,web/src/features/chart-view-prototype/是一个状态明确的可丢弃设计原型:它只跑在 Storybook 里、只使用 mock 数据,当前应用的任何代码都还没有 import 它。它的存在意义是「design by building」——在把功能接入 v4 真实读路径之前,先用代码把「any view is a chart」的完整交互体验建出来,供团队(README 中提到的 Nikita)拍板方向。
这一阶段设计(phase 0)的产物不是可上线的功能,而是一份可运行、可点击、可对比的设计决议,这正是它与传统静态设计稿的本质区别。
从源码结构看(目录清单),原型由三部分组成:
web/src/features/chart-view-prototype/ ├── types.ts # 配置规格 ChartViewConfig + mock 事件行 PrototypeEvent ├── vocab.ts # 指标/维度/聚合/图表类型词汇表(重导出生产词汇) ├── lib/ │ ├── aggregate.ts # 纯函数数据层:aggregateEvents(events, config) → DataPoint[] │ ├── aggregate.clienttest.ts # 聚合器与时间分桶的单元测试 │ └── fixtures.ts # 确定性 mock 事件生成器 + 命名场景 ├── components/ │ ├── ChartViewPrototype.tsx # 根组件:持有 mode + config 状态,渲染两种 take │ └── MockEventsTable.tsx # 代表性的「表格」一侧 └── ChartViewPrototype.stories.tsx # 真正的交付物:Storybook stories二、核心体验设计:一次「table → chart」的就地可视化
原型要还原的核心体验是:在 v4 events 视图中,把表格翻转成图表,并在原位配置可视化,配置项是四元组:
- metric(指标):对什么做聚合,如 count、latency、totalCost、totalTokens;
- aggregation(聚合方式):sum / avg / min / max / p50 / p95 / p99 / count;
- breakdown(分组维度):按 model、name、level、type、environment 拆分,或不做拆分;
- chart-type(图表类型):折线、面积、柱状、横向排行、饼图、大数字。
同时提供Ask AI入口:用自然语言描述需求,由 AI 直接产出这份配置,从而改变图表配置方式。整个配置模型刻意设计成「扁平的、枚举密集的」结构——正如 chart-view 的 types.ts 注释所说,这样既便宜地放进 URL,又方便 LLM 生成(即 phase 2 的「Ask AI → chart」),当前只覆盖 happy path:1 个指标 × 1 个维度 × 1 种标准图表类型。
Take A 与 Take B:两种配置承载形态
README 明确要求同时交付两种 UX 形态做对比:
| Take | 形态 | 特征 |
|---|---|---|
| Take A — inline bar | 紧凑的、常驻的配置条,位于画布上方 | 密集、快速,像是表格的「活扩展」,改动配置零跳转 |
| Take B — side panel | 最大化画布 + 可折叠的侧边配置面板 | 更干净、更有引导性,给图表留出更大空间 |
两个 take 共用同一个 table↔chart 切换开关、同一个纯聚合器、同一个 mock 的「Ask AI → chart」入口,差异只在于配置 UI 的呈现方式(见 ChartViewPrototype.stories.tsx 的注释)。
从 ChartViewPrototype.tsx 的实现可以看到两种 take 的分流逻辑:mode === "table"时渲染 MockEventsTable;否则按affordance属性决定渲染内联条(InlineTake,原型内自建)还是侧边面板——后者直接复用生产组件 ChartViewPanel,且 README 的组件注释明确记录了团队最终选择的方向:Take B(侧边面板)被 Nikita 采纳为共享的生产形态,Take A 仅作为设计记录保留在原型中。
三、运行方式:在 Storybook 里体验这套设计
README 给出的运行方式非常简单,在仓库根目录执行:
pnpm --filter web run storybook启动后进入Charts / Chart View Prototype分组即可查看全部 stories。原型的故事集(ChartViewPrototype.stories.tsx)本身就是设计交付物,包含:
TakeA_InlineBar:旗舰场景,默认配置是「按模型拆分的、随时间变化的事件计数折线图」;TakeB_SidePanel:同样体验但配置收入可折叠面板;TableView:熟悉的 events 表格起始态,右上角翻转开关切到图表;AskAI:针对带 error-spike 的数据集,尝试「errors over time by level」这类自然语言问法;LatencyP95ByModel、CostByModelRanked、EventsByLevelPie:预设配置直接打开对应图表;EmptyState:空数据状态;Comparison:两种 take 上下堆叠、直接并排对比。
stories 的 args 支持affordance(inline/panel)和initialMode(table/chart)两个切换,方便在 Storybook 画布里快速对比。原型内部还通过initialConfig支持注入预设配置(见 stories 中的PRESETS)。
四、Owner map:每个文件各司其职
README 给出了清晰的职责划分表,这里完整继承并补充源码依据:
| 文件 | 职责 | 源码要点 |
|---|---|---|
| types.ts | 配置规格ChartViewConfig+ mock 事件行PrototypeEvent | ChartViewConfig从生产 chart-view/types.ts 重导出;PrototypeEvent是原型的 v4 事件行替身,字段名镜像 observations 视图声明(startTime、type、name、model、level、environment、latencyMs、totalCost、totalTokens) |
| vocab.ts | 指标/维度/聚合/图表类型词汇表(真实 widget 词汇表的忠实子集)+ 取值器 + 配置强制转换;纯逻辑、无 React | 第一行即export * from "@/src/features/chart-view/vocab",确保 harness 与真实视图共用同一份事实来源;另定义 mock 专用的METRIC_EXTRACTORS/DIMENSION_EXTRACTORS |
| lib/aggregate.ts | 数据层。纯函数aggregateEvents(events, config) → DataPoint[],镜像未来 v4 聚合端点的返回形状 | 内部按图表类型分派:NUMBER→ 单点聚合;时间序列 → 时间分桶 × 系列;分类 → 分组聚合后按指标降序排序;含floorToGranularity时间分桶与线性插值分位数 |
| lib/fixtures.ts | 确定性(种子化)mock 事件生成器 + 命名场景 | 基于 mulberry32 微型 PRNG,时间窗锚定固定时刻(2026-06-25T18:00:00Z),输出default/errorSpike/sparse/empty四个场景 |
| components/ChartViewPrototype.tsx | 根组件:持有mode+config状态,渲染两种 take 之一;其下全部为纯展示 | 状态用useState+coerceConfig归一化;patchConfig用useCallback保持稳定;data用useMemo派生 |
| components/ChartCanvas.tsx | React.memo图表渲染边界(派生数据并渲染chart-library) | 原型实际复用生产 chart-view/components/ChartCanvas.tsx |
| components/ConfigControls.tsx | 共享、纯展示的配置选择器(指标/聚合/分组/粒度/图表类型) | 原型直接复用生产 chart-view/components/ConfigControls.tsx 中的MetricSelect、AggregationSelect、BreakdownSelect、GranularitySelect、ChartTypePicker |
| components/MockEventsTable.tsx | 切换开关中代表「表格」的一侧 | 刻意做成轻量、纯展示的表格(非完整虚拟化DataTable),只显示前 14 行,让切换「诚实」而不重造轮子 |
| components/ViewModeToggle.tsx | table↔chart 切换开关 | 复用生产 chart-view/components/ViewModeToggle.tsx |
| ChartViewPrototype.stories.tsx | stories(实际交付物) | 见上文第三节 |
五、数据层原理:纯函数模拟未来聚合端点
原型最关键的抽象在 lib/aggregate.ts:aggregateEvents(events, config)是一个纯函数,输入 mock 事件数组 + 配置,输出chart-library的DataPoint[]。它的设计目标是镜像生产环境dashboard.executeQuery对 observations 视图的返回形状,这样 Storybook 里渲染的视图组件与真实EventsChartView完全一致,只差数据来源(见 aggregate.ts 注释)。
五.1 取值器(extractor):mock 世界的「列映射」
vocab.ts 中,METRIC_EXTRACTORS定义了每个指标如何从PrototypeEvent取出数值:
export const METRIC_EXTRACTORS: Record<MetricKey, ((e: PrototypeEvent) => number) | null> = { count: null, // null = 行数(count 度量) latency: (e) => e.latencyMs, totalCost: (e) => e.totalCost, totalTokens: (e) => e.totalTokens, }; export const DIMENSION_EXTRACTORS: Record<DimensionKey, ((e: PrototypeEvent) => string) | null> = { none: null, // null = 不做分组 model: (e) => e.model ?? "unknown", name: (e) => e.name, level: (e) => e.level, type: (e) => e.type, environment: (e) => e.environment, };生产环境从 ClickHouse 的 observations 视图读取这些字段;harness 则在客户端取值,null的语义分别是「行数度量」和「不分组」。
五.2 聚合分派:三种图表家族的三种算法
aggregateEvents按图表类型走三条路径(aggregate.ts):
NUMBER(大数字):忽略分组,全量聚合成单个DataPoint,time_dimension与dimension均为undefined;- 时间序列(折线/面积/柱状):先按
timeGranularity把startTime用floorToGranularity分桶(aggregate.ts,UTC 下向下取整到分钟/小时/天),再按系列二次分组,最后按时间桶排序输出,保证时间轴有序; - 分类(横向排行/饼图):按分组维度聚合后,按指标值降序排序(
b.metric - a.metric),得到「排名」语义。
底层aggregate函数(aggregate.ts)实现了全部聚合算子:count直接返回行数;sum/avg/min/max遍历取值并过滤非有限数;p50/p95/p99走线性插值分位数算法(percentile):排序后按rank = (p/100) * (n-1)在相邻两个值之间插值,与常见统计库的分位数口径一致。
五.3 测试验证:聚合器的行为契约
aggregate.clienttest.ts 用 Vitest 锁定了上述行为,关键断言包括:
- 空事件集返回空数组;
- count 指标按「时间桶 × 分组系列」计数(gpt-4o 在 10:00 桶计 2,claude-opus-4 在 11:00 计 1);
- 时间桶无论输入顺序如何都按时间序输出;
- 组内
avg计算正确(100/200/300 → 200); - 线性插值 p95 精确到
95.05(对 1..100 序列,rank = 0.95 × 99 = 94.05); - 分类结果按指标降序排名;
NUMBER图表类型产出单一聚合点;floorToGranularity对 minute/hour/day 的向下取整(如2026-06-25T10:45:12.345Z按 hour 取整为10:00:00.000Z)。
这套测试让「mock 数据层」本身也有行为契约,为 phase 1 替换成真实 tRPC 调用提供了等价的参照基准。
六、Mock 数据:种子化的确定性事件流
lib/fixtures.ts 的目标是确定性:stories 和聚合器测试跨运行完全稳定,不依赖Math.random和墙钟时间。实现手段是:
- mulberry32 微型 PRNG(fixtures.ts):32 位种子驱动的确定性伪随机数生成器;
- 固定时间窗锚点:
WINDOW_END = Date.parse("2026-06-25T18:00:00.000Z"); - 加权随机采样:
weightedPick让模型/环境出现频率符合真实分布。
生成器刻意模拟真实 trace 流的形态(fixtures.ts):
- 5 个模型规格(
gpt-4o、gpt-4o-mini、claude-opus-4、claude-haiku-4、gemini-2.5-pro),各带权重、基础延迟、延迟散布与每 token 成本; - 5 个典型操作名(
generate-answer、summarize、classify-intent、embed-docs、rerank-results); - 80% 为
GENERATION,其余为SPAN/EVENT;级别以DEFAULT为主、薄尾分布 WARNING/ERROR/DEBUG; - 成本随 token 数线性变化,延迟随模型变化,且非生成事件延迟和成本都显著更低;
- 可选
errorSpike开关:把 ERROR 集中到最近 4 小时窗口,让「errors over time」在图上呈现真实的凸起。
导出的四个命名场景(SCENARIOS)对应不同故事:default(640 条、24h)、errorSpike(种子 7,ERROR 聚集)、sparse(28 条、6h)、empty(空数组)。
七、架构原则:frontend-large-feature-architecture 的实践样本
README 明确标注原型遵循frontend-large-feature-architecture,三条原则在源码中逐一对得上:
纯派生(Pure derivation):所有数据变换集中在
lib/aggregate.ts;组件纯展示、只渲染派生数据。「同样的 events + config → 同样的图表」,可预测、可测试。这体现在 ChartViewPrototype.tsx 的useMemo(() => aggregateEvents(events, config), [events, config])。单向数据流(One-way data flow):根组件持有
mode+config,子组件只接收值 + 稳定的(memoized)onChange回调,除回调外没有任何向上回传的通道。patchConfig用useCallback包装且依赖为空数组(ChartViewPrototype.tsx),保证子组件引用稳定。渲染边界(Render boundaries):
ChartCanvas和选择器都做了React.memo,单次配置变更不会触发重新聚合或无关 UI 重渲染。MockEventsTable同样React.memo化(MockEventsTable.tsx)。当图表类型切换时,isTimeSeries计算结果用于禁用/启用粒度选择器,粒度选择只在时间序列图表下有意义。
配置的强制归一化(coercion)
vocab.ts 的coerceConfig是配置安全的最后防线:把配置的每个字段钳制到已知枚举成员(URL 参数不可信),并在指标改变时把聚合方式重置为该指标支持的默认聚合。视图组件和 URL 状态都经过它往返,因此两者都不可能产出非法查询。根组件初始化和每次patchConfig都调用它:
const [config, setConfig] = useState<ChartViewConfig>(() => coerceConfig({ ...DEFAULT_CONFIG, ...initialConfig }), ); const patchConfig = useCallback( (patch: Partial<ChartViewConfig>) => setConfig((prev) => coerceConfig({ ...prev, ...patch })), [], );默认配置(DEFAULT_CONFIG)是「count 指标 × count 聚合 × 按 model 分组 × 折线时间序列 × 小时粒度」,即旗舰 story 的打开状态。
八、与生产 feature 的共享:词汇表是唯一事实来源
原型并非从零造轮子。它重导出并复用生产 features/chart-view 的能力:
- 类型:chart-view/types.ts 定义了
ViewMode、MetricKey、DimensionKey、AggregationFn、TimeGranularity和扁平枚举密集的ChartViewConfig; - 词汇表:chart-view/vocab.ts 中,
METRICS的每个指标都携带真实查询标识measure(count/latency/totalCost/totalTokens)和单位(millisecond/USD),DIMENSIONS的每个维度都携带 observations 视图字段名(如providedModelName、level、environment),使后续构建聚合查询是「直接映射」;图表类型则从DashboardWidgetChartType(位于 packages/shared/src/db 的@langfuse/shared/src/db导入)中选取折线/面积/柱状时间序列、横向排行、饼图、大数字六种,刻意排除透视表(与表格侧重叠)和直方图(happy path 外); - 配置语义:
describeConfig把配置渲染成人类可读的句子,既是图表副标题,也是 Ask-AI 流程中「我为你生成了什么」的确认文案——例如Count of events by model over time(count 指标省略聚合前缀,NUMBER图表不声称分组,见 vocab.ts)。
原型之所以能做到「harness 与真实视图共用同一份事实来源」,核心在于 prototype/vocab.ts 的第一行export * from "@/src/features/chart-view/vocab"。
九、接入路径:后续阶段的既定计划
README 明确指出原型文件夹尚未包含生产接入代码,并给出清晰的后续路线:
- Phase 1:把 lib/aggregate.ts 的客户端聚合函数替换为tRPC
events.aggregate调用,返回同样的DataPoint[];把mode+config移入URL 状态(实现可逆的切换);挂载进features/events/components/EventsTable.tsx,并以 v4 读路径做特性门控(gated)。
后续的「Ask AI → chart」(phase 2 方向)则建立在配置模型的 LLM 友好性之上:扁平、枚举密集的ChartViewConfig让自然语言到配置的转换对 LLM 而言成本极低。
对应地,生产侧 EventsChartView.tsx 与 lib/buildChartQuery.ts、lib/chartConfigToWidget.ts 已经搭起了从配置到查询、到 widget 的桥,原型正是这套生产代码在接真实数据前的「设计验证床」。
十、小结:一次可运行的 UI 设计决议
chart-view-prototype 的价值在于把「表格视图内嵌可配置图表」这一体验从想法变成了可点击、可对比、可测试的 Storybook 原型:纯函数数据层(aggregateEvents)提前锁定了未来聚合端点的数据契约,种子化 fixtures 保证了演示与测试的确定性,而 Take A / Take B 两种配置形态则把交互方向的取舍摆到桌面上供决策。对于想理解 Langfuse 前端如何「design by building」、以及 v4 chart view 背后配置模型与数据流的开发者,这个原型文件夹连同其测试、stories 和生产 chart-view 模块,是一份完整可循的参考路径。
【免费下载链接】langfuse🪢 Open source AI engineering platform: LLM evals, observability, metrics, prompt management, playground, datasets. Integrates with OpenTelemetry, LangChain, OpenAI SDK, LiteLLM, and more. 🍊YC W23项目地址: https://gitcode.com/GitHub_Trending/la/langfuse
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考