Skip to content

fix(server-utils): Nest LangChain run spans under their parent run - #25364

Open
RulaKhaled wants to merge 3 commits into
developfrom
fix/langchain-callback-handler-parenting
Open

RulaKhaled wants to merge 3 commits into
developfrom
fix/langchain-callback-handler-parenting

Conversation

@RulaKhaled

@RulaKhaled RulaKhaled commented Oct 14, 2026 •

Copy link
Copy Markdown
Collaborator

With Sentry.createLangChainCallbackHandler() passed explicitly, nested LangChain runs came out as siblings under the caller's span, and chain and tool spans had no gen_ai.conversation.id on @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 from Sentry.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

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>
@github-actions

github-actions Bot commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

size-limit report 📦

Path Size % Change Change
@sentry/browser 29.88 kB - -
@sentry/browser - with treeshaking flags 28.02 kB - -
@sentry/browser - with treeshaking flags tracing without tracing 27.92 kB - -
@sentry/browser (incl. Tracing) 52.07 kB - -
@sentry/browser (incl. Tracing + Span Streaming) 52.08 kB - -
@sentry/browser (incl. Tracing, Profiling) 55.02 kB - -
@sentry/browser (incl. Tracing, Replay) 91.82 kB - -
@sentry/browser (incl. Tracing, Replay) - with treeshaking flags 80.68 kB - -
@sentry/browser (incl. Tracing, Replay with Canvas) 96.55 kB - -
@sentry/browser (incl. Tracing, Replay, Feedback) 109.49 kB - -
@sentry/browser (incl. Feedback) 47.4 kB - -
@sentry/browser (incl. sendFeedback) 34.94 kB - -
@sentry/browser (incl. FeedbackAsync) 40.05 kB - -
@sentry/browser (incl. Metrics) 30.9 kB - -
@sentry/browser (incl. Logs) 31.18 kB - -
@sentry/browser (incl. Metrics & Logs) 31.81 kB - -
@sentry/react 31.7 kB - -
@sentry/react (incl. Tracing) 54.39 kB - -
@sentry/vue 38.04 kB - -
@sentry/vue (incl. Tracing) 55.01 kB - -
@sentry/svelte 29.9 kB - -
@sentry/remix (Remix 3 client bundle) 57.09 kB - -
CDN Bundle 31.64 kB - -
CDN Bundle (incl. Tracing) 52.58 kB - -
CDN Bundle (incl. Logs, Metrics) 33.84 kB - -
CDN Bundle (incl. Tracing, Logs, Metrics) 54.52 kB - -
CDN Bundle (incl. Replay, Logs, Metrics) 74.77 kB - -
CDN Bundle (incl. Tracing, Replay) 90.25 kB - -
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) 92.18 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback) 96.4 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) 98.34 kB - -
CDN Bundle - uncompressed 93.18 kB - -
CDN Bundle (incl. Tracing) - uncompressed 156.03 kB - -
CDN Bundle (incl. Logs, Metrics) - uncompressed 99.72 kB - -
CDN Bundle (incl. Tracing, Logs, Metrics) - uncompressed 161.99 kB - -
CDN Bundle (incl. Replay, Logs, Metrics) - uncompressed 229.76 kB - -
CDN Bundle (incl. Tracing, Replay) - uncompressed 276.2 kB - -
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) - uncompressed 282.14 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed 289.9 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) - uncompressed 295.84 kB - -
@sentry/nextjs (client) 56.8 kB - -
@sentry/sveltekit (client) 52.45 kB - -
@sentry/core/server 41.12 kB - -
@sentry/core/browser 13.81 kB - -
@sentry/node 152.63 kB +0.12% +170 B 🔺
@sentry/node/import (ESM hook with diagnostics-channel injection) 83.81 kB - -
@sentry/node - without tracing 94.61 kB +0.01% +3 B 🔺
@sentry/node - without channel injection 130.86 kB +0.14% +172 B 🔺
@sentry/aws-serverless 102.77 kB +0.01% +5 B 🔺
@sentry/cloudflare (withSentry) - minified 210.6 kB - -
@sentry/cloudflare (withSentry) 522.53 kB - -
@sentry/nextjs/cloudflare (withSentry) - minified 228.27 kB - -

View base workflow run

RulaKhaled and others added 2 commits October 15, 2026 11:37
…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>
@RulaKhaled
RulaKhaled marked this pull request as ready for review October 15, 2026 11:47
@RulaKhaled
RulaKhaled requested a review from a team as a code owner October 15, 2026 11:47
@RulaKhaled
RulaKhaled requested review from andreiborza, isaacs and mydea and removed request for a team October 15, 2026 11:47

@isaacs isaacs left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 => {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

LangChain: explicit callback handler produces flat spans and drops the conversation id on chain and tool spans

2 participants