Skip to content

Conversation

@logaretm
Copy link
Contributor

@logaretm logaretm commented Nov 9, 2025

This PR adds tracing instrumentation for middleware and fetch handlers using Node.js diagnostics/tracing channels, enabling integration with observability tools like OpenTelemetry and Sentry.

Implementation

  • Emits srvx.middleware and srvx.fetch tracing events with full lifecycle hooks (start, end, asyncStart, asyncEnd, error)
  • Includes request context, server info, middleware names, and execution order in trace data

I think this has to be available out of the box, so it "just works" with SDK providers rather than OPT in.

Example usage

import { tracingChannel } from 'node:diagnostics_channel';

const middlewareChannel = tracingChannel('srvx.middleware');

middlewareChannel.subscribe({
  start: (data) => console.log(`${data.name} started`),
  asyncEnd: (data) => console.log(`${data.name} completed`),
  // ... asyncStart, end, error
});

Span Relationships

Something I noticed is 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

Now this can be fine since 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
  • test it out with express, hono, and other frameworks.
  • Figure out how to handle streams

Summary by CodeRabbit

  • New Features

    • Added a tracing plugin that instruments server middleware and fetch handlers for performance monitoring and diagnostics.
    • Includes a new example project demonstrating tracing setup and usage.
  • Documentation

    • Updated guides and README with tracing example and starter template.

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

@logaretm logaretm changed the title feat: add tracing channels for fetch and middleware operations feat: add tracing channels for fetch and middleware handlers Nov 9, 2025
@logaretm logaretm force-pushed the awad/add-tracing-channels branch from 0ca8e01 to 4c49d41 Compare November 24, 2025 22:41
@coderabbitai
Copy link

coderabbitai bot commented Nov 24, 2025

📝 Walkthrough

Walkthrough

This PR introduces a new tracing plugin system for srvx that instruments middleware and fetch handlers using Node's diagnostics_channel API. Changes include the core tracing implementation, build configuration updates, documentation additions, comprehensive test coverage, and an example project demonstrating the feature.

Changes

Cohort / File(s) Summary
Core Tracing Implementation
src/tracing.ts
Adds tracingPlugin function and RequestEvent type to instrument middleware and fetch handlers with diagnostics_channel tracing. Supports optional toggling of fetch and middleware tracing via configuration options.
Build & Export Configuration
package.json, build.config.mjs
Adds "./tracing" export mapping to package.json and includes src/tracing.ts in the bundle configuration.
Documentation
README.md, docs/1.guide/1.index.md
Adds new Starter Example row for the tracing example with source link and command reference in both documentation files.
Example Project
examples/tracing/package.json, examples/tracing/server.ts
Creates a new tracing example project with package configuration and a demonstration server that wires the tracing plugin and logs diagnostic channel events.
Test Suite
test/tracing.test.ts
Adds comprehensive test coverage for tracing functionality, including fetch and middleware instrumentation, event sequencing, error handling, and data validation.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • src/tracing.ts: Logic density for diagnostics_channel integration and promise wrapping patterns requires careful verification of event emission timing and context preservation.
  • test/tracing.test.ts: Comprehensive test coverage with multiple scenarios; verify all edge cases are properly addressed and event assertions are accurate.
  • examples/tracing/server.ts: Ensure the example correctly demonstrates the API and error handling patterns.

Poem

🐰 A trace through the stacks, a path crystal clear,
Events now whisper what once we couldn't hear,
From fetch to middleware, each hop gets its mark—
Diagnostics shining bright through the code's dark,
Instrumentation hops, leaving paw prints in light! 🔍✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.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 channel support' clearly and specifically summarizes the main change: adding tracing instrumentation via diagnostics channels for middleware and fetch handlers.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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 force-pushed the awad/add-tracing-channels branch from a89a68d to c8e96a0 Compare December 1, 2025 14:42
@pkg-pr-new
Copy link

pkg-pr-new bot commented Dec 11, 2025

Open in StackBlitz

npm i https://pkg.pr.new/h3js/srvx@141

commit: 9742f74

