Skip to content

Commit c4734fd

Browse files
committed
handle some edge cases
1 parent cdda89d commit c4734fd

File tree

1 file changed

+45
-33
lines changed
  • packages/core/src/tracing/openai

1 file changed

+45
-33
lines changed

packages/core/src/tracing/openai/index.ts

Lines changed: 45 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import { getClient } from '../../currentScopes';
2+
import { DEBUG_BUILD } from '../../debug-build';
23
import { captureException } from '../../exports';
34
import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '../../semanticAttributes';
45
import { SPAN_STATUS_ERROR } from '../../tracing';
56
import { startSpan, startSpanManual } from '../../tracing/trace';
67
import type { Span, SpanAttributeValue } from '../../types-hoist/span';
8+
import { debug } from '../../utils/debug-logger';
79
import { isThenable } from '../../utils/is';
810
import {
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

Comments
 (0)