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

midline-agent

v0.7.0

Published

Midline — request, error and security monitoring for Node, and error, network, Web Vitals and pageview/visitor monitoring for browsers

Readme

midline-agent

SDK for Midline — request and error monitoring with security and threat detection. It also ships midline-agent/browser for web apps: see Browser apps.

Midline itself is not tied to Node. Every event lands on the same stream through a plain JSON endpoint, so a Django, Rails, Laravel, Spring, Go or .NET service reports exactly what an Express service does. This package is the convenience wrapper for Node — if you are on another stack, skip to Any other backend.


How it fits together

There are two separate things, and the agent never confuses them:

| | What it is | Configured with | | --- | --- | --- | | Midline server (control plane) | Where events, API keys, logs, analytics and security data live. https://api.usemidline.com | endpoint / MIDLINE_ENDPOINT, apiKey / MIDLINE_API_KEY, ca / MIDLINE_CUSTOM_CA | | Destination API (data plane) | The API being monitored — local, staging or production | target / TARGET_API_URL, targetCa / TARGET_API_CA (proxy mode only) |

Your traffic never goes through the Midline server. The agent runs next to your API, observes each request, and ships a redacted event to Midline in the background:

                 ┌──────────────── your infrastructure ────────────────┐
client ────────► │  midline-agent  ──────────────►  destination API    │
                 │  (middleware in your app, or proxy in front of it)  │
                 └───────┬─────────────────────────────────────────────┘
                         │ events, batched, redacted, async (HTTPS, verified)
                         ▼
               https://api.usemidline.com

That is deliberate. If Midline is down, slow or misconfigured, your API keeps serving and only telemetry is buffered. A hosted gateway that sits in the request path can't do that, and it can't reach http://localhost:4000 on your laptop at all.

Two ways to run the agent:

  • Middleware — inside an Express/NestJS/Node app. No extra hop.
  • Proxy — a small reverse proxy in front of any HTTP API, in any language. TARGET_API_URL decides where requests go.

Install

npm install midline-agent

Get a project API key from the Midline dashboard (Project → API Keys).


Middleware mode

Express

import express from "express";
import { MidlineAgent, midlineMiddleware, midlineErrorHandler } from "midline-agent";

MidlineAgent.init({
  apiKey: process.env.MIDLINE_API_KEY,
  serviceName: "checkout-api",
  environment: process.env.NODE_ENV,
});

const app = express();
app.use(midlineMiddleware());   // before your routes
app.use(express.json());

app.post("/v1/charges", (req, res) => res.json({ ok: true }));

app.use(midlineErrorHandler()); // after your routes; passes the error on untouched
app.listen(3000);

Behind a load balancer or CDN

By default the IP recorded for each request is Express's req.ip. Behind a load balancer that is the load balancer's address, not your user's. Tell the middleware how many proxies sit in front of your server and it reads the real address from X-Forwarded-For:

app.use(midlineMiddleware({ trustProxy: 1 })); // one load balancer or CDN in front

It counts that many hops in from the end of the chain, so an address a client puts in the header itself is ignored. Leave it unset if clients reach your server directly, or if you already set app.set("trust proxy", …) yourself. trustProxy: true believes the whole header, which is only safe when nothing can reach the server except through your proxies.

NestJS

import { NestFactory } from "@nestjs/core";
import { MidlineAgent, midlineMiddleware } from "midline-agent";
import { AppModule } from "./app.module";

async function bootstrap() {
  MidlineAgent.init({
    apiKey: process.env.MIDLINE_API_KEY,
    serviceName: "nest-api",
    environment: process.env.NODE_ENV,
  });

  const app = await NestFactory.create(AppModule);
  app.use(midlineMiddleware());
  app.enableShutdownHooks();
  await app.listen(3000);
}
bootstrap();

// On shutdown (e.g. in onApplicationShutdown): await MidlineAgent.shutdown();

Nest handles exceptions in its own filters, so they rarely reach Express error middleware. The request middleware still records every 4xx/5xx response, and Midline groups the 5xx responses into Issues by method, route and status.

