From c77dd5135f860d751740612f89349e59ee23deae Mon Sep 17 00:00:00 2001 From: s1gr1d <32902192+s1gr1d@users.noreply.github.com> Date: Mon, 11 May 2026 13:48:05 +0200 Subject: [PATCH 1/2] fix(hono): Capture transaction name on request for correct culprit --- .../src/route-groups/test-middleware.ts | 2 ++ .../test-applications/hono-4/src/routes.ts | 1 + .../hono-4/tests/errors.test.ts | 3 ++- .../hono-4/tests/middleware.test.ts | 24 +++++++++++++++++- .../hono/src/shared/middlewareHandlers.ts | 25 ++++++++++++------- 5 files changed, 44 insertions(+), 11 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/hono-4/src/route-groups/test-middleware.ts b/dev-packages/e2e-tests/test-applications/hono-4/src/route-groups/test-middleware.ts index 49ca50c591bf..d82201b7cdb3 100644 --- a/dev-packages/e2e-tests/test-applications/hono-4/src/route-groups/test-middleware.ts +++ b/dev-packages/e2e-tests/test-applications/hono-4/src/route-groups/test-middleware.ts @@ -7,6 +7,7 @@ middlewareRoutes.get('/named', c => c.json({ middleware: 'named' })); middlewareRoutes.get('/anonymous', c => c.json({ middleware: 'anonymous' })); middlewareRoutes.get('/multi', c => c.json({ middleware: 'multi' })); middlewareRoutes.get('/error', c => c.text('should not reach')); +middlewareRoutes.get('/param/:id', c => c.json({ paramId: c.req.param('id') })); // Self-contained sub-app registering its own middleware via .use() const subAppWithMiddleware = new Hono(); @@ -18,6 +19,7 @@ subAppWithMiddleware.use('/anonymous/*', async (c, next) => { }); subAppWithMiddleware.use('/multi/*', middlewareA, middlewareB); subAppWithMiddleware.use('/error/*', failingMiddleware); +subAppWithMiddleware.use('/param/*', middlewareA); // .all() handler (1 parameter) — should NOT be wrapped as middleware by patchRoute. subAppWithMiddleware.all('/all-handler', async function allCatchAll(c) { diff --git a/dev-packages/e2e-tests/test-applications/hono-4/src/routes.ts b/dev-packages/e2e-tests/test-applications/hono-4/src/routes.ts index cfb13146b6f7..f1f9c3a783b3 100644 --- a/dev-packages/e2e-tests/test-applications/hono-4/src/routes.ts +++ b/dev-packages/e2e-tests/test-applications/hono-4/src/routes.ts @@ -34,6 +34,7 @@ export function addRoutes(app: Hono<{ Bindings?: { E2E_TEST_DSN: string } }>): v }); app.use('/test-middleware/multi/*', middlewareA, middlewareB); app.use('/test-middleware/error/*', failingMiddleware); + app.use('/test-middleware/param/*', middlewareA); app.route('/test-middleware', middlewareRoutes); // Sub-app middleware: registered on the sub-app, wrapped at mount time by route() patching diff --git a/dev-packages/e2e-tests/test-applications/hono-4/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/hono-4/tests/errors.test.ts index 98c81d30afeb..8ee1f13f0c0a 100644 --- a/dev-packages/e2e-tests/test-applications/hono-4/tests/errors.test.ts +++ b/dev-packages/e2e-tests/test-applications/hono-4/tests/errors.test.ts @@ -147,6 +147,7 @@ test.describe('middleware errors', () => { expect(errorEvent.exception?.values?.[0]?.value).toBe('Service Unavailable from middleware'); expect(errorEvent.exception?.values?.[0]?.mechanism?.type).toBe('auto.middleware.hono'); expect(errorEvent.exception?.values?.[0]?.mechanism?.handled).toBe(false); + expect(errorEvent.transaction).toBe('GET /test-errors/middleware-http-exception'); const transaction = await transactionPromise; const middlewareSpan = (transaction.spans || []).find(s => s.op === 'middleware.hono'); @@ -183,7 +184,7 @@ test.describe('middleware errors', () => { const transaction = await transactionPromise; if (RUNTIME === 'cloudflare') { - expect(transaction.transaction).toBe('GET /test-errors/middleware-http-exception-4xx/*'); + expect(transaction.transaction).toBe('GET /test-errors/middleware-http-exception-4xx'); const middlewareSpan = (transaction.spans || []).find(s => s.op === 'middleware.hono'); expect(middlewareSpan?.status).not.toBe('internal_error'); diff --git a/dev-packages/e2e-tests/test-applications/hono-4/tests/middleware.test.ts b/dev-packages/e2e-tests/test-applications/hono-4/tests/middleware.test.ts index d984ac0d38a8..c4bf34874f2b 100644 --- a/dev-packages/e2e-tests/test-applications/hono-4/tests/middleware.test.ts +++ b/dev-packages/e2e-tests/test-applications/hono-4/tests/middleware.test.ts @@ -116,6 +116,9 @@ for (const { name, prefix } of SCENARIOS) { type: 'auto.middleware.hono', }), ); + + // The transaction name on the error event determines the culprit shown in Sentry. + expect(errorEvent.transaction).toBe(`GET ${prefix}/error`); }); test('sets error status on middleware span when middleware throws', async ({ baseURL }) => { @@ -126,7 +129,7 @@ for (const { name, prefix } of SCENARIOS) { await fetch(`${baseURL}${prefix}/error`); const transaction = await transactionPromise; - expect(transaction.transaction).toBe(`GET ${prefix}/error/*`); + expect(transaction.transaction).toBe(`GET ${prefix}/error`); const spans = transaction.spans || []; @@ -138,6 +141,25 @@ for (const { name, prefix } of SCENARIOS) { expect(failingSpan?.status).toBe('internal_error'); }); + test('uses parameterized route in transaction name', async ({ baseURL }) => { + const transactionPromise = waitForTransaction(APP_NAME, event => { + return event.contexts?.trace?.op === 'http.server' && !!event.transaction?.includes(`${prefix}/param/`); + }); + + const response = await fetch(`${baseURL}${prefix}/param/42`); + expect(response.status).toBe(200); + + const transaction = await transactionPromise; + expect(transaction.transaction).toBe(`GET ${prefix}/param/:id`); + + const spans = transaction.spans || []; + const middlewareSpan = spans.find( + (span: { description?: string; op?: string }) => + span.op === 'middleware.hono' && span.description === 'middlewareA', + ); + expect(middlewareSpan).toBeDefined(); + }); + test('includes request data on error events from middleware', async ({ baseURL }) => { const errorPromise = waitForError(APP_NAME, event => { return event.exception?.values?.[0]?.value === 'Middleware error' && !!event.request?.url?.includes(prefix); diff --git a/packages/hono/src/shared/middlewareHandlers.ts b/packages/hono/src/shared/middlewareHandlers.ts index 41902d90f84f..03bb6e16da58 100644 --- a/packages/hono/src/shared/middlewareHandlers.ts +++ b/packages/hono/src/shared/middlewareHandlers.ts @@ -6,6 +6,7 @@ import { getRootSpan, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, updateSpanName, + type Scope, winterCGRequestToRequestData, } from '@sentry/core'; import type { Context } from 'hono'; @@ -22,6 +23,8 @@ export function requestHandler(context: Context): void { const isolationScope = defaultScope === currentIsolationScope ? defaultScope : currentIsolationScope; + updateSpanRouteName(isolationScope, context); + isolationScope.setSDKProcessingMetadata({ normalizedRequest: winterCGRequestToRequestData(hasFetchEvent(context) ? context.event.request : context.req.raw), }); @@ -31,21 +34,25 @@ export function requestHandler(context: Context): void { * Response handler for Hono framework */ export function responseHandler(context: Context): void { + if (context.error && !isExpectedError(context.error)) { + getClient()?.captureException(context.error, { + mechanism: { handled: false, type: 'auto.http.hono.context_error' }, + }); + } +} + +function updateSpanRouteName(isolationScope: Scope, context: Context): void { const activeSpan = getActiveSpan(); + const lastMatchedRoute = routePath(context, -1); + if (activeSpan) { - activeSpan.updateName(`${context.req.method} ${routePath(context)}`); + activeSpan.updateName(`${context.req.method} ${lastMatchedRoute}`); activeSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); const rootSpan = getRootSpan(activeSpan); - updateSpanName(rootSpan, `${context.req.method} ${routePath(context)}`); + updateSpanName(rootSpan, `${context.req.method} ${lastMatchedRoute}`); rootSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); } - getIsolationScope().setTransactionName(`${context.req.method} ${routePath(context)}`); - - if (context.error && !isExpectedError(context.error)) { - getClient()?.captureException(context.error, { - mechanism: { handled: false, type: 'auto.http.hono.context_error' }, - }); - } + isolationScope.setTransactionName(`${context.req.method} ${lastMatchedRoute}`); } From 6b162175d521dabf5e47606291a0828cd9b9eac8 Mon Sep 17 00:00:00 2001 From: s1gr1d <32902192+s1gr1d@users.noreply.github.com> Date: Mon, 11 May 2026 14:25:47 +0200 Subject: [PATCH 2/2] fix(hono): Capture transaction name on request for correct culprit --- packages/hono/test/shared/middlewareHandlers.test.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/hono/test/shared/middlewareHandlers.test.ts b/packages/hono/test/shared/middlewareHandlers.test.ts index b8e4cdef1062..accf5fe5f91a 100644 --- a/packages/hono/test/shared/middlewareHandlers.test.ts +++ b/packages/hono/test/shared/middlewareHandlers.test.ts @@ -1,6 +1,6 @@ import * as SentryCore from '@sentry/core'; import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { responseHandler } from '../../src/shared/middlewareHandlers'; +import { requestHandler, responseHandler } from '../../src/shared/middlewareHandlers'; vi.mock('hono/route', () => ({ routePath: () => '/test', @@ -11,6 +11,7 @@ vi.mock('../../src/utils/hono-context', () => ({ })); const mockSetTransactionName = vi.fn(); +const mockSetSDKProcessingMetadata = vi.fn(); vi.mock('@sentry/core', async () => { const actual = await vi.importActual('@sentry/core'); @@ -19,6 +20,7 @@ vi.mock('@sentry/core', async () => { getActiveSpan: vi.fn(() => null), getIsolationScope: vi.fn(() => ({ setTransactionName: mockSetTransactionName, + setSDKProcessingMetadata: mockSetSDKProcessingMetadata, })), getClient: vi.fn(() => undefined), }; @@ -110,7 +112,7 @@ describe('responseHandler', () => { describe('transaction name', () => { it('sets transaction name on isolation scope', () => { // oxlint-disable-next-line typescript/no-explicit-any - responseHandler(createMockContext(200) as any); + requestHandler(createMockContext(200) as any); expect(mockSetTransactionName).toHaveBeenCalledWith('GET /test'); });