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

@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/node

Quick 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/checkout

The 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:

  1. The error is captured and queued for delivery.
  2. The error is printed to stderr.
  3. A bounded flush (≤ 2 seconds) is started.
  4. 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:

  1. Reads the incoming traceparent and reelay-session-id headers (sent automatically by @usereelay/browser).
  2. Seeds an AsyncLocalStorage context with the trace ID, session ID, HTTP context (method, URL, safe headers), and fresh log/span/breadcrumb buffers.
  3. Listens for the response finish event 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 finish event 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 valuesbearer: 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/catch boundary. Internal failures go to the debug hook, 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.
  • fetch wrapping is not applicable (Node SDK uses globalThis.fetch directly 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-After support) 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