diff --git a/dev-packages/node-integration-tests/suites/aws-serverless/bedrock/test.ts b/dev-packages/node-integration-tests/suites/aws-serverless/bedrock/test.ts index 0a4f575ebb7e..af7e80bd10b2 100644 --- a/dev-packages/node-integration-tests/suites/aws-serverless/bedrock/test.ts +++ b/dev-packages/node-integration-tests/suites/aws-serverless/bedrock/test.ts @@ -1,59 +1,41 @@ -import type { TransactionEvent } from '@sentry/core'; +import type { SerializedStreamedSpanContainer } from '@sentry/core'; import { afterAll, describe, expect } from 'vitest'; -import { isOrchestrionEnabled } from '../../../utils'; import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runner'; -// The suite runs twice on CI: once with the OTel `Aws` integration (default) and once with the -// orchestrion diagnostics-channel integration auto-injected (`INJECT_ORCHESTRION`). Both emit the -// same gen_ai spans; only the origin differs. -const ORIGIN = isOrchestrionEnabled() ? 'auto.aws.aws_sdk' : 'auto.otel.aws'; - const MODEL_ID = 'anthropic.claude-3-5-sonnet-20240620-v1:0'; -function assertBedrockSpans(transaction: TransactionEvent): void { - const spans = transaction.spans ?? []; - - expect(transaction.transaction).toBe('Test Transaction'); - +function assertBedrockSpans(container: SerializedStreamedSpanContainer): void { // Converse (non-streaming) - expect(spans, 'expected a Bedrock Converse span').toContainEqual( - expect.objectContaining({ - description: `chat ${MODEL_ID}`, - origin: ORIGIN, - status: 'ok', - data: expect.objectContaining({ - 'sentry.origin': ORIGIN, - 'gen_ai.provider.name': 'aws.bedrock', - 'gen_ai.operation.name': 'chat', - 'gen_ai.request.model': MODEL_ID, - 'gen_ai.request.max_tokens': 100, - 'gen_ai.request.temperature': 0.5, - 'gen_ai.request.top_p': 0.9, - 'gen_ai.usage.input_tokens': 12, - 'gen_ai.usage.output_tokens': 8, - 'gen_ai.response.finish_reasons': ['end_turn'], - }), - }), - ); + const converseSpan = container.items.find(span => span.name === `chat ${MODEL_ID}`); + expect(converseSpan).toBeDefined(); + expect(converseSpan!.status).toBe('ok'); + expect(converseSpan!.attributes['sentry.origin'].value).toBe('auto.aws.aws_sdk'); + expect(converseSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); + expect(converseSpan!.attributes['gen_ai.provider.name'].value).toBe('aws.bedrock'); + expect(converseSpan!.attributes['gen_ai.operation.name'].value).toBe('chat'); + expect(converseSpan!.attributes['gen_ai.request.model'].value).toBe(MODEL_ID); + expect(converseSpan!.attributes['gen_ai.request.max_tokens'].value).toBe(100); + expect(converseSpan!.attributes['gen_ai.request.temperature'].value).toBe(0.5); + expect(converseSpan!.attributes['gen_ai.request.top_p'].value).toBe(0.9); + expect(converseSpan!.attributes['gen_ai.usage.input_tokens'].value).toBe(12); + expect(converseSpan!.attributes['gen_ai.usage.output_tokens'].value).toBe(8); + expect(converseSpan!.attributes['gen_ai.response.finish_reasons'].value).toEqual(['end_turn']); // InvokeModel (non-streaming, anthropic.claude request/response body) - expect(spans, 'expected a Bedrock InvokeModel span').toContainEqual( - expect.objectContaining({ - origin: ORIGIN, - status: 'ok', - data: expect.objectContaining({ - 'sentry.origin': ORIGIN, - 'gen_ai.provider.name': 'aws.bedrock', - 'gen_ai.request.model': MODEL_ID, - 'gen_ai.request.max_tokens': 100, - 'gen_ai.request.temperature': 0.5, - 'gen_ai.request.top_p': 0.9, - 'gen_ai.usage.input_tokens': 15, - 'gen_ai.usage.output_tokens': 9, - 'gen_ai.response.finish_reasons': ['end_turn'], - }), - }), - ); + const invokeModelSpan = container.items.find(span => span.name === `generate_content ${MODEL_ID}`); + expect(invokeModelSpan).toBeDefined(); + expect(invokeModelSpan!.status).toBe('ok'); + expect(invokeModelSpan!.attributes['sentry.origin'].value).toBe('auto.aws.aws_sdk'); + expect(invokeModelSpan!.attributes['sentry.op'].value).toBe('gen_ai.generate_content'); + expect(invokeModelSpan!.attributes['gen_ai.provider.name'].value).toBe('aws.bedrock'); + expect(invokeModelSpan!.attributes['gen_ai.operation.name'].value).toBe('generate_content'); + expect(invokeModelSpan!.attributes['gen_ai.request.model'].value).toBe(MODEL_ID); + expect(invokeModelSpan!.attributes['gen_ai.request.max_tokens'].value).toBe(100); + expect(invokeModelSpan!.attributes['gen_ai.request.temperature'].value).toBe(0.5); + expect(invokeModelSpan!.attributes['gen_ai.request.top_p'].value).toBe(0.9); + expect(invokeModelSpan!.attributes['gen_ai.usage.input_tokens'].value).toBe(15); + expect(invokeModelSpan!.attributes['gen_ai.usage.output_tokens'].value).toBe(9); + expect(invokeModelSpan!.attributes['gen_ai.response.finish_reasons'].value).toEqual(['end_turn']); } describe('awsIntegration - Bedrock', () => { @@ -67,7 +49,12 @@ describe('awsIntegration - Bedrock', () => { 'instrument.mjs', (createTestRunner, test) => { test('auto-instruments Bedrock Converse and InvokeModel', { timeout: 90_000 }, async () => { - await createTestRunner().ignore('event').expect({ transaction: assertBedrockSpans }).start().completed(); + await createTestRunner() + .ignore('event') + .expect({ transaction: transaction => expect(transaction.transaction).toBe('Test Transaction') }) + .expect({ span: assertBedrockSpans }) + .start() + .completed(); }); }, { additionalDependencies: { '@aws-sdk/client-bedrock-runtime': '^3.1046.0' } }, diff --git a/dev-packages/node-integration-tests/suites/tracing/anthropic/test.ts b/dev-packages/node-integration-tests/suites/tracing/anthropic/test.ts index 1c16ad136210..f1c94b2b1dd6 100644 --- a/dev-packages/node-integration-tests/suites/tracing/anthropic/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/anthropic/test.ts @@ -1,4 +1,5 @@ import { afterAll, describe, expect } from 'vitest'; +import type { TransactionEvent } from '@sentry/core'; import { GEN_AI_INPUT_MESSAGES, GEN_AI_OPERATION_NAME, @@ -27,16 +28,14 @@ describe('Anthropic integration', () => { cleanupChildProcesses(); }); - const EXPECTED_TRANSACTION_DEFAULT_PII_FALSE = { - transaction: 'main', - }; - - const EXPECTED_TRANSACTION_DEFAULT_PII_TRUE = { - transaction: 'main', - }; - - const EXPECTED_TRANSACTION_WITH_OPTIONS = { - transaction: 'main', + // `models.retrieve` reports a generic `function` op (model retrieval is not an inference call), so it is + // delivered as a plain transaction span rather than extracted into the gen_ai span container. + const expectModelsSpanOnTransaction = (event: TransactionEvent): void => { + expect(event.transaction).toBe('main'); + const modelsSpan = (event.spans ?? []).find(span => span.description === 'models claude-3-haiku-20240307'); + expect(modelsSpan).toBeDefined(); + expect(modelsSpan!.op).toBe('function'); + expect(modelsSpan!.status).toBe('ok'); }; const EXPECTED_MODEL_ERROR = { @@ -102,10 +101,10 @@ describe('Anthropic integration', () => { } await runner - .expect({ transaction: EXPECTED_TRANSACTION_DEFAULT_PII_FALSE }) + .expect({ transaction: expectModelsSpanOnTransaction }) .expect({ span: container => { - expect(container.items).toHaveLength(5); + expect(container.items).toHaveLength(4); const completionSpan = container.items.find( span => span.attributes[GEN_AI_RESPONSE_ID]?.value === 'msg_mock123', ); @@ -126,11 +125,6 @@ describe('Anthropic integration', () => { expect(tokenCountingSpan).toBeDefined(); expect(tokenCountingSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); - const modelsSpan = container.items.find(span => span.name === 'models claude-3-haiku-20240307'); - expect(modelsSpan).toBeDefined(); - expect(modelsSpan!.status).toBe('ok'); - expect(modelsSpan!.attributes['sentry.op'].value).toBe('gen_ai.models'); - const streamingSpan = container.items.find( span => span.attributes[GEN_AI_RESPONSE_ID]?.value === 'msg_stream123', ); @@ -157,10 +151,10 @@ describe('Anthropic integration', () => { } await runner - .expect({ transaction: EXPECTED_TRANSACTION_DEFAULT_PII_TRUE }) + .expect({ transaction: expectModelsSpanOnTransaction }) .expect({ span: container => { - expect(container.items).toHaveLength(5); + expect(container.items).toHaveLength(4); const completionSpan = container.items.find( span => span.attributes[GEN_AI_RESPONSE_ID]?.value === 'msg_mock123', ); @@ -200,11 +194,6 @@ describe('Anthropic integration', () => { expect(tokenCountingSpan!.status).toBe('ok'); expect(tokenCountingSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); - const modelsSpan = container.items.find(span => span.name === 'models claude-3-haiku-20240307'); - expect(modelsSpan).toBeDefined(); - expect(modelsSpan!.status).toBe('ok'); - expect(modelsSpan!.attributes['sentry.op'].value).toBe('gen_ai.models'); - // TODO: messages.stream() should produce its own distinct gen_ai span, but it // currently does not (pre-existing bug). Once fixed, add an additional indexed span assertion. const streamingSpan = container.items.find( @@ -240,10 +229,23 @@ describe('Anthropic integration', () => { } await runner - .expect({ transaction: EXPECTED_TRANSACTION_WITH_OPTIONS }) + .expect({ + transaction: event => { + expect(event.transaction).toBe('main'); + const modelsSpan = (event.spans ?? []).find(span => span.description === 'models claude-3-haiku-20240307'); + expect(modelsSpan).toBeDefined(); + expect(modelsSpan!.op).toBe('function'); + expect(modelsSpan!.status).toBe('ok'); + expect(modelsSpan!.data[GEN_AI_OPERATION_NAME]).toBe('models'); + expect(modelsSpan!.data[GEN_AI_PROVIDER_NAME]).toBe('anthropic'); + expect(modelsSpan!.data[GEN_AI_REQUEST_MODEL]).toBe('claude-3-haiku-20240307'); + expect(modelsSpan!.data[GEN_AI_RESPONSE_ID]).toBe('claude-3-haiku-20240307'); + expect(modelsSpan!.data[GEN_AI_RESPONSE_MODEL]).toBe('claude-3-haiku-20240307'); + }, + }) .expect({ span: container => { - expect(container.items).toHaveLength(5); + expect(container.items).toHaveLength(4); const completionSpan = container.items.find( span => span.attributes[GEN_AI_RESPONSE_ID]?.value === 'msg_mock123', ); @@ -268,16 +270,6 @@ describe('Anthropic integration', () => { expect(tokenCountingSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); expect(tokenCountingSpan!.attributes[GEN_AI_OPERATION_NAME].value).toBe('chat'); - const modelsSpan = container.items.find(span => span.name === 'models claude-3-haiku-20240307'); - expect(modelsSpan).toBeDefined(); - expect(modelsSpan!.status).toBe('ok'); - expect(modelsSpan!.attributes[GEN_AI_OPERATION_NAME].value).toBe('models'); - expect(modelsSpan!.attributes['sentry.op'].value).toBe('gen_ai.models'); - expect(modelsSpan!.attributes[GEN_AI_PROVIDER_NAME].value).toBe('anthropic'); - expect(modelsSpan!.attributes[GEN_AI_REQUEST_MODEL].value).toBe('claude-3-haiku-20240307'); - expect(modelsSpan!.attributes[GEN_AI_RESPONSE_ID].value).toBe('claude-3-haiku-20240307'); - expect(modelsSpan!.attributes[GEN_AI_RESPONSE_MODEL].value).toBe('claude-3-haiku-20240307'); - const streamingSpan = container.items.find( span => span.attributes[GEN_AI_RESPONSE_ID]?.value === 'msg_stream123', ); @@ -556,31 +548,29 @@ describe('Anthropic integration', () => { }); }); - // Additional error scenarios - Tool errors and model retrieval errors - const EXPECTED_ERROR_SPANS = { - transaction: 'main', - }; - createEsmAndCjsTests(__dirname, 'scenario-errors.mjs', 'instrument-with-pii.mjs', (createRunner, test) => { test('handles tool errors and model retrieval errors correctly', async () => { await createRunner() .ignore('event') - .expect({ transaction: EXPECTED_ERROR_SPANS }) + .expect({ + transaction: event => { + expect(event.transaction).toBe('main'); + const modelErrorSpan = (event.spans ?? []).find(span => span.description === 'models nonexistent-model'); + expect(modelErrorSpan).toBeDefined(); + expect(modelErrorSpan!.op).toBe('function'); + expect(modelErrorSpan!.status).toBe('internal_error'); + expect(modelErrorSpan!.data[GEN_AI_REQUEST_MODEL]).toBe('nonexistent-model'); + }, + }) .expect({ span: container => { - expect(container.items).toHaveLength(3); + expect(container.items).toHaveLength(2); const invalidFormatSpan = container.items.find(span => span.name === 'chat invalid-format'); expect(invalidFormatSpan).toBeDefined(); expect(invalidFormatSpan!.status).toBe('error'); expect(invalidFormatSpan!.attributes[GEN_AI_REQUEST_MODEL].value).toBe('invalid-format'); expect(invalidFormatSpan!.attributes['sentry.op'].value).toBe('gen_ai.chat'); - const modelErrorSpan = container.items.find(span => span.name === 'models nonexistent-model'); - expect(modelErrorSpan).toBeDefined(); - expect(modelErrorSpan!.status).toBe('error'); - expect(modelErrorSpan!.attributes[GEN_AI_REQUEST_MODEL].value).toBe('nonexistent-model'); - expect(modelErrorSpan!.attributes['sentry.op'].value).toBe('gen_ai.models'); - const toolSuccessSpan = container.items.find(span => span.name === 'chat claude-3-haiku-20240307'); expect(toolSuccessSpan).toBeDefined(); expect(toolSuccessSpan!.status).toBe('ok'); diff --git a/packages/server-utils/src/ai/anthropic-ai/index.ts b/packages/server-utils/src/ai/anthropic-ai/index.ts index 6dc1a257cd54..1d0f007f18ed 100644 --- a/packages/server-utils/src/ai/anthropic-ai/index.ts +++ b/packages/server-utils/src/ai/anthropic-ai/index.ts @@ -25,7 +25,12 @@ import { } from '@sentry/conventions/attributes'; import { GEN_AI_REQUEST_STREAM_ATTRIBUTE } from '../core/gen-ai-attributes'; import type { InstrumentedMethodEntry } from '../core/utils'; -import { resolveAIRecordingOptions, setTokenUsageAttributes, wrapPromiseWithMethods } from '../core/utils'; +import { + getGenAiSpanOp, + resolveAIRecordingOptions, + setTokenUsageAttributes, + wrapPromiseWithMethods, +} from '../core/utils'; import { ANTHROPIC_METHOD_REGISTRY } from './constants'; import { instrumentAsyncIterableStream, instrumentMessageStream } from './streaming'; import type { AnthropicAiOptions, AnthropicAiResponse, AnthropicAiStreamingEvent, ContentBlock } from './types'; @@ -67,13 +72,11 @@ export function extractRequestAttributes( 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 if (methodPath === 'models.retrieve' || methodPath === 'models.get') { + // `models.retrieve(model-id)` / `models.get(model-id)` pass the model id as a positional arg + attributes[GEN_AI_REQUEST_MODEL] = args[0]; } else { - if (methodPath === 'models.retrieve' || methodPath === 'models.get') { - // models.retrieve(model-id) and models.get(model-id) - attributes[GEN_AI_REQUEST_MODEL] = args[0]; - } else { - attributes[GEN_AI_REQUEST_MODEL] = 'unknown'; - } + attributes[GEN_AI_REQUEST_MODEL] = 'unknown'; } return attributes; @@ -204,7 +207,7 @@ function handleStreamingRequest( const model = requestAttributes[GEN_AI_REQUEST_MODEL] ?? 'unknown'; const spanConfig = { name: `${operationName} ${model}`, - op: `gen_ai.${operationName}`, + op: getGenAiSpanOp(operationName), attributes: requestAttributes as Record, }; @@ -310,7 +313,7 @@ function instrumentMethod( const instrumentedPromise = startSpan( { name: `${operationName} ${model}`, - op: `gen_ai.${operationName}`, + op: getGenAiSpanOp(operationName), attributes: requestAttributes as Record, }, span => { diff --git a/packages/server-utils/src/ai/core/utils.ts b/packages/server-utils/src/ai/core/utils.ts index 3d69a32f0461..c770778a3369 100644 --- a/packages/server-utils/src/ai/core/utils.ts +++ b/packages/server-utils/src/ai/core/utils.ts @@ -15,6 +15,7 @@ import { GEN_AI_USAGE_OUTPUT_TOKENS, GEN_AI_USAGE_TOTAL_TOKENS, } from '@sentry/conventions/attributes'; +import { GENERAL_FUNCTION_SPAN_OP } from '@sentry/conventions/op'; export interface AIRecordingOptions { recordInputs?: boolean; @@ -41,6 +42,20 @@ export interface InstrumentedMethodEntry { */ export type InstrumentedMethodRegistry = Record; +// Operation names that are not inference calls: `models` retrieves model metadata and `unknown` is +// the fallback for methods with no registered operation. Neither should surface as a `gen_ai.*` op +// (an unknown string must not masquerade as a convention), so they map to the generic `function` op. +// The operation name itself is preserved on `gen_ai.operation.name`. +const NON_INFERENCE_OPERATIONS = new Set(['models', 'unknown']); + +/** + * Derive the span op from a gen_ai operation name. Inference operations become `gen_ai.`; + * non-inference operations (`models`, `unknown`) become the generic `function` op. + */ +export function getGenAiSpanOp(operationName: string): string { + return NON_INFERENCE_OPERATIONS.has(operationName) ? GENERAL_FUNCTION_SPAN_OP : `gen_ai.${operationName}`; +} + /** * Resolves AI recording options by falling back to the client's `dataCollection.genAI` settings. * Precedence: explicit option > dataCollection.genAI > true (genAI data collected by default) diff --git a/packages/server-utils/src/ai/google-genai/index.ts b/packages/server-utils/src/ai/google-genai/index.ts index 160e004a3536..c7ec34b5b70d 100644 --- a/packages/server-utils/src/ai/google-genai/index.ts +++ b/packages/server-utils/src/ai/google-genai/index.ts @@ -32,7 +32,7 @@ import { GEN_AI_USAGE_TOTAL_TOKENS, } from '@sentry/conventions/attributes'; import type { InstrumentedMethodEntry } from '../core/utils'; -import { buildMethodPath, extractSystemInstructions, resolveAIRecordingOptions } from '../core/utils'; +import { buildMethodPath, extractSystemInstructions, getGenAiSpanOp, resolveAIRecordingOptions } from '../core/utils'; import { GOOGLE_GENAI_METHOD_REGISTRY, GOOGLE_GENAI_SYSTEM_NAME } from './constants'; import { instrumentStream } from './streaming'; import type { Candidate, ContentPart, GoogleGenAIOptions, GoogleGenAIResponse } from './types'; @@ -277,7 +277,7 @@ function instrumentMethod( return startSpanManual( { name: `${operationName} ${model}`, - op: `gen_ai.${operationName}`, + op: getGenAiSpanOp(operationName), attributes: requestAttributes, }, async (span: Span) => { @@ -306,7 +306,7 @@ function instrumentMethod( return startSpan( { name: `${operationName} ${model}`, - op: `gen_ai.${operationName}`, + op: getGenAiSpanOp(operationName), attributes: requestAttributes, }, (span: Span) => { diff --git a/packages/server-utils/src/ai/openai/index.ts b/packages/server-utils/src/ai/openai/index.ts index 78d27f4a1438..ded2f28d755d 100644 --- a/packages/server-utils/src/ai/openai/index.ts +++ b/packages/server-utils/src/ai/openai/index.ts @@ -23,6 +23,7 @@ import type { InstrumentedMethodEntry } from '../core/utils'; import { buildMethodPath, extractSystemInstructions, + getGenAiSpanOp, resolveAIRecordingOptions, wrapPromiseWithMethods, } from '../core/utils'; @@ -146,7 +147,7 @@ function instrumentMethod( const spanConfig = { name: `${operationName} ${model}`, - op: `gen_ai.${operationName}`, + op: getGenAiSpanOp(operationName), attributes: requestAttributes as Record, }; diff --git a/packages/server-utils/src/ai/vercel-ai/index.ts b/packages/server-utils/src/ai/vercel-ai/index.ts index 489ae85be5a4..5d9e84947cb7 100644 --- a/packages/server-utils/src/ai/vercel-ai/index.ts +++ b/packages/server-utils/src/ai/vercel-ai/index.ts @@ -30,6 +30,7 @@ import { GEN_AI_USAGE_REASONING_OUTPUT_TOKENS, GEN_AI_USAGE_TOTAL_TOKENS, } from '@sentry/conventions/attributes'; +import { GENERAL_FUNCTION_SPAN_OP } from '@sentry/conventions/op'; import { GEN_AI_TOOL_CALL_ID_ATTRIBUTE } from '../core/gen-ai-attributes'; import { SPAN_TO_OPERATION_NAME, toolCallSpanContextMap, toolDescriptionMap } from './constants'; import type { TokenSummary } from './types'; @@ -449,7 +450,9 @@ function processGenerateSpan(span: Span, name: string, attributes: SpanAttribute if (operationName) { span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, `gen_ai.${operationName}`); } else if (name.startsWith('ai.stream')) { - span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'ai.run'); + // Unmapped streaming pipeline span: we can't classify it as a specific gen_ai operation, + // so use the generic `function` op. + span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, GENERAL_FUNCTION_SPAN_OP); } // For invoke_agent pipeline spans, use 'invoke_agent' as the description diff --git a/packages/server-utils/src/integrations/tracing-channel/anthropic.ts b/packages/server-utils/src/integrations/tracing-channel/anthropic.ts index 02fe7f21db78..d3f13aa765da 100644 --- a/packages/server-utils/src/integrations/tracing-channel/anthropic.ts +++ b/packages/server-utils/src/integrations/tracing-channel/anthropic.ts @@ -7,7 +7,7 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startInactiveSpan, } from '@sentry/core'; -import { resolveAIRecordingOptions } from '../../ai/core/utils'; +import { getGenAiSpanOp, resolveAIRecordingOptions } from '../../ai/core/utils'; import { addPrivateRequestAttributes, addResponseAttributes, extractRequestAttributes } from '../../ai/anthropic-ai'; import { instrumentAsyncIterableStream, instrumentMessageStream } from '../../ai/anthropic-ai/streaming'; import type { AnthropicAiOptions, AnthropicAiResponse } from '../../ai/anthropic-ai/types'; @@ -105,7 +105,7 @@ function createGenAiSpan( const span = startInactiveSpan({ name: `${operation} ${model}`, - op: `gen_ai.${operation}`, + op: getGenAiSpanOp(operation), attributes: attributes as Record, }); diff --git a/packages/server-utils/src/integrations/tracing-channel/aws-sdk/constants.ts b/packages/server-utils/src/integrations/tracing-channel/aws-sdk/constants.ts index 96aff7cf5bb6..871dd4ff99bc 100644 --- a/packages/server-utils/src/integrations/tracing-channel/aws-sdk/constants.ts +++ b/packages/server-utils/src/integrations/tracing-channel/aws-sdk/constants.ts @@ -19,4 +19,5 @@ export const MESSAGING_DESTINATION_KIND_VALUE_TOPIC = 'topic'; // Bedrock (gen_ai) attribute values (not keys, so not covered by conventions) export const GEN_AI_OPERATION_NAME_VALUE_CHAT = 'chat'; +export const GEN_AI_OPERATION_NAME_VALUE_GENERATE_CONTENT = 'generate_content'; export const GEN_AI_SYSTEM_VALUE_AWS_BEDROCK = 'aws.bedrock'; diff --git a/packages/server-utils/src/integrations/tracing-channel/aws-sdk/services/bedrock-runtime.ts b/packages/server-utils/src/integrations/tracing-channel/aws-sdk/services/bedrock-runtime.ts index e5e24aea875f..64bae4e337c8 100644 --- a/packages/server-utils/src/integrations/tracing-channel/aws-sdk/services/bedrock-runtime.ts +++ b/packages/server-utils/src/integrations/tracing-channel/aws-sdk/services/bedrock-runtime.ts @@ -13,7 +13,11 @@ import { GEN_AI_USAGE_OUTPUT_TOKENS, } from '@sentry/conventions/attributes'; import { DEBUG_BUILD } from '../../../../debug-build'; -import { GEN_AI_OPERATION_NAME_VALUE_CHAT, GEN_AI_SYSTEM_VALUE_AWS_BEDROCK } from '../constants'; +import { + GEN_AI_OPERATION_NAME_VALUE_CHAT, + GEN_AI_OPERATION_NAME_VALUE_GENERATE_CONTENT, + GEN_AI_SYSTEM_VALUE_AWS_BEDROCK, +} from '../constants'; import type { NormalizedRequest, NormalizedResponse } from '../types'; import type { RequestMetadata, ServiceExtension } from './ServiceExtension'; @@ -113,19 +117,23 @@ export class BedrockRuntimeServiceExtension implements ServiceExtension { return { spanName, + spanOp: 'gen_ai.chat', isStream, spanAttributes, }; } private _requestPreSpanHookInvokeModel(request: NormalizedRequest, isStream: boolean): RequestMetadata { + let spanName = GEN_AI_OPERATION_NAME_VALUE_GENERATE_CONTENT; const spanAttributes: Record = { [GEN_AI_PROVIDER_NAME]: GEN_AI_SYSTEM_VALUE_AWS_BEDROCK, + [GEN_AI_OPERATION_NAME]: GEN_AI_OPERATION_NAME_VALUE_GENERATE_CONTENT, }; const modelId = request.commandInput?.modelId; if (modelId) { spanAttributes[GEN_AI_REQUEST_MODEL] = modelId; + spanName += ` ${modelId}`; } if (request.commandInput?.body) { @@ -234,6 +242,8 @@ export class BedrockRuntimeServiceExtension implements ServiceExtension { } return { + spanName, + spanOp: 'gen_ai.generate_content', isStream, spanAttributes, }; diff --git a/packages/server-utils/src/integrations/tracing-channel/google-genai.ts b/packages/server-utils/src/integrations/tracing-channel/google-genai.ts index 5e827095030b..b958263d41a1 100644 --- a/packages/server-utils/src/integrations/tracing-channel/google-genai.ts +++ b/packages/server-utils/src/integrations/tracing-channel/google-genai.ts @@ -9,7 +9,7 @@ import { spanToJSON, startInactiveSpan, } from '@sentry/core'; -import { resolveAIRecordingOptions } from '../../ai/core/utils'; +import { getGenAiSpanOp, resolveAIRecordingOptions } from '../../ai/core/utils'; import { addPrivateRequestAttributes, addResponseAttributes, extractRequestAttributes } from '../../ai/google-genai'; import { instrumentStream } from '../../ai/google-genai/streaming'; import type { GoogleGenAIOptions, GoogleGenAIResponse } from '../../ai/google-genai/types'; @@ -109,7 +109,7 @@ function createGenAiSpan( const span = startInactiveSpan({ name: `${operation} ${model}`, - op: `gen_ai.${operation}`, + op: getGenAiSpanOp(operation), attributes, }); diff --git a/packages/server-utils/src/integrations/tracing-channel/openai.ts b/packages/server-utils/src/integrations/tracing-channel/openai.ts index 0eda1f62c57c..0e481c985c80 100644 --- a/packages/server-utils/src/integrations/tracing-channel/openai.ts +++ b/packages/server-utils/src/integrations/tracing-channel/openai.ts @@ -6,7 +6,7 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startInactiveSpan, } from '@sentry/core'; -import { resolveAIRecordingOptions } from '../../ai/core/utils'; +import { getGenAiSpanOp, resolveAIRecordingOptions } from '../../ai/core/utils'; import { addRequestAttributes, extractRequestAttributes } from '../../ai/openai'; import { instrumentStream } from '../../ai/openai/streaming'; import type { OpenAiOptions } from '../../ai/openai/types'; @@ -85,7 +85,7 @@ function createGenAiSpan(data: OpenAiChatChannelContext, operation: string, opti const span = startInactiveSpan({ name: `${operation} ${model}`, - op: `gen_ai.${operation}`, + op: getGenAiSpanOp(operation), attributes: attributes as Record, });