-
-
Notifications
You must be signed in to change notification settings - Fork 1.7k
test(node): Test vercelai errors are properly linked to parent span #17130
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
import * as Sentry from '@sentry/node'; | ||
import { generateText, tool } from 'ai'; | ||
import { MockLanguageModelV1 } from 'ai/test'; | ||
import { z } from 'zod'; | ||
|
||
async function run() { | ||
// Create a manual outer span (simulating the root span) | ||
await Sentry.startSpan({ op: 'outer', name: 'outer span', description: 'outer span' }, async () => { | ||
// It is expected that the error will bubble up naturally to the outer span | ||
await generateText({ | ||
model: new MockLanguageModelV1({ | ||
doGenerate: async () => ({ | ||
rawCall: { rawPrompt: null, rawSettings: {} }, | ||
finishReason: 'stop', | ||
usage: { promptTokens: 10, completionTokens: 20 }, | ||
text: 'First span here!', | ||
toolCalls: [ | ||
{ | ||
toolCallType: 'function', | ||
toolCallId: 'call-1', | ||
toolName: 'calculateTool', | ||
args: '{ "a": 1, "b": 2 }', | ||
}, | ||
], | ||
}), | ||
}), | ||
experimental_telemetry: { | ||
functionId: 'Simple Agent', | ||
recordInputs: true, | ||
recordOutputs: true, | ||
isEnabled: true, | ||
}, | ||
tools: { | ||
calculateTool: tool({ | ||
description: 'Calculate the result of a math problem. Returns a number.', | ||
parameters: z.object({ | ||
a: z.number().describe('First number'), | ||
b: z.number().describe('Second number'), | ||
}), | ||
type: 'function', | ||
execute: async () => { | ||
throw new Error('Not implemented'); | ||
}, | ||
}), | ||
}, | ||
maxSteps: 2, | ||
system: 'You help users with their math problems.', | ||
prompt: 'What is 1 + 1?', | ||
}); | ||
}); | ||
} | ||
|
||
run(); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
import * as Sentry from '@sentry/node'; | ||
import { generateText, tool } from 'ai'; | ||
import { MockLanguageModelV1 } from 'ai/test'; | ||
import express from 'express'; | ||
import { createServer } from 'http'; | ||
import { z } from 'zod'; | ||
|
||
async function run() { | ||
const app = express(); | ||
|
||
app.get('/api/chat', async (req, res) => { | ||
try { | ||
await generateText({ | ||
model: new MockLanguageModelV1({ | ||
doGenerate: async () => ({ | ||
rawCall: { rawPrompt: null, rawSettings: {} }, | ||
finishReason: 'stop', | ||
usage: { promptTokens: 10, completionTokens: 20 }, | ||
text: 'Processing your request...', | ||
toolCalls: [ | ||
{ | ||
toolCallType: 'function', | ||
toolCallId: 'call-1', | ||
toolName: 'calculateTool', | ||
args: '{ "a": 1, "b": 2 }', | ||
}, | ||
], | ||
}), | ||
}), | ||
experimental_telemetry: { | ||
functionId: 'Chat Assistant', | ||
recordInputs: true, | ||
recordOutputs: true, | ||
isEnabled: true, | ||
}, | ||
tools: { | ||
calculateTool: tool({ | ||
description: 'Calculate the result of a math problem. Returns a number.', | ||
parameters: z.object({ | ||
a: z.number().describe('First number'), | ||
b: z.number().describe('Second number'), | ||
}), | ||
type: 'function', | ||
execute: async () => { | ||
throw new Error('Calculation service unavailable'); | ||
}, | ||
}), | ||
}, | ||
maxSteps: 2, | ||
system: 'You are a helpful chat assistant.', | ||
prompt: 'What is 1 + 1?', | ||
}); | ||
|
||
res.json({ success: true }); | ||
} catch (error) { | ||
res.status(500).json({ error: error.message }); | ||
} | ||
}); | ||
|
||
Sentry.setupExpressErrorHandler(app); | ||
|
||
const server = createServer(app); | ||
|
||
// Start server and make request | ||
server.listen(0, () => { | ||
const port = server.address()?.port; | ||
// eslint-disable-next-line no-console | ||
console.log(JSON.stringify({ port })); | ||
|
||
// Make the request that will trigger the error | ||
fetch(`http://localhost:${port}/api/chat`) | ||
.then(() => { | ||
server.close(); | ||
}) | ||
.catch(() => { | ||
server.close(); | ||
}); | ||
}); | ||
} | ||
|
||
run(); |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -416,4 +416,94 @@ | |
await createRunner().expect({ transaction: EXPECTED_TRANSACTION_DEFAULT_PII_TRUE }).start().completed(); | ||
}); | ||
}); | ||
|
||
createEsmAndCjsTests(__dirname, 'scenario-error.mjs', 'instrument-with-pii.mjs', (createRunner, test) => { | ||
test('Vercel AI errors should inherit parent trace context from manually created outer span', async () => { | ||
let capturedTransaction: any; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. l: let's avoid |
||
let capturedEvent: any; | ||
|
||
const runner = createRunner() | ||
.expect({ | ||
transaction: (transaction: any) => { | ||
capturedTransaction = transaction; | ||
expect(transaction.transaction).toBe('outer span'); | ||
}, | ||
}) | ||
.expect({ | ||
event: (event: any) => { | ||
capturedEvent = event; | ||
|
||
expect(event).toMatchObject({ | ||
exception: { | ||
values: expect.arrayContaining([ | ||
expect.objectContaining({ | ||
type: 'AI_ToolExecutionError', | ||
value: 'Error executing tool calculateTool: Not implemented', | ||
}), | ||
]), | ||
}, | ||
}); | ||
}, | ||
}) | ||
.start(); | ||
|
||
await runner.completed(); | ||
|
||
const transactionTraceId = capturedTransaction?.contexts?.trace?.trace_id; | ||
const errorTraceId = capturedEvent?.contexts?.trace?.trace_id; | ||
|
||
expect(transactionTraceId).toBeDefined(); | ||
expect(errorTraceId).toBeDefined(); | ||
expect(transactionTraceId).toMatch(/^[a-f0-9]{32}$/); | ||
expect(errorTraceId).toMatch(/^[a-f0-9]{32}$/); | ||
|
||
expect(errorTraceId).toBe(transactionTraceId); | ||
Check failure on line 460 in dev-packages/node-integration-tests/suites/tracing/vercelai/test.ts
|
||
}); | ||
}); | ||
|
||
createEsmAndCjsTests(__dirname, 'scenario-express-error.mjs', 'instrument-with-pii.mjs', (createRunner, test) => { | ||
test('Vercel AI errors should inherit parent trace context from server HTTP request', async () => { | ||
Check failure on line 465 in dev-packages/node-integration-tests/suites/tracing/vercelai/test.ts
|
||
let capturedTransaction: any; | ||
let capturedEvent: any; | ||
|
||
const runner = createRunner() | ||
.withMockSentryServer() | ||
.expect({ | ||
transaction: (transaction: any) => { | ||
capturedTransaction = transaction; | ||
// Express creates a transaction like "GET /api/chat" | ||
expect(transaction.transaction).toBe('GET /api/chat'); | ||
}, | ||
}) | ||
.expect({ | ||
event: (event: any) => { | ||
capturedEvent = event; | ||
|
||
expect(event).toMatchObject({ | ||
exception: { | ||
values: expect.arrayContaining([ | ||
expect.objectContaining({ | ||
type: 'AI_ToolExecutionError', | ||
value: 'Error executing tool calculateTool: Calculation service unavailable', | ||
}), | ||
]), | ||
}, | ||
}); | ||
}, | ||
}) | ||
.start(); | ||
|
||
await runner.completed(); | ||
|
||
const transactionTraceId = capturedTransaction?.contexts?.trace?.trace_id; | ||
const errorTraceId = capturedEvent?.contexts?.trace?.trace_id; | ||
|
||
expect(transactionTraceId).toBeDefined(); | ||
expect(errorTraceId).toBeDefined(); | ||
expect(transactionTraceId).toMatch(/^[a-f0-9]{32}$/); | ||
expect(errorTraceId).toMatch(/^[a-f0-9]{32}$/); | ||
|
||
expect(errorTraceId).toBe(transactionTraceId); | ||
}); | ||
}); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
description is an invalid field that does not exist anymore!