@@ -44,34 +44,23 @@ import {
44
44
45
45
/**
46
46
* Extract request attributes from method arguments
47
- * Following Sentry's AI Agents conventions
48
47
*/
49
48
function extractRequestAttributes ( args : unknown [ ] , methodPath : string ) : Record < string , unknown > {
50
49
const attributes : Record < string , unknown > = {
51
50
[ GEN_AI_SYSTEM_ATTRIBUTE ] : 'openai' ,
52
51
[ GEN_AI_OPERATION_NAME_ATTRIBUTE ] : getOperationName ( methodPath ) ,
53
52
} ;
54
53
55
- if ( args . length > 0 && args [ 0 ] && typeof args [ 0 ] === 'object' ) {
54
+ if ( args . length > 0 && typeof args [ 0 ] === 'object' && args [ 0 ] !== null ) {
56
55
const params = args [ 0 ] as Record < string , unknown > ;
57
56
58
- attributes [ GEN_AI_REQUEST_MODEL_ATTRIBUTE ] = params . model || 'unknown' ;
59
-
60
- if ( params . temperature !== undefined ) {
61
- attributes [ GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE ] = params . temperature ;
62
- }
63
- if ( params . top_p !== undefined ) {
64
- attributes [ GEN_AI_REQUEST_TOP_P_ATTRIBUTE ] = params . top_p ;
65
- }
66
-
67
- if ( params . frequency_penalty !== undefined ) {
57
+ attributes [ GEN_AI_REQUEST_MODEL_ATTRIBUTE ] = params . model ?? 'unknown' ;
58
+ if ( 'temperature' in params ) attributes [ GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE ] = params . temperature ;
59
+ if ( 'top_p' in params ) attributes [ GEN_AI_REQUEST_TOP_P_ATTRIBUTE ] = params . top_p ;
60
+ if ( 'frequency_penalty' in params )
68
61
attributes [ GEN_AI_REQUEST_FREQUENCY_PENALTY_ATTRIBUTE ] = params . frequency_penalty ;
69
- }
70
- if ( params . presence_penalty !== undefined ) {
71
- attributes [ GEN_AI_REQUEST_PRESENCE_PENALTY_ATTRIBUTE ] = params . presence_penalty ;
72
- }
62
+ if ( 'presence_penalty' in params ) attributes [ GEN_AI_REQUEST_PRESENCE_PENALTY_ATTRIBUTE ] = params . presence_penalty ;
73
63
} else {
74
- // REQUIRED: Ensure model is always set even when no params provided
75
64
attributes [ GEN_AI_REQUEST_MODEL_ATTRIBUTE ] = 'unknown' ;
76
65
}
77
66
@@ -134,7 +123,6 @@ function setCommonResponseAttributes(span: Span, id?: string, model?: string, ti
134
123
*/
135
124
function addChatCompletionAttributes ( span : Span , response : OpenAiChatCompletionObject ) : void {
136
125
setCommonResponseAttributes ( span , response . id , response . model , response . created ) ;
137
-
138
126
if ( response . usage ) {
139
127
setTokenUsageAttributes (
140
128
span ,
@@ -143,11 +131,10 @@ function addChatCompletionAttributes(span: Span, response: OpenAiChatCompletionO
143
131
response . usage . total_tokens ,
144
132
) ;
145
133
}
146
-
147
- // Finish reasons - must be stringified array
148
- if ( response . choices && Array . isArray ( response . choices ) ) {
149
- const finishReasons = response . choices . map ( choice => choice . finish_reason ) . filter ( reason => reason !== null ) ;
150
-
134
+ if ( Array . isArray ( response . choices ) ) {
135
+ const finishReasons = response . choices
136
+ . map ( choice => choice . finish_reason )
137
+ . filter ( ( reason ) : reason is string => reason !== null ) ;
151
138
if ( finishReasons . length > 0 ) {
152
139
span . setAttributes ( {
153
140
[ GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE ] : JSON . stringify ( finishReasons ) ,
@@ -161,14 +148,11 @@ function addChatCompletionAttributes(span: Span, response: OpenAiChatCompletionO
161
148
*/
162
149
function addResponsesApiAttributes ( span : Span , response : OpenAIResponseObject ) : void {
163
150
setCommonResponseAttributes ( span , response . id , response . model , response . created_at ) ;
164
-
165
151
if ( response . status ) {
166
152
span . setAttributes ( {
167
153
[ GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE ] : JSON . stringify ( [ response . status ] ) ,
168
154
} ) ;
169
155
}
170
-
171
- // Token usage for responses API
172
156
if ( response . usage ) {
173
157
setTokenUsageAttributes (
174
158
span ,
@@ -190,27 +174,28 @@ function addResponseAttributes(span: Span, result: unknown, recordOutputs?: bool
190
174
191
175
if ( isChatCompletionResponse ( response ) ) {
192
176
addChatCompletionAttributes ( span , response ) ;
193
-
194
- if ( recordOutputs && response . choices && response . choices . length > 0 ) {
177
+ if ( recordOutputs && response . choices ?. length ) {
195
178
const responseTexts = response . choices . map ( choice => choice . message ?. content || '' ) ;
196
- span . setAttributes ( {
197
- [ GEN_AI_RESPONSE_TEXT_ATTRIBUTE ] : JSON . stringify ( responseTexts ) ,
198
- } ) ;
179
+ span . setAttributes ( { [ GEN_AI_RESPONSE_TEXT_ATTRIBUTE ] : JSON . stringify ( responseTexts ) } ) ;
199
180
}
200
181
} else if ( isResponsesApiResponse ( response ) ) {
201
182
addResponsesApiAttributes ( span , response ) ;
202
-
203
183
if ( recordOutputs && response . output_text ) {
204
- span . setAttributes ( {
205
- [ GEN_AI_RESPONSE_TEXT_ATTRIBUTE ] : response . output_text ,
206
- } ) ;
184
+ span . setAttributes ( { [ GEN_AI_RESPONSE_TEXT_ATTRIBUTE ] : response . output_text } ) ;
207
185
}
208
186
}
209
187
}
210
188
211
- /**
212
- * Get options from integration configuration
213
- */
189
+ // Extract and record AI request inputs, if present. This is intentionally separate from response attributes.
190
+ function addRequestAttributes ( span : Span , params : Record < string , unknown > ) : void {
191
+ if ( 'messages' in params ) {
192
+ span . setAttributes ( { [ GEN_AI_REQUEST_MESSAGES_ATTRIBUTE ] : JSON . stringify ( params . messages ) } ) ;
193
+ }
194
+ if ( 'input' in params ) {
195
+ span . setAttributes ( { [ GEN_AI_REQUEST_MESSAGES_ATTRIBUTE ] : JSON . stringify ( params . input ) } ) ;
196
+ }
197
+ }
198
+
214
199
function getOptionsFromIntegration ( ) : OpenAiOptions {
215
200
const scope = getCurrentScope ( ) ;
216
201
const client = scope . getClient ( ) ;
@@ -237,33 +222,24 @@ function instrumentMethod<T extends unknown[], R>(
237
222
return async function instrumentedMethod ( ...args : T ) : Promise < R > {
238
223
const finalOptions = options || getOptionsFromIntegration ( ) ;
239
224
const requestAttributes = extractRequestAttributes ( args , methodPath ) ;
240
- const model = requestAttributes [ GEN_AI_REQUEST_MODEL_ATTRIBUTE ] || 'unknown' ;
225
+ const model = ( requestAttributes [ GEN_AI_REQUEST_MODEL_ATTRIBUTE ] as string ) || 'unknown' ;
241
226
const operationName = getOperationName ( methodPath ) ;
242
227
243
228
return startSpan (
244
229
{
245
- // Span name follows Sentry convention: "{operation_name} {model}"
246
- // e.g., "chat gpt-4", "chat o3-mini", "embeddings text-embedding-3-small"
247
230
name : `${ operationName } ${ model } ` ,
248
231
op : getSpanOperation ( methodPath ) ,
249
232
attributes : requestAttributes as Record < string , SpanAttributeValue > ,
250
233
} ,
251
234
async ( span : Span ) => {
252
235
try {
253
- // Record inputs if enabled - must be stringified JSON
254
236
if ( finalOptions . recordInputs && args [ 0 ] && typeof args [ 0 ] === 'object' ) {
255
- const params = args [ 0 ] as Record < string , unknown > ;
256
- if ( params . messages ) {
257
- span . setAttributes ( {
258
- [ GEN_AI_REQUEST_MESSAGES_ATTRIBUTE ] : JSON . stringify ( params . messages ) ,
259
- } ) ;
260
- }
237
+ addRequestAttributes ( span , args [ 0 ] as Record < string , unknown > ) ;
261
238
}
262
239
263
240
const result = await originalMethod . apply ( context , args ) ;
264
241
// TODO: Add streaming support
265
242
addResponseAttributes ( span , result , finalOptions . recordOutputs ) ;
266
-
267
243
return result ;
268
244
} catch ( error ) {
269
245
captureException ( error ) ;
@@ -279,20 +255,16 @@ function instrumentMethod<T extends unknown[], R>(
279
255
*/
280
256
function createDeepProxy ( target : object , currentPath = '' , options ?: OpenAiOptions ) : OpenAiClient {
281
257
return new Proxy ( target , {
282
- get ( obj : Record < string | symbol , unknown > , prop : string | symbol ) : unknown {
283
- if ( typeof prop === 'symbol' ) {
284
- return obj [ prop ] ;
285
- }
286
-
287
- const value = obj [ prop ] ;
288
- const methodPath = buildMethodPath ( currentPath , prop ) ;
258
+ get ( obj : object , prop : string ) : unknown {
259
+ const value = ( obj as Record < string , unknown > ) [ prop ] ;
260
+ const methodPath = buildMethodPath ( currentPath , String ( prop ) ) ;
289
261
290
262
if ( typeof value === 'function' && shouldInstrument ( methodPath ) ) {
291
263
return instrumentMethod ( value as ( ...args : unknown [ ] ) => Promise < unknown > , methodPath , obj , options ) ;
292
264
}
293
265
294
- if ( typeof value === 'object' && value !== null ) {
295
- return createDeepProxy ( value , methodPath , options ) ;
266
+ if ( value && typeof value === 'object' ) {
267
+ return createDeepProxy ( value as object , methodPath , options ) ;
296
268
}
297
269
298
270
return value ;
0 commit comments