From 7e0cfbf7ed1b60326268485eafacb28debd8f7c7 Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Wed, 29 Apr 2026 11:49:32 -0400 Subject: [PATCH 1/8] fix(tracing): wrap ~findRoute handlers with h3 tracing channel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Nitro resolves file-based routes at request time via h3["~findRoute"], bypassing h3["~routes"]. The h3 tracing plugin only wraps routes registered through ~routes / .on() / .use(), so file-based route handlers were never traced — only middleware spans were emitted. Use the new wrapFindRouteWithTracing helper exported from h3/tracing to wrap ~findRoute so route handler spans are emitted too. Ref: h3js/h3#1369 Co-Authored-By: Claude Opus 4.6 --- src/build/virtual/tracing.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/build/virtual/tracing.ts b/src/build/virtual/tracing.ts index bcda97c50a..64371a87b0 100644 --- a/src/build/virtual/tracing.ts +++ b/src/build/virtual/tracing.ts @@ -12,8 +12,11 @@ export default function tracing(nitro: Nitro) { imports.push(`import { tracingPlugin as srvxTracing } from "srvx/tracing";`); } if (h3) { - imports.push(`import { tracingPlugin as h3Tracing } from "h3/tracing";`); + imports.push(`import { tracingPlugin as h3Tracing, wrapFindRouteWithTracing } from "h3/tracing";`); setup.push(` h3Tracing()(nitroApp.h3);`); + setup.push(` if (nitroApp.h3["~findRoute"]) {`); + setup.push(` nitroApp.h3["~findRoute"] = wrapFindRouteWithTracing(nitroApp.h3["~findRoute"]);`); + setup.push(` }`); } return [ From 67a498f716c152372265c40a16256b4d1139ff01 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Wed, 29 Apr 2026 15:50:49 +0000 Subject: [PATCH 2/8] chore: apply automated updates --- src/build/virtual/tracing.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/build/virtual/tracing.ts b/src/build/virtual/tracing.ts index 64371a87b0..095134463c 100644 --- a/src/build/virtual/tracing.ts +++ b/src/build/virtual/tracing.ts @@ -12,10 +12,14 @@ export default function tracing(nitro: Nitro) { imports.push(`import { tracingPlugin as srvxTracing } from "srvx/tracing";`); } if (h3) { - imports.push(`import { tracingPlugin as h3Tracing, wrapFindRouteWithTracing } from "h3/tracing";`); + imports.push( + `import { tracingPlugin as h3Tracing, wrapFindRouteWithTracing } from "h3/tracing";` + ); setup.push(` h3Tracing()(nitroApp.h3);`); setup.push(` if (nitroApp.h3["~findRoute"]) {`); - setup.push(` nitroApp.h3["~findRoute"] = wrapFindRouteWithTracing(nitroApp.h3["~findRoute"]);`); + setup.push( + ` nitroApp.h3["~findRoute"] = wrapFindRouteWithTracing(nitroApp.h3["~findRoute"]);` + ); setup.push(` }`); } From a1d705dafb6b9c790cd99a745f1c6b2519dfcc24 Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Wed, 29 Apr 2026 12:14:48 -0400 Subject: [PATCH 3/8] feat: wrap route handlers with tracing at build time Use h3's new `wrapHandlerWithTracing` export to wrap compiled route handlers during codegen instead of patching ~findRoute at runtime. This ensures route traces are emitted with zero per-request overhead. Co-Authored-By: Claude Opus 4.6 --- src/build/virtual/routing.ts | 22 +++++++++++++++++++++- src/build/virtual/tracing.ts | 9 +-------- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/src/build/virtual/routing.ts b/src/build/virtual/routing.ts index 6b0c6d1c6b..c77d878e7e 100644 --- a/src/build/virtual/routing.ts +++ b/src/build/virtual/routing.ts @@ -15,10 +15,13 @@ export default function routing(nitro: Nitro) { "_importHash" ); + const traceH3 = !!nitro.options.tracingChannel?.h3; + return /* js */ ` import * as __routeRules__ from "#nitro/runtime/route-rules"; import * as srvxNode from "srvx/node" import * as h3 from "h3"; +${traceH3 ? `import { wrapHandlerWithTracing as __wrapHandler__ } from "h3/tracing";` : ""} export const findRouteRules = ${nitro.routing.routeRules.compileToString({ serialize: serializeRouteRule, matchAll: true })} @@ -41,7 +44,7 @@ ${allHandlers ) .join("\n")} -export const findRoute = ${nitro.routing.routes.compileToString({ serialize: serializeHandler })} +export const findRoute = ${nitro.routing.routes.compileToString({ serialize: traceH3 ? tracedSerializeHandler : serializeHandler })} export const findRoutedMiddleware = ${nitro.routing.routedMiddleware.compileToString({ serialize: serializeHandler, matchAll: true })}; @@ -89,6 +92,23 @@ function serializeHandlerFn(h: NitroEventHandler & { _importHash: string }): str return code; } +function tracedSerializeHandler(h: MaybeArray): string { + const meta = Array.isArray(h) ? h[0] : h; + + return `{${[ + `route:${JSON.stringify(meta.route)}`, + meta.method && `method:${JSON.stringify(meta.method)}`, + meta.meta && `meta:${JSON.stringify(meta.meta)}`, + `handler:__wrapHandler__(${ + Array.isArray(h) + ? `multiHandler(${h.map((handler) => serializeHandlerFn(handler)).join(",")})` + : serializeHandlerFn(h) + })`, + ] + .filter(Boolean) + .join(",")}}`; +} + function serializeRouteRule(h: NitroRouteRules & { _route: string }): string { return `[${Object.entries(h) .filter(([name, options]) => options !== undefined && name[0] !== "_") diff --git a/src/build/virtual/tracing.ts b/src/build/virtual/tracing.ts index 095134463c..bcda97c50a 100644 --- a/src/build/virtual/tracing.ts +++ b/src/build/virtual/tracing.ts @@ -12,15 +12,8 @@ export default function tracing(nitro: Nitro) { imports.push(`import { tracingPlugin as srvxTracing } from "srvx/tracing";`); } if (h3) { - imports.push( - `import { tracingPlugin as h3Tracing, wrapFindRouteWithTracing } from "h3/tracing";` - ); + imports.push(`import { tracingPlugin as h3Tracing } from "h3/tracing";`); setup.push(` h3Tracing()(nitroApp.h3);`); - setup.push(` if (nitroApp.h3["~findRoute"]) {`); - setup.push( - ` nitroApp.h3["~findRoute"] = wrapFindRouteWithTracing(nitroApp.h3["~findRoute"]);` - ); - setup.push(` }`); } return [ From 89a518f0230ee147c41455196381a71130b1b466 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Wed, 29 Apr 2026 16:16:04 +0000 Subject: [PATCH 4/8] chore: apply automated updates --- src/build/virtual/routing.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/build/virtual/routing.ts b/src/build/virtual/routing.ts index c77d878e7e..cc7250c995 100644 --- a/src/build/virtual/routing.ts +++ b/src/build/virtual/routing.ts @@ -92,7 +92,9 @@ function serializeHandlerFn(h: NitroEventHandler & { _importHash: string }): str return code; } -function tracedSerializeHandler(h: MaybeArray): string { +function tracedSerializeHandler( + h: MaybeArray +): string { const meta = Array.isArray(h) ? h[0] : h; return `{${[ From 40a2aa9b0bace35d1cd0e81fd92071fefbaf08a9 Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Tue, 5 May 2026 13:59:16 -0400 Subject: [PATCH 5/8] chore: upgrade h3 to ^2.0.1-rc.22 import wrapHandlerWithTracing from h3/tracing export --- package.json | 2 +- pnpm-lock.yaml | 21 +++++++++++++++++++-- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index b4cfb99d80..54159b4e6b 100644 --- a/package.json +++ b/package.json @@ -72,7 +72,7 @@ "crossws": "^0.4.5", "db0": "^0.3.4", "env-runner": "^0.1.7", - "h3": "^2.0.1-rc.20", + "h3": "^2.0.1-rc.22", "hookable": "^6.1.1", "nf3": "^0.3.16", "ocache": "^0.1.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e6687a2011..4295de5dbd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -30,8 +30,8 @@ importers: specifier: ^0.1.7 version: 0.1.7(miniflare@4.20260426.0) h3: - specifier: ^2.0.1-rc.20 - version: 2.0.1-rc.20(crossws@0.4.5) + specifier: ^2.0.1-rc.22 + version: 2.0.1-rc.22(crossws@0.4.5) hookable: specifier: ^6.1.1 version: 6.1.1 @@ -4381,6 +4381,16 @@ packages: crossws: optional: true + h3@2.0.1-rc.22: + resolution: {integrity: sha512-Esv0DMIuPkCTSWCA0vO73vcTqwzH1wjSrAO1TXNu/K3up1sZHa9EKMapbmxCDYBeymC3fVTk4qxp7ogQWQ+KgA==} + engines: {node: '>=20.11.1'} + hasBin: true + peerDependencies: + crossws: ^0.4.1 + peerDependenciesMeta: + crossws: + optional: true + has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} @@ -10659,6 +10669,13 @@ snapshots: optionalDependencies: crossws: 0.4.5(srvx@0.11.15) + h3@2.0.1-rc.22(crossws@0.4.5): + dependencies: + rou3: 0.8.1 + srvx: 0.11.15 + optionalDependencies: + crossws: 0.4.5(srvx@0.11.15) + has-flag@4.0.0: {} has-symbols@1.1.0: {} From 823d77943f5e665c414837c94e48ef48f466dc4e Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Fri, 8 May 2026 11:46:52 +0200 Subject: [PATCH 6/8] up --- src/build/virtual/routing.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/build/virtual/routing.ts b/src/build/virtual/routing.ts index cc7250c995..052df751ef 100644 --- a/src/build/virtual/routing.ts +++ b/src/build/virtual/routing.ts @@ -20,8 +20,7 @@ export default function routing(nitro: Nitro) { return /* js */ ` import * as __routeRules__ from "#nitro/runtime/route-rules"; import * as srvxNode from "srvx/node" -import * as h3 from "h3"; -${traceH3 ? `import { wrapHandlerWithTracing as __wrapHandler__ } from "h3/tracing";` : ""} +import * as h3 from "h3";${traceH3 ? `\nimport { wrapHandlerWithTracing } from "h3/tracing";` : ""} export const findRouteRules = ${nitro.routing.routeRules.compileToString({ serialize: serializeRouteRule, matchAll: true })} @@ -101,7 +100,7 @@ function tracedSerializeHandler( `route:${JSON.stringify(meta.route)}`, meta.method && `method:${JSON.stringify(meta.method)}`, meta.meta && `meta:${JSON.stringify(meta.meta)}`, - `handler:__wrapHandler__(${ + `handler:wrapHandlerWithTracing(${ Array.isArray(h) ? `multiHandler(${h.map((handler) => serializeHandlerFn(handler)).join(",")})` : serializeHandlerFn(h) From 556a0ae68a697b2af73a22d412e1bd7f90895c1e Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Fri, 8 May 2026 11:49:21 +0200 Subject: [PATCH 7/8] update --- src/build/virtual/routing.ts | 35 +++++++++-------------------------- 1 file changed, 9 insertions(+), 26 deletions(-) diff --git a/src/build/virtual/routing.ts b/src/build/virtual/routing.ts index 052df751ef..e24b3b86df 100644 --- a/src/build/virtual/routing.ts +++ b/src/build/virtual/routing.ts @@ -43,7 +43,7 @@ ${allHandlers ) .join("\n")} -export const findRoute = ${nitro.routing.routes.compileToString({ serialize: traceH3 ? tracedSerializeHandler : serializeHandler })} +export const findRoute = ${nitro.routing.routes.compileToString({ serialize: (h) => serializeHandler(h, { tracing: traceH3 }) })} export const findRoutedMiddleware = ${nitro.routing.routedMiddleware.compileToString({ serialize: serializeHandler, matchAll: true })}; @@ -63,18 +63,20 @@ function uniqueBy(arr: T[], key: keyof T): T[] { type MaybeArray = T | T[]; -function serializeHandler(h: MaybeArray): string { +function serializeHandler( + h: MaybeArray, + opts: { tracing?: boolean } = {} +): string { const meta = Array.isArray(h) ? h[0] : h; + const handler = Array.isArray(h) + ? `multiHandler(${h.map((handler) => serializeHandlerFn(handler)).join(",")})` + : serializeHandlerFn(h); return `{${[ `route:${JSON.stringify(meta.route)}`, meta.method && `method:${JSON.stringify(meta.method)}`, meta.meta && `meta:${JSON.stringify(meta.meta)}`, - `handler:${ - Array.isArray(h) - ? `multiHandler(${h.map((handler) => serializeHandlerFn(handler)).join(",")})` - : serializeHandlerFn(h) - }`, + `handler:${opts.tracing ? `wrapHandlerWithTracing(${handler})` : handler}`, ] .filter(Boolean) .join(",")}}`; @@ -91,25 +93,6 @@ function serializeHandlerFn(h: NitroEventHandler & { _importHash: string }): str return code; } -function tracedSerializeHandler( - h: MaybeArray -): string { - const meta = Array.isArray(h) ? h[0] : h; - - return `{${[ - `route:${JSON.stringify(meta.route)}`, - meta.method && `method:${JSON.stringify(meta.method)}`, - meta.meta && `meta:${JSON.stringify(meta.meta)}`, - `handler:wrapHandlerWithTracing(${ - Array.isArray(h) - ? `multiHandler(${h.map((handler) => serializeHandlerFn(handler)).join(",")})` - : serializeHandlerFn(h) - })`, - ] - .filter(Boolean) - .join(",")}}`; -} - function serializeRouteRule(h: NitroRouteRules & { _route: string }): string { return `[${Object.entries(h) .filter(([name, options]) => options !== undefined && name[0] !== "_") From b4662f449bd5c5835edd29659150aec18896d05a Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Fri, 8 May 2026 11:54:38 +0200 Subject: [PATCH 8/8] add test --- test/unit/virtual-routing.test.ts | 56 +++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 test/unit/virtual-routing.test.ts diff --git a/test/unit/virtual-routing.test.ts b/test/unit/virtual-routing.test.ts new file mode 100644 index 0000000000..e33aefc62d --- /dev/null +++ b/test/unit/virtual-routing.test.ts @@ -0,0 +1,56 @@ +import { describe, expect, it } from "vitest"; +import type { Nitro, NitroEventHandler } from "nitro/types"; + +import routing from "../../src/build/virtual/routing.ts"; + +function createNitroStub(tracingChannel: Nitro["options"]["tracingChannel"]): Nitro { + const handler: NitroEventHandler & { _importHash: string } = { + route: "/foo", + method: "GET", + handler: "/path/to/handler.ts", + _importHash: "_abc123", + }; + + return { + options: { + tracingChannel, + }, + routing: { + routes: { + routes: [{ route: "/foo", method: "GET", data: handler }], + compileToString: ({ serialize }: { serialize: (h: unknown) => string }) => + `{"/foo":${serialize(handler)}}`, + }, + routedMiddleware: { + routes: [], + compileToString: () => `{}`, + }, + globalMiddleware: [], + routeRules: { + compileToString: () => `() => []`, + }, + }, + } as unknown as Nitro; +} + +describe("virtual/routing template", () => { + it("does not wrap route handlers when tracingChannel is disabled", () => { + const template = routing(createNitroStub(undefined)).template(); + expect(template).not.toContain("h3/tracing"); + expect(template).not.toContain("wrapHandlerWithTracing"); + }); + + it("does not wrap route handlers when tracingChannel.h3 is false", () => { + const template = routing( + createNitroStub({ srvx: true, h3: false, unstorage: true }) + ).template(); + expect(template).not.toContain("h3/tracing"); + expect(template).not.toContain("wrapHandlerWithTracing"); + }); + + it("wraps route handlers with wrapHandlerWithTracing when tracingChannel.h3 is true", () => { + const template = routing(createNitroStub({ srvx: true, h3: true, unstorage: true })).template(); + expect(template).toContain(`import { wrapHandlerWithTracing } from "h3/tracing"`); + expect(template).toContain("wrapHandlerWithTracing(h3.toEventHandler(_abc123))"); + }); +});