Repository navigation
feat(langchain): Derive gen_ai.conversation.id from the invoke config - #24831
Conversation
size-limit report 📦
|
LangChain copies every primitive `config.configurable` entry into run metadata and child runs inherit it, so a `thread_id`, `session_id` or `sessionId` passed to `invoke()` is visible to the callback handler on every chat, chain and tool run of that invocation. Read it there and set `gen_ai.conversation.id` on each span. An id set on the scope via `Sentry.setConversationId()` still wins, since `conversationIdIntegration` applies it on `spanStart`, after the initial attributes. This also fills the gap where LangGraph only stamped the id on its `invoke_agent` span: the child spans come from this handler and now carry the same `thread_id`. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
`@sentry/bun` builds its own default integration list and never included `conversationIdIntegration`, so `Sentry.setConversationId()` had no effect on Bun: the id landed on the scope but was never stamped onto gen_ai spans. Node, Deno, Cloudflare, Vercel Edge and the browser all register it by default. Register it on Bun in the same position. Surfaced by the LangChain conversation id suite, whose "scope id wins" case failed on the Bun runner. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
0a049c1 to
574aa91
Compare
isaacs
left a comment
There was a problem hiding this comment.
A few things that could be either addressed now, or called out in the commit message or pr description for followups if you wanna keep the scope limited. LGTM!
| // Skip tool spans when inside an agent context (createReactAgent). | ||
| // Tool spans are created by wrapToolsWithSpans with richer attributes. | ||
| if (metadata?.__sentry_langgraph__) { | ||
| return; |
There was a problem hiding this comment.
The description says LangGraph "stamped it on its own invoke_agent span only, so the chat and tool child spans went out without it." This PR fixes the chat spans, because handleChatModelStart / handleLLMStart don't skip on __sentry_langgraph__. But this returns early here when metadata.__sentry_langgraph__ is set. So in the createReactAgent path, tool spans come from wrapToolsWithSpans, and those spans still go out without gen_ai.conversation.id, and as a result, LangGraph tool spans still have no conversation id.
wrapToolsWithSpans already reads callConfig.metadata.lc_agent_name at call time, so the same metadata carries thread_id.
Suggestion: in wrapToolsWithSpans, spread getConversationIdFromMetadata(callConfig?.metadata) into spanAttributes, next to the agent name read. Or narrow the PR description so it we don't claim tool spans are fixed also, and we could fix them in a followup.
There was a problem hiding this comment.
True! went with your first option. wrapToolsWithSpans reads it next to the agent name now + a test
|
|
||
| const chainName = runName || chain.name; | ||
| const attributes: Record<string, SpanAttributeValue> = { | ||
| ...getConversationIdFromMetadata(metadata), |
There was a problem hiding this comment.
One interesting impact of this, chain spans get no agent name, but do get the conversation id.
Chat and tool spans spread getAgentNameFromMetadata(metadata) and then getConversationIdFromMetadata(metadata). Chain spans spread only the conversation id. I wouldn't call this a regression, and the chain span is itself the invoke_agent span, so an agent name from lc_agent_name may not apply.
I'm only mentioning to confirm it's on purpose; it's not an unreasonable choice imo. No change needed if so :)
There was a problem hiding this comment.
yah on purpose, and pre existing from #20344
lc_agent_name only gets injected by our langgraph proxy, and that same proxy sets __sentry_langgraph__ so handleChainStart bails before there'd be anything to read
…the LangChain precedence `createReactAgent` tool spans come from `wrapToolsWithSpans`, which never read the conversation id. They now read it from the call metadata. The `invoke_agent` span now uses `getConversationIdFromMetadata` in its initial attributes, so it reads the same keys as its child spans and an id from `Sentry.setConversationId()` wins over `thread_id`, as it does on the children. Numeric ids that are `NaN` or `Infinity` are ignored. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts: # packages/server-utils/src/ai/langchain/index.ts # packages/server-utils/src/ai/langgraph/index.ts
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Want reviews to match your repository better? Bugbot Learning can learn team-specific rules from PR activity. A team admin can enable Learning in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 8e09532. Configure here.
…gchain/core` >= 1.1.40 `@langchain/core` 1.1.40 stopped copying `configurable` into run metadata for handlers other than LangSmith's tracer, so our handler never saw `thread_id` or `sessionId` there. The chat-model hook and the LangGraph `invoke` proxy now copy those keys into the metadata themselves, with existing metadata taking precedence. Adds a LangChain v1 integration test and asserts `thread_id` on every span of the LangGraph `createReactAgent` tools scenario. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… and config helpers `getConversationIdMetadataFromConfig` now copies only the first valid key, using the same rule as `getConversationIdFromMetadata`. The span gets the same id, and less ends up in the user's run metadata. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

The LangChain integration only set
gen_ai.conversation.idwhen the user calledSentry.setConversationId()themselves. LangGraph readconfigurable.thread_id, but stamped it on its owninvoke_agentspan only, so the chat and tool child spans went out without it.Child runs inherit their parent's metadata, so this PR reads the conversation id from run metadata and sets the attribute on the chat, chain and tool spans. On
@langchain/core0.1.20 to 1.1.39,ensureConfigcopies every primitiveconfig.configurableentry into that metadata, so an id passed toinvoke()is already there.@langchain/core1.1.40 stopped doing that for any handler except LangSmith's tracer. So where we inject our handler, in the chat-model hook and the LangGraphinvokeproxy, we copy the three keys below fromconfigurableintometadataourselves. Metadata the user set takes precedence.Keys read, in order:
thread_id, the LangGraph checkpointer key and one of the two keys LangSmith groups threads bysession_id, the other LangSmith keysessionId, whatRunnableWithMessageHistoryrequires in JSAn id set on the scope via
Sentry.setConversationId()still wins.conversationIdIntegrationapplies it onspanStart, after the initial attributes, so an explicit user id overwrites the derived one. The integration test covers this.LangGraph's
invoke_agentspan and the tool spanscreateReactAgentcreates now use the same helper. So they read the same keys, accept numeric ids, and let an id fromSentry.setConversationId()win overthread_id. Before,thread_idoverwrote the scope id on theinvoke_agentspan only, and the tool spans never picked upthread_id.One gap remains on
@langchain/core>= 1.1.40. If you passcreateLangChainCallbackHandler()yourself to a chain outside LangGraph, the chain and tool spans get no id fromconfigurable, because the handler never sees it. The chat spans still get it. Closing that would mean hooking more than chat models, so it is left for a follow-up.Bun. The new suite failed on the Bun runner because
@sentry/bunbuilds its own default integration list and never registeredconversationIdIntegration, soSentry.setConversationId()was a no-op there. The second commit adds it, in the same position Node uses.Part of #24832.
🤖 Generated with Claude Code