Repository navigation
fix(server-utils): Nest LangChain run spans under their parent run - #25364
RulaKhaled wants to merge 3 commits into
Conversation
The callback handler ignored parentRunId, so nested runs became siblings under the caller's span. Chain and tool spans also lost the conversation id on @langchain/core >= 1.1.40, which keeps configurable out of run metadata. Start each run under its parent run's span, and share the conversation id across the open spans of the run tree. Fixes #25327 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
size-limit report 📦
|
…arents A child run was always started under its parent run's span. That span may be dropped by ignoreSpans, which made every descendant unrecorded, and a span opened inside the run body (e.g. Sentry.startSpan around a nested invoke) got bypassed. Walk up to the nearest run span that isn't an ignored child, as core does for ignored spans, so an ignored root still drops its subtree. Keep the active span as the parent when it is no longer the one the ancestor run started under. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
spanMap, runs and decideRunIds always held the same run ids. Keep each run's span, parent, active span, conversation id and decide flag in one record, so the ancestor walks do one lookup per step and exitSpan clears a single entry. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
isaacs
left a comment
There was a problem hiding this comment.
I'd update the PR description (or just, the message when landing) to mention the behavior fix. LGTM, this is the right change 👍
| return undefined; | ||
| }; | ||
|
|
||
| const startRunSpan = (runId: string, parentRunId: string | undefined, start: () => Span): LangChainRun => { |
There was a problem hiding this comment.
This behavior change is a bit wider than the PR description says. Not a problem, but should probably be called out.
With no active span, each run used to become its own root span. With traceLifecycle: 'static' that means one transaction per run (chain outer, execute_tool ..., chat ...). Now the whole run tree nests under the first run, so the user gets one transaction. I confirmed this with a scratch test: with no outer span, execute_tool and chat now have chain outer as their parent.
That is the correct result, but it changes transaction counts and names, which can affect dashboards and alerts.
I'd suggest adding a line like "Runs started without an active span now form one trace, instead of one root span per run."
| // Internal state - single instance tracks all spans | ||
| const spanMap = new Map<string, Span>(); | ||
| const decideRunIds = new Set<string>(); | ||
| const runs = new Map<string, LangChainRun>(); |
There was a problem hiding this comment.
If LangChain never calls the end or error callback for a run (for example, a consumer abandons a stream), the entry stays in runs forever. That leak existed before with spanMap. Now each entry also holds activeSpan (line 56), which can be a long-lived caller span (and through it, its span tree). The auto-instrumentation handler is one shared instance for the whole process, so these entries could add up.
No change required in this PR, but it is maybe worth a follow-up issue, for example clearing entries whose span has already ended when a new run starts, or capping the map size.
With
Sentry.createLangChainCallbackHandler()passed explicitly, nested LangChain runs came out as siblings under the caller's span, and chain and tool spans had nogen_ai.conversation.idon@langchain/core>= 1.1.40.The handler ignored
parentRunId, so every run attached to whatever span was active. Each run now starts under its parent run's span, skipping ignored spans like core does, and falls back to the active span when the parent run has no span (e.g. a skipped LangGraph step) or another span was opened inside it. Since 1.1.40 only the chat span tends to learn the conversation id, so a run now inherits it from its parent, and a span that knows it passes it up to open ancestors without one. An id fromSentry.setConversationId()still wins.Limits. A run that ends before the id appears keeps none.
It also tightens the test from #25326 to assert the exact parent chain.
Fixes #25327
🤖 Generated with Claude Code