Skip to content

Commit 129d794

Browse files
committed
fix(core): Intercept .withResponse() to preserve OpenAI stream instrumentation
1 parent 87001a9 commit 129d794

File tree

3 files changed

+325
-51
lines changed

3 files changed

+325
-51
lines changed
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
import * as Sentry from '@sentry/node';
2+
import express from 'express';
3+
import OpenAI from 'openai';
4+
5+
function startMockServer() {
6+
const app = express();
7+
app.use(express.json());
8+
9+
app.post('/openai/chat/completions', (req, res) => {
10+
const { model } = req.body;
11+
12+
res.set({
13+
'x-request-id': 'req_withresponse_test',
14+
'openai-organization': 'test-org',
15+
'openai-processing-ms': '150',
16+
'openai-version': '2020-10-01',
17+
});
18+
19+
res.send({
20+
id: 'chatcmpl-withresponse',
21+
object: 'chat.completion',
22+
created: 1677652288,
23+
model: model,
24+
choices: [
25+
{
26+
index: 0,
27+
message: {
28+
role: 'assistant',
29+
content: 'Testing .withResponse() method!',
30+
},
31+
finish_reason: 'stop',
32+
},
33+
],
34+
usage: {
35+
prompt_tokens: 8,
36+
completion_tokens: 12,
37+
total_tokens: 20,
38+
},
39+
});
40+
});
41+
42+
return new Promise(resolve => {
43+
const server = app.listen(0, () => {
44+
resolve(server);
45+
});
46+
});
47+
}
48+
49+
async function run() {
50+
const server = await startMockServer();
51+
52+
await Sentry.startSpan({ op: 'function', name: 'main' }, async () => {
53+
const client = new OpenAI({
54+
baseURL: `http://localhost:${server.address().port}/openai`,
55+
apiKey: 'mock-api-key',
56+
});
57+
58+
// Verify .withResponse() method exists and can be called
59+
const result = client.chat.completions.create({
60+
model: 'gpt-4',
61+
messages: [{ role: 'user', content: 'Test withResponse' }],
62+
});
63+
64+
// Verify method exists
65+
if (typeof result.withResponse !== 'function') {
66+
throw new Error('.withResponse() method does not exist');
67+
}
68+
69+
// Call .withResponse() and verify structure
70+
const withResponseResult = await result.withResponse();
71+
72+
// Verify all three properties exist
73+
if (!withResponseResult.data) {
74+
throw new Error('.withResponse() did not return data');
75+
}
76+
if (!withResponseResult.response) {
77+
throw new Error('.withResponse() did not return response');
78+
}
79+
if (withResponseResult.request_id === undefined) {
80+
throw new Error('.withResponse() did not return request_id');
81+
}
82+
83+
// Verify data structure matches expected OpenAI response
84+
const { data } = withResponseResult;
85+
if (data.id !== 'chatcmpl-withresponse') {
86+
throw new Error(`Expected data.id to be 'chatcmpl-withresponse', got '${data.id}'`);
87+
}
88+
if (data.choices[0].message.content !== 'Testing .withResponse() method!') {
89+
throw new Error(`Expected specific content, got '${data.choices[0].message.content}'`);
90+
}
91+
if (data.usage.total_tokens !== 20) {
92+
throw new Error(`Expected 20 total tokens, got ${data.usage.total_tokens}`);
93+
}
94+
95+
// Verify response is a Response object with correct headers
96+
if (!(withResponseResult.response instanceof Response)) {
97+
throw new Error('response is not a Response object');
98+
}
99+
if (withResponseResult.response.headers.get('x-request-id') !== 'req_withresponse_test') {
100+
throw new Error(
101+
`Expected x-request-id header 'req_withresponse_test', got '${withResponseResult.response.headers.get('x-request-id')}'`,
102+
);
103+
}
104+
105+
// Verify request_id matches the header
106+
if (withResponseResult.request_id !== 'req_withresponse_test') {
107+
throw new Error(`Expected request_id 'req_withresponse_test', got '${withResponseResult.request_id}'`);
108+
}
109+
110+
// Test 2: Verify .asResponse() method works
111+
const result2 = client.chat.completions.create({
112+
model: 'gpt-4',
113+
messages: [{ role: 'user', content: 'Test asResponse' }],
114+
});
115+
116+
// Verify method exists
117+
if (typeof result2.asResponse !== 'function') {
118+
throw new Error('.asResponse() method does not exist');
119+
}
120+
121+
// Call .asResponse() and verify it returns raw Response
122+
const rawResponse = await result2.asResponse();
123+
124+
if (!(rawResponse instanceof Response)) {
125+
throw new Error('.asResponse() did not return a Response object');
126+
}
127+
128+
// Verify response has correct status
129+
if (rawResponse.status !== 200) {
130+
throw new Error(`Expected status 200, got ${rawResponse.status}`);
131+
}
132+
133+
// Verify response headers
134+
if (rawResponse.headers.get('x-request-id') !== 'req_withresponse_test') {
135+
throw new Error(
136+
`Expected x-request-id header 'req_withresponse_test', got '${rawResponse.headers.get('x-request-id')}'`,
137+
);
138+
}
139+
140+
// Verify we can manually parse the body
141+
const body = await rawResponse.json();
142+
if (body.id !== 'chatcmpl-withresponse') {
143+
throw new Error(`Expected body.id 'chatcmpl-withresponse', got '${body.id}'`);
144+
}
145+
if (body.choices[0].message.content !== 'Testing .withResponse() method!') {
146+
throw new Error(`Expected specific content in body, got '${body.choices[0].message.content}'`);
147+
}
148+
});
149+
150+
server.close();
151+
}
152+
153+
run();

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

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -945,4 +945,42 @@ describe('OpenAI integration', () => {
945945
});
946946
},
947947
);
948+
949+
createEsmAndCjsTests(__dirname, 'scenario-with-response.mjs', 'instrument.mjs', (createRunner, test) => {
950+
test('preserves .withResponse() method and works correctly', async () => {
951+
await createRunner()
952+
.ignore('event')
953+
.expect({
954+
transaction: {
955+
transaction: 'main',
956+
spans: expect.arrayContaining([
957+
// First call using .withResponse()
958+
expect.objectContaining({
959+
data: expect.objectContaining({
960+
[GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'chat',
961+
[GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'gpt-4',
962+
[GEN_AI_RESPONSE_ID_ATTRIBUTE]: 'chatcmpl-withresponse',
963+
}),
964+
description: 'chat gpt-4',
965+
op: 'gen_ai.chat',
966+
status: 'ok',
967+
}),
968+
// Second call using .asResponse()
969+
expect.objectContaining({
970+
data: expect.objectContaining({
971+
[GEN_AI_OPERATION_NAME_ATTRIBUTE]: 'chat',
972+
[GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'gpt-4',
973+
[GEN_AI_RESPONSE_ID_ATTRIBUTE]: 'chatcmpl-withresponse',
974+
}),
975+
description: 'chat gpt-4',
976+
op: 'gen_ai.chat',
977+
status: 'ok',
978+
}),
979+
]),
980+
},
981+
})
982+
.start()
983+
.completed();
984+
});
985+
});
948986
});

0 commit comments

Comments
 (0)