Skip to content

Commit 1a3cc3d

Browse files
committed
Add fastify v5 e2e test app
1 parent cd31371 commit 1a3cc3d

File tree

11 files changed

+722
-0
lines changed

11 files changed

+722
-0
lines changed

.github/workflows/build.yml

+1
Original file line numberDiff line numberDiff line change
@@ -921,6 +921,7 @@ jobs:
921921
'tanstack-router',
922922
'generic-ts3.8',
923923
'node-fastify',
924+
'node-fastify-5',
924925
'node-hapi',
925926
'node-nestjs-basic',
926927
'node-nestjs-distributed-tracing',
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
dist
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,31 @@
1+
{
2+
"name": "node-fastify-5",
3+
"version": "1.0.0",
4+
"private": true,
5+
"scripts": {
6+
"start": "ts-node src/app.ts",
7+
"test": "playwright test",
8+
"clean": "npx rimraf node_modules pnpm-lock.yaml",
9+
"typecheck": "tsc",
10+
"test:build": "pnpm install && pnpm run typecheck",
11+
"test:assert": "pnpm test"
12+
},
13+
"dependencies": {
14+
"@sentry/node": "latest || *",
15+
"@sentry/types": "latest || *",
16+
"@sentry/core": "latest || *",
17+
"@sentry/utils": "latest || *",
18+
"@sentry/opentelemetry": "latest || *",
19+
"@types/node": "22.7.5",
20+
"fastify": "5.0.0",
21+
"typescript": "5.6.3",
22+
"ts-node": "10.9.2"
23+
},
24+
"devDependencies": {
25+
"@playwright/test": "^1.44.1",
26+
"@sentry-internal/test-utils": "link:../../../test-utils"
27+
},
28+
"volta": {
29+
"extends": "../../package.json"
30+
}
31+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { getPlaywrightConfig } from '@sentry-internal/test-utils';
2+
3+
const config = getPlaywrightConfig({
4+
startCommand: `pnpm start`,
5+
});
6+
7+
export default config;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
import type * as S from '@sentry/node';
2+
const Sentry = require('@sentry/node') as typeof S;
3+
4+
// We wrap console.warn to find out if a warning is incorrectly logged
5+
console.warn = new Proxy(console.warn, {
6+
apply: function (target, thisArg, argumentsList) {
7+
const msg = argumentsList[0];
8+
if (typeof msg === 'string' && msg.startsWith('[Sentry]')) {
9+
console.error(`Sentry warning was triggered: ${msg}`);
10+
process.exit(1);
11+
}
12+
13+
return target.apply(thisArg, argumentsList);
14+
},
15+
});
16+
17+
Sentry.init({
18+
environment: 'qa', // dynamic sampling bias to keep transactions
19+
dsn: process.env.E2E_TEST_DSN,
20+
integrations: [],
21+
tracesSampleRate: 1,
22+
tunnel: 'http://localhost:3031/', // proxy server
23+
tracePropagationTargets: ['http://localhost:3030', '/external-allowed'],
24+
});
25+
26+
import type * as H from 'http';
27+
import type * as F from 'fastify';
28+
29+
// Make sure fastify is imported after Sentry is initialized
30+
const { fastify } = require('fastify') as typeof F;
31+
const http = require('http') as typeof H;
32+
33+
const app = fastify();
34+
const port = 3030;
35+
const port2 = 3040;
36+
37+
Sentry.setupFastifyErrorHandler(app);
38+
39+
app.get('/test-success', function (_req, res) {
40+
res.send({ version: 'v1' });
41+
});
42+
43+
app.get<{ Params: { param: string } }>('/test-param/:param', function (req, res) {
44+
res.send({ paramWas: req.params.param });
45+
});
46+
47+
app.get<{ Params: { id: string } }>('/test-inbound-headers/:id', function (req, res) {
48+
const headers = req.headers;
49+
50+
res.send({ headers, id: req.params.id });
51+
});
52+
53+
app.get<{ Params: { id: string } }>('/test-outgoing-http/:id', async function (req, res) {
54+
const id = req.params.id;
55+
const data = await makeHttpRequest(`http://localhost:3030/test-inbound-headers/${id}`);
56+
57+
res.send(data);
58+
});
59+
60+
app.get<{ Params: { id: string } }>('/test-outgoing-fetch/:id', async function (req, res) {
61+
const id = req.params.id;
62+
const response = await fetch(`http://localhost:3030/test-inbound-headers/${id}`);
63+
const data = await response.json();
64+
65+
res.send(data);
66+
});
67+
68+
app.get('/test-transaction', async function (req, res) {
69+
Sentry.startSpan({ name: 'test-span' }, () => {
70+
Sentry.startSpan({ name: 'child-span' }, () => {});
71+
});
72+
73+
res.send({});
74+
});
75+
76+
app.get('/test-error', async function (req, res) {
77+
const exceptionId = Sentry.captureException(new Error('This is an error'));
78+
79+
await Sentry.flush(2000);
80+
81+
res.send({ exceptionId });
82+
});
83+
84+
app.get<{ Params: { id: string } }>('/test-exception/:id', async function (req, res) {
85+
throw new Error(`This is an exception with id ${req.params.id}`);
86+
});
87+
88+
app.get('/test-outgoing-fetch-external-allowed', async function (req, res) {
89+
const fetchResponse = await fetch(`http://localhost:${port2}/external-allowed`);
90+
const data = await fetchResponse.json();
91+
92+
res.send(data);
93+
});
94+
95+
app.get('/test-outgoing-fetch-external-disallowed', async function (req, res) {
96+
const fetchResponse = await fetch(`http://localhost:${port2}/external-disallowed`);
97+
const data = await fetchResponse.json();
98+
99+
res.send(data);
100+
});
101+
102+
app.get('/test-outgoing-http-external-allowed', async function (req, res) {
103+
const data = await makeHttpRequest(`http://localhost:${port2}/external-allowed`);
104+
res.send(data);
105+
});
106+
107+
app.get('/test-outgoing-http-external-disallowed', async function (req, res) {
108+
const data = await makeHttpRequest(`http://localhost:${port2}/external-disallowed`);
109+
res.send(data);
110+
});
111+
112+
app.listen({ port: port });
113+
114+
// A second app so we can test header propagation between external URLs
115+
const app2 = fastify();
116+
app2.get('/external-allowed', function (req, res) {
117+
const headers = req.headers;
118+
119+
res.send({ headers, route: '/external-allowed' });
120+
});
121+
122+
app2.get('/external-disallowed', function (req, res) {
123+
const headers = req.headers;
124+
125+
res.send({ headers, route: '/external-disallowed' });
126+
});
127+
128+
app2.listen({ port: port2 });
129+
130+
function makeHttpRequest(url: string) {
131+
return new Promise(resolve => {
132+
const data: any[] = [];
133+
134+
http
135+
.request(url, httpRes => {
136+
httpRes.on('data', chunk => {
137+
data.push(chunk);
138+
});
139+
httpRes.on('error', error => {
140+
resolve({ error: error.message, url });
141+
});
142+
httpRes.on('end', () => {
143+
try {
144+
const json = JSON.parse(Buffer.concat(data).toString());
145+
resolve(json);
146+
} catch {
147+
resolve({ data: Buffer.concat(data).toString(), url });
148+
}
149+
});
150+
})
151+
.end();
152+
});
153+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { startEventProxyServer } from '@sentry-internal/test-utils';
2+
3+
startEventProxyServer({
4+
port: 3031,
5+
proxyServerName: 'node-fastify-5',
6+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { expect, test } from '@playwright/test';
2+
import { waitForError } from '@sentry-internal/test-utils';
3+
4+
test('Sends correct error event', async ({ baseURL }) => {
5+
const errorEventPromise = waitForError('node-fastify-5', event => {
6+
return !event.type && event.exception?.values?.[0]?.value === 'This is an exception with id 123';
7+
});
8+
9+
await fetch(`${baseURL}/test-exception/123`);
10+
11+
const errorEvent = await errorEventPromise;
12+
13+
expect(errorEvent.exception?.values).toHaveLength(1);
14+
expect(errorEvent.exception?.values?.[0]?.value).toBe('This is an exception with id 123');
15+
16+
expect(errorEvent.request).toEqual({
17+
method: 'GET',
18+
cookies: {},
19+
headers: expect.any(Object),
20+
url: 'http://localhost:3030/test-exception/123',
21+
});
22+
23+
expect(errorEvent.transaction).toEqual('GET /test-exception/:id');
24+
25+
expect(errorEvent.contexts?.trace).toEqual({
26+
trace_id: expect.any(String),
27+
span_id: expect.any(String),
28+
});
29+
});

0 commit comments

Comments
 (0)