Skip to content

feat(tracing): export wrapHandlerWithTracing for manual handler wrapping#1369

Merged
pi0 merged 10 commits into
h3js:mainfrom
logaretm:fix/wrap-find-route-with-tracing
Apr 29, 2026
Merged

feat(tracing): export wrapHandlerWithTracing for manual handler wrapping#1369
pi0 merged 10 commits into
h3js:mainfrom
logaretm:fix/wrap-find-route-with-tracing

Conversation

@logaretm

@logaretm logaretm commented Apr 16, 2026

Copy link
Copy Markdown
Contributor

Summary

Exports wrapHandlerWithTracing from h3/tracing — a utility that wraps a single EventHandler with diagnostics_channel tracing (type: "route"). Designed to be called once at init/codegen time, not per request.

This enables frameworks like Nitro to wrap file-based route handlers at build time in their virtual routing module, ensuring route traces are emitted for handlers resolved via custom ~findRoute implementations — without any per-request overhead.

API

import { wrapHandlerWithTracing } from "h3/tracing";

const tracedHandler = wrapHandlerWithTracing(myHandler);
// tracedHandler emits h3.request traces with type: "route"
// Already-traced handlers (.__traced__ === true) are returned as-is
// Returns handler unchanged if diagnostics_channel is unavailable

Companion PR

Test plan

  • Wraps a handler so route traces are emitted via diagnostics_channel
  • Idempotent — wrapping twice returns the same wrapper
  • Emits exactly one trace per request on a pre-wrapped handler

🤖 Generated with Claude Code

Copilot AI review requested due to automatic review settings April 16, 2026 20:19
@logaretm logaretm requested a review from pi0 as a code owner April 16, 2026 20:19
@coderabbitai

coderabbitai Bot commented Apr 16, 2026

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The tracing plugin now intercepts h3["~findRoute"] via a getter/setter that maintains a wrapped resolver; on each resolved route it mutates route.data to wrap route.data.handler with wrapEventHandler and any route.data.middleware entries with wrapMiddleware.

Changes

Cohort / File(s) Summary
Route Handler Interception
src/tracing.ts
Add a property accessor for h3["~findRoute"] that builds and returns a wrappedFindRoute; the wrapper calls the original resolver and mutates returned route.data in-place to wrap handler and middleware, and updates when ~findRoute is reassigned to avoid recursion.
Test Coverage
test/tracing.test.ts
Add 6 Vitest cases verifying tracingPlugin() behavior with request-time ~findRoute: route asyncStart emission, post-install reassignment support, non-enumerable ~findRoute, no recursive wrapping when replacing via previous ref, no double-wrap for cached route objects, and in-place mutation of data.middleware entries.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested reviewers

  • pi0

Poem

🐰 I tucked a getter where findRoute used to be,
so handlers wear traces now for every plea.
I wrap each hop, but only once —
no loops, no doubles, just tracing fun. 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Title check ⚠️ Warning The PR title mentions exporting wrapHandlerWithTracing, but the actual changes wrap ~findRoute with tracing; no export of this function is shown in the code changes. Update the title to accurately reflect the main change: 'feat(tracing): wrap findRoute with tracing wrapper' or similar, as the primary objective is wrapping ~findRoute, not exporting wrapHandlerWithTracing.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The PR directly addresses issue #4211 by implementing wrapper logic for ~findRoute to ensure route handlers resolved at request time are wrapped with tracing, matching the stated objective.
Out of Scope Changes check ✅ Passed All changes are scoped to tracing plugin functionality: modifications to src/tracing.ts add ~findRoute wrapping logic, and test/tracing.test.ts adds comprehensive test coverage for the new behavior. No unrelated changes detected.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@pkg-pr-new

pkg-pr-new Bot commented Apr 16, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/h3@1369

commit: e42432e

@codecov

codecov Bot commented Apr 16, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
src/tracing.ts (1)

91-100: Mutating route.data is idempotent but shared — confirm intent.

