From ac906103442cca3d8d720047aefdf1cf1d6d38e4 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Wed, 9 Sep 2026 15:10:25 +0200 Subject: [PATCH 1/6] feat(node): Add `eveConversationHook()` to link eve sessions as Sentry conversations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../node-eve/agent/hooks/sentry.ts | 7 ++ .../node-eve/tests/eve.test.ts | 10 ++- .../test-applications/node-eve/tests/utils.ts | 7 +- packages/node/src/eve.ts | 64 +++++++++++++++++++ packages/node/src/index.ts | 1 + packages/node/test/eve.test.ts | 52 +++++++++++++++ 6 files changed, 139 insertions(+), 2 deletions(-) create mode 100644 dev-packages/e2e-tests/test-applications/node-eve/agent/hooks/sentry.ts create mode 100644 packages/node/src/eve.ts create mode 100644 packages/node/test/eve.test.ts diff --git a/dev-packages/e2e-tests/test-applications/node-eve/agent/hooks/sentry.ts b/dev-packages/e2e-tests/test-applications/node-eve/agent/hooks/sentry.ts new file mode 100644 index 000000000000..3e7b0ad0a5d5 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-eve/agent/hooks/sentry.ts @@ -0,0 +1,7 @@ +import * as Sentry from '@sentry/node'; +import { defineHook } from 'eve/hooks'; + +// Tags every turn of an eve session with the session id as the Sentry conversation id, so a +// session's AI spans — which land in separate traces (each turn is its own durable workflow) — +// group into one conversation in Sentry. +export default defineHook(Sentry.eveConversationHook()); diff --git a/dev-packages/e2e-tests/test-applications/node-eve/tests/eve.test.ts b/dev-packages/e2e-tests/test-applications/node-eve/tests/eve.test.ts index 3e7adc612c8a..32141b9f3944 100644 --- a/dev-packages/e2e-tests/test-applications/node-eve/tests/eve.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-eve/tests/eve.test.ts @@ -34,7 +34,7 @@ test('captures Vercel AI agent spans (invoke_agent, generate_content, execute_to ) && spansOfTrace.some(isAgentServerSpan), ); - await runAgentTurn(baseURL!, 'What is the weather in Paris?'); + const sessionId = await runAgentTurn(baseURL!, 'What is the weather in Paris?'); const traceSpans = await traceSpansPromise; @@ -66,6 +66,14 @@ test('captures Vercel AI agent spans (invoke_agent, generate_content, execute_to // The tool returns `{ city, condition: 'Sunny', temperatureC: 22 }`. expect(executeTool?.attributes?.['gen_ai.tool.call.result']?.value).toContain('Sunny'); + // `agent/hooks/sentry.ts` sets the eve session id as the conversation id via + // `Sentry.eveConversationHook()`, so every gen_ai span in the turn is tagged with it — that is + // what links a multi-turn session (each turn is its own trace) into one Sentry conversation. + expect(sessionId).toBeTruthy(); + for (const span of [invokeAgent, generateContent, executeTool]) { + expect(span?.attributes?.['gen_ai.conversation.id']?.value).toBe(sessionId); + } + // The agent turn is captured as an http.server span on one of eve's two agent // request paths (the other http.server spans — health and the event stream — // are not in this trace). diff --git a/dev-packages/e2e-tests/test-applications/node-eve/tests/utils.ts b/dev-packages/e2e-tests/test-applications/node-eve/tests/utils.ts index 6e3f0bd7a301..50542a116320 100644 --- a/dev-packages/e2e-tests/test-applications/node-eve/tests/utils.ts +++ b/dev-packages/e2e-tests/test-applications/node-eve/tests/utils.ts @@ -5,8 +5,11 @@ import { expect } from '@playwright/test'; * settle, so the agent has finished and its spans have been flushed before we * assert. eve runs the turn in a durable workflow, so the POST only needs to be * accepted; we drain the event stream to know when the turn is done. + * + * Returns the eve session id, which the SDK also records as `gen_ai.conversation.id` on the turn's + * AI spans (see `agent/hooks/sentry.ts`), so a test can assert the two match. */ -export async function runAgentTurn(baseURL: string, message: string): Promise { +export async function runAgentTurn(baseURL: string, message: string): Promise { const createRes = await fetch(`${baseURL}/eve/v1/session`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, @@ -36,4 +39,6 @@ export async function runAgentTurn(baseURL: string, message: string): Promise void; + +export interface EveConversationHookOptions { + /** + * Derive the Sentry conversation id from the eve hook context. Defaults to the durable session id + * (`ctx.session.id`), which is stable across every turn of a session and so groups them into one + * conversation. + */ + getConversationId?: (context: EveHookContext) => string | undefined; +} + +/** + * Builds the hook definition for an eve `agent/hooks/*.ts` file that tags a session's AI spans with + * a Sentry conversation id, linking every turn of the session in the Agents "Conversations" view. + * + * ```ts + * // agent/hooks/sentry.ts + * import * as Sentry from '@sentry/node'; + * import { defineHook } from 'eve/hooks'; + * + * export default defineHook(Sentry.eveConversationHook()); + * ``` + * + * The id is set on the isolation scope; the default (always-on) `conversationIdIntegration` then + * stamps `gen_ai.conversation.id` onto the gen_ai spans the Vercel AI instrumentation records for + * that turn. That indirection is why the id has to be set here and not on the AI call: eve's session + * id never reaches the AI SDK's telemetry channel, so it can only be attached via the scope. + * + * Subscribes to both `turn.started` and `step.started`. Each eve turn is a fresh durable-workflow + * request with its own isolation scope, and a turn that parks and resumes (approvals, compaction) + * resumes in yet another request; `turn.started` alone would miss the model calls after a resume. + * `step.started` fires before every model call, so together they cover each request that produces + * spans. Re-setting the same id is idempotent, so the overlap is harmless. + */ +export function eveConversationHook(options: EveConversationHookOptions = {}): { + events: Record<'turn.started' | 'step.started', EveHookHandler>; +} { + const resolveConversationId = options.getConversationId ?? (context => context.session.id); + + const setConversationIdFromContext: EveHookHandler = (_event, context) => { + const conversationId = resolveConversationId(context); + if (conversationId) { + setConversationId(conversationId); + } + }; + + return { + events: { + 'turn.started': setConversationIdFromContext, + 'step.started': setConversationIdFromContext, + }, + }; +} diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts index 03532223a506..20c3ddcc264d 100644 --- a/packages/node/src/index.ts +++ b/packages/node/src/index.ts @@ -216,6 +216,7 @@ export { makeNodeTransport } from './transports'; export { createGetModuleFromFilename } from './utils/module'; export { SENTRY_SEGMENT_NAME_SOURCE } from '@sentry/conventions/attributes'; +export { eveConversationHook, type EveConversationHookOptions } from './eve'; export { httpServerIntegration } from './integrations/http/httpServerIntegration'; export { httpServerSpansIntegration } from './integrations/http/httpServerSpansIntegration'; export { processSessionIntegration } from './integrations/processSession'; diff --git a/packages/node/test/eve.test.ts b/packages/node/test/eve.test.ts new file mode 100644 index 000000000000..a4ce01c64a39 --- /dev/null +++ b/packages/node/test/eve.test.ts @@ -0,0 +1,52 @@ +import * as SentryCore from '@sentry/core'; +import { afterEach, describe, expect, test, vi } from 'vitest'; +import { eveConversationHook } from '../src/eve'; + +describe('eveConversationHook', () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + test('subscribes to turn.started and step.started', () => { + const { events } = eveConversationHook(); + + expect(Object.keys(events).sort()).toEqual(['step.started', 'turn.started']); + }); + + test('sets the session id as the conversation id on turn.started', () => { + const setConversationId = vi.spyOn(SentryCore, 'setConversationId').mockImplementation(() => undefined); + + eveConversationHook().events['turn.started'](undefined, { session: { id: 'sess_abc' } }); + + expect(setConversationId).toHaveBeenCalledWith('sess_abc'); + }); + + test('sets it on step.started too, so model calls after a parked-turn resume are covered', () => { + const setConversationId = vi.spyOn(SentryCore, 'setConversationId').mockImplementation(() => undefined); + + eveConversationHook().events['step.started'](undefined, { session: { id: 'sess_resumed' } }); + + expect(setConversationId).toHaveBeenCalledWith('sess_resumed'); + }); + + test('honors a custom getConversationId', () => { + const setConversationId = vi.spyOn(SentryCore, 'setConversationId').mockImplementation(() => undefined); + + eveConversationHook({ getConversationId: context => `conv-${context.session.id}` }).events['turn.started']( + undefined, + { session: { id: 'xyz' } }, + ); + + expect(setConversationId).toHaveBeenCalledWith('conv-xyz'); + }); + + test('does not set a conversation id when the resolver returns nothing', () => { + const setConversationId = vi.spyOn(SentryCore, 'setConversationId').mockImplementation(() => undefined); + + eveConversationHook({ getConversationId: () => undefined }).events['turn.started'](undefined, { + session: { id: 'xyz' }, + }); + + expect(setConversationId).not.toHaveBeenCalled(); + }); +}); From dabdfbaff7cb2c2594531fc6cb47bd4d9cee9b1c Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Wed, 9 Sep 2026 15:41:24 +0200 Subject: [PATCH 2/6] ref(node): Move `eveConversationHook` to `@sentry/server-utils` and re-export from every runtime SDK MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- packages/aws-serverless/src/index.ts | 2 ++ packages/bun/src/index.ts | 2 ++ packages/cloudflare/src/index.ts | 2 ++ packages/deno/src/index.ts | 2 ++ packages/google-cloud-serverless/src/index.ts | 2 ++ packages/node/src/index.ts | 2 +- packages/{node => server-utils}/src/eve.ts | 4 ++-- packages/server-utils/src/index.ts | 1 + packages/{node => server-utils}/test/eve.test.ts | 0 9 files changed, 14 insertions(+), 3 deletions(-) rename packages/{node => server-utils}/src/eve.ts (92%) rename packages/{node => server-utils}/test/eve.test.ts (100%) diff --git a/packages/aws-serverless/src/index.ts b/packages/aws-serverless/src/index.ts index aa5aaaf33cb2..9c7c7e498b21 100644 --- a/packages/aws-serverless/src/index.ts +++ b/packages/aws-serverless/src/index.ts @@ -170,6 +170,8 @@ export { withStaticSpan, // oxlint-disable-next-line typescript/no-deprecated withStreamedSpan, + eveConversationHook, + type EveConversationHookOptions, } from '@sentry/node'; export { diff --git a/packages/bun/src/index.ts b/packages/bun/src/index.ts index fe427aa97424..2d342f83015e 100644 --- a/packages/bun/src/index.ts +++ b/packages/bun/src/index.ts @@ -187,6 +187,8 @@ export { withStaticSpan, // oxlint-disable-next-line typescript/no-deprecated withStreamedSpan, + eveConversationHook, + type EveConversationHookOptions, } from '@sentry/node'; export { diff --git a/packages/cloudflare/src/index.ts b/packages/cloudflare/src/index.ts index e347cbc9fdab..214b484ab739 100644 --- a/packages/cloudflare/src/index.ts +++ b/packages/cloudflare/src/index.ts @@ -134,6 +134,8 @@ export { instrumentStateGraph, instrumentCreateReactAgent, vercelAIIntegration, + eveConversationHook, + type EveConversationHookOptions, } from '@sentry/server-utils'; export { instrumentWorkflowWithSentry } from './workflows'; diff --git a/packages/deno/src/index.ts b/packages/deno/src/index.ts index 56840b35f654..daf2d7c5acdc 100644 --- a/packages/deno/src/index.ts +++ b/packages/deno/src/index.ts @@ -144,6 +144,8 @@ export { postgresIntegration, postgresJsIntegration, tediousIntegration, + eveConversationHook, + type EveConversationHookOptions, } from '@sentry/server-utils'; export { openTelemetryIntegration, getOtlpTracesEndpoint } from '@sentry/server-utils/no-diagnostic-channels'; // Deprecated aliases kept for back-compat. Each forwards to the shared diff --git a/packages/google-cloud-serverless/src/index.ts b/packages/google-cloud-serverless/src/index.ts index 0540b2e6879b..7db11bfa1ad8 100644 --- a/packages/google-cloud-serverless/src/index.ts +++ b/packages/google-cloud-serverless/src/index.ts @@ -170,6 +170,8 @@ export { withStaticSpan, // oxlint-disable-next-line typescript/no-deprecated withStreamedSpan, + eveConversationHook, + type EveConversationHookOptions, } from '@sentry/node'; export { diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts index 20c3ddcc264d..9a57164be310 100644 --- a/packages/node/src/index.ts +++ b/packages/node/src/index.ts @@ -216,7 +216,7 @@ export { makeNodeTransport } from './transports'; export { createGetModuleFromFilename } from './utils/module'; export { SENTRY_SEGMENT_NAME_SOURCE } from '@sentry/conventions/attributes'; -export { eveConversationHook, type EveConversationHookOptions } from './eve'; +export { eveConversationHook, type EveConversationHookOptions } from '@sentry/server-utils'; export { httpServerIntegration } from './integrations/http/httpServerIntegration'; export { httpServerSpansIntegration } from './integrations/http/httpServerSpansIntegration'; export { processSessionIntegration } from './integrations/processSession'; diff --git a/packages/node/src/eve.ts b/packages/server-utils/src/eve.ts similarity index 92% rename from packages/node/src/eve.ts rename to packages/server-utils/src/eve.ts index 0ebe23452738..9cc59aa81878 100644 --- a/packages/node/src/eve.ts +++ b/packages/server-utils/src/eve.ts @@ -2,8 +2,8 @@ import { setConversationId } from '@sentry/core'; /** * The subset of eve's hook context (`HookContext` from `eve/hooks`) this helper reads. Typed - * structurally rather than importing from `eve`, so `@sentry/node` carries no dependency on the - * framework — the shape is checked at the `defineHook(...)` call site in the user's app instead. + * structurally rather than importing from `eve`, so the SDK carries no dependency on the framework — + * the shape is checked at the `defineHook(...)` call site in the user's app instead. */ interface EveHookContext { session: { id: string }; diff --git a/packages/server-utils/src/index.ts b/packages/server-utils/src/index.ts index 2f918e2d5422..92ae938a3167 100644 --- a/packages/server-utils/src/index.ts +++ b/packages/server-utils/src/index.ts @@ -13,6 +13,7 @@ export type { InstrumentationConfig } from './orchestrion/apmTypes'; // `orchestrion/bundler/moduleInjectedTransform.ts`); it is a plain runtime // helper with no orchestrion build-time dependency. export { orchestrionModuleInjected } from './utils/moduleInjected'; +export { eveConversationHook, type EveConversationHookOptions } from './eve'; export { fastifyIntegration, // oxlint-disable-next-line typescript/no-deprecated diff --git a/packages/node/test/eve.test.ts b/packages/server-utils/test/eve.test.ts similarity index 100% rename from packages/node/test/eve.test.ts rename to packages/server-utils/test/eve.test.ts From 6aad2447adef1b39da6c61a76b10690892fcccd7 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Wed, 9 Sep 2026 15:43:44 +0200 Subject: [PATCH 3/6] ref: Keep EveConversationHookOptions internal instead of exporting it 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) --- packages/aws-serverless/src/index.ts | 1 - packages/bun/src/index.ts | 1 - packages/cloudflare/src/index.ts | 1 - packages/deno/src/index.ts | 1 - packages/google-cloud-serverless/src/index.ts | 1 - packages/node/src/index.ts | 2 +- packages/server-utils/src/eve.ts | 2 +- packages/server-utils/src/index.ts | 2 +- 8 files changed, 3 insertions(+), 8 deletions(-) diff --git a/packages/aws-serverless/src/index.ts b/packages/aws-serverless/src/index.ts index 9c7c7e498b21..e8077a9782b1 100644 --- a/packages/aws-serverless/src/index.ts +++ b/packages/aws-serverless/src/index.ts @@ -171,7 +171,6 @@ export { // oxlint-disable-next-line typescript/no-deprecated withStreamedSpan, eveConversationHook, - type EveConversationHookOptions, } from '@sentry/node'; export { diff --git a/packages/bun/src/index.ts b/packages/bun/src/index.ts index 2d342f83015e..4daeac999485 100644 --- a/packages/bun/src/index.ts +++ b/packages/bun/src/index.ts @@ -188,7 +188,6 @@ export { // oxlint-disable-next-line typescript/no-deprecated withStreamedSpan, eveConversationHook, - type EveConversationHookOptions, } from '@sentry/node'; export { diff --git a/packages/cloudflare/src/index.ts b/packages/cloudflare/src/index.ts index 214b484ab739..a1043149b427 100644 --- a/packages/cloudflare/src/index.ts +++ b/packages/cloudflare/src/index.ts @@ -135,7 +135,6 @@ export { instrumentCreateReactAgent, vercelAIIntegration, eveConversationHook, - type EveConversationHookOptions, } from '@sentry/server-utils'; export { instrumentWorkflowWithSentry } from './workflows'; diff --git a/packages/deno/src/index.ts b/packages/deno/src/index.ts index daf2d7c5acdc..bac6b216fb07 100644 --- a/packages/deno/src/index.ts +++ b/packages/deno/src/index.ts @@ -145,7 +145,6 @@ export { postgresJsIntegration, tediousIntegration, eveConversationHook, - type EveConversationHookOptions, } from '@sentry/server-utils'; export { openTelemetryIntegration, getOtlpTracesEndpoint } from '@sentry/server-utils/no-diagnostic-channels'; // Deprecated aliases kept for back-compat. Each forwards to the shared diff --git a/packages/google-cloud-serverless/src/index.ts b/packages/google-cloud-serverless/src/index.ts index 7db11bfa1ad8..f8d6659f24d7 100644 --- a/packages/google-cloud-serverless/src/index.ts +++ b/packages/google-cloud-serverless/src/index.ts @@ -171,7 +171,6 @@ export { // oxlint-disable-next-line typescript/no-deprecated withStreamedSpan, eveConversationHook, - type EveConversationHookOptions, } from '@sentry/node'; export { diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts index 9a57164be310..5ac0d207e455 100644 --- a/packages/node/src/index.ts +++ b/packages/node/src/index.ts @@ -216,7 +216,7 @@ export { makeNodeTransport } from './transports'; export { createGetModuleFromFilename } from './utils/module'; export { SENTRY_SEGMENT_NAME_SOURCE } from '@sentry/conventions/attributes'; -export { eveConversationHook, type EveConversationHookOptions } from '@sentry/server-utils'; +export { eveConversationHook } from '@sentry/server-utils'; export { httpServerIntegration } from './integrations/http/httpServerIntegration'; export { httpServerSpansIntegration } from './integrations/http/httpServerSpansIntegration'; export { processSessionIntegration } from './integrations/processSession'; diff --git a/packages/server-utils/src/eve.ts b/packages/server-utils/src/eve.ts index 9cc59aa81878..0dafe50993db 100644 --- a/packages/server-utils/src/eve.ts +++ b/packages/server-utils/src/eve.ts @@ -11,7 +11,7 @@ interface EveHookContext { type EveHookHandler = (event: unknown, context: EveHookContext) => void; -export interface EveConversationHookOptions { +interface EveConversationHookOptions { /** * Derive the Sentry conversation id from the eve hook context. Defaults to the durable session id * (`ctx.session.id`), which is stable across every turn of a session and so groups them into one diff --git a/packages/server-utils/src/index.ts b/packages/server-utils/src/index.ts index 92ae938a3167..9a1520c560c8 100644 --- a/packages/server-utils/src/index.ts +++ b/packages/server-utils/src/index.ts @@ -13,7 +13,7 @@ export type { InstrumentationConfig } from './orchestrion/apmTypes'; // `orchestrion/bundler/moduleInjectedTransform.ts`); it is a plain runtime // helper with no orchestrion build-time dependency. export { orchestrionModuleInjected } from './utils/moduleInjected'; -export { eveConversationHook, type EveConversationHookOptions } from './eve'; +export { eveConversationHook } from './eve'; export { fastifyIntegration, // oxlint-disable-next-line typescript/no-deprecated From 7abe9a8aafca6813b5584c59f1eec23b3d896c6f Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Wed, 9 Sep 2026 16:51:03 +0200 Subject: [PATCH 4/6] feat(astro): Re-export eveConversationHook 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) --- packages/astro/src/index.server.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/astro/src/index.server.ts b/packages/astro/src/index.server.ts index 030d150878d7..2ff55bb1b8b8 100644 --- a/packages/astro/src/index.server.ts +++ b/packages/astro/src/index.server.ts @@ -184,6 +184,7 @@ export { // oxlint-disable-next-line typescript/no-deprecated withStreamedSpan, metrics, + eveConversationHook, } from '@sentry/node'; export { init } from './server/sdk'; From 4bf787524340f012f259328bdc60f70a123329c7 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Thu, 10 Sep 2026 13:59:59 +0200 Subject: [PATCH 5/6] fix conversation id setting --- packages/server-utils/src/eve.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/packages/server-utils/src/eve.ts b/packages/server-utils/src/eve.ts index 0dafe50993db..8ab70981f41b 100644 --- a/packages/server-utils/src/eve.ts +++ b/packages/server-utils/src/eve.ts @@ -17,7 +17,7 @@ interface EveConversationHookOptions { * (`ctx.session.id`), which is stable across every turn of a session and so groups them into one * conversation. */ - getConversationId?: (context: EveHookContext) => string | undefined; + getConversationId?: (context: EveHookContext) => string | null | undefined; } /** @@ -46,13 +46,11 @@ interface EveConversationHookOptions { export function eveConversationHook(options: EveConversationHookOptions = {}): { events: Record<'turn.started' | 'step.started', EveHookHandler>; } { - const resolveConversationId = options.getConversationId ?? (context => context.session.id); + const { getConversationId } = options; const setConversationIdFromContext: EveHookHandler = (_event, context) => { - const conversationId = resolveConversationId(context); - if (conversationId) { - setConversationId(conversationId); - } + const conversationId = getConversationId ? getConversationId(context) : context.session.id; + setConversationId(conversationId); }; return { From 611068072c4dc34214e394b2bf1438b4de257943 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Thu, 10 Sep 2026 14:07:27 +0200 Subject: [PATCH 6/6] update test --- packages/server-utils/test/eve.test.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/server-utils/test/eve.test.ts b/packages/server-utils/test/eve.test.ts index a4ce01c64a39..2212f7b8e387 100644 --- a/packages/server-utils/test/eve.test.ts +++ b/packages/server-utils/test/eve.test.ts @@ -40,13 +40,16 @@ describe('eveConversationHook', () => { expect(setConversationId).toHaveBeenCalledWith('conv-xyz'); }); - test('does not set a conversation id when the resolver returns nothing', () => { + test.each([ + ['undefined', undefined], + ['null', null], + ])('unsets the conversation id when the resolver returns %s', (_label, returnValue) => { const setConversationId = vi.spyOn(SentryCore, 'setConversationId').mockImplementation(() => undefined); - eveConversationHook({ getConversationId: () => undefined }).events['turn.started'](undefined, { + eveConversationHook({ getConversationId: () => returnValue }).events['turn.started'](undefined, { session: { id: 'xyz' }, }); - expect(setConversationId).not.toHaveBeenCalled(); + expect(setConversationId).toHaveBeenCalledWith(returnValue); }); });