Skip to content

Commit ff46dce

Browse files
committed
feat(node): Add @sentry/node/preload hook
1 parent cc1cb7b commit ff46dce

40 files changed

+1195
-363
lines changed

.github/workflows/build.yml

+2
Original file line numberDiff line numberDiff line change
@@ -1003,7 +1003,9 @@ jobs:
10031003
'create-remix-app-express-vite-dev',
10041004
'debug-id-sourcemaps',
10051005
'node-express-esm-loader',
1006+
'node-express-esm-preload',
10061007
'node-express-esm-without-loader',
1008+
'node-express-cjs-preload',
10071009
'nextjs-app-dir',
10081010
'nextjs-14',
10091011
'nextjs-15',
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
@sentry:registry=http://127.0.0.1:4873
2+
@sentry-internal:registry=http://127.0.0.1:4873
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"name": "node-express-cjs-preload",
3+
"version": "1.0.0",
4+
"private": true,
5+
"scripts": {
6+
"start": "node --require @sentry/node/preload src/app.js",
7+
"clean": "npx rimraf node_modules pnpm-lock.yaml",
8+
"test:build": "pnpm install",
9+
"test:assert": "playwright test"
10+
},
11+
"dependencies": {
12+
"@sentry/node": "latest || *",
13+
"@sentry/opentelemetry": "latest || *",
14+
"express": "4.19.2"
15+
},
16+
"devDependencies": {
17+
"@sentry-internal/event-proxy-server": "link:../../../event-proxy-server",
18+
"@playwright/test": "^1.27.1"
19+
},
20+
"volta": {
21+
"extends": "../../package.json",
22+
"node": "18.19.1"
23+
}
24+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { devices } from '@playwright/test';
2+
3+
// Fix urls not resolving to localhost on Node v17+
4+
// See: https://github.com/axios/axios/issues/3821#issuecomment-1413727575
5+
import { setDefaultResultOrder } from 'dns';
6+
setDefaultResultOrder('ipv4first');
7+
8+
const eventProxyPort = 3031;
9+
const expressPort = 3030;
10+
11+
/**
12+
* See https://playwright.dev/docs/test-configuration.
13+
*/
14+
const config = {
15+
testDir: './tests',
16+
/* Maximum time one test can run for. */
17+
timeout: 150_000,
18+
expect: {
19+
/**
20+
* Maximum time expect() should wait for the condition to be met.
21+
* For example in `await expect(locator).toHaveText();`
22+
*/
23+
timeout: 5000,
24+
},
25+
/* Run tests in files in parallel */
26+
fullyParallel: true,
27+
/* Fail the build on CI if you accidentally left test.only in the source code. */
28+
forbidOnly: !!process.env.CI,
29+
/* Retry on CI only */
30+
retries: 0,
31+
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
32+
reporter: 'list',
33+
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
34+
use: {
35+
/* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */
36+
actionTimeout: 0,
37+
38+
/* Base URL to use in actions like `await page.goto('/')`. */
39+
baseURL: `http://localhost:${expressPort}`,
40+
},
41+
42+
/* Configure projects for major browsers */
43+
projects: [
44+
{
45+
name: 'chromium',
46+
use: {
47+
...devices['Desktop Chrome'],
48+
},
49+
},
50+
],
51+
52+
/* Run your local dev server before starting the tests */
53+
webServer: [
54+
{
55+
command: 'node start-event-proxy.mjs',
56+
port: eventProxyPort,
57+
stdout: 'pipe',
58+
stderr: 'pipe',
59+
},
60+
{
61+
command: 'pnpm start',
62+
port: expressPort,
63+
stdout: 'pipe',
64+
stderr: 'pipe',
65+
},
66+
],
67+
};
68+
69+
export default config;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
const Sentry = require('@sentry/node');
2+
const express = require('express');
3+
4+
Sentry.init({
5+
environment: 'qa', // dynamic sampling bias to keep transactions
6+
dsn: process.env.E2E_TEST_DSN,
7+
tunnel: `http://localhost:3031/`, // proxy server
8+
tracesSampleRate: 1,
9+
});
10+
11+
const app = express();
12+
const port = 3030;
13+
14+
app.get('/test-success', function (req, res) {
15+
setTimeout(() => {
16+
res.status(200).end();
17+
}, 100);
18+
});
19+
20+
app.get('/test-transaction/:param', function (req, res) {
21+
setTimeout(() => {
22+
res.status(200).end();
23+
}, 100);
24+
});
25+
26+
app.get('/test-error', function (req, res) {
27+
Sentry.captureException(new Error('This is an error'));
28+
setTimeout(() => {
29+
Sentry.flush(2000).then(() => {
30+
res.status(200).end();
31+
});
32+
}, 100);
33+
});
34+
35+
Sentry.setupExpressErrorHandler(app);
36+
37+
app.use(function onError(err, req, res, next) {
38+
// The error id is attached to `res.sentry` to be returned
39+
// and optionally displayed to the user for support.
40+
res.statusCode = 500;
41+
res.end(res.sentry + '\n');
42+
});
43+
44+
app.listen(port, () => {
45+
console.log(`Example app listening on port ${port}`);
46+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { startEventProxyServer } from '@sentry-internal/event-proxy-server';
2+
3+
startEventProxyServer({
4+
port: 3031,
5+
proxyServerName: 'node-express-cjs-preload',
6+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import { expect, test } from '@playwright/test';
2+
import { waitForError, waitForTransaction } from '@sentry-internal/event-proxy-server';
3+
4+
test('Should record exceptions captured inside handlers', async ({ request }) => {
5+
const errorEventPromise = waitForError('node-express-cjs-preload', errorEvent => {
6+
return !!errorEvent?.exception?.values?.[0]?.value?.includes('This is an error');
7+
});
8+
9+
await request.get('/test-error');
10+
11+
await expect(errorEventPromise).resolves.toBeDefined();
12+
});
13+
14+
test('Should record a transaction for a parameterless route', async ({ request }) => {
15+
const transactionEventPromise = waitForTransaction('node-express-cjs-preload', transactionEvent => {
16+
return transactionEvent?.transaction === 'GET /test-success';
17+
});
18+
19+
await request.get('/test-success');
20+
21+
await expect(transactionEventPromise).resolves.toBeDefined();
22+
});
23+
24+
test('Should record a transaction for route with parameters', async ({ request }) => {
25+
const transactionEventPromise = waitForTransaction('node-express-cjs-preload', transactionEvent => {
26+
return transactionEvent.contexts?.trace?.data?.['http.target'] === '/test-transaction/1';
27+
});
28+
29+
await request.get('/test-transaction/1');
30+
31+
const transactionEvent = await transactionEventPromise;
32+
33+
expect(transactionEvent).toBeDefined();
34+
expect(transactionEvent.transaction).toEqual('GET /test-transaction/:param');
35+
expect(transactionEvent.contexts?.trace?.data).toEqual(
36+
expect.objectContaining({
37+
'http.flavor': '1.1',
38+
'http.host': 'localhost:3030',
39+
'http.method': 'GET',
40+
'http.response.status_code': 200,
41+
'http.route': '/test-transaction/:param',
42+
'http.scheme': 'http',
43+
'http.status_code': 200,
44+
'http.status_text': 'OK',
45+
'http.target': '/test-transaction/1',
46+
'http.url': 'http://localhost:3030/test-transaction/1',
47+
'http.user_agent': expect.any(String),
48+
'net.host.ip': expect.any(String),
49+
'net.host.name': 'localhost',
50+
'net.host.port': 3030,
51+
'net.peer.ip': expect.any(String),
52+
'net.peer.port': expect.any(Number),
53+
'net.transport': 'ip_tcp',
54+
'otel.kind': 'SERVER',
55+
'sentry.op': 'http.server',
56+
'sentry.origin': 'auto.http.otel.http',
57+
'sentry.sample_rate': 1,
58+
'sentry.source': 'route',
59+
url: 'http://localhost:3030/test-transaction/1',
60+
}),
61+
);
62+
63+
const spans = transactionEvent.spans || [];
64+
expect(spans).toContainEqual({
65+
data: {
66+
'express.name': 'query',
67+
'express.type': 'middleware',
68+
'http.route': '/',
69+
'otel.kind': 'INTERNAL',
70+
'sentry.origin': 'auto.http.otel.express',
71+
'sentry.op': 'middleware.express',
72+
},
73+
op: 'middleware.express',
74+
description: 'query',
75+
origin: 'auto.http.otel.express',
76+
parent_span_id: expect.any(String),
77+
span_id: expect.any(String),
78+
start_timestamp: expect.any(Number),
79+
status: 'ok',
80+
timestamp: expect.any(Number),
81+
trace_id: expect.any(String),
82+
});
83+
84+
expect(spans).toContainEqual({
85+
data: {
86+
'express.name': 'expressInit',
87+
'express.type': 'middleware',
88+
'http.route': '/',
89+
'otel.kind': 'INTERNAL',
90+
'sentry.origin': 'auto.http.otel.express',
91+
'sentry.op': 'middleware.express',
92+
},
93+
op: 'middleware.express',
94+
description: 'expressInit',
95+
origin: 'auto.http.otel.express',
96+
parent_span_id: expect.any(String),
97+
span_id: expect.any(String),
98+
start_timestamp: expect.any(Number),
99+
status: 'ok',
100+
timestamp: expect.any(Number),
101+
trace_id: expect.any(String),
102+
});
103+
104+
expect(spans).toContainEqual({
105+
data: {
106+
'express.name': '/test-transaction/:param',
107+
'express.type': 'request_handler',
108+
'http.route': '/test-transaction/:param',
109+
'otel.kind': 'INTERNAL',
110+
'sentry.origin': 'auto.http.otel.express',
111+
'sentry.op': 'request_handler.express',
112+
},
113+
op: 'request_handler.express',
114+
description: '/test-transaction/:param',
115+
origin: 'auto.http.otel.express',
116+
parent_span_id: expect.any(String),
117+
span_id: expect.any(String),
118+
start_timestamp: expect.any(Number),
119+
status: 'ok',
120+
timestamp: expect.any(Number),
121+
trace_id: expect.any(String),
122+
});
123+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
@sentry:registry=http://127.0.0.1:4873
2+
@sentry-internal:registry=http://127.0.0.1:4873
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"name": "node-express-esm-preload",
3+
"version": "1.0.0",
4+
"private": true,
5+
"scripts": {
6+
"start": "node --import @sentry/node/preload src/app.mjs",
7+
"clean": "npx rimraf node_modules pnpm-lock.yaml",
8+
"test:build": "pnpm install",
9+
"test:assert": "playwright test"
10+
},
11+
"dependencies": {
12+
"@sentry/node": "latest || *",
13+
"@sentry/opentelemetry": "latest || *",
14+
"express": "4.19.2"
15+
},
16+
"devDependencies": {
17+
"@sentry-internal/event-proxy-server": "link:../../../event-proxy-server",
18+
"@playwright/test": "^1.27.1"
19+
},
20+
"volta": {
21+
"extends": "../../package.json",
22+
"node": "18.19.1"
23+
}
24+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { devices } from '@playwright/test';
2+
3+
// Fix urls not resolving to localhost on Node v17+
4+
// See: https://github.com/axios/axios/issues/3821#issuecomment-1413727575
5+
import { setDefaultResultOrder } from 'dns';
6+
setDefaultResultOrder('ipv4first');
7+
8+
const eventProxyPort = 3031;
9+
const expressPort = 3030;
10+
11+
/**
12+
* See https://playwright.dev/docs/test-configuration.
13+
*/
14+
const config = {
15+
testDir: './tests',
16+
/* Maximum time one test can run for. */
17+
timeout: 150_000,
18+
expect: {
19+
/**
20+
* Maximum time expect() should wait for the condition to be met.
21+
* For example in `await expect(locator).toHaveText();`
22+
*/
23+
timeout: 5000,
24+
},
25+
/* Run tests in files in parallel */
26+
fullyParallel: true,
27+
/* Fail the build on CI if you accidentally left test.only in the source code. */
28+
forbidOnly: !!process.env.CI,
29+
/* Retry on CI only */
30+
retries: 0,
31+
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
32+
reporter: 'list',
33+
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
34+
use: {
35+
/* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */
36+
actionTimeout: 0,
37+
38+
/* Base URL to use in actions like `await page.goto('/')`. */
39+
baseURL: `http://localhost:${expressPort}`,
40+
},
41+
42+
/* Configure projects for major browsers */
43+
projects: [
44+
{
45+
name: 'chromium',
46+
use: {
47+
...devices['Desktop Chrome'],
48+
},
49+
},
50+
],
51+
52+
/* Run your local dev server before starting the tests */
53+
webServer: [
54+
{
55+
command: 'node start-event-proxy.mjs',
56+
port: eventProxyPort,
57+
stdout: 'pipe',
58+
stderr: 'pipe',
59+
},
60+
{
61+
command: 'pnpm start',
62+
port: expressPort,
63+
stdout: 'pipe',
64+
stderr: 'pipe',
65+
},
66+
],
67+
};
68+
69+
export default config;

0 commit comments

Comments
 (0)