route.data.handler = wrapEventHandler(...) and the middleware remap mutate the returned route's data object. If ~findRoute returns a reference to an entry in h3["~routes"] (the typical radix router behavior), the handler/middleware are replaced in-place. This is safe here because wrapEventHandler/wrapMiddleware are idempotent via __traced__, and it mirrors the existing h3.on wrapping (Line 121-122). Worth a one-line comment noting the intentional mutation so a future refactor doesn’t accidentally clone the route and break the idempotence on subsequent requests.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/tracing.ts` around lines 91 - 100, Add a one-line explanatory comment
inside the wrappedFindRoute function above the mutations to route.data (where
route.data.handler is set with wrapEventHandler and route.data.middleware is
remapped with wrapMiddleware) stating that this in-place mutation is intentional
because findRoute returns shared references (e.g., h3["~routes"]) and the
wrappers are idempotent via __traced__; mention it mirrors the existing h3.on
wrapping behavior (originalFindRoute / h3.on) and warn future maintainers not to
clone the route object or change to non-idempotent wrappers.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/tracing.ts`:
- Around line 101-108: The property descriptor for the private method
"~findRoute" is currently making it enumerable; update the Object.defineProperty
call for h3 with key "~findRoute" to use enumerable: false (keep configurable
and the getter/setter semantics intact) so the non-enumerable convention for
~-prefixed members is preserved; locate the defineProperty that references h3,
"~findRoute", wrappedFindRoute and originalFindRoute and change only the
enumerable flag to false.
- Around line 90-108: The setter for h3["~findRoute"] mutates the closure
variable originalFindRoute which breaks safe fallback chains and enables
infinite recursion; instead, make originalFindRoute immutable and expose it as a
separate property callers can capture (e.g., h3["~findRouteUnwrapped"] or
h3["~findRouteOriginal"]) and change the setter to not reassign
originalFindRoute (either ignore assignments or store the user-supplied function
in a separate variable that the getter can wrap without overwriting
originalFindRoute); update wrappedFindRoute to call the immutable
originalFindRoute (or the composed wrapper) so fallback chains that capture the
original function remain safe.

---

Nitpick comments:
In `@src/tracing.ts`:
- Around line 91-100: Add a one-line explanatory comment inside the
wrappedFindRoute function above the mutations to route.data (where
route.data.handler is set with wrapEventHandler and route.data.middleware is
remapped with wrapMiddleware) stating that this in-place mutation is intentional
because findRoute returns shared references (e.g., h3["~routes"]) and the
wrappers are idempotent via __traced__; mention it mirrors the existing h3.on
wrapping behavior (originalFindRoute / h3.on) and warn future maintainers not to
clone the route object or change to non-idempotent wrappers.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 3cb1fcb0-a31c-47be-a055-dbe51d985dd6

📥 Commits

Reviewing files that changed from the base of the PR and between e2f8cbd and 644edd2.

📒 Files selected for processing (2)
  • src/tracing.ts
  • test/tracing.test.ts

Comment thread src/tracing.ts Outdated
Comment thread src/tracing.ts Outdated

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds tracing support for route handlers returned via a custom ~findRoute implementation (e.g., Nitro-style file-based routes) so that “route” tracing spans are emitted even when routes are resolved dynamically at request time.

Changes:

  • Wrap ~findRoute in the tracing plugin to trace handlers/middleware returned from dynamic route resolution.
  • Add a regression test covering Nitro-style file-based routing where routes are not pushed into ~routes.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
src/tracing.ts Wrapes ~findRoute results so returned handlers/middleware get tracing instrumentation.
test/tracing.test.ts Adds a test ensuring route traces are emitted for handlers returned only from a custom ~findRoute.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread test/tracing.test.ts Outdated
Comment thread test/tracing.test.ts Outdated
Comment thread src/tracing.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
test/tracing.test.ts (2)

1206-1248: Strong test for the double-wrapping guard.

Exactly-3 assertion on routeAsyncStarts is the right shape — it pins the __traced__ short-circuit in wrapEventHandler (src/tracing.ts:61-75) against the memoized-route case from frameworks like nitro.

Two minor notes:

  1. Because wrappedFindRoute mutates route.data.handler in place (src/tracing.ts context snippet 1), after the first request cachedRoute.data.handler is replaced with the traced wrapper. An additional assertion that cachedRoute.data.handler.__traced__ === true and that it's the same reference across runs would make the non-double-wrap contract explicit rather than inferred from the count.
  2. If the wrapper ever regresses to returning a new wrapper each call, the count would still be 3 (one per request) — the current assertion wouldn't catch handler re-wrapping per se, only per-request duplication. Consider also spying on wrapEventHandler behavior indirectly by asserting handler identity stability:
Optional stronger assertion
     await run();
     await run();
     await run();
+
+    // Handler should be wrapped exactly once and reused across calls.
+    expect((cachedRoute.data.handler as any).__traced__).toBe(true);
 
     await new Promise((resolve) => setTimeout(resolve, 10));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/tracing.test.ts` around lines 1206 - 1248, Add explicit assertions that
