8
8
* and users have to manually these to get spans.
9
9
*/
10
10
11
- /* eslint-disable @typescript-eslint/no-dynamic-delete */
12
- /* eslint-disable complexity */
13
- import type { Client , IntegrationFn } from '@sentry/core' ;
14
- import { defineIntegration , SEMANTIC_ATTRIBUTE_SENTRY_OP , spanToJSON } from '@sentry/core' ;
15
- import { addOriginToSpan } from '../../../utils/addOriginToSpan' ;
11
+ import type { Client , IntegrationFn } from '@sentry/core' ;
12
+ import { defineIntegration , processVercelAiSpan } from '@sentry/core' ;
16
13
import type { modulesIntegration } from '../../modules' ;
17
- import {
18
- AI_MODEL_ID_ATTRIBUTE ,
19
- AI_MODEL_PROVIDER_ATTRIBUTE ,
20
- AI_PROMPT_ATTRIBUTE ,
21
- AI_PROMPT_MESSAGES_ATTRIBUTE ,
22
- AI_PROMPT_TOOLS_ATTRIBUTE ,
23
- AI_RESPONSE_TEXT_ATTRIBUTE ,
24
- AI_RESPONSE_TOOL_CALLS_ATTRIBUTE ,
25
- AI_TELEMETRY_FUNCTION_ID_ATTRIBUTE ,
26
- AI_TOOL_CALL_ID_ATTRIBUTE ,
27
- AI_TOOL_CALL_NAME_ATTRIBUTE ,
28
- AI_USAGE_COMPLETION_TOKENS_ATTRIBUTE ,
29
- AI_USAGE_PROMPT_TOKENS_ATTRIBUTE ,
30
- GEN_AI_RESPONSE_MODEL_ATTRIBUTE ,
31
- GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE ,
32
- GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE ,
33
- } from './ai_sdk_attributes' ;
34
14
import { INTEGRATION_NAME } from './constants' ;
35
15
import type { VercelAiOptions } from './types' ;
36
16
37
17
/**
38
18
* Determines if the integration should be forced based on environment and package availability.
39
19
* Returns true if the 'ai' package is available.
40
20
*/
41
- function shouldForceIntegration ( client : Client ) : boolean {
21
+ function shouldRunIntegration ( client : Client ) : boolean {
42
22
const modules = client . getIntegrationByName < ReturnType < typeof modulesIntegration > > ( 'Modules' ) ;
43
23
return ! ! modules ?. getModules ?.( ) ?. ai ;
44
24
}
@@ -47,173 +27,12 @@ const _vercelAIIntegration = ((options: VercelAiOptions = {}) => {
47
27
return {
48
28
name : INTEGRATION_NAME ,
49
29
options,
50
- afterAllSetup ( client ) {
30
+ setup ( client ) {
51
31
function registerProcessors ( ) : void {
52
- client . on ( 'spanStart' , span => {
53
- const { data : attributes , description : name } = spanToJSON ( span ) ;
54
-
55
- if ( ! name ) {
56
- return ;
57
- }
58
-
59
- // Tool call spans
60
- // https://ai-sdk.dev/docs/ai-sdk-core/telemetry#tool-call-spans
61
- if (
62
- attributes [ AI_TOOL_CALL_NAME_ATTRIBUTE ] &&
63
- attributes [ AI_TOOL_CALL_ID_ATTRIBUTE ] &&
64
- name === 'ai.toolCall'
65
- ) {
66
- addOriginToSpan ( span , 'auto.vercelai.otel' ) ;
67
- span . setAttribute ( SEMANTIC_ATTRIBUTE_SENTRY_OP , 'gen_ai.execute_tool' ) ;
68
- span . setAttribute ( 'gen_ai.tool.call.id' , attributes [ AI_TOOL_CALL_ID_ATTRIBUTE ] ) ;
69
- span . setAttribute ( 'gen_ai.tool.name' , attributes [ AI_TOOL_CALL_NAME_ATTRIBUTE ] ) ;
70
- span . updateName ( `execute_tool ${ attributes [ AI_TOOL_CALL_NAME_ATTRIBUTE ] } ` ) ;
71
- return ;
72
- }
73
-
74
- // The AI and Provider must be defined for generate, stream, and embed spans.
75
- // The id of the model
76
- const aiModelId = attributes [ AI_MODEL_ID_ATTRIBUTE ] ;
77
- // the provider of the model
78
- const aiModelProvider = attributes [ AI_MODEL_PROVIDER_ATTRIBUTE ] ;
79
- if ( typeof aiModelId !== 'string' || typeof aiModelProvider !== 'string' || ! aiModelId || ! aiModelProvider ) {
80
- return ;
81
- }
82
-
83
- addOriginToSpan ( span , 'auto.vercelai.otel' ) ;
84
-
85
- const nameWthoutAi = name . replace ( 'ai.' , '' ) ;
86
- span . setAttribute ( 'ai.pipeline.name' , nameWthoutAi ) ;
87
- span . updateName ( nameWthoutAi ) ;
88
-
89
- // If a Telemetry name is set and it is a pipeline span, use that as the operation name
90
- const functionId = attributes [ AI_TELEMETRY_FUNCTION_ID_ATTRIBUTE ] ;
91
- if ( functionId && typeof functionId === 'string' && name . split ( '.' ) . length - 1 === 1 ) {
92
- span . updateName ( `${ nameWthoutAi } ${ functionId } ` ) ;
93
- span . setAttribute ( 'ai.pipeline.name' , functionId ) ;
94
- }
95
-
96
- if ( attributes [ AI_PROMPT_ATTRIBUTE ] ) {
97
- span . setAttribute ( 'gen_ai.prompt' , attributes [ AI_PROMPT_ATTRIBUTE ] ) ;
98
- }
99
- if ( attributes [ AI_MODEL_ID_ATTRIBUTE ] && ! attributes [ GEN_AI_RESPONSE_MODEL_ATTRIBUTE ] ) {
100
- span . setAttribute ( GEN_AI_RESPONSE_MODEL_ATTRIBUTE , attributes [ AI_MODEL_ID_ATTRIBUTE ] ) ;
101
- }
102
- span . setAttribute ( 'ai.streaming' , name . includes ( 'stream' ) ) ;
103
-
104
- // Generate Spans
105
- if ( name === 'ai.generateText' ) {
106
- span . setAttribute ( SEMANTIC_ATTRIBUTE_SENTRY_OP , 'gen_ai.invoke_agent' ) ;
107
- return ;
108
- }
109
-
110
- if ( name === 'ai.generateText.doGenerate' ) {
111
- span . setAttribute ( SEMANTIC_ATTRIBUTE_SENTRY_OP , 'gen_ai.generate_text' ) ;
112
- span . updateName ( `generate_text ${ attributes [ AI_MODEL_ID_ATTRIBUTE ] } ` ) ;
113
- return ;
114
- }
115
-
116
- if ( name === 'ai.streamText' ) {
117
- span . setAttribute ( SEMANTIC_ATTRIBUTE_SENTRY_OP , 'gen_ai.invoke_agent' ) ;
118
- return ;
119
- }
120
-
121
- if ( name === 'ai.streamText.doStream' ) {
122
- span . setAttribute ( SEMANTIC_ATTRIBUTE_SENTRY_OP , 'gen_ai.stream_text' ) ;
123
- span . updateName ( `stream_text ${ attributes [ AI_MODEL_ID_ATTRIBUTE ] } ` ) ;
124
- return ;
125
- }
126
-
127
- if ( name === 'ai.generateObject' ) {
128
- span . setAttribute ( SEMANTIC_ATTRIBUTE_SENTRY_OP , 'gen_ai.invoke_agent' ) ;
129
- return ;
130
- }
131
-
132
- if ( name === 'ai.generateObject.doGenerate' ) {
133
- span . setAttribute ( SEMANTIC_ATTRIBUTE_SENTRY_OP , 'gen_ai.generate_object' ) ;
134
- span . updateName ( `generate_object ${ attributes [ AI_MODEL_ID_ATTRIBUTE ] } ` ) ;
135
- return ;
136
- }
137
-
138
- if ( name === 'ai.streamObject' ) {
139
- span . setAttribute ( SEMANTIC_ATTRIBUTE_SENTRY_OP , 'gen_ai.invoke_agent' ) ;
140
- return ;
141
- }
142
-
143
- if ( name === 'ai.streamObject.doStream' ) {
144
- span . setAttribute ( SEMANTIC_ATTRIBUTE_SENTRY_OP , 'gen_ai.stream_object' ) ;
145
- span . updateName ( `stream_object ${ attributes [ AI_MODEL_ID_ATTRIBUTE ] } ` ) ;
146
- return ;
147
- }
148
-
149
- if ( name === 'ai.embed' ) {
150
- span . setAttribute ( SEMANTIC_ATTRIBUTE_SENTRY_OP , 'gen_ai.invoke_agent' ) ;
151
- return ;
152
- }
153
-
154
- if ( name === 'ai.embed.doEmbed' ) {
155
- span . setAttribute ( SEMANTIC_ATTRIBUTE_SENTRY_OP , 'gen_ai.embed' ) ;
156
- span . updateName ( `embed ${ attributes [ AI_MODEL_ID_ATTRIBUTE ] } ` ) ;
157
- return ;
158
- }
159
-
160
- if ( name === 'ai.embedMany' ) {
161
- span . setAttribute ( SEMANTIC_ATTRIBUTE_SENTRY_OP , 'gen_ai.invoke_agent' ) ;
162
- return ;
163
- }
164
-
165
- if ( name === 'ai.embedMany.doEmbed' ) {
166
- span . setAttribute ( SEMANTIC_ATTRIBUTE_SENTRY_OP , 'gen_ai.embed_many' ) ;
167
- span . updateName ( `embed_many ${ attributes [ AI_MODEL_ID_ATTRIBUTE ] } ` ) ;
168
- return ;
169
- }
170
-
171
- if ( name . startsWith ( 'ai.stream' ) ) {
172
- span . setAttribute ( SEMANTIC_ATTRIBUTE_SENTRY_OP , 'ai.run' ) ;
173
- return ;
174
- }
175
- } ) ;
176
-
177
- client . addEventProcessor ( event => {
178
- if ( event . type === 'transaction' && event . spans ?. length ) {
179
- for ( const span of event . spans ) {
180
- const { data : attributes , description : name } = span ;
181
-
182
- if ( ! name || span . origin !== 'auto.vercelai.otel' ) {
183
- continue ;
184
- }
185
-
186
- renameAttributeKey (
187
- attributes ,
188
- AI_USAGE_COMPLETION_TOKENS_ATTRIBUTE ,
189
- GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE ,
190
- ) ;
191
- renameAttributeKey ( attributes , AI_USAGE_PROMPT_TOKENS_ATTRIBUTE , GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE ) ;
192
- if (
193
- typeof attributes [ GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE ] === 'number' &&
194
- typeof attributes [ GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE ] === 'number'
195
- ) {
196
- attributes [ 'gen_ai.usage.total_tokens' ] =
197
- attributes [ GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE ] + attributes [ GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE ] ;
198
- }
199
-
200
- // Rename AI SDK attributes to standardized gen_ai attributes
201
- renameAttributeKey ( attributes , AI_PROMPT_MESSAGES_ATTRIBUTE , 'gen_ai.request.messages' ) ;
202
- renameAttributeKey ( attributes , AI_RESPONSE_TEXT_ATTRIBUTE , 'gen_ai.response.text' ) ;
203
- renameAttributeKey ( attributes , AI_RESPONSE_TOOL_CALLS_ATTRIBUTE , 'gen_ai.response.tool_calls' ) ;
204
- renameAttributeKey ( attributes , AI_PROMPT_TOOLS_ATTRIBUTE , 'gen_ai.request.available_tools' ) ;
205
- }
206
- }
207
-
208
- return event ;
209
- } ) ;
32
+ client . on ( 'spanEnd' , processVercelAiSpan ) ;
210
33
}
211
34
212
- // Auto-detect if we should force the integration when running with 'ai' package available
213
- // Note that this can only be detected if the 'Modules' integration is available, and running in CJS mode
214
- const shouldForce = options . force ?? shouldForceIntegration ( client ) ;
215
-
216
- if ( shouldForce ) {
35
+ if ( options . force || shouldRunIntegration ( client ) ) {
217
36
registerProcessors ( ) ;
218
37
}
219
38
} ,
@@ -259,14 +78,3 @@ const _vercelAIIntegration = ((options: VercelAiOptions = {}) => {
259
78
* });
260
79
*/
261
80
export const vercelAIIntegration = defineIntegration ( _vercelAIIntegration ) ;
262
-
263
- /**
264
- * Renames an attribute key in the provided attributes object if the old key exists.
265
- * This function safely handles null and undefined values.
266
- */
267
- function renameAttributeKey ( attributes : Record < string , unknown > , oldKey : string , newKey : string ) : void {
268
- if ( attributes [ oldKey ] != null ) {
269
- attributes [ newKey ] = attributes [ oldKey ] ;
270
- delete attributes [ oldKey ] ;
271
- }
272
- }
0 commit comments