4.2 tRPC 链路
学习目标
完成本节后,你将能够:
- 从 Browser UI 追到 tRPC procedure、service、repository 和数据层。
- 理解
protectedProjectProcedure为什么是 UI 内部 API 的控制面核心。 - 判断 router、middleware、service、repository 的职责是否放错位置。
4.2.1 先给结论
tRPC 链路是 Langfuse Web UI 的内部 API 通道。它的核心不是“类型安全调用”四个字,而是把 UI 请求放进一条受控管道:
Browser hook -> appRouter -> procedure middleware -> feature service/repository -> Postgres/ClickHouse在这条链路里:
root.ts负责聚合业务 router;trpc.ts负责 context、OpenTelemetry、错误处理、session 和 project/org/trace 访问控制;- feature router 负责把 input schema 和业务调用接起来;
- service/repository 负责真正的数据读取、写入和转换;
- shared query builder/repository 负责跨视图的查询语义。
4.2.2 链路图
4.2.3 root.ts:router 聚合入口
源码:web/src/server/api/root.ts
appRouter 把各 feature router 统一挂到 tRPC 根路由,例如:
events: eventsRoutertraces: traceRouterscores: scoresRouterdashboard: dashboardRouterprojects: projectsRoutersearchBar: searchBarRouterv4Transition: v4TransitionRouter
这说明 UI 内部 API 的主入口不是某个 Next.js file route,而是 appRouter。当你在前端看到 api.events.all.useQuery(...),要去 eventsRouter 找对应 procedure。
4.2.4 trpc.ts:控制面工厂
源码:web/src/server/api/trpc.ts
这个文件有四类关键职责。
Context
createTRPCContext 从 Next.js request/response 中拿到:
- NextAuth session;
- request headers;
- Prisma client;
- user 信息写入当前 span。
这让后续 procedure 可以访问 ctx.session、ctx.headers、ctx.prisma。
OpenTelemetry
withOtelInstrumentation 会读取 raw input,取出 projectId,再调用 contextWithLangfuseProps 设置:
- headers;
- userId;
- projectId;
- ClickHouse surface =
trpc; - route = tRPC path。
这让后续 ClickHouse 查询和日志能知道自己来自哪条 UI route。
错误处理
withErrorHandling 会统一处理 tRPC error:
ClickHouseResourceError转成对用户更友好的 422;- 4xx 暴露安全 message;
- 5xx 在 cloud/self-hosted 下返回不同提示;
- stack 对敏感 ClickHouse error 会被 formatter 移除。
这说明 router 内部不要随意绕过 procedure 工厂,否则错误格式和可观测性会不一致。
Procedure 分层
| procedure | 作用 |
|---|---|
publicProcedure | 不要求登录,但仍接入 otel 和错误处理。 |
authenticatedProcedure | 要求用户登录。 |
protectedProjectProcedure | 要求登录,并从 input 中读取 projectId,验证 project membership。 |
protectedOrganizationProcedure | 要求登录,并验证 org membership。 |
protectedGetTraceProcedure | 保护 trace 详情读取,支持 public trace/session 的特殊访问规则。 |
最常见的 UI 项目级数据读取应该用 protectedProjectProcedure。
4.2.5 protectedProjectProcedure 的工作原理
它做的不是简单 “is logged in”:
- 检查 session 是否存在;
- 从 raw input 解析
projectId; - 在
ctx.session.user.organizations[].projects[]中查 project membership; - admin 用户走特殊路径,必要时从 Postgres 查询 project org;
- 调用
sendAdminAccessWebhook记录 admin access; - 把
orgId、orgRole、projectId、projectRole写回ctx.session; - 后续 handler 只应该使用
ctx.session.projectId,而不是信任客户端传入的input.projectId。
这个设计的重点是:tenant scope 在 middleware 中被标准化,后续业务逻辑不要重新发明权限判断。
4.2.6 以 v4 Events 列表为例
源码:
web/src/features/events/server/eventsRouter.tsweb/src/features/events/server/eventsService.tspackages/shared/src/server/queries/clickhouse-sql/event-query-builder.ts
eventsRouter.all 的链路是:
protectedProjectProcedure.input(GetAllEventsInput)校验输入;applyCommentFilters把评论相关筛选转换成可查询条件;normalizeOrderByForTable规范排序,避免和表的时间列不一致;getEventList设置selectIOAndMetadata: false,列表默认不取重字段;- shared repository/query builder 生成 ClickHouse SQL;
- 另外读取 observation score 和 trace-level score;
- 返回给 UI 表格。
这个例子说明:router 不应该直接拼 SQL;它只负责输入、权限、少量 orchestration,然后交给 service 和 shared query 层。
4.2.7 tRPC 和 shared 的边界
| 逻辑 | 应该在哪里 |
|---|---|
| session/project/org 权限 | web/src/server/api/trpc.ts procedure middleware。 |
| feature-specific input schema | feature router 附近。 |
| 只给 UI 用的组合逻辑 | web/src/features/*/server/*Service.ts。 |
| 跨 UI/API/worker 的 domain schema | packages/shared/src/domain/**。 |
| ClickHouse 字段映射、FilterState lowering | packages/shared/src/server/queries/**。 |
| 纯 worker 执行逻辑 | worker/src/**。 |
如果 router 里开始出现大量 SQL、queue payload 结构、ClickHouse insert、跨 worker 的状态处理,通常说明边界放错了。
4.2.8 自检清单
- input schema 是否包含
projectId,并且 procedure 会验证它? - handler 是否使用
ctx.session.projectId,而不是盲信 input? - router 是否只做 orchestration,没有塞大量业务转换?
- 查询是否走 shared repository/query builder,而不是手写散落 SQL?
- ClickHouse 查询是否有 project filter、时间窗口、field set 和 query tag?
- 这个 API 是否只是 UI 内部使用?如果给外部用户用,应该走 Public API contract。