npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@sonawaneyogesh/live-api-tracing

v1.1.1

Published

Reusable Istanbul/nyc coverage tracer for Express apps: a guarded HTTP route that dumps global.__coverage__ to disk on demand.

Readme

@sonawaneyogesh/live-api-tracing

Real-time API code-coverage tracing for Express apps, built on Istanbul/nyc. See exactly which lines and branches a running app has executed — across all traffic, for one specific request, or as a full execution-path report with each callee's source inlined at its call site.

What you get

| Mode | Class | Answers | |---|---|---| | Cumulative dump | CoverageRouterFactory | "What has this process executed so far, across every request?" | | Per-request diff | RequestCoverageMiddlewareFactory | "What did this one API call touch?" | | Call-tree report | TypeScriptCallGraphAnalyzer + HtmlCallTreeRenderer | "Show me the actual execution path for this call, with each callee's source inlined at its call site." | | Live variable trace | VariableCaptureRegistry | "What were the actual variable values at each executed line?" |

All four are opt-in, add near-zero overhead when unused, and are hard-gated to only run in development / test / staging / qa — never production — and only while the process is actually running under nyc instrumentation.

See HOW_TO_USE.md for full usage instructions — two concrete, copy-pasteable quick starts (§2 for a plain ESM app, §3 for a TypeScript/tsc app walking through all four modes together), then the per-mode reference, configuration options, and the steps to publish this package to npmjs.com.

Before you install — four things that will trip you up

These aren't edge cases — they're the exact issues hit while integrating this package for real. Read this before you start; it'll save you the debugging time.

  1. If your app uses "type": "module" (ESM import), you need a second CommonJS entry point for tracing sessions. nyc's live instrumentation hook only intercepts require(), not ESM import. Your app runs fine as ESM day-to-day — you only need a CommonJS mirror of your entry file when you actually run it under nyc. Full pattern in the Quick Start below.
  2. Pin typescript to ^5.5.3 — do not let a plain npm install typescript grab latest. This only matters if you want Mode C (the call-tree report). TypeScript 7 does not populate ts.sys, which TypeScriptCallGraphAnalyzer depends on to read your tsconfig.json — it fails immediately with TypeError: Cannot read properties of undefined (reading 'readFile'). Install it explicitly: npm install --save-dev typescript@^5.5.3.
  3. Mode C only resolves class methods. findFunction('ClassName', 'methodName') looks for an actual class ClassName { methodName(req, res) {...} } declaration in your source — not a plain arrow function passed straight to app.get(). If your routes are written as inline handlers (the norm in most Express tutorials), Mode C has nothing to find. Wrap the handlers you want call-tree reports for in small classes — see the Mode C section below.
  4. Mode D (live variable trace) pauses the whole process briefly on every hit, and it captures real application data. It's pure runtime middleware, like A/B/C — no build step — but it works by setting real debugger breakpoints (via node:inspector), and V8's Debugger Protocol pauses aren't scoped to just the traced request. Every captured value is real data your app produced — request payloads, computed results, whatever was in scope. Never point it at production, and never trace a function without checking what it might hold (see the Mode D section below for the redaction defaults, the pause cost, and how to extend them).

Install

npm install @sonawaneyogesh/live-api-tracing express istanbul-lib-coverage

typescript is a peer dependency too, but only needed if you're using Mode C — install it pinned, per the warning above:

npm install --save-dev typescript@^5.5.3   # only if you want the Mode C call-tree report

Quick start (plain JavaScript, ESM app — the common case)

1. Wire up your app as normal. src/server.js:

import express from 'express';
import { CoverageRouterFactory, RequestCoverageMiddlewareFactory } from '@sonawaneyogesh/live-api-tracing';

const app = express();

app.use(CoverageRouterFactory.create());          // Mode A: GET /api/dev/coverage-dump
app.use(RequestCoverageMiddlewareFactory.create()); // Mode B: always-on per-request diffs

app.get('/', (req, res) => res.send('Hello World!'));

app.listen(3000);

Run it with node src/server.js for everyday development — both tracers no-op (near-zero cost) until the process is actually instrumented, so there's no downside to leaving them mounted.

2. Add a CommonJS mirror for tracing sessions. nyc can't see through ESM import, so give it a .cjs twin of your entry file — Node treats .cjs as CommonJS regardless of your package.json's "type": "module". src/server.cjs:

const express = require('express');
const { CoverageRouterFactory, RequestCoverageMiddlewareFactory } = require('@sonawaneyogesh/live-api-tracing');

const app = express();

app.use(CoverageRouterFactory.create());
app.use(RequestCoverageMiddlewareFactory.create());

app.get('/', (req, res) => res.send('Hello World!'));

app.listen(3000);

