From e2e00c5c7f9c0bc9e2ce31bb517b2776091a6dfa Mon Sep 17 00:00:00 2001 From: s1gr1d <32902192+s1gr1d@users.noreply.github.com> Date: Fri, 16 Oct 2026 13:57:16 +0200 Subject: [PATCH 1/7] fix(hono): Make the Hono middleware types compile on Hono 5 Hono 5 removes c.req.matchedRoutes and c.req.routePath. The vendored HonoRequest type required both, so app.use(honoMiddleware(app)) failed with TS2769 on Hono 5. Make matchedRoutes optional and drop routePath, which the SDK does not read. Add Vitest type tests against Hono 4 and a pinned Hono 5 release candidate, a type check of the SDK calls in the Hono 4 E2E apps, and a lint rule that keeps hono imports out of @sentry/server-utils. Pin the hono devDependency of @sentry/hono to 4.13.9, so the package no longer builds against the hoisted version by chance. Refs #25223 Co-Authored-By: Claude Opus 5.5 --- .../hono-4-legacy/package.json | 3 +- .../hono-4-legacy/src/sentry.typecheck.ts | 20 +++++++ .../hono-4-legacy/tsconfig.typecheck.json | 6 ++ .../test-applications/hono-4/package.json | 3 +- .../hono-4/src/honoMiddleware.typecheck.ts | 13 ++++ .../hono-4/tsconfig.typecheck.json | 6 ++ packages/hono/package.json | 2 + packages/hono/test/types.test-d.ts | 59 +++++++++++++++++++ packages/hono/vite.config.ts | 8 +++ packages/server-utils/.oxlintrc.json | 10 ++++ .../src/integrations/hono/honoTypes.ts | 6 +- .../src/orchestrion/config/hono.ts | 3 + yarn.lock | 10 ++++ 13 files changed, 143 insertions(+), 6 deletions(-) create mode 100644 dev-packages/e2e-tests/test-applications/hono-4-legacy/src/sentry.typecheck.ts create mode 100644 dev-packages/e2e-tests/test-applications/hono-4-legacy/tsconfig.typecheck.json create mode 100644 dev-packages/e2e-tests/test-applications/hono-4/src/honoMiddleware.typecheck.ts create mode 100644 dev-packages/e2e-tests/test-applications/hono-4/tsconfig.typecheck.json create mode 100644 packages/hono/test/types.test-d.ts diff --git a/dev-packages/e2e-tests/test-applications/hono-4-legacy/package.json b/dev-packages/e2e-tests/test-applications/hono-4-legacy/package.json index c707ee5f2608..231900020d4f 100644 --- a/dev-packages/e2e-tests/test-applications/hono-4-legacy/package.json +++ b/dev-packages/e2e-tests/test-applications/hono-4-legacy/package.json @@ -9,7 +9,8 @@ "dev:bun": "bun src/entry.bun.ts", "dev:deno": "deno run --allow-net --allow-env --allow-read src/entry.deno.ts", "build": "wrangler deploy --dry-run", - "test:build": "pnpm install && pnpm build", + "test:build": "pnpm install && pnpm typecheck && pnpm build", + "typecheck": "tsc --noEmit -p tsconfig.typecheck.json", "test:assert": "TEST_ENV=production playwright test" }, "dependencies": { diff --git a/dev-packages/e2e-tests/test-applications/hono-4-legacy/src/sentry.typecheck.ts b/dev-packages/e2e-tests/test-applications/hono-4-legacy/src/sentry.typecheck.ts new file mode 100644 index 000000000000..14237ae092ae --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/hono-4-legacy/src/sentry.typecheck.ts @@ -0,0 +1,20 @@ +// Type check only: `tsc --noEmit` in `test:build` checks that these calls compile with the real Hono +// types. Nothing imports or runs this file. +import { sentry as sentryBun } from '@sentry/hono/bun'; +import { sentry as sentryCloudflare } from '@sentry/hono/cloudflare'; +import { sentry as sentryDeno } from '@sentry/hono/deno'; +import { sentry as sentryNode } from '@sentry/hono/node'; +import { Hono } from 'hono'; + +type AppEnv = { Bindings: { E2E_TEST_DSN: string }; Variables: { user: string } }; + +const dsn = 'https://public@dsn.ingest.sentry.io/1337'; + +const app = new Hono(); +app.use(sentryNode(app)); +app.use(sentryBun(app, { dsn })); +app.use(sentryDeno(app, { dsn })); +app.use(sentryCloudflare(app, env => ({ dsn: env.E2E_TEST_DSN }))); + +const api = app.basePath('/api'); +api.use(sentryNode(api)); diff --git a/dev-packages/e2e-tests/test-applications/hono-4-legacy/tsconfig.typecheck.json b/dev-packages/e2e-tests/test-applications/hono-4-legacy/tsconfig.typecheck.json new file mode 100644 index 000000000000..62059f72a594 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/hono-4-legacy/tsconfig.typecheck.json @@ -0,0 +1,6 @@ +{ + // Checks only the type-only files that call the SDK with the real Hono types. The rest of the app + // source runs on several runtimes and has no Node, Bun or Deno globals in its types. + "extends": "./tsconfig.json", + "include": ["src/*.typecheck.ts"] +} diff --git a/dev-packages/e2e-tests/test-applications/hono-4/package.json b/dev-packages/e2e-tests/test-applications/hono-4/package.json index 4efa27a21f54..6aba3043d05f 100644 --- a/dev-packages/e2e-tests/test-applications/hono-4/package.json +++ b/dev-packages/e2e-tests/test-applications/hono-4/package.json @@ -8,7 +8,8 @@ "dev:bun": "bun run dist/entry.bun.js", "dev:deno": "deno run --allow-net --allow-env --allow-read --preload=npm:@sentry/deno/import src/entry.deno.ts", "dev:cloudflare": "wrangler dev --config ./dist/hono_4/wrangler.json --var \"E2E_TEST_DSN:$E2E_TEST_DSN\" --log-level=$(test $CI && echo 'none' || echo 'log')", - "test:build": "pnpm install", + "test:build": "pnpm install && pnpm typecheck", + "typecheck": "tsc --noEmit -p tsconfig.typecheck.json", "test": "TEST_ENV=production playwright test", "test:assert": "pnpm test:assert:node", "test:assert:node": "RUNTIME=node pnpm test", diff --git a/dev-packages/e2e-tests/test-applications/hono-4/src/honoMiddleware.typecheck.ts b/dev-packages/e2e-tests/test-applications/hono-4/src/honoMiddleware.typecheck.ts new file mode 100644 index 000000000000..6083ce500085 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/hono-4/src/honoMiddleware.typecheck.ts @@ -0,0 +1,13 @@ +// Type check only: `tsc --noEmit` in `test:build` checks that these calls compile with the real Hono +// types. Nothing imports or runs this file. +import * as Sentry from '@sentry/node'; +import { Hono } from 'hono'; + +type AppEnv = { Bindings: { E2E_TEST_DSN: string }; Variables: { user: string } }; + +const app = new Hono(); +app.use(Sentry.honoMiddleware(app)); +app.use('*', Sentry.honoMiddleware(app)); + +const api = app.basePath('/api'); +api.use(Sentry.honoMiddleware(api)); diff --git a/dev-packages/e2e-tests/test-applications/hono-4/tsconfig.typecheck.json b/dev-packages/e2e-tests/test-applications/hono-4/tsconfig.typecheck.json new file mode 100644 index 000000000000..62059f72a594 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/hono-4/tsconfig.typecheck.json @@ -0,0 +1,6 @@ +{ + // Checks only the type-only files that call the SDK with the real Hono types. The rest of the app + // source runs on several runtimes and has no Node, Bun or Deno globals in its types. + "extends": "./tsconfig.json", + "include": ["src/*.typecheck.ts"] +} diff --git a/packages/hono/package.json b/packages/hono/package.json index 982ee76c2748..f3b00ef9b8c1 100644 --- a/packages/hono/package.json +++ b/packages/hono/package.json @@ -110,6 +110,8 @@ "@cloudflare/workers-types": "4.20250922.0", "@hono/node-server": "^2.0.10", "@types/node": "^18.19.1", + "hono": "4.13.9", + "hono-5": "npm:hono@5.0.0-rc.1", "wrangler": "4.62.0" }, "scripts": { diff --git a/packages/hono/test/types.test-d.ts b/packages/hono/test/types.test-d.ts new file mode 100644 index 000000000000..cd2149874fab --- /dev/null +++ b/packages/hono/test/types.test-d.ts @@ -0,0 +1,59 @@ +import { honoMiddleware } from '@sentry/server-utils'; +import { Hono } from 'hono'; +import type { Hono as Hono5 } from 'hono-5'; +import { describe, it } from 'vitest'; +import { sentry as sentryBun } from '../src/bun/middleware'; +import { sentry as sentryCloudflare } from '../src/cloudflare/middleware'; +import { sentry as sentryDeno } from '../src/deno/middleware'; +import { sentry as sentryNode } from '../src/node/middleware'; + +type AppEnv = { Bindings: { SENTRY_DSN: string }; Variables: { user: string } }; + +declare const app5: Hono5; + +const dsn = 'https://public@dsn.ingest.sentry.io/1337'; + +describe('Hono 4', () => { + it('accepts honoMiddleware(app)', () => { + const app = new Hono(); + app.use(honoMiddleware(app)); + + const api = app.basePath('/api'); + api.use(honoMiddleware(api)); + }); + + it('accepts sentry(app) from each runtime entry', () => { + const app = new Hono(); + app.use(sentryNode(app)); + app.use(sentryBun(app, { dsn })); + app.use(sentryDeno(app, { dsn })); + app.use(sentryCloudflare(app, env => ({ dsn: env.SENTRY_DSN }))); + + const api = app.basePath('/api'); + api.use(sentryNode(api)); + }); +}); + +describe('Hono 5', () => { + it('accepts honoMiddleware(app)', () => { + app5.use(honoMiddleware(app5)); + + const api = app5.basePath('/api'); + api.use(honoMiddleware(api)); + }); + + // In this package `hono` resolves to Hono 4, and `Hono` has `private` members, so a Hono 5 app never + // matches the `Hono` type of `sentry()`. In a Hono 5 app, the same import resolves to Hono 5, so a + // Hono 5 app checks these calls with its own types. Each `@ts-expect-error` fails if the call starts + // to compile here. + it('rejects sentry(app) here, because `hono` is Hono 4 in this package', () => { + // @ts-expect-error -- Hono 5 app against the Hono 4 types of `sentry()` + app5.use(sentryNode(app5)); + // @ts-expect-error -- Hono 5 app against the Hono 4 types of `sentry()` + app5.use(sentryBun(app5, { dsn })); + // @ts-expect-error -- Hono 5 app against the Hono 4 types of `sentry()` + app5.use(sentryDeno(app5, { dsn })); + // @ts-expect-error -- Hono 5 app against the Hono 4 types of `sentry()` + app5.use(sentryCloudflare(app5, env => ({ dsn: env.SENTRY_DSN }))); + }); +}); diff --git a/packages/hono/vite.config.ts b/packages/hono/vite.config.ts index b2150cd225a4..bac0e296db14 100644 --- a/packages/hono/vite.config.ts +++ b/packages/hono/vite.config.ts @@ -3,4 +3,12 @@ import baseConfig from '../../vite/vite.config'; export default defineConfig({ ...baseConfig, + test: { + ...baseConfig.test, + typecheck: { + enabled: true, + tsconfig: './tsconfig.test.json', + ignoreSourceErrors: true, + }, + }, }); diff --git a/packages/server-utils/.oxlintrc.json b/packages/server-utils/.oxlintrc.json index d9e07c9dd0cd..001bf4e0b298 100644 --- a/packages/server-utils/.oxlintrc.json +++ b/packages/server-utils/.oxlintrc.json @@ -31,6 +31,16 @@ "name": "diagnostics_channel", "allowTypeImports": true, "message": "Bun garbage-collects a channel that no code references, together with its subscribers. Import the wrappers from `src/utils/diagnosticsChannel.ts` instead. See https://github.com/oven-sh/bun/issues/43086" + }, + { + "name": "hono", + "message": "`@sentry/server-utils` has no `hono` dependency and must work on every Hono major. Use the vendored types in `src/integrations/hono/honoTypes.ts`." + } + ], + "patterns": [ + { + "group": ["hono/*"], + "message": "`@sentry/server-utils` has no `hono` dependency and must work on every Hono major. Use the vendored types in `src/integrations/hono/honoTypes.ts`." } ] } diff --git a/packages/server-utils/src/integrations/hono/honoTypes.ts b/packages/server-utils/src/integrations/hono/honoTypes.ts index a4dc01e14152..9ef1d15b64a6 100644 --- a/packages/server-utils/src/integrations/hono/honoTypes.ts +++ b/packages/server-utils/src/integrations/hono/honoTypes.ts @@ -34,10 +34,8 @@ export interface HonoRequest { method: string; path: string; routeIndex: number; - // These are public (though deprecated) getters on Hono's request. Reading them avoids a runtime - // import of the `hono/route` helpers, which are their non-deprecated replacements. - matchedRoutes: HonoRoute[]; - routePath: string; + // Deprecated getter that Hono 5 removes. + matchedRoutes?: HonoRoute[]; [key: string]: any; } diff --git a/packages/server-utils/src/orchestrion/config/hono.ts b/packages/server-utils/src/orchestrion/config/hono.ts index eda1a8084ff3..74075179e31a 100644 --- a/packages/server-utils/src/orchestrion/config/hono.ts +++ b/packages/server-utils/src/orchestrion/config/hono.ts @@ -8,6 +8,9 @@ const honoInstrumentationConfig: InstrumentationConfig[] = [ { // `new Context()` runs once per request: where the Sentry middleware is injected and matched // middleware handlers are wrapped for spans. + // TODO(v12): Raise the minimum Hono version to 4.4.9, here and in the `hono` peer range of + // `@sentry/hono`. Before 4.4.9, `#dispatch` passes `matchResult` to `new HonoRequest()`, not to + // `new Context()`, so this hook gets no match result and instruments nothing. channelName: 'context', module: { name: 'hono', versionRange: '>=4.0.0 <5', filePath: /^dist\/(?:cjs\/)?context\.js$/ }, functionQuery: { className: 'Context' }, diff --git a/yarn.lock b/yarn.lock index ef8f10db812c..493b5df4dbc4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -17184,6 +17184,16 @@ homedir-polyfill@^1.0.1: dependencies: parse-passwd "^1.0.0" +"hono-5@npm:hono@5.0.0-rc.1": + version "5.0.0-rc.1" + resolved "https://sfw.security.sentry.io/npm/hono/-/hono-5.0.0-rc.1.tgz#7115abe957dd0a5b794e999267dec3a6b217196f" + integrity sha512-vTcfU9yAggXh/+ZSi8zpYbzYDubCgFnSt8d5afCAlgkd38dyYb3CVtOh8YWhS65NkEqBseCsTj/P8mceJ+7KNA== + +hono@4.13.9: + version "4.13.9" + resolved "https://sfw.security.sentry.io/npm/hono/-/hono-4.13.9.tgz#682943308ab2f228012460d596a9af58090a1528" + integrity sha512-7dMkQmZoC4E6F7AtaQSPhlWAdnBti+j7rreMZl8QB4jFiEhP9TWbGWUMi8WYzBCgmgulxuvLQupKqo+Co6Omyg== + hono@^4.13.7: version "4.13.7" resolved "https://registry.yarnpkg.com/hono/-/hono-4.13.7.tgz#80fdab8326f03d02f049117fd968101933493600" From a93c25c95ac8824a099d50fc3839f22a2fd44164 Mon Sep 17 00:00:00 2001 From: s1gr1d <32902192+s1gr1d@users.noreply.github.com> Date: Fri, 16 Oct 2026 14:28:03 +0200 Subject: [PATCH 2/7] build: Deduplicate hono in yarn.lock The hono devDependency of @sentry/hono (4.13.9) and the integration tests (^4.13.7) resolved to two Hono copies. Their HonoRequest types differ by a unique symbol, so sentry(app) did not type-check in the bun and cloudflare integration tests, and yarn dedupe-deps:check failed. Resolve both to 4.13.9. Refs #25223 Co-Authored-By: Claude Opus 5.5 --- yarn.lock | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/yarn.lock b/yarn.lock index 493b5df4dbc4..a01932a72162 100644 --- a/yarn.lock +++ b/yarn.lock @@ -17189,16 +17189,11 @@ homedir-polyfill@^1.0.1: resolved "https://sfw.security.sentry.io/npm/hono/-/hono-5.0.0-rc.1.tgz#7115abe957dd0a5b794e999267dec3a6b217196f" integrity sha512-vTcfU9yAggXh/+ZSi8zpYbzYDubCgFnSt8d5afCAlgkd38dyYb3CVtOh8YWhS65NkEqBseCsTj/P8mceJ+7KNA== -hono@4.13.9: +hono@4.13.9, hono@^4.13.7: version "4.13.9" resolved "https://sfw.security.sentry.io/npm/hono/-/hono-4.13.9.tgz#682943308ab2f228012460d596a9af58090a1528" integrity sha512-7dMkQmZoC4E6F7AtaQSPhlWAdnBti+j7rreMZl8QB4jFiEhP9TWbGWUMi8WYzBCgmgulxuvLQupKqo+Co6Omyg== -hono@^4.13.7: - version "4.13.7" - resolved "https://registry.yarnpkg.com/hono/-/hono-4.13.7.tgz#80fdab8326f03d02f049117fd968101933493600" - integrity sha512-c8/gF9ac8Y78/agExVocyLevgR+JlpNB444Py0FSX8pJoPdYUfUzRcXtYEYGwt6l19qIlVZPN5Mfsw9jFShmQQ== - hookable@^5.5.3: version "5.5.3" resolved "https://registry.yarnpkg.com/hookable/-/hookable-5.5.3.tgz#6cfc358984a1ef991e2518cb9ed4a778bbd3215d" From 2a39959cefe0223f4bb85fb86bd13ef6285d9573 Mon Sep 17 00:00:00 2001 From: s1gr1d <32902192+s1gr1d@users.noreply.github.com> Date: Fri, 16 Oct 2026 14:41:26 +0200 Subject: [PATCH 3/7] ref(hono): Shorten comments Co-Authored-By: Claude Opus 5.5 --- .../hono-4-legacy/src/sentry.typecheck.ts | 3 +-- .../hono-4-legacy/tsconfig.typecheck.json | 3 +-- .../hono-4/src/honoMiddleware.typecheck.ts | 3 +-- .../hono-4/tsconfig.typecheck.json | 3 +-- packages/hono/test/types.test-d.ts | 14 ++++++-------- .../server-utils/src/orchestrion/config/hono.ts | 5 ++--- 6 files changed, 12 insertions(+), 19 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/hono-4-legacy/src/sentry.typecheck.ts b/dev-packages/e2e-tests/test-applications/hono-4-legacy/src/sentry.typecheck.ts index 14237ae092ae..9df86b4b4b85 100644 --- a/dev-packages/e2e-tests/test-applications/hono-4-legacy/src/sentry.typecheck.ts +++ b/dev-packages/e2e-tests/test-applications/hono-4-legacy/src/sentry.typecheck.ts @@ -1,5 +1,4 @@ -// Type check only: `tsc --noEmit` in `test:build` checks that these calls compile with the real Hono -// types. Nothing imports or runs this file. +// Only `tsc` in `test:build` reads this file. Nothing runs it. import { sentry as sentryBun } from '@sentry/hono/bun'; import { sentry as sentryCloudflare } from '@sentry/hono/cloudflare'; import { sentry as sentryDeno } from '@sentry/hono/deno'; diff --git a/dev-packages/e2e-tests/test-applications/hono-4-legacy/tsconfig.typecheck.json b/dev-packages/e2e-tests/test-applications/hono-4-legacy/tsconfig.typecheck.json index 62059f72a594..9131a4be8ad3 100644 --- a/dev-packages/e2e-tests/test-applications/hono-4-legacy/tsconfig.typecheck.json +++ b/dev-packages/e2e-tests/test-applications/hono-4-legacy/tsconfig.typecheck.json @@ -1,6 +1,5 @@ { - // Checks only the type-only files that call the SDK with the real Hono types. The rest of the app - // source runs on several runtimes and has no Node, Bun or Deno globals in its types. + // The rest of `src` needs Node, Bun and Deno types, which this app does not install. "extends": "./tsconfig.json", "include": ["src/*.typecheck.ts"] } diff --git a/dev-packages/e2e-tests/test-applications/hono-4/src/honoMiddleware.typecheck.ts b/dev-packages/e2e-tests/test-applications/hono-4/src/honoMiddleware.typecheck.ts index 6083ce500085..396d90942d11 100644 --- a/dev-packages/e2e-tests/test-applications/hono-4/src/honoMiddleware.typecheck.ts +++ b/dev-packages/e2e-tests/test-applications/hono-4/src/honoMiddleware.typecheck.ts @@ -1,5 +1,4 @@ -// Type check only: `tsc --noEmit` in `test:build` checks that these calls compile with the real Hono -// types. Nothing imports or runs this file. +// Only `tsc` in `test:build` reads this file. Nothing runs it. import * as Sentry from '@sentry/node'; import { Hono } from 'hono'; diff --git a/dev-packages/e2e-tests/test-applications/hono-4/tsconfig.typecheck.json b/dev-packages/e2e-tests/test-applications/hono-4/tsconfig.typecheck.json index 62059f72a594..9131a4be8ad3 100644 --- a/dev-packages/e2e-tests/test-applications/hono-4/tsconfig.typecheck.json +++ b/dev-packages/e2e-tests/test-applications/hono-4/tsconfig.typecheck.json @@ -1,6 +1,5 @@ { - // Checks only the type-only files that call the SDK with the real Hono types. The rest of the app - // source runs on several runtimes and has no Node, Bun or Deno globals in its types. + // The rest of `src` needs Node, Bun and Deno types, which this app does not install. "extends": "./tsconfig.json", "include": ["src/*.typecheck.ts"] } diff --git a/packages/hono/test/types.test-d.ts b/packages/hono/test/types.test-d.ts index cd2149874fab..3d069bd46138 100644 --- a/packages/hono/test/types.test-d.ts +++ b/packages/hono/test/types.test-d.ts @@ -42,18 +42,16 @@ describe('Hono 5', () => { api.use(honoMiddleware(api)); }); - // In this package `hono` resolves to Hono 4, and `Hono` has `private` members, so a Hono 5 app never - // matches the `Hono` type of `sentry()`. In a Hono 5 app, the same import resolves to Hono 5, so a - // Hono 5 app checks these calls with its own types. Each `@ts-expect-error` fails if the call starts - // to compile here. + // In this package, `sentry()` gets its `Hono` type from Hono 4. That class has private members, so a + // Hono 5 app cannot match it. In a real Hono 5 app, `sentry()` gets the Hono 5 types. it('rejects sentry(app) here, because `hono` is Hono 4 in this package', () => { - // @ts-expect-error -- Hono 5 app against the Hono 4 types of `sentry()` + // @ts-expect-error -- Hono 5 app, Hono 4 types app5.use(sentryNode(app5)); - // @ts-expect-error -- Hono 5 app against the Hono 4 types of `sentry()` + // @ts-expect-error -- Hono 5 app, Hono 4 types app5.use(sentryBun(app5, { dsn })); - // @ts-expect-error -- Hono 5 app against the Hono 4 types of `sentry()` + // @ts-expect-error -- Hono 5 app, Hono 4 types app5.use(sentryDeno(app5, { dsn })); - // @ts-expect-error -- Hono 5 app against the Hono 4 types of `sentry()` + // @ts-expect-error -- Hono 5 app, Hono 4 types app5.use(sentryCloudflare(app5, env => ({ dsn: env.SENTRY_DSN }))); }); }); diff --git a/packages/server-utils/src/orchestrion/config/hono.ts b/packages/server-utils/src/orchestrion/config/hono.ts index 74075179e31a..f56ff5a12039 100644 --- a/packages/server-utils/src/orchestrion/config/hono.ts +++ b/packages/server-utils/src/orchestrion/config/hono.ts @@ -8,9 +8,8 @@ const honoInstrumentationConfig: InstrumentationConfig[] = [ { // `new Context()` runs once per request: where the Sentry middleware is injected and matched // middleware handlers are wrapped for spans. - // TODO(v12): Raise the minimum Hono version to 4.4.9, here and in the `hono` peer range of - // `@sentry/hono`. Before 4.4.9, `#dispatch` passes `matchResult` to `new HonoRequest()`, not to - // `new Context()`, so this hook gets no match result and instruments nothing. + // TODO(v12): Raise the minimum Hono version to 4.4.9, here and in the `@sentry/hono` peer range. + // Before 4.4.9, Hono passes `matchResult` to `new HonoRequest()`, so this hook does nothing. channelName: 'context', module: { name: 'hono', versionRange: '>=4.0.0 <5', filePath: /^dist\/(?:cjs\/)?context\.js$/ }, functionQuery: { className: 'Context' }, From 2980a0855f6cde184b37d43e77c94d10762ab96e Mon Sep 17 00:00:00 2001 From: s1gr1d <32902192+s1gr1d@users.noreply.github.com> Date: Fri, 16 Oct 2026 13:58:51 +0200 Subject: [PATCH 4/7] ref(hono): Share the middleware classification The Context hook and wrapSubAppMiddleware each had their own copy of the rule that tells middleware from route handlers. Move the rule to getMiddlewareFlags() and check the shape of the match result entries before the hook reads them. No behavior change. Refs #25223 Co-Authored-By: Claude Opus 5.5 --- .../src/integrations/hono/honoIntegration.ts | 55 ++++++------------- .../src/integrations/hono/honoTypes.ts | 4 ++ .../src/integrations/hono/isMiddleware.ts | 20 ++++++- .../src/integrations/hono/matchResult.ts | 26 +++++++++ .../src/integrations/hono/patchRoute.ts | 26 ++------- 5 files changed, 69 insertions(+), 62 deletions(-) create mode 100644 packages/server-utils/src/integrations/hono/matchResult.ts diff --git a/packages/server-utils/src/integrations/hono/honoIntegration.ts b/packages/server-utils/src/integrations/hono/honoIntegration.ts index c2b504db2dab..96091a61c4c0 100644 --- a/packages/server-utils/src/integrations/hono/honoIntegration.ts +++ b/packages/server-utils/src/integrations/hono/honoIntegration.ts @@ -5,7 +5,7 @@ import { HTTP_SERVER } from '@sentry/conventions/op'; import type { IntegrationFn } from '@sentry/core'; import { debug, defineIntegration, startInactiveSpan } from '@sentry/core'; import { DEBUG_BUILD } from '../../debug-build'; -import type { Env, GetConnInfo, Hono, MiddlewareHandler } from './honoTypes'; +import type { Env, GetConnInfo, Hono, MatchedHandler, MiddlewareHandler } from './honoTypes'; import type { OrchestrionChannelContext } from '../../orchestrion/types'; import { CHANNELS } from '../../orchestrion/channels'; import { honoModuleNames } from '../../orchestrion/config/hono'; @@ -14,7 +14,8 @@ import { bindTracingChannelToSpan, safeChannelCallback } from '../../tracing-cha import { applyPatches } from './applyPatches'; import { INTERNAL_REQUEST_ORIGIN } from './constants'; import { createHonoRequestMiddleware } from './createHonoMiddleware'; -import { isMiddleware } from './isMiddleware'; +import { getMiddlewareFlags } from './isMiddleware'; +import { isMatchedHandlerList } from './matchResult'; import { extractPathname, isInternalRequestSpanActive } from './patchAppRequest'; import { wrapMiddlewareWithSpan } from './wrapMiddlewareSpan'; import type { SentryHonoMiddlewareOptions } from './types'; @@ -83,10 +84,14 @@ export function honoMiddleware(app: Hono, options: HonoIntegra }); } -// A Hono `matchResult[0]` entry: `[[handler, routeMeta], paramIndexMap]` — `compose` runs the -// handler (`entry[0][0]`) and the `matchedRoutes` getter reads `routeMeta` (`entry[0][1]`). -// oxlint-disable-next-line typescript/no-explicit-any -type MatchedHandlerEntry = [[any, any], any]; +function wrapMatchedMiddleware(handlers: MatchedHandler[]): void { + const middlewareFlags = getMiddlewareFlags(handlers.map(([[, route]]) => route)); + for (const [i, [pair]] of handlers.entries()) { + if (middlewareFlags[i]) { + pair[0] = wrapMiddlewareWithSpan(pair[0]); + } + } +} /** * Per-request Context hook. `#dispatch` builds `new Context(req, { matchResult })` before its @@ -103,47 +108,19 @@ function injectHonoInstrumentation( requestMiddleware: MiddlewareHandler, injectedHandlerLists: WeakSet, ): void { - const ctorOptions = message.arguments?.[1] as { matchResult?: [MatchedHandlerEntry[], unknown] } | undefined; + const ctorOptions = message.arguments?.[1] as { matchResult?: [unknown[], unknown] } | undefined; const handlers = ctorOptions?.matchResult?.[0]; if (!Array.isArray(handlers) || injectedHandlerLists.has(handlers)) { return; } injectedHandlerLists.add(handlers); - // Classify matched handlers with the same positional heuristic as `wrapSubAppMiddleware`: within a - // method+path group the last handler is the route handler and earlier ones are middleware; `.use()` - // registers as method 'ALL' with a lone genuine-middleware entry, so fall back to arity there. - // Position is needed because a route handler declared with an unused `next` param has middleware arity. - const lastIndexByKey = new Map(); - for (const [i, entry] of handlers.entries()) { - const routeMeta = entry?.[0]?.[1] as { method?: string; path?: string } | undefined; - if (routeMeta?.method != null && routeMeta.path != null) { - // \0 is a collision-free delimiter: it cannot appear in a valid HTTP method or URL path. - lastIndexByKey.set(`${routeMeta.method}\0${routeMeta.path}`, i); - } - } - - for (const [i, entry] of handlers.entries()) { - const pair = entry?.[0]; - const handler = pair?.[0]; - if (typeof handler !== 'function') { - continue; - } - - const routeMeta = pair?.[1] as { method?: string; path?: string } | undefined; - const isMW = - routeMeta?.method != null && routeMeta.path != null - ? lastIndexByKey.get(`${routeMeta.method}\0${routeMeta.path}`) !== i || - (routeMeta.method === 'ALL' && isMiddleware(handler)) - : isMiddleware(handler); - - if (isMW) { - pair[0] = wrapMiddlewareWithSpan(handler as MiddlewareHandler); - } + if (isMatchedHandlerList(handlers)) { + wrapMatchedMiddleware(handlers); } - // Prepend the Sentry request/response middleware. `routeMeta` is what the `matchedRoutes` getter - // exposes; its middleware arity keeps route-name resolution from picking it. + // Prepend the Sentry request/response middleware. `c.req.matchedRoutes` and `hono/route` read + // `routeMeta`; its middleware arity keeps route-name resolution from picking it. const routeMeta = { basePath: '/', path: '/*', method: 'ALL', handler: requestMiddleware }; handlers.unshift([[requestMiddleware, routeMeta], {}]); } diff --git a/packages/server-utils/src/integrations/hono/honoTypes.ts b/packages/server-utils/src/integrations/hono/honoTypes.ts index 9ef1d15b64a6..acd070cd3fe0 100644 --- a/packages/server-utils/src/integrations/hono/honoTypes.ts +++ b/packages/server-utils/src/integrations/hono/honoTypes.ts @@ -29,6 +29,10 @@ export interface HonoRoute { handler: (...args: any[]) => any; } +// An entry of Hono's match result (`matchResult[0]`). `compose` runs `entry[0][0]`; `hono/route` and +// `c.req.matchedRoutes` read `entry[0][1]`. +export type MatchedHandler = [[handler: HonoRoute['handler'], route: HonoRoute], paramIndexMap: unknown]; + export interface HonoRequest { raw: Request; method: string; diff --git a/packages/server-utils/src/integrations/hono/isMiddleware.ts b/packages/server-utils/src/integrations/hono/isMiddleware.ts index 9c18e43e8602..c0ca0c399908 100644 --- a/packages/server-utils/src/integrations/hono/isMiddleware.ts +++ b/packages/server-utils/src/integrations/hono/isMiddleware.ts @@ -1,3 +1,5 @@ +import type { HonoRoute } from './honoTypes'; + // Hono stores the original (unwrapped) handler under this key when it wraps a sub-app's handlers // for a custom `onError`. Unwrapping it lets us inspect the real handler instead of the wrapper. // See https://github.com/honojs/hono/blob/9f0dadf141a3242a6c3b77462c7d33c6ce0f599d/src/hono-base.ts#L224-L226 @@ -12,7 +14,7 @@ const COMPOSED_HANDLER = '__COMPOSED_HANDLER'; * https://github.com/honojs/hono/blob/97c6fe1f12298c715eb7b2da65b4b6e0d81682bb/src/utils/handler.ts#L8 * * This is only one signal — callers that must classify inline middleware sharing a method+path with - * their handler (e.g. `wrapSubAppMiddleware`) additionally need positional information. + * their handler additionally need positional information (`getMiddlewareFlags`). */ export function isMiddleware(handler: unknown): boolean { if (typeof handler !== 'function') { @@ -24,3 +26,19 @@ export function isMiddleware(handler: unknown): boolean { return (original as (...args: unknown[]) => unknown).length >= 2; } + +/** + * Tells for each route whether it is a middleware. Hono has no such flag, so within a method+path + * group the last handler is the route handler and earlier ones are middleware (`app.get('/path', mw, + * handler)`). `.use()` registers as method 'ALL' with a lone middleware entry, so arity decides there. + * Position is needed because a route handler declared with an unused `next` param has middleware arity. + */ +export function getMiddlewareFlags(routes: HonoRoute[]): boolean[] { + // \0 is a collision-free delimiter: it cannot appear in a valid HTTP method or URL path. + const groupKey = (route: HonoRoute): string => `${route.method}\0${route.path}`; + const lastIndexByKey = new Map(routes.map((route, i) => [groupKey(route), i])); + + return routes.map( + (route, i) => lastIndexByKey.get(groupKey(route)) !== i || (route.method === 'ALL' && isMiddleware(route.handler)), + ); +} diff --git a/packages/server-utils/src/integrations/hono/matchResult.ts b/packages/server-utils/src/integrations/hono/matchResult.ts new file mode 100644 index 000000000000..c271a3a7032b --- /dev/null +++ b/packages/server-utils/src/integrations/hono/matchResult.ts @@ -0,0 +1,26 @@ +import { isPlainObject } from '@sentry/core'; +import type { MatchedHandler } from './honoTypes'; + +function isMatchedHandler(entry: unknown): entry is MatchedHandler { + const pair: unknown = Array.isArray(entry) ? entry[0] : undefined; + if (!Array.isArray(pair)) { + return false; + } + + const [handler, route]: unknown[] = pair; + return ( + typeof handler === 'function' && + isPlainObject(route) && + typeof route.method === 'string' && + typeof route.path === 'string' && + typeof route.handler === 'function' + ); +} + +/** + * Checks the shape that `compose` and `hono/route` read from `matchResult[0]`. These are Hono + * internals, so the SDK does not read or wrap the entries of a list of another shape. + */ +export function isMatchedHandlerList(handlers: unknown): handlers is MatchedHandler[] { + return Array.isArray(handlers) && handlers.every(isMatchedHandler); +} diff --git a/packages/server-utils/src/integrations/hono/patchRoute.ts b/packages/server-utils/src/integrations/hono/patchRoute.ts index 4f13b1b760ad..3ee20927b0bd 100644 --- a/packages/server-utils/src/integrations/hono/patchRoute.ts +++ b/packages/server-utils/src/integrations/hono/patchRoute.ts @@ -2,7 +2,7 @@ import { debug, getOriginalFunction } from '@sentry/core'; import type { WrappedFunction } from '@sentry/core'; import { DEBUG_BUILD } from '../../debug-build'; import type { Hono, HonoRoute } from './honoTypes'; -import { isMiddleware } from './isMiddleware'; +import { getMiddlewareFlags } from './isMiddleware'; import { patchAppRequest } from './patchAppRequest'; import { wrapMiddlewareWithSpan } from './wrapMiddlewareSpan'; @@ -104,30 +104,12 @@ export function installRouteHookOnPrototype(honoBaseProto: HonoBaseProto): Route /** * Identifies middleware handlers in a sub-app's flat routes array and wraps them in spans. - * - * Heuristics (since Hono has no "isMiddleware" flag): - * 1. Position: `app.get('/path', mw, handler)` produces entries with the same method+path. - * All but the LAST are middleware (they call `next()`). - * 2. Arity (# of params) for method 'ALL': `.use()` handlers always have 2+ params (context, next), - * while `.all()` route handlers typically have 1 (`context` only). - * See: https://github.com/honojs/hono/blob/18fe604c8cefc2628240651b1af219692e1918c1/src/hono-base.ts#L156-L168 + * See: https://github.com/honojs/hono/blob/18fe604c8cefc2628240651b1af219692e1918c1/src/hono-base.ts#L156-L168 */ export function wrapSubAppMiddleware(routes: HonoRoute[]): void { - const lastIndexByKey = new Map(); - for (const [i, route] of routes.entries()) { - // \0 (null byte) is a collision-free delimiter: it cannot appear in a valid HTTP method name or URL path - lastIndexByKey.set(`${route.method}\0${route.path}`, i); - } - + const middlewareFlags = getMiddlewareFlags(routes); for (const [i, route] of routes.entries()) { - if (typeof route.handler !== 'function') { - continue; - } - - const isLastForGroup = lastIndexByKey.get(`${route.method}\0${route.path}`) === i; - - const isMW = !isLastForGroup || (route.method === 'ALL' && isMiddleware(route.handler)); - if (isMW) { + if (middlewareFlags[i] && typeof route.handler === 'function') { route.handler = wrapMiddlewareWithSpan(route.handler); } } From 1de41db68c0e0a7b82b0ed29b35114a4e1562da9 Mon Sep 17 00:00:00 2001 From: s1gr1d <32902192+s1gr1d@users.noreply.github.com> Date: Fri, 16 Oct 2026 14:42:02 +0200 Subject: [PATCH 5/7] ref(hono): Shorten comments Co-Authored-By: Claude Opus 5.5 --- .../src/integrations/hono/honoIntegration.ts | 4 ++-- .../server-utils/src/integrations/hono/honoTypes.ts | 3 +-- .../server-utils/src/integrations/hono/isMiddleware.ts | 10 +++++----- .../server-utils/src/integrations/hono/matchResult.ts | 4 ++-- .../server-utils/src/integrations/hono/patchRoute.ts | 3 +-- 5 files changed, 11 insertions(+), 13 deletions(-) diff --git a/packages/server-utils/src/integrations/hono/honoIntegration.ts b/packages/server-utils/src/integrations/hono/honoIntegration.ts index 96091a61c4c0..0ed8a6ac3a7b 100644 --- a/packages/server-utils/src/integrations/hono/honoIntegration.ts +++ b/packages/server-utils/src/integrations/hono/honoIntegration.ts @@ -119,8 +119,8 @@ function injectHonoInstrumentation( wrapMatchedMiddleware(handlers); } - // Prepend the Sentry request/response middleware. `c.req.matchedRoutes` and `hono/route` read - // `routeMeta`; its middleware arity keeps route-name resolution from picking it. + // Prepend the Sentry request/response middleware. Its arity marks it as a middleware, so its `/*` + // never becomes the route name. const routeMeta = { basePath: '/', path: '/*', method: 'ALL', handler: requestMiddleware }; handlers.unshift([[requestMiddleware, routeMeta], {}]); } diff --git a/packages/server-utils/src/integrations/hono/honoTypes.ts b/packages/server-utils/src/integrations/hono/honoTypes.ts index acd070cd3fe0..6ace7bb8f595 100644 --- a/packages/server-utils/src/integrations/hono/honoTypes.ts +++ b/packages/server-utils/src/integrations/hono/honoTypes.ts @@ -29,8 +29,7 @@ export interface HonoRoute { handler: (...args: any[]) => any; } -// An entry of Hono's match result (`matchResult[0]`). `compose` runs `entry[0][0]`; `hono/route` and -// `c.req.matchedRoutes` read `entry[0][1]`. +// One entry of Hono's `matchResult[0]`. export type MatchedHandler = [[handler: HonoRoute['handler'], route: HonoRoute], paramIndexMap: unknown]; export interface HonoRequest { diff --git a/packages/server-utils/src/integrations/hono/isMiddleware.ts b/packages/server-utils/src/integrations/hono/isMiddleware.ts index c0ca0c399908..ee7ad8d08afa 100644 --- a/packages/server-utils/src/integrations/hono/isMiddleware.ts +++ b/packages/server-utils/src/integrations/hono/isMiddleware.ts @@ -28,13 +28,13 @@ export function isMiddleware(handler: unknown): boolean { } /** - * Tells for each route whether it is a middleware. Hono has no such flag, so within a method+path - * group the last handler is the route handler and earlier ones are middleware (`app.get('/path', mw, - * handler)`). `.use()` registers as method 'ALL' with a lone middleware entry, so arity decides there. - * Position is needed because a route handler declared with an unused `next` param has middleware arity. + * Tells for each route if it is a middleware. Hono has no flag for this. In a method+path group, the + * last handler is the route handler (`app.get('/path', mw, handler)`). `.use()` adds middleware with + * method `ALL` and no route handler, so for `ALL` routes the arity also counts. Arity alone is not + * enough, because a route handler can declare an unused `next`. */ export function getMiddlewareFlags(routes: HonoRoute[]): boolean[] { - // \0 is a collision-free delimiter: it cannot appear in a valid HTTP method or URL path. + // `\0` cannot appear in an HTTP method or a URL path. const groupKey = (route: HonoRoute): string => `${route.method}\0${route.path}`; const lastIndexByKey = new Map(routes.map((route, i) => [groupKey(route), i])); diff --git a/packages/server-utils/src/integrations/hono/matchResult.ts b/packages/server-utils/src/integrations/hono/matchResult.ts index c271a3a7032b..e6bd298f1206 100644 --- a/packages/server-utils/src/integrations/hono/matchResult.ts +++ b/packages/server-utils/src/integrations/hono/matchResult.ts @@ -18,8 +18,8 @@ function isMatchedHandler(entry: unknown): entry is MatchedHandler { } /** - * Checks the shape that `compose` and `hono/route` read from `matchResult[0]`. These are Hono - * internals, so the SDK does not read or wrap the entries of a list of another shape. + * Checks the entry shape that `compose` and `hono/route` read. The match result is a Hono internal, so + * the SDK leaves other shapes alone. */ export function isMatchedHandlerList(handlers: unknown): handlers is MatchedHandler[] { return Array.isArray(handlers) && handlers.every(isMatchedHandler); diff --git a/packages/server-utils/src/integrations/hono/patchRoute.ts b/packages/server-utils/src/integrations/hono/patchRoute.ts index 3ee20927b0bd..398df72fb924 100644 --- a/packages/server-utils/src/integrations/hono/patchRoute.ts +++ b/packages/server-utils/src/integrations/hono/patchRoute.ts @@ -103,8 +103,7 @@ export function installRouteHookOnPrototype(honoBaseProto: HonoBaseProto): Route } /** - * Identifies middleware handlers in a sub-app's flat routes array and wraps them in spans. - * See: https://github.com/honojs/hono/blob/18fe604c8cefc2628240651b1af219692e1918c1/src/hono-base.ts#L156-L168 + * Wraps the middleware handlers of a sub-app's routes in spans. */ export function wrapSubAppMiddleware(routes: HonoRoute[]): void { const middlewareFlags = getMiddlewareFlags(routes); From 03e9663c3cb5b3b51dd020c99d6135cfb3e0a3ca Mon Sep 17 00:00:00 2001 From: s1gr1d <32902192+s1gr1d@users.noreply.github.com> Date: Fri, 23 Oct 2026 15:02:11 +0200 Subject: [PATCH 6/7] build: Deduplicate hono in pnpm-lock.yaml --- pnpm-lock.yaml | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e90a4460f6d6..cc9587b89d54 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -363,7 +363,7 @@ importers: version: link:../../packages/hono hono: specifier: ^4.13.7 - version: 4.13.7 + version: 4.13.9 mysql: specifier: ^2.18.1 version: 2.18.1 @@ -486,7 +486,7 @@ importers: version: 6.0.218(zod@4.6.5) hono: specifier: ^4.13.7 - version: 4.13.7 + version: 4.13.9 langchain: specifier: ^1.5.16 version: 1.5.16(@langchain/core@1.2.17(@opentelemetry/api@1.10.0)(openai@5.18.1(ws@8.21.3)(zod@4.6.5))(ws@8.21.3))(@opentelemetry/api@1.10.0)(openai@5.18.1(ws@8.21.3)(zod@4.6.5))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(ws@8.21.3) @@ -615,7 +615,7 @@ importers: version: 21.3.10 '@hono/node-server': specifier: ^2.0.10 - version: 2.0.10(hono@4.13.7) + version: 2.0.10(hono@4.13.9) '@koa/router': specifier: ^12.0.1 version: 12.0.2(supports-color@10.0.0) @@ -729,7 +729,7 @@ importers: version: 2.0.1-rc.22(crossws@0.4.10(srvx@0.11.22)) hono: specifier: ^4.13.7 - version: 4.13.7 + version: 4.13.9 http-terminator: specifier: ^3.2.0 version: 3.2.0 @@ -12883,10 +12883,6 @@ packages: resolution: {integrity: sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==} engines: {node: '>=0.10.0'} - hono@4.13.7: - resolution: {integrity: sha512-c8/gF9ac8Y78/agExVocyLevgR+JlpNB444Py0FSX8pJoPdYUfUzRcXtYEYGwt6l19qIlVZPN5Mfsw9jFShmQQ==} - engines: {node: '>=16.9.0'} - hono@4.13.9: resolution: {integrity: sha512-7dMkQmZoC4E6F7AtaQSPhlWAdnBti+j7rreMZl8QB4jFiEhP9TWbGWUMi8WYzBCgmgulxuvLQupKqo+Co6Omyg==} engines: {node: '>=16.9.0'} @@ -23643,9 +23639,9 @@ snapshots: '@hapi/bourne': 3.0.0 '@hapi/hoek': 11.0.4 - '@hono/node-server@2.0.10(hono@4.13.7)': + '@hono/node-server@2.0.10(hono@4.13.9)': dependencies: - hono: 4.13.7 + hono: 4.13.9 '@humanwhocodes/config-array@0.11.14(supports-color@10.0.0)': dependencies: @@ -32206,8 +32202,6 @@ snapshots: dependencies: parse-passwd: 1.0.0 - hono@4.13.7: {} - hono@4.13.9: {} hono@5.0.0-rc.1: {} From 668df692cf6369362b102e98c43c254f5d4954e2 Mon Sep 17 00:00:00 2001 From: s1gr1d <32902192+s1gr1d@users.noreply.github.com> Date: Fri, 23 Oct 2026 16:55:38 +0200 Subject: [PATCH 7/7] fix(hono): Name every tuple member for TypeScript 5.0 --- packages/server-utils/src/integrations/hono/honoTypes.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server-utils/src/integrations/hono/honoTypes.ts b/packages/server-utils/src/integrations/hono/honoTypes.ts index a750b3e07dfb..ce8661a073f8 100644 --- a/packages/server-utils/src/integrations/hono/honoTypes.ts +++ b/packages/server-utils/src/integrations/hono/honoTypes.ts @@ -30,7 +30,7 @@ export interface HonoRoute { } // One entry of Hono's `matchResult[0]`. -export type MatchedHandler = [[handler: HonoRoute['handler'], route: HonoRoute], paramIndexMap: unknown]; +export type MatchedHandler = [pair: [handler: HonoRoute['handler'], route: HonoRoute], paramIndexMap: unknown]; export interface HonoRequest { raw: Request;