Skip to content

Commit c6e8561

Browse files
committed
feat: Instrument openai for node
1 parent 65162a2 commit c6e8561

File tree

12 files changed

+1250
-1
lines changed

12 files changed

+1250
-1
lines changed

packages/core/src/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,9 @@ export type { ReportDialogOptions } from './report-dialog';
124124
export { _INTERNAL_captureLog, _INTERNAL_flushLogsBuffer, _INTERNAL_captureSerializedLog } from './logs/exports';
125125
export { consoleLoggingIntegration } from './logs/console-integration';
126126
export { addVercelAiProcessors } from './utils/vercel-ai';
127-
127+
export { instrumentOpenAiClient } from './utils/openai';
128+
export { INTEGRATION_NAME } from './utils/openai-constants';
129+
export type { OpenAiClient, OpenAiOptions, InstrumentedMethod } from './utils/openai-types';
128130
export type { FeatureFlag } from './utils/featureFlags';
129131
export {
130132
_INTERNAL_copyFlagsFromScopeToEvent,
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
/**
2+
* OpenAI Integration Telemetry Attributes
3+
* Based on OpenTelemetry Semantic Conventions for Generative AI
4+
* @see https://opentelemetry.io/docs/specs/semconv/gen-ai/
5+
*/
6+
7+
// =============================================================================
8+
// OPENTELEMETRY SEMANTIC CONVENTIONS FOR GENAI
9+
// =============================================================================
10+
11+
/**
12+
* The Generative AI system being used
13+
* For OpenAI, this should always be "openai"
14+
*/
15+
export const GEN_AI_SYSTEM_ATTRIBUTE = 'gen_ai.system';
16+
17+
/**
18+
* The name of the model as requested
19+
* Examples: "gpt-4", "gpt-3.5-turbo"
20+
*/
21+
export const GEN_AI_REQUEST_MODEL_ATTRIBUTE = 'gen_ai.request.model';
22+
23+
/**
24+
* The temperature setting for the model request
25+
*/
26+
export const GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE = 'gen_ai.request.temperature';
27+
28+
/**
29+
* The maximum number of tokens requested
30+
*/
31+
export const GEN_AI_REQUEST_MAX_TOKENS_ATTRIBUTE = 'gen_ai.request.max_tokens';
32+
33+
/**
34+
* The frequency penalty setting for the model request
35+
*/
36+
export const GEN_AI_REQUEST_FREQUENCY_PENALTY_ATTRIBUTE = 'gen_ai.request.frequency_penalty';
37+
38+
/**
39+
* The presence penalty setting for the model request
40+
*/
41+
export const GEN_AI_REQUEST_PRESENCE_PENALTY_ATTRIBUTE = 'gen_ai.request.presence_penalty';
42+
43+
/**
44+
* The top_p (nucleus sampling) setting for the model request
45+
*/
46+
export const GEN_AI_REQUEST_TOP_P_ATTRIBUTE = 'gen_ai.request.top_p';
47+
48+
/**
49+
* The top_k setting for the model request
50+
*/
51+
export const GEN_AI_REQUEST_TOP_K_ATTRIBUTE = 'gen_ai.request.top_k';
52+
53+
/**
54+
* Stop sequences for the model request
55+
*/
56+
export const GEN_AI_REQUEST_STOP_SEQUENCES_ATTRIBUTE = 'gen_ai.request.stop_sequences';
57+
58+
/**
59+
* Array of reasons why the model stopped generating tokens
60+
*/
61+
export const GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE = 'gen_ai.response.finish_reasons';
62+
63+
/**
64+
* The name of the model that generated the response
65+
*/
66+
export const GEN_AI_RESPONSE_MODEL_ATTRIBUTE = 'gen_ai.response.model';
67+
68+
/**
69+
* The unique identifier for the response
70+
*/
71+
export const GEN_AI_RESPONSE_ID_ATTRIBUTE = 'gen_ai.response.id';
72+
73+
/**
74+
* The number of tokens used in the prompt
75+
*/
76+
export const GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE = 'gen_ai.usage.input_tokens';
77+
78+
/**
79+
* The number of tokens used in the response
80+
*/
81+
export const GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE = 'gen_ai.usage.output_tokens';
82+
83+
/**
84+
* The total number of tokens used (input + output)
85+
*/
86+
export const GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE = 'gen_ai.usage.total_tokens';
87+
88+
/**
89+
* The operation name for OpenAI API calls
90+
*/
91+
export const GEN_AI_OPERATION_NAME_ATTRIBUTE = 'gen_ai.operation.name';
92+
93+
/**
94+
* The prompt messages sent to OpenAI (stringified JSON)
95+
* Only recorded when recordInputs is enabled
96+
*/
97+
export const GEN_AI_REQUEST_MESSAGES_ATTRIBUTE = 'gen_ai.request.messages';
98+
99+
/**
100+
* The response text from OpenAI (stringified JSON array)
101+
* Only recorded when recordOutputs is enabled
102+
*/
103+
export const GEN_AI_RESPONSE_TEXT_ATTRIBUTE = 'gen_ai.response.text';
104+
105+
// =============================================================================
106+
// OPENAI-SPECIFIC ATTRIBUTES
107+
// =============================================================================
108+
109+
/**
110+
* The response ID from OpenAI
111+
*/
112+
export const OPENAI_RESPONSE_ID_ATTRIBUTE = 'openai.response.id';
113+
114+
/**
115+
* The response model from OpenAI
116+
*/
117+
export const OPENAI_RESPONSE_MODEL_ATTRIBUTE = 'openai.response.model';
118+
119+
/**
120+
* The response timestamp from OpenAI (ISO string)
121+
*/
122+
export const OPENAI_RESPONSE_TIMESTAMP_ATTRIBUTE = 'openai.response.timestamp';
123+
124+
/**
125+
* The number of completion tokens used (OpenAI specific)
126+
*/
127+
export const OPENAI_USAGE_COMPLETION_TOKENS_ATTRIBUTE = 'openai.usage.completion_tokens';
128+
129+
/**
130+
* The number of prompt tokens used (OpenAI specific)
131+
*/
132+
export const OPENAI_USAGE_PROMPT_TOKENS_ATTRIBUTE = 'openai.usage.prompt_tokens';
133+
134+
// =============================================================================
135+
// OPENAI OPERATIONS
136+
// =============================================================================
137+
138+
/**
139+
* OpenAI API operations
140+
*/
141+
export const OPENAI_OPERATIONS = {
142+
CHAT: 'chat',
143+
} as const;
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export const INTEGRATION_NAME = 'openAI';
2+
3+
// https://platform.openai.com/docs/quickstart?api-mode=responses
4+
// https://platform.openai.com/docs/quickstart?api-mode=chat
5+
export const INSTRUMENTED_METHODS = ['responses.create', 'chat.completions.create'] as const;
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
import { INSTRUMENTED_METHODS } from './openai-constants';
2+
3+
/**
4+
* Attribute values may be any non-nullish primitive value except an object.
5+
*
6+
* null or undefined attribute values are invalid and will result in undefined behavior.
7+
*/
8+
export type AttributeValue =
9+
| string
10+
| number
11+
| boolean
12+
| Array<null | undefined | string>
13+
| Array<null | undefined | number>
14+
| Array<null | undefined | boolean>;
15+
16+
export interface OpenAiOptions {
17+
/**
18+
* Enable or disable input recording. Enabled if `sendDefaultPii` is `true`
19+
*/
20+
recordInputs?: boolean;
21+
/**
22+
* Enable or disable output recording. Enabled if `sendDefaultPii` is `true`
23+
*/
24+
recordOutputs?: boolean;
25+
}
26+
27+
export interface OpenAiClient {
28+
responses?: {
29+
create: (...args: unknown[]) => Promise<unknown>;
30+
};
31+
chat?: {
32+
completions?: {
33+
create: (...args: unknown[]) => Promise<unknown>;
34+
};
35+
};
36+
}
37+
38+
/**
39+
* @see https://platform.openai.com/docs/api-reference/chat/object
40+
*/
41+
export interface OpenAiChatCompletionObject {
42+
id: string;
43+
object: 'chat.completion';
44+
created: number;
45+
model: string;
46+
choices: Array<{
47+
index: number;
48+
message: {
49+
role: 'assistant' | 'user' | 'system' | string;
50+
content: string | null;
51+
refusal?: string | null;
52+
annotations?: Array<unknown>; // Depends on whether annotations are enabled
53+
};
54+
logprobs?: unknown | null;
55+
finish_reason: string | null;
56+
}>;
57+
usage: {
58+
prompt_tokens: number;
59+
completion_tokens: number;
60+
total_tokens: number;
61+
prompt_tokens_details?: {
62+
cached_tokens?: number;
63+
audio_tokens?: number;
64+
};
65+
completion_tokens_details?: {
66+
reasoning_tokens?: number;
67+
audio_tokens?: number;
68+
accepted_prediction_tokens?: number;
69+
rejected_prediction_tokens?: number;
70+
};
71+
};
72+
service_tier?: string;
73+
system_fingerprint?: string;
74+
}
75+
76+
/**
77+
* @see https://platform.openai.com/docs/api-reference/responses/object
78+
*/
79+
export interface OpenAIResponseObject {
80+
id: string;
81+
object: 'response';
82+
created_at: number;
83+
status: 'in_progress' | 'completed' | 'failed' | 'cancelled';
84+
error: string | null;
85+
incomplete_details: unknown | null;
86+
instructions: unknown | null;
87+
max_output_tokens: number | null;
88+
model: string;
89+
output: Array<{
90+
type: 'message';
91+
id: string;
92+
status: 'completed' | string;
93+
role: 'assistant' | string;
94+
content: Array<{
95+
type: 'output_text';
96+
text: string;
97+
annotations: Array<unknown>;
98+
}>;
99+
}>;
100+
output_text: string; // Direct text output field
101+
parallel_tool_calls: boolean;
102+
previous_response_id: string | null;
103+
reasoning: {
104+
effort: string | null;
105+
summary: string | null;
106+
};
107+
store: boolean;
108+
temperature: number;
109+
text: {
110+
format: {
111+
type: 'text' | string;
112+
};
113+
};
114+
tool_choice: 'auto' | string;
115+
tools: Array<unknown>;
116+
top_p: number;
117+
truncation: 'disabled' | string;
118+
usage: {
119+
input_tokens: number;
120+
input_tokens_details?: {
121+
cached_tokens?: number;
122+
};
123+
output_tokens: number;
124+
output_tokens_details?: {
125+
reasoning_tokens?: number;
126+
};
127+
total_tokens: number;
128+
};
129+
user: string | null;
130+
metadata: Record<string, unknown>;
131+
}
132+
133+
export type OpenAiResponse = OpenAiChatCompletionObject | OpenAIResponseObject;
134+
135+
export interface OpenAiOptions {
136+
recordInputs?: boolean;
137+
recordOutputs?: boolean;
138+
}
139+
140+
export interface OpenAiClient {
141+
responses?: {
142+
create: (...args: unknown[]) => Promise<unknown>;
143+
};
144+
chat?: {
145+
completions?: {
146+
create: (...args: unknown[]) => Promise<unknown>;
147+
};
148+
};
149+
}
150+
151+
/**
152+
* OpenAI Integration interface for type safety
153+
*/
154+
export interface OpenAiIntegration {
155+
name: string;
156+
options: OpenAiOptions;
157+
}
158+
159+
export type InstrumentedMethod = (typeof INSTRUMENTED_METHODS)[number];
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { OPENAI_OPERATIONS } from './openai-attributes';
2+
import { INSTRUMENTED_METHODS } from './openai-constants';
3+
import type { InstrumentedMethod, OpenAiChatCompletionObject, OpenAIResponseObject } from './openai-types';
4+
5+
/**
6+
* Maps OpenAI method paths to Sentry operation names
7+
*/
8+
export function getOperationName(methodPath: string): string {
9+
if (methodPath.includes('chat.completions')) {
10+
return OPENAI_OPERATIONS.CHAT;
11+
}
12+
if (methodPath.includes('responses')) {
13+
// The responses API is also a chat operation
14+
return OPENAI_OPERATIONS.CHAT;
15+
}
16+
if (methodPath.includes('embeddings')) {
17+
return 'embeddings';
18+
}
19+
// Default to the last part of the method path
20+
return methodPath.split('.').pop() || 'unknown';
21+
}
22+
23+
/**
24+
* Get the span operation for OpenAI methods
25+
* Following Sentry's convention: "gen_ai.{operation_name}"
26+
*/
27+
export function getSpanOperation(methodPath: string): string {
28+
return `gen_ai.${getOperationName(methodPath)}`;
29+
}
30+
31+
/**
32+
* Check if a method path should be instrumented
33+
*/
34+
export function shouldInstrument(methodPath: string): methodPath is InstrumentedMethod {
35+
return INSTRUMENTED_METHODS.includes(methodPath as InstrumentedMethod);
36+
}
37+
38+
/**
39+
* Build method path from current traversal
40+
*/
41+
export function buildMethodPath(currentPath: string, prop: string): string {
42+
return currentPath ? `${currentPath}.${prop}` : prop;
43+
}
44+
45+
/**
46+
* Check if response is a Chat Completion object
47+
*/
48+
export function isChatCompletionResponse(response: unknown): response is OpenAiChatCompletionObject {
49+
return (
50+
response !== null &&
51+
typeof response === 'object' &&
52+
'object' in response &&
53+
(response as Record<string, unknown>).object === 'chat.completion'
54+
);
55+
}
56+
57+
/**
58+
* Check if response is a Responses API object
59+
*/
60+
export function isResponsesApiResponse(response: unknown): response is OpenAIResponseObject {
61+
return (
62+
response !== null &&
63+
typeof response === 'object' &&
64+
'object' in response &&
65+
(response as Record<string, unknown>).object === 'response'
66+
);
67+
}

0 commit comments

Comments
 (0)