@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.
Maintainers
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.
- If your app uses
"type": "module"(ESMimport), you need a second CommonJS entry point for tracing sessions.nyc's live instrumentation hook only interceptsrequire(), not ESMimport. Your app runs fine as ESM day-to-day — you only need a CommonJS mirror of your entry file when you actually run it undernyc. Full pattern in the Quick Start below. - Pin
typescriptto^5.5.3— do not let a plainnpm install typescriptgrab latest. This only matters if you want Mode C (the call-tree report). TypeScript 7 does not populatets.sys, whichTypeScriptCallGraphAnalyzerdepends on to read yourtsconfig.json— it fails immediately withTypeError: Cannot read properties of undefined (reading 'readFile'). Install it explicitly:npm install --save-dev typescript@^5.5.3. - Mode C only resolves class methods.
findFunction('ClassName', 'methodName')looks for an actualclass ClassName { methodName(req, res) {...} }declaration in your source — not a plain arrow function passed straight toapp.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. - 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-coveragetypescript 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 reportQuick 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_outputThat'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 customIRedactionPolicypassed toSafeValueSerializer. - 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 honestdroppedCountin 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 readglobal.__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 testsThis is a dev/QA/staging-only library — both the dump route and per-request tracing refuse to
activate when NODE_ENV=production.
License
MIT