@pi0 pi0 changed the title feat: add tracing channels for fetch and middleware handlers feat: experimental tracing channel support Dec 11, 2025
@pi0 pi0 marked this pull request as ready for review December 11, 2025 01:56
Copilot AI review requested due to automatic review settings December 11, 2025 01:56
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.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

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.

Nice start 🚀

I have made a few refactors:

  • Plugin simplified and accepts an init options object
  • Tracing channel is conditionally loaded. So if unavailable, it won't break
  • Merged events format (ideas welcome until we make it stable on structure!)
  • Added a dummy example to show how to subscribe

Let's iterate and see how integration of this looks like with higher level like nitro and sentry SDK.

@pi0 pi0 merged commit d745677 into h3js:main Dec 11, 2025
10 of 11 checks passed
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 (3)
examples/tracing/package.json (1)

1-11: Consider pinning srvx instead of "latest" for reproducible examples

Using "srvx": "latest" means the example behavior can drift over time as new versions are released. For more reproducible starter templates (especially when users scaffold from a specific commit or tag), consider pinning to a caret range like ^0.9.7 or similar, or documenting that this example intentionally tracks the latest release.

test/tracing.test.ts (1)

1-443: Ensure unsubscribe uses the same subscriber object and consider a small helper to DRY tests

The test coverage here is solid and exercises fetch, middleware, async, and error paths well. One subtle point:

  • For each channel, you do:

    const channel = tracingChannel("srvx.middleware");
    
    const startHandler = /* ... */;
    const endHandler = /* ... */;
    
    channel.subscribe({
      start: startHandler,
      end: endHandler,
      asyncStart: noop,
      asyncEnd: noop,
      error: noop,
    });
    
    cleanupFns.push(() => {
      channel.unsubscribe({
        start: startHandler,
        end: endHandler,
        asyncStart: noop,
        asyncEnd: noop,
        error: noop,
      });
    });

    If tracingChannel().unsubscribe expects the same subscriber object instance that was passed to subscribe, these unsubscribe calls won’t actually detach the handlers, and you’ll accumulate live subscriptions across tests (each still closing over its own events array). This might not fail tests immediately but can lead to leaks and surprising behavior.

Two suggestions:

  1. Reuse the same subscriber object so unsubscribe is guaranteed to match what was subscribed:

    const subscriber = {
      start: startHandler,
      end: endHandler,
      asyncStart: noop,
      asyncEnd: noop,
      error: noop,
    };
    
    channel.subscribe(subscriber);
    cleanupFns.push(() => {
      channel.unsubscribe(subscriber);
    });
  2. Optionally introduce a tiny helper to reduce repetition and centralize this pattern:

    type Subscriber = Parameters<
      ReturnType<typeof tracingChannel>["subscribe"]
    >[0];
    
    function subscribeWithCleanup(
      name: string,
      subscriber: Subscriber,
      cleanupFns: Array<() => void>,
    ) {
      const channel = tracingChannel(name);
      channel.subscribe(subscriber);
      cleanupFns.push(() => channel.unsubscribe(subscriber));
      return channel;
    }

    Then in tests:

    const subscriber = { start: startHandler, end: endHandler, asyncStart: noop, asyncEnd: noop, error: noop };
    const fetchChannel = subscribeWithCleanup("srvx.fetch", subscriber, cleanupFns);

This keeps the lifecycle explicit and guards against any identity-based behavior in the diagnostics/tracing APIs.

Please double‑check Node’s diagnostics_channel.tracingChannel subscribe/unsubscribe contract to confirm whether it keys unsubscription by subscriber object identity; if it does, the refactor above becomes important to avoid leaks.

examples/tracing/server.ts (1)

1-45: Example should match the defensive pattern used in src/tracing.ts

The debugChannel function in this example does not guard against missing process.getBuiltinModule or diagnostics_channel.tracingChannel, unlike the actual tracingPlugin implementation in src/tracing.ts (which uses globalThis.process?.getBuiltinModule?.("node:diagnostics_channel")). While all supported Node versions (>=20.16.0, per package.json) have both APIs available, the example should match the defensive pattern for consistency and to serve as better documentation of the recommended approach.

Update debugChannel to guard against missing APIs and make serializeData handle unexpected payloads:

