11import { 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' ;
235import type { Span } from '@sentry/core' ;
336import {
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