Skip to content

feat: Add tracing channels support - #193

Open
logaretm wants to merge 13 commits into
unjs:mainfrom
logaretm:awad/add-tracing-channels
Open

feat: Add tracing channels support#193
logaretm wants to merge 13 commits into
unjs:mainfrom
logaretm:awad/add-tracing-channels

Conversation

@logaretm

@logaretm logaretm commented Nov 7, 2025

Copy link
Copy Markdown

Adds Node.js tracing channels for db query operations, enabling observability and performance monitoring.

Channel Naming Convention

All channels use the db0.query convention which when coupled with tracing channels produce the following diagnostic channels:

  • tracing:db0.query:start
  • tracing:db0.query:end
  • tracing:db0.query:asyncStart
  • tracing:db0.query:asyncEnd
  • tracing:db0.query:error

Which matches nicely with fastify but there isn't a lot of convention for them other than the Node.js recommendation.

Unlike unstorage it didn't feel like we need multiple channels since more or less it boils down to executing a query.

Tracing context includes

  • query - query to be executed
  • method - source of the query: exec, sql, prepare.all, prepare.run or prepare.get
  • connector - the connector name (e.g. sqlite, postgresql)
  • dialect - the db dialect

Bound parameters are never included in the context. The query string can still contain literal values (raw db.exec() strings, or static ${} interpolations inside {} in the sql template).

Example

Tracing is opt-in like unstorage. Users create traced instances with withTracing from db0/tracing:

import { createDatabase } from "db0";
import { withTracing } from "db0/tracing";
import sqlite from "db0/connectors/node-sqlite";

// Create traced DB
const db = withTracing(
  createDatabase(
    sqlite({
      name: "db",
    }),
  ),
);

SDKs and consumers can then do:

import { tracingChannel } from "node:diagnostics_channel";

const channel = tracingChannel("db0.query");

channel.subscribe({
  start: (data) => {
    // start timing, or spans or whatever...
  },
  asyncEnd: (data) => {
    console.log(`${data.connector} ran ${data.query} in ${duration}ms`);
  },
  error: (data) => {
    console.error(`Error in ${data.connector}:`, data.error);
  },
});

All operations emit standard tracing events: start, end, asyncStart, asyncEnd, and error. Connectors that throw synchronously emit the same sequence as connectors that reject asynchronously, so asyncEnd always fires.

Tracing requires node:diagnostics_channel (Node.js 20.16+ / 22.3+). On runtimes where it is unavailable, withTracing returns the database unchanged. When nothing is subscribed to the channel, traced instances take a fast path and behave like untraced ones.

Summary by CodeRabbit

  • New Features

    • Added opt-in tracing for database queries through Node.js diagnostics channels.
    • Added tracing support for SQL execution and prepared statements, including success and error events.
    • Exposed tracing through the ./tracing package entry point.
    • Added query context including the operation, connector, dialect, results, and errors.
    • Added a tracing example demonstrating setup and query lifecycle logging.
  • Documentation

    • Added a tracing guide covering configuration, events, payloads, and usage examples.
    • Added navigation to the new tracing guide.

Copilot AI review requested due to automatic review settings December 11, 2025 11:19
@logaretm
logaretm force-pushed the awad/add-tracing-channels branch from 375b187 to 0ae9a84 Compare December 11, 2025 11:19

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

This PR adds Node.js tracing channels support to db0, enabling observability and performance monitoring for database query operations. The implementation follows an opt-in approach where users must explicitly wrap their database instance with withTracing() to enable tracing.

Key Changes:

  • Introduces withTracing() wrapper function that instruments database operations with Node.js diagnostic channels
  • Adds comprehensive test coverage for all traced database operations (exec, sql, prepare.all, prepare.run, prepare.get)
  • Exports tracing functionality and types through the main index

Reviewed changes

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

File Description
src/tracing.ts Implements the core tracing functionality with withTracing() wrapper and channel management for db query operations
test/tracing.test.ts Provides comprehensive test coverage for tracing behavior including opt-in verification, success/error scenarios, and query reconstruction
src/index.ts Exports the new tracing API (withTracing, TraceContext, TracedOperation) for public consumption

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

Comment thread src/index.ts Outdated
Comment thread src/tracing.ts Outdated
Comment thread test/tracing.test.ts Outdated
Comment thread src/tracing.ts Outdated
Comment thread src/tracing.ts Outdated
Comment thread test/tracing.test.ts
@logaretm

Copy link
Copy Markdown
Author

@pi0 Friendly ping on this, it's the only remaining piece 😄

@coderabbitai

coderabbitai Bot commented Jul 13, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds opt-in Node.js diagnostics channel tracing for database queries and prepared statements, exports and builds the tracing entrypoint, and adds validation, documentation, and a runnable SQLite example.

Changes

Query tracing