the memoized route handler is mutated to the traced wrapper and remains the same
reference across runs: after the first run assert
cachedRoute.data.handler.__traced__ === true and save a reference (e.g., const
firstHandler = cachedRoute.data.handler) then after subsequent runs assert
cachedRoute.data.handler === firstHandler; this verifies
wrapEventHandler/wrappedFindRoute actually short-circuits re-wrapping rather
than relying solely on the asyncStart count. Use the existing cachedRoute,
tracingPlugin, and wrapEventHandler/wrappedFindRoute behavior to locate where to
add these checks in the test.

1160-1204: LGTM — good coverage for nitro-style ~findRoute.

Test correctly verifies route tracing works when handlers are only exposed via a custom ~findRoute (not pushed into ~routes), which is exactly the regression from nitrojs/nitro#4211.

One small optional tightening: the test only asserts routeEvents.length > 0 but doesn't verify the handler actually ran / returned its value. Adding an assertion on app.handler(event)'s resolved value (or on routeEvents[0].asyncStart?.data.event.url.pathname === "/file-route") would make a regression where the wrapper swallows the handler result more obvious.

Also, the comment on Line 1187-1188 referring to "the order documented in the suggested fix" reads as review-context leakage — consider rephrasing to something self-contained like "Apply tracing plugin after ~findRoute is set to verify the getter/setter wrapper picks up the reassigned function."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/tracing.test.ts` around lines 1160 - 1204, Add a concrete assertion that
the handler actually executed and returned the expected value and make the
comment self-contained: after awaiting app.handler(event) capture its resolved
result and assert it equals "file-based response" (or assert
routeEvents[0].asyncStart?.data.event.url.pathname === "/file-route") to ensure
the wrapper didn't swallow the return value; also change the comment referencing
"the order documented in the suggested fix" to a self-contained sentence like
"Apply tracing plugin after ~findRoute is set to verify the getter/setter
wrapper picks up the reassigned function." Reference H3Core, tracingPlugin,
app["~findRoute"], H3Event, and routeEvents in the test to locate the changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@test/tracing.test.ts`:
- Around line 1206-1248: Add explicit assertions that the memoized route handler
is mutated to the traced wrapper and remains the same reference across runs:
after the first run assert cachedRoute.data.handler.__traced__ === true and save
a reference (e.g., const firstHandler = cachedRoute.data.handler) then after
subsequent runs assert cachedRoute.data.handler === firstHandler; this verifies
wrapEventHandler/wrappedFindRoute actually short-circuits re-wrapping rather
than relying solely on the asyncStart count. Use the existing cachedRoute,
tracingPlugin, and wrapEventHandler/wrappedFindRoute behavior to locate where to
add these checks in the test.
- Around line 1160-1204: Add a concrete assertion that the handler actually
executed and returned the expected value and make the comment self-contained:
after awaiting app.handler(event) capture its resolved result and assert it
equals "file-based response" (or assert
routeEvents[0].asyncStart?.data.event.url.pathname === "/file-route") to ensure
the wrapper didn't swallow the return value; also change the comment referencing
"the order documented in the suggested fix" to a self-contained sentence like
"Apply tracing plugin after ~findRoute is set to verify the getter/setter
wrapper picks up the reassigned function." Reference H3Core, tracingPlugin,
app["~findRoute"], H3Event, and routeEvents in the test to locate the changes.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 8f4b6b2d-f990-4370-8df6-fce6e46313f9

📥 Commits

Reviewing files that changed from the base of the PR and between 644edd2 and bf32dce.

📒 Files selected for processing (1)
  • test/tracing.test.ts

logaretm and others added 3 commits April 16, 2026 17:02
Previously the ~findRoute getter returned a singleton wrapper that
delegated to a mutable `originalFindRoute`. Framework code that
captures the current ~findRoute and reassigns it to a delegating
wrapper (`const prev = app["~findRoute"]; app["~findRoute"] = (e) =>
prev(e)`) would then infinite-recurse: `prev` pointed at the singleton,
which now called back into the new delegator.

Build a fresh wrapper closure on every set so captured references stay
stable. Adds regression tests for the recursion scenario and for
double-wrap avoidance across repeated calls.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Addresses PR review feedback:
- Keep the ~ prefix convention: the descriptor installed by tracingPlugin
  was enumerable, leaking ~findRoute into Object.keys/spread/for…in.
- Add coverage for ~findRoute assigned *after* the plugin registers
  (the typical nitro ordering), complementing the existing before-plugin
  test.
