Skip to content

fix(node): Ensure tool errors for vercelAiIntegration have correct trace connected #17132

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 8 commits into from
Jul 24, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
{
"name": "node-express-app",
"name": "aws-lambda-layer-cjs",
"version": "1.0.0",
"private": true,
"type": "commonjs",
"scripts": {
"start": "node src/run.js",
"test": "playwright test",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ Sentry.init({
dsn: 'https://[email protected]/1337',
release: '1.0',
transport: loggingTransport,
tracesSampleRate: 1,
tracesSampleRate: 0,
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this was actually not testing what the test said it would ^^

});

import express from 'express';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ afterAll(() => {

test('should capture and send Express controller error with txn name if tracesSampleRate is 0', async () => {
const runner = createRunner(__dirname, 'server.ts')
.ignore('transaction')
.expect({
event: {
exception: {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import * as Sentry from '@sentry/node';
import { loggingTransport, startExpressServerAndSendPortToRunner } from '@sentry-internal/node-integration-tests';

Sentry.init({
dsn: 'https://[email protected]/1337',
release: '1.0',
tracesSampleRate: 1,
transport: loggingTransport,
});

import express from 'express';

const app = express();

app.get('/test/express/:id', req => {
throw new Error(`test_error with id ${req.params.id}`);
});

Sentry.setupExpressErrorHandler(app);

startExpressServerAndSendPortToRunner(app);
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { afterAll, expect, test } from 'vitest';
import { cleanupChildProcesses, createRunner } from '../../../utils/runner';

afterAll(() => {
cleanupChildProcesses();
});

test('should capture and send Express controller error with txn name if tracesSampleRate is 1', async () => {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

noticed this test was missing, just adding for completeness sake.

const runner = createRunner(__dirname, 'server.ts')
.expect({
transaction: {
transaction: 'GET /test/express/:id',
},
})
.expect({
event: {
exception: {
values: [
{
mechanism: {
type: 'middleware',
handled: false,
},
type: 'Error',
value: 'test_error with id 123',
stacktrace: {
frames: expect.arrayContaining([
expect.objectContaining({
function: expect.any(String),
lineno: expect.any(Number),
colno: expect.any(Number),
}),
]),
},
},
],
},
transaction: 'GET /test/express/:id',
},
})
.start();
runner.makeRequest('get', '/test/express/123', { expectError: true });
await runner.completed();
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
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,
transport: loggingTransport,
});

// eslint-disable-next-line @typescript-eslint/no-floating-promises
recordSpan(async () => {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
doSomething();
// eslint-disable-next-line @typescript-eslint/no-floating-promises
doSomethingWithError();
});

async function doSomething(): Promise<void> {
return Promise.resolve();
}

async function doSomethingWithError(): Promise<void> {
await new Promise(resolve => setTimeout(resolve, 100));
throw new Error('test error');
}

// Duplicating some code from vercel-ai to verify how things work in more complex/weird scenarios
function recordSpan(fn: (span: unknown) => Promise<void>): Promise<void> {
return Sentry.startSpanManual({ name: 'test-span' }, async span => {
try {
const result = await fn(span);
span.end();
return result;
} catch (error) {
try {
span.setStatus({ code: 2 });
} finally {
// always stop the span when there is an error:
span.end();
}

throw error;
}
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
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,
transport: loggingTransport,
});

// eslint-disable-next-line @typescript-eslint/no-floating-promises
Sentry.startSpan({ name: 'test-span' }, async () => {
throw new Error('test error');
});
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { Event } from '@sentry/node';
import * as childProcess from 'child_process';
import * as path from 'path';
import { afterAll, describe, expect, test } from 'vitest';
Expand Down Expand Up @@ -123,4 +124,58 @@ test rejection`);
.start()
.completed();
});

test('handles unhandled rejection in spans', async () => {
let transactionEvent: Event | undefined;
let errorEvent: Event | undefined;

await createRunner(__dirname, 'scenario-with-span.ts')
.expect({
transaction: transaction => {
transactionEvent = transaction;
},
})
.expect({
event: event => {
errorEvent = event;
},
})
.start()
.completed();

expect(transactionEvent).toBeDefined();
expect(errorEvent).toBeDefined();

expect(transactionEvent!.transaction).toBe('test-span');

expect(transactionEvent!.contexts!.trace!.trace_id).toBe(errorEvent!.contexts!.trace!.trace_id);
expect(transactionEvent!.contexts!.trace!.span_id).toBe(errorEvent!.contexts!.trace!.span_id);
});

test('handles unhandled rejection in spans that are ended early', async () => {
let transactionEvent: Event | undefined;
let errorEvent: Event | undefined;

await createRunner(__dirname, 'scenario-with-span-ended.ts')
.expect({
transaction: transaction => {
transactionEvent = transaction;
},
})
.expect({
event: event => {
errorEvent = event;
},
})
.start()
.completed();

expect(transactionEvent).toBeDefined();
expect(errorEvent).toBeDefined();

expect(transactionEvent!.transaction).toBe('test-span');

expect(transactionEvent!.contexts!.trace!.trace_id).toBe(errorEvent!.contexts!.trace!.trace_id);
expect(transactionEvent!.contexts!.trace!.span_id).toBe(errorEvent!.contexts!.trace!.span_id);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@ Sentry.init({
tracesSampleRate: 1.0,
sendDefaultPii: true,
transport: loggingTransport,
integrations: [Sentry.vercelAIIntegration({ force: true })],
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

leftover that should not be needed anymore

integrations: [Sentry.vercelAIIntegration()],
});
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@ Sentry.init({
release: '1.0',
tracesSampleRate: 1.0,
transport: loggingTransport,
integrations: [Sentry.vercelAIIntegration({ force: true })],
integrations: [Sentry.vercelAIIntegration()],
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import * as Sentry from '@sentry/node';
import { startExpressServerAndSendPortToRunner } from '@sentry-internal/node-integration-tests';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:O

import { generateText } from 'ai';
import { MockLanguageModelV1 } from 'ai/test';
import express from 'express';
import { z } from 'zod';

const app = express();

app.get('/test/error-in-tool', async (_req, res, next) => {
Sentry.setTag('test-tag', 'test-value');

try {
await generateText({
model: new MockLanguageModelV1({
doGenerate: async () => ({
rawCall: { rawPrompt: null, rawSettings: {} },
finishReason: 'tool-calls',
usage: { promptTokens: 15, completionTokens: 25 },
text: 'Tool call completed!',
toolCalls: [
{
toolCallType: 'function',
toolCallId: 'call-1',
toolName: 'getWeather',
args: '{ "location": "San Francisco" }',
},
],
}),
}),
tools: {
getWeather: {
parameters: z.object({ location: z.string() }),
execute: async () => {
throw new Error('Error in tool');
},
},
},
prompt: 'What is the weather in San Francisco?',
});
} catch (error) {
next(error);
return;
}

res.send({ message: 'OK' });
});
Sentry.setupExpressErrorHandler(app);

startExpressServerAndSendPortToRunner(app);
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import * as Sentry from '@sentry/node';
import { generateText } from 'ai';
import { MockLanguageModelV1 } from 'ai/test';
import { z } from 'zod';

async function run() {
Sentry.setTag('test-tag', 'test-value');

await Sentry.startSpan({ op: 'function', name: 'main' }, async () => {
await generateText({
model: new MockLanguageModelV1({
doGenerate: async () => ({
rawCall: { rawPrompt: null, rawSettings: {} },
finishReason: 'tool-calls',
usage: { promptTokens: 15, completionTokens: 25 },
text: 'Tool call completed!',
toolCalls: [
{
toolCallType: 'function',
toolCallId: 'call-1',
toolName: 'getWeather',
args: '{ "location": "San Francisco" }',
},
],
}),
}),
tools: {
getWeather: {
parameters: z.object({ location: z.string() }),
execute: async () => {
throw new Error('Error in tool');
},
},
},
prompt: 'What is the weather in San Francisco?',
});
});
}

run();
Loading
Loading