Skip to content

Commit 01fa69d

Browse files
authored
feat(node): Add instrumentation for hono handler (#17428)
## Summary This PR enhances the Hono integration by adding comprehensive handler instrumentation, error handling capabilities, and thorough test coverage. The changes build upon the basic Hono integration to provide a complete tracing and error monitoring solution. ## New Features - Handler Instrumentation: Added instrumentation for Hono handlers and middleware, providing detailed tracing capabilities - Error Handler: Implemented setupHonoErrorHandler() function to capture and report errors to Sentry with configurable error filtering - Public API: Added Hono integration to the main package exports, making it available as @sentry/node - Tracing Module: Included Hono integration in the tracing integrations index ## Bug Fixes - CJS Compatibility: Fixed an issue where applying patches failed in CommonJS environments - Type Corrections: Fixed incorrect MiddlewareHandler type definition to ensure proper TypeScript support ## Implementation Details - Instrumentation: Created HonoInstrumentation class that wraps Hono middleware handlers via class extension instead of function replacement for better compatibility - Type Definitions: Added comprehensive TypeScript type definitions vendored from Hono's official types - Constants: Defined Hono-specific attribute names for OpenTelemetry integration - CJS Compatibility: Fixed patching issues in CommonJS environments ## Testing - Integration Tests: Added comprehensive test suite covering: - ESM and CJS compatibility - Multiple HTTP methods (GET, POST, PUT, DELETE, PATCH) - Various route patterns (sync/async, different paths) - Middleware and handler instrumentation verification - Error handling scenarios - Span attribute validation ## Related Issue close #15260
1 parent 73f356d commit 01fa69d

File tree

17 files changed

+801
-21
lines changed

17 files changed

+801
-21
lines changed

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
"@aws-sdk/client-s3": "^3.552.0",
2828
"@google/genai": "^1.20.0",
2929
"@hapi/hapi": "^21.3.10",
30+
"@hono/node-server": "^1.19.4",
3031
"@nestjs/common": "11.1.3",
3132
"@nestjs/core": "11.1.3",
3233
"@nestjs/platform-express": "11.1.3",
@@ -49,6 +50,7 @@
4950
"express": "^4.21.1",
5051
"generic-pool": "^3.9.0",
5152
"graphql": "^16.3.0",
53+
"hono": "^4.9.8",
5254
"http-terminator": "^3.2.0",
5355
"ioredis": "^5.4.1",
5456
"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: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
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+
// anonymous middleware
47+
baseApp[method]('/middleware/separately', async (c, next) => {
48+
await next();
49+
});
50+
51+
baseApp[method]('/middleware/separately', async c => {
52+
const response = c.text('response 200');
53+
if (basePath === '/sync') return response;
54+
return Promise.resolve(response);
55+
});
56+
57+
baseApp.all('/all', c => {
58+
const response = c.text('response 200');
59+
if (basePath === '/sync') return response;
60+
return Promise.resolve(response);
61+
});
62+
63+
baseApp.all(
64+
'/all/middleware',
65+
// anonymous middleware
66+
async (c, next) => {
67+
await next();
68+
},
69+
c => {
70+
const response = c.text('response 200');
71+
if (basePath === '/sync') return response;
72+
return Promise.resolve(response);
73+
},
74+
);
75+
76+
// anonymous middleware
77+
baseApp.all('/all/middleware/separately', async (c, next) => {
78+
await next();
79+
});
80+
81+
baseApp.all('/all/middleware/separately', async c => {
82+
const response = c.text('response 200');
83+
if (basePath === '/sync') return response;
84+
return Promise.resolve(response);
85+
});
86+
87+
baseApp.on(method, '/on', c => {
88+
const response = c.text('response 200');
89+
if (basePath === '/sync') return response;
90+
return Promise.resolve(response);
91+
});
92+
93+
baseApp.on(
94+
method,
95+
'/on/middleware',
96+
// anonymous middleware
97+
async (c, next) => {
98+
await next();
99+
},
100+
c => {
101+
const response = c.text('response 200');
102+
if (basePath === '/sync') return response;
103+
return Promise.resolve(response);
104+
},
105+
);
106+
107+
// anonymous middleware
108+
baseApp.on(method, '/on/middleware/separately', async (c, next) => {
109+
await next();
110+
});
111+
112+
baseApp.on(method, '/on/middleware/separately', async c => {
113+
const response = c.text('response 200');
114+
if (basePath === '/sync') return response;
115+
return Promise.resolve(response);
116+
});
117+
118+
baseApp[method]('/401', () => {
119+
const response = new HTTPException(401, { message: 'response 401' });
120+
if (basePath === '/sync') throw response;
121+
return Promise.reject(response);
122+
});
123+
124+
baseApp.all('/all/401', () => {
125+
const response = new HTTPException(401, { message: 'response 401' });
126+
if (basePath === '/sync') throw response;
127+
return Promise.reject(response);
128+
});
129+
130+
baseApp.on(method, '/on/401', () => {
131+
const response = new HTTPException(401, { message: 'response 401' });
132+
if (basePath === '/sync') throw response;
133+
return Promise.reject(response);
134+
});
135+
136+
baseApp[method]('/402', () => {
137+
const response = new HTTPException(402, { message: 'response 402' });
138+
if (basePath === '/sync') throw response;
139+
return Promise.reject(response);
140+
});
141+
142+
baseApp.all('/all/402', () => {
143+
const response = new HTTPException(402, { message: 'response 402' });
144+
if (basePath === '/sync') throw response;
145+
return Promise.reject(response);
146+
});
147+
148+
baseApp.on(method, '/on/402', () => {
149+
const response = new HTTPException(402, { message: 'response 402' });
150+
if (basePath === '/sync') throw response;
151+
return Promise.reject(response);
152+
});
153+
154+
baseApp[method]('/403', () => {
155+
const response = new HTTPException(403, { message: 'response 403' });
156+
if (basePath === '/sync') throw response;
157+
return Promise.reject(response);
158+
});
159+
160+
baseApp.all('/all/403', () => {
161+
const response = new HTTPException(403, { message: 'response 403' });
162+
if (basePath === '/sync') throw response;
163+
return Promise.reject(response);
164+
});
165+
166+
baseApp.on(method, '/on/403', () => {
167+
const response = new HTTPException(403, { message: 'response 403' });
168+
if (basePath === '/sync') throw response;
169+
return Promise.reject(response);
170+
});
171+
172+
baseApp[method]('/500', () => {
173+
const response = new HTTPException(500, { message: 'response 500' });
174+
if (basePath === '/sync') throw response;
175+
return Promise.reject(response);
176+
});
177+
178+
baseApp.all('/all/500', () => {
179+
const response = new HTTPException(500, { message: 'response 500' });
180+
if (basePath === '/sync') throw response;
181+
return Promise.reject(response);
182+
});
183+
184+
baseApp.on(method, '/on/500', () => {
185+
const response = new HTTPException(500, { message: 'response 500' });
186+
if (basePath === '/sync') throw response;
187+
return Promise.reject(response);
188+
});
189+
});
190+
191+
app.route(basePath, baseApp);
192+
});
193+
194+
const port = 8787;
195+
serve({ fetch: app.fetch, port });
196+
sendPortToRunner(port);

0 commit comments

Comments
 (0)