Skip to content

Commit b2edcf8

Browse files
mydeaclaude
andauthored
test(e2e/node-eve): Assert a manual span nests under gen_ai.execute_tool (#24330)
Extends the `node-eve` e2e app so a tool call (`get_weather`) wraps its work in a manual `Sentry.startSpan`, then asserts the resulting user span lands as a direct child of the SDK's `gen_ai.execute_tool` span (same trace, `parent_span_id` = the tool span's `span_id`). The point is to prove that manual instrumentation added inside a tool slots into the active gen_ai context rather than floating off as its own segment — i.e. eve runs `execute` while the tool span is active. We fold the check into the existing weather turn instead of adding a new agent turn to avoid a second LLM call. Stacked on #24254. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 5cded57 commit b2edcf8

2 files changed

Lines changed: 19 additions & 2 deletions

File tree

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,17 @@
1+
import * as Sentry from '@sentry/node';
12
import { defineTool } from 'eve/tools';
23
import { z } from 'zod';
34

45
export default defineTool({
56
description: 'Get the current weather for a city.',
67
inputSchema: z.object({ city: z.string().min(1) }),
78
async execute({ city }) {
8-
return { city, condition: 'Sunny', temperatureC: 22 };
9+
// Manual instrumentation inside a tool call: eve runs `execute` while the
10+
// SDK's `gen_ai.execute_tool` span is active, so this user span should nest
11+
// under it. The e2e test asserts that parent/child link.
12+
return Sentry.startSpan(
13+
{ name: 'resolve-weather', op: 'gen_ai.tool.manual', attributes: { 'weather.city': city } },
14+
() => ({ city, condition: 'Sunny', temperatureC: 22 }),
15+
);
916
},
1017
});

‎dev-packages/e2e-tests/test-applications/node-eve/tests/eve.test.ts‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ test('captures Vercel AI agent spans (invoke_agent, generate_content, execute_to
2929
const traceSpansPromise = collectStreamedSpans(
3030
APP,
3131
spansOfTrace =>
32-
['gen_ai.invoke_agent', 'gen_ai.generate_content', 'gen_ai.execute_tool'].every(op =>
32+
['gen_ai.invoke_agent', 'gen_ai.generate_content', 'gen_ai.execute_tool', 'gen_ai.tool.manual'].every(op =>
3333
spansOfTrace.some(span => getSpanOp(span) === op),
3434
) && spansOfTrace.some(isAgentServerSpan),
3535
);
@@ -66,6 +66,16 @@ test('captures Vercel AI agent spans (invoke_agent, generate_content, execute_to
6666
// The tool returns `{ city, condition: 'Sunny', temperatureC: 22 }`.
6767
expect(executeTool?.attributes?.['gen_ai.tool.call.result']?.value).toContain('Sunny');
6868

69+
// `get_weather` wraps its work in a manual `Sentry.startSpan`. Because eve runs
70+
// the tool while the SDK's `execute_tool` span is active, that user span nests
71+
// directly under it — this is the manual-instrumentation-inside-a-tool case.
72+
const manualSpan = traceSpans.find(span => getSpanOp(span) === 'gen_ai.tool.manual');
73+
expect(manualSpan?.name).toBe('resolve-weather');
74+
expect(manualSpan?.attributes?.['weather.city']?.value).toBe('Paris');
75+
expect(manualSpan?.is_segment).toBe(false);
76+
expect(manualSpan?.trace_id).toBe(executeTool?.trace_id);
77+
expect(manualSpan?.parent_span_id).toBe(executeTool?.span_id);
78+
6979
// `agent/hooks/sentry.ts` sets the eve session id as the conversation id via
7080
// `Sentry.eveConversationHook()`, so every gen_ai span in the turn is tagged with it — that is
7181
// what links a multi-turn session (each turn is its own trace) into one Sentry conversation.

0 commit comments

Comments
 (0)