Skip to content

Commit 505c480

Browse files
committed
test(node): Add testing of Hono Instrumentation
1 parent e1ac1e9 commit 505c480

File tree

6 files changed

+366
-4
lines changed

6 files changed

+366
-4
lines changed

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
"dependencies": {
2626
"@aws-sdk/client-s3": "^3.552.0",
2727
"@hapi/hapi": "^21.3.10",
28+
"@hono/node-server": "^1.18.2",
2829
"@nestjs/common": "11.1.3",
2930
"@nestjs/core": "11.1.3",
3031
"@nestjs/platform-express": "11.1.3",
@@ -45,6 +46,7 @@
4546
"express": "^4.21.1",
4647
"generic-pool": "^3.9.0",
4748
"graphql": "^16.3.0",
49+
"hono": "^4.8.12",
4850
"http-terminator": "^3.2.0",
4951
"ioredis": "^5.4.1",
5052
"kafkajs": "2.2.4",
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import * as Sentry from '@sentry/node';
2+
import { loggingTransport } from '@sentry-internal/node-integration-tests';
3+
4+
Sentry.init({
5+
dsn: 'https://[email protected]/1337',
6+
release: '1.0',
7+
tracesSampleRate: 1.0,
8+
transport: loggingTransport,
9+
});
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
import { serve } from '@hono/node-server';
2+
import * as Sentry from '@sentry/node';
3+
import { sendPortToRunner } from '@sentry-internal/node-core-integration-tests';
4+
import { Hono } from 'hono';
5+
import { HTTPException } from 'hono/http-exception';
6+
7+
const app = new Hono();
8+
9+
Sentry.setupHonoErrorHandler(app);
10+
11+
// Global middleware to capture all requests
12+
app.use(async function global(c, next) {
13+
await next();
14+
});
15+
16+
const basePaths = ['/sync', '/async'];
17+
const methods = ['get', 'post', 'put', 'delete', 'patch'];
18+
19+
basePaths.forEach(basePath => {
20+
// Sub-path middleware to capture all requests under the basePath
21+
app.use(`${basePath}/*`, async function base(c, next) {
22+
await next();
23+
});
24+
25+
const baseApp = new Hono();
26+
methods.forEach(method => {
27+
baseApp[method]('/', c => {
28+
const response = c.text('response 200');
29+
if (basePath === '/sync') return response;
30+
return Promise.resolve(response);
31+
});
32+
33+
baseApp[method](
34+
'/middleware',
35+
// anonymous middleware
36+
async (c, next) => {
37+
await next();
38+
},
39+
c => {
40+
const response = c.text('response 200');
41+
if (basePath === '/sync') return response;
42+
return Promise.resolve(response);
43+
},
44+
);
45+
46+
baseApp.all('/all', c => {
47+
const response = c.text('response 200');
48+
if (basePath === '/sync') return response;
49+
return Promise.resolve(response);
50+
});
51+
52+
baseApp.all(
53+
'/all/middleware',
54+
// anonymous middleware
55+
async (c, next) => {
56+
await next();
57+
},
58+
c => {
59+
const response = c.text('response 200');
60+
if (basePath === '/sync') return response;
61+
return Promise.resolve(response);
62+
},
63+
);
64+
65+
baseApp.on(method, '/on', c => {
66+
const response = c.text('response 200');
67+
if (basePath === '/sync') return response;
68+
return Promise.resolve(response);
69+
});
70+
71+
baseApp.on(
72+
method,
73+
'/on/middleware',
74+
// anonymous middleware
75+
async (c, next) => {
76+
await next();
77+
},
78+
c => {
79+
const response = c.text('response 200');
80+
if (basePath === '/sync') return response;
81+
return Promise.resolve(response);
82+
},
83+
);
84+
85+
baseApp[method]('/401', () => {
86+
const response = new HTTPException(401, { message: 'response 401' });
87+
if (basePath === '/sync') throw response;
88+
return Promise.reject(response);
89+
});
90+
91+
baseApp.all('/all/401', () => {
92+
const response = new HTTPException(401, { message: 'response 401' });
93+
if (basePath === '/sync') throw response;
94+
return Promise.reject(response);
95+
});
96+
97+
baseApp.on(method, '/on/401', () => {
98+
const response = new HTTPException(401, { message: 'response 401' });
99+
if (basePath === '/sync') throw response;
100+
return Promise.reject(response);
101+
});
102+
103+
baseApp[method]('/402', () => {
104+
const response = new HTTPException(402, { message: 'response 402' });
105+
if (basePath === '/sync') throw response;
106+
return Promise.reject(response);
107+
});
108+
109+
baseApp.all('/all/402', () => {
110+
const response = new HTTPException(402, { message: 'response 402' });
111+
if (basePath === '/sync') throw response;
112+
return Promise.reject(response);
113+
});
114+
115+
baseApp.on(method, '/on/402', () => {
116+
const response = new HTTPException(402, { message: 'response 402' });
117+
if (basePath === '/sync') throw response;
118+
return Promise.reject(response);
119+
});
120+
121+
baseApp[method]('/403', () => {
122+
const response = new HTTPException(403, { message: 'response 403' });
123+
if (basePath === '/sync') throw response;
124+
return Promise.reject(response);
125+
});
126+
127+
baseApp.all('/all/403', () => {
128+
const response = new HTTPException(403, { message: 'response 403' });
129+
if (basePath === '/sync') throw response;
130+
return Promise.reject(response);
131+
});
132+
133+
baseApp.on(method, '/on/403', () => {
134+
const response = new HTTPException(403, { message: 'response 403' });
135+
if (basePath === '/sync') throw response;
136+
return Promise.reject(response);
137+
});
138+
139+
baseApp[method]('/500', () => {
140+
const response = new HTTPException(500, { message: 'response 500' });
141+
if (basePath === '/sync') throw response;
142+
return Promise.reject(response);
143+
});
144+
145+
baseApp.all('/all/500', () => {
146+
const response = new HTTPException(500, { message: 'response 500' });
147+
if (basePath === '/sync') throw response;
148+
return Promise.reject(response);
149+
});
150+
151+
baseApp.on(method, '/on/500', () => {
152+
const response = new HTTPException(500, { message: 'response 500' });
153+
if (basePath === '/sync') throw response;
154+
return Promise.reject(response);
155+
});
156+
});
157+
158+
app.route(basePath, baseApp);
159+
});
160+
161+
const port = 8787;
162+
serve({ fetch: app.fetch, port });
163+
sendPortToRunner(port);
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
import { afterAll, describe, expect } from 'vitest';
2+
import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runner';
3+
4+
describe('hono tracing', () => {
5+
afterAll(() => {
6+
cleanupChildProcesses();
7+
});
8+
9+
createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument.mjs', (createRunner, test) => {
10+
describe.each(['/sync', '/async'] as const)('when using %s route', route => {
11+
describe.each(['get', 'post', 'put', 'delete', 'patch'] as const)('when using %s method', method => {
12+
describe.each(['/', '/all', '/on'])('when using %s path', path => {
13+
test('should handle transaction', async () => {
14+
const runner = createRunner()
15+
.expect({
16+
transaction: {
17+
transaction: `${method.toUpperCase()} ${route}${path === '/' ? '' : path}`,
18+
spans: expect.arrayContaining([
19+
expect.objectContaining({
20+
data: expect.objectContaining({
21+
'hono.name': 'sentryRequestMiddleware',
22+
'hono.type': 'middleware',
23+
}),
24+
description: 'sentryRequestMiddleware',
25+
op: 'middleware.hono',
26+
origin: 'auto.http.otel.hono',
27+
}),
28+
expect.objectContaining({
29+
data: expect.objectContaining({
30+
'hono.name': 'sentryErrorMiddleware',
31+
'hono.type': 'middleware',
32+
}),
33+
description: 'sentryErrorMiddleware',
34+
op: 'middleware.hono',
35+
origin: 'auto.http.otel.hono',
36+
}),
37+
expect.objectContaining({
38+
data: expect.objectContaining({
39+
'hono.name': 'global',
40+
'hono.type': 'middleware',
41+
}),
42+
description: 'global',
43+
op: 'middleware.hono',
44+
origin: 'auto.http.otel.hono',
45+
}),
46+
expect.objectContaining({
47+
data: expect.objectContaining({
48+
'hono.name': 'base',
49+
'hono.type': 'middleware',
50+
}),
51+
description: 'base',
52+
op: 'middleware.hono',
53+
origin: 'auto.http.otel.hono',
54+
}),
55+
expect.objectContaining({
56+
data: expect.objectContaining({
57+
'hono.name': `${route}${path === '/' ? '' : path}`,
58+
'hono.type': 'request_handler',
59+
}),
60+
description: `${route}${path === '/' ? '' : path}`,
61+
op: 'request_handler.hono',
62+
origin: 'auto.http.otel.hono',
63+
}),
64+
]),
65+
},
66+
})
67+
.start();
68+
runner.makeRequest(method, `${route}${path === '/' ? '' : path}`);
69+
await runner.completed();
70+
});
71+
72+
test('should handle transaction with anonymous middleware', async () => {
73+
const runner = createRunner()
74+
.expect({
75+
transaction: {
76+
transaction: `${method.toUpperCase()} ${route}${path === '/' ? '' : path}/middleware`,
77+
spans: expect.arrayContaining([
78+
expect.objectContaining({
79+
data: expect.objectContaining({
80+
'hono.name': 'sentryRequestMiddleware',
81+
'hono.type': 'middleware',
82+
}),
83+
description: 'sentryRequestMiddleware',
84+
op: 'middleware.hono',
85+
origin: 'auto.http.otel.hono',
86+
}),
87+
expect.objectContaining({
88+
data: expect.objectContaining({
89+
'hono.name': 'sentryErrorMiddleware',
90+
'hono.type': 'middleware',
91+
}),
92+
description: 'sentryErrorMiddleware',
93+
op: 'middleware.hono',
94+
origin: 'auto.http.otel.hono',
95+
}),
96+
expect.objectContaining({
97+
data: expect.objectContaining({
98+
'hono.name': 'global',
99+
'hono.type': 'middleware',
100+
}),
101+
description: 'global',
102+
op: 'middleware.hono',
103+
origin: 'auto.http.otel.hono',
104+
}),
105+
expect.objectContaining({
106+
data: expect.objectContaining({
107+
'hono.name': 'base',
108+
'hono.type': 'middleware',
109+
}),
110+
description: 'base',
111+
op: 'middleware.hono',
112+
origin: 'auto.http.otel.hono',
113+
}),
114+
expect.objectContaining({
115+
data: expect.objectContaining({
116+
'hono.name': 'anonymous',
117+
'hono.type': 'middleware',
118+
}),
119+
description: 'anonymous',
120+
op: 'middleware.hono',
121+
origin: 'auto.http.otel.hono',
122+
}),
123+
expect.objectContaining({
124+
data: expect.objectContaining({
125+
'hono.name': `${route}${path === '/' ? '' : path}/middleware`,
126+
'hono.type': 'request_handler',
127+
}),
128+
description: `${route}${path === '/' ? '' : path}/middleware`,
129+
op: 'request_handler.hono',
130+
origin: 'auto.http.otel.hono',
131+
}),
132+
]),
133+
},
134+
})
135+
.start();
136+
runner.makeRequest(method, `${route}${path === '/' ? '' : path}/middleware`);
137+
await runner.completed();
138+
});
139+
140+
test('should handle returned errors for %s path', async () => {
141+
const runner = createRunner()
142+
.ignore('transaction')
143+
.expect({
144+
event: {
145+
exception: {
146+
values: [
147+
{
148+
type: 'Error',
149+
value: 'response 500',
150+
},
151+
],
152+
},
153+
},
154+
})
155+
.start();
156+
runner.makeRequest(method, `${route}${path === '/' ? '' : path}/500`, { expectError: true });
157+
await runner.completed();
158+
});
159+
160+
test.each(['/401', '/402', '/403', '/does-not-exist'])(
161+
'should ignores error %s path by default',
162+
async (subPath: string) => {
163+
const runner = createRunner()
164+
.expect({
165+
transaction: {
166+
transaction: `${method.toUpperCase()} ${route}`,
167+
},
168+
})
169+
.start();
170+
runner.makeRequest(method, `${route}${path === '/' ? '' : path}${subPath}`, { expectError: true });
171+
runner.makeRequest(method, route);
172+
await runner.completed();
173+
},
174+
);
175+
});
176+
});
177+
});
178+
});
179+
});

