Skip to content

feat: experimental tracing support#1251

Merged
pi0 merged 17 commits intoh3js:mainfrom
logaretm:feat/tracing-channels
Dec 30, 2025
Merged

feat: experimental tracing support#1251
pi0 merged 17 commits intoh3js:mainfrom
logaretm:feat/tracing-channels

Conversation

@logaretm
Copy link
Contributor

@logaretm logaretm commented Dec 1, 2025

As per the discussion I created, this PR implements support for tracing channel events, allow subscribers to create instrumentations without monkey patching.

Implementation

  • Emits h3.request.handler tracing events with full lifecycle hooks (start, end, asyncStart, asyncEnd, error)
  • Includes request context.

Example usage

import { tracingChannel } from 'node:diagnostics_channel';

const reqChannel = tracingChannel('h3.request.handler');

reqChannel.subscribe({
  start: (data) => console.log(`${data.event.req.method} ${data.event.url.pathname}`),
  asyncEnd: (data) => console.log(`${data.event.req.method} completed`),
  // ... asyncStart, end, error
});

Span Relationships

Since we have an onion effect here with each middleware being able to wait for the response/next, then it means if the SDK provider isn't careful, they might end up creating middleware spans as children of one another rather than siblings.

HTTP Request
└── Middleware: auth
			└── Middleware: cors
						└── Middleware: logging
									└── Fetch Handler

This is fine at h3 level because it is technically correct from an execution standpoint, but each provider can handle this manually when subscribing to those diagnostic events, and they can manually unscope each span from the previous one to get the desired effect.

HTTP Request
├── Middleware: auth
├── Middleware: cors
├── Middleware: logging
└── Fetch Handler

TODO:

  • Figure out a way to fix the nested middleware trace calls
  • Register plugin on nested apps
  • trace app.mount Fetch handlers

Summary by CodeRabbit

  • New Features

    • Introduced a tracing plugin for H3 apps to emit diagnostic events for route and middleware execution, with options to enable/disable route or middleware tracing and support for async/error events and nested apps.
    • Public package entry added to expose the tracing module.
  • Tests

    • Added comprehensive test suite covering tracing behavior, async flow, error reporting, middleware chains and plugin options.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Dec 1, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

📝 Walkthrough

Walkthrough

Adds a new tracing plugin for H3 using node:diagnostics_channel, exposes a new public export entry "./tracing" in package.json, implements runtime wrapping of middleware and route handlers to emit trace payloads, and adds comprehensive tests and test setup changes to exercise tracing behavior.

Changes

Cohort / File(s) Summary
Public Export Configuration
package.json
Added a new export entry "./tracing" pointing to "./dist/tracing.mjs".
Tracing Plugin Implementation
src/tracing.ts
New file providing tracingPlugin(traceOpts?), plus types/interfaces: TracingRequestEvent, MaybeTracedMiddleware, MaybeTracedEventHandler, TracingPluginOptions. Uses diagnostics_channel (when available) to create h3.fetch channel and wraps middleware and route handlers (idempotent via __traced__) to emit asyncStart/asyncEnd and error traces; overrides on, use, and mount to handle new and mounted apps.
Test Bootstrap
test/_setup.ts
Imports tracingPlugin and conditionally injects it into H3.app via setupBaseTest when opts.tracing is truthy; adds optional tracing?: boolean to TestOptions.
Tracing Test Suite
test/tracing.test.ts
New comprehensive test suite validating diagnostics_channel trace emission for routes and middlewares, asyncStart/asyncEnd pairing, error traces, middleware chains, async middleware behavior, mounted/nested apps, and plugin options (traceMiddlewares, traceRoutes).

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐇✨ I nibbled code and found a thread,
Traces hopping where requests are led,
Middleware wrapped in gentle loop,
Routes report each tiny hoop,
Async tails and errors too—hip hooray for every view!

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: experimental tracing support' clearly and concisely summarizes the main change: addition of tracing capabilities to H3. It matches the primary objective of implementing tracing channel events.
✨ Finishing touches
  • 📝 Generate docstrings

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.

