From abeef91b8fb3abd865412b39258c082e6b781480 Mon Sep 17 00:00:00 2001 From: s1gr1d <32902192+s1gr1d@users.noreply.github.com> Date: Fri, 27 Feb 2026 14:00:57 +0100 Subject: [PATCH 01/11] add mechanism (wip) --- .../cloudflare-integration-tests/expect.ts | 1 + .../suites/hono-sdk/index.ts | 6 +++--- .../suites/hono-sdk/test.ts | 14 +++++++++----- packages/hono/src/shared/middlewareHandlers.ts | 4 +++- 4 files changed, 16 insertions(+), 9 deletions(-) diff --git a/dev-packages/cloudflare-integration-tests/expect.ts b/dev-packages/cloudflare-integration-tests/expect.ts index b33926ffce11..7a7403d87771 100644 --- a/dev-packages/cloudflare-integration-tests/expect.ts +++ b/dev-packages/cloudflare-integration-tests/expect.ts @@ -62,6 +62,7 @@ export function eventEnvelope( event: Event, { includeSampleRand = false, sdk = 'cloudflare' }: { includeSampleRand?: boolean; sdk?: 'cloudflare' | 'hono' } = {}, ): Envelope { + console.log('version::', SDK_VERSION); return [ { event_id: UUID_MATCHER, diff --git a/dev-packages/cloudflare-integration-tests/suites/hono-sdk/index.ts b/dev-packages/cloudflare-integration-tests/suites/hono-sdk/index.ts index 63464d4e2237..23bac960b05d 100644 --- a/dev-packages/cloudflare-integration-tests/suites/hono-sdk/index.ts +++ b/dev-packages/cloudflare-integration-tests/suites/hono-sdk/index.ts @@ -13,8 +13,8 @@ app.use( dsn: process.env.SENTRY_DSN, tracesSampleRate: 1.0, debug: true, - // fixme - check out what removing this integration changes - // integrations: integrations => integrations.filter(integration => integration.name !== 'Hono'), + // check out what removing this integration changes + integrations: (integrations: unknown[]) => integrations.filter(integration => integration?.name !== 'Hono'), }), ); @@ -26,7 +26,7 @@ app.get('/json', c => { return c.json({ message: 'Hello from Hono', framework: 'hono', platform: 'cloudflare' }); }); -app.get('/error', () => { +app.get('/error/:param', () => { throw new Error('Test error from Hono app'); }); diff --git a/dev-packages/cloudflare-integration-tests/suites/hono-sdk/test.ts b/dev-packages/cloudflare-integration-tests/suites/hono-sdk/test.ts index 9c1f3cda8d66..0a229458495f 100644 --- a/dev-packages/cloudflare-integration-tests/suites/hono-sdk/test.ts +++ b/dev-packages/cloudflare-integration-tests/suites/hono-sdk/test.ts @@ -8,7 +8,7 @@ it('Hono app captures errors (Hono SDK)', async ({ signal }) => { eventEnvelope( { level: 'error', - transaction: 'GET /error', + transaction: 'GET /error/:param', exception: { values: [ { @@ -24,12 +24,16 @@ it('Hono app captures errors (Hono SDK)', async ({ signal }) => { request: { headers: expect.any(Object), method: 'GET', - url: expect.any(String), + url: 'http://localhost:8787/error/param-123', }, }, { includeSampleRand: true, sdk: 'hono' }, ), ) + .expect(envelope => { + console.log('event::', JSON.stringify(envelope, null, 2)); + }) + .expect(envelope => { const [, envelopeItems] = envelope; const [itemHeader, itemPayload] = envelopeItems[0]; @@ -39,7 +43,7 @@ it('Hono app captures errors (Hono SDK)', async ({ signal }) => { expect(itemPayload).toMatchObject({ type: 'transaction', platform: 'javascript', - transaction: 'GET /error', + transaction: 'GET /error/:param', contexts: { trace: { span_id: expect.any(String), @@ -51,7 +55,7 @@ it('Hono app captures errors (Hono SDK)', async ({ signal }) => { }, request: expect.objectContaining({ method: 'GET', - url: expect.stringContaining('/error'), + url: expect.stringContaining('/error/param-123'), }), }); }) @@ -59,7 +63,7 @@ it('Hono app captures errors (Hono SDK)', async ({ signal }) => { .unordered() .start(signal); - await runner.makeRequest('get', '/error', { expectError: true }); + await runner.makeRequest('get', '/error/param-123', { expectError: true }); await runner.completed(); }); diff --git a/packages/hono/src/shared/middlewareHandlers.ts b/packages/hono/src/shared/middlewareHandlers.ts index 6edc58eb9939..9745bcfa3988 100644 --- a/packages/hono/src/shared/middlewareHandlers.ts +++ b/packages/hono/src/shared/middlewareHandlers.ts @@ -38,6 +38,8 @@ export function responseHandler(context: Context): void { getIsolationScope().setTransactionName(`${context.req.method} ${routePath(context)}`); if (context.error) { - getClient()?.captureException(context.error); + getClient()?.captureException(context.error, { + mechanism: { handled: false, type: 'auto.faas.hono.error_handler' }, + }); } } From e4a35b8dd01edf71cedc9075fa7f123662f51b01 Mon Sep 17 00:00:00 2001 From: s1gr1d <32902192+s1gr1d@users.noreply.github.com> Date: Mon, 2 Mar 2026 11:00:34 +0100 Subject: [PATCH 02/11] feat(hono): Use parametrized names for errors --- .../suites/hono-sdk/index.ts | 3 - .../suites/hono-sdk/test.ts | 32 +++++++-- packages/hono/src/cloudflare/middleware.ts | 20 +++++- .../hono/test/cloudflare/middleware.test.ts | 66 +++++++++++++++++++ 4 files changed, 110 insertions(+), 11 deletions(-) diff --git a/dev-packages/cloudflare-integration-tests/suites/hono-sdk/index.ts b/dev-packages/cloudflare-integration-tests/suites/hono-sdk/index.ts index 23bac960b05d..27dfdafbc7a8 100644 --- a/dev-packages/cloudflare-integration-tests/suites/hono-sdk/index.ts +++ b/dev-packages/cloudflare-integration-tests/suites/hono-sdk/index.ts @@ -12,9 +12,6 @@ app.use( sentry(app, { dsn: process.env.SENTRY_DSN, tracesSampleRate: 1.0, - debug: true, - // check out what removing this integration changes - integrations: (integrations: unknown[]) => integrations.filter(integration => integration?.name !== 'Hono'), }), ); diff --git a/dev-packages/cloudflare-integration-tests/suites/hono-sdk/test.ts b/dev-packages/cloudflare-integration-tests/suites/hono-sdk/test.ts index 0a229458495f..4f8472ee8164 100644 --- a/dev-packages/cloudflare-integration-tests/suites/hono-sdk/test.ts +++ b/dev-packages/cloudflare-integration-tests/suites/hono-sdk/test.ts @@ -2,7 +2,7 @@ import { expect, it } from 'vitest'; import { eventEnvelope, SHORT_UUID_MATCHER, UUID_MATCHER } from '../../expect'; import { createRunner } from '../../runner'; -it('Hono app captures errors (Hono SDK)', async ({ signal }) => { +it('Hono app captures parametrized errors (Hono SDK)', async ({ signal }) => { const runner = createRunner(__dirname) .expect( eventEnvelope( @@ -24,15 +24,24 @@ it('Hono app captures errors (Hono SDK)', async ({ signal }) => { request: { headers: expect.any(Object), method: 'GET', - url: 'http://localhost:8787/error/param-123', + url: expect.stringContaining('/error/param-123'), }, + breadcrumbs: [ + { + timestamp: expect.any(Number), + category: 'console', + level: 'error', + message: 'Error: Test error from Hono app', + data: expect.objectContaining({ + logger: 'console', + arguments: [{ message: 'Test error from Hono app', name: 'Error', stack: expect.any(String) }], + }), + }, + ], }, { includeSampleRand: true, sdk: 'hono' }, ), ) - .expect(envelope => { - console.log('event::', JSON.stringify(envelope, null, 2)); - }) .expect(envelope => { const [, envelopeItems] = envelope; @@ -57,9 +66,20 @@ it('Hono app captures errors (Hono SDK)', async ({ signal }) => { method: 'GET', url: expect.stringContaining('/error/param-123'), }), + breadcrumbs: [ + { + timestamp: expect.any(Number), + category: 'console', + level: 'error', + message: 'Error: Test error from Hono app', + data: expect.objectContaining({ + logger: 'console', + arguments: [{ message: 'Test error from Hono app', name: 'Error', stack: expect.any(String) }], + }), + }, + ], }); }) - .unordered() .start(signal); diff --git a/packages/hono/src/cloudflare/middleware.ts b/packages/hono/src/cloudflare/middleware.ts index 43f229a9a5f1..5d8bbce2aab3 100644 --- a/packages/hono/src/cloudflare/middleware.ts +++ b/packages/hono/src/cloudflare/middleware.ts @@ -1,5 +1,5 @@ import { withSentry } from '@sentry/cloudflare'; -import { applySdkMetadata, type BaseTransportOptions, debug, type Options } from '@sentry/core'; +import { applySdkMetadata, type BaseTransportOptions, debug, type Integration, type Options } from '@sentry/core'; import type { Context, Hono, MiddlewareHandler } from 'hono'; import { requestHandler, responseHandler } from '../shared/middlewareHandlers'; @@ -7,13 +7,29 @@ export interface HonoOptions extends Options { context?: Context; } +const filterHonoIntegration = (integration: Integration): boolean => integration.name !== 'Hono'; + export const sentry = (app: Hono, options: HonoOptions | undefined = {}): MiddlewareHandler => { const isDebug = options.debug; isDebug && debug.log('Initialized Sentry Hono middleware (Cloudflare)'); applySdkMetadata(options, 'hono'); - withSentry(() => options, app); + + const { integrations: userIntegrations } = options; + withSentry( + () => ({ + ...options, + // Always filter out the Hono integration from user-provided integrations (or when nothing is specified). + // The Hono integration is already set up by withSentry, so adding it again would cause double-capturing (and non-parametrized URLs). + integrations: Array.isArray(userIntegrations) + ? userIntegrations.filter(filterHonoIntegration) + : typeof userIntegrations === 'function' + ? (defaults: Integration[]) => userIntegrations(defaults).filter(filterHonoIntegration) + : (defaults: Integration[]) => defaults.filter(filterHonoIntegration), + }), + app, + ); return async (context, next) => { requestHandler(context); diff --git a/packages/hono/test/cloudflare/middleware.test.ts b/packages/hono/test/cloudflare/middleware.test.ts index dff1d154dd16..d720a7cafd71 100644 --- a/packages/hono/test/cloudflare/middleware.test.ts +++ b/packages/hono/test/cloudflare/middleware.test.ts @@ -125,4 +125,70 @@ describe('Hono Cloudflare Middleware', () => { expect(middleware.constructor.name).toBe('AsyncFunction'); }); }); + + describe('filters Hono integration from user-provided integrations', () => { + const honoIntegration = { name: 'Hono' } as SentryCore.Integration; + const otherIntegration = { name: 'Other' } as SentryCore.Integration; + + const getIntegrationsResult = (): SentryCore.Integration[] => { + const optionsCallback = withSentryMock.mock.calls[0]?.[0]; + return optionsCallback().integrations; + }; + + it.each([ + ['filters Hono integration out', [honoIntegration, otherIntegration], [otherIntegration]], + ['keeps non-Hono integrations', [otherIntegration], [otherIntegration]], + ['returns empty array when only Hono integration provided', [honoIntegration], []], + ])('%s (array)', (_name, input, expected) => { + const app = new Hono(); + sentry(app, { integrations: input }); + + expect(getIntegrationsResult()).toEqual(expected); + }); + + it('filters Hono integration out of a function result', () => { + const app = new Hono(); + sentry(app, { integrations: () => [honoIntegration, otherIntegration] }); + + const integrationsFn = getIntegrationsResult() as unknown as ( + defaults: SentryCore.Integration[], + ) => SentryCore.Integration[]; + expect(integrationsFn([])).toEqual([otherIntegration]); + }); + + it('passes defaults through to the user-provided integrations function', () => { + const app = new Hono(); + const userFn = vi.fn((_defaults: SentryCore.Integration[]) => [otherIntegration]); + const defaults = [{ name: 'Default' } as SentryCore.Integration]; + + sentry(app, { integrations: userFn }); + + const integrationsFn = getIntegrationsResult() as unknown as ( + defaults: SentryCore.Integration[], + ) => SentryCore.Integration[]; + integrationsFn(defaults); + + expect(userFn).toHaveBeenCalledWith(defaults); + }); + + it('filters Hono integration returned by the user-provided integrations function', () => { + const app = new Hono(); + sentry(app, { integrations: (_defaults: SentryCore.Integration[]) => [honoIntegration] }); + + const integrationsFn = getIntegrationsResult() as unknown as ( + defaults: SentryCore.Integration[], + ) => SentryCore.Integration[]; + expect(integrationsFn([])).toEqual([]); + }); + + it('filters Hono integration from defaults when integrations is undefined', () => { + const app = new Hono(); + sentry(app, {}); + + const integrationsFn = getIntegrationsResult() as unknown as ( + defaults: SentryCore.Integration[], + ) => SentryCore.Integration[]; + expect(integrationsFn([honoIntegration, otherIntegration])).toEqual([otherIntegration]); + }); + }); }); From 4f0e13fd975fb74c24752b322653ea8020c4a1a0 Mon Sep 17 00:00:00 2001 From: s1gr1d <32902192+s1gr1d@users.noreply.github.com> Date: Mon, 2 Mar 2026 11:03:49 +0100 Subject: [PATCH 03/11] remove console log --- dev-packages/cloudflare-integration-tests/expect.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/dev-packages/cloudflare-integration-tests/expect.ts b/dev-packages/cloudflare-integration-tests/expect.ts index 7a7403d87771..b33926ffce11 100644 --- a/dev-packages/cloudflare-integration-tests/expect.ts +++ b/dev-packages/cloudflare-integration-tests/expect.ts @@ -62,7 +62,6 @@ export function eventEnvelope( event: Event, { includeSampleRand = false, sdk = 'cloudflare' }: { includeSampleRand?: boolean; sdk?: 'cloudflare' | 'hono' } = {}, ): Envelope { - console.log('version::', SDK_VERSION); return [ { event_id: UUID_MATCHER, From 4b014ce2cefd0f676ccbaf6ec10c02997f50daae Mon Sep 17 00:00:00 2001 From: s1gr1d <32902192+s1gr1d@users.noreply.github.com> Date: Mon, 2 Mar 2026 13:30:28 +0100 Subject: [PATCH 04/11] fix default integrations case --- packages/hono/src/cloudflare/middleware.ts | 6 +++--- packages/hono/test/cloudflare/middleware.test.ts | 15 +++++++++++++-- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/packages/hono/src/cloudflare/middleware.ts b/packages/hono/src/cloudflare/middleware.ts index 5d8bbce2aab3..cfc0a7e6dccd 100644 --- a/packages/hono/src/cloudflare/middleware.ts +++ b/packages/hono/src/cloudflare/middleware.ts @@ -23,10 +23,10 @@ export const sentry = (app: Hono, options: HonoOptions | undefined = {}): Middle // Always filter out the Hono integration from user-provided integrations (or when nothing is specified). // The Hono integration is already set up by withSentry, so adding it again would cause double-capturing (and non-parametrized URLs). integrations: Array.isArray(userIntegrations) - ? userIntegrations.filter(filterHonoIntegration) + ? defaults => [...defaults.filter(filterHonoIntegration), ...userIntegrations.filter(filterHonoIntegration)] : typeof userIntegrations === 'function' - ? (defaults: Integration[]) => userIntegrations(defaults).filter(filterHonoIntegration) - : (defaults: Integration[]) => defaults.filter(filterHonoIntegration), + ? defaults => userIntegrations(defaults).filter(filterHonoIntegration) + : defaults => defaults.filter(filterHonoIntegration), }), app, ); diff --git a/packages/hono/test/cloudflare/middleware.test.ts b/packages/hono/test/cloudflare/middleware.test.ts index d720a7cafd71..07fb79f8f859 100644 --- a/packages/hono/test/cloudflare/middleware.test.ts +++ b/packages/hono/test/cloudflare/middleware.test.ts @@ -130,7 +130,7 @@ describe('Hono Cloudflare Middleware', () => { const honoIntegration = { name: 'Hono' } as SentryCore.Integration; const otherIntegration = { name: 'Other' } as SentryCore.Integration; - const getIntegrationsResult = (): SentryCore.Integration[] => { + const getIntegrationsResult = () => { const optionsCallback = withSentryMock.mock.calls[0]?.[0]; return optionsCallback().integrations; }; @@ -143,7 +143,18 @@ describe('Hono Cloudflare Middleware', () => { const app = new Hono(); sentry(app, { integrations: input }); - expect(getIntegrationsResult()).toEqual(expected); + const integrationsFn = getIntegrationsResult() as (defaults: SentryCore.Integration[]) => SentryCore.Integration[]; + expect(integrationsFn([])).toEqual(expected); + }); + + it('filters Hono from defaults when user provides an array', () => { + const app = new Hono(); + sentry(app, { integrations: [otherIntegration] }); + + const integrationsFn = getIntegrationsResult() as (defaults: SentryCore.Integration[]) => SentryCore.Integration[]; + // Simulates getIntegrationsToSetup: defaults (from Cloudflare) include Hono; result must exclude it + const defaultsWithHono = [honoIntegration, otherIntegration]; + expect(integrationsFn(defaultsWithHono)).toEqual([otherIntegration, otherIntegration]); }); it('filters Hono integration out of a function result', () => { From e5409652659306b9db10dcd07b25162f4adfddc0 Mon Sep 17 00:00:00 2001 From: s1gr1d <32902192+s1gr1d@users.noreply.github.com> Date: Tue, 3 Mar 2026 17:22:07 +0100 Subject: [PATCH 05/11] feat(hono): Instrument middlewares `app.use()` --- packages/hono/src/cloudflare/middleware.ts | 3 + packages/hono/src/shared/patchAppUse.ts | 71 ++++++++ packages/hono/test/shared/patchAppUse.test.ts | 154 ++++++++++++++++++ 3 files changed, 228 insertions(+) create mode 100644 packages/hono/src/shared/patchAppUse.ts create mode 100644 packages/hono/test/shared/patchAppUse.test.ts diff --git a/packages/hono/src/cloudflare/middleware.ts b/packages/hono/src/cloudflare/middleware.ts index cfc0a7e6dccd..f2549cf0faf4 100644 --- a/packages/hono/src/cloudflare/middleware.ts +++ b/packages/hono/src/cloudflare/middleware.ts @@ -2,6 +2,7 @@ import { withSentry } from '@sentry/cloudflare'; import { applySdkMetadata, type BaseTransportOptions, debug, type Integration, type Options } from '@sentry/core'; import type { Context, Hono, MiddlewareHandler } from 'hono'; import { requestHandler, responseHandler } from '../shared/middlewareHandlers'; +import { patchAppUse } from '../shared/patchAppUse'; export interface HonoOptions extends Options { context?: Context; @@ -31,6 +32,8 @@ export const sentry = (app: Hono, options: HonoOptions | undefined = {}): Middle app, ); + patchAppUse(app); + return async (context, next) => { requestHandler(context); diff --git a/packages/hono/src/shared/patchAppUse.ts b/packages/hono/src/shared/patchAppUse.ts new file mode 100644 index 000000000000..99347982217f --- /dev/null +++ b/packages/hono/src/shared/patchAppUse.ts @@ -0,0 +1,71 @@ +import { + captureException, + SEMANTIC_ATTRIBUTE_SENTRY_OP, + SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, + SPAN_STATUS_ERROR, + SPAN_STATUS_OK, + startInactiveSpan, +} from '@sentry/core'; +import type { Hono, MiddlewareHandler } from 'hono'; + +const MIDDLEWARE_ORIGIN = 'auto.middleware.hono'; + +// Module-level counter for anonymous middleware span names +let MIDDLEWARE_IDX = 0; + +/** + * Patches `app.use` so that every middleware registered through it is automatically + * wrapped in a Sentry span. Supports both forms: `app.use(...handlers)` and `app.use(path, ...handlers)`. + */ +export function patchAppUse(app: Hono): void { + app.use = new Proxy(app.use, { + apply(target: typeof app.use, thisArg: typeof app, args: Parameters): ReturnType { + const [first, ...rest] = args as [unknown, ...MiddlewareHandler[]]; + + if (typeof first === 'string') { + const wrappedHandlers = rest.map(handler => wrapMiddlewareWithSpan(handler, MIDDLEWARE_IDX++)); + return Reflect.apply(target, thisArg, [first, ...wrappedHandlers]); + } + + const allHandlers = [first as MiddlewareHandler, ...rest].map(handler => + wrapMiddlewareWithSpan(handler, MIDDLEWARE_IDX++), + ); + return Reflect.apply(target, thisArg, allHandlers); + }, + }); +} + +/** + * Wraps a Hono middleware handler so that its execution is traced as a Sentry span. + * Uses startInactiveSpan so that all middleware spans are siblings under the request/transaction + * (onion order: A → B → handler → B → A does not nest B under A in the trace). + */ +function wrapMiddlewareWithSpan(handler: MiddlewareHandler, index: number): MiddlewareHandler { + const spanName = handler.name || ``; + + return async function sentryTracedMiddleware(context, next) { + const span = startInactiveSpan({ + name: spanName, + op: 'middleware.hono', + onlyIfParent: true, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'middleware.hono', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: MIDDLEWARE_ORIGIN, + }, + }); + + try { + const result = await handler(context, next); + span.setStatus({ code: SPAN_STATUS_OK }); + return result; + } catch (error) { + span.setStatus({ code: SPAN_STATUS_ERROR, message: 'internal_error' }); + captureException(error, { + mechanism: { handled: false, type: MIDDLEWARE_ORIGIN }, + }); + throw error; + } finally { + span.end(); + } + }; +} diff --git a/packages/hono/test/shared/patchAppUse.test.ts b/packages/hono/test/shared/patchAppUse.test.ts new file mode 100644 index 000000000000..a3585a187cb7 --- /dev/null +++ b/packages/hono/test/shared/patchAppUse.test.ts @@ -0,0 +1,154 @@ +import * as SentryCore from '@sentry/core'; +import { Hono } from 'hono'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { patchAppUse } from '../../src/shared/patchAppUse'; + +vi.mock('@sentry/core', async () => { + const actual = await vi.importActual('@sentry/core'); + return { + ...actual, + startInactiveSpan: vi.fn((_opts: unknown) => ({ + setStatus: vi.fn(), + end: vi.fn(), + })), + captureException: vi.fn(), + }; +}); + +const startInactiveSpanMock = SentryCore.startInactiveSpan as ReturnType; +const captureExceptionMock = SentryCore.captureException as ReturnType; + +describe('patchAppUse (middleware spans)', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('wraps handlers in app.use(handler) so startInactiveSpan is called when middleware runs', async () => { + const app = new Hono(); + patchAppUse(app); + + const userHandler = vi.fn(async (_c: unknown, next: () => Promise) => { + await next(); + }); + app.use(userHandler); + + expect(startInactiveSpanMock).not.toHaveBeenCalled(); + + const fetchHandler = app.fetch; + const req = new Request('http://localhost/'); + await fetchHandler(req); + + expect(startInactiveSpanMock).toHaveBeenCalledTimes(1); + expect(startInactiveSpanMock).toHaveBeenCalledWith( + expect.objectContaining({ + op: 'middleware.hono', + onlyIfParent: true, + attributes: expect.objectContaining({ + 'sentry.op': 'middleware.hono', + 'sentry.origin': 'auto.middleware.hono', + }), + }), + ); + expect(userHandler).toHaveBeenCalled(); + }); + + describe('span naming', () => { + it('uses handler.name for span when handler has a name', async () => { + const app = new Hono(); + patchAppUse(app); + + async function myNamedMiddleware(_c: unknown, next: () => Promise) { + await next(); + } + app.use(myNamedMiddleware); + + await app.fetch(new Request('http://localhost/')); + + expect(startInactiveSpanMock).toHaveBeenCalledWith(expect.objectContaining({ name: 'myNamedMiddleware' })); + }); + + it('uses for span when handler is anonymous', async () => { + const app = new Hono(); + patchAppUse(app); + + app.use(async (_c: unknown, next: () => Promise) => await next()); + + await app.fetch(new Request('http://localhost/')); + + expect(startInactiveSpanMock).toHaveBeenCalledTimes(1); + const name = startInactiveSpanMock.mock.calls[0][0].name; + expect(name).toMatch(/^$/); + }); + }); + + it('wraps each handler in app.use(path, ...handlers) and passes path through', async () => { + const app = new Hono(); + patchAppUse(app); + + const handler = async (_c: unknown, next: () => Promise) => await next(); + app.use('/api', handler); + app.get('/api', () => new Response('ok')); + + await app.fetch(new Request('http://localhost/api')); + + expect(startInactiveSpanMock).toHaveBeenCalled(); + }); + + it('calls captureException when middleware throws', async () => { + const app = new Hono(); + patchAppUse(app); + + const err = new Error('middleware error'); + app.use(async () => { + throw err; + }); + + const res = await app.fetch(new Request('http://localhost/')); + expect(res.status).toBe(500); + + expect(captureExceptionMock).toHaveBeenCalledWith(err, { + mechanism: { handled: false, type: 'auto.middleware.hono' }, + }); + }); + + it('creates sibling spans for multiple middlewares (onion order, not parent-child)', async () => { + const app = new Hono(); + patchAppUse(app); + + app.use( + async (_c: unknown, next: () => Promise) => next(), + async (_c: unknown, next: () => Promise) => next(), + ); + + await app.fetch(new Request('http://localhost/')); + + expect(startInactiveSpanMock).toHaveBeenCalledTimes(2); + const [firstCall, secondCall] = startInactiveSpanMock.mock.calls; + expect(firstCall[0]).toMatchObject({ op: 'middleware.hono' }); + expect(secondCall[0]).toMatchObject({ op: 'middleware.hono' }); + expect(firstCall[0].name).toMatch(/^$/); + expect(secondCall[0].name).toMatch(/^$/); + expect(firstCall[0].name).not.toBe(secondCall[0].name); + }); + + it('preserves this context when calling the original use (Proxy forwards thisArg)', () => { + type FakeApp = { + _capturedThis: unknown; + use: (...args: unknown[]) => FakeApp; + }; + const fakeApp: FakeApp = { + _capturedThis: null, + use(this: FakeApp, ..._args: unknown[]) { + this._capturedThis = this; + return this; + }, + }; + + patchAppUse(fakeApp as unknown as Parameters[0]); + + const noop = async (_c: unknown, next: () => Promise) => next(); + fakeApp.use(noop); + + expect(fakeApp._capturedThis).toBe(fakeApp); + }); +}); From a09b7759573ba3f88c2229d0c083358b44fb5443 Mon Sep 17 00:00:00 2001 From: s1gr1d <32902192+s1gr1d@users.noreply.github.com> Date: Wed, 4 Mar 2026 11:52:11 +0100 Subject: [PATCH 06/11] add missing function --- packages/hono/src/cloudflare/middleware.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/hono/src/cloudflare/middleware.ts b/packages/hono/src/cloudflare/middleware.ts index 2335333af034..ffcdf5e40346 100644 --- a/packages/hono/src/cloudflare/middleware.ts +++ b/packages/hono/src/cloudflare/middleware.ts @@ -43,6 +43,8 @@ export const sentry = (app: Hono, options: HonoOptions | undefined = {}): Middle app, ); + patchAppUse(app); + return async (context, next) => { requestHandler(context); From 58bb05041329e71e6d9efdb0b6b87d27555ccf9e Mon Sep 17 00:00:00 2001 From: s1gr1d <32902192+s1gr1d@users.noreply.github.com> Date: Wed, 4 Mar 2026 14:00:35 +0100 Subject: [PATCH 07/11] only increment on anonymous middleware --- packages/hono/src/shared/patchAppUse.ts | 10 +++++----- packages/hono/test/shared/patchAppUse.test.ts | 15 +++++++++++---- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/packages/hono/src/shared/patchAppUse.ts b/packages/hono/src/shared/patchAppUse.ts index 99347982217f..3535439049fa 100644 --- a/packages/hono/src/shared/patchAppUse.ts +++ b/packages/hono/src/shared/patchAppUse.ts @@ -22,13 +22,15 @@ export function patchAppUse(app: Hono): void { apply(target: typeof app.use, thisArg: typeof app, args: Parameters): ReturnType { const [first, ...rest] = args as [unknown, ...MiddlewareHandler[]]; + const getSpanName = (handler: MiddlewareHandler): string => handler.name || ``; + if (typeof first === 'string') { - const wrappedHandlers = rest.map(handler => wrapMiddlewareWithSpan(handler, MIDDLEWARE_IDX++)); + const wrappedHandlers = rest.map(handler => wrapMiddlewareWithSpan(handler, getSpanName(handler))); return Reflect.apply(target, thisArg, [first, ...wrappedHandlers]); } const allHandlers = [first as MiddlewareHandler, ...rest].map(handler => - wrapMiddlewareWithSpan(handler, MIDDLEWARE_IDX++), + wrapMiddlewareWithSpan(handler, getSpanName(handler)), ); return Reflect.apply(target, thisArg, allHandlers); }, @@ -40,9 +42,7 @@ export function patchAppUse(app: Hono): void { * Uses startInactiveSpan so that all middleware spans are siblings under the request/transaction * (onion order: A → B → handler → B → A does not nest B under A in the trace). */ -function wrapMiddlewareWithSpan(handler: MiddlewareHandler, index: number): MiddlewareHandler { - const spanName = handler.name || ``; - +function wrapMiddlewareWithSpan(handler: MiddlewareHandler, spanName: string): MiddlewareHandler { return async function sentryTracedMiddleware(context, next) { const span = startInactiveSpan({ name: spanName, diff --git a/packages/hono/test/shared/patchAppUse.test.ts b/packages/hono/test/shared/patchAppUse.test.ts index a3585a187cb7..8854c8b9e289 100644 --- a/packages/hono/test/shared/patchAppUse.test.ts +++ b/packages/hono/test/shared/patchAppUse.test.ts @@ -112,22 +112,29 @@ describe('patchAppUse (middleware spans)', () => { }); it('creates sibling spans for multiple middlewares (onion order, not parent-child)', async () => { + vi.resetModules(); // resets the module-level counter variable + const { patchAppUse } = await import('../../src/shared/patchAppUse'); + const app = new Hono(); patchAppUse(app); app.use( async (_c: unknown, next: () => Promise) => next(), + async function namedMiddleware(_c: unknown, next: () => Promise) { + await next(); + }, async (_c: unknown, next: () => Promise) => next(), ); await app.fetch(new Request('http://localhost/')); - expect(startInactiveSpanMock).toHaveBeenCalledTimes(2); - const [firstCall, secondCall] = startInactiveSpanMock.mock.calls; + expect(startInactiveSpanMock).toHaveBeenCalledTimes(3); + const [firstCall, secondCall, thirdCall] = startInactiveSpanMock.mock.calls; expect(firstCall[0]).toMatchObject({ op: 'middleware.hono' }); expect(secondCall[0]).toMatchObject({ op: 'middleware.hono' }); - expect(firstCall[0].name).toMatch(/^$/); - expect(secondCall[0].name).toMatch(/^$/); + expect(firstCall[0].name).toMatch(''); + expect(secondCall[0].name).toBe('namedMiddleware'); + expect(thirdCall[0].name).toBe(''); expect(firstCall[0].name).not.toBe(secondCall[0].name); }); From 0f398219e9d6a5fbdc824ee61dbaa88e61112555 Mon Sep 17 00:00:00 2001 From: s1gr1d <32902192+s1gr1d@users.noreply.github.com> Date: Wed, 4 Mar 2026 14:00:44 +0100 Subject: [PATCH 08/11] only increment on anonymous middleware --- packages/hono/package.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/hono/package.json b/packages/hono/package.json index d5ee50e4a596..6655e4ec29dc 100644 --- a/packages/hono/package.json +++ b/packages/hono/package.json @@ -39,6 +39,11 @@ } }, "typesVersions": { + "*": { + "cloudflare": [ + "./build/types/index.cloudflare.d.ts" + ] + }, "<5.0": { "build/types/index.d.ts": [ "build/types-ts3.8/index.d.ts" From 0e032d6256ebd91386d6f241fbe30ac25571640b Mon Sep 17 00:00:00 2001 From: s1gr1d <32902192+s1gr1d@users.noreply.github.com> Date: Wed, 4 Mar 2026 14:26:49 +0100 Subject: [PATCH 09/11] remove incremental naming of middlewares --- packages/hono/src/shared/patchAppUse.ts | 15 ++++----------- packages/hono/test/shared/patchAppUse.test.ts | 9 +++------ 2 files changed, 7 insertions(+), 17 deletions(-) diff --git a/packages/hono/src/shared/patchAppUse.ts b/packages/hono/src/shared/patchAppUse.ts index 3535439049fa..dfcd186dc38a 100644 --- a/packages/hono/src/shared/patchAppUse.ts +++ b/packages/hono/src/shared/patchAppUse.ts @@ -10,9 +10,6 @@ import type { Hono, MiddlewareHandler } from 'hono'; const MIDDLEWARE_ORIGIN = 'auto.middleware.hono'; -// Module-level counter for anonymous middleware span names -let MIDDLEWARE_IDX = 0; - /** * Patches `app.use` so that every middleware registered through it is automatically * wrapped in a Sentry span. Supports both forms: `app.use(...handlers)` and `app.use(path, ...handlers)`. @@ -22,16 +19,12 @@ export function patchAppUse(app: Hono): void { apply(target: typeof app.use, thisArg: typeof app, args: Parameters): ReturnType { const [first, ...rest] = args as [unknown, ...MiddlewareHandler[]]; - const getSpanName = (handler: MiddlewareHandler): string => handler.name || ``; - if (typeof first === 'string') { - const wrappedHandlers = rest.map(handler => wrapMiddlewareWithSpan(handler, getSpanName(handler))); + const wrappedHandlers = rest.map(handler => wrapMiddlewareWithSpan(handler)); return Reflect.apply(target, thisArg, [first, ...wrappedHandlers]); } - const allHandlers = [first as MiddlewareHandler, ...rest].map(handler => - wrapMiddlewareWithSpan(handler, getSpanName(handler)), - ); + const allHandlers = [first as MiddlewareHandler, ...rest].map(handler => wrapMiddlewareWithSpan(handler)); return Reflect.apply(target, thisArg, allHandlers); }, }); @@ -42,10 +35,10 @@ export function patchAppUse(app: Hono): void { * Uses startInactiveSpan so that all middleware spans are siblings under the request/transaction * (onion order: A → B → handler → B → A does not nest B under A in the trace). */ -function wrapMiddlewareWithSpan(handler: MiddlewareHandler, spanName: string): MiddlewareHandler { +function wrapMiddlewareWithSpan(handler: MiddlewareHandler): MiddlewareHandler { return async function sentryTracedMiddleware(context, next) { const span = startInactiveSpan({ - name: spanName, + name: handler.name || '', op: 'middleware.hono', onlyIfParent: true, attributes: { diff --git a/packages/hono/test/shared/patchAppUse.test.ts b/packages/hono/test/shared/patchAppUse.test.ts index 8854c8b9e289..f68dade5bb52 100644 --- a/packages/hono/test/shared/patchAppUse.test.ts +++ b/packages/hono/test/shared/patchAppUse.test.ts @@ -77,7 +77,7 @@ describe('patchAppUse (middleware spans)', () => { expect(startInactiveSpanMock).toHaveBeenCalledTimes(1); const name = startInactiveSpanMock.mock.calls[0][0].name; - expect(name).toMatch(/^$/); + expect(name).toMatch(''); }); }); @@ -112,9 +112,6 @@ describe('patchAppUse (middleware spans)', () => { }); it('creates sibling spans for multiple middlewares (onion order, not parent-child)', async () => { - vi.resetModules(); // resets the module-level counter variable - const { patchAppUse } = await import('../../src/shared/patchAppUse'); - const app = new Hono(); patchAppUse(app); @@ -132,9 +129,9 @@ describe('patchAppUse (middleware spans)', () => { const [firstCall, secondCall, thirdCall] = startInactiveSpanMock.mock.calls; expect(firstCall[0]).toMatchObject({ op: 'middleware.hono' }); expect(secondCall[0]).toMatchObject({ op: 'middleware.hono' }); - expect(firstCall[0].name).toMatch(''); + expect(firstCall[0].name).toMatch(''); expect(secondCall[0].name).toBe('namedMiddleware'); - expect(thirdCall[0].name).toBe(''); + expect(thirdCall[0].name).toBe(''); expect(firstCall[0].name).not.toBe(secondCall[0].name); }); From 429d41876bbc038e1f951e75f0c52c60ebc495b0 Mon Sep 17 00:00:00 2001 From: s1gr1d <32902192+s1gr1d@users.noreply.github.com> Date: Wed, 4 Mar 2026 14:31:34 +0100 Subject: [PATCH 10/11] remove typed version --- packages/hono/package.json | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/hono/package.json b/packages/hono/package.json index 6655e4ec29dc..d5ee50e4a596 100644 --- a/packages/hono/package.json +++ b/packages/hono/package.json @@ -39,11 +39,6 @@ } }, "typesVersions": { - "*": { - "cloudflare": [ - "./build/types/index.cloudflare.d.ts" - ] - }, "<5.0": { "build/types/index.d.ts": [ "build/types-ts3.8/index.d.ts" From 8e7268c81d35e6ee88b06b1d1975ea4670c96d04 Mon Sep 17 00:00:00 2001 From: s1gr1d <32902192+s1gr1d@users.noreply.github.com> Date: Wed, 4 Mar 2026 15:02:12 +0100 Subject: [PATCH 11/11] remove lint problems --- packages/hono/test/shared/patchAppUse.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/hono/test/shared/patchAppUse.test.ts b/packages/hono/test/shared/patchAppUse.test.ts index f68dade5bb52..8f4e3bc0cc6c 100644 --- a/packages/hono/test/shared/patchAppUse.test.ts +++ b/packages/hono/test/shared/patchAppUse.test.ts @@ -71,7 +71,7 @@ describe('patchAppUse (middleware spans)', () => { const app = new Hono(); patchAppUse(app); - app.use(async (_c: unknown, next: () => Promise) => await next()); + app.use(async (_c: unknown, next: () => Promise) => next()); await app.fetch(new Request('http://localhost/')); @@ -85,7 +85,7 @@ describe('patchAppUse (middleware spans)', () => { const app = new Hono(); patchAppUse(app); - const handler = async (_c: unknown, next: () => Promise) => await next(); + const handler = async (_c: unknown, next: () => Promise) => next(); app.use('/api', handler); app.get('/api', () => new Response('ok'));