Plain Node http

import http from "http";
import { MidlineAgent, midlineMiddleware } from "midline-agent";

MidlineAgent.init({ apiKey: process.env.MIDLINE_API_KEY, serviceName: "raw-node" });
const midline = midlineMiddleware();

http.createServer((req, res) => {
  midline(req, res, () => {
    res.writeHead(200, { "content-type": "application/json" });
    res.end('{"ok":true}');
  });
}).listen(3000);

Proxy mode

Put the agent in front of any API — Node or not — without touching its code:

MIDLINE_API_KEY=... TARGET_API_URL=http://localhost:4000 npx midline-agent proxy --port 8080
# clients now call http://localhost:8080 instead of :4000

The same binary, pointed elsewhere:

TARGET_API_URL=https://staging.example.com npx midline-agent proxy
TARGET_API_URL=https://api.example.com     npx midline-agent proxy --host 0.0.0.0

Or from code:

import { startMidlineProxy } from "midline-agent";

await startMidlineProxy({
  target: process.env.TARGET_API_URL,          // required; never defaulted
  port: 8080,
  midline: { apiKey: process.env.MIDLINE_API_KEY, serviceName: "orders-gateway" },
});

createMidlineProxy(options) returns a plain (req, res) handler if you want your own server.

What the proxy does per request:

  • Forwards method, path, query, headers and body to the target, streaming both ways
  • Strips hop-by-hop headers; sets Host to the target (unless preserveHost), adds X-Forwarded-*, X-Request-Id and X-Correlation-Id
  • Only accepts origin-form request lines (GET /path). It can't be used as an open forward proxy.
  • Answers 502 if the destination is unreachable, 504 if it's too slow (timeoutMs, default 30s), 413 if the body exceeds maxRequestBodyBytes (default 10 MiB). Error bodies carry a requestId and no internal detail.
  • Retries (retries, default 0) only GET/HEAD/OPTIONS without a body, and only when the request never reached the destination. A write is never replayed.
  • Records the exchange — including destination failures as infrastructure errors — to Midline

WebSocket upgrades are not proxied.


TLS

Certificate verification is always on, for both the Midline server and the destination. There is no option to turn it off, and the agent never sets rejectUnauthorized: false.

The Midline server

https://api.usemidline.com has a publicly trusted certificate, so nothing needs configuring. If verification ever fails, you'll see one line like:

midline: https://api.usemidline.com presented a self-signed certificate (DEPTH_ZERO_SELF_SIGNED_CERT),
so the connection was refused. Certificate verification stays on. ...
Delivery resumes on its own once a trusted certificate is served. 12 event(s) buffered; your application is unaffected.

That is a server-side problem, not something to work around in your app. Events stay buffered and delivery resumes on its own once the server is fixed — no restart needed.

For a self-hosted Midline server behind a private CA, trust that CA for the Midline connection only:

MidlineAgent.init({ apiKey, endpoint: "https://midline.internal", ca: fs.readFileSync("/etc/ssl/internal-ca.pem") });
MIDLINE_CUSTOM_CA=/etc/ssl/internal-ca.pem   # PEM text also works

The CA is added to Node's default trust store, not substituted for it. Hostname checks still apply.

endpoint must be https://. Plain http:// is only accepted for localhost/127.0.0.1 (running a Midline server on your own machine), so the API key never crosses a network in cleartext.

The destination (proxy mode)

A local destination is fine over plain HTTP: TARGET_API_URL=http://localhost:4000. That doesn't relax anything about the Midline connection.

For an HTTPS destination signed by a private/internal CA:

TARGET_API_URL=https://orders.internal TARGET_API_CA=/etc/ssl/internal-ca.pem npx midline-agent proxy

TARGET_API_CA and MIDLINE_CUSTOM_CA are independent. Trusting a CA for one never trusts it for the other.


Configuration

