Skip to content

Commit fa0de7f

Browse files
committed
feat(node): Support Express v5
1 parent cea9484 commit fa0de7f

File tree

66 files changed

+3156
-33
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

66 files changed

+3156
-33
lines changed

dev-packages/node-integration-tests/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,11 @@
1818
"clean:script": "node scripts/clean.js",
1919
"prisma-v5:init": "cd suites/tracing/prisma-orm-v5 && yarn && yarn setup",
2020
"prisma-v6:init": "cd suites/tracing/prisma-orm-v6 && yarn && yarn setup",
21+
"express-v5-install": "cd suites/express-v5 && yarn --no-lockfile",
2122
"lint": "eslint . --format stylish",
2223
"fix": "eslint . --format stylish --fix",
2324
"type-check": "tsc",
24-
"pretest": "run-s --silent prisma-v5:init prisma-v6:init",
25+
"pretest": "run-s --silent prisma-v5:init prisma-v6:init express-v5-install",
2526
"test": "jest --config ./jest.config.js",
2627
"test:watch": "yarn test --watch"
2728
},
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { loggingTransport } from '@sentry-internal/node-integration-tests';
2+
import * as Sentry from '@sentry/node';
3+
4+
Sentry.init({
5+
dsn: 'https://[email protected]/1337',
6+
release: '1.0',
7+
transport: loggingTransport,
8+
});
9+
10+
import { startExpressServerAndSendPortToRunner } from '@sentry-internal/node-integration-tests';
11+
import express from 'express';
12+
13+
const app = express();
14+
15+
Sentry.setTag('global', 'tag');
16+
17+
app.get('/test/withScope', () => {
18+
Sentry.withScope(scope => {
19+
scope.setTag('local', 'tag');
20+
throw new Error('test_error');
21+
});
22+
});
23+
24+
app.get('/test/isolationScope', () => {
25+
Sentry.getIsolationScope().setTag('isolation-scope', 'tag');
26+
throw new Error('isolation_test_error');
27+
});
28+
29+
Sentry.setupExpressErrorHandler(app);
30+
31+
startExpressServerAndSendPortToRunner(app);
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import { cleanupChildProcesses, createRunner } from '../../../utils/runner';
2+
3+
afterAll(() => {
4+
cleanupChildProcesses();
5+
});
6+
7+
/**
8+
* Why does this test exist?
9+
*
10+
* We recently discovered that errors caught by global handlers will potentially loose scope data from the active scope
11+
* where the error was originally thrown in. The simple example in this test (see subject.ts) demonstrates this behavior
12+
* (in a Node environment but the same behavior applies to the browser; see the test there).
13+
*
14+
* This test nevertheless covers the behavior so that we're aware.
15+
*/
16+
test('withScope scope is NOT applied to thrown error caught by global handler', done => {
17+
createRunner(__dirname, 'server.ts')
18+
.expect({
19+
event: {
20+
exception: {
21+
values: [
22+
{
23+
mechanism: {
24+
type: 'middleware',
25+
handled: false,
26+
},
27+
type: 'Error',
28+
value: 'test_error',
29+
stacktrace: {
30+
frames: expect.arrayContaining([
31+
expect.objectContaining({
32+
function: expect.any(String),
33+
lineno: expect.any(Number),
34+
colno: expect.any(Number),
35+
}),
36+
]),
37+
},
38+
},
39+
],
40+
},
41+
// 'local' tag is not applied to the event
42+
tags: expect.not.objectContaining({ local: expect.anything() }),
43+
},
44+
})
45+
.start(done)
46+
.makeRequest('get', '/test/withScope', { expectError: true });
47+
});
48+
49+
/**
50+
* This test shows that the isolation scope set tags are applied correctly to the error.
51+
*/
52+
test('isolation scope is applied to thrown error caught by global handler', done => {
53+
createRunner(__dirname, 'server.ts')
54+
.expect({
55+
event: {
56+
exception: {
57+
values: [
58+
{
59+
mechanism: {
60+
type: 'middleware',
61+
handled: false,
62+
},
63+
type: 'Error',
64+
value: 'isolation_test_error',
65+
stacktrace: {
66+
frames: expect.arrayContaining([
67+
expect.objectContaining({
68+
function: expect.any(String),
69+
lineno: expect.any(Number),
70+
colno: expect.any(Number),
71+
}),
72+
]),
73+
},
74+
},
75+
],
76+
},
77+
tags: {
78+
global: 'tag',
79+
'isolation-scope': 'tag',
80+
},
81+
},
82+
})
83+
.start(done)
84+
.makeRequest('get', '/test/isolationScope', { expectError: true });
85+
});
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { loggingTransport } from '@sentry-internal/node-integration-tests';
2+
import * as Sentry from '@sentry/node';
3+
4+
Sentry.init({
5+
dsn: 'https://[email protected]/1337',
6+
release: '1.0',
7+
transport: loggingTransport,
8+
tracesSampleRate: 1,
9+
});
10+
11+
import { startExpressServerAndSendPortToRunner } from '@sentry-internal/node-integration-tests';
12+
import express from 'express';
13+
14+
const app = express();
15+
16+
app.get('/test/express/:id', req => {
17+
throw new Error(`test_error with id ${req.params.id}`);
18+
});
19+
20+
Sentry.setupExpressErrorHandler(app);
21+
22+
startExpressServerAndSendPortToRunner(app);
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { cleanupChildProcesses, createRunner } from '../../../utils/runner';
2+
3+
afterAll(() => {
4+
cleanupChildProcesses();
5+
});
6+
7+
test('should capture and send Express controller error with txn name if tracesSampleRate is 0', done => {
8+
createRunner(__dirname, 'server.ts')
9+
.ignore('transaction')
10+
.expect({
11+
event: {
12+
exception: {
13+
values: [
14+
{
15+
mechanism: {
16+
type: 'middleware',
17+
handled: false,
18+
},
19+
type: 'Error',
20+
value: 'test_error with id 123',
21+
stacktrace: {
22+
frames: expect.arrayContaining([
23+
expect.objectContaining({
24+
function: expect.any(String),
25+
lineno: expect.any(Number),
26+
colno: expect.any(Number),
27+
}),
28+
]),
29+
},
30+
},
31+
],
32+
},
33+
transaction: 'GET /test/express/:id',
34+
},
35+
})
36+
.start(done)
37+
.makeRequest('get', '/test/express/123', { expectError: true });
38+
});
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { loggingTransport } from '@sentry-internal/node-integration-tests';
2+
import * as Sentry from '@sentry/node';
3+
4+
Sentry.init({
5+
dsn: 'https://[email protected]/1337',
6+
release: '1.0',
7+
transport: loggingTransport,
8+
});
9+
10+
import { startExpressServerAndSendPortToRunner } from '@sentry-internal/node-integration-tests';
11+
import express from 'express';
12+
13+
const app = express();
14+
15+
app.get('/test/express/:id', req => {
16+
throw new Error(`test_error with id ${req.params.id}`);
17+
});
18+
19+
Sentry.setupExpressErrorHandler(app);
20+
21+
startExpressServerAndSendPortToRunner(app);
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { cleanupChildProcesses, createRunner } from '../../../utils/runner';
2+
3+
afterAll(() => {
4+
cleanupChildProcesses();
5+
});
6+
7+
test('should capture and send Express controller error if tracesSampleRate is not set.', done => {
8+
createRunner(__dirname, 'server.ts')
9+
.ignore('transaction')
10+
.expect({
11+
event: {
12+
exception: {
13+
values: [
14+
{
15+
mechanism: {
16+
type: 'middleware',
17+
handled: false,
18+
},
19+
type: 'Error',
20+
value: 'test_error with id 123',
21+
stacktrace: {
22+
frames: expect.arrayContaining([
23+
expect.objectContaining({
24+
function: expect.any(String),
25+
lineno: expect.any(Number),
26+
colno: expect.any(Number),
27+
}),
28+
]),
29+
},
30+
},
31+
],
32+
},
33+
},
34+
})
35+
.start(done)
36+
.makeRequest('get', '/test/express/123', { expectError: true });
37+
});
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { loggingTransport } from '@sentry-internal/node-integration-tests';
2+
import * as Sentry from '@sentry/node';
3+
4+
Sentry.init({
5+
// No dsn, means client is disabled
6+
// dsn: 'https://[email protected]/1337',
7+
release: '1.0',
8+
transport: loggingTransport,
9+
});
10+
11+
// We add http integration to ensure request isolation etc. works
12+
const initialClient = Sentry.getClient();
13+
initialClient?.addIntegration(Sentry.httpIntegration());
14+
15+
// Store this so we can update the client later
16+
const initialCurrentScope = Sentry.getCurrentScope();
17+
18+
import { startExpressServerAndSendPortToRunner } from '@sentry-internal/node-integration-tests';
19+
import express from 'express';
20+
21+
const app = express();
22+
23+
Sentry.setTag('global', 'tag');
24+
25+
app.get('/test/no-init', (_req, res) => {
26+
Sentry.addBreadcrumb({ message: 'no init breadcrumb' });
27+
Sentry.setTag('no-init', 'tag');
28+
29+
res.send({});
30+
});
31+
32+
app.get('/test/init', (_req, res) => {
33+
// Call init again, but with DSN
34+
Sentry.init({
35+
dsn: 'https://[email protected]/1337',
36+
release: '1.0',
37+
transport: loggingTransport,
38+
});
39+
// Set this on initial scope, to ensure it can be inherited
40+
initialCurrentScope.setClient(Sentry.getClient()!);
41+
42+
Sentry.addBreadcrumb({ message: 'init breadcrumb' });
43+
Sentry.setTag('init', 'tag');
44+
45+
res.send({});
46+
});
47+
48+
app.get('/test/error/:id', (req, res) => {
49+
const id = req.params.id;
50+
Sentry.addBreadcrumb({ message: `error breadcrumb ${id}` });
51+
Sentry.setTag('error', id);
52+
53+
Sentry.captureException(new Error(`This is an exception ${id}`));
54+
55+
setTimeout(() => {
56+
// We flush to ensure we are sending exceptions in a certain order
57+
Sentry.flush(1000).then(
58+
() => {
59+
// We send this so we can wait for this, to know the test is ended & server can be closed
60+
if (id === '3') {
61+
Sentry.captureException(new Error('Final exception was captured'));
62+
}
63+
res.send({});
64+
},
65+
() => {
66+
res.send({});
67+
},
68+
);
69+
}, 1);
70+
});
71+
72+
Sentry.setupExpressErrorHandler(app);
73+
74+
startExpressServerAndSendPortToRunner(app);
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { cleanupChildProcesses, createRunner } from '../../../utils/runner';
2+
3+
afterAll(() => {
4+
cleanupChildProcesses();
5+
});
6+
7+
test('allows to call init multiple times', done => {
8+
const runner = createRunner(__dirname, 'server.ts')
9+
.expect({
10+
event: {
11+
exception: {
12+
values: [
13+
{
14+
value: 'This is an exception 2',
15+
},
16+
],
17+
},
18+
breadcrumbs: [
19+
{
20+
message: 'error breadcrumb 2',
21+
timestamp: expect.any(Number),
22+
},
23+
],
24+
tags: {
25+
global: 'tag',
26+
error: '2',
27+
},
28+
},
29+
})
30+
.expect({
31+
event: {
32+
exception: {
33+
values: [
34+
{
35+
value: 'This is an exception 3',
36+
},
37+
],
38+
},
39+
breadcrumbs: [
40+
{
41+
message: 'error breadcrumb 3',
42+
timestamp: expect.any(Number),
43+
},
44+
],
45+
tags: {
46+
global: 'tag',
47+
error: '3',
48+
},
49+
},
50+
})
51+
.expect({
52+
event: {
53+
exception: {
54+
values: [
55+
{
56+
value: 'Final exception was captured',
57+
},
58+
],
59+
},
60+
},
61+
})
62+
.start(done);
63+
64+
runner
65+
.makeRequest('get', '/test/no-init')
66+
.then(() => runner.makeRequest('get', '/test/error/1'))
67+
.then(() => runner.makeRequest('get', '/test/init'))
68+
.then(() => runner.makeRequest('get', '/test/error/2'))
69+
.then(() => runner.makeRequest('get', '/test/error/3'));
70+
});

0 commit comments

Comments
 (0)