@logaretm logaretm marked this pull request as ready for review December 15, 2025 11:07
@logaretm logaretm requested a review from pi0 as a code owner December 15, 2025 11:07
Copilot AI review requested due to automatic review settings December 15, 2025 11:07
@logaretm
Copy link
Contributor Author

logaretm commented Dec 15, 2025

Tracing has been implemented as a plugin, now looking at a way to auto trace nested H3 instances, perhaps by calling the plugin recursively whenever an app is mounted.

EDIT: Implemented but unsure what to do about fetch handlers.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (4)
src/tracing.ts (2)

131-148: Consider using .at(-1) for cleaner array access.

The eslint disable comment suggests using .at(-1) would be preferred. Since the project targets Node.js >=20.11.1 (per package.json), .at() is fully supported.

-      // eslint-disable-next-line unicorn/prefer-at
-      const lastRoute = instance["~routes"][instance["~routes"].length - 1];
+      const lastRoute = instance["~routes"].at(-1);

82-99: Consider using traceSync for synchronous handlers to avoid unnecessary Promise wrapping.

The async () => await middleware(...args) pattern forces all handlers through a Promise, even synchronous ones. Node.js diagnostics_channel provides both tracePromise (for async operations) and traceSync (for synchronous operations). Using tracePromise for sync handlers adds overhead and changes the execution model unnecessarily.

This could be optimized by detecting handler synchronicity and using traceSync where appropriate, though this would require more complex wrapper logic to distinguish between sync and async handlers at runtime.

test/_setup.ts (1)

160-160: Consider simplifying the conditional plugin array.

The current pattern works but could be more concise using spread with conditional:

-      plugins: [opts.tracing ? tracingPlugin() : undefined].filter((p) => !!p),
+      plugins: [...(opts.tracing ? [tracingPlugin()] : [])],

Alternatively, if plugins accepts undefined values gracefully, the filter could be removed entirely.

test/tracing.test.ts (1)

270-296: Minor: Unnecessary manual mock restoration.

Line 294 manually assigns originalOnError back to t.hooks.onError, but the test setup's afterEach already calls vi.resetAllMocks(). This manual restoration is redundant and the pattern of reassigning a mock reference may not work as expected (it replaces the mock object rather than resetting it).

       } finally {
         listener.cleanup();
-        t.hooks.onError = originalOnError;
       }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d6475e1 and b60af42.

📒 Files selected for processing (4)
  • package.json (1 hunks)
  • src/tracing.ts (1 hunks)
  • test/_setup.ts (3 hunks)
  • test/tracing.test.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/tracing.ts (1)
src/h3.ts (2)
  • H3Core (36-112)
  • handler (53-67)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Agent
🔇 Additional comments (14)
src/tracing.ts (3)

1-35: LGTM!

The type definitions are well-structured. The payload interfaces (H3InitPayload, H3MountPayload, H3THandlerTracePayload) provide clear contracts for tracing consumers, and the MaybeTracedMiddleware/MaybeTracedEventHandler types properly extend the base types with the __traced__ marker.


40-78: LGTM!

The graceful degradation pattern is well-implemented. The plugin correctly:

  1. Handles environments where diagnostics_channel is unavailable
  2. Stores and properly delegates to the original mount method
  3. Publishes events only when subscribers exist (avoiding unnecessary overhead)

150-170: LGTM!

