Skip to content

Commit 1aa5bbe

Browse files
authored
feat(node): Support Express v5 (#15380)
The goal is for Express to eventually support publishing events to `diagnostics_channel` (#15107) so that Open Telemetry can instrument it without monkey-patching internal code. However, this might take a while and it would be great to support Express v5 now. This PR is a stop-gap solution until that work is complete and published. This PR vendors the code added in my otel PR: - open-telemetry/opentelemetry-js-contrib#2437 - Adds a new instrumentation specifically for hooking express v5 - Copies the Express v4 integration tests to test v5 - The only changes in the tests is the removal of a couple of complex regex tests where the regexes are no longer supported by Express. - Modifies the NestJs v11 tests which now support full Express spans
1 parent d804dd4 commit 1aa5bbe

File tree

68 files changed

+3166
-49
lines changed

Some content is hidden

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

68 files changed

+3166
-49
lines changed

dev-packages/e2e-tests/test-applications/nestjs-11/tests/errors.test.ts

+4-8
Original file line numberDiff line numberDiff line change
@@ -50,13 +50,11 @@ test('Does not send HttpExceptions to Sentry', async ({ baseURL }) => {
5050
});
5151

5252
const transactionEventPromise400 = waitForTransaction('nestjs-11', transactionEvent => {
53-
// todo(express-5): parametrize /test-expected-400-exception/:id
54-
return transactionEvent?.transaction === 'GET /test-expected-400-exception/123';
53+
return transactionEvent?.transaction === 'GET /test-expected-400-exception/:id';
5554
});
5655

5756
const transactionEventPromise500 = waitForTransaction('nestjs-11', transactionEvent => {
58-
// todo(express-5): parametrize /test-expected-500-exception/:id
59-
return transactionEvent?.transaction === 'GET /test-expected-500-exception/123';
57+
return transactionEvent?.transaction === 'GET /test-expected-500-exception/:id';
6058
});
6159

6260
const response400 = await fetch(`${baseURL}/test-expected-400-exception/123`);
@@ -81,13 +79,11 @@ test('Does not send RpcExceptions to Sentry', async ({ baseURL }) => {
8179
errorEventOccurred = true;
8280
}
8381

84-
// todo(express-5): parametrize /test-expected-rpc-exception/:id
85-
return event?.transaction === 'GET /test-expected-rpc-exception/123';
82+
return event?.transaction === 'GET /test-expected-rpc-exception/:id';
8683
});
8784

8885
const transactionEventPromise = waitForTransaction('nestjs-11', transactionEvent => {
89-
// todo(express-5): parametrize /test-expected-rpc-exception/:id
90-
return transactionEvent?.transaction === 'GET /test-expected-rpc-exception/123';
86+
return transactionEvent?.transaction === 'GET /test-expected-rpc-exception/:id';
9187
});
9288

9389
const response = await fetch(`${baseURL}/test-expected-rpc-exception/123`);

dev-packages/e2e-tests/test-applications/nestjs-11/tests/transactions.test.ts

+6-9
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ test('Sends an API route transaction', async ({ baseURL }) => {
1515

1616
expect(transactionEvent.contexts?.trace).toEqual({
1717
data: {
18-
'sentry.source': 'url', // todo(express-5): 'route'
18+
'sentry.source': 'route',
1919
'sentry.origin': 'auto.http.otel.http',
2020
'sentry.op': 'http.server',
2121
'sentry.sample_rate': 1,
@@ -37,7 +37,7 @@ test('Sends an API route transaction', async ({ baseURL }) => {
3737
'net.peer.port': expect.any(Number),
3838
'http.status_code': 200,
3939
'http.status_text': 'OK',
40-
// 'http.route': '/test-transaction', // todo(express-5): add this line again
40+
'http.route': '/test-transaction',
4141
},
4242
op: 'http.server',
4343
span_id: expect.stringMatching(/[a-f0-9]{16}/),
@@ -49,7 +49,6 @@ test('Sends an API route transaction', async ({ baseURL }) => {
4949
expect(transactionEvent).toEqual(
5050
expect.objectContaining({
5151
spans: expect.arrayContaining([
52-
/* todo(express-5): add this part again
5352
{
5453
data: {
5554
'express.name': '/test-transaction',
@@ -67,7 +66,7 @@ test('Sends an API route transaction', async ({ baseURL }) => {
6766
timestamp: expect.any(Number),
6867
trace_id: expect.stringMatching(/[a-f0-9]{32}/),
6968
origin: 'auto.http.otel.express',
70-
}, */
69+
},
7170
{
7271
data: {
7372
'sentry.origin': 'manual',
@@ -117,7 +116,7 @@ test('Sends an API route transaction', async ({ baseURL }) => {
117116
transaction: 'GET /test-transaction',
118117
type: 'transaction',
119118
transaction_info: {
120-
source: 'url', // todo(express-5): 'route'
119+
source: 'route',
121120
},
122121
}),
123122
);
@@ -272,8 +271,7 @@ test('API route transaction includes nest pipe span for valid request', async ({
272271
const transactionEventPromise = waitForTransaction('nestjs-11', transactionEvent => {
273272
return (
274273
transactionEvent?.contexts?.trace?.op === 'http.server' &&
275-
// todo(express-5): parametrize test-pipe-instrumentation/:id
276-
transactionEvent?.transaction === 'GET /test-pipe-instrumentation/123' &&
274+
transactionEvent?.transaction === 'GET /test-pipe-instrumentation/:id' &&
277275
transactionEvent?.request?.url?.includes('/test-pipe-instrumentation/123')
278276
);
279277
});
@@ -310,8 +308,7 @@ test('API route transaction includes nest pipe span for invalid request', async
310308
const transactionEventPromise = waitForTransaction('nestjs-11', transactionEvent => {
311309
return (
312310
transactionEvent?.contexts?.trace?.op === 'http.server' &&
313-
// todo(express-5): parametrize test-pipe-instrumentation/:id
314-
transactionEvent?.transaction === 'GET /test-pipe-instrumentation/abc' &&
311+
transactionEvent?.transaction === 'GET /test-pipe-instrumentation/:id' &&
315312
transactionEvent?.request?.url?.includes('/test-pipe-instrumentation/abc')
316313
);
317314
});

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

+2
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,11 @@
1616
"build:types": "tsc -p tsconfig.types.json",
1717
"clean": "rimraf -g **/node_modules && run-p clean:script",
1818
"clean:script": "node scripts/clean.js",
19+
"express-v5-install": "cd suites/express-v5 && yarn --no-lockfile",
1920
"lint": "eslint . --format stylish",
2021
"fix": "eslint . --format stylish --fix",
2122
"type-check": "tsc",
23+
"pretest": "yarn express-v5-install",
2224
"test": "jest --config ./jest.config.js",
2325
"test:no-prisma": "jest --config ./jest.config.js",
2426
"test:watch": "yarn test --watch"
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);
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+
});
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);
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+
});
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);
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+
});

0 commit comments

Comments
 (0)