MidlineAgent.init({
  apiKey: string,                 // MIDLINE_API_KEY
  serviceName?: string,           // MIDLINE_SERVICE_NAME
  endpoint?: string,              // MIDLINE_ENDPOINT — default https://api.usemidline.com (base or full ingest URL)
  ca?: string | Buffer | Array,   // MIDLINE_CUSTOM_CA — extra CA for the Midline server only
  environment?: string,           // MIDLINE_ENVIRONMENT
  release?: string,               // MIDLINE_RELEASE
  host?: string,
  region?: string,
  enabled?: boolean,              // MIDLINE_ENABLED=false keeps the agent inert

  // What to capture beyond method, path, status and timing. All off by default.
  capture?: {
    headers?: boolean,
    query?: boolean,
    requestBody?: boolean,        // whatever your body parser produced (req.body)
    responseBody?: boolean,
    maxBodyBytes?: number,        // default 4096
  },
  redactFields?: string[],        // added to the built-in list (maskFields still works)
  redactHeaders?: string[],
  captureConsole?: boolean,       // MIDLINE_CAPTURE_CONSOLE — also send what the process prints (off by default)
  captureIp?: boolean,            // MIDLINE_CAPTURE_IP — record the caller's IP (on by default; false leaves it out)

  // Delivery
  flushIntervalMs?: number,       // default 1500
  timeoutMs?: number,             // whole request deadline, default 10000
  connectTimeoutMs?: number,      // TCP + TLS handshake, default 5000
  maxBatchSize?: number,          // default 100
  maxQueueSize?: number,          // default 1000; oldest dropped past this
  maxEventBytes?: number,         // default 65536; bodies dropped first
  maxRetryDelayMs?: number,       // backoff cap, default 300000

  onError?: (message: string) => void, // route diagnostics to your logger
  debug?: boolean,                // MIDLINE_DEBUG — log every diagnostic, not one per fault
});

Environment

MIDLINE_API_KEY=...
MIDLINE_ENDPOINT=https://api.usemidline.com
# MIDLINE_CAPTURE_CONSOLE=true
# MIDLINE_CAPTURE_IP=false

# proxy mode
TARGET_API_URL=http://localhost:4000
# TARGET_API_CA=/etc/ssl/internal-ca.pem
# TARGET_API_TIMEOUT_MS=30000
# TARGET_API_RETRIES=0
# MIDLINE_PROXY_PORT=8080
# MIDLINE_PROXY_HOST=127.0.0.1

Console output

Set captureConsole: true (or MIDLINE_CAPTURE_CONSOLE=true) and whatever the process prints is sent as console events, one per line, and shows up on the dashboard's Terminal page. That covers console.log, Nest's logger, pino, winston: anything written to stdout or stderr. The Logs page stays request traffic.

MidlineAgent.init({ apiKey: process.env.MIDLINE_API_KEY, captureConsole: true });
const app = await NestFactory.create(AppModule); // startup lines are captured from here on
  • Initialise the agent before creating the app. Anything printed before init() isn't captured.
  • Your terminal output doesn't change. The agent reads each write after it has happened.
  • Colour codes are stripped, each line is capped at 4096 characters, and lines are redacted like any other text.
  • Severity comes from the line: ERROR or FATAL is high, WARN is medium, other stderr output is medium, and everything else is low.
  • Up to 100 lines a second are sent, with room for a 1,000-line burst such as Nest mapping its routes at startup. Lines past that still print but aren't sent.
  • Printed lines don't count towards request totals, error rates, latency or Issues.
  • It needs a Midline server that knows the console event type. An older server refuses the first line; the agent then turns console capture off, says so once, and keeps sending requests and errors.
  • The agent's own diagnostics are never captured.

IP addresses

Request events carry the address of whoever made the request. It shows in the dashboard as the IP address column on Logs and on the Visitors page's Recent visitors table. Where it comes from depends on how you connect:

| You use | Address recorded | | --- | --- | | Express or NestJS middleware | Express's req.ip. With trustProxy, the client read from X-Forwarded-For. | | Plain Node http middleware | The socket's peer. With trustProxy, the client read from X-Forwarded-For. | | Proxy mode | The socket's peer. With trustForwardedHeaders, the first X-Forwarded-For entry. | | Browser SDK | The address the Midline server saw the browser connect from. Nothing to configure, and any IP the page sends is ignored. |

  • Behind a load balancer or CDN, set trustProxy (see Express) or every request shows the proxy's address instead of your user's.
  • On your own machine every caller is ::1 or 127.0.0.1: the address a computer uses for itself. Real addresses appear once real traffic reaches the service.
  • A self-hosted Midline server that sits behind a proxy needs TRUST_PROXY=<number of proxies> in its own environment, so browser events record the visitor rather than the proxy. Left unset, X-Forwarded-For is ignored, which stops a client from choosing the address that is recorded.

Not recording IP addresses

An IP address can count as personal data. To keep it out entirely:

MidlineAgent.init({ apiKey: process.env.MIDLINE_API_KEY, captureIp: false }); // or MIDLINE_CAPTURE_IP=false
Midline.init({ apiKey: "pk_…", captureIp: false }); // browser SDK

The Node agent drops the address in your process, so it is never sent. The browser SDK can only ask: the address is read by the Midline server, which then doesn't store it. A server that predates 0.7.0 ignores the request, so update a self-hosted server before relying on it. Events without an address show "—" in the dashboard.


Sensitive data

Redaction happens in your process, before an event is queued. What's masked never reaches a socket, a log line or the Midline server. The server redacts again on ingest as a second line of defence.

  • Headers, always masked when captured: Authorization, Proxy-Authorization, Cookie, Set-Cookie, X-API-Key, API-Key, X-Auth-Token, X-Access-Token, X-Refresh-Token, X-CSRF-Token, X-XSRF-Token, X-Amz-Security-Token, plus anything matching the field rules below.
  • Fields, at any depth, in bodies, queries and payloads. Matched case- and punctuation-insensitively, so api_key, apiKey and X-API-Key are the same key. Anything containing password, passwd, passphrase, secret, token, apikey, accesskey, privatekey, authorization, cookie, session, credential, csrf, xsrf, signature, creditcard, cardnumber, cvv, cvc, ssn, socialsecurity; and the exact keys auth, pwd, pin, otp, sid, jwt, bearer.
  • Values inside any text — error messages, stack traces, routes, console lines: bearer/basic credentials, JWTs, Midline keys (ak_…), Stripe and AWS key formats, PEM private keys, user:pass@ in URLs, and key=value / "key": value pairs whose key is sensitive.
  • Query strings are never part of route.
  • Bodies are only captured if you turn them on. They're capped at maxBodyBytes, and compressed or binary bodies are skipped.

The lists err on the side of masking (tokenCount is masked too). Extend them with redactFields and redactHeaders.


Delivery and failure behaviour

The agent's contract is that the Midline server can never break your application.

  • Recording happens after the response is handed to the socket; nothing waits on the network
  • Events are batched (POST /api/api-monitor/events/batch) with the key in the X-API-Key header
  • Unreachable, DNS failure, timeout, TLS failure, 5xx, 408, 429: events stay buffered. Retries use exponential backoff with jitter, capped by maxRetryDelayMs, and honour Retry-After.
  • 401/403: the key is wrong. The agent logs once and turns itself off — retrying can't fix it.
  • 413: the batch is split and retried; a single event that's still too large is dropped
  • Other 4xx: the batch is re-sent one event at a time, so a malformed event only costs itself
  • Redirects are never followed, because that would hand the API key to wherever they point
  • One log line per distinct fault, and one when delivery recovers
  • The buffer is bounded (maxQueueSize, 16 MB total); past that, the oldest events go first
  • The flush timer and background sockets are unref'd, so telemetry never keeps your process alive

Events still in memory are lost if the process crashes. That's fine for operational telemetry, but not for billing or audit. On graceful shutdown:

process.on("SIGTERM", async () => {
  await MidlineAgent.shutdown(5000); // flush with a deadline, then close
  process.exit(0);
});

Request and correlation IDs

Every request gets a requestId: an incoming X-Request-Id if it's sane, otherwise a UUID. It also gets a correlationId: X-Correlation-Id if present, otherwise the request ID. A W3C traceparent header becomes traceId/spanId. Read them in your own code to stamp logs:

import { getRequestContext } from "midline-agent";
app.get("/x", (req, res) => { logger.info({ requestId: getRequestContext(req)?.requestId }); });

Browser apps

midline-agent/browser monitors a web app from the page: uncaught errors and unhandled rejections, failed fetch and XMLHttpRequest calls, Web Vitals (LCP, INP, CLS, FCP, TTFB), pageviews for visitor counting and funnel views, and, if you ask, console output. It has no dependencies, no Node code, and works under React, Vue, Angular, Svelte, Next.js or no framework at all, because it instruments the page rather than a framework.

import * as Midline from "midline-agent/browser";

Midline.init({
  apiKey: import.meta.env.VITE_MIDLINE_BROWSER_KEY, // pk_… browser key
  service: "checkout-web",
  environment: import.meta.env.MODE,
  release: import.meta.env.VITE_RELEASE,
});

Use a browser key, never a server key. Anything in a bundle can be read by whoever loads the page, so browser keys (pk_…) are public by design and the Midline server accepts them only from the origins listed on the key (create one under Project → API Keys → Browser). Server keys (ak_…) are refused whenever a browser sends them, and the SDK won't start with one.

Errors a framework catches itself never reach the window. Forward them:

// React error boundary
componentDidCatch(error: Error, info: ErrorInfo) {
  Midline.captureException(error, { extra: { componentStack: info.componentStack } });
}

// Vue
app.config.errorHandler = (error, _instance, info) => Midline.captureException(error, { extra: { info } });

Linking a page to your backend. Same-origin requests get a W3C traceparent header, which the Node agent on the backend turns into traceId/spanId (see above), so the page's failed call and the server request behind it share a trace id. For an API on another origin, list it in tracePropagationTargets and allow the traceparent header in that API's CORS configuration, or the browser will block the call's preflight.

| Option | Default | | | --- | --- | --- | | apiKey | — | Browser key (pk_…). | | endpoint | https://api.usemidline.com | http:// only for localhost. | | service, environment, release | — | Stamped on every event. | | captureErrors | true | Uncaught errors and unhandled rejections. | | captureRequests | "failed" | "failed" (4xx, 5xx, network errors), "all", or false. Every call is a breadcrumb either way. | | captureWebVitals | true | Reported once, when the page is first hidden. | | capturePageviews | true | A pageview event on load and on every SPA route change (pushState/replaceState/popstate), tagged with a per-tab sessionId — what the dashboard's visitor and funnel views are built from. | | captureIp | true | The visitor's IP is read by the Midline server from the connection, not by the browser. false asks the server not to keep it. Needs a server that supports it (an older one ignores the request). | | captureConsole | false | true for error and warn, or a list of levels. Lines appear on the Terminal page. Opt-in because wrapped console calls show the SDK as their source in devtools. | | tracePropagationTargets | same origin | Strings match as URL prefixes (or path prefixes starting with /); RegExps match the full URL. | | ignoreErrors, ignoreUrls | [] | Strings match as substrings. | | sampleRate | 1 | Fraction of events sent. | | maxEventsPerMinute | 120 | So an error in a render loop can't flood the project. | | beforeSend | — | Return null to drop an event; edit payload and metadata freely. | | redactFields | — | Extra field names to redact. | | enabled, debug | true, false | |

Also: captureMessage(message, severity), setUser({ id }), setTag(key, value), addBreadcrumb(message), flush() and close(), which restores everything the SDK wrapped.

What it will not do: it never sends cookies, query strings of the page URL, or request and response bodies; it redacts the same credential patterns as the Node agent before an event is queued; it never throws into your code; and if Midline is down it keeps a bounded queue and backs off. Pending events leave with keepalive when the tab is hidden. During server-side rendering, init does nothing.

No bundler? Load the ES module build directly:

<script type="module">
  import * as Midline from "https://cdn.jsdelivr.net/npm/[email protected]/dist/esm/browser/index.js";
  Midline.init({ apiKey: "pk_…", service: "web" });
