Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -53,12 +53,35 @@ export default Sentry.withSentry(
const client = Sentry.instrumentGoogleGenAIClient(new GoogleGenAI({ apiKey: 'mock-api-key' }));

// Test 1: chats.create and sendMessage flow
// Every field here is read back off the chat instance for each message the chat sends, so
// the create config covers all of them at once.
const chat = client.chats.create({
model: 'gemini-1.5-pro',
config: {
temperature: 0.8,
topP: 0.9,
topK: 40,
maxOutputTokens: 150,
frequencyPenalty: 0.5,
presencePenalty: 0.3,
systemInstruction: 'You are a friendly robot.',
tools: [
{
functionDeclarations: [
{
name: 'controlLight',
parametersJsonSchema: {
type: 'object',
properties: {
brightness: { type: 'number' },
colorTemperature: { type: 'string' },
},
required: ['brightness', 'colorTemperature'],
},
},
],
},
],
},
history: [
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,16 @@ import {
GEN_AI_INPUT_MESSAGES,
GEN_AI_OPERATION_NAME,
GEN_AI_PROVIDER_NAME,
GEN_AI_REQUEST_FREQUENCY_PENALTY,
GEN_AI_REQUEST_MAX_TOKENS,
GEN_AI_REQUEST_MODEL,
GEN_AI_REQUEST_PRESENCE_PENALTY,
GEN_AI_REQUEST_TEMPERATURE,
GEN_AI_REQUEST_TOP_K,
GEN_AI_REQUEST_TOP_P,
GEN_AI_RESPONSE_TEXT,
GEN_AI_SYSTEM_INSTRUCTIONS,
GEN_AI_TOOL_DEFINITIONS,
GEN_AI_USAGE_INPUT_TOKENS,
GEN_AI_USAGE_OUTPUT_TOKENS,
GEN_AI_USAGE_TOTAL_TOKENS,
Expand Down Expand Up @@ -49,7 +54,24 @@ it('traces Google GenAI chat, generateContent, and embedContent calls', async ({
[GEN_AI_PROVIDER_NAME]: { value: 'google_genai', type: 'string' },
[GEN_AI_OPERATION_NAME]: { value: 'chat', type: 'string' },
[GEN_AI_REQUEST_MODEL]: { value: 'gemini-1.5-pro', type: 'string' },
// Set once on `chats.create()` and reused for every message the chat sends.
[GEN_AI_REQUEST_TEMPERATURE]: { value: 0.8, type: 'double' },
[GEN_AI_REQUEST_TOP_P]: { value: 0.9, type: 'double' },
[GEN_AI_REQUEST_TOP_K]: { value: 40, type: 'integer' },
[GEN_AI_REQUEST_MAX_TOKENS]: { value: 150, type: 'integer' },
[GEN_AI_REQUEST_FREQUENCY_PENALTY]: { value: 0.5, type: 'double' },
[GEN_AI_REQUEST_PRESENCE_PENALTY]: { value: 0.3, type: 'double' },
[GEN_AI_TOOL_DEFINITIONS]: {
value:
'[{"name":"controlLight","parametersJsonSchema":{"type":"object","properties":{"brightness":{"type":"number"},"colorTemperature":{"type":"string"}},"required":["brightness","colorTemperature"]}}]',
type: 'string',
},
// collect LLM input and outputs (default true)
[GEN_AI_SYSTEM_INSTRUCTIONS]: {
value: '[{"type":"text","content":"You are a friendly robot."}]',
type: 'string',
},
// The create `history` stays off the span; only the message being sent is reported.
[GEN_AI_INPUT_MESSAGES]: { value: '[{"role":"user","content":"Tell me a joke"}]', type: 'string' },
[GEN_AI_RESPONSE_TEXT]: { value: 'Hello from Google GenAI!', type: 'string' },
[GEN_AI_USAGE_INPUT_TOKENS]: { value: 8, type: 'integer' },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ async function run() {
});

// Test 1: chats.create and sendMessage flow
// This should generate two spans: one for chats.create and one for sendMessage
// `chats.create()` builds a local object and emits no span; only `sendMessage` does, and it
// reports the config given here.
const chat = client.chats.create({
model: 'gemini-1.5-pro',
config: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ async function run() {
});

// Test 1: chats.create and sendMessage flow
// This should generate two spans: one for chats.create and one for sendMessage
// `chats.create()` builds a local object and emits no span; only `sendMessage` does, and it
// reports the config given here.
const chat = client.chats.create({
model: 'gemini-1.5-pro',
config: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,11 @@ describe('Google GenAI integration', () => {
expect(chatSpan!.attributes['sentry.origin'].value).toBe(EXPECTED_ORIGIN);
expect(chatSpan!.attributes[GEN_AI_PROVIDER_NAME].value).toBe('google_genai');
expect(chatSpan!.attributes[GEN_AI_REQUEST_MODEL].value).toBe('gemini-1.5-pro');
// Given once to `chats.create()` and reused for every message the chat sends.
expect(chatSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE].value).toBe(0.8);
expect(chatSpan!.attributes[GEN_AI_REQUEST_TOP_P].value).toBe(0.9);
expect(chatSpan!.attributes[GEN_AI_REQUEST_MAX_TOKENS].value).toBe(150);
expect(chatSpan!.attributes[GEN_AI_SYSTEM_INSTRUCTIONS]).toBeUndefined();
expect(chatSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS].value).toBe(8);
expect(chatSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS].value).toBe(12);
expect(chatSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS].value).toBe(20);
Expand Down Expand Up @@ -104,6 +109,13 @@ describe('Google GenAI integration', () => {
expect(chatSpan!.attributes[GEN_AI_REQUEST_MODEL].value).toBe('gemini-1.5-pro');
expect(chatSpan!.attributes[GEN_AI_INPUT_MESSAGES]).toBeDefined();
expect(chatSpan!.attributes[GEN_AI_RESPONSE_TEXT]).toBeDefined();
// Given once to `chats.create()` and reused for every message the chat sends.
expect(chatSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE].value).toBe(0.8);
expect(chatSpan!.attributes[GEN_AI_REQUEST_TOP_P].value).toBe(0.9);
expect(chatSpan!.attributes[GEN_AI_REQUEST_MAX_TOKENS].value).toBe(150);
expect(chatSpan!.attributes[GEN_AI_SYSTEM_INSTRUCTIONS].value).toBe(
'[{"type":"text","content":"You are a friendly robot who likes to be funny."}]',
);
expect(chatSpan!.attributes[GEN_AI_USAGE_INPUT_TOKENS].value).toBe(8);
expect(chatSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS].value).toBe(12);
expect(chatSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS].value).toBe(20);
Expand Down Expand Up @@ -275,6 +287,10 @@ describe('Google GenAI integration', () => {
expect(chatSpan!.status).toBe('ok');
expect(chatSpan!.attributes[GEN_AI_OPERATION_NAME].value).toBe('chat');
expect(chatSpan!.attributes[GEN_AI_RESPONSE_STREAMING].value).toBe(true);
// Given once to `chats.create()` and reused for every message the chat sends.
expect(chatSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE].value).toBe(0.8);
expect(chatSpan!.attributes[GEN_AI_REQUEST_TOP_P].value).toBe(0.9);
expect(chatSpan!.attributes[GEN_AI_REQUEST_MAX_TOKENS].value).toBe(150);
expect(chatSpan!.attributes[GEN_AI_RESPONSE_ID].value).toBe('mock-response-streaming-id');
expect(chatSpan!.attributes[GEN_AI_RESPONSE_MODEL].value).toBe('gemini-1.5-pro');

Expand Down Expand Up @@ -342,6 +358,10 @@ describe('Google GenAI integration', () => {
expect(chatSpan!.attributes[GEN_AI_OPERATION_NAME].value).toBe('chat');
expect(chatSpan!.attributes[GEN_AI_RESPONSE_STREAMING].value).toBe(true);
expect(chatSpan!.attributes[GEN_AI_INPUT_MESSAGES]).toBeDefined();
// Given once to `chats.create()` and reused for every message the chat sends.
expect(chatSpan!.attributes[GEN_AI_REQUEST_TEMPERATURE].value).toBe(0.8);
expect(chatSpan!.attributes[GEN_AI_REQUEST_TOP_P].value).toBe(0.9);
expect(chatSpan!.attributes[GEN_AI_REQUEST_MAX_TOKENS].value).toBe(150);
expect(chatSpan!.attributes[GEN_AI_RESPONSE_FINISH_REASONS].value).toBe('["STOP"]');

const blockedSpan = container.items.find(span => span.name === 'generate_content blocked-model');
Expand Down
42 changes: 37 additions & 5 deletions packages/server-utils/src/ai/google-genai/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,37 @@ export function addResponseAttributes(span: Span, response: GoogleGenAIResponse,
}
}

/**
* Recover the config a chat message sends but does not carry in its own arguments.
*
* `chats.create()` takes a config that `@google/genai` reuses for every message, resolving each
* request as `params.config ?? chat.config`. A per-message config therefore replaces the chat
* config rather than merging into it, and the chat config applies only when the message omits one.
* That config lives on the chat instance, which both instrumentation paths already hold: the client
* proxy passes it as the method's `context`, the diagnostics-channel path as `data.self`.
*
* The chat `history` stays off the message spans. The SDK does send it, folded into `contents`, and
* the instance carries the whole transcript, but repeating every past turn on every message span
* duplicates what earlier spans already reported and grows without bound.
*/
export function resolveChatParams(
operationName: string,
params: Record<string, unknown> | undefined,
context: unknown,
): Record<string, unknown> | undefined {
// `!= null` mirrors the SDK's `??`: an explicit `null` config falls back to the chat's.
if (operationName !== 'chat' || params?.config != null || !context || typeof context !== 'object') {
return params;
}

const chatConfig = (context as Record<string, unknown>).config;
if (!chatConfig || typeof chatConfig !== 'object') {
return params;
}

return { ...params, config: chatConfig };
}

/**
* Instrument any async or synchronous genai method with Sentry spans
* Handles operations like models.generateContent and chat.sendMessage and chats.create
Expand All @@ -269,7 +300,8 @@ function instrumentMethod<T extends unknown[], R>(
apply(target, _, args: T): R | Promise<R> {
const operationName = instrumentedMethod.operation || 'unknown';
const params = args[0] as Record<string, unknown> | undefined;
const requestAttributes = extractRequestAttributes(operationName, params, context);
const attributeParams = resolveChatParams(operationName, params, context);
const requestAttributes = extractRequestAttributes(operationName, attributeParams, context);
const model = requestAttributes[GEN_AI_REQUEST_MODEL] || 'unknown';
const client = getClient();
// With span streaming, omit the `'unknown'` model sentinel so the name stays low-cardinality.
Expand All @@ -289,8 +321,8 @@ function instrumentMethod<T extends unknown[], R>(
},
async (span: Span) => {
try {
if (options.recordInputs && params) {
addPrivateRequestAttributes(span, params, operationName);
if (options.recordInputs && attributeParams) {
addPrivateRequestAttributes(span, attributeParams, operationName);
}
const stream = await target.apply(context, args);
return instrumentStream(stream, span, Boolean(options.recordOutputs)) as R;
Expand All @@ -310,8 +342,8 @@ function instrumentMethod<T extends unknown[], R>(
attributes: requestAttributes,
},
(span: Span) => {
if (options.recordInputs && params) {
addPrivateRequestAttributes(span, params, operationName);
if (options.recordInputs && attributeParams) {
addPrivateRequestAttributes(span, attributeParams, operationName);
}

// `onError` is a no-op because the rejection is rethrown to the caller and `startSpan` already
Expand Down
11 changes: 9 additions & 2 deletions packages/server-utils/src/integrations/google-genai.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,12 @@ import {
startInactiveSpan,
} from '@sentry/core';
import { getGenAiSpanOp, resolveAIRecordingOptions } from '../ai/core/utils';
import { addPrivateRequestAttributes, addResponseAttributes, extractRequestAttributes } from '../ai/google-genai';
import {
addPrivateRequestAttributes,
addResponseAttributes,
extractRequestAttributes,
resolveChatParams,
} from '../ai/google-genai';
import { instrumentStream } from '../ai/google-genai/streaming';
import type { GoogleGenAIOptions, GoogleGenAIResponse } from '../ai/google-genai/types';
import { CHANNELS } from '../orchestrion/channels';
Expand Down Expand Up @@ -103,7 +108,9 @@ function createGenAiSpan(
}

const args = data.arguments ?? [];
const params = args[0] as Record<string, unknown> | undefined;
// `chat.sendMessage()` carries no config of its own unless the caller passes one, so recover the
// one `chats.create()` left on the chat instance that the transform stashed in `data.self`.
const params = resolveChatParams(operation, args[0] as Record<string, unknown> | undefined, data.self);

const { recordInputs } = resolveAIRecordingOptions(options);

Expand Down
Loading
Loading