Skip to content

Commit c8daa12

Browse files
committed
Use conventions constants in Mastra exporter tests
1 parent f43962d commit c8daa12

1 file changed

Lines changed: 85 additions & 52 deletions

File tree

‎packages/server-utils/test/ai/lib/tracing/mastra-exporter.test.ts‎

Lines changed: 85 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,37 @@
11
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
2+
import {
3+
ERROR_TYPE,
4+
GEN_AI_AGENT_NAME,
5+
GEN_AI_CONVERSATION_ID,
6+
GEN_AI_INPUT_MESSAGES,
7+
GEN_AI_OPERATION_NAME,
8+
GEN_AI_OUTPUT_MESSAGES,
9+
GEN_AI_PIPELINE_NAME,
10+
GEN_AI_PROVIDER_NAME,
11+
GEN_AI_REQUEST_MODEL,
12+
GEN_AI_REQUEST_STOP_SEQUENCES,
13+
GEN_AI_RESPONSE_MODEL,
14+
GEN_AI_RESPONSE_TEXT,
15+
GEN_AI_RESPONSE_TOOL_CALLS,
16+
GEN_AI_SYSTEM_INSTRUCTIONS,
17+
GEN_AI_TOOL_CALL_ARGUMENTS,
18+
GEN_AI_TOOL_NAME,
19+
GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS,
20+
GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS,
21+
GEN_AI_USAGE_INPUT_TOKENS,
22+
GEN_AI_USAGE_OUTPUT_TOKENS,
23+
GEN_AI_USAGE_REASONING_OUTPUT_TOKENS,
24+
GEN_AI_USAGE_TOTAL_TOKENS,
25+
SENTRY_OP,
26+
SENTRY_ORIGIN,
27+
} from '@sentry/conventions/attributes';
28+
import {
29+
GEN_AI_CHAT,
30+
GEN_AI_EMBEDDINGS,
31+
GEN_AI_EVALUATE,
32+
GEN_AI_EXECUTE_TOOL,
33+
GEN_AI_INVOKE_AGENT,
34+
} from '@sentry/conventions/op';
235
import type { Span } from '@sentry/core';
336
import {
437
_INTERNAL_clearAiProviderSkips,
@@ -81,11 +114,11 @@ describe('SentryMastraExporter', () => {
81114

82115
const json = spanToStaticSpanJSON(endedSpans[0]!);
83116
expect(json.description).toBe('invoke_agent weather_agent');
84-
expect(json.data['sentry.op']).toBe('gen_ai.invoke_agent');
85-
expect(json.data['sentry.origin']).toBe('auto.ai.mastra');
86-
expect(json.data['gen_ai.operation.name']).toBe('invoke_agent');
87-
expect(json.data['gen_ai.agent.name']).toBe('weather_agent');
88-
expect(json.data['gen_ai.pipeline.name']).toBe('weather_agent');
117+
expect(json.data[SENTRY_OP]).toBe(GEN_AI_INVOKE_AGENT);
118+
expect(json.data[SENTRY_ORIGIN]).toBe('auto.ai.mastra');
119+
expect(json.data[GEN_AI_OPERATION_NAME]).toBe('invoke_agent');
120+
expect(json.data[GEN_AI_AGENT_NAME]).toBe('weather_agent');
121+
expect(json.data[GEN_AI_PIPELINE_NAME]).toBe('weather_agent');
89122
});
90123

91124
it('names generation spans `chat {model}` and maps usage to the current conventions', async () => {
@@ -108,17 +141,17 @@ describe('SentryMastraExporter', () => {
108141

109142
const json = spanToStaticSpanJSON(endedSpans[0]!);
110143
expect(json.description).toBe('chat gpt-5');
111-
expect(json.data['sentry.op']).toBe('gen_ai.chat');
112-
expect(json.data['gen_ai.provider.name']).toBe('openai');
113-
expect(json.data['gen_ai.usage.input_tokens']).toBe(10);
114-
expect(json.data['gen_ai.usage.output_tokens']).toBe(4);
115-
expect(json.data['gen_ai.usage.total_tokens']).toBe(14);
116-
expect(json.data['gen_ai.usage.reasoning.output_tokens']).toBe(3);
117-
expect(json.data['gen_ai.usage.cache_read.input_tokens']).toBe(2);
118-
expect(json.data['gen_ai.usage.cache_creation.input_tokens']).toBe(5);
144+
expect(json.data[SENTRY_OP]).toBe(GEN_AI_CHAT);
145+
expect(json.data[GEN_AI_PROVIDER_NAME]).toBe('openai');
146+
expect(json.data[GEN_AI_USAGE_INPUT_TOKENS]).toBe(10);
147+
expect(json.data[GEN_AI_USAGE_OUTPUT_TOKENS]).toBe(4);
148+
expect(json.data[GEN_AI_USAGE_TOTAL_TOKENS]).toBe(14);
149+
expect(json.data[GEN_AI_USAGE_REASONING_OUTPUT_TOKENS]).toBe(3);
150+
expect(json.data[GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS]).toBe(2);
151+
expect(json.data[GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS]).toBe(5);
119152
expect(json.data['gen_ai.usage.reasoning_tokens']).toBeUndefined();
120-
expect(json.data['gen_ai.response.text']).toBeUndefined();
121-
expect(json.data['gen_ai.response.tool_calls']).toBeUndefined();
153+
expect(json.data[GEN_AI_RESPONSE_TEXT]).toBeUndefined();
154+
expect(json.data[GEN_AI_RESPONSE_TOOL_CALLS]).toBeUndefined();
122155
});
123156

124157
it('maps classifier evaluations to `evaluate {model}` spans', async () => {
@@ -132,14 +165,14 @@ describe('SentryMastraExporter', () => {
132165
const json = spanToStaticSpanJSON(endedSpans[0]!);
133166
expect(json.description).toBe('evaluate jev-1');
134167
expect(json.data).toMatchObject({
135-
'sentry.op': 'gen_ai.evaluate',
136-
'sentry.origin': 'auto.ai.mastra',
137-
'gen_ai.operation.name': 'evaluate',
138-
'gen_ai.request.model': 'jev-1',
139-
'gen_ai.provider.name': 'typesafe.evaluation',
140-
'gen_ai.usage.input_tokens': 30,
141-
'gen_ai.usage.output_tokens': 2,
142-
'gen_ai.usage.total_tokens': 32,
168+
[SENTRY_OP]: GEN_AI_EVALUATE,
169+
[SENTRY_ORIGIN]: 'auto.ai.mastra',
170+
[GEN_AI_OPERATION_NAME]: 'evaluate',
171+
[GEN_AI_REQUEST_MODEL]: 'jev-1',
172+
[GEN_AI_PROVIDER_NAME]: 'typesafe.evaluation',
173+
[GEN_AI_USAGE_INPUT_TOKENS]: 30,
174+
[GEN_AI_USAGE_OUTPUT_TOKENS]: 2,
175+
[GEN_AI_USAGE_TOTAL_TOKENS]: 32,
143176
});
144177
});
145178

@@ -153,10 +186,10 @@ describe('SentryMastraExporter', () => {
153186
await run(started(span), ended(span));
154187

155188
const json = spanToStaticSpanJSON(endedSpans[0]!);
156-
expect(json.data['gen_ai.input.messages']).toBe('[{"role":"user","content":"hi"}]');
157-
expect(json.data['gen_ai.output.messages']).toBe('{"text":"hello"}');
158-
expect(json.data['gen_ai.response.text']).toBe('hello');
159-
expect(json.data['gen_ai.system_instructions']).toBe('be brief');
189+
expect(json.data[GEN_AI_INPUT_MESSAGES]).toBe('[{"role":"user","content":"hi"}]');
190+
expect(json.data[GEN_AI_OUTPUT_MESSAGES]).toBe('{"text":"hello"}');
191+
expect(json.data[GEN_AI_RESPONSE_TEXT]).toBe('hello');
192+
expect(json.data[GEN_AI_SYSTEM_INSTRUCTIONS]).toBe('be brief');
160193
});
161194

162195
it.each([
@@ -172,36 +205,36 @@ describe('SentryMastraExporter', () => {
172205
});
173206
await run(started(span), ended(span));
174207

175-
expect(spanToStaticSpanJSON(endedSpans[0]!).data['gen_ai.response.text']).toBe(expected);
208+
expect(spanToStaticSpanJSON(endedSpans[0]!).data[GEN_AI_RESPONSE_TEXT]).toBe(expected);
176209
});
177210

178211
it.each([
179212
[
180213
'workflow_run',
181214
{ type: 'workflow_run' as const, entityName: 'math_workflow' },
182215
'invoke_agent math_workflow',
183-
'gen_ai.invoke_agent',
216+
GEN_AI_INVOKE_AGENT,
184217
],
185218
[
186219
'rag_embedding',
187220
{ type: 'rag_embedding' as const, attributes: { model: 'text-embedding-3' } },
188221
'embeddings text-embedding-3',
189-
'gen_ai.embeddings',
222+
GEN_AI_EMBEDDINGS,
190223
],
191224
[
192225
'mcp_tool_call',
193226
{ type: 'mcp_tool_call' as const, entityName: 'search' },
194227
'execute_tool search',
195-
'gen_ai.execute_tool',
228+
GEN_AI_EXECUTE_TOOL,
196229
],
197-
['entityId when entityName is missing', { entityId: 'agent-42' }, 'invoke_agent agent-42', 'gen_ai.invoke_agent'],
230+
['entityId when entityName is missing', { entityId: 'agent-42' }, 'invoke_agent agent-42', GEN_AI_INVOKE_AGENT],
198231
])('names a %s span', async (_label, overrides, description, op) => {
199232
const span = makeSpan(overrides);
200233
await run(started(span), ended(span));
201234

202235
const json = spanToStaticSpanJSON(endedSpans[0]!);
203236
expect(json.description).toBe(description);
204-
expect(json.data['sentry.op']).toBe(op);
237+
expect(json.data[SENTRY_OP]).toBe(op);
205238
});
206239

207240
it('omits inputs and outputs when recording is disabled', async () => {
@@ -210,8 +243,8 @@ describe('SentryMastraExporter', () => {
210243
await run(started(span), ended(span));
211244

212245
const json = spanToStaticSpanJSON(endedSpans[0]!);
213-
expect(json.data['gen_ai.input.messages']).toBeUndefined();
214-
expect(json.data['gen_ai.output.messages']).toBeUndefined();
246+
expect(json.data[GEN_AI_INPUT_MESSAGES]).toBeUndefined();
247+
expect(json.data[GEN_AI_OUTPUT_MESSAGES]).toBeUndefined();
215248
expect(JSON.stringify(json.data)).not.toContain('pii');
216249
});
217250

@@ -248,9 +281,9 @@ describe('SentryMastraExporter', () => {
248281
const chatJson = json.find(span => span.description === 'chat gpt-5');
249282
expect(toolJson).toBeDefined();
250283
expect(chatJson).toBeDefined();
251-
expect(toolJson!.data['sentry.op']).toBe('gen_ai.execute_tool');
252-
expect(toolJson!.data['gen_ai.tool.name']).toBe('get_weather');
253-
expect(toolJson!.data['gen_ai.tool.call.arguments']).toBe('{"city":"Berlin"}');
284+
expect(toolJson!.data[SENTRY_OP]).toBe(GEN_AI_EXECUTE_TOOL);
285+
expect(toolJson!.data[GEN_AI_TOOL_NAME]).toBe('get_weather');
286+
expect(toolJson!.data[GEN_AI_TOOL_CALL_ARGUMENTS]).toBe('{"city":"Berlin"}');
254287
expect(toolJson!.parent_span_id).toBe(chatJson!.span_id);
255288
});
256289

@@ -273,10 +306,10 @@ describe('SentryMastraExporter', () => {
273306
.map(span => spanToStaticSpanJSON(span))
274307
.find(span => span.description === 'invoke_agent agent');
275308
expect(agentJson).toBeDefined();
276-
expect(agentJson!.data['gen_ai.usage.input_tokens']).toBe(10);
277-
expect(agentJson!.data['gen_ai.usage.output_tokens']).toBe(4);
278-
expect(agentJson!.data['gen_ai.usage.total_tokens']).toBe(14);
279-
expect(agentJson!.data['gen_ai.response.model']).toBe('gpt-5-2026');
309+
expect(agentJson!.data[GEN_AI_USAGE_INPUT_TOKENS]).toBe(10);
310+
expect(agentJson!.data[GEN_AI_USAGE_OUTPUT_TOKENS]).toBe(4);
311+
expect(agentJson!.data[GEN_AI_USAGE_TOTAL_TOKENS]).toBe(14);
312+
expect(agentJson!.data[GEN_AI_RESPONSE_MODEL]).toBe('gpt-5-2026');
280313
});
281314

282315
it('sums usage across multiple generations onto the parent agent', async () => {
@@ -307,11 +340,11 @@ describe('SentryMastraExporter', () => {
307340
.map(span => spanToStaticSpanJSON(span))
308341
.find(span => span.description === 'invoke_agent agent');
309342
expect(agentJson).toBeDefined();
310-
expect(agentJson!.data['gen_ai.usage.input_tokens']).toBe(40);
311-
expect(agentJson!.data['gen_ai.usage.output_tokens']).toBe(12);
312-
expect(agentJson!.data['gen_ai.usage.total_tokens']).toBe(52);
313-
expect(agentJson!.data['gen_ai.usage.cache_read.input_tokens']).toBe(3);
314-
expect(agentJson!.data['gen_ai.response.model']).toBe('gpt-5-2026');
343+
expect(agentJson!.data[GEN_AI_USAGE_INPUT_TOKENS]).toBe(40);
344+
expect(agentJson!.data[GEN_AI_USAGE_OUTPUT_TOKENS]).toBe(12);
345+
expect(agentJson!.data[GEN_AI_USAGE_TOTAL_TOKENS]).toBe(52);
346+
expect(agentJson!.data[GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS]).toBe(3);
347+
expect(agentJson!.data[GEN_AI_RESPONSE_MODEL]).toBe('gpt-5-2026');
315348
});
316349

317350
it('marks the span errored without capturing a reconstructed exception', async () => {
@@ -320,7 +353,7 @@ describe('SentryMastraExporter', () => {
320353

321354
const json = spanToStaticSpanJSON(endedSpans[0]!);
322355
expect(json.status).toBe('internal_error');
323-
expect(json.data['error.type']).toBe('ToolError');
356+
expect(json.data[ERROR_TYPE]).toBe('ToolError');
324357
});
325358

326359
it.each([
@@ -356,7 +389,7 @@ describe('SentryMastraExporter', () => {
356389

357390
const json = spanToStaticSpanJSON(endedSpans[0]!);
358391
expect(Object.keys(json.data).filter(key => key.startsWith('mastra.'))).toEqual([]);
359-
expect(json.data['gen_ai.conversation.id']).toBe('thread-9');
392+
expect(json.data[GEN_AI_CONVERSATION_ID]).toBe('thread-9');
360393
});
361394

362395
it.each([
@@ -370,14 +403,14 @@ describe('SentryMastraExporter', () => {
370403
const span = makeSpan({ entityName: 'agent', ...overrides });
371404
await run(started(span), ended(span));
372405

373-
expect(spanToStaticSpanJSON(endedSpans[0]!).data['gen_ai.conversation.id']).toBe(expected);
406+
expect(spanToStaticSpanJSON(endedSpans[0]!).data[GEN_AI_CONVERSATION_ID]).toBe(expected);
374407
});
375408

376409
it('does not use runId as gen_ai.conversation.id', async () => {
377410
const span = makeSpan({ entityName: 'agent', metadata: { runId: 'run-3' } });
378411
await run(started(span), ended(span));
379412

380-
expect(spanToStaticSpanJSON(endedSpans[0]!).data['gen_ai.conversation.id']).toBeUndefined();
413+
expect(spanToStaticSpanJSON(endedSpans[0]!).data[GEN_AI_CONVERSATION_ID]).toBeUndefined();
381414
});
382415

383416
it('falls back to the bare operation name when there is no identifier', async () => {
@@ -433,7 +466,7 @@ describe('SentryMastraExporter', () => {
433466
});
434467
await run(started(span), ended(span));
435468

436-
expect(spanToStaticSpanJSON(endedSpans[0]!).data['gen_ai.request.stop_sequences']).toEqual(['\n\n', 'END']);
469+
expect(spanToStaticSpanJSON(endedSpans[0]!).data[GEN_AI_REQUEST_STOP_SEQUENCES]).toEqual(['\n\n', 'END']);
437470
});
438471

439472
// Parentless dropped spans used to be stored under a falsy sentinel that `LRUMap.remove` never

0 commit comments

Comments
 (0)