Keep this file a thin shell — put real business logic in shared modules both entries can call into, so you're not maintaining two copies of actual behavior. (If your app is TypeScript compiled via tsc, you don't need a hand-written mirror at all — just add a second tsconfig targeting CommonJS output, exactly the pattern this repo uses on itself; see tsconfig.trace.json and the build:trace script below.)

3. Install nyc and add a script to run the CommonJS entry under it:

npm install --save-dev nyc
{
  "scripts": {
    "trace:server": "nyc --reporter=none node src/server.cjs"
  }
}

trace:server isn't a magic name — the package's own 500 error just suggests it as a convention ("Start the server with the trace:server script") when it detects you're not instrumented. Call your script whatever you like, as long as it runs the .cjs entry through nyc.

4. Run it, exercise the API, then dump coverage:

npm run trace:server
curl http://localhost:3000/          # generate some traffic
curl http://localhost:3000/api/dev/coverage-dump
nyc report --reporter=html --temp-dir=.nyc_output

That's the full loop for Mode A. Mode B (per-request diffs) needs no extra step — it's already writing a diff per request to .nyc_output/requests/. Mode C needs one more piece; see below.

Mode A — cumulative coverage-dump route

app.use(CoverageRouterFactory.create());

Mounts GET /api/dev/coverage-dump by default. Refuses to respond (403) outside development/test/staging/qa, and 500s if the process isn't running under nyc. Hitting it writes .nyc_output/out.json and zeroes the in-memory counters — stop the server, then nyc report --reporter=html --temp-dir=.nyc_output to view it.

Mode B — per-request coverage diff

// Mount this as early as possible, before your routes.
app.use(RequestCoverageMiddlewareFactory.create());

Once mounted, this is always on — no dump call needed. Every request gets a snapshot taken before its handler runs and again once the response finishes; the diff is written to .nyc_output/requests/<requestId>.json, with an index.json listing every recorded request (method, path, status, duration). Works identically no matter how the route was declared — flat, chained (router.route('/x').get().post()), nested/mounted sub-routers, param routes, all of it — the middleware only cares that a response eventually finishes. Turn a trace file into an HTML report with a small script built on the nyc package — see the full example in HOW_TO_USE.md.

Mode C — call-tree report (execution path with inlined source)

Requires a class-based handler — see warning #3 above. Given:

class UserController {
  create(req, res) { /* ... */ }
}
import { TypeScriptCallGraphAnalyzer, HtmlCallTreeRenderer } from '@sonawaneyogesh/live-api-tracing';

const callGraph = new TypeScriptCallGraphAnalyzer().analyze('tsconfig.json');
const entry = callGraph.findFunction('UserController', 'create'); // <ClassName>, <methodName>

const html = new HtmlCallTreeRenderer().render(entry, callGraph, coverageMap);

Purely static call resolution (TypeScript Compiler API), spliced together with the coverage diff from Mode B — only executed statements get highlighted, and only calls resolving to your own project source get expanded inline.

If your project is plain JavaScript (not TypeScript), the analyzer still works — it just needs a tsconfig.json with allowJs: true, scoped to the file(s) you're tracing:

{
  "compilerOptions": { "allowJs": true, "checkJs": false },
  "include": ["src/server.cjs"]
}

Keep include scoped tightly: if two classes in your analyzed source share the same name, findFunction throws an "Ambiguous lookup" error rather than guessing which one you meant.

See HOW_TO_USE.md for a complete, runnable report script.

Mode D — live variable trace (execution values, not just execution counts)

No build step. Unlike the version of this mode that shipped earlier, Mode D now works purely at runtime, on top of your normal build (or no build at all, for plain JS) — it uses Node's own node:inspector debugger protocol in-process to set breakpoints at the methods you name, exactly the same way nyc/coverage already runs entirely at runtime. It needs a class-based method to attach to (same addressing scheme as Mode C: "ClassName.methodName").

1. Turn it on by passing variableCapture in the config given to RequestCoverageMiddlewareFactory.create() — variable-capture events are drained and persisted in the same res.on('finish') handler as the coverage diff:

const { RequestCoverageMiddlewareFactory, FileSystemVariableCaptureStorage } = require('@sonawaneyogesh/live-api-tracing');

app.use(
  RequestCoverageMiddlewareFactory.create(
    {
      variableCapture: {
        targets: ['OrderController.charge'], // "ClassName.methodName", explicit allowlist only
        sourceDir: 'dist', // where to scan for the compiled .js/.cjs/.mjs files containing these methods
      },
    },
    { variableCaptureStorage: new FileSystemVariableCaptureStorage('.nyc_output/requests') },
  ),
);

That's the whole setup. This writes a sibling .nyc_output/requests/<requestId>.variables.json next to Mode B's own <requestId>.json coverage diff.

