-
-
Notifications
You must be signed in to change notification settings - Fork 1.7k
feat(node): Add OpenAI integration #17022
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 5 commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
c6e8561
feat: Instrument openai for node
RulaKhaled 6966214
add tests, refactor some types
RulaKhaled 247bf9f
fix responses test
RulaKhaled 8535978
quick refactor to record inputs option
RulaKhaled ca2d43e
update name to reference integration name constant
RulaKhaled db18c0a
rename to gen-ai-attributes, and fix test
RulaKhaled e9d9f51
Move stuff to sub folder, update with small refactor
RulaKhaled 04a2dde
Remove vercel code leftovers from openai's
RulaKhaled 86927ff
fix tests
RulaKhaled File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
19 changes: 19 additions & 0 deletions
19
dev-packages/node-integration-tests/suites/tracing/openai/instrument-with-options.mjs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
import * as Sentry from '@sentry/node'; | ||
import { loggingTransport } from '@sentry-internal/node-integration-tests'; | ||
|
||
// Set environment variable to trigger the fourth test | ||
process.env.TEST_OPTIONS = '1'; | ||
|
||
Sentry.init({ | ||
dsn: 'https://[email protected]/1337', | ||
release: '1.0', | ||
tracesSampleRate: 1.0, | ||
sendDefaultPii: false, | ||
transport: loggingTransport, | ||
integrations: [ | ||
Sentry.openAIIntegration({ | ||
recordInputs: true, | ||
recordOutputs: true, | ||
}), | ||
], | ||
}); |
11 changes: 11 additions & 0 deletions
11
dev-packages/node-integration-tests/suites/tracing/openai/instrument-with-pii.mjs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
import * as Sentry from '@sentry/node'; | ||
import { loggingTransport } from '@sentry-internal/node-integration-tests'; | ||
|
||
Sentry.init({ | ||
dsn: 'https://[email protected]/1337', | ||
release: '1.0', | ||
tracesSampleRate: 1.0, | ||
sendDefaultPii: true, | ||
transport: loggingTransport, | ||
integrations: [Sentry.openAIIntegration()], | ||
}); |
11 changes: 11 additions & 0 deletions
11
dev-packages/node-integration-tests/suites/tracing/openai/instrument.mjs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
import * as Sentry from '@sentry/node'; | ||
import { loggingTransport } from '@sentry-internal/node-integration-tests'; | ||
|
||
Sentry.init({ | ||
dsn: 'https://[email protected]/1337', | ||
release: '1.0', | ||
tracesSampleRate: 1.0, | ||
sendDefaultPii: false, | ||
transport: loggingTransport, | ||
integrations: [Sentry.openAIIntegration()], | ||
}); |
116 changes: 116 additions & 0 deletions
116
dev-packages/node-integration-tests/suites/tracing/openai/scenario.mjs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,116 @@ | ||
import { instrumentOpenAiClient } from '@sentry/core'; | ||
import * as Sentry from '@sentry/node'; | ||
|
||
class MockOpenAI { | ||
constructor(config) { | ||
this.apiKey = config.apiKey; | ||
|
||
this.chat = { | ||
completions: { | ||
create: async params => { | ||
// Simulate processing time | ||
await new Promise(resolve => setTimeout(resolve, 10)); | ||
|
||
if (params.model === 'error-model') { | ||
const error = new Error('Model not found'); | ||
error.status = 404; | ||
error.headers = { 'x-request-id': 'mock-request-123' }; | ||
throw error; | ||
} | ||
|
||
return { | ||
id: 'chatcmpl-mock123', | ||
object: 'chat.completion', | ||
created: 1677652288, | ||
model: params.model, | ||
system_fingerprint: 'fp_44709d6fcb', | ||
choices: [ | ||
{ | ||
index: 0, | ||
message: { | ||
role: 'assistant', | ||
content: 'Hello from OpenAI mock!', | ||
}, | ||
finish_reason: 'stop', | ||
}, | ||
], | ||
usage: { | ||
prompt_tokens: 10, | ||
completion_tokens: 15, | ||
total_tokens: 25, | ||
}, | ||
}; | ||
}, | ||
}, | ||
}; | ||
|
||
this.responses = { | ||
create: async params => { | ||
await new Promise(resolve => setTimeout(resolve, 10)); | ||
|
||
return { | ||
id: 'resp_mock456', | ||
object: 'response', | ||
created: 1677652290, | ||
model: params.model, | ||
input_text: params.input, | ||
output_text: `Response to: ${params.input}`, | ||
finish_reason: 'stop', | ||
usage: { | ||
input_tokens: 5, | ||
output_tokens: 8, | ||
total_tokens: 13, | ||
}, | ||
}; | ||
}, | ||
}; | ||
} | ||
} | ||
|
||
async function run() { | ||
await Sentry.startSpan({ op: 'function', name: 'main' }, async () => { | ||
const mockClient = new MockOpenAI({ | ||
apiKey: 'mock-api-key', | ||
}); | ||
|
||
const sentryClient = Sentry.getCurrentScope().getClient(); | ||
const sendDefaultPii = sentryClient?.getOptions().sendDefaultPii || false; | ||
|
||
const options = | ||
process.env.TEST_OPTIONS === '1' | ||
? { recordInputs: true, recordOutputs: true } | ||
: { recordInputs: sendDefaultPii, recordOutputs: sendDefaultPii }; | ||
|
||
const client = instrumentOpenAiClient(mockClient, options); | ||
|
||
// First test: basic chat completion | ||
await client.chat.completions.create({ | ||
model: 'gpt-3.5-turbo', | ||
messages: [ | ||
{ role: 'system', content: 'You are a helpful assistant.' }, | ||
{ role: 'user', content: 'What is the capital of France?' }, | ||
], | ||
temperature: 0.7, | ||
max_tokens: 100, | ||
}); | ||
|
||
// Second test: responses API | ||
await client.responses.create({ | ||
model: 'gpt-3.5-turbo', | ||
input: 'Translate this to French: Hello', | ||
instructions: 'You are a translator', | ||
}); | ||
|
||
// Third test: error handling | ||
try { | ||
await client.chat.completions.create({ | ||
model: 'error-model', | ||
messages: [{ role: 'user', content: 'This will fail' }], | ||
}); | ||
} catch (error) { | ||
RulaKhaled marked this conversation as resolved.
Show resolved
Hide resolved
|
||
// Error is expected and handled | ||
} | ||
}); | ||
} | ||
|
||
run(); |
183 changes: 183 additions & 0 deletions
183
dev-packages/node-integration-tests/suites/tracing/openai/test.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,183 @@ | ||
import { afterAll, describe, expect } from 'vitest'; | ||
import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runner'; | ||
|
||
describe('OpenAI integration', () => { | ||
afterAll(() => { | ||
cleanupChildProcesses(); | ||
}); | ||
|
||
const EXPECTED_TRANSACTION_DEFAULT_PII_FALSE = { | ||
transaction: 'main', | ||
spans: expect.arrayContaining([ | ||
// First span - basic chat completion without PII | ||
expect.objectContaining({ | ||
data: { | ||
'gen_ai.operation.name': 'chat', | ||
'sentry.op': 'gen_ai.chat', | ||
'sentry.origin': 'manual', | ||
'gen_ai.system': 'openai', | ||
'gen_ai.request.model': 'gpt-3.5-turbo', | ||
'gen_ai.request.temperature': 0.7, | ||
'gen_ai.response.model': 'gpt-3.5-turbo', | ||
'gen_ai.response.id': 'chatcmpl-mock123', | ||
'gen_ai.response.finish_reasons': '["stop"]', | ||
'gen_ai.usage.input_tokens': 10, | ||
'gen_ai.usage.output_tokens': 15, | ||
'gen_ai.usage.total_tokens': 25, | ||
'openai.response.id': 'chatcmpl-mock123', | ||
'openai.response.model': 'gpt-3.5-turbo', | ||
'openai.response.timestamp': '2023-03-01T06:31:28.000Z', | ||
'openai.usage.completion_tokens': 15, | ||
'openai.usage.prompt_tokens': 10, | ||
}, | ||
description: 'chat gpt-3.5-turbo', | ||
op: 'gen_ai.chat', | ||
origin: 'manual', | ||
status: 'ok', | ||
}), | ||
// Second span - responses API | ||
expect.objectContaining({ | ||
data: { | ||
'gen_ai.operation.name': 'chat', | ||
'sentry.op': 'gen_ai.chat', | ||
'sentry.origin': 'manual', | ||
'gen_ai.system': 'openai', | ||
'gen_ai.request.model': 'gpt-3.5-turbo', | ||
'gen_ai.response.model': 'gpt-3.5-turbo', | ||
'gen_ai.response.id': 'resp_mock456', | ||
'gen_ai.usage.input_tokens': 5, | ||
'gen_ai.usage.output_tokens': 8, | ||
'gen_ai.usage.total_tokens': 13, | ||
'openai.response.id': 'resp_mock456', | ||
'openai.response.model': 'gpt-3.5-turbo', | ||
'openai.usage.completion_tokens': 8, | ||
'openai.usage.prompt_tokens': 5, | ||
}, | ||
description: 'chat gpt-3.5-turbo', | ||
op: 'gen_ai.chat', | ||
origin: 'manual', | ||
status: 'ok', | ||
}), | ||
// Third span - error handling | ||
expect.objectContaining({ | ||
data: { | ||
'gen_ai.operation.name': 'chat', | ||
'sentry.op': 'gen_ai.chat', | ||
'sentry.origin': 'manual', | ||
'gen_ai.system': 'openai', | ||
'gen_ai.request.model': 'error-model', | ||
}, | ||
description: 'chat error-model', | ||
op: 'gen_ai.chat', | ||
origin: 'manual', | ||
status: 'unknown_error', | ||
}), | ||
]), | ||
}; | ||
|
||
const EXPECTED_TRANSACTION_DEFAULT_PII_TRUE = { | ||
transaction: 'main', | ||
spans: expect.arrayContaining([ | ||
// First span - basic chat completion with PII | ||
expect.objectContaining({ | ||
data: { | ||
'gen_ai.operation.name': 'chat', | ||
'sentry.op': 'gen_ai.chat', | ||
'sentry.origin': 'manual', | ||
'gen_ai.system': 'openai', | ||
'gen_ai.request.model': 'gpt-3.5-turbo', | ||
'gen_ai.request.temperature': 0.7, | ||
'gen_ai.request.messages': | ||
'[{"role":"system","content":"You are a helpful assistant."},{"role":"user","content":"What is the capital of France?"}]', | ||
'gen_ai.response.model': 'gpt-3.5-turbo', | ||
'gen_ai.response.id': 'chatcmpl-mock123', | ||
'gen_ai.response.finish_reasons': '["stop"]', | ||
'gen_ai.response.text': '["Hello from OpenAI mock!"]', | ||
'gen_ai.usage.input_tokens': 10, | ||
'gen_ai.usage.output_tokens': 15, | ||
'gen_ai.usage.total_tokens': 25, | ||
'openai.response.id': 'chatcmpl-mock123', | ||
'openai.response.model': 'gpt-3.5-turbo', | ||
'openai.response.timestamp': '2023-03-01T06:31:28.000Z', | ||
'openai.usage.completion_tokens': 15, | ||
'openai.usage.prompt_tokens': 10, | ||
}, | ||
description: 'chat gpt-3.5-turbo', | ||
op: 'gen_ai.chat', | ||
origin: 'manual', | ||
status: 'ok', | ||
}), | ||
// Second span - responses API with PII | ||
expect.objectContaining({ | ||
data: { | ||
'gen_ai.operation.name': 'chat', | ||
'sentry.op': 'gen_ai.chat', | ||
'sentry.origin': 'manual', | ||
'gen_ai.system': 'openai', | ||
'gen_ai.request.model': 'gpt-3.5-turbo', | ||
'gen_ai.request.messages': '"Translate this to French: Hello"', | ||
'gen_ai.response.text': 'Response to: Translate this to French: Hello', | ||
'gen_ai.response.model': 'gpt-3.5-turbo', | ||
'gen_ai.response.id': 'resp_mock456', | ||
'gen_ai.usage.input_tokens': 5, | ||
'gen_ai.usage.output_tokens': 8, | ||
'gen_ai.usage.total_tokens': 13, | ||
'openai.response.id': 'resp_mock456', | ||
'openai.response.model': 'gpt-3.5-turbo', | ||
'openai.usage.completion_tokens': 8, | ||
'openai.usage.prompt_tokens': 5, | ||
}, | ||
description: 'chat gpt-3.5-turbo', | ||
op: 'gen_ai.chat', | ||
origin: 'manual', | ||
status: 'ok', | ||
}), | ||
// Third span - error handling with PII | ||
expect.objectContaining({ | ||
data: { | ||
'gen_ai.operation.name': 'chat', | ||
'sentry.op': 'gen_ai.chat', | ||
'sentry.origin': 'manual', | ||
'gen_ai.system': 'openai', | ||
'gen_ai.request.model': 'error-model', | ||
'gen_ai.request.messages': '[{"role":"user","content":"This will fail"}]', | ||
}, | ||
description: 'chat error-model', | ||
op: 'gen_ai.chat', | ||
origin: 'manual', | ||
status: 'unknown_error', | ||
}), | ||
]), | ||
}; | ||
|
||
const EXPECTED_TRANSACTION_WITH_OPTIONS = { | ||
transaction: 'main', | ||
spans: expect.arrayContaining([ | ||
// Check that custom options are respected | ||
expect.objectContaining({ | ||
data: expect.objectContaining({ | ||
'gen_ai.request.messages': expect.any(String), // Should include messages when recordInputs: true | ||
'gen_ai.response.text': expect.any(String), // Should include response text when recordOutputs: true | ||
}), | ||
}), | ||
]), | ||
}; | ||
|
||
createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument.mjs', (createRunner, test) => { | ||
test('creates openai related spans with sendDefaultPii: false', async () => { | ||
await createRunner().expect({ transaction: EXPECTED_TRANSACTION_DEFAULT_PII_FALSE }).start().completed(); | ||
}); | ||
}); | ||
|
||
createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument-with-pii.mjs', (createRunner, test) => { | ||
test('creates openai related spans with sendDefaultPii: true', async () => { | ||
await createRunner().expect({ transaction: EXPECTED_TRANSACTION_DEFAULT_PII_TRUE }).start().completed(); | ||
}); | ||
}); | ||
|
||
createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument-with-options.mjs', (createRunner, test) => { | ||
test('creates openai related spans with custom options', async () => { | ||
await createRunner().expect({ transaction: EXPECTED_TRANSACTION_WITH_OPTIONS }).start().completed(); | ||
}); | ||
}); | ||
}); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.