[WIP] Implementation of a Node.js Backend Service for Handling Transparent Span Transmission #242
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Coding agent has begun work on Implementation of a Node.js Backend Service for Handling Transparent Span Transmission and will replace this description as work progresses.
See problem context
The following is a snapshot of a chat conversation between a user and an AI coding assistant. Prioritize later messages in the conversation.
User: @workspace /explain 现在的设计能不能实现 Langfuese Observation 被解析为 ReadableSpan然后被透明传输到后端的设计?
AI: 当前结构无法直接把 Langfuse 的 Observation 视作
AI: 并交给
AI: 。Exporter 只会处理由
AI: 生成并送入
AI: 的 OpenTelemetry span,而
AI: 里通过
AI: 产生的是 Langfuse 自有对象,与 OTel span 管道隔离。
AI: 只是让 Langfuse 能共享同一个 Provider,并不会把 Observation 注入 OTel。若要“透明传输”,需要在回调里显式把每个 Observation 映射成 OTel span(或另写一个针对 Observation 的 exporter)后再交给 Provider 处理。
User: https://github.com/langfuse/langfuse-js/tree/main/packages/tracing/src
阅读这个仓库下的代码,然后给出解决方案
AI: 先梳理 Langfuse tracing 包和我们现有追踪初始化,再把两边打通:
packages/tracing/src关键实现做了梳理,确认 Observation -> OpenTelemetry Span 的映射路径。init-tracing.ts/TransparentBrowserExporter设计,提出如何复用同一个TracerProvider,让 Langfuse Observation 产出的ReadableSpan自动进入我们的透明传输链路。核心发现
startObservation等 API 最终都会调用getLangfuseTracer()→Tracer.startSpan()(见AI: 第 112 行起)。返回的
LangfuseSpan等对象只是对 OTelSpan的包装,内部持有otelSpan;结束时会直接调用otelSpan.end()。setLangfuseTracerProvider会把我们自定义的TracerProvider存进全局状态,后续getLangfuseTracer()就会从这里取(AI: 第 119 行起)。
BatchSpanProcessor+TransparentBrowserExporter的BasicTracerProvider传进去,Langfuse 生成的所有 Observation 都会作为普通ReadableSpan交给 exporter,无需再做二次转换。LangfuseSpanProcessor(来自@langfuse/otel),OTel 允许挂多个 Processor。推荐方案
初始化阶段共用 Provider
initTracing中我们已经构造了BasicTracerProvider并调用了setLangfuseTracerProvider(provider)——保留这一步即可。initTracing总是在调用createLangfuseCallback之前执行。这样 Langfuse callback 内部所有startObservation都会复用同一个 provider。Exporter 链路
BatchSpanProcessor→TransparentBrowserExporter会收到所有ReadableSpan,包括我们在业务里手工启动的 span、Langfuse Planner / Agent / Generation Observation。provider.addSpanProcessor(new LangfuseSpanProcessor({ … }))。它与我们的透明 exporter 互不干扰。可选:在 Transparent exporter 中识别 Langfuse 元数据
LangfuseOtelSpanAttributes枚举)。回调层的小改动(若需要同步 Langfuse 的 sessionId 到 span)
createLangfuseCallback已尝试从taskPrompt/context提取sessionId并写到root.updateTrace。startObservation前针对agentContext写一个小工具,为当前活动 span 调用span.setAttribute(LangfuseOtelSpanAttributes.TRACE_SESSION_ID, sessionId)。这属于可选增量。测试验证
packages/eko-core/example/nodejs_debug下运行一次 Task,观察我们的后端是否成功收到 Planner/Generation 等 span,或在浏览器端用navigator.sendBeacon调试TransparentBrowserExporterpayload。LangfuseSpanProcessor并确认它和透明 exporter 都能拿到同一批ReadableSpan。质量门
需求覆盖
User: 请检查当前的exporter实现,这个透明传输的设计是否合理,是否会漏掉属性?
AI: 先快速复盘
TransparentBrowserExporter的结构,再对照ReadableSpan的字段检查有没有被忽略的关键信息,最后给出改进建议。检查结果
traceId、spanId、name、kind、startTime、endTime、attributes、status、events、resource、instrumentationScope)都有覆盖,Langfuse 写入到 attributes 的内容会被完整保留。ReadableSpan里的以下信息:links:链路信息在链状工作流里很常用,丢掉后无法还原代理-工具之间的关联。droppedAttributesCount、droppedEventsCount、droppedLinksCount:会影响排障时对采样/丢弃行为的认知。duration(虽然可由 start/end 复算,但直接带上可省一步)。span.spanContext()里的traceFlags、traceState也没带,如果后端要继续分发或做抽样判定会有信息缺口。json.length计算字符数,遇上非 ASCII 或JSON.stringify的 Unicode 转义时会和真实字节数不一致,可能提前或延后触发限额。useSendBeacon控制项也清楚。建议
toTransportSpan里补上上述遗漏字段,例如:links: span.linksdroppedCounts三个字段duration或后端再算traceFlags: span.spanContext().traceFlags、traceState: span.spanContext().traceState?.serialize?.()new TextEncoder().encode(json).length计算;Node 环境可以回退到Buffer.byteLength(json, "utf8")。ReadableSpan的浅拷贝(去掉函数字段)或遵循 OTLP JSON 约定格式;这样能减少后续维护时同步字段的工作量。需求覆盖
User: 那就直接发送ReadableSpan的浅拷贝也行,或者用OTPL JSON。总之要保证后端能够很容易的解析成Langfuse对应的Observation。
AI:
AI:
User: [I] ~/Project/eko/packages/eko-core x> pnpm run build feat/eko-debugger *
src/index.ts → dist/index.cjs.js...
(!) "this" has been rewritten to "undefined"
https://rollupjs.org/troubleshooting/#error-this-is-undefined
../../node_modules/.pnpm/@opentelemetry[email protected]/node_modules/@opentelemetry/api/build/esm/metrics/NoopMeter.js
14: * limitations under the License.
15: */
16: var __extends = (this && this.__extends) || (function () {
^
17: var extendStatics = function (d, b) {
18: extendStatics = Object.setPrototypeOf ||
...and 1 other occurrence
../../node_modules/.pnpm/@opentelemetry[email protected]/node_modules/@opentelemetry/api/build/esm/api/diag.js
14: * limitations under the License.
15: */
16: var __read = (this && this.__read) || function (o, n) {
^
17: var m = typeof Symbol === "function" && o[Symbol.iterator];
18: if (!m) return o;
...and 3 other occurrences
../../node_modules/.pnpm/@opentelemetry[email protected]/node_modules/@opentelemetry/api/build/esm/api/context.js
14: * limitations under the License.
15: */
16: var __read = (this && this.__read) || function (o, n) {
^
17: var m = typeof Symbol === "function" && o[Symbol.iterator];
18: if (!m) return o;
...and 3 other occurrences
...and 3 other files
(!) Circular dependencies
../../node_modules/.pnpm/[email protected]/node_modules/zod/v4/classic/schemas.js -> ../../node_modules/.pnpm/[email protected]/node_modules/zod/v4/classic/iso.js -> ../../node_modules/.pnpm/[email protected]/node_modules/zod/v4/classic/schemas.js
../../node_modules/.pnpm/@[email protected]/node_modules/@aws-crypto/crc32/build/module/index.js -> ../../node_modules/.pnpm/@[email protected]/node_modules/@aws-crypto/crc32/build/module/aws_crc32.js -> ../../node_modules/.pnpm/@[email protected]/node_modules/@aws-crypto/crc32/build/module/index.js
src/agent/llm.ts -> src/memory/index.ts -> src/agent/llm.ts
(!) Mixing named and default exports
https://rollupjs.org/configuration-options/#output-exports
The following entry modules are using named and default exports together:
src/index.ts
Consumers of your bundle will have to use chunk.default to access their default export, which may not be what you want. Use
output.exports: "named"to disable this warning.created dist/index.cjs.js in 1.7s
src/index.ts → dist/index.esm.js...
(!) "this" has been rewritten to "undefined"
https://rollupjs.org/troubleshooting/#error-this-is-undefined
../../node_modules/.pnpm/@opentelemetry[email protected]/node_modules/@opentelemetry/api/build/esm/metrics/NoopMeter.js
14: * limitations under the License.
15: */
16: var __extends = (this && this.__extends) || (function () {
^
17: var extendStatics = function (d, b) {
18: extendStatics = Object.setPrototypeOf ||
...and 1 other occurrence
../../node_modules/.pnpm/@opentelemetry[email protected]/node_modules/@opentelemetry/api/build/esm/api/context.js
14: * limitations under the License.
15: */
16: var __read = (this && this.__read) || function (o, n) {
^
17: var m = typeof Symbol === "function" && o[Symbol.iterator];
18: if (!m) return o;
...and 3 other occurrences
../../node_modules/.pnpm/@opentelemetry[email protected]/node_modules/@opentelemetry/api/build/esm/api/diag.js
14: * limitations under the License.
15: */
16: var __read = (this && this.__read) || function (o, n) {
^
17: var m = typeof Symbol === "function" && o[Symbol.iterator];
18: if (!m) return o;
...and 3 other occurrences
...and 3 other files
(!) Circular dependencies
../../node_modules/.pnpm/[email protected]/node_modules/zod/v4/classic/schemas.js -> ../../node_modules/.pnpm/[email protected]/node_modules/zod/v4/classic/iso.js -> ../../node_modules/.pnpm/[email protected]/node_modules/zod/v4/classic/schemas.js
../../node_modules/.pnpm/@[email protected]/node_modules/@aws-crypto/crc32/build/module/index.js -> ../../node_modules/.pnpm/@[email protected]/node_modules/@aws-crypto/crc32/build/module/aws_crc32.js -> ../../node_modules/.pnpm/@[email protected]/node_modules/@aws-crypto/crc32/build/module/index.js
src/agent/llm.ts -> src/memory/index.ts -> src/agent/llm.ts
created dist/index.esm.js in 1.2s
[I] ~/Project/eko/packages/eko-core x> feat/eko-debugger *
这是构建结果
AI: 先把 exporter 改成按原始
AI: 结构序列化,确保后端能直接当作 Langfuse Observation 解析;主要动作:
TransparentBrowserExporter里补齐traceFlags/traceState、AI: 、
AI: 、
dropped*Count等字段,并把AI: 、父上下文都序列化。
getByteLength,浏览器用TextEncoder、Node 用Buffer.byteLength,保证批大小限制按真实字节计算。构建结果
pnpm --filter eko-core build✅(Rollup 只提示 opentelemetry 自带的"this" has been rewritten、循环依赖、mixed exports,这些是既有依赖的已知告警)。需求覆盖
AI: /OTLP JSON,让后端易于恢复 Langfuse Observation” → ✅ 已改写 exporter 并验证可以编译。
如还想进一步贴近 OTLP,可把
toTransportSpan改成产出标准 OTLP JSON 的ResourceSpans结构;后端若已有解析器,可以直接复用。User: 非常好,我现在要用Node来做一个后端服务来处理这些被透明传输来的Span了。
我的想法是这个API会先转成Span对象,然后构建对应的Observation,然后使用opentelemetry node sdk,配合langfuse otel 中的 LangfuseSpanProcesser来将透明传输到我这个API端点的内容传输的到Langfuse。
User: 非常好,我现在要用Node来做一个后端服务来处理这些被透明传输来的Span了。
我的想法是这个API会先转成Span对象,然后构建对应的Observation,然后使用opentelemetry node sdk,配合langfuse otel 中的 LangfuseSpanProcesser来将透明传输到我这个API端点的内容传输的到Langfuse。
你现在最好是找一个合适的文件夹来创建这个后端服务。
TITLE: Implementation of a Node.js Backend Service for Handling Transparent Span Transmission
USER INTENT: The user aims to create a Node.js backend service that processes spans transmitted transparently from the frontend, converting them into observations and sending them to Langfuse using the OpenTelemetry Node SDK.
TASK DESCRIPTION: The user wants to set up a backend service that:
EXISTING:
TransparentBrowserExporter.ReadableSpanstructures or OTLP JSON to the backend.PENDING:
CODE STATE:
RELEVANT CODE/DOCUMENTATION SNIPPETS:
LangfuseSpanProcessorfrom the Langfuse OpenTelemetry integration, which will be used in the backend service.ReadableSpanand its attributes, which will be relevant for processing spans in the backend.OTHER NOTES:
Original prompt
非常好,我现在要用Node来做一个后端服务来处理这些被透明传输来的Span了。
我的想法是这个API会先转成Span对象,然后构建对应的Observation,然后使用opentelemetry node sdk,配合langfuse otel 中的 LangfuseSpanProcesser来将透明传输到我这个API端点的内容传输的到Langfuse。
你现在最好是找一个合适的文件夹来创建这个后端服务。
The following is a snapshot of a chat conversation between a user and an AI coding assistant. Prioritize later messages in the conversation.
User: @workspace /explain 现在的设计能不能实现 Langfuese Observation 被解析为 ReadableSpan然后被透明传输到后端的设计?
AI: 当前结构无法直接把 Langfuse 的 Observation 视作
AI: 并交给
AI: 。Exporter 只会处理由
AI: 生成并送入
AI: 的 OpenTelemetry span,而
AI: 里通过
AI: 产生的是 Langfuse 自有对象,与 OTel span 管道隔离。
AI: 只是让 Langfuse 能共享同一个 Provider,并不会把 Observation 注入 OTel。若要“透明传输”,需要在回调里显式把每个 Observation 映射成 OTel span(或另写一个针对 Observation 的 exporter)后再交给 Provider 处理。
User: https://github.com/langfuse/langfuse-js/tree/main/packages/tracing/src
阅读这个仓库下的代码,然后给出解决方案
AI: 先梳理 Langfuse tracing 包和我们现有追踪初始化,再把两边打通:
packages/tracing/src关键实现做了梳理,确认 Observation -> OpenTelemetry Span 的映射路径。init-tracing.ts/TransparentBrowserExporter设计,提出如何复用同一个TracerProvider,让 Langfuse Observation 产出的ReadableSpan自动进入我们的透明传输链路。核心发现
startObservation等 API 最终都会调用getLangfuseTracer()→Tracer.startSpan()(见AI: 第 112 行起)。返回的
LangfuseSpan等对象只是对 OTelSpan的包装,内部持有otelSpan;结束时会直接调用otelSpan.end()。setLangfuseTracerProvider会把我们自定义的TracerProvider存进全局状态,后续getLangfuseTracer()就会从这里取(AI: 第 119 行起)。
BatchSpanProcessor+TransparentBrowserExporter的BasicTracerProvider传进去,Langfuse 生成的所有 Observation 都会作为普通ReadableSpan交给 exporter,无需再做二次转换。LangfuseSpanProcessor(来自@langfuse/otel),OTel 允许挂多个 Processor。推荐方案
初始化阶段共用 Provider
initTracing中我们已经构造了BasicTracerProvider并调用了setLangfuseTracerProvider(provider)——保留这一步即可。initTracing总是在调用createLangfuseCallback之前执行。这样 Langfuse callback 内部所有startObservation都会复用同一个 provider。Exporter 链路
BatchSpanProcessor→TransparentBrowserExporter会收到所有ReadableSpan,包括我们在业务里手工启动的 span、Langfuse Planner / Agent / Generation Observation。provider.addSpanProcessor(new LangfuseSpanProcessor({ … }))。它与我们的透明 exporter 互不干扰。可选:在 Transparent exporter 中识别 Langfuse 元数据
LangfuseOtelSpanAttributes枚举)。回调层的小改动(若需要同步 Langfuse 的 sessionId 到 span)
createLangfuseCallback已尝试从taskPrompt/context提取sessionId并写到root.updateTrace。startObservation前针对agentContext写一个小工具,为当前活动 span 调用span.setAttribute(LangfuseOtelSpanAttributes.TRACE_SESSION_ID, sessionId)。这属于可选增量。测试验证
packages/eko-core/example/nodejs_debug下运行一次 Task,观察我们的后端是否成功收到 Planner/Generation 等 span,或在浏览器端用navigator.sendBeacon调试TransparentBrowserExporterpayload。LangfuseSpanProcessor并确认它和透明 exporter 都能拿到同一批ReadableSpan。质量门
需求覆盖
💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.