</script>

The Midline server must be recent enough to accept browser keys (it answers the CORS preflight on the ingest routes). Against an older server the SDK logs nothing unless debug is on and keeps retrying with backoff.


Any other backend

The agent does two things: it turns a request or an error into an event, and it POSTs that event to the Midline ingest API. Your own service can do both directly, in whatever language it's written in — or run midline-agent proxy in front of it.

Endpoints

| Endpoint | Use | | --- | --- | | POST https://api.usemidline.com/api/api-monitor/events | One event | | POST https://api.usemidline.com/api/api-monitor/events/batch | Many events — { "events": [ … ] }, up to 500 |

Authenticate with the X-API-Key header. An apiKey field in the body is still accepted.

Event shape

| Field | Required | Notes | | --- | --- | --- | | eventType | yes | request · error · security · performance · pageview · custom | | route | yes | Path only, e.g. /v1/charges. For pageview, the page path. | | method | no | HTTP method | | statusCode | no | 100–599 | | responseTime | no | Milliseconds | | severity | no | low · medium · high · critical | | category | no | application · infrastructure · security · performance · business | | service / environment / release | no | Where it came from | | timestamp | no | ISO 8601; clamped to server time if implausibly far off | | requestId / correlationId / traceId | no | For joining events across services | | sessionId | no | A stable id for one visitor's session (e.g. a browser tab). Send the same value on every event from that session and Midline can build visitor counts and funnels from pageview events. Keep it opaque — never an email or a real user id. | | payload | no | Free-form context — error message, stack, captured request/response | | metadata | no | Free-form, e.g. SDK name and version |

Mask sensitive fields in your own process before you build the payload. Nothing you don't send can be stored.

curl -X POST https://api.usemidline.com/api/api-monitor/events \
  -H "Content-Type: application/json" \
  -H "X-API-Key: $MIDLINE_API_KEY" \
  -d '{
    "eventType": "error",
    "service": "checkout-api",
    "route": "/v1/charges",
    "method": "POST",
    "statusCode": 500,
    "responseTime": 1240,
    "severity": "critical",
    "category": "application",
    "payload": { "error": "Database connection failed" }
  }'
import os, threading, requests

INGEST = "https://api.usemidline.com/api/api-monitor/events"

def report(event: dict) -> None:
    # off the request path - never make a user wait on telemetry
    threading.Thread(
        target=lambda: requests.post(INGEST, json=event, headers={"X-API-Key": os.environ["MIDLINE_API_KEY"]}, timeout=3),
        daemon=True,
    ).start()
func Report(body []byte) {
    go func() {
        req, _ := http.NewRequest("POST", "https://api.usemidline.com/api/api-monitor/events", bytes.NewReader(body))
        req.Header.Set("Content-Type", "application/json")
        req.Header.Set("X-API-Key", os.Getenv("MIDLINE_API_KEY"))
        client := &http.Client{Timeout: 3 * time.Second}
        if resp, err := client.Do(req); err == nil {
            resp.Body.Close()
        }
    }()
}

Same rule everywhere: send events from a goroutine, a background thread or a queue worker. Keep TLS verification on. Telemetry should never be able to slow down or fail a response.


Upgrading from 0.1.x

  • insecureTLS / MIDLINE_INSECURE_TLS are gone (they were never published). Fix the server's certificate, or pass the private CA with ca / MIDLINE_CUSTOM_CA.
  • endpoint can now be just https://api.usemidline.com; full ingest URLs still work.
  • maskFields used to be accepted and ignored. It's now honoured, as an alias for redactFields.
  • serviceName is optional; MIDLINE_API_KEY / MIDLINE_ENDPOINT are read if not passed.
  • cross-fetch is no longer a dependency; Node 18+ is required.
  • New: MidlineAgent.shutdown(timeoutMs) flushes with a deadline and stops the agent.
  • The agent sends the key both in the X-API-Key header and in each event body. That keeps it compatible with Midline servers that predate header auth.

License

MIT