The argument parsing correctly handles both use(middleware, opts) and use(route, middleware, opts) signatures. The comment explains why middleware is wrapped at the arg level (to avoid tracing H3's internal wrappers), which differs from the on handler wrapping strategy.

test/_setup.ts (2)

8-8: LGTM!

Clean import of the new tracing plugin.


189-189: LGTM!

The optional tracing flag in TestOptions provides a clean way to enable tracing in specific test suites.

package.json (1)

25-29: LGTM!

The new ./tracing export entry follows the established pattern with proper TypeScript types and default module paths. This allows consumers to import tracing functionality separately: import { tracingPlugin } from 'h3/tracing'.

test/tracing.test.ts (8)

1-10: LGTM!

Clean imports with proper type imports for payload interfaces. The imports from node:diagnostics_channel and the tracing module are well-organized.


11-25: LGTM!

The TracingEvent type accurately models the tracing channel lifecycle events, and createH3WithTracing provides a clean factory for tests needing a standalone traced H3 instance.


27-70: LGTM!

The createTracingListener helper provides proper test isolation with:

  • Separate handlers for each lifecycle event
  • A cleanup function to unsubscribe all handlers
  • Collection of events for assertions

The pattern of returning { events, cleanup } enables clean test structure with try/finally.


75-91: LGTM!

Good test structure with proper cleanup in finally block. The test verifies that the init channel fires exactly once and provides the correct app reference.


177-212: LGTM!

The test properly verifies that tracing events fire for handlers, checking for asyncStart/asyncEnd events and route-type events. The payload structure verification ensures the event contains required fields.


298-327: LGTM!

Good test coverage for middleware tracing. The test verifies:

  • Middleware events have type: "middleware"
  • Route events have type: "route"
  • Multiple middlewares produce expected event counts

377-394: LGTM!

This is an important regression test ensuring the tracing instrumentation doesn't affect the normal request/response flow. It validates that response data is correctly returned despite the tracing wrapper.


468-470: LGTM!

Properly enables tracing for the entire test matrix via the { tracing: true } option.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR implements tracing channel support for H3 apps, enabling observability instrumentation without monkey patching. The implementation uses Node.js's built-in diagnostics_channel API to emit lifecycle events for handlers and middleware execution.

Key changes:

  • Adds a tracingPlugin that emits h3.init, h3.mount, and h3.request.handler tracing events
  • Wraps route handlers and middleware to emit tracing events with full lifecycle hooks (start, end, asyncStart, asyncEnd, error)
  • Exports tracing types and plugin via a new h3/tracing entry point

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 12 comments.

File Description
src/tracing.ts Core tracing plugin implementation with channel event publishers and handler wrappers
test/tracing.test.ts Comprehensive test suite covering tracing channels, event payloads, and middleware/route tracing
test/_setup.ts Adds optional tracing flag to test configuration to conditionally enable tracing plugin
package.json Adds ./tracing export entry for the tracing module

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

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (8)
test/tracing.test.ts (2)

123-123: Inconsistent test naming style.

This test uses a colon-separated format inconsistent with other tests in the file that use plain English descriptions.

-    it("tracing:h3.request.handler:asyncStart/asyncEnd fire for async handlers", async () => {
+    it("tracing channels fire asyncStart and asyncEnd events for async handlers", async () => {

157-157: Inconsistent test naming style.

Same naming inconsistency as the previous test.

-    it("tracing:h3.request.handler:error fires when handler throws", async () => {
+    it("tracing channels fire error events when handler throws", async () => {
src/tracing.ts (6)

10-24: Consider adding JSDoc documentation for public interfaces.

The exported types HandlerType, H3THandlerTracePayload, and TracingPluginOptions are part of the public API for tracing consumers. Adding JSDoc comments would help users understand the purpose and expected usage of each type.


46-53: Async wrapper forces all handlers to become async.

The async () => await middleware(...args) pattern creates unnecessary promise overhead for synchronous handlers. Since tracePromise should handle both sync and async functions, consider passing the handler result directly.

-        return requestHandlerChannel.tracePromise(
-          async () => await middleware(...args),
-          {
+        return requestHandlerChannel.tracePromise(
+          () => middleware(...args),
+          {

Also applies to: 65-72


99-103: Direct mutation of route object.

The code mutates lastRoute.handler and lastRoute.middleware directly. Consider creating a new route object using the spread pattern (like lines 81-87) to avoid unexpected side effects if routes are cached elsewhere.

-      if (lastRoute) {
-        lastRoute.handler = wrapEventHandler(lastRoute.handler);
-        lastRoute.middleware = lastRoute.middleware?.map((m) =>
-          wrapMiddleware(m),
-        );
-      }
+      if (lastRoute) {
+        const lastRouteIndex = instance["~routes"].length - 1;
+        instance["~routes"][lastRouteIndex] = {
+          ...lastRoute,
+          handler: wrapEventHandler(lastRoute.handler),
+          middleware: lastRoute.middleware?.map((m) => wrapMiddleware(m)),
+        } satisfies H3Route;
+      }

49-52: The route field is never populated in trace payloads.

The H3THandlerTracePayload interface includes route?: string, but neither wrapMiddleware nor wrapEventHandler sets this field. For route handlers, the matched route path could be obtained from the route configuration and included in the payload to improve tracing usefulness.

Also applies to: 68-71


90-107: No guard against double-wrapping of h3.on.

If the tracing plugin is registered multiple times, h3.on gets wrapped repeatedly, leading to duplicate trace events and performance degradation. Consider adding a __traced__ check similar to handler wrapping.

+    if ((h3.on as any).__traced__) {
+      return;
+    }
+
     const originalOn = h3.on;

     h3.on = (...args) => {
       // ... existing implementation
     };
+
+    Object.defineProperty(h3.on, "__traced__", {
+      value: true,
+      configurable: true,
+      enumerable: false,
+      writable: false,
+    });

109-129: No guard against double-wrapping of h3.use.

Same concern as h3.on — if registered multiple times, h3.use will be wrapped repeatedly.

🧹 Nitpick comments (5)
test/tracing.test.ts (3)

160-165: Mock implementation restoration may not properly reset the mock.

Saving the mock reference and reassigning it later doesn't properly restore the mock's original implementation. If onError is a Vitest mock, consider using mockRestore() or mockReset() instead.

-      const originalOnError = t.hooks.onError;
-      t.hooks.onError.mockImplementation(() => {
-        // Silence error - we're testing the tracing channel
-      });
+      t.hooks.onError.mockImplementation(() => {
+        // Silence error - we're testing the tracing channel
+      });

And in the finally block:

-        t.hooks.onError = originalOnError;
+        t.hooks.onError.mockRestore();

216-238: Consider adding test for TracingPluginOptions.

The test suite validates middleware and route tracing well, but lacks coverage for TracingPluginOptions (traceMiddlewares: false and traceRoutes: false). These options allow selective disabling of tracing but have no tests verifying the behavior.

Would you like me to help generate tests that verify:

  1. traceMiddlewares: false prevents middleware trace events
  2. traceRoutes: false prevents route handler trace events

240-262: Consider verifying the route field in payload.

The test checks for event, url, and req.method but doesn't verify the route field defined in H3THandlerTracePayload. Since route is part of the public API, it would be valuable to test its presence/value.

src/tracing.ts (2)

131-131: TODO: Trace mount remains unaddressed.

The TODO comment indicates mount tracing is still pending. This aligns with the PR comments mentioning exploration of auto-tracing nested H3 instances.

Would you like me to help implement mount tracing, or should this be tracked as a separate issue?


133-135: Return value is correct but consider early return consistency.

The function returns h3 at the end, but note that the early return on line 36 returns undefined (implicitly). For consistency, consider returning h3 in the early bail-out case as well, or document that plugins may return undefined when no-op.

     // If tracingChannel is not available, then we can't trace request handlers
     if (!tracingChannel) {
-      return;
+      return h3;
     }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b60af42 and c9c5081.

📒 Files selected for processing (2)
  • src/tracing.ts (1 hunks)
  • test/tracing.test.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
test/tracing.test.ts (2)
src/tracing.ts (1)
  • H3THandlerTracePayload (12-16)
test/_setup.ts (2)
  • TestOptions (185-190)
  • describeMatrix (25-52)
src/tracing.ts (1)
src/index.ts (7)
  • H3Event (24-24)
  • Middleware (43-43)
  • EventHandler (35-35)
  • H3Plugin (6-6)
  • definePlugin (17-17)
  • H3Route (7-7)
  • MiddlewareOptions (12-12)
🔇 Additional comments (4)
test/tracing.test.ts (2)

1-11: LGTM!

Clean imports and well-typed TracingEvent interface that correctly represents all possible tracing lifecycle phases.


13-56: Well-structured listener helper with proper cleanup.

The createTracingListener function correctly manages the subscription lifecycle and provides a cleanup method that unsubscribes all handlers. This ensures tests don't leak event listeners.

src/tracing.ts (2)

1-8: LGTM!

Clean imports of the necessary H3 types for the tracing plugin implementation.


31-37: Good defensive handling for environments without diagnostics_channel.

The graceful no-op when tracingChannel is unavailable ensures the plugin doesn't break in environments that lack this Node.js feature.

@logaretm logaretm force-pushed the feat/tracing-channels branch from c7af908 to 3ef3a60 Compare December 15, 2025 11:44
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

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

18-19: Consider using Symbol instead of string property for __traced__.

Using a string property __traced__ on function objects could potentially conflict with user-defined properties. A Symbol would provide better isolation and follow JavaScript best practices for internal metadata.

+const TRACED = Symbol('traced');
+
-type MaybeTracedMiddleware = Middleware & { __traced__?: boolean };
-type MaybeTracedEventHandler = EventHandler & { __traced__?: boolean };
+type MaybeTracedMiddleware = Middleware & { [TRACED]?: boolean };
+type MaybeTracedEventHandler = EventHandler & { [TRACED]?: boolean };

Then update the checks on lines 51, 64, 70, and 83 to use [TRACED] instead of __traced__.


24-33: Add JSDoc documentation for public interface.

The TracingPluginOptions interface is part of the public API but lacks documentation explaining its purpose and usage. Consider adding JSDoc comments to improve the developer experience.

 /**
  * Options for the tracing plugin.
+ * 
+ * @example
+ * ```typescript
+ * tracingPlugin({
+ *   traceMiddlewares: true,
+ *   traceRoutes: true
+ * })
+ * ```
  */
 export interface TracingPluginOptions {
   /**
    * Whether to trace middleware executions.
+   * @default true
    */
   traceMiddlewares?: boolean;
   /**
    * Whether to trace route executions.
+   * @default true
    */
   traceRoutes?: boolean;
 }

55-57: Remove redundant async wrapper for better performance.

The async () => middleware(...args) pattern forces all middleware to become async, creating unnecessary promise overhead for synchronous middleware. Since tracePromise likely handles both sync and async functions, you can call the middleware directly.

       const wrappedMiddleware: MaybeTracedMiddleware = (...args) => {
         return requestHandlerChannel.tracePromise(
-          async () => middleware(...args),
+          () => middleware(...args),
           {
             event: args[0],
             type: "middleware",
           } satisfies H3THandlerTracePayload,
         );
       };

74-76: Remove redundant async wrapper for better performance.

Same issue as the middleware wrapper: the async () => handler(...args) pattern creates unnecessary promise overhead for synchronous handlers.

       const wrappedHandler: MaybeTracedEventHandler = (...args) => {
         return requestHandlerChannel.tracePromise(
-          async () => handler(...args),
+          () => handler(...args),
           {
             event: args[0],
             type: "route",
           } satisfies H3THandlerTracePayload,
         );
       };

107-112: Inconsistent mutation pattern compared to initial route wrapping.

This code directly mutates lastRoute.handler and lastRoute.middleware, which is inconsistent with the immutable pattern used in lines 89-97 where new route objects are created with spread operators. While the __traced__ flag prevents double-wrapping, maintaining consistency with the immutable approach would be cleaner and less error-prone.

       // eslint-disable-next-line unicorn/prefer-at
-      const lastRoute = instance["~routes"][instance["~routes"].length - 1];
-      if (lastRoute) {
-        lastRoute.handler = wrapEventHandler(lastRoute.handler);
-        lastRoute.middleware = lastRoute.middleware?.map((m) =>
-          wrapMiddleware(m),
-        );
+      const lastRouteIndex = instance["~routes"].length - 1;
+      if (lastRouteIndex >= 0) {
+        const lastRoute = instance["~routes"][lastRouteIndex];
+        instance["~routes"][lastRouteIndex] = {
+          ...lastRoute,
+          handler: wrapEventHandler(lastRoute.handler),
+          middleware: lastRoute.middleware?.map((m) => wrapMiddleware(m)),
+        } satisfies H3Route;
       }

140-140: TODO: Trace mount functionality.

The mount tracing is not yet implemented. This would complete the tracing coverage for all H3 lifecycle hooks.

Do you want me to generate an implementation for mount tracing or open a new issue to track this task?

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c9c5081 and c7af908.

📒 Files selected for processing (2)
  • src/tracing.ts (1 hunks)
  • test/tracing.test.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • test/tracing.test.ts
🔇 Additional comments (3)
src/tracing.ts (3)

88-97: LGTM: Proper immutable wrapping of existing middleware and routes.

The code correctly uses .map() with spread operators to create new route objects rather than mutating existing ones. This approach is safer and follows immutability best practices.


119-138: LGTM: h3.use override correctly handles multiple signatures.

The wrapper properly handles both the route-based and standalone middleware signatures, wrapping middleware at the argument level as intended. This approach ensures tracing works correctly with H3's internal middleware processing.


38-39: The definePlugin usage here is correct. The type annotation (traceOpts?: TracingPluginOptions) => H3Plugin accurately reflects what definePlugin returns. The implementation pattern definePlugin((h3, traceOpts?: TracingPluginOptions) => {...}) correctly passes the optional traceOpts parameter through to the plugin body via closure, where it is properly used on lines 51 and 70 via optional chaining (traceOpts?.traceMiddlewares and traceOpts?.traceRoutes).

Likely an incorrect or invalid review comment.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (2)
test/tracing.test.ts (2)

124-124: Test naming is inconsistent with other tests in the file.

This test uses the colon-separated format "tracing:h3.request.handler:asyncStart/asyncEnd" while other tests use plain English descriptions. Consider renaming to match the style of other tests, such as "tracing channels fire asyncStart and asyncEnd events for async handlers".


158-158: Test naming is inconsistent with other tests in the file.

This test uses the colon-separated format "tracing:h3.request.handler:error" while other tests use plain English descriptions. Consider renaming to match the style of other tests, such as "tracing channels fire error events when handler throws".

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

55-57: Consider removing redundant async wrapper for better performance.

The async () => middleware(...args) pattern forces synchronous middleware to become async, adding promise overhead. Since tracePromise typically handles both sync and async functions, you can likely pass the function directly.

Apply this diff:

       const wrappedMiddleware: MaybeTracedMiddleware = (...args) => {
         return requestHandlerChannel.tracePromise(
-          async () => middleware(...args),
+          () => middleware(...args),
           {
             event: args[0],
             type: "middleware",
           } satisfies H3THandlerTracePayload,
         );
       };

Apply the same pattern to wrapEventHandler at Line 76.


108-112: Consider creating new route object for consistency.

While direct mutation works here, it's inconsistent with lines 89-96 which create new route objects using the spread operator. Creating a new object would improve consistency and avoid potential issues if routes are referenced elsewhere.

Apply this diff:

       const lastRoute = instance["~routes"][instance["~routes"].length - 1];
       if (lastRoute) {
-        lastRoute.handler = wrapEventHandler(lastRoute.handler);
-        lastRoute.middleware = lastRoute.middleware?.map((m) =>
-          wrapMiddleware(m),
-        );
+        instance["~routes"][instance["~routes"].length - 1] = {
+          ...lastRoute,
+          handler: wrapEventHandler(lastRoute.handler),
+          middleware: lastRoute.middleware?.map((m) => wrapMiddleware(m)),
+        } satisfies H3Route;
       }

140-140: Offer to implement nested app tracing.

The PR objectives mention that registering the plugin on nested apps remains an open TODO. This would complete the tracing functionality for mounted H3 instances.

Would you like me to help implement tracing for h3.mount to automatically trace nested H3 apps? This could follow a similar pattern to the h3.on override.


10-16: Consider adding JSDoc describing event lifecycle.

The H3THandlerTracePayload interface would benefit from documentation explaining when tracing events fire (start, asyncStart, asyncEnd, end, error) and their relationship to request processing.

Example:

/**
 * Payload sent to the tracing channels.
 * 
 * Tracing events fire throughout the request lifecycle:
 * - start: Handler invocation begins (sync)
 * - asyncStart: Async handler execution begins
 * - asyncEnd: Async handler execution completes (includes result/error)
 * - end: Handler invocation ends (sync)
 * - error: Handler throws an error
 */
export interface H3THandlerTracePayload {
  event: H3Event;
  type: "middleware" | "route";
}

18-19: Consider using Symbol for __traced__ flag.

Using a string property like __traced__ could potentially conflict with user-defined properties. A Symbol would avoid naming conflicts and make the implementation more robust.

Example:

const TRACED = Symbol('traced');
type MaybeTracedMiddleware = Middleware & { [TRACED]?: boolean };
type MaybeTracedEventHandler = EventHandler & { [TRACED]?: boolean };

// Then use: middleware[TRACED] instead of middleware.__traced__
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c7af908 and 3ef3a60.

📒 Files selected for processing (4)
  • package.json (1 hunks)
  • src/tracing.ts (1 hunks)
  • test/_setup.ts (3 hunks)
  • test/tracing.test.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • test/_setup.ts
  • package.json
🧰 Additional context used
🧬 Code graph analysis (2)
test/tracing.test.ts (3)
src/tracing.ts (2)
  • H3THandlerTracePayload (13-16)
  • tracingPlugin (38-143)
test/_setup.ts (2)
  • TestOptions (185-190)
  • describeMatrix (25-52)
src/index.ts (1)
  • H3 (19-19)
src/tracing.ts (2)
src/index.ts (6)
  • H3Event (24-24)
  • Middleware (43-43)
  • EventHandler (35-35)
  • H3Plugin (6-6)
  • H3Route (7-7)
  • MiddlewareOptions (12-12)
src/h3.ts (1)
  • handler (53-67)
🔇 Additional comments (2)
test/tracing.test.ts (1)

14-57: LGTM!

The tracing listener helper is well-designed with proper subscription/cleanup lifecycle and provides good test isolation.

src/tracing.ts (1)

38-48: LGTM!

The plugin correctly uses definePlugin with options as the second parameter and gracefully handles environments where diagnostics_channel is unavailable.

@logaretm logaretm force-pushed the feat/tracing-channels branch from e9c489c to a630b35 Compare December 15, 2025 11:54
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (2)
test/tracing.test.ts (2)

126-126: Inconsistent test naming style.

The test name uses a colon-separated format that is inconsistent with other test names in the file. Other tests use plain English descriptions like "tracing channels fire for handlers".


160-160: Inconsistent test naming style.

The test name uses a colon-separated format that is inconsistent with other test names in the file. Consider using a plain English description like "tracing channels fire error events when handler throws".

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

789-1209: Consider extracting H3Core test setup to reduce duplication.

The H3Core tests follow a repetitive pattern: dynamic imports, manual route/middleware setup, ~findRoute mocking, and event creation. While the tests provide valuable coverage, extracting the common setup into a helper function could improve maintainability.

Example helper:

async function createH3CoreTestSetup(config?: { traceMiddlewares?: boolean; traceRoutes?: boolean }) {
  const { H3Core } = await import("../src/h3.ts");
  const { tracingPlugin } = await import("../src/tracing.ts");
  const { H3Event } = await import("../src/event.ts");
  
  return { H3Core, tracingPlugin, H3Event };
}

This would reduce the 40+ lines of duplicated dynamic imports across the test suite.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 09ff659 and ce6b8ab.

📒 Files selected for processing (1)
  • test/tracing.test.ts
🧰 Additional context used
🧬 Code graph analysis (1)
test/tracing.test.ts (5)
src/tracing.ts (2)
  • H3THandlerTracePayload (13-16)
  • tracingPlugin (38-159)
test/_setup.ts (1)
  • TestOptions (185-190)
src/index.ts (4)
  • HTTPError (75-75)
  • H3 (19-19)
  • H3Core (19-19)
  • H3Event (24-24)
src/event.ts (2)
  • res (77-79)
  • H3Event (33-144)
src/h3.ts (4)
  • H3 (114-233)
  • H3 (235-235)
  • H3Core (36-112)
  • request (69-94)
🔇 Additional comments (2)
test/tracing.test.ts (2)

1-124: LGTM! Well-structured test foundation.

The imports, type definitions, and createTracingListener helper provide a solid foundation for the test suite. The first two tests properly verify basic tracing functionality and event pairing.


188-787: Excellent test coverage.

This section provides comprehensive coverage of middleware tracing, nested apps, configuration options, and fetch handler scenarios. The tests properly verify payload structure, event isolation, and configuration behavior.

@pi0 pi0 changed the title feat: Implement tracing channels feat: experimental tracing support Dec 30, 2025
Copy link
Member

@pi0 pi0 left a comment

Choose a reason for hiding this comment

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

🚀

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (1)
src/tracing.ts (1)

99-118: Route mutation after registration works but differs from the immutable pattern used earlier.

Lines 110-113 mutate the lastRoute object directly, while lines 88-97 use an immutable spread pattern. For consistency and to avoid potential issues with cached routes, consider using the same immutable approach here.

This was mentioned in a previous review comment. If you've intentionally kept the mutation pattern here for a specific reason (e.g., the route object is freshly created and not shared), no action is needed.

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

144-155: Duck-typing for H3 instance detection could match non-H3 objects.

The check "register" in input will match any object with a register method, not just H3 instances. If another framework or object happens to have a register method, this could cause unexpected behavior.

Consider a more specific check:

-        if ("register" in input) {
+        if ("register" in input && "~routes" in input) {
           input.register(tracingPlugin(traceOpts));
         }

This adds an additional H3-specific internal property check to reduce false positives.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ce6b8ab and 1186397.

📒 Files selected for processing (3)
  • package.json
  • src/tracing.ts
  • test/tracing.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • test/tracing.test.ts
🧰 Additional context used
🧬 Code graph analysis (1)
src/tracing.ts (3)
src/event.ts (1)
  • H3Event (33-144)
src/index.ts (8)
  • H3Event (24-24)
  • Middleware (43-43)
  • EventHandler (35-35)
  • H3Plugin (6-6)
  • H3 (19-19)
  • H3Core (19-19)
  • H3Route (7-7)
  • MiddlewareOptions (12-12)
src/h3.ts (4)
  • H3 (114-233)
  • H3 (235-235)
  • H3Core (36-112)
  • handler (53-67)
🔇 Additional comments (5)
src/tracing.ts (4)

1-19: Well-structured type definitions and imports.

The TracingRequestEvent interface clearly defines the payload shape for trace events, and the MaybeTracedMiddleware/MaybeTracedEventHandler types provide a clean way to track wrapping state.


38-48: Graceful degradation when diagnostics_channel is unavailable.

The early return when tracingChannel is not available is a good pattern—this allows the plugin to be registered unconditionally without runtime errors in environments that don't support diagnostics_channel.


88-97: Clean immutable transformation of existing routes and middleware.

Using spread operator and satisfies to create new route objects instead of mutating is a good pattern that avoids issues with shared references.


50-86: The async wrappers are necessary and correctly implemented. Node.js's diagnostics_channel.TracingChannel.tracePromise API specifically requires a promise-returning function, so async () => middleware(...args) and async () => handler(...args) cannot be simplified without breaking the tracing contract. The current approach is correct.

package.json (1)

25-26: The new tracing export is correctly configured for TypeScript type resolution.

The build configuration automatically includes src/tracing.ts via the inferExports() function that discovers entries from package.json exports. With dts: { resolve: true } enabled in the obuild configuration, the build process will generate the corresponding ./dist/tracing.d.mts file, enabling TypeScript consumers to import types from h3/tracing.

@pi0 pi0 merged commit d6b5f2a into h3js:main Dec 30, 2025
4 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