Capture points, per target method: function entry (parameters), immediately before every return/throw, and — only with granularity: 'statement' (default is 'boundary', entry + return/throw only) — after every other top-level statement or of a top-level try-block. The try-block recursion exists specifically because almost every real Express handler wraps its logic in exactly one top-level try { ... } catch { ... }; without it, a local declared inside that block would never get captured. If/loop/catch/finally bodies still aren't recursed into (a known v1 limitation, not silently wrong).

2. Render it inline in a Mode C report — pass a JsonVariableAnnotationProvider as HtmlCallTreeRenderer's second constructor argument; omitting it (the default) leaves Mode C's output exactly as before:

const { HtmlCallTreeRenderer, JsonVariableAnnotationProvider } = require('@sonawaneyogesh/live-api-tracing');

const variableProvider = new JsonVariableAnnotationProvider(`.nyc_output/requests/${requestId}.variables.json`);
const html = new HtmlCallTreeRenderer(undefined, variableProvider).render(entry, callGraph, coverageMap);

The trade-off, stated plainly, not hidden: every breakpoint hit briefly pauses the whole process, not just the traced request — V8's Debugger Protocol pauses the entire event loop for as long as one capture round trip takes. The breakpoint's own condition keeps unrelated hits cheap (it's checked by V8 before ever pausing at all), and the 'boundary' default keeps the traced endpoint's own pause count down — but this is a real cost the earlier build-time version never had, acceptable for its actual use case (never production, tracing one request at a time) rather than a free win.

If you're compiling TypeScript with source maps on (sourceMap: true, tsc's default in most setups), Mode D automatically maps its captured positions back to your original .ts source via that map — so point Mode C's TypeScriptCallGraphAnalyzer at your normal tsconfig.json (real .ts sources), not at the compiled output; the two line up on their own, and the report shows your actual source. Without a source map (a plain-JS project, most commonly), captured positions are just the compiled file's own — which is also what a plain-JS Mode C analysis already reads, so nothing needs to change there either.

Safety, by default, not by configuration:

  • Any value structurally resembling a large host object (req/res/sockets/streams/event emitters) is replaced with a type tag and never descended into — regardless of what the variable holding it is named. This is a structural check, not a name-based one.
  • Property names matching password|token|secret|authoriz|cookie|apikey (case-insensitive), at any depth, are replaced with [Redacted]. Override via a custom IRedactionPolicy passed to SafeValueSerializer.
  • Depth, array length, string length, and total-node caps are hard limits, not configurable away to zero — they're what keeps a captured value bounded and safe to write to disk.
  • A per-request cap (variableCapture.maxEventsPerRequest, default 200) drops the oldest events once hit, with an honest droppedCount in the output rather than an unbounded buffer.

Configuration

Both CoverageRouterFactory.create() and RequestCoverageMiddlewareFactory.create() take an optional config-overrides object as their first argument:

CoverageRouterFactory.create({
  routePath: '/internal/coverage',        // default: /api/dev/coverage-dump
  outputDir: '.nyc_output',               // default: .nyc_output
  outputFileName: 'out.json',             // default: out.json
  enabledEnvironments: ['development'],   // default: ['development', 'test', 'staging', 'qa']
});

RequestCoverageMiddlewareFactory.create({
  variableCapture: {                      // omit entirely to leave Mode D off
    targets: ['OrderController.charge'],
    sourceDir: 'dist',
    granularity: 'boundary',              // default: 'boundary'; or 'statement'
    maxEventsPerRequest: 200,             // default: 200
  },
});

Every dependency (tracer, storage, guard, differ, variableCaptureStorage, variableCaptureRegistry) is also injectable as a second argument, for tests or a custom storage backend — implement ICoverageStorage's save(snapshot): Promise<void> to plug in your own persistence layer.

Requirements

  • You must run under nyc. These tools read global.__coverage__, which only exists once Istanbul instrumentation is active.
  • nyc's live require-hook needs CommonJS — see the Quick Start above.
  • Environment gating is intentional and hard-coded — both the dump route and the per-request middleware refuse to activate outside development/test/staging/qa.
  • Express ^4.19.2 || ^5.0.0. Both major versions are tested and supported.
  • Mode D pauses the whole process on every breakpoint hit (V8's Debugger Protocol, not scoped to just the traced request) — see the Mode D section above for the full trade-off.

Full usage guide, troubleshooting, and internals: HOW_TO_USE.md

Working on this repo itself

npm install
npm run build        # ESM build -> dist/
npm run build:trace   # CommonJS build -> dist-cjs/, for tracing sessions
npm test              # unit + integration tests

This is a dev/QA/staging-only library — both the dump route and per-request tracing refuse to activate when NODE_ENV=production.

License

MIT