Skip to content

Commit 4a43db2

Browse files
authored
ref(core): Consolidate getOperationName into one shared utility (#19971)
We have this function in both the shared utilities (used by `google-genai` and `anthropic`) and in `openai` with slightly different names for no apparent reason. We also had a separate helper that just prepends `gen_ai` to the operation name in both cases, which seems unnecessary. Doing some cleanup here Closes #19978 (added automatically)
1 parent c8d4677 commit 4a43db2

File tree

7 files changed

+30
-92
lines changed

7 files changed

+30
-92
lines changed

packages/core/src/tracing/ai/gen-ai-attributes.ts

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -312,19 +312,6 @@ export const OPENAI_USAGE_COMPLETION_TOKENS_ATTRIBUTE = 'openai.usage.completion
312312
*/
313313
export const OPENAI_USAGE_PROMPT_TOKENS_ATTRIBUTE = 'openai.usage.prompt_tokens';
314314

315-
// =============================================================================
316-
// OPENAI OPERATIONS
317-
// =============================================================================
318-
319-
/**
320-
* OpenAI API operations following OpenTelemetry semantic conventions
321-
* @see https://opentelemetry.io/docs/specs/semconv/gen-ai/gen-ai-spans/#llm-request-spans
322-
*/
323-
export const OPENAI_OPERATIONS = {
324-
CHAT: 'chat',
325-
EMBEDDINGS: 'embeddings',
326-
} as const;
327-
328315
// =============================================================================
329316
// ANTHROPIC AI OPERATIONS
330317
// =============================================================================

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

Lines changed: 16 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -34,35 +34,34 @@ export function resolveAIRecordingOptions<T extends AIRecordingOptions>(options?
3434
* Maps AI method paths to OpenTelemetry semantic convention operation names
3535
* @see https://opentelemetry.io/docs/specs/semconv/gen-ai/gen-ai-spans/#llm-request-spans
3636
*/
37-
export function getFinalOperationName(methodPath: string): string {
38-
if (methodPath.includes('messages')) {
37+
export function getOperationName(methodPath: string): string {
38+
// OpenAI: chat.completions.create, responses.create, conversations.create
39+
// Anthropic: messages.create, messages.stream, completions.create
40+
// Google GenAI: chats.create, chat.sendMessage, chat.sendMessageStream
41+
if (
42+
methodPath.includes('completions') ||
43+
methodPath.includes('responses') ||
44+
methodPath.includes('conversations') ||
45+
methodPath.includes('messages') ||
46+
methodPath.includes('chat')
47+
) {
3948
return 'chat';
4049
}
41-
if (methodPath.includes('completions')) {
42-
return 'text_completion';
50+
// OpenAI: embeddings.create
51+
if (methodPath.includes('embeddings')) {
52+
return 'embeddings';
4353
}
44-
// Google GenAI: models.generateContent* -> generate_content (actually generates AI responses)
54+
// Google GenAI: models.generateContent, models.generateContentStream (must be before 'models' check)
4555
if (methodPath.includes('generateContent')) {
4656
return 'generate_content';
4757
}
48-
// Anthropic: models.get/retrieve -> models (metadata retrieval only)
58+
// Anthropic: models.get, models.retrieve (metadata retrieval only)
4959
if (methodPath.includes('models')) {
5060
return 'models';
5161
}
52-
if (methodPath.includes('chat')) {
53-
return 'chat';
54-
}
5562
return methodPath.split('.').pop() || 'unknown';
5663
}
5764

58-
/**
59-
* Get the span operation for AI methods
60-
* Following Sentry's convention: "gen_ai.{operation_name}"
61-
*/
62-
export function getSpanOperation(methodPath: string): string {
63-
return `gen_ai.${getFinalOperationName(methodPath)}`;
64-
}
65-
6665
/**
6766
* Build method path from current traversal
6867
*/

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

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,7 @@ import {
2323
} from '../ai/gen-ai-attributes';
2424
import {
2525
buildMethodPath,
26-
getFinalOperationName,
27-
getSpanOperation,
26+
getOperationName,
2827
resolveAIRecordingOptions,
2928
setTokenUsageAttributes,
3029
wrapPromiseWithMethods,
@@ -45,7 +44,7 @@ import { handleResponseError, messagesFromParams, setMessagesAttribute, shouldIn
4544
function extractRequestAttributes(args: unknown[], methodPath: string): Record<string, unknown> {
4645
const attributes: Record<string, unknown> = {
4746
[GEN_AI_SYSTEM_ATTRIBUTE]: 'anthropic',
48-
[GEN_AI_OPERATION_NAME_ATTRIBUTE]: getFinalOperationName(methodPath),
47+
[GEN_AI_OPERATION_NAME_ATTRIBUTE]: getOperationName(methodPath),
4948
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.anthropic',
5049
};
5150

@@ -212,7 +211,7 @@ function handleStreamingRequest<T extends unknown[], R>(
212211
const model = requestAttributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE] ?? 'unknown';
213212
const spanConfig = {
214213
name: `${operationName} ${model}`,
215-
op: getSpanOperation(methodPath),
214+
op: `gen_ai.${operationName}`,
216215
attributes: requestAttributes as Record<string, SpanAttributeValue>,
217216
};
218217

@@ -272,7 +271,7 @@ function instrumentMethod<T extends unknown[], R>(
272271
apply(target, thisArg, args: T): R | Promise<R> {
273272
const requestAttributes = extractRequestAttributes(args, methodPath);
274273
const model = requestAttributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE] ?? 'unknown';
275-
const operationName = getFinalOperationName(methodPath);
274+
const operationName = getOperationName(methodPath);
276275

277276
const params = typeof args[0] === 'object' ? (args[0] as Record<string, unknown>) : undefined;
278277
const isStreamRequested = Boolean(params?.stream);
@@ -299,7 +298,7 @@ function instrumentMethod<T extends unknown[], R>(
299298
const instrumentedPromise = startSpan(
300299
{
301300
name: `${operationName} ${model}`,
302-
op: getSpanOperation(methodPath),
301+
op: `gen_ai.${operationName}`,
303302
attributes: requestAttributes as Record<string, SpanAttributeValue>,
304303
},
305304
span => {

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

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,7 @@ import {
2626
GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE,
2727
} from '../ai/gen-ai-attributes';
2828
import { truncateGenAiMessages } from '../ai/messageTruncation';
29-
import {
30-
buildMethodPath,
31-
extractSystemInstructions,
32-
getFinalOperationName,
33-
getSpanOperation,
34-
resolveAIRecordingOptions,
35-
} from '../ai/utils';
29+
import { buildMethodPath, extractSystemInstructions, getOperationName, resolveAIRecordingOptions } from '../ai/utils';
3630
import { CHAT_PATH, CHATS_CREATE_METHOD, GOOGLE_GENAI_SYSTEM_NAME } from './constants';
3731
import { instrumentStream } from './streaming';
3832
import type {
@@ -111,7 +105,7 @@ function extractRequestAttributes(
111105
): Record<string, SpanAttributeValue> {
112106
const attributes: Record<string, SpanAttributeValue> = {
113107
[GEN_AI_SYSTEM_ATTRIBUTE]: GOOGLE_GENAI_SYSTEM_NAME,
114-
[GEN_AI_OPERATION_NAME_ATTRIBUTE]: getFinalOperationName(methodPath),
108+
[GEN_AI_OPERATION_NAME_ATTRIBUTE]: getOperationName(methodPath),
115109
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ai.google_genai',
116110
};
117111

@@ -268,15 +262,15 @@ function instrumentMethod<T extends unknown[], R>(
268262
const params = args[0] as Record<string, unknown> | undefined;
269263
const requestAttributes = extractRequestAttributes(methodPath, params, context);
270264
const model = requestAttributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE] ?? 'unknown';
271-
const operationName = getFinalOperationName(methodPath);
265+
const operationName = getOperationName(methodPath);
272266

273267
// Check if this is a streaming method
274268
if (isStreamingMethod(methodPath)) {
275269
// Use startSpanManual for streaming methods to control span lifecycle
276270
return startSpanManual(
277271
{
278272
name: `${operationName} ${model}`,
279-
op: getSpanOperation(methodPath),
273+
op: `gen_ai.${operationName}`,
280274
attributes: requestAttributes,
281275
},
282276
async (span: Span) => {
@@ -305,7 +299,7 @@ function instrumentMethod<T extends unknown[], R>(
305299
return startSpan(
306300
{
307301
name: isSyncCreate ? `${operationName} ${model} create` : `${operationName} ${model}`,
308-
op: getSpanOperation(methodPath),
302+
op: `gen_ai.${operationName}`,
309303
attributes: requestAttributes,
310304
},
311305
(span: Span) => {

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

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,10 @@ import {
1515
GEN_AI_RESPONSE_TEXT_ATTRIBUTE,
1616
GEN_AI_SYSTEM_ATTRIBUTE,
1717
GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE,
18-
OPENAI_OPERATIONS,
1918
} from '../ai/gen-ai-attributes';
2019
import {
2120
extractSystemInstructions,
21+
getOperationName,
2222
getTruncatedJsonString,
2323
resolveAIRecordingOptions,
2424
wrapPromiseWithMethods,
@@ -39,8 +39,6 @@ import {
3939
addEmbeddingsAttributes,
4040
addResponsesApiAttributes,
4141
extractRequestParameters,
42-
getOperationName,
43-
getSpanOperation,
4442
isChatCompletionResponse,
4543
isConversationResponse,
4644
isEmbeddingsResponse,
@@ -127,7 +125,7 @@ function addResponseAttributes(span: Span, result: unknown, recordOutputs?: bool
127125
// Extract and record AI request inputs, if present. This is intentionally separate from response attributes.
128126
function addRequestAttributes(span: Span, params: Record<string, unknown>, operationName: string): void {
129127
// Store embeddings input on a separate attribute and do not truncate it
130-
if (operationName === OPENAI_OPERATIONS.EMBEDDINGS && 'input' in params) {
128+
if (operationName === 'embeddings' && 'input' in params) {
131129
const input = params.input;
132130

133131
// No input provided
@@ -197,7 +195,7 @@ function instrumentMethod<T extends unknown[], R>(
197195

198196
const spanConfig = {
199197
name: `${operationName} ${model}`,
200-
op: getSpanOperation(methodPath),
198+
op: `gen_ai.${operationName}`,
201199
attributes: requestAttributes as Record<string, SpanAttributeValue>,
202200
};
203201

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

Lines changed: 0 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ import {
1616
GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE,
1717
GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE,
1818
GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE,
19-
OPENAI_OPERATIONS,
2019
OPENAI_RESPONSE_ID_ATTRIBUTE,
2120
OPENAI_RESPONSE_MODEL_ATTRIBUTE,
2221
OPENAI_RESPONSE_TIMESTAMP_ATTRIBUTE,
@@ -34,34 +33,6 @@ import type {
3433
ResponseStreamingEvent,
3534
} from './types';
3635

37-
/**
38-
* Maps OpenAI method paths to OpenTelemetry semantic convention operation names
39-
* @see https://opentelemetry.io/docs/specs/semconv/gen-ai/gen-ai-spans/#llm-request-spans
40-
*/
41-
export function getOperationName(methodPath: string): string {
42-
if (methodPath.includes('chat.completions')) {
43-
return OPENAI_OPERATIONS.CHAT;
44-
}
45-
if (methodPath.includes('responses')) {
46-
return OPENAI_OPERATIONS.CHAT;
47-
}
48-
if (methodPath.includes('embeddings')) {
49-
return OPENAI_OPERATIONS.EMBEDDINGS;
50-
}
51-
if (methodPath.includes('conversations')) {
52-
return OPENAI_OPERATIONS.CHAT;
53-
}
54-
return methodPath.split('.').pop() || 'unknown';
55-
}
56-
57-
/**
58-
* Get the span operation for OpenAI methods
59-
* Following Sentry's convention: "gen_ai.{operation_name}"
60-
*/
61-
export function getSpanOperation(methodPath: string): string {
62-
return `gen_ai.${getOperationName(methodPath)}`;
63-
}
64-
6536
/**
6637
* Check if a method path should be instrumented
6738
*/

packages/core/test/lib/utils/openai-utils.test.ts

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
import { describe, expect, it } from 'vitest';
2-
import { buildMethodPath } from '../../../src/tracing/ai/utils';
2+
import { buildMethodPath, getOperationName } from '../../../src/tracing/ai/utils';
33
import {
4-
getOperationName,
5-
getSpanOperation,
64
isChatCompletionChunk,
75
isChatCompletionResponse,
86
isConversationResponse,
@@ -38,14 +36,6 @@ describe('openai-utils', () => {
3836
});
3937
});
4038

41-
describe('getSpanOperation', () => {
42-
it('should prefix operation with gen_ai', () => {
43-
expect(getSpanOperation('chat.completions.create')).toBe('gen_ai.chat');
44-
expect(getSpanOperation('responses.create')).toBe('gen_ai.chat');
45-
expect(getSpanOperation('some.custom.operation')).toBe('gen_ai.operation');
46-
});
47-
});
48-
4939
describe('shouldInstrument', () => {
5040
it('should return true for instrumented methods', () => {
5141
expect(shouldInstrument('responses.create')).toBe(true);

0 commit comments

Comments
 (0)