Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -105,10 +105,8 @@ describe('OpenAI Tool Calls integration', () => {
type: 'string',
value: 'gpt-4',
});
expect(chatToolsSpan!.attributes[GEN_AI_TOOL_DEFINITIONS]).toEqual({
type: 'string',
value: WEATHER_TOOL_DEFINITION,
});
// Tool definitions are gen AI input data, so `genAI.inputs: false` drops them.
expect(chatToolsSpan!.attributes[GEN_AI_TOOL_DEFINITIONS]).toBeUndefined();
expect(chatToolsSpan!.attributes[GEN_AI_RESPONSE_MODEL]).toEqual({
type: 'string',
value: 'gpt-4',
Expand Down Expand Up @@ -164,10 +162,7 @@ describe('OpenAI Tool Calls integration', () => {
type: 'boolean',
value: true,
});
expect(streamingChatToolsSpan!.attributes[GEN_AI_TOOL_DEFINITIONS]).toEqual({
type: 'string',
value: WEATHER_TOOL_DEFINITION,
});
expect(streamingChatToolsSpan!.attributes[GEN_AI_TOOL_DEFINITIONS]).toBeUndefined();
expect(streamingChatToolsSpan!.attributes[GEN_AI_RESPONSE_MODEL]).toEqual({
type: 'string',
value: 'gpt-4',
Expand Down Expand Up @@ -224,10 +219,7 @@ describe('OpenAI Tool Calls integration', () => {
value: 'gpt-4',
});
expect(responsesToolsSpan!.attributes[GEN_AI_REQUEST_STREAM_ATTRIBUTE]).toBeUndefined();
expect(responsesToolsSpan!.attributes[GEN_AI_TOOL_DEFINITIONS]).toEqual({
type: 'string',
value: WEATHER_TOOL_DEFINITION,
});
expect(responsesToolsSpan!.attributes[GEN_AI_TOOL_DEFINITIONS]).toBeUndefined();
expect(responsesToolsSpan!.attributes[GEN_AI_RESPONSE_MODEL]).toEqual({
type: 'string',
value: 'gpt-4',
Expand Down Expand Up @@ -283,10 +275,7 @@ describe('OpenAI Tool Calls integration', () => {
type: 'boolean',
value: true,
});
expect(streamingResponsesToolsSpan!.attributes[GEN_AI_TOOL_DEFINITIONS]).toEqual({
type: 'string',
value: WEATHER_TOOL_DEFINITION,
});
expect(streamingResponsesToolsSpan!.attributes[GEN_AI_TOOL_DEFINITIONS]).toBeUndefined();
expect(streamingResponsesToolsSpan!.attributes[GEN_AI_RESPONSE_MODEL]).toEqual({
type: 'string',
value: 'gpt-4',
Expand Down
10 changes: 7 additions & 3 deletions packages/server-utils/src/ai/anthropic-ai/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,11 @@ const INSTRUMENTED_METHODS = new WeakSet<object>();
/**
* Extract request attributes from method arguments
*/
export function extractRequestAttributes(args: unknown[], operationName: string): Record<string, unknown> {
export function extractRequestAttributes(
args: unknown[],
operationName: string,
recordInputs: boolean,
): Record<string, unknown> {
const attributes: Record<string, unknown> = {
[GEN_AI_PROVIDER_NAME]: 'anthropic',
[GEN_AI_OPERATION_NAME]: operationName,
Expand All @@ -57,7 +61,7 @@ export function extractRequestAttributes(args: unknown[], operationName: string)

if (args.length > 0 && typeof args[0] === 'object' && args[0] !== null) {
const params = args[0] as Record<string, unknown>;
if (params.tools && Array.isArray(params.tools)) {
if (recordInputs && params.tools && Array.isArray(params.tools)) {
attributes[GEN_AI_TOOL_DEFINITIONS] = JSON.stringify(params.tools);
}

Expand Down Expand Up @@ -259,7 +263,7 @@ function instrumentMethod<T extends unknown[], R>(
}

const operationName = instrumentedMethod.operation || 'unknown';
const requestAttributes = extractRequestAttributes(args, operationName);
const requestAttributes = extractRequestAttributes(args, operationName, !!options.recordInputs);
const model = requestAttributes[GEN_AI_REQUEST_MODEL] || 'unknown';
const client = getClient();
// With span streaming, omit the `'unknown'` model sentinel so the name stays low-cardinality.
Expand Down
10 changes: 8 additions & 2 deletions packages/server-utils/src/ai/google-genai/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ export function extractRequestAttributes(
operationName: string,
params?: Record<string, unknown>,
context?: unknown,
recordInputs = true,
): Record<string, SpanAttributeValue> {
const attributes: Record<string, SpanAttributeValue> = {
[GEN_AI_PROVIDER_NAME]: GOOGLE_GENAI_SYSTEM_NAME,
Expand All @@ -119,7 +120,7 @@ export function extractRequestAttributes(
Object.assign(attributes, extractConfigAttributes(config));

// Extract available tools from config
if ('tools' in config && Array.isArray(config.tools)) {
if (recordInputs && 'tools' in config && Array.isArray(config.tools)) {
const functionDeclarations = config.tools.flatMap(
(tool: { functionDeclarations: unknown[] }) => tool.functionDeclarations,
);
Expand Down Expand Up @@ -301,7 +302,12 @@ function instrumentMethod<T extends unknown[], R>(
const operationName = instrumentedMethod.operation || 'unknown';
const params = args[0] as Record<string, unknown> | undefined;
const attributeParams = resolveChatParams(operationName, params, context);
const requestAttributes = extractRequestAttributes(operationName, attributeParams, context);
const requestAttributes = extractRequestAttributes(
operationName,
attributeParams,
context,
!!options.recordInputs,
);
const model = requestAttributes[GEN_AI_REQUEST_MODEL] || 'unknown';
const client = getClient();
// With span streaming, omit the `'unknown'` model sentinel so the name stays low-cardinality.
Expand Down
2 changes: 1 addition & 1 deletion packages/server-utils/src/ai/langchain/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ export function createLangChainCallbackHandler(options: LangChainOptions = {}):
metadata,
);

const toolDefsJson = extractToolDefinitions(extraParams);
const toolDefsJson = recordInputs ? extractToolDefinitions(extraParams) : undefined;
if (toolDefsJson) {
attributes[GEN_AI_TOOL_DEFINITIONS] = toolDefsJson;
}
Expand Down
12 changes: 8 additions & 4 deletions packages/server-utils/src/ai/langgraph/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,15 @@ const SENTRY_PATCHED = '__sentry_patched__';
*/
export function instrumentStateGraphCompile(
originalCompile: (...args: unknown[]) => CompiledGraph,
options: LangGraphOptions,
rawOptions: LangGraphOptions,
): (...args: unknown[]) => CompiledGraph {
if (Object.prototype.hasOwnProperty.call(originalCompile, SENTRY_PATCHED)) {
return originalCompile;
}

// This is exported, so callers can hand us an options object with no recording flags set. Resolving
// here (rather than only in `instrumentStateGraph`) keeps that path on the `dataCollection` defaults.
const options = resolveAIRecordingOptions(rawOptions);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Bug: The LangGraph integration passes unresolved options to instrumentCompiledGraphInvoke, causing recordInputs to be undefined and preventing tool extraction even when globally enabled.
Severity: MEDIUM

Suggested Fix

In packages/server-utils/src/integrations/langgraph.ts, the call to wrapCompiledGraphInvoke should pass the resolvedOptions variable instead of the original options parameter. This ensures instrumentCompiledGraphInvoke receives fully resolved recording options.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.

Location: packages/server-utils/src/ai/langgraph/index.ts#L48

Potential issue: In the diagnostics-channel integration for LangGraph, the
`wrapCompiledGraphInvoke` function is called with an unresolved `options` object. This
object is then passed to `instrumentCompiledGraphInvoke`, which incorrectly assumes the
options have been resolved. When `options.recordInputs` is `undefined`, the logic to
extract tool definitions from the graph instance is skipped. This prevents tool data
from being recorded, even if data collection for inputs is enabled globally via
`dataCollection.genAI.inputs`.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

False positive. Both wrapCompiledGraphInvoke calls in integrations/langgraph.ts already pass resolvedOptions (lines 58 and 87), built on line 47 from resolveAIRecordingOptions(options). The suggested fix describes what the code already does.

All three callers of instrumentCompiledGraphInvoke pass resolved options: the diagnostics-channel integration, instrumentCreateReactAgent (line 205), and instrumentStateGraphCompile since the previous commit. instrumentCompiledGraphInvoke is not exported from any package entry point, so no unresolved path is left.

const sentryHandler = createLangChainCallbackHandler(options);

const wrapped = new Proxy(originalCompile, {
Expand Down Expand Up @@ -142,15 +145,16 @@ export function instrumentCompiledGraphInvoke(
);
}

const recordInputs = options.recordInputs;
const recordOutputs = options.recordOutputs;

// Extract available tools from the graph instance
const tools = extractToolsFromCompiledGraph(graphInstance);
const tools = recordInputs ? extractToolsFromCompiledGraph(graphInstance) : null;
Comment thread
sentry[bot] marked this conversation as resolved.
if (tools) {
span.setAttribute(GEN_AI_TOOL_DEFINITIONS, JSON.stringify(tools));
}

// Parse input messages
const recordInputs = options.recordInputs;
const recordOutputs = options.recordOutputs;
const inputMessages =
args.length > 0 ? ((args[0] as { messages?: LangChainMessage[] } | null)?.messages ?? []) : [];

Expand Down
10 changes: 7 additions & 3 deletions packages/server-utils/src/ai/openai/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,11 @@ function extractAvailableTools(params: Record<string, unknown>): string | undefi
/**
* Extract request attributes from method arguments
*/
export function extractRequestAttributes(args: unknown[], operationName: string): Record<string, unknown> {
export function extractRequestAttributes(
args: unknown[],
operationName: string,
recordInputs: boolean,
): Record<string, unknown> {
const attributes: Record<string, unknown> = {
[GEN_AI_PROVIDER_NAME]: 'openai',
[GEN_AI_OPERATION_NAME]: operationName,
Expand All @@ -69,7 +73,7 @@ export function extractRequestAttributes(args: unknown[], operationName: string)
if (args.length > 0 && typeof args[0] === 'object' && args[0] !== null) {
const params = args[0] as Record<string, unknown>;

const availableTools = extractAvailableTools(params);
const availableTools = recordInputs ? extractAvailableTools(params) : undefined;
if (availableTools) {
attributes[GEN_AI_TOOL_DEFINITIONS] = availableTools;
}
Expand Down Expand Up @@ -140,7 +144,7 @@ function instrumentMethod<T extends unknown[], R>(
): (...args: T) => Promise<R> {
return function instrumentedCall(...args: T): Promise<R> {
const operationName = instrumentedMethod.operation || 'unknown';
const requestAttributes = extractRequestAttributes(args, operationName);
const requestAttributes = extractRequestAttributes(args, operationName, !!options.recordInputs);
const model = (requestAttributes[GEN_AI_REQUEST_MODEL] as string) || 'unknown';

const params = args[0] as Record<string, unknown> | undefined;
Expand Down
2 changes: 1 addition & 1 deletion packages/server-utils/src/integrations/anthropic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ function createGenAiSpan(

const { recordInputs } = resolveAIRecordingOptions(options);

const attributes = extractRequestAttributes(args, operation);
const attributes = extractRequestAttributes(args, operation, recordInputs);
const model = (attributes[GEN_AI_REQUEST_MODEL] as string) || 'unknown';
attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN] = ORIGIN;
const client = getClient();
Expand Down
2 changes: 1 addition & 1 deletion packages/server-utils/src/integrations/google-genai.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ function createGenAiSpan(

const { recordInputs } = resolveAIRecordingOptions(options);

const attributes = extractRequestAttributes(operation, params, data.self);
const attributes = extractRequestAttributes(operation, params, data.self, recordInputs);
const model = (attributes[GEN_AI_REQUEST_MODEL] as string) || 'unknown';
attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN] = ORIGIN;
const client = getClient();
Expand Down
2 changes: 1 addition & 1 deletion packages/server-utils/src/integrations/openai.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ function createGenAiSpan(data: OpenAiChatChannelContext, operation: string, opti

const { recordInputs } = resolveAIRecordingOptions(options);

const attributes = extractRequestAttributes(args, operation);
const attributes = extractRequestAttributes(args, operation, recordInputs);
attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN] = ORIGIN;
const model = (params?.model as string) || 'unknown';
const client = getClient();
Expand Down
Loading