Skip to content

Commit 3af75a0

Browse files
nicohrubecclaude
andcommitted
feat(core): Instrument Google GenAI embeddings API (embedContent)
Add embeddings support to the Google GenAI integration using the new method registry architecture. Records embeddings input when PII recording is enabled, and skips response attribute extraction for embeddings responses. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent e3bdbed commit 3af75a0

File tree

6 files changed

+243
-5
lines changed

6 files changed

+243
-5
lines changed

dev-packages/browser-integration-tests/suites/tracing/ai-providers/google-genai/mocks.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,24 @@ export class MockGoogleGenAI {
3939
},
4040
};
4141
},
42+
embedContent: async (...args) => {
43+
const params = args[0];
44+
await new Promise(resolve => setTimeout(resolve, 10));
45+
46+
if (params.model === 'error-model') {
47+
const error = new Error('Model not found');
48+
error.status = 404;
49+
throw error;
50+
}
51+
52+
return {
53+
embeddings: [
54+
{
55+
values: [0.1, 0.2, 0.3, 0.4, 0.5],
56+
},
57+
],
58+
};
59+
},
4260
generateContentStream: async () => {
4361
// Return a promise that resolves to an async generator
4462
return (async function* () {

dev-packages/browser-integration-tests/suites/tracing/ai-providers/google-genai/subject.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,11 @@ const response = await chat.sendMessage({
3030
});
3131

3232
console.log('Received response', response);
33+
34+
// Test embedContent
35+
const embedResponse = await client.models.embedContent({
36+
model: 'text-embedding-004',
37+
contents: 'Hello world',
38+
});
39+
40+
console.log('Received embed response', embedResponse);
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { GoogleGenAI } from '@google/genai';
2+
import * as Sentry from '@sentry/node';
3+
import express from 'express';
4+
5+
function startMockGoogleGenAIServer() {
6+
const app = express();
7+
app.use(express.json());
8+
9+
app.post('/v1beta/models/:model\\:batchEmbedContents', (req, res) => {
10+
const model = req.params.model;
11+
12+
if (model === 'error-model') {
13+
res.status(404).set('x-request-id', 'mock-request-123').end('Model not found');
14+
return;
15+
}
16+
17+
res.send({
18+
embeddings: [
19+
{
20+
values: [0.1, 0.2, 0.3, 0.4, 0.5],
21+
},
22+
],
23+
});
24+
});
25+
26+
return new Promise(resolve => {
27+
const server = app.listen(0, () => {
28+
resolve(server);
29+
});
30+
});
31+
}
32+
33+
async function run() {
34+
const server = await startMockGoogleGenAIServer();
35+
36+
await Sentry.startSpan({ op: 'function', name: 'main' }, async () => {
37+
const client = new GoogleGenAI({
38+
apiKey: 'mock-api-key',
39+
httpOptions: { baseUrl: `http://localhost:${server.address().port}` },
40+
});
41+
42+
// Test 1: Basic embedContent with string contents
43+
await client.models.embedContent({
44+
model: 'text-embedding-004',
45+
contents: 'What is the capital of France?',
46+
});
47+
48+
// Test 2: Error handling
49+
try {
50+
await client.models.embedContent({
51+
model: 'error-model',
52+
contents: 'This will fail',
53+
});
54+
} catch {
55+
// Expected error
56+
}
57+
58+
// Test 3: embedContent with array contents
59+
await client.models.embedContent({
60+
model: 'text-embedding-004',
61+
contents: [
62+
{
63+
role: 'user',
64+
parts: [{ text: 'First input text' }],
65+
},
66+
{
67+
role: 'user',
68+
parts: [{ text: 'Second input text' }],
69+
},
70+
],
71+
});
72+
});
73+
74+
server.close();
75+
}
76+
77+
run();

dev-packages/node-integration-tests/suites/tracing/google-genai/test.ts

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core';
22
import { afterAll, describe, expect } from 'vitest';
33
import {
4+
GEN_AI_EMBEDDINGS_INPUT_ATTRIBUTE,
45
GEN_AI_INPUT_MESSAGES_ATTRIBUTE,
56
GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE,
67
GEN_AI_OPERATION_NAME_ATTRIBUTE,
@@ -601,4 +602,124 @@ describe('Google GenAI integration', () => {
601602
});
602603
},
603604
);
605+
606+
const EXPECTED_TRANSACTION_DEFAULT_PII_FALSE_EMBEDDINGS = {
607+
transaction: 'main',
608+
spans: expect.arrayContaining([
609+
// First span - embedContent with string contents
610+
expect.objectContaining({
611+
data: {
612+
[GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'embeddings',
613+
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.embeddings',
614+
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.google_genai',
615+
[GEN_AI_SYSTEM_ATTRIBUTE]: 'google_genai',
616+
[GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'text-embedding-004',
617+
},
618+
description: 'embeddings text-embedding-004',
619+
op: 'gen_ai.embeddings',
620+
origin: 'auto.ai.google_genai',
621+
status: 'ok',
622+
}),
623+
// Second span - embedContent error model
624+
expect.objectContaining({
625+
data: {
626+
[GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'embeddings',
627+
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.embeddings',
628+
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.google_genai',
629+
[GEN_AI_SYSTEM_ATTRIBUTE]: 'google_genai',
630+
[GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'error-model',
631+
},
632+
description: 'embeddings error-model',
633+
op: 'gen_ai.embeddings',
634+
origin: 'auto.ai.google_genai',
635+
status: 'internal_error',
636+
}),
637+
// Third span - embedContent with array contents
638+
expect.objectContaining({
639+
data: {
640+
[GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'embeddings',
641+
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.embeddings',
642+
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.google_genai',
643+
[GEN_AI_SYSTEM_ATTRIBUTE]: 'google_genai',
644+
[GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'text-embedding-004',
645+
},
646+
description: 'embeddings text-embedding-004',
647+
op: 'gen_ai.embeddings',
648+
origin: 'auto.ai.google_genai',
649+
status: 'ok',
650+
}),
651+
]),
652+
};
653+
654+
const EXPECTED_TRANSACTION_DEFAULT_PII_TRUE_EMBEDDINGS = {
655+
transaction: 'main',
656+
spans: expect.arrayContaining([
657+
// First span - embedContent with PII
658+
expect.objectContaining({
659+
data: {
660+
[GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'embeddings',
661+
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.embeddings',
662+
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.google_genai',
663+
[GEN_AI_SYSTEM_ATTRIBUTE]: 'google_genai',
664+
[GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'text-embedding-004',
665+
[GEN_AI_EMBEDDINGS_INPUT_ATTRIBUTE]: 'What is the capital of France?',
666+
},
667+
description: 'embeddings text-embedding-004',
668+
op: 'gen_ai.embeddings',
669+
origin: 'auto.ai.google_genai',
670+
status: 'ok',
671+
}),
672+
// Second span - embedContent error model with PII
673+
expect.objectContaining({
674+
data: {
675+
[GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'embeddings',
676+
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.embeddings',
677+
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.google_genai',
678+
[GEN_AI_SYSTEM_ATTRIBUTE]: 'google_genai',
679+
[GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'error-model',
680+
[GEN_AI_EMBEDDINGS_INPUT_ATTRIBUTE]: 'This will fail',
681+
},
682+
description: 'embeddings error-model',
683+
op: 'gen_ai.embeddings',
684+
origin: 'auto.ai.google_genai',
685+
status: 'internal_error',
686+
}),
687+
// Third span - embedContent with array contents and PII
688+
expect.objectContaining({
689+
data: {
690+
[GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'embeddings',
691+
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.embeddings',
692+
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.google_genai',
693+
[GEN_AI_SYSTEM_ATTRIBUTE]: 'google_genai',
694+
[GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'text-embedding-004',
695+
[GEN_AI_EMBEDDINGS_INPUT_ATTRIBUTE]:
696+
'[{"role":"user","parts":[{"text":"First input text"}]},{"role":"user","parts":[{"text":"Second input text"}]}]',
697+
},
698+
description: 'embeddings text-embedding-004',
699+
op: 'gen_ai.embeddings',
700+
origin: 'auto.ai.google_genai',
701+
status: 'ok',
702+
}),
703+
]),
704+
};
705+
706+
createEsmAndCjsTests(__dirname, 'scenario-embeddings.mjs', 'instrument.mjs', (createRunner, test) => {
707+
test('creates google genai embeddings spans with sendDefaultPii: false', async () => {
708+
await createRunner()
709+
.ignore('event')
710+
.expect({ transaction: EXPECTED_TRANSACTION_DEFAULT_PII_FALSE_EMBEDDINGS })
711+
.start()
712+
.completed();
713+
});
714+
});
715+
716+
createEsmAndCjsTests(__dirname, 'scenario-embeddings.mjs', 'instrument-with-pii.mjs', (createRunner, test) => {
717+
test('creates google genai embeddings spans with sendDefaultPii: true', async () => {
718+
await createRunner()
719+
.ignore('event')
720+
.expect({ transaction: EXPECTED_TRANSACTION_DEFAULT_PII_TRUE_EMBEDDINGS })
721+
.start()
722+
.completed();
723+
});
724+
});
604725
});

packages/core/src/tracing/google-genai/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export const GOOGLE_GENAI_INTEGRATION_NAME = 'Google_GenAI';
99
export const GOOGLE_GENAI_METHOD_REGISTRY = {
1010
'models.generateContent': { operation: 'generate_content' },
1111
'models.generateContentStream': { operation: 'generate_content', streaming: true },
12+
'models.embedContent': { operation: 'embeddings' },
1213
'chats.create': { operation: 'chat' },
1314
// chat.* paths are built by createDeepProxy when it proxies the chat instance with CHAT_PATH as base
1415
'chat.sendMessage': { operation: 'chat' },

packages/core/src/tracing/google-genai/index.ts

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { startSpan, startSpanManual } from '../../tracing/trace';
55
import type { Span, SpanAttributeValue } from '../../types-hoist/span';
66
import { handleCallbackErrors } from '../../utils/handleCallbackErrors';
77
import {
8+
GEN_AI_EMBEDDINGS_INPUT_ATTRIBUTE,
89
GEN_AI_INPUT_MESSAGES_ATTRIBUTE,
910
GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE,
1011
GEN_AI_OPERATION_NAME_ATTRIBUTE,
@@ -132,7 +133,18 @@ function extractRequestAttributes(
132133
* This is only recorded if recordInputs is true.
133134
* Handles different parameter formats for different Google GenAI methods.
134135
*/
135-
function addPrivateRequestAttributes(span: Span, params: Record<string, unknown>): void {
136+
function addPrivateRequestAttributes(span: Span, params: Record<string, unknown>, isEmbeddings: boolean): void {
137+
if (isEmbeddings) {
138+
const contents = params.contents;
139+
if (contents != null) {
140+
span.setAttribute(
141+
GEN_AI_EMBEDDINGS_INPUT_ATTRIBUTE,
142+
typeof contents === 'string' ? contents : JSON.stringify(contents),
143+
);
144+
}
145+
return;
146+
}
147+
136148
const messages: Message[] = [];
137149

138150
// config.systemInstruction: ContentUnion
@@ -252,6 +264,7 @@ function instrumentMethod<T extends unknown[], R>(
252264
options: GoogleGenAIOptions,
253265
): (...args: T) => R | Promise<R> {
254266
const isSyncCreate = methodPath === CHATS_CREATE_METHOD;
267+
const isEmbeddings = instrumentedMethod.operation === 'embeddings';
255268

256269
return new Proxy(originalMethod, {
257270
apply(target, _, args: T): R | Promise<R> {
@@ -272,7 +285,7 @@ function instrumentMethod<T extends unknown[], R>(
272285
async (span: Span) => {
273286
try {
274287
if (options.recordInputs && params) {
275-
addPrivateRequestAttributes(span, params);
288+
addPrivateRequestAttributes(span, params, isEmbeddings);
276289
}
277290
const stream = await target.apply(context, args);
278291
return instrumentStream(stream, span, Boolean(options.recordOutputs)) as R;
@@ -300,7 +313,7 @@ function instrumentMethod<T extends unknown[], R>(
300313
},
301314
(span: Span) => {
302315
if (options.recordInputs && params) {
303-
addPrivateRequestAttributes(span, params);
316+
addPrivateRequestAttributes(span, params, isEmbeddings);
304317
}
305318

306319
return handleCallbackErrors(
@@ -312,8 +325,8 @@ function instrumentMethod<T extends unknown[], R>(
312325
},
313326
() => {},
314327
result => {
315-
// Only add response attributes for content-producing methods, not for chats.create
316-
if (!isSyncCreate) {
328+
// Only add response attributes for content-producing methods, not for chats.create or embeddings
329+
if (!isSyncCreate && !isEmbeddings) {
317330
addResponseAttributes(span, result, options.recordOutputs);
318331
}
319332
},

0 commit comments

Comments
 (0)