Skip to content

Commit 4816963

Browse files
authored
ref(core): Do not emit spans for chats.create in google-genai (#19990)
We currently emit spans for the google-genai `chats.create()` API. I think this is basically useless because `chats.create()` doesn't represent an actual interaction with an LLM model, instead it just constructs a local chat object as a result that then further exposes methods like `sendMessage()` that represent actual LLM interactions. This PR removes the spans for `chats.create()`. Since this API is a special case where we actually need to proxy the return object instead of the method call itself we had some surrounding logic hardcoded to this method. To make this a bit more future proof and also more explicit we now add a `proxyResultPath` field to the method registry that allows to define this behavior in the method registry without needing any hardcoded logic. Another benefit this has is that the full logic in the `createDeepProxy` methods in all our client-proxy based AI integrations (google-genai, openai, anthropic) is now essentially the same so could potentially in the future be easily merged into a shared abstraction. Limitation: We do"loose" some data by not emitting this create span anymore, because the way this API works is that the user defines certain parameters only on the `chats.create()` call and these are then subsequently used for each `chat.sendMessage()` call. The correct way would be to send this data as part of each `chat.sendMessage()` chat span. We can think about doing this as part of this PR or doing a follow up.
1 parent ff0cee1 commit 4816963

File tree

8 files changed

+50
-142
lines changed

8 files changed

+50
-142
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ sentryTest('manual Google GenAI instrumentation sends gen_ai transactions', asyn
2020
const eventData = envelopeRequestParser(req);
2121

2222
// Verify it's a gen_ai transaction
23-
expect(eventData.transaction).toBe('chat gemini-1.5-pro create');
23+
expect(eventData.transaction).toBe('chat gemini-1.5-pro');
2424
expect(eventData.contexts?.trace?.op).toBe('gen_ai.chat');
2525
expect(eventData.contexts?.trace?.origin).toBe('auto.ai.google_genai');
2626
expect(eventData.contexts?.trace?.data).toMatchObject({

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

Lines changed: 3 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -27,23 +27,7 @@ it('traces Google GenAI chat creation and message sending', async () => {
2727
expect(transactionEvent.transaction).toBe('GET /');
2828
expect(transactionEvent.spans).toEqual(
2929
expect.arrayContaining([
30-
// First span - chats.create
31-
expect.objectContaining({
32-
data: expect.objectContaining({
33-
[GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'chat',
34-
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.chat',
35-
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.google_genai',
36-
[GEN_AI_SYSTEM_ATTRIBUTE]: 'google_genai',
37-
[GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'gemini-1.5-pro',
38-
[GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE]: 0.8,
39-
[GEN_AI_REQUEST_TOP_P_ATTRIBUTE]: 0.9,
40-
[GEN_AI_REQUEST_MAX_TOKENS_ATTRIBUTE]: 150,
41-
}),
42-
description: 'chat gemini-1.5-pro create',
43-
op: 'gen_ai.chat',
44-
origin: 'auto.ai.google_genai',
45-
}),
46-
// Second span - chat.sendMessage
30+
// chat.sendMessage
4731
expect.objectContaining({
4832
data: expect.objectContaining({
4933
[GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'chat',
@@ -59,7 +43,7 @@ it('traces Google GenAI chat creation and message sending', async () => {
5943
op: 'gen_ai.chat',
6044
origin: 'auto.ai.google_genai',
6145
}),
62-
// Third span - models.generateContent
46+
// models.generateContent
6347
expect.objectContaining({
6448
data: expect.objectContaining({
6549
[GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'generate_content',
@@ -78,7 +62,7 @@ it('traces Google GenAI chat creation and message sending', async () => {
7862
op: 'gen_ai.generate_content',
7963
origin: 'auto.ai.google_genai',
8064
}),
81-
// Fourth span - models.embedContent
65+
// models.embedContent
8266
expect.objectContaining({
8367
data: expect.objectContaining({
8468
[GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'embeddings',

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

Lines changed: 14 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -32,24 +32,7 @@ describe('Google GenAI integration', () => {
3232
const EXPECTED_TRANSACTION_DEFAULT_PII_FALSE = {
3333
transaction: 'main',
3434
spans: expect.arrayContaining([
35-
// First span - chats.create
36-
expect.objectContaining({
37-
data: {
38-
[GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'chat',
39-
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.chat',
40-
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.google_genai',
41-
[GEN_AI_SYSTEM_ATTRIBUTE]: 'google_genai',
42-
[GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'gemini-1.5-pro',
43-
[GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE]: 0.8,
44-
[GEN_AI_REQUEST_TOP_P_ATTRIBUTE]: 0.9,
45-
[GEN_AI_REQUEST_MAX_TOKENS_ATTRIBUTE]: 150,
46-
},
47-
description: 'chat gemini-1.5-pro create',
48-
op: 'gen_ai.chat',
49-
origin: 'auto.ai.google_genai',
50-
status: 'ok',
51-
}),
52-
// Second span - chat.sendMessage (should get model from context)
35+
// chat.sendMessage (should get model from context)
5336
expect.objectContaining({
5437
data: {
5538
[GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'chat',
@@ -66,7 +49,7 @@ describe('Google GenAI integration', () => {
6649
origin: 'auto.ai.google_genai',
6750
status: 'ok',
6851
}),
69-
// Third span - models.generateContent
52+
// models.generateContent
7053
expect.objectContaining({
7154
data: {
7255
[GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'generate_content',
@@ -86,7 +69,7 @@ describe('Google GenAI integration', () => {
8669
origin: 'auto.ai.google_genai',
8770
status: 'ok',
8871
}),
89-
// Fourth span - error handling
72+
// error handling
9073
expect.objectContaining({
9174
data: {
9275
[GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'generate_content',
@@ -106,25 +89,7 @@ describe('Google GenAI integration', () => {
10689
const EXPECTED_TRANSACTION_DEFAULT_PII_TRUE = {
10790
transaction: 'main',
10891
spans: expect.arrayContaining([
109-
// First span - chats.create with PII
110-
expect.objectContaining({
111-
data: expect.objectContaining({
112-
[GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'chat',
113-
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.chat',
114-
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.google_genai',
115-
[GEN_AI_SYSTEM_ATTRIBUTE]: 'google_genai',
116-
[GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'gemini-1.5-pro',
117-
[GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE]: 0.8,
118-
[GEN_AI_REQUEST_TOP_P_ATTRIBUTE]: 0.9,
119-
[GEN_AI_REQUEST_MAX_TOKENS_ATTRIBUTE]: 150,
120-
[GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: '[{"role":"user","parts":[{"text":"Hello, how are you?"}]}]',
121-
}),
122-
description: 'chat gemini-1.5-pro create',
123-
op: 'gen_ai.chat',
124-
origin: 'auto.ai.google_genai',
125-
status: 'ok',
126-
}),
127-
// Second span - chat.sendMessage with PII
92+
// chat.sendMessage with PII
12893
expect.objectContaining({
12994
data: expect.objectContaining({
13095
[GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'chat',
@@ -143,7 +108,7 @@ describe('Google GenAI integration', () => {
143108
origin: 'auto.ai.google_genai',
144109
status: 'ok',
145110
}),
146-
// Third span - models.generateContent with PII
111+
// models.generateContent with PII
147112
expect.objectContaining({
148113
data: expect.objectContaining({
149114
[GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'generate_content',
@@ -165,7 +130,7 @@ describe('Google GenAI integration', () => {
165130
origin: 'auto.ai.google_genai',
166131
status: 'ok',
167132
}),
168-
// Fourth span - error handling with PII
133+
// error handling with PII
169134
expect.objectContaining({
170135
data: expect.objectContaining({
171136
[GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'generate_content',
@@ -309,7 +274,7 @@ describe('Google GenAI integration', () => {
309274
const EXPECTED_TRANSACTION_STREAMING = {
310275
transaction: 'main',
311276
spans: expect.arrayContaining([
312-
// First span - models.generateContentStream (streaming)
277+
// models.generateContentStream (streaming)
313278
expect.objectContaining({
314279
data: expect.objectContaining({
315280
[GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'generate_content',
@@ -333,24 +298,7 @@ describe('Google GenAI integration', () => {
333298
origin: 'auto.ai.google_genai',
334299
status: 'ok',
335300
}),
336-
// Second span - chat.create
337-
expect.objectContaining({
338-
data: expect.objectContaining({
339-
[GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'chat',
340-
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.chat',
341-
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.google_genai',
342-
[GEN_AI_SYSTEM_ATTRIBUTE]: 'google_genai',
343-
[GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'gemini-1.5-pro',
344-
[GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE]: 0.8,
345-
[GEN_AI_REQUEST_TOP_P_ATTRIBUTE]: 0.9,
346-
[GEN_AI_REQUEST_MAX_TOKENS_ATTRIBUTE]: 150,
347-
}),
348-
description: 'chat gemini-1.5-pro create',
349-
op: 'gen_ai.chat',
350-
origin: 'auto.ai.google_genai',
351-
status: 'ok',
352-
}),
353-
// Third span - chat.sendMessageStream (streaming)
301+
// chat.sendMessageStream (streaming)
354302
expect.objectContaining({
355303
data: expect.objectContaining({
356304
[GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'chat',
@@ -367,7 +315,7 @@ describe('Google GenAI integration', () => {
367315
origin: 'auto.ai.google_genai',
368316
status: 'ok',
369317
}),
370-
// Fourth span - blocked content streaming
318+
// blocked content streaming
371319
expect.objectContaining({
372320
data: expect.objectContaining({
373321
[GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'generate_content',
@@ -379,7 +327,7 @@ describe('Google GenAI integration', () => {
379327
origin: 'auto.ai.google_genai',
380328
status: 'internal_error',
381329
}),
382-
// Fifth span - error handling for streaming
330+
// error handling for streaming
383331
expect.objectContaining({
384332
data: expect.objectContaining({
385333
[GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'generate_content',
@@ -397,7 +345,7 @@ describe('Google GenAI integration', () => {
397345
const EXPECTED_TRANSACTION_STREAMING_PII_TRUE = {
398346
transaction: 'main',
399347
spans: expect.arrayContaining([
400-
// First span - models.generateContentStream (streaming) with PII
348+
// models.generateContentStream (streaming) with PII
401349
expect.objectContaining({
402350
data: expect.objectContaining({
403351
[GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'generate_content',
@@ -422,24 +370,7 @@ describe('Google GenAI integration', () => {
422370
origin: 'auto.ai.google_genai',
423371
status: 'ok',
424372
}),
425-
// Second span - chat.create
426-
expect.objectContaining({
427-
data: expect.objectContaining({
428-
[GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'chat',
429-
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'gen_ai.chat',
430-
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.google_genai',
431-
[GEN_AI_SYSTEM_ATTRIBUTE]: 'google_genai',
432-
[GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'gemini-1.5-pro',
433-
[GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE]: 0.8,
434-
[GEN_AI_REQUEST_TOP_P_ATTRIBUTE]: 0.9,
435-
[GEN_AI_REQUEST_MAX_TOKENS_ATTRIBUTE]: 150,
436-
}),
437-
description: 'chat gemini-1.5-pro create',
438-
op: 'gen_ai.chat',
439-
origin: 'auto.ai.google_genai',
440-
status: 'ok',
441-
}),
442-
// Third span - chat.sendMessageStream (streaming) with PII
373+
// chat.sendMessageStream (streaming) with PII
443374
expect.objectContaining({
444375
data: expect.objectContaining({
445376
[GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'chat',
@@ -461,7 +392,7 @@ describe('Google GenAI integration', () => {
461392
origin: 'auto.ai.google_genai',
462393
status: 'ok',
463394
}),
464-
// Fourth span - blocked content stream with PII
395+
// blocked content stream with PII
465396
expect.objectContaining({
466397
data: expect.objectContaining({
467398
[GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'generate_content',
@@ -478,7 +409,7 @@ describe('Google GenAI integration', () => {
478409
origin: 'auto.ai.google_genai',
479410
status: 'internal_error',
480411
}),
481-
// Fifth span - error handling for streaming with PII
412+
// error handling for streaming with PII
482413
expect.objectContaining({
483414
data: expect.objectContaining({
484415
[GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'generate_content',

packages/core/src/tracing/ai/utils.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,12 @@ export interface AIRecordingOptions {
2828
* which gen_ai operation it maps to and whether it is intrinsically streaming.
2929
*/
3030
export interface InstrumentedMethodEntry {
31-
/** Operation name (e.g. 'chat', 'embeddings', 'generate_content') */
32-
operation: string;
31+
/** Operation name (e.g. 'chat', 'embeddings', 'generate_content'). Omit for factory methods that only need result proxying. */
32+
operation?: string;
3333
/** True if the method itself is always streaming (not param-based) */
3434
streaming?: boolean;
35+
/** When set, the method's return value is re-proxied with this as the base path */
36+
proxyResultPath?: string;
3537
}
3638

3739
/**

packages/core/src/tracing/anthropic-ai/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,7 @@ function instrumentMethod<T extends unknown[], R>(
253253
): (...args: T) => R | Promise<R> {
254254
return new Proxy(originalMethod, {
255255
apply(target, thisArg, args: T): R | Promise<R> {
256-
const operationName = instrumentedMethod.operation;
256+
const operationName = instrumentedMethod.operation || 'unknown';
257257
const requestAttributes = extractRequestAttributes(args, methodPath, operationName);
258258
const model = requestAttributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE] ?? 'unknown';
259259

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

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,10 @@ export const GOOGLE_GENAI_METHOD_REGISTRY = {
1010
'models.generateContent': { operation: 'generate_content' },
1111
'models.generateContentStream': { operation: 'generate_content', streaming: true },
1212
'models.embedContent': { operation: 'embeddings' },
13-
'chats.create': { operation: 'chat' },
14-
// chat.* paths are built by createDeepProxy when it proxies the chat instance with CHAT_PATH as base
13+
'chats.create': { proxyResultPath: 'chat' },
1514
'chat.sendMessage': { operation: 'chat' },
1615
'chat.sendMessageStream': { operation: 'chat', streaming: true },
1716
} as const satisfies InstrumentedMethodRegistry;
1817

1918
// Constants for internal use
2019
export const GOOGLE_GENAI_SYSTEM_NAME = 'google_genai';
21-
export const CHATS_CREATE_METHOD = 'chats.create';
22-
export const CHAT_PATH = 'chat';

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

Lines changed: 25 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ import {
3030
import { truncateGenAiMessages } from '../ai/messageTruncation';
3131
import type { InstrumentedMethodEntry } from '../ai/utils';
3232
import { buildMethodPath, extractSystemInstructions, resolveAIRecordingOptions } from '../ai/utils';
33-
import { CHAT_PATH, CHATS_CREATE_METHOD, GOOGLE_GENAI_METHOD_REGISTRY, GOOGLE_GENAI_SYSTEM_NAME } from './constants';
33+
import { GOOGLE_GENAI_METHOD_REGISTRY, GOOGLE_GENAI_SYSTEM_NAME } from './constants';
3434
import { instrumentStream } from './streaming';
3535
import type { Candidate, ContentPart, GoogleGenAIOptions, GoogleGenAIResponse } from './types';
3636
import type { ContentListUnion, ContentUnion, Message, PartListUnion } from './utils';
@@ -264,12 +264,11 @@ function instrumentMethod<T extends unknown[], R>(
264264
context: unknown,
265265
options: GoogleGenAIOptions,
266266
): (...args: T) => R | Promise<R> {
267-
const isSyncCreate = methodPath === CHATS_CREATE_METHOD;
268267
const isEmbeddings = instrumentedMethod.operation === 'embeddings';
269268

270269
return new Proxy(originalMethod, {
271270
apply(target, _, args: T): R | Promise<R> {
272-
const operationName = instrumentedMethod.operation;
271+
const operationName = instrumentedMethod.operation || 'unknown';
273272
const params = args[0] as Record<string, unknown> | undefined;
274273
const requestAttributes = extractRequestAttributes(operationName, params, context);
275274
const model = requestAttributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE] ?? 'unknown';
@@ -308,7 +307,7 @@ function instrumentMethod<T extends unknown[], R>(
308307
// Single span for both sync and async operations
309308
return startSpan(
310309
{
311-
name: isSyncCreate ? `${operationName} ${model} create` : `${operationName} ${model}`,
310+
name: `${operationName} ${model}`,
312311
op: `gen_ai.${operationName}`,
313312
attributes: requestAttributes,
314313
},
@@ -326,8 +325,8 @@ function instrumentMethod<T extends unknown[], R>(
326325
},
327326
() => {},
328327
result => {
329-
// Only add response attributes for content-producing methods, not for chats.create or embeddings
330-
if (!isSyncCreate && !isEmbeddings) {
328+
// Only add response attributes for content-producing methods, not for embeddings
329+
if (!isEmbeddings) {
331330
addResponseAttributes(span, result, options.recordOutputs);
332331
}
333332
},
@@ -348,34 +347,29 @@ function createDeepProxy<T extends object>(target: T, currentPath = '', options:
348347
const value = Reflect.get(t, prop, receiver);
349348
const methodPath = buildMethodPath(currentPath, String(prop));
350349

351-
const instrumentedMethod = GOOGLE_GENAI_METHOD_REGISTRY[methodPath as keyof typeof GOOGLE_GENAI_METHOD_REGISTRY];
350+
const instrumentedMethod: InstrumentedMethodEntry | undefined =
351+
GOOGLE_GENAI_METHOD_REGISTRY[methodPath as keyof typeof GOOGLE_GENAI_METHOD_REGISTRY];
352352
if (typeof value === 'function' && instrumentedMethod) {
353-
// Special case: chats.create is synchronous but needs both instrumentation AND result proxying
354-
if (methodPath === CHATS_CREATE_METHOD) {
355-
const wrappedMethod = instrumentMethod(
356-
value as (...args: unknown[]) => unknown,
357-
methodPath,
358-
instrumentedMethod,
359-
t,
360-
options,
361-
);
362-
return function instrumentedAndProxiedCreate(...args: unknown[]): unknown {
363-
const result = wrappedMethod(...args);
364-
// If the result is an object (like a chat instance), proxy it too
365-
if (result && typeof result === 'object') {
366-
return createDeepProxy(result, CHAT_PATH, options);
367-
}
368-
return result;
369-
};
353+
// If an operation is specified, we need to instrument the method itself
354+
const wrappedMethod = instrumentedMethod.operation
355+
? instrumentMethod(value as (...args: unknown[]) => unknown, methodPath, instrumentedMethod, t, options)
356+
: value.bind(t);
357+
358+
if (!instrumentedMethod.proxyResultPath) {
359+
return wrappedMethod;
370360
}
371361

372-
return instrumentMethod(
373-
value as (...args: unknown[]) => Promise<unknown>,
374-
methodPath,
375-
instrumentedMethod,
376-
t,
377-
options,
378-
);
362+
// If a proxyResultPath is specified, we need to proxy the result of the method.
363+
// Note: This currently only properly handles synchronous methods. For async methods,
364+
// the Promise itself would be proxied instead of the resolved value. Currently we
365+
// don't have a case where this is needed, so I'll keep it simple for now.
366+
return function (...args: unknown[]): unknown {
367+
const result = wrappedMethod(...args);
368+
if (result && typeof result === 'object') {
369+
return createDeepProxy(result as object, instrumentedMethod.proxyResultPath, options);
370+
}
371+
return result;
372+
};
379373
}
380374

381375
if (typeof value === 'function') {

packages/core/src/tracing/openai/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ function instrumentMethod<T extends unknown[], R>(
142142
options: OpenAiOptions,
143143
): (...args: T) => Promise<R> {
144144
return function instrumentedCall(...args: T): Promise<R> {
145-
const operationName = instrumentedMethod.operation;
145+
const operationName = instrumentedMethod.operation || 'unknown';
146146
const requestAttributes = extractRequestAttributes(args, operationName);
147147
const model = (requestAttributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE] as string) || 'unknown';
148148

0 commit comments

Comments
 (0)