- Tighten the nitro-style test to assert the route trace matches the
  requested pathname instead of only checking event count.
- Assert the descriptor stays non-enumerable.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/tracing.ts (1)

98-100: Optional: skip reallocating route.data.middleware when all entries are already traced.

On every request the wrapper unconditionally allocates a fresh array via .map(wrapMiddleware), even when route.data is a cached object whose middleware entries are already __traced__. wrapMiddleware is a no-op in that case, but the array allocation/assignment still runs per request. Minor and safe to defer.

♻️ Proposed micro-optimization
-        if (route?.data.middleware) {
-          route.data.middleware = route.data.middleware.map((m) => wrapMiddleware(m));
+        if (route?.data.middleware && route.data.middleware.some((m) => !(m as MaybeTracedMiddleware).__traced__)) {
+          route.data.middleware = route.data.middleware.map((m) => wrapMiddleware(m));
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/tracing.ts` around lines 98 - 100, The code currently always reassigns
route.data.middleware = route.data.middleware.map((m) => wrapMiddleware(m));
avoid allocating a new array when all middleware are already traced by checking
the __traced__ flag first: inspect route.data.middleware (use
Array.prototype.some or every) to detect any entry without m.__traced__, and
only call .map(wrapMiddleware) and reassign when at least one middleware is not
traced; leave route.data.middleware untouched otherwise. Ensure you reference
route.data.middleware, wrapMiddleware, and the __traced__ property in your
change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/tracing.ts`:
- Around line 98-100: The code currently always reassigns route.data.middleware
= route.data.middleware.map((m) => wrapMiddleware(m)); avoid allocating a new
array when all middleware are already traced by checking the __traced__ flag
first: inspect route.data.middleware (use Array.prototype.some or every) to
detect any entry without m.__traced__, and only call .map(wrapMiddleware) and
reassign when at least one middleware is not traced; leave route.data.middleware
untouched otherwise. Ensure you reference route.data.middleware, wrapMiddleware,
and the __traced__ property in your change.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 16b32424-95da-45d3-b264-2ae24841ef60

📥 Commits

Reviewing files that changed from the base of the PR and between bf32dce and 51f21fe.

📒 Files selected for processing (2)
  • src/tracing.ts
  • test/tracing.test.ts

logaretm and others added 2 commits April 16, 2026 17:11
…p test

- Comment in wrapFindRoute noting the in-place mutation of route.data is
  intentional (shared references + idempotent wrappers) so a future
  refactor doesn't accidentally clone the route.
- Double-wrap test now pins handler reference stability and the
  __traced__ flag explicitly, not just the per-request event count.
- Drop review-context phrasing from the after-plugin test comment.

Skipped: micro-opt to avoid reallocating route.data.middleware when all
entries are already traced (wrapMiddleware already no-ops on __traced__;
the extra .some() check is a wash) and a return-value assertion in the
nitro-style test (already indirectly covered via tracePromise emission).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Match the handler path's in-place mutation pattern so repeated resolves
of a cached route.data don't allocate a fresh middleware array per
request. wrapMiddleware remains idempotent via __traced__, so the loop
is a cheap no-op after the first call.

Add a test pinning array and entry identity across requests.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@pi0 pi0 changed the title fix: wrap findRoute with tracing wrapper fix(tracing): wrap findRoute with tracing wrapper Apr 29, 2026
…~findRoute patching

Per maintainer feedback, move ~findRoute wrapping out of the tracing
plugin and into a standalone exported helper. Frameworks like nitro can
opt in by calling wrapFindRouteWithTracing() on their own ~findRoute
function, keeping h3's plugin lean and the runtime cost explicit.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Comment thread src/tracing.ts Outdated
Export a simpler `wrapHandlerWithTracing` that wraps a single EventHandler
at init/codegen time instead of wrapping the entire ~findRoute function at
runtime. This eliminates per-request overhead — tracing is applied once
during build, not on every route lookup.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@logaretm logaretm changed the title fix(tracing): wrap findRoute with tracing wrapper feat(tracing): export wrapHandlerWithTracing for build-time handler wrapping Apr 29, 2026

@pi0 pi0 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💯

@pi0 pi0 changed the title feat(tracing): export wrapHandlerWithTracing for build-time handler wrapping feat(tracing): export wrapHandlerWithTracing for manual handler wrapping Apr 29, 2026
@pi0 pi0 merged commit db7ef49 into h3js:main Apr 29, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants