Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 3 additions & 5 deletions packages/server-utils/src/ai/anthropic-ai/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,15 +59,13 @@ export function extractRequestAttributes(
attributes[GEN_AI_TOOL_DEFINITIONS] = JSON.stringify(params.tools);
}

attributes[GEN_AI_REQUEST_MODEL] = params.model ?? 'unknown';
attributes[GEN_AI_REQUEST_MODEL] = params.model;
if ('temperature' in params) attributes[GEN_AI_REQUEST_TEMPERATURE] = params.temperature;
if ('top_p' in params) attributes[GEN_AI_REQUEST_TOP_P] = params.top_p;
if ('stream' in params) attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE] = params.stream;
if ('top_k' in params) attributes[GEN_AI_REQUEST_TOP_K] = params.top_k;
if ('frequency_penalty' in params) attributes[GEN_AI_REQUEST_FREQUENCY_PENALTY] = params.frequency_penalty;
if ('max_tokens' in params) attributes[GEN_AI_REQUEST_MAX_TOKENS] = params.max_tokens;
} else {
attributes[GEN_AI_REQUEST_MODEL] = 'unknown';
}

return attributes;
Expand Down Expand Up @@ -261,12 +259,12 @@ function instrumentMethod<T extends unknown[], R>(

const operationName = instrumentedMethod.operation || 'unknown';
const requestAttributes = extractRequestAttributes(args, operationName, !!options.recordInputs);
const model = requestAttributes[GEN_AI_REQUEST_MODEL] || 'unknown';
const model = requestAttributes[GEN_AI_REQUEST_MODEL];
const client = getClient();
// With span streaming, omit the `'unknown'` model sentinel so the name stays low-cardinality.
const spanConfig = {
name:
(typeof model === 'string' && model !== 'unknown') || !(client && hasSpanStreamingEnabled(client))
(typeof model === 'string') || !(client && hasSpanStreamingEnabled(client))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing model interpolates as undefined

Medium Severity

When no model is resolved, the name still interpolates model whenever span streaming is off (and in the Google GenAI integration, even when it is on). Template strings then produce names like chat undefined instead of chat or chat unknown. Empty-string models also become chat with a trailing space.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit f339a32. Configure here.

? `${operationName} ${model}`
: operationName,
op: getGenAiSpanOp(operationName),
Expand Down
4 changes: 1 addition & 3 deletions packages/server-utils/src/ai/google-genai/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,6 @@ export function extractModel(params: Record<string, unknown>, context?: unknown)
return contextObj.modelVersion;
}
}

return 'unknown';
}

