@all-wits/witslog
v0.6.4
Published
Framework-agnostic Node.js SDK for witslog structured error logging
Maintainers
Readme
🪵 witslog (Node.js SDK)
Framework-agnostic Node.js SDK for witslog structured error logging.
Thin wrapper over the native witslog-ffi C ABI via koffi — one
dependency, prebuilt, no native build step. As of 0.4.0 it also bundles the real witslog
CLI binary per platform, so witslog query/stats/export/serve-mcp/doctor etc. (the
read/ops surface that has no FFI equivalent — see CONTRACT.md) work straight after install, no
separate CLI install required. See CONTRACT.md
for the full SDK↔native ABI, and CHANGELOG.md
for this package's release history.
📦 Install
npm install @all-wits/witslogpnpm add @all-wits/witslogbun add @all-wits/witslogNative libraries and the witslog CLI binary for Windows x64, Linux x64/arm64, and macOS
(Apple Silicon) are bundled — npm install / pnpm add / bun add alone is enough on those
platforms, for both the SDK and npx witslog <command> / a global-install witslog on your
PATH. See Platform support below for the current gap.
🚀 Quick Start
const witslog = require('@all-wits/witslog');
witslog.init({ createProject: true }); // scaffolds .witslog/ if missing, then mounts
witslog.error('myapp', 'db timeout', { context: { request_id: 'r1' }, tags: ['db'] });
try {
risky();
} catch (e) {
witslog.exception('myapp', e); // captures err.stack (and, if e.cause is set —
// e.g. Node's own fetch() failures — the full
// cause chain, folded into stacktrace + context.root_cause)
}init() needs a .witslog/ project directory to write into — pass createProject: true
(scaffolds one at process.cwd()) or createProject: '/path/to/project' the first time you
mount in a fresh project; it's a no-op on later runs once .witslog/ already exists.
As of 0.4.0, npm install @all-wits/witslog also gives you the real witslog CLI — on the
4 bundled platforms (see Platform support), a plain npm install wires up
npx witslog <command> (and a global install puts witslog on your PATH) with the same binary
docs/install.md or Homebrew/Scoop/cargo install would give you —
witslog init, witslog query, witslog stats, witslog serve-mcp, all of it:
npx witslog init .
npx witslog query "db timeout*"On a real terminal, npx witslog init also asks (arrow keys/spacebar/enter, plain language,
no jargon) whether you want to protect sensitive data you might log later — say yes and it
generates a secret key, shows it to you once (never saved to disk — you store it yourself), and
wires up .witslog/config.toml. Skip the prompt with npx witslog init --encrypt or --yes;
piped/CI usage never sees a prompt either way. Revisit the choice anytime with
npx witslog config. See
CONTRACT.md's "Metadata encryption" section
for what this does and doesn't cover.
For programmatic use createProject: true remains the way to scaffold .witslog/ from code
without shelling out:
const witslog = require('@all-wits/witslog');
witslog.init({ createProject: true }); // scaffolds .witslog/, cross-platformIf you also separately install the witslog CLI (cargo install,
Homebrew, Scoop, or a release binary), or the bundled binary isn't available for your platform,
point WITSLOG_CLI=/path/to/witslog at it — the npm-bundled and separately-installed CLIs are
interchangeable, pick whichever is already in your toolchain.
⚠️ For MCP (AI-assistant integration), install the CLI globally instead of relying on the npm-bundled binary — see the root README's MCP section for why: macOS Intel has no npm-bundled CLI at all (only curl/irm/ Homebrew/Scoop/
cargo installcover it), and an MCP config generated from a path inside this project'snode_modules/breaks ifnode_modulesis ever removed or reinstalled elsewhere. The npm-bundlednpx witslog <command>is for ad-hoc/manual use from inside a project — a globally installed CLI is whatserve-mcp --print-mcp-configshould point an MCP client at.
ℹ️ MCP tools are self-teaching:
initializereturns worked-exampleinstructions, and every tool description carries anExample: {...}call — helps lightweight/under-informed models pick the right tool (e.g.search_errorsvs.latest_errors) on the first try. See the root README's MCP section.
🔒 Security:
argvenrichment defaults on and captures the full command line. If your app may receive secrets as bare CLI args, callwitslog.init({ enrich: { argv: false } })— see CONTRACT.md.
🧩 Express
const { witslogErrorHandler } = require('@all-wits/witslog/frameworks/express');
witslog.init();
app.use(witslogErrorHandler('myapp')); // last, after routes🌐 Browser-side error capture
Pairs with the browser reporter — a zero-dep client that batches window.onerror /
unhandled-rejection events (plus, opt-in, console.error/console.warn and resource-load
failures) and ships them via navigator.sendBeacon to this ingest endpoint. Available two
ways: as the npm subpath @all-wits/witslog/browser (0.6.1+, packaged copy) for
bundler/import usage, or the standalone
bindings/browser/witslog-browser.js
for a plain <script src> — same API either way.
import WitslogBrowser from '@all-wits/witslog/browser';
const reporter = WitslogBrowser.init({
endpoint: '/api/witslog-ingest',
app: 'my-web-app',
captureConsole: true, // also captures console.error/warn + resource-load failures
});const { witslogBrowserIngest } = require('@all-wits/witslog/frameworks/express');
app.use(witslogBrowserIngest({
allowedOrigins: ['https://your-app.example'], // required — fail-closed, default []
}));🔒 Security: the request body is untrusted input that lands in
events.message, which MCP serves verbatim to an AI assistant. This handler is armed fail-closed: empty origin allowlist by default, refuses to run underNODE_ENV=productionunless{ force: true }, rate-limited per client, and severity clamped toerror/warn(neverfatal/critical).tags: ['browser']is advisory only, not a trust boundary. See CONTRACT.md for the Python/PHP ingest recipe and the full guardrail rationale.
🧵 Zero-boilerplate auto-instrumentation
Mount instrumentation once instead of hand-writing try/catch +
witslog.exception/witslog.error at every route handler and outbound fetch call.
Instrumented fetch — witslogFetch
Explicit wrapper around fetch (no global monkeypatch — safe alongside Next.js's own fetch
caching/instrumentation). Swap it in at your outbound-request choke points:
const { witslogFetch } = require('@all-wits/witslog/fetch');
const res = await witslogFetch(upstreamUrl, init, {
application: 'my-proxy',
tags: ['proxy'],
context: { path: '/cards/123' },
});Auto-captures a correlation id (x-request-id by default, propagated to the outbound
request), context.timing.latency_ms, and on failure:
- Thrown error (network unreachable/timeout) — logs via
exception()(full.causechain,error_code: 'UPSTREAM_UNREACHABLE'), then rethrows the original error unchanged. - Non-2xx response — peeks the body via
.clone()(your code still gets the untouchedResponse), extractserror_code/message/detailsfrom a{error:{code,message,details}}body when present, and logs atwarnfor 4xx (expected client-caused conflicts) /errorfor 5xx.
Next.js adapter
// instrumentation.ts — Next.js's own server-boot hook. Captures every uncaught error in
// route handlers, Server Components, Server Actions, and middleware — zero per-route code.
import { register as registerWitslog, onRequestError as witslogOnRequestError } from '@all-wits/witslog/frameworks/next';
export function register() {
registerWitslog('my-app', { createProject: true });
}
export const onRequestError = witslogOnRequestError;withWitslog(handler, opts?) wraps a single route handler explicitly, for Next < 15 or when
you want per-route timing/correlation without global instrumentation.
witslogNextIngest(options) is the Next.js Route Handler-shaped equivalent of
witslogBrowserIngest above (Express's raw req/res and Next's Web Request/Response
aren't interchangeable, so this is a separate export, not a re-export — same guardrails):
// app/api/witslog-ingest/route.ts — do NOT start the folder name with `_`;
// Next.js's App Router treats any path segment starting with `_` as a
// private folder excluded from routing, so app/api/__witslog/route.ts
// would silently never register a route (every POST 404s).
import { witslogNextIngest } from '@all-wits/witslog/frameworks/next';
export const POST = witslogNextIngest({ allowedOrigins: ['https://your-app.example'] });React Query client capture
Subscribes to a TanStack QueryClient's MutationCache/QueryCache — the same event
stream TanStack Query Devtools itself observes — so every failed query/mutation (key,
variables, error) is captured with zero per-hook code. Browser-safe, no hard
@tanstack/react-query dependency.
import { attachWitslog } from '@all-wits/witslog/frameworks/react-query';
// `report` is any {enqueue(event)} sink — typically WitslogBrowser.init(...)
// from bindings/browser/witslog-browser.js, which ships events to witslogNextIngest/witslogBrowserIngest
attachWitslog(queryClient, { report: myBrowserReporter, tags: ['my-app'] });See CONTRACT.md
for the full design (including the context.root_cause convention exception()/witslogFetch
use, and how clampContext bounds what an ingest endpoint accepts).
Correlation id + network-tab-equivalent capture
Closes the "a React Query failure can't be correlated with the proxy log for the same request" gap, plus transport-layer failures React Query never sees (WebSocket disconnects, direct axios calls).
// client.ts — mints/reuses a correlation id per request (propagated as a header,
// default x-request-id), stamps correlationId/latencyMs onto the response/rejected
// error. Does NOT log every rejection itself (attachWitslog already captures every
// React-Query-managed failure) — opt a specific call into direct capture instead:
import { witslogAxiosInterceptor } from '@all-wits/witslog/frameworks/axios';
witslogAxiosInterceptor(apiClient, { report: myBrowserReporter, tags: ['my-app'] });
// a call that bypasses React Query entirely (e.g. an imperative token fetch)
apiClient.get('/collab/ticket', { witslogDirectCapture: true });frameworks/react-query.js's buildEvent reads error.correlationId/error.latencyMs
(when stamped by the interceptor above) into correlation_id/context.timing.latency_ms,
and independently computes latency from TanStack Query v5's
state.submittedAt/state.errorUpdatedAt when those aren't present — no extra wiring
needed for either.
bindings/browser/witslog-websocket.js's witslogWebSocketWatch(opts) (vendored file,
alongside witslog-browser.js — not an npm subpath) returns {onClose, onDisconnect}
handlers shaped for HocuspocusProvider's constructor options, logging abnormal closes
(code not 1000/1001) with error_code: WS_CLOSE_<code> and
context.ws: {code, reason, wasClean}:
const { witslogWebSocketWatch } = require('./witslog-websocket');
const watch = witslogWebSocketWatch({ report: myBrowserReporter });
new HocuspocusProvider({ ..., onClose: watch.onClose, onDisconnect: watch.onDisconnect });Hocuspocus/Yjs collab-provider adapter (Node-side)
frameworks/hocuspocus.js — attachWitslogHocuspocus(provider, opts) — is the Node-process
counterpart to witslog-websocket.js above, purpose-built for a HocuspocusProvider (or any
EventEmitter-shaped target exposing on(event, fn)/off(event, fn)) rather than a raw
CloseEvent hook. Two improvements over the vendored watcher for this target specifically:
isAbnormalClose(code, wasClean) treats any wasClean:true close as normal — the vendored
watcher checks code alone, so a clean disconnect that synthesizes close code 1005 ("No Status
Rcvd", which the browser does locally on a routine provider.destroy()/tab-nav/HMR reload) is
misclassified as abnormal — and it additionally captures authenticationFailed
(error_code: COLLAB_AUTH_FAILED). Like the vendored watcher, it flushes the reporter
immediately after every emit (connection loss/auth failure is rare and urgent, unlike
high-volume sources such as react-query/axios that rely on the reporter's batch window), and
duck-types against the target's public API — no hard @hocuspocus/provider dependency.
const { HocuspocusProvider } = require('@hocuspocus/provider');
const { attachWitslogHocuspocus } = require('@all-wits/witslog/frameworks/hocuspocus');
const hp = new HocuspocusProvider({ url, name, document, token });
const detach = attachWitslogHocuspocus(hp, { report: myBrowserReporter, tags: ['my-app'] });
// later: detach(); hp.destroy();Tests: test/hocuspocus.test.js.
🧱 Works with your Node.js stack
@all-wits/witslog is a plain npm/pnpm/bun package with no bundler-specific glue — it works
in any Node.js process, which covers the server side of most modern frameworks:
- Next.js — call it from Route Handlers / API Routes, Server Actions, or Server
Components (
witslog.init()once at module scope,witslog.error(...)in acatch). Next.js bundles server route code by default (webpack/turbopack), and that breaks resolution ofkoffi's native.nodeaddon — you'll hit:
Fix: tell Next toError: Cannot find the native Koffi module; did you bundle it correctly?require()both packages natively instead of bundling them, innext.config.ts:
(Next.js ≥15; pre-15 useconst nextConfig: NextConfig = { serverExternalPackages: ["@all-wits/witslog", "koffi"], };experimental.serverComponentsExternalPackagesinstead.) This is the same fix any native-addon npm package needs under Next.js (e.g. Prisma, Sharp) — no witslog code change can make a.nodebinary bundler-safe, so this config is required, not optional. - Nuxt.js — same idea inside server routes / the Nitro server (
server/api/*.ts,server/plugins/*.ts). - Vite — use it in a Vite SSR entry (
vite-node,vite-plugin-ssr, a customserver.js) or invite.config.jsbuild hooks — anywhere Vite code actually executes in Node, not in code shipped to the browser.
⚠️ Not for browser bundles. The native
witslog_ffilibrary is loaded viakoffi, which needs a real Node.js process — it cannot run inside client-rendered Vue.js components, React components, or any code that ends up in a browser bundle. If you're using Vue.js/React purely client-side, log from your Node backend (API route, SSR middleware, server action) instead of the browser-rendered component itself.
📖 API
| Function | Description |
|----------|--------------|
| init(config?) | Mount the SDK; pass { createProject: true } to scaffold .witslog/ first, plus optional enrich/redact/buffer config (see CONTRACT.md). |
| error/warn/info(app, message, opts?) | Log at the given severity. opts: context, tags, metadata, error_code, exception, stacktrace, correlation_id, parent_event_id, category, version, environment. |
| log(app, message, opts?) | Same as error, explicit severity via opts.severity. |
| exception(app, err, opts?) | Log a caught Error, capturing err.stack and, when set, err.cause's full chain (folded into stacktrace + context.root_cause). |
| flush() / shutdown() | Drain buffered events before exit. |
Auto-instrumentation (see above)
| Import | Function | Description |
|--------|----------|--------------|
| @all-wits/witslog/fetch | witslogFetch(input, init, opts?) | Instrumented fetch wrapper. |
| @all-wits/witslog/frameworks/next | register(app, config?) / onRequestError(err, req, ctx) / withWitslog(handler, opts?) | Next.js server-error capture. |
| @all-wits/witslog/frameworks/next | witslogNextIngest(options) | Browser-ingest endpoint, Next.js Route Handler shape. |
| @all-wits/witslog/frameworks/react-query | attachWitslog(queryClient, opts) | Global React Query mutation/query failure capture. |
| @all-wits/witslog/frameworks/axios | witslogAxiosInterceptor(axiosInstance, opts?) | Correlation-id propagation + latency stamping on an axios instance. |
| @all-wits/witslog/browser (0.6.1+) | WitslogBrowser.init(config) | Browser-side client reporter — window.onerror/unhandled-rejection by default, plus console.error/console.warn + resource-load failures with captureConsole: true. |
| bindings/browser/witslog-websocket.js (vendored, not an npm subpath) | witslogWebSocketWatch(opts) | Abnormal WebSocket close/disconnect capture (browser-side). |
| @all-wits/witslog/frameworks/hocuspocus | attachWitslogHocuspocus(provider, opts) | Node-side Hocuspocus/Yjs collab-provider adapter — close/disconnect + authenticationFailed capture, wasClean-aware. |
🌍 Platform support
| Platform | Status | |----------|--------| | Windows x64 | ✅ | | Linux x64 | ✅ | | Linux arm64 | ✅ | | macOS arm64 (Apple Silicon) | ✅ | | macOS x64 (Intel) | ⬜ not yet built by CI — see CHANGELOG |
If your platform isn't bundled, point at a local build via WITSLOG_LIB=/path/to/witslog_ffi.*
(native lib) and WITSLOG_CLI=/path/to/witslog[.exe] (CLI binary).
🧪 Test
pnpm install && pnpm test📄 License
Apache License 2.0 — see LICENSE.
