diff --git a/dev-packages/browser-integration-tests/suites/tracing/ai-providers/google-genai/mocks.js b/dev-packages/browser-integration-tests/suites/tracing/ai-providers/google-genai/mocks.js index 8aab37fb3a1e..d33f5dfbb285 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/ai-providers/google-genai/mocks.js +++ b/dev-packages/browser-integration-tests/suites/tracing/ai-providers/google-genai/mocks.js @@ -39,6 +39,24 @@ export class MockGoogleGenAI { }, }; }, + embedContent: async (...args) => { + const params = args[0]; + await new Promise(resolve => setTimeout(resolve, 10)); + + if (params.model === 'error-model') { + const error = new Error('Model not found'); + error.status = 404; + throw error; + } + + return { + embeddings: [ + { + values: [0.1, 0.2, 0.3, 0.4, 0.5], + }, + ], + }; + }, generateContentStream: async () => { // Return a promise that resolves to an async generator return (async function* () { diff --git a/dev-packages/browser-integration-tests/suites/tracing/ai-providers/google-genai/subject.js b/dev-packages/browser-integration-tests/suites/tracing/ai-providers/google-genai/subject.js index 14b95f2b6942..b506ec52195b 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/ai-providers/google-genai/subject.js +++ b/dev-packages/browser-integration-tests/suites/tracing/ai-providers/google-genai/subject.js @@ -30,3 +30,11 @@ const response = await chat.sendMessage({ }); console.log('Received response', response); + +// Test embedContent +const embedResponse = await client.models.embedContent({ + model: 'text-embedding-004', + contents: 'Hello world', +}); + +console.log('Received embed response', embedResponse); diff --git a/dev-packages/node-integration-tests/suites/tracing/google-genai/scenario-embeddings.mjs b/dev-packages/node-integration-tests/suites/tracing/google-genai/scenario-embeddings.mjs new file mode 100644 index 000000000000..166e741cf199 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/google-genai/scenario-embeddings.mjs @@ -0,0 +1,77 @@ +import { GoogleGenAI } from '@google/genai'; +import * as Sentry from '@sentry/node'; +import express from 'express'; + +function startMockGoogleGenAIServer() { + const app = express(); + app.use(express.json()); + + app.post('/v1beta/models/:model\\:batchEmbedContents', (req, res) => { + const model = req.params.model; + + if (model === 'error-model') { + res.status(404).set('x-request-id', 'mock-request-123').end('Model not found'); + return; + } + + res.send({ + embeddings: [ + { + values: [0.1, 0.2, 0.3, 0.4, 0.5], + }, + ], + }); + }); + + return new Promise(resolve => { + const server = app.listen(0, () => { + resolve(server); + }); + }); +} + +async function run() { + const server = await startMockGoogleGenAIServer(); + + await Sentry.startSpan({ op: 'function', name: 'main' }, async () => { + const client = new GoogleGenAI({ + apiKey: 'mock-api-key', + httpOptions: { baseUrl: `http://localhost:${server.address().port}` }, + }); + + // Test 1: Basic embedContent with string contents + await client.models.embedContent({ + model: 'text-embedding-004', + contents: 'What is the capital of France?', + }); + + // Test 2: Error handling + try { + await client.models.embedContent({ + model: 'error-model', + contents: 'This will fail', + }); + } catch { + // Expected error + } + + // Test 3: embedContent with array contents + await client.models.embedContent({ + model: 'text-embedding-004', + contents: [ + { + role: 'user', + parts: [{ text: 'First input text' }], + }, + { + role: 'user', + parts: [{ text: 'Second input text' }], + }, + ], + }); + }); + + server.close(); +} + +run(); diff --git a/dev-packages/node-integration-tests/suites/tracing/google-genai/test.ts b/dev-packages/node-integration-tests/suites/tracing/google-genai/test.ts index 993984cc6b3d..91784a2de0e5 100644 --- a/dev-packages/node-integration-tests/suites/tracing/google-genai/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/google-genai/test.ts @@ -1,6 +1,7 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; import { afterAll, describe, expect } from 'vitest'; import { + GEN_AI_EMBEDDINGS_INPUT_ATTRIBUTE, GEN_AI_INPUT_MESSAGES_ATTRIBUTE, GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE, GEN_AI_OPERATION_NAME_ATTRIBUTE, @@ -601,4 +602,124 @@ describe('Google GenAI integration', () => { }); }, ); + + const EXPECTED_TRANSACTION_DEFAULT_PII_FALSE_EMBEDDINGS = { + transaction: 'main', + spans: expect.arrayContaining([ + // First span - embedContent with string contents + expect.objectContaining({ + data: { + [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'embeddings', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.embeddings', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.google_genai', + [GEN_AI_SYSTEM_ATTRIBUTE]: 'google_genai', + [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'text-embedding-004', + }, + description: 'embeddings text-embedding-004', + op: 'gen_ai.embeddings', + origin: 'auto.ai.google_genai', + status: 'ok', + }), + // Second span - embedContent error model + expect.objectContaining({ + data: { + [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'embeddings', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.embeddings', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.google_genai', + [GEN_AI_SYSTEM_ATTRIBUTE]: 'google_genai', + [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'error-model', + }, + description: 'embeddings error-model', + op: 'gen_ai.embeddings', + origin: 'auto.ai.google_genai', + status: 'internal_error', + }), + // Third span - embedContent with array contents + expect.objectContaining({ + data: { + [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'embeddings', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.embeddings', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.google_genai', + [GEN_AI_SYSTEM_ATTRIBUTE]: 'google_genai', + [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'text-embedding-004', + }, + description: 'embeddings text-embedding-004', + op: 'gen_ai.embeddings', + origin: 'auto.ai.google_genai', + status: 'ok', + }), + ]), + }; + + const EXPECTED_TRANSACTION_DEFAULT_PII_TRUE_EMBEDDINGS = { + transaction: 'main', + spans: expect.arrayContaining([ + // First span - embedContent with PII + expect.objectContaining({ + data: { + [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'embeddings', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.embeddings', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.google_genai', + [GEN_AI_SYSTEM_ATTRIBUTE]: 'google_genai', + [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'text-embedding-004', + [GEN_AI_EMBEDDINGS_INPUT_ATTRIBUTE]: 'What is the capital of France?', + }, + description: 'embeddings text-embedding-004', + op: 'gen_ai.embeddings', + origin: 'auto.ai.google_genai', + status: 'ok', + }), + // Second span - embedContent error model with PII + expect.objectContaining({ + data: { + [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'embeddings', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.embeddings', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.google_genai', + [GEN_AI_SYSTEM_ATTRIBUTE]: 'google_genai', + [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'error-model', + [GEN_AI_EMBEDDINGS_INPUT_ATTRIBUTE]: 'This will fail', + }, + description: 'embeddings error-model', + op: 'gen_ai.embeddings', + origin: 'auto.ai.google_genai', + status: 'internal_error', + }), + // Third span - embedContent with array contents and PII + expect.objectContaining({ + data: { + [GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'embeddings', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.embeddings', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.google_genai', + [GEN_AI_SYSTEM_ATTRIBUTE]: 'google_genai', + [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'text-embedding-004', + [GEN_AI_EMBEDDINGS_INPUT_ATTRIBUTE]: + '[{"role":"user","parts":[{"text":"First input text"}]},{"role":"user","parts":[{"text":"Second input text"}]}]', + }, + description: 'embeddings text-embedding-004', + op: 'gen_ai.embeddings', + origin: 'auto.ai.google_genai', + status: 'ok', + }), + ]), + }; + + createEsmAndCjsTests(__dirname, 'scenario-embeddings.mjs', 'instrument.mjs', (createRunner, test) => { + test('creates google genai embeddings spans with sendDefaultPii: false', async () => { + await createRunner() + .ignore('event') + .expect({ transaction: EXPECTED_TRANSACTION_DEFAULT_PII_FALSE_EMBEDDINGS }) + .start() + .completed(); + }); + }); + + createEsmAndCjsTests(__dirname, 'scenario-embeddings.mjs', 'instrument-with-pii.mjs', (createRunner, test) => { + test('creates google genai embeddings spans with sendDefaultPii: true', async () => { + await createRunner() + .ignore('event') + .expect({ transaction: EXPECTED_TRANSACTION_DEFAULT_PII_TRUE_EMBEDDINGS }) + .start() + .completed(); + }); + }); }); diff --git a/packages/core/src/tracing/ai/utils.ts b/packages/core/src/tracing/ai/utils.ts index 8f08b65c6171..e3618b326801 100644 --- a/packages/core/src/tracing/ai/utils.ts +++ b/packages/core/src/tracing/ai/utils.ts @@ -23,6 +23,10 @@ export function getFinalOperationName(methodPath: string): string { if (methodPath.includes('generateContent')) { return 'generate_content'; } + // Google GenAI: models.embedContent -> embeddings + if (methodPath.includes('embedContent')) { + return 'embeddings'; + } // Anthropic: models.get/retrieve -> models (metadata retrieval only) if (methodPath.includes('models')) { return 'models'; diff --git a/packages/core/src/tracing/google-genai/constants.ts b/packages/core/src/tracing/google-genai/constants.ts index b06e46e18755..22f1d3c5c057 100644 --- a/packages/core/src/tracing/google-genai/constants.ts +++ b/packages/core/src/tracing/google-genai/constants.ts @@ -7,6 +7,7 @@ export const GOOGLE_GENAI_INTEGRATION_NAME = 'Google_GenAI'; export const GOOGLE_GENAI_INSTRUMENTED_METHODS = [ 'models.generateContent', 'models.generateContentStream', + 'models.embedContent', 'chats.create', 'sendMessage', 'sendMessageStream', diff --git a/packages/core/src/tracing/google-genai/index.ts b/packages/core/src/tracing/google-genai/index.ts index 7781b67d6db0..d0559d050359 100644 --- a/packages/core/src/tracing/google-genai/index.ts +++ b/packages/core/src/tracing/google-genai/index.ts @@ -1,26 +1,17 @@ import { getClient } from '../../currentScopes'; import { captureException } from '../../exports'; -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '../../semanticAttributes'; import { SPAN_STATUS_ERROR } from '../../tracing'; import { startSpan, startSpanManual } from '../../tracing/trace'; -import type { Span, SpanAttributeValue } from '../../types-hoist/span'; +import type { Span } from '../../types-hoist/span'; import { handleCallbackErrors } from '../../utils/handleCallbackErrors'; import { + GEN_AI_EMBEDDINGS_INPUT_ATTRIBUTE, GEN_AI_INPUT_MESSAGES_ATTRIBUTE, GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE, - GEN_AI_OPERATION_NAME_ATTRIBUTE, - GEN_AI_REQUEST_AVAILABLE_TOOLS_ATTRIBUTE, - GEN_AI_REQUEST_FREQUENCY_PENALTY_ATTRIBUTE, - GEN_AI_REQUEST_MAX_TOKENS_ATTRIBUTE, GEN_AI_REQUEST_MODEL_ATTRIBUTE, - GEN_AI_REQUEST_PRESENCE_PENALTY_ATTRIBUTE, - GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE, - GEN_AI_REQUEST_TOP_K_ATTRIBUTE, - GEN_AI_REQUEST_TOP_P_ATTRIBUTE, GEN_AI_RESPONSE_MODEL_ATTRIBUTE, GEN_AI_RESPONSE_TEXT_ATTRIBUTE, GEN_AI_RESPONSE_TOOL_CALLS_ATTRIBUTE, - GEN_AI_SYSTEM_ATTRIBUTE, GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE, GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE, GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE, @@ -28,7 +19,7 @@ import { } from '../ai/gen-ai-attributes'; import { truncateGenAiMessages } from '../ai/messageTruncation'; import { buildMethodPath, extractSystemInstructions, getFinalOperationName, getSpanOperation } from '../ai/utils'; -import { CHAT_PATH, CHATS_CREATE_METHOD, GOOGLE_GENAI_SYSTEM_NAME } from './constants'; +import { CHAT_PATH, CHATS_CREATE_METHOD } from './constants'; import { instrumentStream } from './streaming'; import type { Candidate, @@ -38,107 +29,31 @@ import type { GoogleGenAIResponse, } from './types'; import type { ContentListUnion, ContentUnion, Message, PartListUnion } from './utils'; -import { contentUnionToMessages, isStreamingMethod, shouldInstrument } from './utils'; - -/** - * Extract model from parameters or chat context object - * For chat instances, the model is available on the chat object as 'model' (older versions) or 'modelVersion' (newer versions) - */ -export function extractModel(params: Record, context?: unknown): string { - if ('model' in params && typeof params.model === 'string') { - return params.model; - } - - // Try to get model from chat context object (chat instance has model property) - if (context && typeof context === 'object') { - const contextObj = context as Record; - - // Check for 'model' property (older versions, and streaming) - if ('model' in contextObj && typeof contextObj.model === 'string') { - return contextObj.model; - } - - // Check for 'modelVersion' property (newer versions) - if ('modelVersion' in contextObj && typeof contextObj.modelVersion === 'string') { - return contextObj.modelVersion; - } - } - - return 'unknown'; -} - -/** - * Extract generation config parameters - */ -function extractConfigAttributes(config: Record): Record { - const attributes: Record = {}; - - if ('temperature' in config && typeof config.temperature === 'number') { - attributes[GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE] = config.temperature; - } - if ('topP' in config && typeof config.topP === 'number') { - attributes[GEN_AI_REQUEST_TOP_P_ATTRIBUTE] = config.topP; - } - if ('topK' in config && typeof config.topK === 'number') { - attributes[GEN_AI_REQUEST_TOP_K_ATTRIBUTE] = config.topK; - } - if ('maxOutputTokens' in config && typeof config.maxOutputTokens === 'number') { - attributes[GEN_AI_REQUEST_MAX_TOKENS_ATTRIBUTE] = config.maxOutputTokens; - } - if ('frequencyPenalty' in config && typeof config.frequencyPenalty === 'number') { - attributes[GEN_AI_REQUEST_FREQUENCY_PENALTY_ATTRIBUTE] = config.frequencyPenalty; - } - if ('presencePenalty' in config && typeof config.presencePenalty === 'number') { - attributes[GEN_AI_REQUEST_PRESENCE_PENALTY_ATTRIBUTE] = config.presencePenalty; - } - - return attributes; -} - -/** - * Extract request attributes from method arguments - * Builds the base attributes for span creation including system info, model, and config - */ -function extractRequestAttributes( - methodPath: string, - params?: Record, - context?: unknown, -): Record { - const attributes: Record = { - [GEN_AI_SYSTEM_ATTRIBUTE]: GOOGLE_GENAI_SYSTEM_NAME, - [GEN_AI_OPERATION_NAME_ATTRIBUTE]: getFinalOperationName(methodPath), - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.google_genai', - }; - - if (params) { - attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE] = extractModel(params, context); - - // Extract generation config parameters - if ('config' in params && typeof params.config === 'object' && params.config) { - const config = params.config as Record; - Object.assign(attributes, extractConfigAttributes(config)); - - // Extract available tools from config - if ('tools' in config && Array.isArray(config.tools)) { - const functionDeclarations = config.tools.flatMap( - (tool: { functionDeclarations: unknown[] }) => tool.functionDeclarations, - ); - attributes[GEN_AI_REQUEST_AVAILABLE_TOOLS_ATTRIBUTE] = JSON.stringify(functionDeclarations); - } - } - } else { - attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE] = extractModel({}, context); - } - - return attributes; -} +import { + contentUnionToMessages, + extractRequestAttributes, + isEmbeddingsMethod, + isStreamingMethod, + shouldInstrument, +} from './utils'; /** * Add private request attributes to spans. * This is only recorded if recordInputs is true. * Handles different parameter formats for different Google GenAI methods. */ -function addPrivateRequestAttributes(span: Span, params: Record): void { +function addPrivateRequestAttributes(span: Span, params: Record, isEmbeddings: boolean): void { + if (isEmbeddings) { + const contents = params.contents; + if (contents != null) { + span.setAttribute( + GEN_AI_EMBEDDINGS_INPUT_ATTRIBUTE, + typeof contents === 'string' ? contents : JSON.stringify(contents), + ); + } + return; + } + const messages: Message[] = []; // config.systemInstruction: ContentUnion @@ -257,6 +172,7 @@ function instrumentMethod( options: GoogleGenAIOptions, ): (...args: T) => R | Promise { const isSyncCreate = methodPath === CHATS_CREATE_METHOD; + const isEmbeddings = isEmbeddingsMethod(methodPath); return new Proxy(originalMethod, { apply(target, _, args: T): R | Promise { @@ -277,7 +193,7 @@ function instrumentMethod( async (span: Span) => { try { if (options.recordInputs && params) { - addPrivateRequestAttributes(span, params); + addPrivateRequestAttributes(span, params, isEmbeddings); } const stream = await target.apply(context, args); return instrumentStream(stream, span, Boolean(options.recordOutputs)) as R; @@ -305,7 +221,7 @@ function instrumentMethod( }, (span: Span) => { if (options.recordInputs && params) { - addPrivateRequestAttributes(span, params); + addPrivateRequestAttributes(span, params, isEmbeddings); } return handleCallbackErrors( @@ -318,7 +234,8 @@ function instrumentMethod( () => {}, result => { // Only add response attributes for content-producing methods, not for chats.create - if (!isSyncCreate) { + // Note: embeddings responses don't include usage metadata or model version + if (!isSyncCreate && !isEmbeddings) { addResponseAttributes(span, result, options.recordOutputs); } }, diff --git a/packages/core/src/tracing/google-genai/utils.ts b/packages/core/src/tracing/google-genai/utils.ts index 4280957ce43f..3a6e1c67c377 100644 --- a/packages/core/src/tracing/google-genai/utils.ts +++ b/packages/core/src/tracing/google-genai/utils.ts @@ -1,4 +1,19 @@ -import { GOOGLE_GENAI_INSTRUMENTED_METHODS } from './constants'; +import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '../../semanticAttributes'; +import type { SpanAttributeValue } from '../../types-hoist/span'; +import { + GEN_AI_OPERATION_NAME_ATTRIBUTE, + GEN_AI_REQUEST_AVAILABLE_TOOLS_ATTRIBUTE, + GEN_AI_REQUEST_FREQUENCY_PENALTY_ATTRIBUTE, + GEN_AI_REQUEST_MAX_TOKENS_ATTRIBUTE, + GEN_AI_REQUEST_MODEL_ATTRIBUTE, + GEN_AI_REQUEST_PRESENCE_PENALTY_ATTRIBUTE, + GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE, + GEN_AI_REQUEST_TOP_K_ATTRIBUTE, + GEN_AI_REQUEST_TOP_P_ATTRIBUTE, + GEN_AI_SYSTEM_ATTRIBUTE, +} from '../ai/gen-ai-attributes'; +import { getFinalOperationName } from '../ai/utils'; +import { GOOGLE_GENAI_INSTRUMENTED_METHODS, GOOGLE_GENAI_SYSTEM_NAME } from './constants'; import type { GoogleGenAIIstrumentedMethod } from './types'; /** @@ -22,6 +37,106 @@ export function isStreamingMethod(methodPath: string): boolean { return methodPath.includes('Stream'); } +/** + * Check if a method is an embeddings method + */ +export function isEmbeddingsMethod(methodPath: string): boolean { + return methodPath.includes('embedContent'); +} + +/** + * Extract model from parameters or chat context object + * For chat instances, the model is available on the chat object as 'model' (older versions) or 'modelVersion' (newer versions) + */ +function extractModel(params: Record, context?: unknown): string { + if ('model' in params && typeof params.model === 'string') { + return params.model; + } + + // Try to get model from chat context object (chat instance has model property) + if (context && typeof context === 'object') { + const contextObj = context as Record; + + // Check for 'model' property (older versions, and streaming) + if ('model' in contextObj && typeof contextObj.model === 'string') { + return contextObj.model; + } + + // Check for 'modelVersion' property (newer versions) + if ('modelVersion' in contextObj && typeof contextObj.modelVersion === 'string') { + return contextObj.modelVersion; + } + } + + return 'unknown'; +} + +/** + * Extract generation config parameters + */ +function extractConfigAttributes(config: Record): Record { + const attributes: Record = {}; + + if ('temperature' in config && typeof config.temperature === 'number') { + attributes[GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE] = config.temperature; + } + if ('topP' in config && typeof config.topP === 'number') { + attributes[GEN_AI_REQUEST_TOP_P_ATTRIBUTE] = config.topP; + } + if ('topK' in config && typeof config.topK === 'number') { + attributes[GEN_AI_REQUEST_TOP_K_ATTRIBUTE] = config.topK; + } + if ('maxOutputTokens' in config && typeof config.maxOutputTokens === 'number') { + attributes[GEN_AI_REQUEST_MAX_TOKENS_ATTRIBUTE] = config.maxOutputTokens; + } + if ('frequencyPenalty' in config && typeof config.frequencyPenalty === 'number') { + attributes[GEN_AI_REQUEST_FREQUENCY_PENALTY_ATTRIBUTE] = config.frequencyPenalty; + } + if ('presencePenalty' in config && typeof config.presencePenalty === 'number') { + attributes[GEN_AI_REQUEST_PRESENCE_PENALTY_ATTRIBUTE] = config.presencePenalty; + } + + return attributes; +} + +/** + * Extract request attributes from method arguments + * Builds the base attributes for span creation including system info, model, and config + */ +export function extractRequestAttributes( + methodPath: string, + params?: Record, + context?: unknown, +): Record { + const attributes: Record = { + [GEN_AI_SYSTEM_ATTRIBUTE]: GOOGLE_GENAI_SYSTEM_NAME, + [GEN_AI_OPERATION_NAME_ATTRIBUTE]: getFinalOperationName(methodPath), + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.google_genai', + }; + + if (params) { + attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE] = extractModel(params, context); + + // Extract generation config parameters + if ('config' in params && typeof params.config === 'object' && params.config) { + const config = params.config as Record; + Object.assign(attributes, extractConfigAttributes(config)); + + // Extract available tools from config + if ('tools' in config && Array.isArray(config.tools)) { + const functionDeclarations = config.tools.flatMap( + (tool: { functionDeclarations: unknown[] }) => tool.functionDeclarations, + ); + attributes[GEN_AI_REQUEST_AVAILABLE_TOOLS_ATTRIBUTE] = JSON.stringify(functionDeclarations); + } + } + } else { + attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE] = extractModel({}, context); + } + + return attributes; +} + // Copied from https://googleapis.github.io/js-genai/release_docs/index.html export type ContentListUnion = Content | Content[] | PartListUnion; export type ContentUnion = Content | PartUnion[] | PartUnion; diff --git a/packages/core/test/lib/utils/google-genai-utils.test.ts b/packages/core/test/lib/utils/google-genai-utils.test.ts index 7b9c6d80c773..902b66c8d01e 100644 --- a/packages/core/test/lib/utils/google-genai-utils.test.ts +++ b/packages/core/test/lib/utils/google-genai-utils.test.ts @@ -1,6 +1,11 @@ import { describe, expect, it } from 'vitest'; import type { ContentListUnion } from '../../../src/tracing/google-genai/utils'; -import { contentUnionToMessages, isStreamingMethod, shouldInstrument } from '../../../src/tracing/google-genai/utils'; +import { + contentUnionToMessages, + isEmbeddingsMethod, + isStreamingMethod, + shouldInstrument, +} from '../../../src/tracing/google-genai/utils'; describe('isStreamingMethod', () => { it('detects streaming methods', () => { @@ -14,9 +19,19 @@ describe('isStreamingMethod', () => { }); }); +describe('isEmbeddingsMethod', () => { + it('detects embeddings methods', () => { + expect(isEmbeddingsMethod('models.embedContent')).toBe(true); + expect(isEmbeddingsMethod('embedContent')).toBe(true); + expect(isEmbeddingsMethod('models.generateContent')).toBe(false); + expect(isEmbeddingsMethod('sendMessage')).toBe(false); + }); +}); + describe('shouldInstrument', () => { it('detects which methods to instrument', () => { expect(shouldInstrument('models.generateContent')).toBe(true); + expect(shouldInstrument('models.embedContent')).toBe(true); expect(shouldInstrument('some.path.to.sendMessage')).toBe(true); expect(shouldInstrument('unknown')).toBe(false); });