feat(node): Add eveConversationHook() to link eve sessions as Sentry conversations - #24247
feat(node): Add eveConversationHook() to link eve sessions as Sentry conversations#24247mydea wants to merge 6 commits into
eveConversationHook() to link eve sessions as Sentry conversations#24247Conversation
…y conversations
Adds `Sentry.eveConversationHook()`, used as the default export of an eve
`agent/hooks/sentry.ts`:
export default defineHook(Sentry.eveConversationHook());
It tags every turn of an eve session with the durable session id as the Sentry
conversation id, so the session's AI spans — which land in separate traces
(each eve turn is its own durable workflow) — group into one conversation in
the Agents "Conversations" view.
The id is set on the isolation scope rather than on the AI call: eve's session
id never reaches the AI SDK's telemetry diagnostics channel, so the only way to
attach it is via the scope, where the always-on `conversationIdIntegration`
picks it up and stamps `gen_ai.conversation.id` onto the gen_ai spans.
Subscribes to both `turn.started` and `step.started`. Each turn is a fresh
request with its own isolation scope, and a turn that parks and resumes
(approvals, compaction) resumes in another request where `turn.started` won't
re-fire — `step.started` runs before every model call, so together they cover
each request that produces spans. Re-setting the same id is idempotent.
The eve hook context is typed structurally (not imported from `eve`) so
`@sentry/node` keeps no dependency on the framework; the shape is checked at the
`defineHook(...)` call site instead.
The node-eve e2e app uses the hook and asserts `gen_ai.conversation.id` on each
gen_ai span equals the eve session id.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
size-limit report 📦
|
…e-export from every runtime SDK The helper only needs `setConversationId` from core, so `@sentry/server-utils` is its natural home — a framework-agnostic shared layer already re-exported by the runtime SDKs — rather than living in `@sentry/node`. - `@sentry/node` now re-exports it from `@sentry/server-utils` (like the other shared server helpers), so every node-based SDK that does `export * from '@sentry/node'` (astro, nitro — eve's own base —, nestjs, hono, effect) surfaces it automatically. - Added to the explicit `@sentry/node` re-export blocks of `@sentry/bun`, `@sentry/aws-serverless` and `@sentry/google-cloud-serverless`. - Added to the `@sentry/server-utils` re-export blocks of `@sentry/deno` and `@sentry/cloudflare`, which build on server-utils rather than node. The unit test moves alongside the implementation into `@sentry/server-utils`. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
It is a single-field options bag callers pass as an inline object literal, so exporting the type name added public API surface across every runtime SDK for no benefit. It stays declared (unexported) alongside the function, so the signature is unaffected and callers still pass options structurally. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Astro's runtime entry curates its `@sentry/node` re-exports (it can't `export *`), so the helper has to be listed explicitly like the other SDKs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
RulaKhaled
left a comment
There was a problem hiding this comment.
left two comments, otherwise lgtm
|
|
||
| const setConversationIdFromContext: EveHookHandler = (_event, context) => { | ||
| const conversationId = resolveConversationId(context); | ||
| if (conversationId) { |
There was a problem hiding this comment.
does this mean a resolver returning undefined cannot unset the conversation id? setConversationId's documentsnull | undefined
There was a problem hiding this comment.
oops, yeah, should not be that way, I refactored this to just use the value as-is and set it!
| * | ||
| * ```ts | ||
| * // agent/hooks/sentry.ts | ||
| * import * as Sentry from '@sentry/node'; |
There was a problem hiding this comment.
nit: I guess this can also come from non-node packages?
There was a problem hiding this comment.
jup, theoretically, but eve is mostly just documented for node, so I think it's fine to keep this as example/docs here?
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ 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 4bf7875. Configure here.
| const setConversationIdFromContext: EveHookHandler = (_event, context) => { | ||
| const conversationId = getConversationId ? getConversationId(context) : context.session.id; | ||
| setConversationId(conversationId); | ||
| }; |
There was a problem hiding this comment.
Bug: The eveConversationHook calls setConversationId even when getConversationId returns null or undefined, which clears the conversation ID instead of being a no-op.
Severity: MEDIUM
Suggested Fix
Add a check to ensure setConversationId is only called when conversationId is a truthy value.
const conversationId = getConversationId ? getConversationId(context) : context.session.id;
if (conversationId) {
setConversationId(conversationId);
}Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.
Location: packages/server-utils/src/eve.ts#L51-L54
Potential issue: The `setConversationIdFromContext` function unconditionally calls
`setConversationId` with the result of the `getConversationId` callback. The callback is
typed to allow `null` or `undefined` return values, which are intended to be no-ops.
However, the `setConversationId` implementation treats `null` or `undefined` as a signal
to clear any existing conversation ID on the scope. This causes an unintended side
effect where opting out of setting a conversation ID for a specific event actively
deletes the existing one, breaking conversation grouping in Sentry.
Did we get this right? 👍 / 👎 to inform future reviews.
There was a problem hiding this comment.
should not be a no-op, if users use this, we use the conversation id as-is

Stacked on #24228 (base branch
feat/e2e-eve-agent).Adds
eveConversationHook(), used as the default export of an eveagent/hooks/sentry.ts:It tags every turn of an eve session with the durable session id (
ctx.session.id) as the Sentry conversation id, so the session's AI spans — which land in separate traces, since each eve turn is its own durable workflow — group into one conversation in the Agents "Conversations" view.Root cause / why this shape:
recordInputs/recordOutputs/functionId;runtimeContext/metadata go to eve's own OTel integration, which Sentry bypasses). So the id can only be attached via the isolation scope, where the always-onconversationIdIntegrationpicks it up and stampsgen_ai.conversation.idonto the gen_ai spans.turn.startedandstep.started. Each turn is a fresh request with its own isolation scope, and a turn that parks and resumes (approvals, compaction) resumes in another request whereturn.startedwon't re-fire.step.startedruns before every model call, so together they cover every request that produces spans. Re-setting the same id is idempotent.@sentry/server-utils. The helper only needssetConversationIdfrom core, so it belongs in the framework-agnostic shared layer, not@sentry/node.@sentry/nodere-exports it (so everyexport * from '@sentry/node'SDK — astro, nitro (eve's own base), nestjs, hono, effect — surfaces it automatically); it is added explicitly to the node-based@sentry/bun,@sentry/aws-serverless,@sentry/google-cloud-serverless, and to the server-utils-based@sentry/denoand@sentry/cloudflare.eve, so the SDK keeps no dependency on the framework; the shape is checked at thedefineHook(...)call site. An optionalgetConversationIdlets users override the default (e.g. to use a root/parent session id for subagents).The
node-evee2e app now uses the hook and assertsgen_ai.conversation.idon each gen_ai span equals the eve session id returned by the session endpoint.🤖 Generated with Claude Code