dev-packages/node-integration-tests/utils/runner.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ type StartResult = {
159159
childHasExited(): boolean;
160160
getLogs(): string[];
161161
makeRequest<T>(
162-
method: 'get' | 'post',
162+
method: 'get' | 'post' | 'put' | 'delete' | 'patch',
163163
path: string,
164164
options?: { headers?: Record<string, string>; data?: BodyInit; expectError?: boolean },
165165
): Promise<T | undefined>;
@@ -544,7 +544,7 @@ export function createRunner(...paths: string[]) {
544544
return logs;
545545
},
546546
makeRequest: async function <T>(
547-
method: 'get' | 'post',
547+
method: 'get' | 'post' | 'put' | 'delete' | 'patch',
548548
path: string,
549549
options: { headers?: Record<string, string>; data?: BodyInit; expectError?: boolean } = {},
550550
): Promise<T | undefined> {
@@ -563,7 +563,7 @@ export function createRunner(...paths: string[]) {
563563
if (process.env.DEBUG) log('making request', method, url, headers, body);
564564

565565
try {
566-
const res = await fetch(url, { headers, method, body });
566+
const res = await fetch(url, { headers, method: method.toUpperCase(), body });
567567

568568
if (!res.ok) {
569569
if (!expectError) {

0 commit comments

Comments
 (0)