/**
Expand Down Expand Up @@ -314,7 +312,7 @@ function instrumentMethod<T extends unknown[], R>(
const client = getClient();
// With span streaming, omit the `'unknown'` model sentinel so the name stays low-cardinality.
const spanName =
(typeof model === 'string' && model !== 'unknown') || !(client && hasSpanStreamingEnabled(client))
Comment thread
cursor[bot] marked this conversation as resolved.
(typeof model === 'string') || !(client && hasSpanStreamingEnabled(client))
? `${operationName} ${model}`
: operationName;

Expand Down
6 changes: 3 additions & 3 deletions packages/server-utils/src/ai/langchain/embeddings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ function extractEmbeddingAttributes(instance: unknown): Record<string, unknown>
[SENTRY_ORIGIN]: LANGCHAIN_ORIGIN,
[SENTRY_OP]: GEN_AI_EMBEDDINGS,
[GEN_AI_OPERATION_NAME]: 'embeddings',
[GEN_AI_REQUEST_MODEL]: embeddingsInstance.model ?? 'unknown',
[GEN_AI_REQUEST_MODEL]: embeddingsInstance.model,
};

attributes[GEN_AI_PROVIDER_NAME] = inferSystemFromInstance(embeddingsInstance);
Expand All @@ -70,7 +70,7 @@ export function _INTERNAL_getLangChainEmbeddingsSpanOptions(
): { name: string; attributes: Record<string, SpanAttributeValue> } {
const { recordInputs } = resolveAIRecordingOptions(options);
const attributes = extractEmbeddingAttributes(instance);
const modelName = attributes[GEN_AI_REQUEST_MODEL] || 'unknown';
const modelName = attributes[GEN_AI_REQUEST_MODEL];
const client = getClient();

if (recordInputs && input != null) {
Expand All @@ -80,7 +80,7 @@ export function _INTERNAL_getLangChainEmbeddingsSpanOptions(
return {
// With span streaming, omit the `'unknown'` model sentinel so the name stays low-cardinality.
name:
(typeof modelName === 'string' && modelName !== 'unknown') || !(client && hasSpanStreamingEnabled(client))
(typeof modelName === 'string') || !(client && hasSpanStreamingEnabled(client))
? `embeddings ${modelName}`
: 'embeddings',
attributes: attributes as Record<string, SpanAttributeValue>,
Expand Down
8 changes: 4 additions & 4 deletions packages/server-utils/src/ai/langchain/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ export function createLangChainCallbackHandler(options: LangChainOptions = {}):
invocationParams,
metadata,
);
const modelName = attributes[GEN_AI_REQUEST_MODEL] || 'unknown';
const modelName = attributes[GEN_AI_REQUEST_MODEL];
const operationName =
typeof attributes[GEN_AI_OPERATION_NAME] === 'string' ? attributes[GEN_AI_OPERATION_NAME] : 'unknown';
const client = getClient();
Expand All @@ -114,7 +114,7 @@ export function createLangChainCallbackHandler(options: LangChainOptions = {}):
{
// With span streaming, omit the `'unknown'` model sentinel so the name stays low-cardinality.
name:
(typeof modelName === 'string' && modelName !== 'unknown') || !(client && hasSpanStreamingEnabled(client))
(typeof modelName === 'string') || !(client && hasSpanStreamingEnabled(client))
? `${operationName} ${modelName}`
: operationName,
attributes: {
Expand Down Expand Up @@ -156,7 +156,7 @@ export function createLangChainCallbackHandler(options: LangChainOptions = {}):
attributes[GEN_AI_TOOL_DEFINITIONS] = toolDefsJson;
}

const modelName = attributes[GEN_AI_REQUEST_MODEL] || 'unknown';
const modelName = attributes[GEN_AI_REQUEST_MODEL];
const operationName =
typeof attributes[GEN_AI_OPERATION_NAME] === 'string' ? attributes[GEN_AI_OPERATION_NAME] : 'unknown';
const client = getClient();
Expand All @@ -165,7 +165,7 @@ export function createLangChainCallbackHandler(options: LangChainOptions = {}):
{
// With span streaming, omit the `'unknown'` model sentinel so the name stays low-cardinality.
name:
(typeof modelName === 'string' && modelName !== 'unknown') || !(client && hasSpanStreamingEnabled(client))
(typeof modelName === 'string') || !(client && hasSpanStreamingEnabled(client))
? `${operationName} ${modelName}`
: operationName,
attributes: {
Expand Down
2 changes: 1 addition & 1 deletion packages/server-utils/src/ai/langchain/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,7 @@ export function extractLLMRequestAttributes(
langSmithMetadata?: Record<string, unknown>,
): Record<string, SpanAttributeValue | undefined> {
const system = langSmithMetadata?.ls_provider;
const modelName = invocationParams?.model ?? langSmithMetadata?.ls_model_name ?? 'unknown';
const modelName = invocationParams?.model ?? langSmithMetadata?.ls_model_name;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Chat models still default to unknown

Medium Severity

extractChatModelRequestAttributes still falls back to 'unknown' for the model, so LangChain chat spans keep setting gen_ai.request.model to 'unknown' and naming spans {operation} unknown. The LLM helper was updated; the chat-model path, which is the common one, was not.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit f339a32. Configure here.


const attrs = baseRequestAttributes(system, modelName, llm, invocationParams, langSmithMetadata);

Expand Down
112 changes: 72 additions & 40 deletions packages/server-utils/src/ai/openai/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/* eslint-disable typescript-eslint/no-deprecated */
import { DEBUG_BUILD } from '../../debug-build';
import { DEBUG_BUILD } from "../../debug-build";
import {
getClient,
hasSpanStreamingEnabled,
Expand All @@ -8,8 +8,8 @@ import {
startSpanManual,
debug,
stringify,
} from '@sentry/core';
import type { Span, SpanAttributeValue } from '@sentry/core';
} from "@sentry/core";
import type { Span, SpanAttributeValue } from "@sentry/core";
import {
GEN_AI_EMBEDDINGS_INPUT,
GEN_AI_INPUT_MESSAGES,
Expand All @@ -19,8 +19,8 @@ import {
GEN_AI_SYSTEM_INSTRUCTIONS,
GEN_AI_TOOL_DEFINITIONS,
SENTRY_ORIGIN,
} from '@sentry/conventions/attributes';
import type { InstrumentedMethodEntry } from '../core/utils';
} from "@sentry/conventions/attributes";
import type { InstrumentedMethodEntry } from "../core/utils";
import {
buildMethodPath,
extractSystemInstructions,
Expand All @@ -37,11 +37,19 @@ import { addResponseAttributes, extractRequestParameters } from './utils';
/**
* Extract available tools from request parameters
*/
function extractAvailableTools(params: Record<string, unknown>): string | undefined {
function extractAvailableTools(
params: Record<string, unknown>,
): string | undefined {
const tools = Array.isArray(params.tools) ? params.tools : [];
const hasWebSearchOptions = params.web_search_options && typeof params.web_search_options === 'object';
const hasWebSearchOptions =
params.web_search_options && typeof params.web_search_options === "object";
const webSearchOptions = hasWebSearchOptions
? [{ type: 'web_search_options', ...(params.web_search_options as Record<string, unknown>) }]
? [
{
type: "web_search_options",
...(params.web_search_options as Record<string, unknown>),
},
]
: [];

const availableTools = [...tools, ...webSearchOptions];
Expand All @@ -52,7 +60,7 @@ function extractAvailableTools(params: Record<string, unknown>): string | undefi
try {
return JSON.stringify(availableTools);
} catch (error) {
DEBUG_BUILD && debug.error('Failed to serialize OpenAI tools:', error);
DEBUG_BUILD && debug.error("Failed to serialize OpenAI tools:", error);
return undefined;
}
}
Expand All @@ -66,12 +74,12 @@ export function extractRequestAttributes(
recordInputs: boolean,
): Record<string, unknown> {
const attributes: Record<string, unknown> = {
[GEN_AI_PROVIDER_NAME]: 'openai',
[GEN_AI_PROVIDER_NAME]: "openai",
[GEN_AI_OPERATION_NAME]: operationName,
[SENTRY_ORIGIN]: 'auto.ai.openai',
[SENTRY_ORIGIN]: "auto.ai.openai",
};

if (args.length > 0 && typeof args[0] === 'object' && args[0] !== null) {
if (args.length > 0 && typeof args[0] === "object" && args[0] !== null) {
const params = args[0] as Record<string, unknown>;

if (recordInputs) {
Expand All @@ -83,22 +91,19 @@ export function extractRequestAttributes(
if (recordInputs && operationName === 'decide') {
attributes[GEN_AI_INPUT_MESSAGES] = getDecisionInputMessages(params);
}
} else {
attributes[GEN_AI_REQUEST_MODEL] = 'unknown';
}

return attributes;
}

// Extract and record AI request inputs, if present. This is intentionally separate from response attributes.
export function addRequestAttributes(span: Span, params: Record<string, unknown>, operationName: string): void {
// A decision's input is recorded when its span starts, in `extractRequestAttributes`.
if (operationName === 'decide') {
return;
}

export function addRequestAttributes(
span: Span,
params: Record<string, unknown>,
operationName: string,
): void {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Decide inputs overwritten as chat messages

High Severity

The early return that skipped addRequestAttributes for decide operations was removed. Decision input is already recorded in evaluation format at span start; this path now re-reads params.input as chat messages and overwrites gen_ai.input_messages.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit f339a32. Configure here.

// Store embeddings input on a separate attribute
if (operationName === 'embeddings' && 'input' in params) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fix PR has no covering tests

Medium Severity

This is a fix PR but the diff has no unit, integration, or E2E tests for omitting gen_ai.request.model or for the new span-name behavior. Existing tests still expect 'unknown' (and 'chat unknown' / 'embeddings unknown' in static mode) and were not updated, so the regression this PR claims to fix is not covered. Flagged because the review rules require a fix PR to include a test that fails without the change and passes with it.

Fix in Cursor Fix in Web

Triggered by project rule: PR Review Guidelines for Cursor Bot

Reviewed by Cursor Bugbot for commit f459c34. Configure here.

if (operationName === "embeddings" && "input" in params) {
const input = params.input;

// No input provided
Expand All @@ -107,7 +112,7 @@ export function addRequestAttributes(span: Span, params: Record<string, unknown>
}

// Empty input string
if (typeof input === 'string' && input.length === 0) {
if (typeof input === "string" && input.length === 0) {
return;
}

Expand All @@ -120,7 +125,12 @@ export function addRequestAttributes(span: Span, params: Record<string, unknown>
return;
}

const src = 'input' in params ? params.input : 'messages' in params ? params.messages : undefined;
const src =
"input" in params
? params.input
: "messages" in params
? params.messages
: undefined;

if (!src) {
return;
Expand All @@ -130,7 +140,8 @@ export function addRequestAttributes(span: Span, params: Record<string, unknown>
return;
}

const { systemInstructions, filteredMessages } = extractSystemInstructions(src);
const { systemInstructions, filteredMessages } =
extractSystemInstructions(src);

if (systemInstructions) {
span.setAttribute(GEN_AI_SYSTEM_INSTRUCTIONS, systemInstructions);
Expand All @@ -152,18 +163,24 @@ function instrumentMethod<T extends unknown[], R>(
options: OpenAiOptions,
): (...args: T) => Promise<R> {
return function instrumentedCall(...args: T): Promise<R> {
const operationName = instrumentedMethod.operation || 'unknown';
const requestAttributes = extractRequestAttributes(args, operationName, !!options.recordInputs);
const model = (requestAttributes[GEN_AI_REQUEST_MODEL] as string) || 'unknown';
const operationName = instrumentedMethod.operation || "unknown";
const requestAttributes = extractRequestAttributes(
args,
operationName,
!!options.recordInputs,
);
const model =
(requestAttributes[GEN_AI_REQUEST_MODEL] as string);

const params = args[0] as Record<string, unknown> | undefined;
const isStreamRequested = params && typeof params === 'object' && params.stream === true;
const isStreamRequested =
params && typeof params === "object" && params.stream === true;
const client = getClient();

const spanConfig = {
// With span streaming, omit the `'unknown'` model sentinel so the name stays low-cardinality.
name:
model !== 'unknown' || !(client && hasSpanStreamingEnabled(client))
!(client && hasSpanStreamingEnabled(client))
Comment thread
cursor[bot] marked this conversation as resolved.
? `${operationName} ${model}`
: operationName,
op: getGenAiSpanOp(operationName),
Expand All @@ -185,12 +202,17 @@ function instrumentMethod<T extends unknown[], R>(
try {
const result = await originalResult;
return instrumentStream(
result as OpenAIStream<ChatCompletionChunk | ResponseStreamingEvent>,
result as OpenAIStream<
ChatCompletionChunk | ResponseStreamingEvent
>,
span,
options.recordOutputs ?? false,
) as unknown as R;
} catch (error) {
span.setStatus({ code: SPAN_STATUS_ERROR, message: 'internal_error' });
span.setStatus({
code: SPAN_STATUS_ERROR,
message: "internal_error",
});
span.end();
throw error;
}
Expand All @@ -211,8 +233,8 @@ function instrumentMethod<T extends unknown[], R>(
addRequestAttributes(span, params, operationName);
}

return originalResult.then(result => {
addResponseAttributes(span, result, options.recordOutputs, operationName);
return originalResult.then((result) => {
addResponseAttributes(span, result, options.recordOutputs);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Decide spans lose recorded outputs

High Severity

The non-streaming OpenAI path now calls addResponseAttributes without operationName. Decision answers are only recorded when that argument is 'decide', so gen_ai.output_messages is no longer set on decisions.create spans.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit f339a32. Configure here.

return result;
});
});
Expand All @@ -224,14 +246,21 @@ function instrumentMethod<T extends unknown[], R>(
/**
* Create a deep proxy for OpenAI client instrumentation
*/
function createDeepProxy<T extends object>(target: T, currentPath = '', options: OpenAiOptions): T {
function createDeepProxy<T extends object>(
target: T,
currentPath = "",
options: OpenAiOptions,
): T {
return new Proxy(target, {
get(obj: object, prop: string): unknown {
const value = (obj as Record<string, unknown>)[prop];
const methodPath = buildMethodPath(currentPath, String(prop));

const instrumentedMethod = OPENAI_METHOD_REGISTRY[methodPath as keyof typeof OPENAI_METHOD_REGISTRY];
if (typeof value === 'function' && instrumentedMethod) {
const instrumentedMethod =
OPENAI_METHOD_REGISTRY[
methodPath as keyof typeof OPENAI_METHOD_REGISTRY
];
if (typeof value === "function" && instrumentedMethod) {
return instrumentMethod(
value as (...args: unknown[]) => Promise<unknown>,
methodPath,
Expand All @@ -241,13 +270,13 @@ function createDeepProxy<T extends object>(target: T, currentPath = '', options:
);
}

if (typeof value === 'function') {
if (typeof value === "function") {
// Bind non-instrumented functions to preserve the original `this` context,
// which is required for accessing private class fields (e.g. #baseURL) in OpenAI SDK v5.
return value.bind(obj);
}

if (value && typeof value === 'object') {
if (value && typeof value === "object") {
return createDeepProxy(value, methodPath, options);
}

Expand All @@ -260,6 +289,9 @@ function createDeepProxy<T extends object>(target: T, currentPath = '', options:
* Instrument an OpenAI client with Sentry tracing
* Can be used across Node.js, Cloudflare Workers, and Vercel Edge
*/
export function instrumentOpenAiClient<T extends object>(client: T, options?: OpenAiOptions): T {
return createDeepProxy(client, '', resolveAIRecordingOptions(options));
export function instrumentOpenAiClient<T extends object>(
client: T,
options?: OpenAiOptions,
): T {
return createDeepProxy(client, "", resolveAIRecordingOptions(options));
}
2 changes: 1 addition & 1 deletion packages/server-utils/src/ai/openai/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ function extractConversationId(params: Record<string, unknown>): string | undefi
*/
export function extractRequestParameters(params: Record<string, unknown>): Record<string, unknown> {
const attributes: Record<string, unknown> = {
[GEN_AI_REQUEST_MODEL]: params.model ?? 'unknown',
[GEN_AI_REQUEST_MODEL]: params.model,
};

if ('temperature' in params) attributes[GEN_AI_REQUEST_TEMPERATURE] = params.temperature;
Expand Down
Loading