From 81029b800dde17ace6dbdbbd64ba5c60c8ae82e9 Mon Sep 17 00:00:00 2001 From: Karibash Date: Wed, 13 Aug 2025 14:21:53 +0900 Subject: [PATCH 01/11] bugfix(node): Fix incorrect type of MiddlewareHandler --- packages/node/src/integrations/tracing/hono/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/node/src/integrations/tracing/hono/types.ts b/packages/node/src/integrations/tracing/hono/types.ts index 3d7e057859f1..e12c005f8312 100644 --- a/packages/node/src/integrations/tracing/hono/types.ts +++ b/packages/node/src/integrations/tracing/hono/types.ts @@ -15,7 +15,7 @@ export type Next = () => Promise; export type Handler = (c: Context, next: Next) => Promise | Response; // Vendored from: https://github.com/honojs/hono/blob/855e5b1adbf685bf4b3e6b76573aa7cb0a108d04/src/types.ts#L80 -export type MiddlewareHandler = (c: Context, next: Next) => Promise | Response | void; +export type MiddlewareHandler = (c: Context, next: Next) => Promise; // Vendored from: https://github.com/honojs/hono/blob/855e5b1adbf685bf4b3e6b76573aa7cb0a108d04/src/types.ts#L109 export type HandlerInterface = { From 7673fef78c21ef145ba82a7f695facf3a7bc68ce Mon Sep 17 00:00:00 2001 From: Karibash Date: Tue, 12 Aug 2025 20:19:58 +0900 Subject: [PATCH 02/11] feature(node): Add instrumentation to the handler in Hono --- .../integrations/tracing/hono/constants.ts | 13 ++ .../tracing/hono/instrumentation.ts | 162 +++++++++++++++++- 2 files changed, 168 insertions(+), 7 deletions(-) create mode 100644 packages/node/src/integrations/tracing/hono/constants.ts diff --git a/packages/node/src/integrations/tracing/hono/constants.ts b/packages/node/src/integrations/tracing/hono/constants.ts new file mode 100644 index 000000000000..5814f5e950f2 --- /dev/null +++ b/packages/node/src/integrations/tracing/hono/constants.ts @@ -0,0 +1,13 @@ +export const AttributeNames = { + HONO_TYPE: 'hono.type', + HONO_NAME: 'hono.name', +} as const; + +export type AttributeNames = (typeof AttributeNames)[keyof typeof AttributeNames]; + +export const HonoTypes = { + MIDDLEWARE: 'middleware', + REQUEST_HANDLER: 'request_handler', +} as const; + +export type HonoTypes = (typeof HonoTypes)[keyof typeof HonoTypes]; diff --git a/packages/node/src/integrations/tracing/hono/instrumentation.ts b/packages/node/src/integrations/tracing/hono/instrumentation.ts index 81e062560051..d1da3dd76036 100644 --- a/packages/node/src/integrations/tracing/hono/instrumentation.ts +++ b/packages/node/src/integrations/tracing/hono/instrumentation.ts @@ -1,5 +1,18 @@ +import type { Span } from '@opentelemetry/api'; +import { context, SpanStatusCode, trace } from '@opentelemetry/api'; import { InstrumentationBase, InstrumentationNodeModuleDefinition } from '@opentelemetry/instrumentation'; -import type { HandlerInterface, Hono, HonoInstance, MiddlewareHandlerInterface, OnHandlerInterface } from './types'; +import { AttributeNames, HonoTypes } from './constants'; +import type { + Context, + Handler, + HandlerInterface, + Hono, + HonoInstance, + MiddlewareHandler, + MiddlewareHandlerInterface, + Next, + OnHandlerInterface, +} from './types'; const PACKAGE_NAME = '@sentry/instrumentation-hono'; const PACKAGE_VERSION = '0.0.1'; @@ -50,10 +63,38 @@ export class HonoInstrumentation extends InstrumentationBase { * Patches the route handler to instrument it. */ private _patchHandler(): (original: HandlerInterface) => HandlerInterface { + // eslint-disable-next-line @typescript-eslint/no-this-alias + const instrumentation = this; + return function (original: HandlerInterface) { return function wrappedHandler(this: HonoInstance, ...args: unknown[]) { - // TODO: Add OpenTelemetry tracing logic here - return original.apply(this, args); + if (typeof args[0] === 'string') { + const path = args[0]; + if (args.length === 1) { + return original.apply(this, [path]); + } + + const handlers = args.slice(1); + return original.apply(this, [ + path, + ...handlers.map((handler, index) => + instrumentation._wrapHandler( + index + 1 === handlers.length ? HonoTypes.REQUEST_HANDLER : HonoTypes.MIDDLEWARE, + handler as Handler | MiddlewareHandler, + ), + ), + ]); + } + + return original.apply( + this, + args.map((handler, index) => + instrumentation._wrapHandler( + index + 1 === args.length ? HonoTypes.REQUEST_HANDLER : HonoTypes.MIDDLEWARE, + handler as Handler | MiddlewareHandler, + ), + ), + ); }; }; } @@ -62,10 +103,21 @@ export class HonoInstrumentation extends InstrumentationBase { * Patches the 'on' handler to instrument it. */ private _patchOnHandler(): (original: OnHandlerInterface) => OnHandlerInterface { + // eslint-disable-next-line @typescript-eslint/no-this-alias + const instrumentation = this; + return function (original: OnHandlerInterface) { return function wrappedHandler(this: HonoInstance, ...args: unknown[]) { - // TODO: Add OpenTelemetry tracing logic here - return original.apply(this, args); + const handlers = args.slice(2); + return original.apply(this, [ + ...args.slice(0, 2), + ...handlers.map((handler, index) => + instrumentation._wrapHandler( + index + 1 === handlers.length ? HonoTypes.REQUEST_HANDLER : HonoTypes.MIDDLEWARE, + handler as Handler | MiddlewareHandler, + ), + ), + ]); }; }; } @@ -74,11 +126,107 @@ export class HonoInstrumentation extends InstrumentationBase { * Patches the middleware handler to instrument it. */ private _patchMiddlewareHandler(): (original: MiddlewareHandlerInterface) => MiddlewareHandlerInterface { + // eslint-disable-next-line @typescript-eslint/no-this-alias + const instrumentation = this; + return function (original: MiddlewareHandlerInterface) { return function wrappedHandler(this: HonoInstance, ...args: unknown[]) { - // TODO: Add OpenTelemetry tracing logic here - return original.apply(this, args); + if (typeof args[0] === 'string') { + const path = args[0]; + if (args.length === 1) { + return original.apply(this, [path]); + } + + const handlers = args.slice(1); + return original.apply(this, [ + path, + ...handlers.map(handler => + instrumentation._wrapHandler(HonoTypes.MIDDLEWARE, handler as MiddlewareHandler), + ), + ]); + } + + return original.apply( + this, + args.map(handler => instrumentation._wrapHandler(HonoTypes.MIDDLEWARE, handler as MiddlewareHandler)), + ); }; }; } + + /** + * Wraps a handler or middleware handler to apply instrumentation. + */ + private _wrapHandler(type: HonoTypes, handler: Handler | MiddlewareHandler): Handler | MiddlewareHandler { + // eslint-disable-next-line @typescript-eslint/no-this-alias + const instrumentation = this; + + return function (this: unknown, c: Context, next: Next) { + if (!instrumentation.isEnabled()) { + return handler.apply(this, [c, next]); + } + + const path = c.req.path; + const spanName = `${type.replace('_', ' ')} - ${path}`; + const span = instrumentation.tracer.startSpan(spanName, { + attributes: { + [AttributeNames.HONO_TYPE]: type, + [AttributeNames.HONO_NAME]: type === 'request_handler' ? path : handler.name || 'anonymous', + }, + }); + + return context.with(trace.setSpan(context.active(), span), () => { + return instrumentation._safeExecute( + () => handler.apply(this, [c, next]), + () => span.end(), + error => { + instrumentation._handleError(span, error); + span.end(); + }, + ); + }); + }; + } + + /** + * Safely executes a function and handles errors. + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private _safeExecute(execute: () => any, onSuccess: () => void, onFailure: (error: unknown) => void): () => any { + try { + const result = execute(); + + if ( + result && + typeof result === 'object' && + typeof Object.getOwnPropertyDescriptor(result, 'then')?.value === 'function' + ) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + result.then( + () => onSuccess(), + (error: unknown) => onFailure(error), + ); + } else { + onSuccess(); + } + + return result; + } catch (error: unknown) { + onFailure(error); + throw error; + } + } + + /** + * Handles errors by setting the span status and recording the exception. + */ + private _handleError(span: Span, error: unknown): void { + if (error instanceof Error) { + span.setStatus({ + code: SpanStatusCode.ERROR, + message: error.message, + }); + span.recordException(error); + } + } } From 72dfddc4f876e3ca9c1e3ffaf9c0c582d71e8767 Mon Sep 17 00:00:00 2001 From: Karibash Date: Thu, 14 Aug 2025 18:28:30 +0900 Subject: [PATCH 03/11] feature(node): Add a Hono error handler --- .../node-core/src/utils/ensureIsWrapped.ts | 2 +- .../src/integrations/tracing/hono/index.ts | 122 +++++++++++++++++- .../src/integrations/tracing/hono/types.ts | 3 + 3 files changed, 123 insertions(+), 4 deletions(-) diff --git a/packages/node-core/src/utils/ensureIsWrapped.ts b/packages/node-core/src/utils/ensureIsWrapped.ts index a73c087ebaf2..921d01da8207 100644 --- a/packages/node-core/src/utils/ensureIsWrapped.ts +++ b/packages/node-core/src/utils/ensureIsWrapped.ts @@ -9,7 +9,7 @@ import { isCjs } from './detection'; */ export function ensureIsWrapped( maybeWrappedFunction: unknown, - name: 'express' | 'connect' | 'fastify' | 'hapi' | 'koa', + name: 'express' | 'connect' | 'fastify' | 'hapi' | 'koa' | 'hono', ): void { const clientOptions = getClient()?.getOptions(); if ( diff --git a/packages/node/src/integrations/tracing/hono/index.ts b/packages/node/src/integrations/tracing/hono/index.ts index 8876d26b829e..e34f297bc8a6 100644 --- a/packages/node/src/integrations/tracing/hono/index.ts +++ b/packages/node/src/integrations/tracing/hono/index.ts @@ -1,7 +1,22 @@ -import type { IntegrationFn } from '@sentry/core'; -import { defineIntegration } from '@sentry/core'; -import { generateInstrumentOnce } from '@sentry/node-core'; +import { ATTR_HTTP_REQUEST_METHOD, ATTR_HTTP_ROUTE } from '@opentelemetry/semantic-conventions'; +import type { IntegrationFn, Span } from '@sentry/core'; +import { + captureException, + debug, + defineIntegration, + getClient, + getDefaultIsolationScope, + getIsolationScope, + httpRequestToRequestData, + SEMANTIC_ATTRIBUTE_SENTRY_OP, + SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, + spanToJSON, +} from '@sentry/core'; +import { ensureIsWrapped, generateInstrumentOnce } from '@sentry/node-core'; +import { DEBUG_BUILD } from '../../../debug-build'; +import { AttributeNames } from './constants'; import { HonoInstrumentation } from './instrumentation'; +import type { Context, MiddlewareHandler, MiddlewareHandlerInterface, Next } from './types'; const INTEGRATION_NAME = 'Hono'; @@ -33,3 +48,104 @@ const _honoIntegration = (() => { * ``` */ export const honoIntegration = defineIntegration(_honoIntegration); + +interface HonoHandlerOptions { + /** + * Callback method deciding whether error should be captured and sent to Sentry + * @param error Captured Hono error + */ + shouldHandleError: (context: Context) => boolean; +} + +function honoRequestHandler(): MiddlewareHandler { + return async function sentryRequestMiddleware(context: Context, next: Next): Promise { + const normalizedRequest = httpRequestToRequestData(context.req); + getIsolationScope().setSDKProcessingMetadata({ normalizedRequest }); + await next(); + }; +} + +function defaultShouldHandleError(context: Context): boolean { + const statusCode = context.res.status; + return statusCode >= 500; +} + +function honoErrorHandler(options?: Partial): MiddlewareHandler { + return async function sentryErrorMiddleware(context: Context, next: Next): Promise { + await next(); + + const shouldHandleError = options?.shouldHandleError || defaultShouldHandleError; + if (shouldHandleError(context)) { + (context.res as { sentry?: string }).sentry = captureException(context.error, { + mechanism: { + type: 'hono', + handled: false, + }, + }); + } + }; +} + +function addHonoSpanAttributes(span: Span): void { + const attributes = spanToJSON(span).data; + const type = attributes[AttributeNames.HONO_TYPE]; + if (attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP] || !type) { + return; + } + + span.setAttributes({ + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.otel.hono', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: `${type}.hono`, + }); + + const name = attributes[AttributeNames.HONO_NAME]; + if (typeof name === 'string') { + span.updateName(name); + } + + if (getIsolationScope() === getDefaultIsolationScope()) { + DEBUG_BUILD && debug.warn('Isolation scope is default isolation scope - skipping setting transactionName'); + return; + } + + const route = attributes[ATTR_HTTP_ROUTE]; + const method = attributes[ATTR_HTTP_REQUEST_METHOD]; + if (typeof route === 'string' && typeof method === 'string') { + getIsolationScope().setTransactionName(`${method} ${route}`); + } +} + +/** + * Add a Hono error handler to capture errors to Sentry. + * + * @param app The Hono instances + * @param options Configuration options for the handler + * + * @example + * ```javascript + * const Sentry = require('@sentry/node'); + * const { Hono } = require("hono"); + * + * const app = new Hono(); + * + * Sentry.setupHonoErrorHandler(app); + * + * // Add your routes, etc. + * ``` + */ +export function setupHonoErrorHandler( + app: { use: MiddlewareHandlerInterface }, + options?: Partial, +): void { + app.use(honoRequestHandler()); + app.use(honoErrorHandler(options)); + + const client = getClient(); + if (client) { + client.on('spanStart', span => { + addHonoSpanAttributes(span); + }); + } + + ensureIsWrapped(app.use, 'hono'); +} diff --git a/packages/node/src/integrations/tracing/hono/types.ts b/packages/node/src/integrations/tracing/hono/types.ts index e12c005f8312..9873f80afa66 100644 --- a/packages/node/src/integrations/tracing/hono/types.ts +++ b/packages/node/src/integrations/tracing/hono/types.ts @@ -1,11 +1,14 @@ // Vendored from: https://github.com/honojs/hono/blob/855e5b1adbf685bf4b3e6b76573aa7cb0a108d04/src/request.ts#L30 export type HonoRequest = { path: string; + method: string; }; // Vendored from: https://github.com/honojs/hono/blob/855e5b1adbf685bf4b3e6b76573aa7cb0a108d04/src/context.ts#L291 export type Context = { req: HonoRequest; + res: Response; + error: Error | undefined; }; // Vendored from: https://github.com/honojs/hono/blob/855e5b1adbf685bf4b3e6b76573aa7cb0a108d04/src/types.ts#L36C1-L36C39 From 1d33a357f0183b59b0b44e1b42091fcd043e6d41 Mon Sep 17 00:00:00 2001 From: Karibash Date: Tue, 12 Aug 2025 22:59:22 +0900 Subject: [PATCH 04/11] feature(node): Add Hono integration to exports --- packages/node/src/index.ts | 1 + packages/node/src/integrations/tracing/index.ts | 3 +++ 2 files changed, 4 insertions(+) diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts index 853ec8dbac2f..67e00660c2a1 100644 --- a/packages/node/src/index.ts +++ b/packages/node/src/index.ts @@ -15,6 +15,7 @@ export { postgresIntegration } from './integrations/tracing/postgres'; export { postgresJsIntegration } from './integrations/tracing/postgresjs'; export { prismaIntegration } from './integrations/tracing/prisma'; export { hapiIntegration, setupHapiErrorHandler } from './integrations/tracing/hapi'; +export { honoIntegration, setupHonoErrorHandler } from './integrations/tracing/hono'; export { koaIntegration, setupKoaErrorHandler } from './integrations/tracing/koa'; export { connectIntegration, setupConnectErrorHandler } from './integrations/tracing/connect'; export { knexIntegration } from './integrations/tracing/knex'; diff --git a/packages/node/src/integrations/tracing/index.ts b/packages/node/src/integrations/tracing/index.ts index e4dd84fc266e..dd9d9ac8df2b 100644 --- a/packages/node/src/integrations/tracing/index.ts +++ b/packages/node/src/integrations/tracing/index.ts @@ -10,6 +10,7 @@ import { genericPoolIntegration, instrumentGenericPool } from './genericPool'; import { googleGenAIIntegration, instrumentGoogleGenAI } from './google-genai'; import { graphqlIntegration, instrumentGraphql } from './graphql'; import { hapiIntegration, instrumentHapi } from './hapi'; +import { honoIntegration, instrumentHono } from './hono'; import { instrumentKafka, kafkaIntegration } from './kafka'; import { instrumentKoa, koaIntegration } from './koa'; import { instrumentLruMemoizer, lruMemoizerIntegration } from './lrumemoizer'; @@ -33,6 +34,7 @@ export function getAutoPerformanceIntegrations(): Integration[] { expressIntegration(), fastifyIntegration(), graphqlIntegration(), + honoIntegration(), mongoIntegration(), mongooseIntegration(), mysqlIntegration(), @@ -70,6 +72,7 @@ export function getOpenTelemetryInstrumentationToPreload(): (((options?: any) => instrumentFastify, instrumentFastifyV3, instrumentHapi, + instrumentHono, instrumentKafka, instrumentKoa, instrumentLruMemoizer, From 97275e0454994264fd35f92f4206e4799a91a650 Mon Sep 17 00:00:00 2001 From: Karibash Date: Fri, 15 Aug 2025 11:05:42 +0900 Subject: [PATCH 05/11] bugfix(node): Fix an issue where applying patches fails in CJS --- .../src/integrations/tracing/hono/instrumentation.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/node/src/integrations/tracing/hono/instrumentation.ts b/packages/node/src/integrations/tracing/hono/instrumentation.ts index d1da3dd76036..644bdb4eb5ff 100644 --- a/packages/node/src/integrations/tracing/hono/instrumentation.ts +++ b/packages/node/src/integrations/tracing/hono/instrumentation.ts @@ -41,7 +41,7 @@ export class HonoInstrumentation extends InstrumentationBase { // eslint-disable-next-line @typescript-eslint/no-this-alias const instrumentation = this; - moduleExports.Hono = class HonoWrapper extends moduleExports.Hono { + class WrappedHono extends moduleExports.Hono { public constructor(...args: unknown[]) { super(...args); @@ -55,7 +55,15 @@ export class HonoInstrumentation extends InstrumentationBase { instrumentation._wrap(this, 'on', instrumentation._patchOnHandler()); instrumentation._wrap(this, 'use', instrumentation._patchMiddlewareHandler()); } - }; + } + + try { + moduleExports.Hono = WrappedHono; + } catch { + // This is a workaround for environments where direct assignment is not allowed. + return { ...moduleExports, Hono: WrappedHono }; + } + return moduleExports; } From b31e2f064ed68fd34d6c7c8a93b406d4f7230fd1 Mon Sep 17 00:00:00 2001 From: Karibash Date: Fri, 15 Aug 2025 14:19:18 +0900 Subject: [PATCH 06/11] test(node): Add testing of Hono Instrumentation --- .../node-integration-tests/package.json | 2 + .../suites/tracing/hono/instrument.mjs | 9 + .../suites/tracing/hono/scenario.mjs | 163 ++++++++++++++++ .../suites/tracing/hono/test.ts | 179 ++++++++++++++++++ .../node-integration-tests/utils/runner.ts | 6 +- yarn.lock | 10 + 6 files changed, 366 insertions(+), 3 deletions(-) create mode 100644 dev-packages/node-integration-tests/suites/tracing/hono/instrument.mjs create mode 100644 dev-packages/node-integration-tests/suites/tracing/hono/scenario.mjs create mode 100644 dev-packages/node-integration-tests/suites/tracing/hono/test.ts diff --git a/dev-packages/node-integration-tests/package.json b/dev-packages/node-integration-tests/package.json index 9004aa9e2fbf..601bc12ccb74 100644 --- a/dev-packages/node-integration-tests/package.json +++ b/dev-packages/node-integration-tests/package.json @@ -27,6 +27,7 @@ "@aws-sdk/client-s3": "^3.552.0", "@google/genai": "^1.20.0", "@hapi/hapi": "^21.3.10", + "@hono/node-server": "^1.18.2", "@nestjs/common": "11.1.3", "@nestjs/core": "11.1.3", "@nestjs/platform-express": "11.1.3", @@ -49,6 +50,7 @@ "express": "^4.21.1", "generic-pool": "^3.9.0", "graphql": "^16.3.0", + "hono": "^4.8.12", "http-terminator": "^3.2.0", "ioredis": "^5.4.1", "kafkajs": "2.2.4", diff --git a/dev-packages/node-integration-tests/suites/tracing/hono/instrument.mjs b/dev-packages/node-integration-tests/suites/tracing/hono/instrument.mjs new file mode 100644 index 000000000000..46a27dd03b74 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/hono/instrument.mjs @@ -0,0 +1,9 @@ +import * as Sentry from '@sentry/node'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracesSampleRate: 1.0, + transport: loggingTransport, +}); diff --git a/dev-packages/node-integration-tests/suites/tracing/hono/scenario.mjs b/dev-packages/node-integration-tests/suites/tracing/hono/scenario.mjs new file mode 100644 index 000000000000..125bd8d4daa4 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/hono/scenario.mjs @@ -0,0 +1,163 @@ +import { serve } from '@hono/node-server'; +import * as Sentry from '@sentry/node'; +import { sendPortToRunner } from '@sentry-internal/node-core-integration-tests'; +import { Hono } from 'hono'; +import { HTTPException } from 'hono/http-exception'; + +const app = new Hono(); + +Sentry.setupHonoErrorHandler(app); + +// Global middleware to capture all requests +app.use(async function global(c, next) { + await next(); +}); + +const basePaths = ['/sync', '/async']; +const methods = ['get', 'post', 'put', 'delete', 'patch']; + +basePaths.forEach(basePath => { + // Sub-path middleware to capture all requests under the basePath + app.use(`${basePath}/*`, async function base(c, next) { + await next(); + }); + + const baseApp = new Hono(); + methods.forEach(method => { + baseApp[method]('/', c => { + const response = c.text('response 200'); + if (basePath === '/sync') return response; + return Promise.resolve(response); + }); + + baseApp[method]( + '/middleware', + // anonymous middleware + async (c, next) => { + await next(); + }, + c => { + const response = c.text('response 200'); + if (basePath === '/sync') return response; + return Promise.resolve(response); + }, + ); + + baseApp.all('/all', c => { + const response = c.text('response 200'); + if (basePath === '/sync') return response; + return Promise.resolve(response); + }); + + baseApp.all( + '/all/middleware', + // anonymous middleware + async (c, next) => { + await next(); + }, + c => { + const response = c.text('response 200'); + if (basePath === '/sync') return response; + return Promise.resolve(response); + }, + ); + + baseApp.on(method, '/on', c => { + const response = c.text('response 200'); + if (basePath === '/sync') return response; + return Promise.resolve(response); + }); + + baseApp.on( + method, + '/on/middleware', + // anonymous middleware + async (c, next) => { + await next(); + }, + c => { + const response = c.text('response 200'); + if (basePath === '/sync') return response; + return Promise.resolve(response); + }, + ); + + baseApp[method]('/401', () => { + const response = new HTTPException(401, { message: 'response 401' }); + if (basePath === '/sync') throw response; + return Promise.reject(response); + }); + + baseApp.all('/all/401', () => { + const response = new HTTPException(401, { message: 'response 401' }); + if (basePath === '/sync') throw response; + return Promise.reject(response); + }); + + baseApp.on(method, '/on/401', () => { + const response = new HTTPException(401, { message: 'response 401' }); + if (basePath === '/sync') throw response; + return Promise.reject(response); + }); + + baseApp[method]('/402', () => { + const response = new HTTPException(402, { message: 'response 402' }); + if (basePath === '/sync') throw response; + return Promise.reject(response); + }); + + baseApp.all('/all/402', () => { + const response = new HTTPException(402, { message: 'response 402' }); + if (basePath === '/sync') throw response; + return Promise.reject(response); + }); + + baseApp.on(method, '/on/402', () => { + const response = new HTTPException(402, { message: 'response 402' }); + if (basePath === '/sync') throw response; + return Promise.reject(response); + }); + + baseApp[method]('/403', () => { + const response = new HTTPException(403, { message: 'response 403' }); + if (basePath === '/sync') throw response; + return Promise.reject(response); + }); + + baseApp.all('/all/403', () => { + const response = new HTTPException(403, { message: 'response 403' }); + if (basePath === '/sync') throw response; + return Promise.reject(response); + }); + + baseApp.on(method, '/on/403', () => { + const response = new HTTPException(403, { message: 'response 403' }); + if (basePath === '/sync') throw response; + return Promise.reject(response); + }); + + baseApp[method]('/500', () => { + const response = new HTTPException(500, { message: 'response 500' }); + if (basePath === '/sync') throw response; + return Promise.reject(response); + }); + + baseApp.all('/all/500', () => { + const response = new HTTPException(500, { message: 'response 500' }); + if (basePath === '/sync') throw response; + return Promise.reject(response); + }); + + baseApp.on(method, '/on/500', () => { + const response = new HTTPException(500, { message: 'response 500' }); + if (basePath === '/sync') throw response; + return Promise.reject(response); + }); + }); + + app.route(basePath, baseApp); +}); + +const port = 8787; +serve({ fetch: app.fetch, port }); +sendPortToRunner(port); diff --git a/dev-packages/node-integration-tests/suites/tracing/hono/test.ts b/dev-packages/node-integration-tests/suites/tracing/hono/test.ts new file mode 100644 index 000000000000..8389cb9ee320 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/hono/test.ts @@ -0,0 +1,179 @@ +import { afterAll, describe, expect } from 'vitest'; +import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runner'; + +describe('hono tracing', () => { + afterAll(() => { + cleanupChildProcesses(); + }); + + createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument.mjs', (createRunner, test) => { + describe.each(['/sync', '/async'] as const)('when using %s route', route => { + describe.each(['get', 'post', 'put', 'delete', 'patch'] as const)('when using %s method', method => { + describe.each(['/', '/all', '/on'])('when using %s path', path => { + test('should handle transaction', async () => { + const runner = createRunner() + .expect({ + transaction: { + transaction: `${method.toUpperCase()} ${route}${path === '/' ? '' : path}`, + spans: expect.arrayContaining([ + expect.objectContaining({ + data: expect.objectContaining({ + 'hono.name': 'sentryRequestMiddleware', + 'hono.type': 'middleware', + }), + description: 'sentryRequestMiddleware', + op: 'middleware.hono', + origin: 'auto.http.otel.hono', + }), + expect.objectContaining({ + data: expect.objectContaining({ + 'hono.name': 'sentryErrorMiddleware', + 'hono.type': 'middleware', + }), + description: 'sentryErrorMiddleware', + op: 'middleware.hono', + origin: 'auto.http.otel.hono', + }), + expect.objectContaining({ + data: expect.objectContaining({ + 'hono.name': 'global', + 'hono.type': 'middleware', + }), + description: 'global', + op: 'middleware.hono', + origin: 'auto.http.otel.hono', + }), + expect.objectContaining({ + data: expect.objectContaining({ + 'hono.name': 'base', + 'hono.type': 'middleware', + }), + description: 'base', + op: 'middleware.hono', + origin: 'auto.http.otel.hono', + }), + expect.objectContaining({ + data: expect.objectContaining({ + 'hono.name': `${route}${path === '/' ? '' : path}`, + 'hono.type': 'request_handler', + }), + description: `${route}${path === '/' ? '' : path}`, + op: 'request_handler.hono', + origin: 'auto.http.otel.hono', + }), + ]), + }, + }) + .start(); + runner.makeRequest(method, `${route}${path === '/' ? '' : path}`); + await runner.completed(); + }); + + test('should handle transaction with anonymous middleware', async () => { + const runner = createRunner() + .expect({ + transaction: { + transaction: `${method.toUpperCase()} ${route}${path === '/' ? '' : path}/middleware`, + spans: expect.arrayContaining([ + expect.objectContaining({ + data: expect.objectContaining({ + 'hono.name': 'sentryRequestMiddleware', + 'hono.type': 'middleware', + }), + description: 'sentryRequestMiddleware', + op: 'middleware.hono', + origin: 'auto.http.otel.hono', + }), + expect.objectContaining({ + data: expect.objectContaining({ + 'hono.name': 'sentryErrorMiddleware', + 'hono.type': 'middleware', + }), + description: 'sentryErrorMiddleware', + op: 'middleware.hono', + origin: 'auto.http.otel.hono', + }), + expect.objectContaining({ + data: expect.objectContaining({ + 'hono.name': 'global', + 'hono.type': 'middleware', + }), + description: 'global', + op: 'middleware.hono', + origin: 'auto.http.otel.hono', + }), + expect.objectContaining({ + data: expect.objectContaining({ + 'hono.name': 'base', + 'hono.type': 'middleware', + }), + description: 'base', + op: 'middleware.hono', + origin: 'auto.http.otel.hono', + }), + expect.objectContaining({ + data: expect.objectContaining({ + 'hono.name': 'anonymous', + 'hono.type': 'middleware', + }), + description: 'anonymous', + op: 'middleware.hono', + origin: 'auto.http.otel.hono', + }), + expect.objectContaining({ + data: expect.objectContaining({ + 'hono.name': `${route}${path === '/' ? '' : path}/middleware`, + 'hono.type': 'request_handler', + }), + description: `${route}${path === '/' ? '' : path}/middleware`, + op: 'request_handler.hono', + origin: 'auto.http.otel.hono', + }), + ]), + }, + }) + .start(); + runner.makeRequest(method, `${route}${path === '/' ? '' : path}/middleware`); + await runner.completed(); + }); + + test('should handle returned errors for %s path', async () => { + const runner = createRunner() + .ignore('transaction') + .expect({ + event: { + exception: { + values: [ + { + type: 'Error', + value: 'response 500', + }, + ], + }, + }, + }) + .start(); + runner.makeRequest(method, `${route}${path === '/' ? '' : path}/500`, { expectError: true }); + await runner.completed(); + }); + + test.each(['/401', '/402', '/403', '/does-not-exist'])( + 'should ignores error %s path by default', + async (subPath: string) => { + const runner = createRunner() + .expect({ + transaction: { + transaction: `${method.toUpperCase()} ${route}`, + }, + }) + .start(); + runner.makeRequest(method, `${route}${path === '/' ? '' : path}${subPath}`, { expectError: true }); + runner.makeRequest(method, route); + await runner.completed(); + }, + ); + }); + }); + }); + }); +}); diff --git a/dev-packages/node-integration-tests/utils/runner.ts b/dev-packages/node-integration-tests/utils/runner.ts index dfe27dc5a4a5..b88f65982956 100644 --- a/dev-packages/node-integration-tests/utils/runner.ts +++ b/dev-packages/node-integration-tests/utils/runner.ts @@ -167,7 +167,7 @@ type StartResult = { getLogs(): string[]; getPort(): number | undefined; makeRequest( - method: 'get' | 'post', + method: 'get' | 'post' | 'put' | 'delete' | 'patch', path: string, options?: { headers?: Record; data?: BodyInit; expectError?: boolean }, ): Promise; @@ -655,7 +655,7 @@ export function createRunner(...paths: string[]) { return scenarioServerPort; }, makeRequest: async function ( - method: 'get' | 'post', + method: 'get' | 'post' | 'put' | 'delete' | 'patch', path: string, options: { headers?: Record; data?: BodyInit; expectError?: boolean } = {}, ): Promise { @@ -674,7 +674,7 @@ export function createRunner(...paths: string[]) { if (process.env.DEBUG) log('making request', method, url, headers, body); try { - const res = await fetch(url, { headers, method, body }); + const res = await fetch(url, { headers, method: method.toUpperCase(), body }); if (!res.ok) { if (!expectError) { diff --git a/yarn.lock b/yarn.lock index 46e96703fc7e..1d76ed50b63f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4596,6 +4596,11 @@ "@hapi/bourne" "^3.0.0" "@hapi/hoek" "^11.0.2" +"@hono/node-server@^1.18.2": + version "1.18.2" + resolved "https://registry.yarnpkg.com/@hono/node-server/-/node-server-1.18.2.tgz#a7195b3e956a3a8025b170c69f2e135303dcce2e" + integrity sha512-icgNvC0vRYivzyuSSaUv9ttcwtN8fDyd1k3AOIBDJgYd84tXRZSS6na8X54CY/oYoFTNhEmZraW/Rb9XYwX4KA== + "@humanwhocodes/config-array@^0.5.0": version "0.5.0" resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.5.0.tgz#1407967d4c6eecd7388f83acf1eaf4d0c6e58ef9" @@ -18604,6 +18609,11 @@ homedir-polyfill@^1.0.1: dependencies: parse-passwd "^1.0.0" +hono@^4.8.12: + version "4.8.12" + resolved "https://registry.yarnpkg.com/hono/-/hono-4.8.12.tgz#9f4729f257f00136881a63cc166b29bd5da38944" + integrity sha512-MQSKk1Mg7b74k8l+A025LfysnLtXDKkE4pLaSsYRQC5iy85lgZnuyeQ1Wynair9mmECzoLu+FtJtqNZSoogBDQ== + hookable@^5.5.3: version "5.5.3" resolved "https://registry.yarnpkg.com/hookable/-/hookable-5.5.3.tgz#6cfc358984a1ef991e2518cb9ed4a778bbd3215d" From 8c5a3ed8be1f3749ae01ac80ff91fc07384058ee Mon Sep 17 00:00:00 2001 From: Karibash Date: Sun, 14 Sep 2025 22:54:52 +0900 Subject: [PATCH 07/11] ref(node): Use the isThenable function from @sentry/core to check whether it is a Promise --- .../node/src/integrations/tracing/hono/instrumentation.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/node/src/integrations/tracing/hono/instrumentation.ts b/packages/node/src/integrations/tracing/hono/instrumentation.ts index 644bdb4eb5ff..833144ef9305 100644 --- a/packages/node/src/integrations/tracing/hono/instrumentation.ts +++ b/packages/node/src/integrations/tracing/hono/instrumentation.ts @@ -1,6 +1,7 @@ import type { Span } from '@opentelemetry/api'; import { context, SpanStatusCode, trace } from '@opentelemetry/api'; import { InstrumentationBase, InstrumentationNodeModuleDefinition } from '@opentelemetry/instrumentation'; +import { isThenable } from '@sentry/core'; import { AttributeNames, HonoTypes } from './constants'; import type { Context, @@ -204,12 +205,7 @@ export class HonoInstrumentation extends InstrumentationBase { try { const result = execute(); - if ( - result && - typeof result === 'object' && - typeof Object.getOwnPropertyDescriptor(result, 'then')?.value === 'function' - ) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + if (isThenable(result)) { result.then( () => onSuccess(), (error: unknown) => onFailure(error), From be8604bc6a8519dc33965a2e1648536f976699dd Mon Sep 17 00:00:00 2001 From: Karibash Date: Tue, 16 Sep 2025 12:22:52 +0900 Subject: [PATCH 08/11] bugfix(node): Fix issue where middleware registered alone is treated as a route handler --- .../suites/tracing/hono/scenario.mjs | 33 +++++++ .../suites/tracing/hono/test.ts | 68 +++++++++++++++ .../src/integrations/tracing/hono/index.ts | 77 +++++++++-------- .../tracing/hono/instrumentation.ts | 85 +++++++++++-------- 4 files changed, 190 insertions(+), 73 deletions(-) diff --git a/dev-packages/node-integration-tests/suites/tracing/hono/scenario.mjs b/dev-packages/node-integration-tests/suites/tracing/hono/scenario.mjs index 125bd8d4daa4..7ef73c3d22c3 100644 --- a/dev-packages/node-integration-tests/suites/tracing/hono/scenario.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/hono/scenario.mjs @@ -43,6 +43,17 @@ basePaths.forEach(basePath => { }, ); + // anonymous middleware + baseApp[method]('/middleware/separately', async (c, next) => { + await next(); + }); + + baseApp[method]('/middleware/separately', async c => { + const response = c.text('response 200'); + if (basePath === '/sync') return response; + return Promise.resolve(response); + }); + baseApp.all('/all', c => { const response = c.text('response 200'); if (basePath === '/sync') return response; @@ -62,6 +73,17 @@ basePaths.forEach(basePath => { }, ); + // anonymous middleware + baseApp.all('/all/middleware/separately', async (c, next) => { + await next(); + }); + + baseApp.all('/all/middleware/separately', async c => { + const response = c.text('response 200'); + if (basePath === '/sync') return response; + return Promise.resolve(response); + }); + baseApp.on(method, '/on', c => { const response = c.text('response 200'); if (basePath === '/sync') return response; @@ -82,6 +104,17 @@ basePaths.forEach(basePath => { }, ); + // anonymous middleware + baseApp.on(method, '/on/middleware/separately', async (c, next) => { + await next(); + }); + + baseApp.on(method, '/on/middleware/separately', async c => { + const response = c.text('response 200'); + if (basePath === '/sync') return response; + return Promise.resolve(response); + }); + baseApp[method]('/401', () => { const response = new HTTPException(401, { message: 'response 401' }); if (basePath === '/sync') throw response; diff --git a/dev-packages/node-integration-tests/suites/tracing/hono/test.ts b/dev-packages/node-integration-tests/suites/tracing/hono/test.ts index 8389cb9ee320..c92a69e5bc84 100644 --- a/dev-packages/node-integration-tests/suites/tracing/hono/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/hono/test.ts @@ -137,6 +137,74 @@ describe('hono tracing', () => { await runner.completed(); }); + test('should handle transaction with separate middleware', async () => { + const runner = createRunner() + .expect({ + transaction: { + transaction: `${method.toUpperCase()} ${route}${path === '/' ? '' : path}/middleware/separately`, + spans: expect.arrayContaining([ + expect.objectContaining({ + data: expect.objectContaining({ + 'hono.name': 'sentryRequestMiddleware', + 'hono.type': 'middleware', + }), + description: 'sentryRequestMiddleware', + op: 'middleware.hono', + origin: 'auto.http.otel.hono', + }), + expect.objectContaining({ + data: expect.objectContaining({ + 'hono.name': 'sentryErrorMiddleware', + 'hono.type': 'middleware', + }), + description: 'sentryErrorMiddleware', + op: 'middleware.hono', + origin: 'auto.http.otel.hono', + }), + expect.objectContaining({ + data: expect.objectContaining({ + 'hono.name': 'global', + 'hono.type': 'middleware', + }), + description: 'global', + op: 'middleware.hono', + origin: 'auto.http.otel.hono', + }), + expect.objectContaining({ + data: expect.objectContaining({ + 'hono.name': 'base', + 'hono.type': 'middleware', + }), + description: 'base', + op: 'middleware.hono', + origin: 'auto.http.otel.hono', + }), + expect.objectContaining({ + data: expect.objectContaining({ + 'hono.name': 'anonymous', + 'hono.type': 'middleware', + }), + description: 'anonymous', + op: 'middleware.hono', + origin: 'auto.http.otel.hono', + }), + expect.objectContaining({ + data: expect.objectContaining({ + 'hono.name': `${route}${path === '/' ? '' : path}/middleware/separately`, + 'hono.type': 'request_handler', + }), + description: `${route}${path === '/' ? '' : path}/middleware/separately`, + op: 'request_handler.hono', + origin: 'auto.http.otel.hono', + }), + ]), + }, + }) + .start(); + runner.makeRequest(method, `${route}${path === '/' ? '' : path}/middleware/separately`); + await runner.completed(); + }); + test('should handle returned errors for %s path', async () => { const runner = createRunner() .ignore('transaction') diff --git a/packages/node/src/integrations/tracing/hono/index.ts b/packages/node/src/integrations/tracing/hono/index.ts index e34f297bc8a6..bf4f1313a39f 100644 --- a/packages/node/src/integrations/tracing/hono/index.ts +++ b/packages/node/src/integrations/tracing/hono/index.ts @@ -4,7 +4,6 @@ import { captureException, debug, defineIntegration, - getClient, getDefaultIsolationScope, getIsolationScope, httpRequestToRequestData, @@ -20,7 +19,44 @@ import type { Context, MiddlewareHandler, MiddlewareHandlerInterface, Next } fro const INTEGRATION_NAME = 'Hono'; -export const instrumentHono = generateInstrumentOnce(INTEGRATION_NAME, () => new HonoInstrumentation()); +function addHonoSpanAttributes(span: Span): void { + const attributes = spanToJSON(span).data; + const type = attributes[AttributeNames.HONO_TYPE]; + if (attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP] || !type) { + return; + } + + span.setAttributes({ + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.otel.hono', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: `${type}.hono`, + }); + + const name = attributes[AttributeNames.HONO_NAME]; + if (typeof name === 'string') { + span.updateName(name); + } + + if (getIsolationScope() === getDefaultIsolationScope()) { + DEBUG_BUILD && debug.warn('Isolation scope is default isolation scope - skipping setting transactionName'); + return; + } + + const route = attributes[ATTR_HTTP_ROUTE]; + const method = attributes[ATTR_HTTP_REQUEST_METHOD]; + if (typeof route === 'string' && typeof method === 'string') { + getIsolationScope().setTransactionName(`${method} ${route}`); + } +} + +export const instrumentHono = generateInstrumentOnce( + INTEGRATION_NAME, + () => + new HonoInstrumentation({ + responseHook: span => { + addHonoSpanAttributes(span); + }, + }), +); const _honoIntegration = (() => { return { @@ -86,35 +122,6 @@ function honoErrorHandler(options?: Partial): MiddlewareHand }; } -function addHonoSpanAttributes(span: Span): void { - const attributes = spanToJSON(span).data; - const type = attributes[AttributeNames.HONO_TYPE]; - if (attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP] || !type) { - return; - } - - span.setAttributes({ - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.otel.hono', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: `${type}.hono`, - }); - - const name = attributes[AttributeNames.HONO_NAME]; - if (typeof name === 'string') { - span.updateName(name); - } - - if (getIsolationScope() === getDefaultIsolationScope()) { - DEBUG_BUILD && debug.warn('Isolation scope is default isolation scope - skipping setting transactionName'); - return; - } - - const route = attributes[ATTR_HTTP_ROUTE]; - const method = attributes[ATTR_HTTP_REQUEST_METHOD]; - if (typeof route === 'string' && typeof method === 'string') { - getIsolationScope().setTransactionName(`${method} ${route}`); - } -} - /** * Add a Hono error handler to capture errors to Sentry. * @@ -139,13 +146,5 @@ export function setupHonoErrorHandler( ): void { app.use(honoRequestHandler()); app.use(honoErrorHandler(options)); - - const client = getClient(); - if (client) { - client.on('spanStart', span => { - addHonoSpanAttributes(span); - }); - } - ensureIsWrapped(app.use, 'hono'); } diff --git a/packages/node/src/integrations/tracing/hono/instrumentation.ts b/packages/node/src/integrations/tracing/hono/instrumentation.ts index 833144ef9305..9a55eaac2776 100644 --- a/packages/node/src/integrations/tracing/hono/instrumentation.ts +++ b/packages/node/src/integrations/tracing/hono/instrumentation.ts @@ -1,5 +1,6 @@ import type { Span } from '@opentelemetry/api'; import { context, SpanStatusCode, trace } from '@opentelemetry/api'; +import type { InstrumentationConfig } from '@opentelemetry/instrumentation'; import { InstrumentationBase, InstrumentationNodeModuleDefinition } from '@opentelemetry/instrumentation'; import { isThenable } from '@sentry/core'; import { AttributeNames, HonoTypes } from './constants'; @@ -18,12 +19,21 @@ import type { const PACKAGE_NAME = '@sentry/instrumentation-hono'; const PACKAGE_VERSION = '0.0.1'; +export interface HonoResponseHookFunction { + (span: Span): void; +} + +export interface HonoInstrumentationConfig extends InstrumentationConfig { + /** Function for adding custom span attributes from the response */ + responseHook?: HonoResponseHookFunction; +} + /** * Hono instrumentation for OpenTelemetry */ -export class HonoInstrumentation extends InstrumentationBase { - public constructor() { - super(PACKAGE_NAME, PACKAGE_VERSION, {}); +export class HonoInstrumentation extends InstrumentationBase { + public constructor(config: HonoInstrumentationConfig = {}) { + super(PACKAGE_NAME, PACKAGE_VERSION, config); } /** @@ -86,23 +96,13 @@ export class HonoInstrumentation extends InstrumentationBase { const handlers = args.slice(1); return original.apply(this, [ path, - ...handlers.map((handler, index) => - instrumentation._wrapHandler( - index + 1 === handlers.length ? HonoTypes.REQUEST_HANDLER : HonoTypes.MIDDLEWARE, - handler as Handler | MiddlewareHandler, - ), - ), + ...handlers.map(handler => instrumentation._wrapHandler(handler as Handler | MiddlewareHandler)), ]); } return original.apply( this, - args.map((handler, index) => - instrumentation._wrapHandler( - index + 1 === args.length ? HonoTypes.REQUEST_HANDLER : HonoTypes.MIDDLEWARE, - handler as Handler | MiddlewareHandler, - ), - ), + args.map(handler => instrumentation._wrapHandler(handler as Handler | MiddlewareHandler)), ); }; }; @@ -120,12 +120,7 @@ export class HonoInstrumentation extends InstrumentationBase { const handlers = args.slice(2); return original.apply(this, [ ...args.slice(0, 2), - ...handlers.map((handler, index) => - instrumentation._wrapHandler( - index + 1 === handlers.length ? HonoTypes.REQUEST_HANDLER : HonoTypes.MIDDLEWARE, - handler as Handler | MiddlewareHandler, - ), - ), + ...handlers.map(handler => instrumentation._wrapHandler(handler as Handler | MiddlewareHandler)), ]); }; }; @@ -149,15 +144,13 @@ export class HonoInstrumentation extends InstrumentationBase { const handlers = args.slice(1); return original.apply(this, [ path, - ...handlers.map(handler => - instrumentation._wrapHandler(HonoTypes.MIDDLEWARE, handler as MiddlewareHandler), - ), + ...handlers.map(handler => instrumentation._wrapHandler(handler as MiddlewareHandler)), ]); } return original.apply( this, - args.map(handler => instrumentation._wrapHandler(HonoTypes.MIDDLEWARE, handler as MiddlewareHandler)), + args.map(handler => instrumentation._wrapHandler(handler as MiddlewareHandler)), ); }; }; @@ -166,7 +159,7 @@ export class HonoInstrumentation extends InstrumentationBase { /** * Wraps a handler or middleware handler to apply instrumentation. */ - private _wrapHandler(type: HonoTypes, handler: Handler | MiddlewareHandler): Handler | MiddlewareHandler { + private _wrapHandler(handler: Handler | MiddlewareHandler): Handler | MiddlewareHandler { // eslint-disable-next-line @typescript-eslint/no-this-alias const instrumentation = this; @@ -176,17 +169,32 @@ export class HonoInstrumentation extends InstrumentationBase { } const path = c.req.path; - const spanName = `${type.replace('_', ' ')} - ${path}`; - const span = instrumentation.tracer.startSpan(spanName, { - attributes: { - [AttributeNames.HONO_TYPE]: type, - [AttributeNames.HONO_NAME]: type === 'request_handler' ? path : handler.name || 'anonymous', - }, - }); + const span = instrumentation.tracer.startSpan(path); return context.with(trace.setSpan(context.active(), span), () => { return instrumentation._safeExecute( - () => handler.apply(this, [c, next]), + () => { + const result = handler.apply(this, [c, next]); + if (isThenable(result)) { + return result.then(result => { + const type = instrumentation._determineHandlerType(result); + span.setAttributes({ + [AttributeNames.HONO_TYPE]: type, + [AttributeNames.HONO_NAME]: type === HonoTypes.REQUEST_HANDLER ? path : handler.name || 'anonymous', + }); + instrumentation.getConfig().responseHook?.(span); + return result; + }); + } else { + const type = instrumentation._determineHandlerType(result); + span.setAttributes({ + [AttributeNames.HONO_TYPE]: type, + [AttributeNames.HONO_NAME]: type === HonoTypes.REQUEST_HANDLER ? path : handler.name || 'anonymous', + }); + instrumentation.getConfig().responseHook?.(span); + return result; + } + }, () => span.end(), error => { instrumentation._handleError(span, error); @@ -221,6 +229,15 @@ export class HonoInstrumentation extends InstrumentationBase { } } + /** + * Determines the handler type based on the result. + * @param result + * @private + */ + private _determineHandlerType(result: unknown): HonoTypes { + return result === undefined ? HonoTypes.MIDDLEWARE : HonoTypes.REQUEST_HANDLER; + } + /** * Handles errors by setting the span status and recording the exception. */ From 17c2e5a7083ef434f0e4eabe9a8db989e5e942e7 Mon Sep 17 00:00:00 2001 From: Karibash Date: Tue, 16 Sep 2025 12:26:00 +0900 Subject: [PATCH 09/11] chore: Fix missing exports --- packages/astro/src/index.server.ts | 2 ++ packages/aws-serverless/src/index.ts | 2 ++ packages/bun/src/index.ts | 2 ++ packages/google-cloud-serverless/src/index.ts | 2 ++ 4 files changed, 8 insertions(+) diff --git a/packages/astro/src/index.server.ts b/packages/astro/src/index.server.ts index de4079c4b5c4..d39cb5e4484d 100644 --- a/packages/astro/src/index.server.ts +++ b/packages/astro/src/index.server.ts @@ -64,6 +64,7 @@ export { winterCGHeadersToDict, graphqlIntegration, hapiIntegration, + honoIntegration, httpIntegration, // eslint-disable-next-line deprecation/deprecation inboundFiltersIntegration, @@ -116,6 +117,7 @@ export { setupConnectErrorHandler, setupExpressErrorHandler, setupHapiErrorHandler, + setupHonoErrorHandler, setupKoaErrorHandler, setUser, spanToBaggageHeader, diff --git a/packages/aws-serverless/src/index.ts b/packages/aws-serverless/src/index.ts index 0cbe5879b02e..cfab7b72754b 100644 --- a/packages/aws-serverless/src/index.ts +++ b/packages/aws-serverless/src/index.ts @@ -111,6 +111,8 @@ export { createSentryWinstonTransport, hapiIntegration, setupHapiErrorHandler, + honoIntegration, + setupHonoErrorHandler, spotlightIntegration, initOpenTelemetry, spanToJSON, diff --git a/packages/bun/src/index.ts b/packages/bun/src/index.ts index b1c4854e5026..68a1e2b6d6ff 100644 --- a/packages/bun/src/index.ts +++ b/packages/bun/src/index.ts @@ -130,6 +130,8 @@ export { prismaIntegration, hapiIntegration, setupHapiErrorHandler, + honoIntegration, + setupHonoErrorHandler, spotlightIntegration, initOpenTelemetry, spanToJSON, diff --git a/packages/google-cloud-serverless/src/index.ts b/packages/google-cloud-serverless/src/index.ts index fc0fe353b919..ac0f41079017 100644 --- a/packages/google-cloud-serverless/src/index.ts +++ b/packages/google-cloud-serverless/src/index.ts @@ -109,6 +109,8 @@ export { prismaIntegration, hapiIntegration, setupHapiErrorHandler, + honoIntegration, + setupHonoErrorHandler, spotlightIntegration, initOpenTelemetry, spanToJSON, From dbb1413f293084d88e3037fa1482a45af4ca0be5 Mon Sep 17 00:00:00 2001 From: Karibash Date: Tue, 23 Sep 2025 17:14:16 +0900 Subject: [PATCH 10/11] chore(deps): Update Hono to the latest version --- dev-packages/node-integration-tests/package.json | 4 ++-- yarn.lock | 16 ++++++++-------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/dev-packages/node-integration-tests/package.json b/dev-packages/node-integration-tests/package.json index 601bc12ccb74..15a3e9beda35 100644 --- a/dev-packages/node-integration-tests/package.json +++ b/dev-packages/node-integration-tests/package.json @@ -27,7 +27,7 @@ "@aws-sdk/client-s3": "^3.552.0", "@google/genai": "^1.20.0", "@hapi/hapi": "^21.3.10", - "@hono/node-server": "^1.18.2", + "@hono/node-server": "^1.19.4", "@nestjs/common": "11.1.3", "@nestjs/core": "11.1.3", "@nestjs/platform-express": "11.1.3", @@ -50,7 +50,7 @@ "express": "^4.21.1", "generic-pool": "^3.9.0", "graphql": "^16.3.0", - "hono": "^4.8.12", + "hono": "^4.9.8", "http-terminator": "^3.2.0", "ioredis": "^5.4.1", "kafkajs": "2.2.4", diff --git a/yarn.lock b/yarn.lock index 1d76ed50b63f..b2f40a0bd2c1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4596,10 +4596,10 @@ "@hapi/bourne" "^3.0.0" "@hapi/hoek" "^11.0.2" -"@hono/node-server@^1.18.2": - version "1.18.2" - resolved "https://registry.yarnpkg.com/@hono/node-server/-/node-server-1.18.2.tgz#a7195b3e956a3a8025b170c69f2e135303dcce2e" - integrity sha512-icgNvC0vRYivzyuSSaUv9ttcwtN8fDyd1k3AOIBDJgYd84tXRZSS6na8X54CY/oYoFTNhEmZraW/Rb9XYwX4KA== +"@hono/node-server@^1.19.4": + version "1.19.4" + resolved "https://registry.yarnpkg.com/@hono/node-server/-/node-server-1.19.4.tgz#2721cda094f7c080ee985494ac3e074f16c503eb" + integrity sha512-AWKQZ/YkHUBSHeL/5Ld8FWgUs6wFf4TxGYxqp9wLZxRdFuHBpXmgOq+CuDoL4vllkZLzovCf5HBJnypiy3EtHA== "@humanwhocodes/config-array@^0.5.0": version "0.5.0" @@ -18609,10 +18609,10 @@ homedir-polyfill@^1.0.1: dependencies: parse-passwd "^1.0.0" -hono@^4.8.12: - version "4.8.12" - resolved "https://registry.yarnpkg.com/hono/-/hono-4.8.12.tgz#9f4729f257f00136881a63cc166b29bd5da38944" - integrity sha512-MQSKk1Mg7b74k8l+A025LfysnLtXDKkE4pLaSsYRQC5iy85lgZnuyeQ1Wynair9mmECzoLu+FtJtqNZSoogBDQ== +hono@^4.9.8: + version "4.9.8" + resolved "https://registry.yarnpkg.com/hono/-/hono-4.9.8.tgz#1710981135ec775fe26fab5ea6535b403e92bcc3" + integrity sha512-JW8Bb4RFWD9iOKxg5PbUarBYGM99IcxFl2FPBo2gSJO11jjUDqlP1Bmfyqt8Z/dGhIQ63PMA9LdcLefXyIasyg== hookable@^5.5.3: version "5.5.3" From 3c89525c22807e35d64f94d8d57136f01b768052 Mon Sep 17 00:00:00 2001 From: Karibash Date: Tue, 23 Sep 2025 17:37:09 +0900 Subject: [PATCH 11/11] ref(node): Adjust ErrorHandler event mechanism --- .../node-integration-tests/suites/tracing/hono/test.ts | 4 ++++ packages/node/src/integrations/tracing/hono/index.ts | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/dev-packages/node-integration-tests/suites/tracing/hono/test.ts b/dev-packages/node-integration-tests/suites/tracing/hono/test.ts index c92a69e5bc84..67d0ff8b56fb 100644 --- a/dev-packages/node-integration-tests/suites/tracing/hono/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/hono/test.ts @@ -213,6 +213,10 @@ describe('hono tracing', () => { exception: { values: [ { + mechanism: { + type: 'auto.middleware.hono', + handled: false, + }, type: 'Error', value: 'response 500', }, diff --git a/packages/node/src/integrations/tracing/hono/index.ts b/packages/node/src/integrations/tracing/hono/index.ts index bf4f1313a39f..4249871b8acf 100644 --- a/packages/node/src/integrations/tracing/hono/index.ts +++ b/packages/node/src/integrations/tracing/hono/index.ts @@ -114,7 +114,7 @@ function honoErrorHandler(options?: Partial): MiddlewareHand if (shouldHandleError(context)) { (context.res as { sentry?: string }).sentry = captureException(context.error, { mechanism: { - type: 'hono', + type: 'auto.middleware.hono', handled: false, }, });