Layer / File(s) Summary
Tracing contract and package wiring
src/tracing.ts, package.json, build.config.ts
Defines tracing types, adds symbol-based wrapping markers, exports withTracing, and bundles and publishes the tracing entrypoint.
Database tracing wrapper
src/tracing.ts
Wraps exec, sql, and prepared statement methods with db0.query diagnostics channel events while preserving properties and preventing duplicate wrapping.
Tracing behavior validation
test/tracing.test.ts
Tests opt-in behavior, event payloads and ordering, errors, query reconstruction, prepared statements, getter passthrough, and nested binds.
Tracing guide and example
docs/1.guide/*, examples/tracing/*
Documents diagnostics channel tracing and adds a SQLite example that logs query lifecycle events.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Application
  participant withTracing
  participant Database
  participant diagnostics_channel
  Application->>withTracing: wrap database
  withTracing->>Database: intercept exec, sql, and prepare
  Application->>Database: execute query
  Database->>diagnostics_channel: publish db0.query context
  diagnostics_channel-->>Application: start, asyncStart, asyncEnd, end, or error
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: adding Node.js tracing channel support for db0.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install failed. For unrecoverable errors, disable the tool in CodeRabbit configuration.


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.

@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: 1

🧹 Nitpick comments (3)
examples/tracing/index.ts (1)

2-2: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider marking the type-only import explicitly.

TraceContext is an interface used only as a generic type argument (line 24). Test file test/tracing.test.ts already uses import type for the same symbols from src/tracing.ts; aligning here avoids relying on the transpiler's local-usage elision and future-proofs against stricter isolatedModules/verbatimModuleSyntax settings.

-import { TraceContext, withTracing } from "../../src/tracing";
+import { withTracing } from "../../src/tracing";
+import type { TraceContext } from "../../src/tracing";
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/tracing/index.ts` at line 2, Update the import in the tracing
example to use a type-only import for TraceContext while keeping withTracing as
a runtime import. Preserve the existing usage of both symbols.
test/tracing.test.ts (1)

78-138: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Manual listener.cleanup() calls won't run if an assertion throws, leaking subscribers on the shared db0.query channel.

Every test creates a fresh listener and unsubscribes at the very end of the test body. If any expect(...) earlier in the test throws, cleanup() is skipped and those handlers remain subscribed to the singleton channel for the rest of the suite run — accumulating stale listeners across (re)test runs and file watch reruns.

♻️ Suggested fix: guarantee cleanup via try/finally or afterEach
-    it("should not emit tracing events without withTracing wrapper", async () => {
-      const plainDb = createDatabase(
-        connector({
-          name: ":memory:",
-        }),
-      );
-      const listener = createTracingListener("query");
-
-      await plainDb.exec(`CREATE TABLE test (id INTEGER PRIMARY KEY)`);
-      await plainDb.sql`SELECT * FROM test`;
-
-      // No tracing events should be emitted
-      expect(listener.handlers.start).not.toHaveBeenCalled();
-      ...
-
-      listener.cleanup();
-    });
+    it("should not emit tracing events without withTracing wrapper", async () => {
+      const plainDb = createDatabase(
+        connector({
+          name: ":memory:",
+        }),
+      );
+      const listener = createTracingListener("query");
+      try {
+        await plainDb.exec(`CREATE TABLE test (id INTEGER PRIMARY KEY)`);
+        await plainDb.sql`SELECT * FROM test`;
+
+        expect(listener.handlers.start).not.toHaveBeenCalled();
+        ...
+      } finally {
+        listener.cleanup();
+      }
+    });

Applying this pattern (or wrapping createTracingListener to auto-register cleanup via afterEach) across the file would prevent cross-test subscriber leakage.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/tracing.test.ts` around lines 78 - 138, Ensure every listener created by
createTracingListener is cleaned up even when an assertion or test operation
throws. Apply a file-wide try/finally cleanup pattern or register listener
cleanup through afterEach, including the listeners in the opt-in behavior tests,
while preserving the existing tracing assertions.
examples/tracing/package.json (1)

7-10: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

jiti pinned to the legacy v1 line.

Latest jiti is 2.x; ^1.21.0 targets the unmaintained v1 branch. Not critical for example scaffolding, but consider bumping for a modern toolchain and consistency with other examples in the repo.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/tracing/package.json` around lines 7 - 10, Update the jiti entry in
the examples package devDependencies from the legacy ^1.21.0 range to the
current 2.x release line, matching the versioning used by other examples where
applicable. Leave the db0 dependency unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@examples/tracing/index.ts`:
- Around line 1-5: Update the imports in the tracing example to use the
published db0 entry points: import createDatabase from db0, TraceContext and
withTracing from db0/tracing, and sqlite from db0/connectors/better-sqlite3,
preserving the existing symbols and usage.

---

Nitpick comments:
In `@examples/tracing/index.ts`:
- Line 2: Update the import in the tracing example to use a type-only import for
TraceContext while keeping withTracing as a runtime import. Preserve the
existing usage of both symbols.

In `@examples/tracing/package.json`:
- Around line 7-10: Update the jiti entry in the examples package
devDependencies from the legacy ^1.21.0 range to the current 2.x release line,
matching the versioning used by other examples where applicable. Leave the db0
dependency unchanged.

In `@test/tracing.test.ts`:
- Around line 78-138: Ensure every listener created by createTracingListener is
cleaned up even when an assertion or test operation throws. Apply a file-wide
try/finally cleanup pattern or register listener cleanup through afterEach,
including the listeners in the opt-in behavior tests, while preserving the
existing tracing assertions.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: e441ed01-5f24-469a-bc26-ead13b588878

📥 Commits

Reviewing files that changed from the base of the PR and between febaa15 and d356eee.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (8)
  • build.config.ts
  • docs/1.guide/1.index.md
  • docs/1.guide/2.tracing.md
  • examples/tracing/index.ts
  • examples/tracing/package.json
  • package.json
  • src/tracing.ts
  • test/tracing.test.ts

Comment thread examples/tracing/index.ts
Comment on lines +1 to +5
import { createDatabase } from "../../src";
import { TraceContext, withTracing } from "../../src/tracing";
import { tracingChannel } from "node:diagnostics_channel";

import sqlite from "../../src/connectors/better-sqlite3";

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check whether other examples/* directories use relative src imports or the public db0 package.
fd -t d . examples | while read -r dir; do
  echo "== $dir =="
  fd -e ts -e js . "$dir" --exec grep -nE "from ['\"](\.\./\.\./src|db0)" {} \;
done

Repository: unjs/db0

Length of output: 530


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== examples files =="
fd -t f . examples

echo
echo "== package.json =="
sed -n '1,220p' package.json

echo
echo "== examples/drizzle/index.ts =="
sed -n '1,120p' examples/drizzle/index.ts

echo
echo "== examples/tracing/index.ts =="
sed -n '1,120p' examples/tracing/index.ts

Repository: unjs/db0

Length of output: 5394


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== examples/drizzle/package.json =="
sed -n '1,200p' examples/drizzle/package.json

echo
echo "== examples/tracing/package.json =="
sed -n '1,200p' examples/tracing/package.json

echo
echo "== tsconfig files =="
fd -a -t f 'tsconfig*.json' .

Repository: unjs/db0

Length of output: 683


Use the published db0 entry points here

This example already declares db0 in examples/tracing/package.json, so it should import from db0, db0/tracing, and db0/connectors/better-sqlite3 instead of ../../src/*. That makes the example exercise the exported ./tracing subpath and match consumer usage.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/tracing/index.ts` around lines 1 - 5, Update the imports in the
tracing example to use the published db0 entry points: import createDatabase
from db0, TraceContext and withTracing from db0/tracing, and sqlite from
db0/connectors/better-sqlite3, preserving the existing symbols and usage.

pi0 and others added 2 commits July 13, 2026 09:55
- wrap traced calls in async fns so connectors that throw synchronously emit
  the same event sequence (error/asyncStart/asyncEnd) as async ones
- build the trace context lazily behind `hasSubscribers`, removing the double
  `sqlTemplate` parse per `db.sql` call
- add `connector` to the trace context (the channel is process-global)
- drop the stale `tracePromise` cast, @types/node returns the promise now
- document `withTracing` and the `db0/tracing` entry point

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- make the trace wrapper async so subscribing never turns a rejection
  (e.g. an invalid sql template) into a synchronous throw
- copy property descriptors instead of Object.create, so spreading or
  serializing a traced database keeps connector/dialect/disposed
- mark traced instances with a non-enumerable symbol instead of `__traced`
- correct the documented connector/dialect values and event ordering
- cover param privacy, event ordering and subscriber cleanup in tests

Co-Authored-By: Claude Opus 4.8 <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)
test/tracing.test.ts (1)

175-180: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Dispose plainDb after the assertion.

Unlike the shared db (disposed in afterEach), this locally created database is never disposed, leaving its sqlite connection open for the rest of the run.

♻️ Proposed fix
-    it("should not leak internal properties of the traced instance", () => {
+    it("should not leak internal properties of the traced instance", async () => {
       const plainDb = createDatabase(connector({ name: ":memory:" }));
       const tracedDb = withTracing(plainDb);

       expect(JSON.stringify(tracedDb)).toBe(JSON.stringify(plainDb));
+      await plainDb.dispose();
     });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/tracing.test.ts` around lines 175 - 180, Dispose the locally created
plainDb in the “should not leak internal properties of the traced instance” test
after the JSON assertion, using the database’s existing disposal API and
ensuring cleanup occurs even if the assertion fails.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@test/tracing.test.ts`:
- Around line 175-180: Dispose the locally created plainDb in the “should not
leak internal properties of the traced instance” test after the JSON assertion,
using the database’s existing disposal API and ensuring cleanup occurs even if
the assertion fails.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 93536c9a-322a-4ea9-932e-2f2f20760037

📥 Commits

Reviewing files that changed from the base of the PR and between d356eee and f854c44.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (4)
  • docs/1.guide/2.tracing.md
  • examples/tracing/index.ts
  • src/tracing.ts
  • test/tracing.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • examples/tracing/index.ts

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