Skip to content

Commit 6a7600e

Browse files
committed
feature(node): Add a Hono error handler
1 parent 6f47778 commit 6a7600e

File tree

3 files changed

+123
-4
lines changed

3 files changed

+123
-4
lines changed

packages/node-core/src/utils/ensureIsWrapped.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { createMissingInstrumentationContext } from './createMissingInstrumentat
99
*/
1010
export function ensureIsWrapped(
1111
maybeWrappedFunction: unknown,
12-
name: 'express' | 'connect' | 'fastify' | 'hapi' | 'koa',
12+
name: 'express' | 'connect' | 'fastify' | 'hapi' | 'koa' | 'hono',
1313
): void {
1414
const clientOptions = getClient<NodeClient>()?.getOptions();
1515
if (

packages/node/src/integrations/tracing/hono/index.ts

Lines changed: 119 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,22 @@
1-
import type { IntegrationFn } from '@sentry/core';
2-
import { defineIntegration } from '@sentry/core';
3-
import { generateInstrumentOnce } from '@sentry/node-core';
1+
import { ATTR_HTTP_REQUEST_METHOD, ATTR_HTTP_ROUTE } from '@opentelemetry/semantic-conventions';
2+
import type { IntegrationFn, Span } from '@sentry/core';
3+
import {
4+
captureException,
5+
debug,
6+
defineIntegration,
7+
getClient,
8+
getDefaultIsolationScope,
9+
getIsolationScope,
10+
httpRequestToRequestData,
11+
SEMANTIC_ATTRIBUTE_SENTRY_OP,
12+
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
13+
spanToJSON,
14+
} from '@sentry/core';
15+
import { ensureIsWrapped, generateInstrumentOnce } from '@sentry/node-core';
16+
import { DEBUG_BUILD } from '../../../debug-build';
17+
import { AttributeNames } from './constants';
418
import { HonoInstrumentation } from './instrumentation';
19+
import type { Context, MiddlewareHandler, MiddlewareHandlerInterface, Next } from './types';
520

621
const INTEGRATION_NAME = 'Hono';
722

@@ -33,3 +48,104 @@ const _honoIntegration = (() => {
3348
* ```
3449
*/
3550
export const honoIntegration = defineIntegration(_honoIntegration);
51+
52+
interface HonoHandlerOptions {
53+
/**
54+
* Callback method deciding whether error should be captured and sent to Sentry
55+
* @param error Captured Hono error
56+
*/
57+
shouldHandleError: (context: Context) => boolean;
58+
}
59+
60+
function honoRequestHandler(): MiddlewareHandler {
61+
return async function sentryRequestMiddleware(context: Context, next: Next): Promise<void> {
62+
const normalizedRequest = httpRequestToRequestData(context.req);
63+
getIsolationScope().setSDKProcessingMetadata({ normalizedRequest });
64+
await next();
65+
};
66+
}
67+
68+
function defaultShouldHandleError(context: Context): boolean {
69+
const statusCode = context.res.status;
70+
return statusCode >= 500;
71+
}
72+
73+
function honoErrorHandler(options?: Partial<HonoHandlerOptions>): MiddlewareHandler {
74+
return async function sentryErrorMiddleware(context: Context, next: Next): Promise<void> {
75+
await next();
76+
77+
const shouldHandleError = options?.shouldHandleError || defaultShouldHandleError;
78+
if (shouldHandleError(context)) {
79+
(context.res as { sentry?: string }).sentry = captureException(context.error, {
80+
mechanism: {
81+
type: 'hono',
82+
handled: false,
83+
},
84+
});
85+
}
86+
};
87+
}
88+
89+
function addHonoSpanAttributes(span: Span): void {
90+
const attributes = spanToJSON(span).data;
91+
const type = attributes[AttributeNames.HONO_TYPE];
92+
if (attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP] || !type) {
93+
return;
94+
}
95+
96+
span.setAttributes({
97+
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.otel.hono',
98+
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: `${type}.hono`,
99+
});
100+
101+
const name = attributes[AttributeNames.HONO_NAME];
102+
if (typeof name === 'string') {
103+
span.updateName(name);
104+
}
105+
106+
if (getIsolationScope() === getDefaultIsolationScope()) {
107+
DEBUG_BUILD && debug.warn('Isolation scope is default isolation scope - skipping setting transactionName');
108+
return;
109+
}
110+
111+
const route = attributes[ATTR_HTTP_ROUTE];
112+
const method = attributes[ATTR_HTTP_REQUEST_METHOD];
113+
if (typeof route === 'string' && typeof method === 'string') {
114+
getIsolationScope().setTransactionName(`${method} ${route}`);
115+
}
116+
}
117+
118+
/**
119+
* Add a Hono error handler to capture errors to Sentry.
120+
*
121+
* @param app The Hono instances
122+
* @param options Configuration options for the handler
123+
*
124+
* @example
125+
* ```javascript
126+
* const Sentry = require('@sentry/node');
127+
* const { Hono } = require("hono");
128+
*
129+
* const app = new Hono();
130+
*
131+
* Sentry.setupHonoErrorHandler(app);
132+
*
133+
* // Add your routes, etc.
134+
* ```
135+
*/
136+
export function setupHonoErrorHandler(
137+
app: { use: MiddlewareHandlerInterface },
138+
options?: Partial<HonoHandlerOptions>,
139+
): void {
140+
app.use(honoRequestHandler());
141+
app.use(honoErrorHandler(options));
142+
143+
const client = getClient();
144+
if (client) {
145+
client.on('spanStart', span => {
146+
addHonoSpanAttributes(span);
147+
});
148+
}
149+
150+
ensureIsWrapped(app.use, 'hono');
151+
}

packages/node/src/integrations/tracing/hono/types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
// Vendored from: https://github.com/honojs/hono/blob/855e5b1adbf685bf4b3e6b76573aa7cb0a108d04/src/request.ts#L30
22
export type HonoRequest = {
33
path: string;
4+
method: string;
45
};
56

67
// Vendored from: https://github.com/honojs/hono/blob/855e5b1adbf685bf4b3e6b76573aa7cb0a108d04/src/context.ts#L291
78
export type Context = {
89
req: HonoRequest;
10+
res: Response;
11+
error: Error | undefined;
912
};
1013

1114
// Vendored from: https://github.com/honojs/hono/blob/855e5b1adbf685bf4b3e6b76573aa7cb0a108d04/src/types.ts#L36C1-L36C39

0 commit comments

Comments
 (0)