From df4ba73e45029735df173f8a0b03e789bb878a9a Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Fri, 13 Mar 2026 12:15:04 +0100 Subject: [PATCH 01/10] instrument google genai embeddings api --- .../ai-providers/google-genai/mocks.js | 25 ++++ .../ai-providers/google-genai/subject.js | 8 ++ .../google-genai/scenario-embeddings.mjs | 84 ++++++++++++ .../suites/tracing/google-genai/test.ts | 128 ++++++++++++++++++ packages/core/src/tracing/ai/utils.ts | 4 + .../src/tracing/google-genai/constants.ts | 2 + .../src/tracing/google-genai/embeddings.ts | 65 +++++++++ .../core/src/tracing/google-genai/index.ts | 24 ++-- .../core/src/tracing/google-genai/types.ts | 31 +++++ .../core/src/tracing/google-genai/utils.ts | 7 + .../test/lib/utils/google-genai-utils.test.ts | 17 ++- 11 files changed, 384 insertions(+), 11 deletions(-) create mode 100644 dev-packages/node-integration-tests/suites/tracing/google-genai/scenario-embeddings.mjs create mode 100644 packages/core/src/tracing/google-genai/embeddings.ts 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..abc446ad2fdb 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,31 @@ 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], + statistics: { + tokenCount: 8, + truncated: false, + }, + }, + ], + metadata: { + billableCharacterCount: 30, + }, + }; + }, 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..7b5704e8b98e --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/google-genai/scenario-embeddings.mjs @@ -0,0 +1,84 @@ +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\\:embedContent', (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], + statistics: { + tokenCount: 8, + truncated: false, + }, + }, + ], + metadata: { + billableCharacterCount: 30, + }, + }); + }); + + 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..e65fa3b65797 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,131 @@ 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', + [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 8, + [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 8, + }, + 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', + [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 8, + [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 8, + }, + 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?', + [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 8, + [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 8, + }, + 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]: expect.any(String), + [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 8, + [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 8, + }, + 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..b26c170d9cf3 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', @@ -15,4 +16,5 @@ export const GOOGLE_GENAI_INSTRUMENTED_METHODS = [ // Constants for internal use export const GOOGLE_GENAI_SYSTEM_NAME = 'google_genai'; export const CHATS_CREATE_METHOD = 'chats.create'; +export const EMBED_CONTENT_METHOD = 'models.embedContent'; export const CHAT_PATH = 'chat'; diff --git a/packages/core/src/tracing/google-genai/embeddings.ts b/packages/core/src/tracing/google-genai/embeddings.ts new file mode 100644 index 000000000000..cfc63d8bd671 --- /dev/null +++ b/packages/core/src/tracing/google-genai/embeddings.ts @@ -0,0 +1,65 @@ +import { + GEN_AI_EMBEDDINGS_INPUT_ATTRIBUTE, + GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE, + GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE, +} from '../ai/gen-ai-attributes'; +import type { Span } from '../../types-hoist/span'; +import type { GoogleGenAIEmbedContentResponse } from './types'; + +/** + * Add private request attributes for embeddings methods. + * Records the embeddings input on gen_ai.embeddings.input instead of gen_ai.input.messages. + * The input is NOT truncated (matching OpenAI behavior). + */ +export function addEmbeddingsRequestAttributes(span: Span, params: Record): void { + if (!('contents' in params)) { + return; + } + + const contents = params.contents; + + if (contents == null) { + return; + } + + if (typeof contents === 'string' && contents.length === 0) { + return; + } + + if (Array.isArray(contents) && contents.length === 0) { + return; + } + + span.setAttribute( + GEN_AI_EMBEDDINGS_INPUT_ATTRIBUTE, + typeof contents === 'string' ? contents : JSON.stringify(contents), + ); +} + +/** + * Add response attributes from the Google GenAI embedContent response. + * The EmbedContentResponse has no usageMetadata/candidates/modelVersion. + * Token counts come from embeddings[].statistics.tokenCount. + * @see https://ai.google.dev/api/embeddings#EmbedContentResponse + */ +export function addEmbedContentResponseAttributes(span: Span, response: unknown): void { + if (!response || typeof response !== 'object') return; + + const embedResponse = response as GoogleGenAIEmbedContentResponse; + + if (Array.isArray(embedResponse.embeddings)) { + let totalTokenCount = 0; + for (const embedding of embedResponse.embeddings) { + if (embedding.statistics && typeof embedding.statistics.tokenCount === 'number') { + totalTokenCount += embedding.statistics.tokenCount; + } + } + + if (totalTokenCount > 0) { + span.setAttributes({ + [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: totalTokenCount, + [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: totalTokenCount, + }); + } + } +} diff --git a/packages/core/src/tracing/google-genai/index.ts b/packages/core/src/tracing/google-genai/index.ts index 7781b67d6db0..95f08f50564d 100644 --- a/packages/core/src/tracing/google-genai/index.ts +++ b/packages/core/src/tracing/google-genai/index.ts @@ -29,16 +29,11 @@ import { 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 { addEmbedContentResponseAttributes, addEmbeddingsRequestAttributes } from './embeddings'; import { instrumentStream } from './streaming'; -import type { - Candidate, - ContentPart, - GoogleGenAIIstrumentedMethod, - GoogleGenAIOptions, - GoogleGenAIResponse, -} from './types'; +import type { Candidate, ContentPart, GoogleGenAIIstrumentedMethod, GoogleGenAIOptions, GoogleGenAIResponse } from './types'; import type { ContentListUnion, ContentUnion, Message, PartListUnion } from './utils'; -import { contentUnionToMessages, isStreamingMethod, shouldInstrument } from './utils'; +import { contentUnionToMessages, isEmbeddingsMethod, isStreamingMethod, shouldInstrument } from './utils'; /** * Extract model from parameters or chat context object @@ -257,6 +252,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 { @@ -305,7 +301,11 @@ function instrumentMethod( }, (span: Span) => { if (options.recordInputs && params) { - addPrivateRequestAttributes(span, params); + if (isEmbeddings) { + addEmbeddingsRequestAttributes(span, params); + } else { + addPrivateRequestAttributes(span, params); + } } return handleCallbackErrors( @@ -319,7 +319,11 @@ function instrumentMethod( result => { // Only add response attributes for content-producing methods, not for chats.create if (!isSyncCreate) { - addResponseAttributes(span, result, options.recordOutputs); + if (isEmbeddings) { + addEmbedContentResponseAttributes(span, result); + } else { + addResponseAttributes(span, result, options.recordOutputs); + } } }, ); diff --git a/packages/core/src/tracing/google-genai/types.ts b/packages/core/src/tracing/google-genai/types.ts index 9a2138a7843d..6752dd15693f 100644 --- a/packages/core/src/tracing/google-genai/types.ts +++ b/packages/core/src/tracing/google-genai/types.ts @@ -181,5 +181,36 @@ export interface GoogleGenAIChat { export type GoogleGenAIIstrumentedMethod = (typeof GOOGLE_GENAI_INSTRUMENTED_METHODS)[number]; +/** + * Google GenAI Content Embedding + * @see https://googleapis.github.io/js-genai/release_docs/classes/types.ContentEmbedding.html + */ +type ContentEmbeddingType = { + /** The embedding values. */ + values?: number[]; + /** Statistics about the embedding. */ + statistics?: { + /** The number of tokens in the content. */ + tokenCount?: number; + /** Whether the content was truncated. */ + truncated?: boolean; + }; +}; + +/** + * Google GenAI Embed Content Response + * @see https://ai.google.dev/api/embeddings#EmbedContentResponse + */ +export type GoogleGenAIEmbedContentResponse = { + [key: string]: unknown; + /** The generated embeddings. */ + embeddings?: ContentEmbeddingType[]; + /** Metadata about the embeddings. */ + metadata?: { + /** Billable character count. */ + billableCharacterCount?: number; + }; +}; + // Export the response type for use in instrumentation export type GoogleGenAIResponse = GenerateContentResponse; diff --git a/packages/core/src/tracing/google-genai/utils.ts b/packages/core/src/tracing/google-genai/utils.ts index 4280957ce43f..c8b0a86c5b71 100644 --- a/packages/core/src/tracing/google-genai/utils.ts +++ b/packages/core/src/tracing/google-genai/utils.ts @@ -22,6 +22,13 @@ 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'); +} + // 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); }); From a1a53a5e049c0355e7147f0fe76528a08ba8998a Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Fri, 13 Mar 2026 13:02:45 +0100 Subject: [PATCH 02/10] token counts --- .../tracing/google-genai/scenario-embeddings.mjs | 11 ++++------- .../core/src/tracing/google-genai/embeddings.ts | 16 ++++++++++++++-- packages/core/src/tracing/google-genai/index.ts | 8 +++++++- packages/core/src/tracing/google-genai/types.ts | 7 +++++++ 4 files changed, 32 insertions(+), 10 deletions(-) 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 index 7b5704e8b98e..8608e7640aa8 100644 --- 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 @@ -6,7 +6,7 @@ function startMockGoogleGenAIServer() { const app = express(); app.use(express.json()); - app.post('/v1beta/models/:model\\:embedContent', (req, res) => { + app.post('/v1beta/models/:model\\:batchEmbedContents', (req, res) => { const model = req.params.model; if (model === 'error-model') { @@ -18,14 +18,11 @@ function startMockGoogleGenAIServer() { embeddings: [ { values: [0.1, 0.2, 0.3, 0.4, 0.5], - statistics: { - tokenCount: 8, - truncated: false, - }, }, ], - metadata: { - billableCharacterCount: 30, + usageMetadata: { + promptTokenCount: 8, + totalTokenCount: 8, }, }); }); diff --git a/packages/core/src/tracing/google-genai/embeddings.ts b/packages/core/src/tracing/google-genai/embeddings.ts index cfc63d8bd671..59eac86ed938 100644 --- a/packages/core/src/tracing/google-genai/embeddings.ts +++ b/packages/core/src/tracing/google-genai/embeddings.ts @@ -38,8 +38,7 @@ export function addEmbeddingsRequestAttributes(span: Span, params: Record Date: Fri, 13 Mar 2026 13:22:33 +0100 Subject: [PATCH 03/10] simplify --- .../ai-providers/google-genai/mocks.js | 7 --- .../google-genai/scenario-embeddings.mjs | 4 -- .../suites/tracing/google-genai/test.ts | 8 ---- .../src/tracing/google-genai/embeddings.ts | 47 +------------------ .../core/src/tracing/google-genai/index.ts | 11 ++--- .../core/src/tracing/google-genai/types.ts | 38 --------------- 6 files changed, 5 insertions(+), 110 deletions(-) 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 abc446ad2fdb..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 @@ -53,15 +53,8 @@ export class MockGoogleGenAI { embeddings: [ { values: [0.1, 0.2, 0.3, 0.4, 0.5], - statistics: { - tokenCount: 8, - truncated: false, - }, }, ], - metadata: { - billableCharacterCount: 30, - }, }; }, generateContentStream: async () => { 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 index 8608e7640aa8..166e741cf199 100644 --- 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 @@ -20,10 +20,6 @@ function startMockGoogleGenAIServer() { values: [0.1, 0.2, 0.3, 0.4, 0.5], }, ], - usageMetadata: { - promptTokenCount: 8, - totalTokenCount: 8, - }, }); }); 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 e65fa3b65797..390fe88663a7 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 @@ -614,8 +614,6 @@ describe('Google GenAI integration', () => { [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.google_genai', [GEN_AI_SYSTEM_ATTRIBUTE]: 'google_genai', [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'text-embedding-004', - [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 8, - [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 8, }, description: 'embeddings text-embedding-004', op: 'gen_ai.embeddings', @@ -644,8 +642,6 @@ describe('Google GenAI integration', () => { [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.google_genai', [GEN_AI_SYSTEM_ATTRIBUTE]: 'google_genai', [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'text-embedding-004', - [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 8, - [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 8, }, description: 'embeddings text-embedding-004', op: 'gen_ai.embeddings', @@ -667,8 +663,6 @@ describe('Google GenAI integration', () => { [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?', - [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 8, - [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 8, }, description: 'embeddings text-embedding-004', op: 'gen_ai.embeddings', @@ -699,8 +693,6 @@ describe('Google GenAI integration', () => { [GEN_AI_SYSTEM_ATTRIBUTE]: 'google_genai', [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'text-embedding-004', [GEN_AI_EMBEDDINGS_INPUT_ATTRIBUTE]: expect.any(String), - [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 8, - [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 8, }, description: 'embeddings text-embedding-004', op: 'gen_ai.embeddings', diff --git a/packages/core/src/tracing/google-genai/embeddings.ts b/packages/core/src/tracing/google-genai/embeddings.ts index 59eac86ed938..468a4f07cc3b 100644 --- a/packages/core/src/tracing/google-genai/embeddings.ts +++ b/packages/core/src/tracing/google-genai/embeddings.ts @@ -1,10 +1,5 @@ -import { - GEN_AI_EMBEDDINGS_INPUT_ATTRIBUTE, - GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE, - GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE, -} from '../ai/gen-ai-attributes'; +import { GEN_AI_EMBEDDINGS_INPUT_ATTRIBUTE } from '../ai/gen-ai-attributes'; import type { Span } from '../../types-hoist/span'; -import type { GoogleGenAIEmbedContentResponse } from './types'; /** * Add private request attributes for embeddings methods. @@ -35,43 +30,3 @@ export function addEmbeddingsRequestAttributes(span: Span, params: Record 0) { - span.setAttributes({ - [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: totalTokenCount, - [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: totalTokenCount, - }); - } - } -} diff --git a/packages/core/src/tracing/google-genai/index.ts b/packages/core/src/tracing/google-genai/index.ts index 11d6495d9a77..6d0c84c4a89a 100644 --- a/packages/core/src/tracing/google-genai/index.ts +++ b/packages/core/src/tracing/google-genai/index.ts @@ -29,7 +29,7 @@ import { 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 { addEmbedContentResponseAttributes, addEmbeddingsRequestAttributes } from './embeddings'; +import { addEmbeddingsRequestAttributes } from './embeddings'; import { instrumentStream } from './streaming'; import type { Candidate, @@ -324,12 +324,9 @@ function instrumentMethod( () => {}, result => { // Only add response attributes for content-producing methods, not for chats.create - if (!isSyncCreate) { - if (isEmbeddings) { - addEmbedContentResponseAttributes(span, result); - } else { - addResponseAttributes(span, result, options.recordOutputs); - } + // 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/types.ts b/packages/core/src/tracing/google-genai/types.ts index 9c6de4115d3d..9a2138a7843d 100644 --- a/packages/core/src/tracing/google-genai/types.ts +++ b/packages/core/src/tracing/google-genai/types.ts @@ -181,43 +181,5 @@ export interface GoogleGenAIChat { export type GoogleGenAIIstrumentedMethod = (typeof GOOGLE_GENAI_INSTRUMENTED_METHODS)[number]; -/** - * Google GenAI Content Embedding - * @see https://googleapis.github.io/js-genai/release_docs/classes/types.ContentEmbedding.html - */ -type ContentEmbeddingType = { - /** The embedding values. */ - values?: number[]; - /** Statistics about the embedding. */ - statistics?: { - /** The number of tokens in the content. */ - tokenCount?: number; - /** Whether the content was truncated. */ - truncated?: boolean; - }; -}; - -/** - * Google GenAI Embed Content Response - * @see https://ai.google.dev/api/embeddings#EmbedContentResponse - */ -export type GoogleGenAIEmbedContentResponse = { - [key: string]: unknown; - /** The generated embeddings. */ - embeddings?: ContentEmbeddingType[]; - /** Metadata about the embeddings. */ - metadata?: { - /** Billable character count. */ - billableCharacterCount?: number; - }; - /** Usage metadata (same shape as GenerateContentResponse). */ - usageMetadata?: { - /** Number of tokens in the request. */ - promptTokenCount?: number; - /** Total token count. */ - totalTokenCount?: number; - }; -}; - // Export the response type for use in instrumentation export type GoogleGenAIResponse = GenerateContentResponse; From 7f80a0a43bc4237cef855aab9bfd4abc9d535cd4 Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Fri, 13 Mar 2026 13:41:18 +0100 Subject: [PATCH 04/10] . --- .../src/tracing/google-genai/embeddings.ts | 32 ------------------- .../core/src/tracing/google-genai/index.ts | 27 +++++++++++----- 2 files changed, 19 insertions(+), 40 deletions(-) delete mode 100644 packages/core/src/tracing/google-genai/embeddings.ts diff --git a/packages/core/src/tracing/google-genai/embeddings.ts b/packages/core/src/tracing/google-genai/embeddings.ts deleted file mode 100644 index 468a4f07cc3b..000000000000 --- a/packages/core/src/tracing/google-genai/embeddings.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { GEN_AI_EMBEDDINGS_INPUT_ATTRIBUTE } from '../ai/gen-ai-attributes'; -import type { Span } from '../../types-hoist/span'; - -/** - * Add private request attributes for embeddings methods. - * Records the embeddings input on gen_ai.embeddings.input instead of gen_ai.input.messages. - * The input is NOT truncated (matching OpenAI behavior). - */ -export function addEmbeddingsRequestAttributes(span: Span, params: Record): void { - if (!('contents' in params)) { - return; - } - - const contents = params.contents; - - if (contents == null) { - return; - } - - if (typeof contents === 'string' && contents.length === 0) { - return; - } - - if (Array.isArray(contents) && contents.length === 0) { - return; - } - - span.setAttribute( - GEN_AI_EMBEDDINGS_INPUT_ATTRIBUTE, - typeof contents === 'string' ? contents : JSON.stringify(contents), - ); -} diff --git a/packages/core/src/tracing/google-genai/index.ts b/packages/core/src/tracing/google-genai/index.ts index 6d0c84c4a89a..28a77906655a 100644 --- a/packages/core/src/tracing/google-genai/index.ts +++ b/packages/core/src/tracing/google-genai/index.ts @@ -6,6 +6,7 @@ import { startSpan, startSpanManual } from '../../tracing/trace'; import type { Span, SpanAttributeValue } 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, @@ -29,7 +30,6 @@ import { 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 { addEmbeddingsRequestAttributes } from './embeddings'; import { instrumentStream } from './streaming'; import type { Candidate, @@ -139,7 +139,22 @@ function extractRequestAttributes( * 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 && + !(typeof contents === 'string' && contents.length === 0) && + !(Array.isArray(contents) && contents.length === 0) + ) { + span.setAttribute( + GEN_AI_EMBEDDINGS_INPUT_ATTRIBUTE, + typeof contents === 'string' ? contents : JSON.stringify(contents), + ); + } + return; + } + const messages: Message[] = []; // config.systemInstruction: ContentUnion @@ -279,7 +294,7 @@ function instrumentMethod( async (span: Span) => { try { if (options.recordInputs && params) { - addPrivateRequestAttributes(span, params); + addPrivateRequestAttributes(span, params, false); } const stream = await target.apply(context, args); return instrumentStream(stream, span, Boolean(options.recordOutputs)) as R; @@ -307,11 +322,7 @@ function instrumentMethod( }, (span: Span) => { if (options.recordInputs && params) { - if (isEmbeddings) { - addEmbeddingsRequestAttributes(span, params); - } else { - addPrivateRequestAttributes(span, params); - } + addPrivateRequestAttributes(span, params, isEmbeddings); } return handleCallbackErrors( From cfefe018b0214e79cb25c3f948c745a807f88bf2 Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Fri, 13 Mar 2026 14:19:42 +0100 Subject: [PATCH 05/10] more specific test check --- .../node-integration-tests/suites/tracing/google-genai/test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 390fe88663a7..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 @@ -692,7 +692,8 @@ describe('Google GenAI integration', () => { [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]: expect.any(String), + [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', From aed61d8749dbd9af48c99e4cc825391a34724bd1 Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Fri, 13 Mar 2026 14:26:15 +0100 Subject: [PATCH 06/10] . --- packages/core/src/tracing/google-genai/index.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/core/src/tracing/google-genai/index.ts b/packages/core/src/tracing/google-genai/index.ts index 28a77906655a..4209d97e3b03 100644 --- a/packages/core/src/tracing/google-genai/index.ts +++ b/packages/core/src/tracing/google-genai/index.ts @@ -142,11 +142,7 @@ function extractRequestAttributes( function addPrivateRequestAttributes(span: Span, params: Record, isEmbeddings: boolean): void { if (isEmbeddings) { const contents = params.contents; - if ( - contents != null && - !(typeof contents === 'string' && contents.length === 0) && - !(Array.isArray(contents) && contents.length === 0) - ) { + if (contents != null) { span.setAttribute( GEN_AI_EMBEDDINGS_INPUT_ATTRIBUTE, typeof contents === 'string' ? contents : JSON.stringify(contents), From 440affdcf6d25fd13795b992e983e9d04ed6f49b Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Fri, 13 Mar 2026 14:28:07 +0100 Subject: [PATCH 07/10] 2. --- packages/core/src/tracing/google-genai/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/tracing/google-genai/index.ts b/packages/core/src/tracing/google-genai/index.ts index 4209d97e3b03..49f9fae88e1a 100644 --- a/packages/core/src/tracing/google-genai/index.ts +++ b/packages/core/src/tracing/google-genai/index.ts @@ -290,7 +290,7 @@ function instrumentMethod( async (span: Span) => { try { if (options.recordInputs && params) { - addPrivateRequestAttributes(span, params, false); + addPrivateRequestAttributes(span, params, isEmbeddings); } const stream = await target.apply(context, args); return instrumentStream(stream, span, Boolean(options.recordOutputs)) as R; From e1c2fcfa3ccfdb61117faa9fcccc2a139c467372 Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Fri, 13 Mar 2026 14:59:02 +0100 Subject: [PATCH 08/10] move functions --- .../core/src/tracing/google-genai/index.ts | 115 ++---------------- .../core/src/tracing/google-genai/utils.ts | 110 ++++++++++++++++- 2 files changed, 118 insertions(+), 107 deletions(-) diff --git a/packages/core/src/tracing/google-genai/index.ts b/packages/core/src/tracing/google-genai/index.ts index 49f9fae88e1a..d0559d050359 100644 --- a/packages/core/src/tracing/google-genai/index.ts +++ b/packages/core/src/tracing/google-genai/index.ts @@ -1,27 +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, @@ -29,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, @@ -39,100 +29,13 @@ import type { GoogleGenAIResponse, } from './types'; import type { ContentListUnion, ContentUnion, Message, PartListUnion } from './utils'; -import { contentUnionToMessages, isEmbeddingsMethod, 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. diff --git a/packages/core/src/tracing/google-genai/utils.ts b/packages/core/src/tracing/google-genai/utils.ts index c8b0a86c5b71..c0c2e692453c 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'; /** @@ -29,6 +44,99 @@ 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) + */ +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 + */ +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; From c4cd2ca9e651fd6f80cf737c9292c80e17094aac Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Fri, 13 Mar 2026 14:59:48 +0100 Subject: [PATCH 09/10] . --- packages/core/src/tracing/google-genai/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/tracing/google-genai/utils.ts b/packages/core/src/tracing/google-genai/utils.ts index c0c2e692453c..3a6e1c67c377 100644 --- a/packages/core/src/tracing/google-genai/utils.ts +++ b/packages/core/src/tracing/google-genai/utils.ts @@ -48,7 +48,7 @@ export function isEmbeddingsMethod(methodPath: string): boolean { * 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 { +function extractModel(params: Record, context?: unknown): string { if ('model' in params && typeof params.model === 'string') { return params.model; } From 0b2070db0e581eadd29eb850af1bd57f1b657972 Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Fri, 13 Mar 2026 15:03:54 +0100 Subject: [PATCH 10/10] . --- packages/core/src/tracing/google-genai/constants.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/core/src/tracing/google-genai/constants.ts b/packages/core/src/tracing/google-genai/constants.ts index b26c170d9cf3..22f1d3c5c057 100644 --- a/packages/core/src/tracing/google-genai/constants.ts +++ b/packages/core/src/tracing/google-genai/constants.ts @@ -16,5 +16,4 @@ export const GOOGLE_GENAI_INSTRUMENTED_METHODS = [ // Constants for internal use export const GOOGLE_GENAI_SYSTEM_NAME = 'google_genai'; export const CHATS_CREATE_METHOD = 'chats.create'; -export const EMBED_CONTENT_METHOD = 'models.embedContent'; export const CHAT_PATH = 'chat';