11import { getClient } from '../../currentScopes' ;
2+ import { DEBUG_BUILD } from '../../debug-build' ;
23import { captureException } from '../../exports' ;
34import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '../../semanticAttributes' ;
45import { SPAN_STATUS_ERROR } from '../../tracing' ;
56import { startSpan , startSpanManual } from '../../tracing/trace' ;
67import type { Span , SpanAttributeValue } from '../../types-hoist/span' ;
8+ import { debug } from '../../utils/debug-logger' ;
79import { isThenable } from '../../utils/is' ;
810import {
911 GEN_AI_EMBEDDINGS_INPUT_ATTRIBUTE ,
@@ -54,7 +56,16 @@ function extractAvailableTools(params: Record<string, unknown>): string | undefi
5456 : [ ] ;
5557
5658 const availableTools = [ ...tools , ...webSearchOptions ] ;
57- return availableTools . length > 0 ? JSON . stringify ( availableTools ) : undefined ;
59+ if ( availableTools . length === 0 ) {
60+ return undefined ;
61+ }
62+
63+ try {
64+ return JSON . stringify ( availableTools ) ;
65+ } catch ( error ) {
66+ DEBUG_BUILD && debug . error ( 'Failed to serialize OpenAI tools:' , error ) ;
67+ return undefined ;
68+ }
5869}
5970
6071/**
@@ -166,52 +177,50 @@ function addRequestAttributes(span: Span, params: Record<string, unknown>, opera
166177 * Creates a wrapped version of .withResponse() that replaces the data field
167178 * with the instrumented result while preserving metadata (response, request_id).
168179 */
169- function createWithResponseWrapper < T > (
180+ async function createWithResponseWrapper < T > (
170181 originalWithResponse : Promise < unknown > ,
171182 instrumentedPromise : Promise < T > ,
172183) : Promise < unknown > {
173- return instrumentedPromise . then ( async instrumentedResult => {
174- try {
175- const originalWrapper = await originalWithResponse ;
176-
177- // If it's a wrapper object with data property, replace data with instrumented result
178- if ( originalWrapper && typeof originalWrapper === 'object' && 'data' in originalWrapper ) {
179- return {
180- ...originalWrapper ,
181- data : instrumentedResult ,
182- } ;
183- }
184+ const instrumentedResult = await instrumentedPromise ;
184185
185- // Otherwise return the instrumented result as-is
186- return instrumentedResult ;
187- } catch ( error ) {
188- // If getting the original wrapper fails, capture the error but still throw
189- // This ensures errors are visible while still being tracked in Sentry
190- captureException ( error , {
191- mechanism : {
192- handled : false ,
193- type : 'auto.ai.openai' ,
194- } ,
195- } ) ;
196- throw error ;
186+ try {
187+ const originalWrapper = await originalWithResponse ;
188+
189+ // Combine instrumented result with original metadata
190+ if ( originalWrapper && typeof originalWrapper === 'object' && 'data' in originalWrapper ) {
191+ return {
192+ ...originalWrapper ,
193+ data : instrumentedResult ,
194+ } ;
197195 }
198- } ) ;
196+ return instrumentedResult ;
197+ } catch ( error ) {
198+ // originalWithResponse failed - capture and rethrow
199+ captureException ( error , {
200+ mechanism : {
201+ handled : false ,
202+ type : 'auto.ai.openai' ,
203+ } ,
204+ } ) ;
205+ throw error ;
206+ }
199207}
200208
201209/**
202210 * Wraps a promise-like object to preserve additional methods (like .withResponse())
203211 */
204- function wrapPromiseWithMethods < T > ( originalPromiseLike : T , instrumentedPromise : Promise < Awaited < T > > ) : T {
212+ function wrapPromiseWithMethods < R > ( originalPromiseLike : Promise < R > , instrumentedPromise : Promise < R > ) : Promise < R > {
205213 // If the original result is not thenable, return the instrumented promise
214+ // Should not happen with current OpenAI SDK instrumented methods, but just in case.
206215 if ( ! isThenable ( originalPromiseLike ) ) {
207- return instrumentedPromise as T ;
216+ return instrumentedPromise ;
208217 }
209218
210219 // Create a proxy that forwards Promise methods to instrumentedPromise
211220 // and preserves additional methods from the original result
212221 return new Proxy ( originalPromiseLike , {
213222 get ( target : object , prop : string | symbol ) : unknown {
214- // For standard Promise methods (.then, .catch, .finally) and Symbol.toStringTag,
223+ // For standard Promise methods (.then, .catch, .finally, Symbol.toStringTag) ,
215224 // use instrumentedPromise to preserve Sentry instrumentation.
216225 // For custom methods (like .withResponse()), use the original target.
217226 const useInstrumentedPromise = prop in Promise . prototype || prop === Symbol . toStringTag ;
@@ -230,7 +239,7 @@ function wrapPromiseWithMethods<T>(originalPromiseLike: T, instrumentedPromise:
230239
231240 return typeof value === 'function' ? value . bind ( source ) : value ;
232241 } ,
233- } ) as T ;
242+ } ) as Promise < R > ;
234243}
235244
236245/**
@@ -252,21 +261,22 @@ function instrumentMethod<T extends unknown[], R>(
252261 const params = args [ 0 ] as Record < string , unknown > | undefined ;
253262 const isStreamRequested = params && typeof params === 'object' && params . stream === true ;
254263
255- // Call the original method to get the result with all its methods
256- const originalResult = originalMethod . apply ( context , args ) ;
257-
258264 const spanConfig = {
259265 name : `${ operationName } ${ model } ${ isStreamRequested ? ' stream-response' : '' } ` ,
260266 op : getSpanOperation ( methodPath ) ,
261267 attributes : requestAttributes as Record < string , SpanAttributeValue > ,
262268 } ;
263269
270+ // Capture originalResult inside span callback to ensure HTTP request starts with active span
271+ let originalResult ! : Promise < R > ;
264272 let instrumentedPromise : Promise < R > ;
265273
266274 if ( isStreamRequested ) {
267275 // For streaming responses, use manual span management to properly handle the async generator lifecycle
268276 instrumentedPromise = startSpanManual ( spanConfig , async ( span : Span ) => {
269277 try {
278+ originalResult = originalMethod . apply ( context , args ) ;
279+
270280 if ( options . recordInputs && params ) {
271281 addRequestAttributes ( span , params , operationName ) ;
272282 }
@@ -296,6 +306,8 @@ function instrumentMethod<T extends unknown[], R>(
296306 // Non-streaming responses
297307 instrumentedPromise = startSpan ( spanConfig , async ( span : Span ) => {
298308 try {
309+ originalResult = originalMethod . apply ( context , args ) ;
310+
299311 if ( options . recordInputs && params ) {
300312 addRequestAttributes ( span , params , operationName ) ;
301313 }
0 commit comments