@usereelay/node
v0.1.3
Published
Reelay SDK for Node.js: non-blocking error capture, HTTP middleware, AsyncLocalStorage request context, trace stitching.
Readme
@usereelay/node
Reelay error monitoring for Node.js backends. Captures uncaught exceptions, unhandled rejections, and request-scoped errors with full HTTP context, per-request log/span timelines, and frontend-to-backend trace stitching.
- Zero runtime dependencies. Everything is built in: transport, PII scrubbing, stack parsing, trace IDs, and AsyncLocalStorage request context.
- Zero host disruption. The SDK never swallows your errors, never replaces your error handling, and never changes your process's crash behavior.
- PII scrubbed before the wire. Secrets are redacted in-process before any payload is sent.
Requires Node >= 18.
Install
npm install @usereelay/nodeQuick start (Express)
import express from 'express';
import * as Reelay from '@usereelay/node';
// 1. Initialize the SDK (before your routes)
const reelay = Reelay.init({
endpoint: 'https://api.usereelay.com',
token: process.env.REELAY_TOKEN!,
});
const app = express();
// 2. Request middleware — seeds trace context per request (first middleware)
app.use(Reelay.requestHandler(reelay));
// 3. Your routes and business logic
app.get('/checkout', (req, res) => {
throw new Error('payment failed');
});
// 4. Error middleware — captures errors and re-throws (before your handler)
app.use(Reelay.errorHandler(reelay));
app.use((err, req, res, next) => {
res.status(500).json({ error: 'internal server error' });
});
app.listen(3000);That's it. Every error thrown in a route (or passed to next(err)) is
captured with the request's HTTP method, URL, status code, safe headers, and
any logs/spans recorded during that request.
Verify your setup
Add a route that throws an error, then hit it:
curl http://localhost:3000/checkoutThe error should appear in your Reelay dashboard within seconds. The event
will include the HTTP context (GET /checkout → 500) and any breadcrumbs or
log lines recorded during the request.
Automatic instrumentation
Uncaught exceptions and unhandled rejections
The SDK registers uncaughtException and unhandledRejection listeners as
soon as init() is called. This is always on and cannot be disabled —
capturing crashes is the SDK's core job.
Crash semantics are preserved. Node's default behavior on an uncaught
exception is to print the error and exit with code 1. The SDK mimics this
exactly when it is the only uncaughtException listener:
- The error is captured and queued for delivery.
- The error is printed to stderr.
- A bounded flush (≤ 2 seconds) is started.
process.exit(1)is called.
If your application has its own uncaughtException handler, the SDK only
captures the error and defers the exit decision to your handler.
Unhandled rejections are captured and then re-thrown, which surfaces them
as uncaught exceptions. The thrown value is de-duplicated so it is reported
exactly once, regardless of Node's --unhandled-rejections mode.
Request middleware
Reelay.requestHandler(client) is a framework-agnostic middleware compatible
with Express, Connect, and frameworks built on them (like Fastify).
What it does for every request:
- Reads the incoming
traceparentandreelay-session-idheaders (sent automatically by@usereelay/browser). - Seeds an AsyncLocalStorage context with the trace ID, session ID, HTTP context (method, URL, safe headers), and fresh log/span/breadcrumb buffers.
- Listens for the response
finishevent to capture the final status code.
The context is available to every function that runs during that request,
across all async boundaries — await, setTimeout, Promise.all, etc.
Error middleware
Reelay.errorHandler(client) captures any error passed to next(err), then
re-throws it to the next error handler in the chain. The SDK never
swallows application errors.
- If the response hasn't been sent yet and the status is < 500, it sets
res.statusCode = 500. - The HTTP status is stamped onto the request context before capture (the
response
finishevent fires too late — it would miss the error payload).
Both middleware have an overhead well under 1 ms on the hot path (a single
header parse and an AsyncLocalStorage run() call).
Manual error capture
captureException(err)
Report a handled error outside of a request context (e.g., a cron job, queue consumer, or startup routine):
import * as Reelay from '@usereelay/node';
try {
await processQueue();
} catch (err) {
Reelay.captureException(err);
}When called inside an active request context, the error is automatically enriched with the request's trace, session, HTTP context, and timeline.
Returns the event ID (evt_...) or '' if the event was dropped.
captureMessage(message)
Report a plain string as an error event:
Reelay.captureMessage('Job completed with warnings');Request context
Logs and spans
During a request, you can append structured data to the timeline. This data is shipped with any error captured in that request.
import * as Reelay from '@usereelay/node';
app.get('/checkout', (req, res) => {
// Stored under the current request context
Reelay.appendLog('info', 'checkout started');
Reelay.appendSpan({ span_id: 'abc', name: 'process-payment', status: 'ok' });
throw new Error('payment failed');
// The error event will include the log and span above
});Timelines are capped at 100 entries per request.
Breadcrumbs
Each request has its own isolated breadcrumb buffer. This prevents one request's breadcrumbs from leaking onto another concurrent request's error report. Breadcrumbs are scrubbed (PII patterns applied) and bounded at 50 entries.
Add breadcrumbs manually with client.addBreadcrumb():
const client = Reelay.getClient();
if (client) {
client.addBreadcrumb({ timestamp: Date.now(), type: 'debug', category: 'db', message: 'query completed' });
}Off-request contexts (cron jobs, queue workers)
Use runWithContext to create a request-like context for non-HTTP workflows:
import { runWithContext, newTraceId, newSpanId } from '@usereelay/node';
const ctx = {
trace: { trace_id: newTraceId(), span_id: newSpanId() },
logs: [],
spans: [],
crumbs: new BreadcrumbBuffer(),
};
runWithContext(ctx, () => {
// errors here are captured with the trace context above
myQueueWorker();
});Trace stitching (frontend ↔ backend)
When a page using @usereelay/browser calls your API, the browser SDK
automatically sends two headers:
traceparent— the W3C trace context header (00-{trace_id}-{span_id}-01)reelay-session-id— the browser session identifier
The Node SDK's requestHandler reads these headers and adopts them. This
means:
- A backend error and the frontend session replay share the same
trace_id. - The Reelay dashboard links the frontend error (with its replay clip) and the backend error (with its request context) into a single incident view.
No configuration is needed on the Node side to make this work.
Configuration reference
init(options)
| Option | Type | Default | Description |
|---|---|---|---|
| endpoint | string | — | Required. Ingestion URL, e.g. https://api.usereelay.com. |
| token | string | — | Required. Node API key (rlyt_live_...). Never a ReelayID. |
| sampleRate | number (0–1) | 1 | Fraction of errors to send. 1 = every error. Lower only to shed load on high-traffic services. |
| beforeSend | (event) => event \| null | — | Mutate or veto (return null) an event before it is sent. Runs after PII scrubbing. |
| scrubPatterns | RegExp[] | [] | Extra regex patterns applied to all string values. Matches are replaced with [redacted]. |
| redactedFields | string[] | [] | Object key names whose values are replaced with [redacted] (case-sensitive, every nesting level). |
| maxQueueSize | number | 30 | Maximum buffered events while offline or rate-limited. Oldest dropped when full. |
| debug | (msg, detail?) => void | — | Receive SDK-internal diagnostics. Never logs to console by default. |
Middleware
Reelay.requestHandler(client) // → Express/Connect middleware (req, res, next)
Reelay.errorHandler(client) // → Express error middleware (err, req, res, next)PII scrubbing
The SDK scrubs sensitive data before any payload leaves the process. The same engine powers both the browser and Node SDKs, so behavior is identical across the stack.
Scrubbing applies to:
- Error messages — pattern-matched and trimmed to 2,000 chars.
- Breadcrumb messages — pattern-matched and trimmed to 500 chars.
- HTTP context objects — deeply walked, keys matched, values pattern-scrubbed.
- Timeline logs — deeply walked and scrubbed.
Scrubbing never applies to:
- Stack trace frames (file paths, line numbers).
- Event metadata (timestamps, event IDs, trace IDs).
Built-in patterns (always active)
- Bearer/token values —
bearer: xyz,token=abc,api_key: xxx,password: hunter2 - JWTs — any string matching
eyJ... - Credit card numbers — 13–19 digit sequences
- Email addresses —
[email protected]
Built-in sensitive keys (always active)
Values under these object keys are replaced wholesale with [redacted]:
authorization, cookie, set-cookie, x-api-key, x-reelay-token,
password, passwd, secret, token, access_token, refresh_token,
credit_card, card_number, cvv, ssn
Only safe headers (user-agent, content-type, accept, referer, origin,
host) are ever captured from incoming requests.
User-configured redaction
Reelay.init({
endpoint: '...',
token: '...',
redactedFields: ['creditCard', 'ssn', 'internalNote'],
scrubPatterns: [/sk_live_[A-Za-z0-9]+/g],
});redactedFields: object keys whose values are replaced with[redacted]. Applied at every nesting level.scrubPatterns: regex patterns applied to every string value in the payload. Matching substrings are replaced with[redacted].
API reference
Exported functions
import * as Reelay from '@usereelay/node';| Function | Signature | Description |
|---|---|---|
| init | (options: NodeOptions) => NodeClient | Initializes the singleton client and installs process-level crash capture. Idempotent — subsequent calls return the existing client. |
| getClient | () => NodeClient \| undefined | Returns the active client, or undefined if init() hasn't been called. |
| captureException | (err: unknown) => string | Reports an error, enriched with the active request context. Returns the event ID. |
| captureMessage | (message: string) => string | Reports a string as an error event. |
| requestHandler | (client: NodeClient) => Middleware | Express/Connect middleware that seeds per-request trace context. |
| errorHandler | (client: NodeClient) => ErrorMiddleware | Express/Connect error middleware that captures and re-throws errors. |
| middleware | () => { request, error } | Convenience shortcut — returns both middleware bound to the active client. Throws if init() hasn't been called. |
| appendLog | (level: string, msg: string) => void | Appends a log line to the active request's timeline. No-op outside a request context. |
| appendSpan | (span: TimelineSpan) => void | Appends a span to the active request's timeline. |
| runWithContext | (ctx: RequestContext, fn) => T | Run a function inside a custom request context (for cron jobs, queue consumers). |
| currentContext | () => RequestContext \| undefined | Returns the active request context, or undefined outside one. |
Exported types
import type { CoreOptions, ErrorEventPayload, Breadcrumb, RequestContext } from '@usereelay/node';| Type | Description |
|---|---|
| NodeOptions | Alias for CoreOptions. |
| CoreOptions | Options shared by all Reelay SDKs. |
| ErrorEventPayload | The event object delivered to the ingest API. |
| Breadcrumb | Shape of a breadcrumb object. |
| RequestContext | The per-request AsyncLocalStorage context (trace, HTTP, logs, spans, breadcrumbs). |
NodeClient methods
const client = Reelay.init({ ... });| Method | Description |
|---|---|
| client.addBreadcrumb(crumb) | Manually add a breadcrumb to the current request's buffer (or the global buffer if outside a request). |
| client.captureWithContext(err, mechanism, extra?) | Capture with an explicit mechanism and optional extra context (trace, session, HTTP). |
| client.captureMessage(message, ctx?) | Capture a string as an error, optionally with context. |
| client.flush() | Drain the pending event queue. Returns a promise. |
| client.uninstall() | Remove all instrumentation and tear down the SDK. |
| client.log(level, msg) | Appends a log to the active request's timeline (safe wrapper around appendLog). |
Engineering guarantees
Zero host disruption
- Every public API runs inside a defensive
try/catchboundary. Internal failures go to thedebughook, never your app. - The SDK never swallows application errors. The error middleware captures and re-throws. The uncaughtException handler captures, prints, and exits. Nothing replaces your error handling.
fetchwrapping is not applicable (Node SDK usesglobalThis.fetchdirectly on the transport, not monkey-patched).
Transport reliability
- Bounded, non-blocking queue.
send()just serializes and pushes to an array. Network I/O is async, never awaited on the critical path. - Exponential backoff with full jitter. Retries start at ~1 s and cap at 30 s. Maximum 5 attempts per event.
- 4xx dropped, 5xx retried. Malformed payloads or authentication failures
(4xx) will never succeed on retry, so they are dropped immediately. Server
errors (5xx) and rate limits (429, with
Retry-Aftersupport) are retried. - 10-second request timeout. A single hung connection can never stall the drain loop.
Crash semantics preserved
When the SDK is the only uncaughtException listener (the common case), it
mimics Node's default fatal behavior: print the error, attempt a bounded
flush (≤ 2 s), and exit with code 1. When your app has its own listener, the
SDK only captures and defers the exit decision.
Bounded memory
| Resource | Cap | |---|---| | Breadcrumbs (per request) | 50 entries (ring buffer) | | Transport queue | 30 entries (drop oldest) | | Timeline logs + spans (per request) | 100 entries total | | Request header capture | 6 safe headers only |
Development
npm install
npm run build # tsup — ESM + CJS + type declarations
npm test # vitest
npm run typecheck # tsc --noEmit