Skip to content

beingmartinbmc/node-request-trace

Repository files navigation

node-request-trace

CI Coverage: >=90% Node >= 16 License: MIT Zero Dependencies

node-request-trace request timeline hero

Debug any Node.js request like a timeline.

node-request-trace shows what happened inside a request, where the time went, and which step caused the slowdown, without asking you to adopt OpenTelemetry, run a collector, or wire up a tracing backend.

GET /checkout 340ms (200)
requestId: req_a1b2c3d4e5f6
bottleneck: dbQuery 210ms (61.8%)
coverage: 302ms traced (88.8%), 38ms uninstrumented
0ms |------------------------------------------------| 340ms
|- authMiddleware           ## 12ms
|- validateInput              # 5ms
|- dbQuery                    ############################## 210ms
`- paymentService                                             ###### 45ms

Why This Exists

OpenTelemetry is powerful, standard, and absolutely worth using for large production observability stacks. It is also a lot when you just want to answer one local question:

Why was this request slow?

node-request-trace is for that moment. Add middleware, wrap important work in trace.step(), and immediately get request timelines, bottleneck detection, N+1 detection, outgoing HTTP tracing, a browser dashboard, and exports for terminal reports, Markdown, Chrome tracing, and Speedscope.

The Pitch

const express = require("express");
const trace = require("node-request-trace");

trace.init({
  slowThreshold: 200,
  traceOutgoing: true,
});

const app = express();

app.use(trace.middleware());
app.use(trace.routes());

app.get("/checkout", async (req, res) => {
  await trace.step("loadUser", () => users.find(req.query.userId));
  await trace.step("chargeCard", () => payments.charge(req.body));
  res.json({ ok: true });
});

app.listen(3000);

Now open the dashboard:

open http://localhost:3000/trace/ui

Or inspect the same request from the terminal:

npx node-request-trace timeline http://localhost:3000 req_a1b2c3d4e5f6

What You Get

  • Request timelines with bottleneck, traced coverage, and uninstrumented gap detection.
  • N+1 and duplicate-work detection within a single request.
  • Async context propagation through AsyncLocalStorage.
  • Manual trace.step() spans for database calls, service calls, queues, cache reads, and any async work.
  • Automatic Express, Fastify, and Koa middleware/lifecycle timing.
  • Automatic database and cache instrumentation for pg, mongodb, ioredis, knex, and Prisma.
  • Outgoing http, https, and fetch tracing.
  • Built-in dashboard at /trace/ui with waterfall, timeline, and flamegraph views.
  • CLI for stats, recent traces, slow traces, timeline reports, tailing, diffing, and exports.
  • Trace diffing to catch regressions between two requests.
  • Optional AI explanations of why a request was slow.
  • Exports for Markdown, a self-contained shareable HTML snapshot, Chrome Trace Event Format, and Speedscope.
  • Sampling, slow request detection, retention limits, and in-memory eviction.
  • Header redaction by default and no request body capture.
  • Zero runtime dependencies.

When To Use It

Use node-request-trace when you want a lightweight developer experience:

  • You are debugging slow Node.js API requests.
  • You want copy-pasteable trace output for PRs, issues, and incident notes.
  • You do not want to run a collector, agent, backend, or dashboard service.
  • You want local tracing now and can move to OpenTelemetry later if needed.

Use OpenTelemetry when you need distributed traces across many services, vendor integration, long-term retention, metrics/log correlation, or organization-wide observability standards.

Ecosystem

node-request-trace is part of a small Node.js observability ecosystem you can adopt independently or together:

  • node-actuator-lite — Spring Boot-style /actuator/health, /info, /metrics, /env, /threaddump, /heapdump, and /prometheus endpoints.
  • node-eventloop-watchdog — Detects event-loop stalls, captures stack traces and hotspots, and triggers recovery.
  • node-request-trace — Per-request timelines, browser dashboard, and CLI without OpenTelemetry.

When all three are installed:

  • node-eventloop-watchdog automatically registers /actuator/eventloop, /actuator/eventloop/history, /actuator/eventloop/hotspots, and /actuator/eventloop/metrics under node-actuator-lite.
  • Event-loop block events include the active request id, route, and method captured by this tracer via getCurrentRequestContext().

Runnable example: node-actuator-lite/examples/ecosystem.

Quickest setup: Use node-observability-lite to wire the three packages together with production-safe presets in one line.

const observability = require('node-observability-lite');
observability.express(app, {
  preset: 'production',
  auth: req => req.get('authorization') === `Bearer ${process.env.OPS_TOKEN}`,
});

Installation

npm install node-request-trace

The package supports Node.js 16 and newer. It has optional peer support for Express, Fastify, and Koa, but no runtime dependencies of its own. AI explanations and fetch tracing use the global fetch, which is available on Node.js 18 and newer.

Quick Start

Express

const express = require("express");
const trace = require("node-request-trace");

trace.init({
  slowThreshold: 200,
  samplingRate: 1,
  traceOutgoing: true,
});

const app = express();

app.use(trace.middleware());
app.use(trace.routes());

app.get("/api/order/:id", async (req, res) => {
  const order = await trace.step("db.order.find", () => db.orders.find(req.params.id));
  const quote = await trace.step("shipping.quote", () => shipping.quote(order));
  res.json({ order, quote });
});

app.listen(3000);

Fastify

const fastify = require("fastify")();
const trace = require("node-request-trace");

trace.init({ autoTrack: true });

fastify.register(trace.fastifyPlugin());

fastify.get("/api", async () => {
  await trace.step("work", async () => {
    // Your code here.
  });
  return { ok: true };
});

Koa

const Koa = require("koa");
const trace = require("node-request-trace");

trace.init({ autoTrack: true });

const app = new Koa();
trace.instrumentKoa(app);

app.use(trace.koaMiddleware());
app.use(async (ctx) => {
  await trace.step("work", async () => {
    // Your code here.
  });
  ctx.body = { ok: true };
});

Timeline Reports

Timeline reports are the main developer experience.

const report = trace.timeline(savedTrace);

console.log(report.summary.bottleneck);
console.log(trace.renderTimeline(savedTrace, { width: 80 }));

Structured reports include:

  • steps: normalized request steps sorted by start time.
  • summary.bottleneck: the slowest step.
  • summary.topSteps: the top five slowest steps.
  • summary.coveredDuration: time covered by recorded steps.
  • summary.uninstrumentedDuration: request time not explained by recorded steps.
  • summary.coveragePercent: how much of the request timeline is explained.
  • summary.gaps: idle or uninstrumented windows between steps.
  • summary.duplicates: repeated steps, including detected N+1 patterns.
  • summary.errorCount: number of failed steps.

This makes it easy to answer:

  • Did the database cause the slowdown?
  • Did middleware run before the handler?
  • How much of the request is still a blind spot?
  • Am I making the same query in a loop?
  • Which step should I optimize first?

Manual Instrumentation

Use trace.step(name, fn) around any async operation you care about.

const user = await trace.step("db.user.findById", async () => {
  return db.users.findById(userId);
});

Steps are recorded whether they succeed or fail. If the wrapped function throws, the error is attached to the step and then re-thrown so your application behavior stays the same.

Automatic Instrumentation

Middleware Timing

Enable autoTrack to capture framework lifecycle and middleware timing.

trace.init({ autoTrack: true });

Express records middleware layers. Fastify records lifecycle phases such as onRequest, preParsing, preValidation, handler, and onSend. Koa wraps app.use() middleware after trace.instrumentKoa(app).

Database and Cache Clients

Enable autoInstrument to time queries from common drivers without wrapping each call in trace.step().

trace.init({ autoInstrument: true });

This patches pg (node-postgres), mongodb, and ioredis when they are present. Queries appear as db and cache steps with truncated, redaction-friendly labels:

pg SELECT * FROM users WHERE id = ?   12ms [db]
mongo users.findOne                    8ms [db]
redis GET                              1ms [cache]

Limit which drivers are patched with the only list:

trace.init({ autoInstrument: { only: ["pg", "mongodb"] } });

knex and Prisma are instance-based, so instrument them explicitly:

trace.instrumentKnex(knex);
trace.instrumentPrisma(prisma);

You can also toggle driver instrumentation at runtime:

trace.enableAutoInstrumentation({ only: ["pg"] });
trace.disableAutoInstrumentation();

Outgoing HTTP

Enable outgoing HTTP tracing to automatically capture calls made with Node's http, https, and global fetch.

trace.init({ traceOutgoing: true });

Or toggle it manually:

trace.enableHttpTracing();
trace.disableHttpTracing();

Outgoing calls are added as http-outgoing steps:

HTTP POST api.stripe.com/v1/charges 180ms [http-outgoing]
HTTP GET cdn.example.com/assets      45ms [http-outgoing]

N+1 and Duplicate Detection

node-request-trace groups semantically identical steps within a request so repeated work stands out. Numbers, ids, UUIDs, and quoted literals are normalized, so SELECT ... WHERE id = 1 and SELECT ... WHERE id = 2 count as the same pattern.

const analysis = trace.analyze(savedTrace);

if (analysis.hasNPlusOne) {
  console.log(analysis.nPlusOne);
}

The analysis reports each repeated pattern with its count, total and average duration, whether it reads as an N+1 pattern, and how much time was wasted on repetition. Detected patterns also surface in timeline reports, the dashboard, Markdown exports, and AI explanations.

Trace Diffing

Compare two traces to find what changed between a fast request and a slow one, or before and after a deploy.

const diff = trace.diff(traceA, traceB);

console.log(diff.totalDeltaMs, diff.regressed);
console.log(trace.diffToMarkdown(traceA, traceB));

Each step is labeled added, removed, slower, faster, or unchanged, with per-step deltas and an overall regression flag. From the CLI:

npx node-request-trace diff http://localhost:3000 req_aaa req_bbb

AI Explanations

Turn a trace into a focused, actionable explanation of why it was slow.

const text = await trace.explain(savedTrace, {
  apiKey: process.env.OPENAI_API_KEY,
});

The explanation uses any OpenAI-compatible chat completions endpoint. You supply the key, model, and base URL, so no vendor is bundled and no secrets are stored. Set OPENAI_API_KEY or LLM_API_KEY, or pass { apiKey } directly.

If you would rather not call a model, build the prompt and paste it into your own chat:

const { system, user } = trace.buildExplainPrompt(savedTrace);

From the CLI, explain calls the model when a key is set, and otherwise prints the prompt for you to copy:

OPENAI_API_KEY=sk-... npx node-request-trace explain http://localhost:3000 req_abc123

Exports

A trace can be exported in several formats for sharing and deeper analysis.

trace.toMarkdown(savedTrace);          // GitHub-flavored markdown report
trace.toShareableHtml(savedTrace);     // self-contained single-file HTML
trace.exportChromeTrace(savedTrace);   // Chrome Trace Event Format object
trace.exportChromeTraceJson(savedTrace);
trace.exportSpeedscope(savedTrace);    // Speedscope profile object
trace.exportSpeedscopeJson(savedTrace);
  • Markdown is ideal for PR comments, issues, and incident notes.
  • HTML snapshot is a single self-contained file you can open in a browser or send to a teammate.
  • Chrome Trace loads in chrome://tracing.
  • Speedscope loads at speedscope.app for flamegraph analysis.

Dashboard

Mount trace.routes() and visit /trace/ui.

app.use(trace.routes());

The dashboard includes:

  • Recent request table.
  • Slow request view.
  • Live feed.
  • Request detail view.
  • Waterfall, timeline, and flamegraph modes.
  • N+1 and duplicate-work warnings.
  • Export buttons for Chrome trace, Speedscope, Markdown, and HTML snapshots.

CLI

The CLI talks to any app that has trace.routes() mounted.

npx node-request-trace stats http://localhost:3000
npx node-request-trace recent http://localhost:3000
npx node-request-trace slow http://localhost:3000
npx node-request-trace inspect http://localhost:3000 req_a1b2c3d4
npx node-request-trace timeline http://localhost:3000 req_a1b2c3d4
npx node-request-trace tail http://localhost:3000
npx node-request-trace diff http://localhost:3000 req_aaa req_bbb
npx node-request-trace markdown http://localhost:3000 req_a1b2c3d4
npx node-request-trace snapshot http://localhost:3000 req_a1b2c3d4 > trace.html
npx node-request-trace explain http://localhost:3000 req_a1b2c3d4
npx node-request-trace export http://localhost:3000 req_a1b2c3d4 > trace.json
npx node-request-trace speedscope http://localhost:3000 req_a1b2c3d4 > scope.json

Use timeline when you want the fastest answer. Use inspect when you want the fuller detail view. Use diff to catch regressions, explain for an AI summary, and export, speedscope, markdown, or snapshot to take a trace elsewhere. markdown is also available as md.

HTTP API

Endpoint Description
GET /trace/ui Browser dashboard
GET /trace/recent Last 50 traces
GET /trace/slow Slow traces using slowThreshold
GET /trace/stats Aggregate latency, error, and throughput stats
GET /trace/:requestId Raw trace JSON
GET /trace/:requestId/timeline Timeline report plus ASCII rendering
GET /trace/:requestId/chrome Chrome Trace Event JSON
GET /trace/:requestId/speedscope Speedscope profile JSON
GET /trace/:requestId/markdown GitHub-flavored Markdown report
GET /trace/:requestId/snapshot Self-contained shareable HTML download
GET /trace/diff/:idA/:idB Diff between two traces

Example timeline response:

{
  "requestId": "req_a1b2c3d4e5f6",
  "method": "GET",
  "path": "/checkout",
  "totalDuration": 340,
  "stepCount": 4,
  "summary": {
    "bottleneck": { "name": "dbQuery", "duration": 210, "percentOfRequest": 61.8 },
    "coveredDuration": 302,
    "uninstrumentedDuration": 38,
    "coveragePercent": 88.8,
    "errorCount": 0
  },
  "text": "GET /checkout 340ms (200)\\n..."
}

API Reference

trace.init(options)

Initializes tracing. Calling it again replaces the current configuration and storage.

trace.init({
  slowThreshold: 200,
  samplingRate: 1,
  maxTraces: 1000,
  retentionSeconds: 300,
  autoTrack: false,
  traceOutgoing: false,
  autoInstrument: false,
  sensitiveHeaders: null,
});

trace.middleware(framework)

Returns Express-compatible middleware by default. Pass "koa" to get Koa middleware.

trace.fastifyPlugin()

Returns a Fastify plugin.

trace.koaMiddleware()

Returns Koa middleware.

trace.instrumentKoa(app)

Patches app.use() so Koa middleware can be timed automatically when autoTrack is enabled.

trace.routes()

Returns middleware that serves the dashboard and JSON endpoints.

trace.step(name, fn)

Records an async step on the current request trace and returns the wrapped function's result.

trace.current()

Returns the active request trace or null outside a traced request.

trace.timeline(trace)

Builds a structured timeline report.

trace.renderTimeline(trace, options)

Builds an ASCII request timeline. Use options.width to control the chart width.

trace.analyze(trace)

Returns N+1 and duplicate-work analysis for a trace's steps.

trace.diff(traceA, traceB, options)

Compares two traces and returns a per-step diff with an overall regression flag.

trace.diffToMarkdown(traceA, traceB, options)

Returns the diff as GitHub-flavored Markdown.

trace.explain(trace, options)

Calls an OpenAI-compatible endpoint and returns a short explanation of why the request was slow. Accepts { apiKey, model, baseUrl }.

trace.buildExplainPrompt(trace)

Returns { system, user } prompt text so you can run the explanation in any chat tool without a key.

trace.enableAutoInstrumentation(options)

Patches supported drivers (pg, mongodb, ioredis) and returns the list that was applied. Accepts { only: [...] }.

trace.disableAutoInstrumentation()

Restores the original driver methods.

trace.instrumentKnex(knexInstance)

Attaches query timing to a knex instance via its event hooks.

trace.instrumentPrisma(prismaClient)

Adds query timing through Prisma's $use middleware.

trace.useLogger(type, logger)

Supports "console", "pino", "winston", or a custom { onTrace(trace) {} } integration.

trace.enableHttpTracing()

Patches outgoing http, https, and fetch calls.

trace.disableHttpTracing()

Restores the original outgoing HTTP functions.

trace.exportChromeTrace(trace) / trace.exportChromeTraceJson(trace)

Returns Chrome Trace Event Format data, as an object or a JSON string.

trace.exportSpeedscope(trace) / trace.exportSpeedscopeJson(trace)

Returns a Speedscope profile, as an object or a JSON string.

trace.toMarkdown(trace, options)

Returns a GitHub-flavored Markdown report for a trace.

trace.toShareableHtml(trace)

Returns a self-contained HTML document for the trace.

trace.sanitizeHeaders(headers)

Redacts sensitive headers using the configured list.

trace.destroy()

Stops cleanup timers, clears stored traces, disables HTTP tracing and auto-instrumentation, and resets the tracer.

Configuration

Option Default Description
slowThreshold 200 Duration in ms above which a request is marked slow
samplingRate 1 Fraction of requests to trace, from 0 to 1
maxTraces 1000 Maximum number of traces stored in memory
retentionSeconds 300 How long traces stay in memory
autoTrack false Enables framework middleware/lifecycle timing
autoInstrument false Enables driver instrumentation; pass { only: [...] } to limit it
traceOutgoing false Enables outgoing HTTP and fetch tracing
sensitiveHeaders null Custom sensitive header list

Dev vs Prod

In development, run with full sampling and rich instrumentation:

trace.init({
  samplingRate: 1,
  autoTrack: true,
  autoInstrument: true,
  traceOutgoing: true,
});

In production, keep the footprint smaller:

trace.init({
  samplingRate: 0.05,
  slowThreshold: 500,
  maxTraces: 500,
  retentionSeconds: 120,
});

This library stores traces in memory. It is intentionally lightweight and local-first. For long-term retention or cross-service distributed tracing, export the data or use a full observability platform.

Security

node-request-trace avoids sensitive data by default:

  • Request and response bodies are not captured.
  • Sensitive headers are redacted.
  • Auto-instrumented query labels are truncated and strip literal values.
  • The default redaction list includes Authorization, Cookie, Set-Cookie, X-API-Key, X-Auth-Token, and Proxy-Authorization.

Customize header redaction:

trace.init({
  sensitiveHeaders: ["authorization", "x-internal-secret"],
});

AI explanations are opt-in and only send the trace summary you generate. No key is bundled, and requests go directly to the endpoint you configure.

Performance

The library is designed to stay small:

  • No runtime dependencies.
  • In-memory Map storage with bounded retention.
  • Optional sampling.
  • Background cleanup timers use unref() when available.
  • Unsampled requests skip tracing immediately.

OpenTelemetry Comparison

Tool Best For Tradeoff
OpenTelemetry Standards-based distributed tracing across services More setup and infrastructure
Logging libraries Simple event logs No request timeline or latency attribution
node-request-trace Local-first request timelines and debugging In-memory, not a full observability backend

The goal is not to replace OpenTelemetry everywhere. The goal is to make request-level debugging fast, visual, and easy to adopt.

Running The Example

npm run example

Then try:

curl http://localhost:3000/checkout
curl http://localhost:3000/fast
curl http://localhost:3000/error
open http://localhost:3000/trace/ui

Testing

npm test
npm run test:coverage

Coverage is enforced at 90% across lines, functions, branches, and statements.

Release

The repository includes GitHub Actions for CI and release:

  • CI runs lint, typecheck, package build, and coverage on Node 20 and 22.
  • Release publishes to npm on v*.*.* tags and creates a GitHub release.

License

MIT

Packages

 
 
 

Contributors