Skip to content

Commit fa74db5

Browse files
nicohrubecclaude
andauthored
feat(core): Support embedding APIs in google-genai (#19797)
Add instrumentation support for the Google GenAI embeddings API (`models.embedContent`). Docs: https://ai.google.dev/gemini-api/docs/embeddings Closes #19535 --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent c0d52df commit fa74db5

File tree

12 files changed

+307
-6
lines changed

12 files changed

+307
-6
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
## Unreleased
44

55
- "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott
6+
- feat(core): Support embedding APIs in google-genai ([#19797](https://github.com/getsentry/sentry-javascript/pull/19797))
7+
8+
Adds instrumentation for the Google GenAI [`embedContent`](https://ai.google.dev/gemini-api/docs/embeddings) API, creating `gen_ai.embeddings` spans.
69

710
## 10.46.0
811

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);

dev-packages/browser-integration-tests/suites/tracing/ai-providers/google-genai/test.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,26 @@ sentryTest('manual Google GenAI instrumentation sends gen_ai transactions', asyn
2929
'gen_ai.request.model': 'gemini-1.5-pro',
3030
});
3131
});
32+
33+
sentryTest('manual Google GenAI instrumentation sends embeddings transactions', async ({ getLocalTestUrl, page }) => {
34+
const transactionPromise = waitForTransactionRequest(page, event => {
35+
return !!event.transaction?.includes('text-embedding-004');
36+
});
37+
38+
const url = await getLocalTestUrl({ testDir: __dirname });
39+
await page.goto(url);
40+
41+
const req = await transactionPromise;
42+
43+
const eventData = envelopeRequestParser(req);
44+
45+
// Verify it's a gen_ai embeddings transaction
46+
expect(eventData.transaction).toBe('embeddings text-embedding-004');
47+
expect(eventData.contexts?.trace?.op).toBe('gen_ai.embeddings');
48+
expect(eventData.contexts?.trace?.origin).toBe('auto.ai.google_genai');
49+
expect(eventData.contexts?.trace?.data).toMatchObject({
50+
'gen_ai.operation.name': 'embeddings',
51+
'gen_ai.system': 'google_genai',
52+
'gen_ai.request.model': 'text-embedding-004',
53+
});
54+
});

dev-packages/cloudflare-integration-tests/suites/tracing/google-genai/index.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,13 @@ export default Sentry.withSentry(
5555
],
5656
});
5757

58-
return new Response(JSON.stringify({ chatResponse, modelResponse }));
58+
// Test 3: models.embedContent
59+
const embedResponse = await client.models.embedContent({
60+
model: 'text-embedding-004',
61+
contents: 'Hello world',
62+
});
63+
64+
return new Response(JSON.stringify({ chatResponse, modelResponse, embedResponse }));
5965
},
6066
},
6167
);

dev-packages/cloudflare-integration-tests/suites/tracing/google-genai/mocks.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ export class MockGoogleGenAI implements GoogleGenAIClient {
44
public models: {
55
generateContent: (...args: unknown[]) => Promise<GoogleGenAIResponse>;
66
generateContentStream: (...args: unknown[]) => Promise<AsyncGenerator<GoogleGenAIResponse, any, unknown>>;
7+
embedContent: (...args: unknown[]) => Promise<{ embeddings: { values: number[] }[] }>;
78
};
89
public chats: {
910
create: (...args: unknown[]) => GoogleGenAIChat;
@@ -49,6 +50,20 @@ export class MockGoogleGenAI implements GoogleGenAIClient {
4950
},
5051
};
5152
},
53+
embedContent: async (...args: unknown[]) => {
54+
const params = args[0] as { model: string; contents?: unknown };
55+
await new Promise(resolve => setTimeout(resolve, 10));
56+
57+
if (params.model === 'error-model') {
58+
const error = new Error('Model not found');
59+
(error as unknown as { status: number }).status = 404;
60+
throw error;
61+
}
62+
63+
return {
64+
embeddings: [{ values: [0.1, 0.2, 0.3, 0.4, 0.5] }],
65+
};
66+
},
5267
generateContentStream: async () => {
5368
// Return a promise that resolves to an async generator
5469
return (async function* (): AsyncGenerator<GoogleGenAIResponse, any, unknown> {

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,19 @@ it('traces Google GenAI chat creation and message sending', async () => {
7878
op: 'gen_ai.generate_content',
7979
origin: 'auto.ai.google_genai',
8080
}),
81+
// Fourth span - models.embedContent
82+
expect.objectContaining({
83+
data: expect.objectContaining({
84+
[GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'embeddings',
85+
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.embeddings',
86+
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.google_genai',
87+
[GEN_AI_SYSTEM_ATTRIBUTE]: 'google_genai',
88+
[GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'text-embedding-004',
89+
}),
90+
description: 'embeddings text-embedding-004',
91+
op: 'gen_ai.embeddings',
92+
origin: 'auto.ai.google_genai',
93+
}),
8194
]),
8295
);
8396
})
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' },

0 commit comments

Comments
 (0)