-function debugChannel(name: string) {
-  const { tracingChannel } = process.getBuiltinModule(
-    "node:diagnostics_channel",
-  );
+function debugChannel(name: string) {
+  const { tracingChannel } =
+    globalThis.process?.getBuiltinModule?.("node:diagnostics_channel") || {};
+  if (!tracingChannel) {
+    // diagnostics tracing not available; skip debug wiring
+    return;
+  }
@@
-  const noop = () => {};
-  const serializeData = (data: any) =>
-    Object.entries(data)
-      .map(([key, value]) => {
+  const noop = () => {};
+  const serializeData = (data: any) =>
+    data && typeof data === "object"
+      ? Object.entries(data).map(([key, value]) => {
         if (key === "request") {
           return `request(url=${(value as Request).url})`;
         }
         if (key === "server") {
           return "server";
         }
         if (key === "result") {
           return `result(status=${(value as Response).status})`;
         }
         return `${key}=${value}`;
-      })
-      .join(", ");
+      }).join(", ")
+      : String(data);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d8e3a4d and 9742f74.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (8)
  • README.md (1 hunks)
  • build.config.mjs (1 hunks)
  • docs/1.guide/1.index.md (1 hunks)
  • examples/tracing/package.json (1 hunks)
  • examples/tracing/server.ts (1 hunks)
  • package.json (1 hunks)
  • src/tracing.ts (1 hunks)
  • test/tracing.test.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
test/tracing.test.ts (2)
src/tracing.ts (1)
  • tracingPlugin (35-84)
src/types.ts (1)
  • ServerMiddleware (40-43)
src/tracing.ts (1)
src/types.ts (4)
  • Server (189-257)
  • ServerRequest (303-328)
  • ServerMiddleware (40-43)
  • ServerPlugin (45-45)
examples/tracing/server.ts (2)
src/tracing.ts (1)
  • tracingPlugin (35-84)
src/log.ts (1)
  • log (9-23)
🔇 Additional comments (5)
build.config.mjs (1)

10-24: Including src/tracing.ts in bundle inputs looks correct

Adding "src/tracing.ts" to the bundle inputs is consistent with the new ./tracing export and ensures dist/tracing.mjs is produced as part of the build. No further changes needed here.

docs/1.guide/1.index.md (1)

90-103: Tracing starter example row looks consistent

The new tracing starter example entry matches the existing examples’ pattern (source path + giget command) and is aligned with the examples/tracing directory. No issues from a docs perspective.

README.md (1)

52-64: README tracing example entry is aligned and clear

The added tracing row in the Starter Examples table matches the docs entry and the examples/tracing layout. Good way to surface the new feature.

src/tracing.ts (1)

8-83: Tracing plugin implementation matches the intended behavior and looks solid

  • The RequestEvent shape (server + request + optional { index, handler } middleware descriptor) aligns with how tests consume the tracing payload.
  • The plugin defensively no-ops when diagnostics_channel.tracingChannel isn’t available, which avoids hard Node coupling for non-Node runtimes.
  • Fetch and middleware wrapping via tracePromise is clean, and the in-place middleware.splice(...) preserves any existing references to the middleware array while still replacing handlers with wrapped versions.

Overall this is a good, minimal surface for consumers to build diagnostics integrations on.

If you haven’t already, it’s worth validating against the current Node diagnostics_channel/tracingChannel docs that tracePromise emits the expected start/end/asyncStart/asyncEnd/error events with the context object you’re passing, ensuring long‑term compatibility.

package.json (1)

10-30: The ./tracing export is properly configured.

The export path in package.json is correctly wired:

  • "./tracing": "./dist/tracing.mjs" is defined in exports
  • src/tracing.ts exists and properly exports both tracingPlugin and RequestEvent
  • build.config.mjs explicitly includes src/tracing.ts in the entries list, so dist/tracing.mjs will be generated
  • Type definitions are centralized in dist/types.d.mts (configured via tsconfig.json with isolatedDeclarations: true)
  • Usage in examples/tracing/server.ts and test/tracing.test.ts confirms the import path works as expected

No changes needed; the build will generate the artifact and types correctly.

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.

2 participants