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

keelnest

v0.1.2

Published

Error capture for Keelnest. Uncaught errors and unhandled rejections from a browser, a Node server and an edge or Workers runtime. No dependencies.

Readme

keelnest

Error capture for Keelnest. It reports uncaught errors and unhandled rejections from a browser, from a Node server, and from an edge or Workers runtime, so that when a client's application breaks you are told what broke instead of finding out from the client.

No dependencies. About 3 KB over the wire in a browser. Nothing it does can throw into your application, block it, or change how it already handles its own errors.

pnpm add keelnest     # npm i / yarn add / bun add

You need a key from Settings → Integrations in Keelnest, and the origin of your Keelnest (https://app.example.com). There are two kinds of key and the difference matters:

| Key | Where it comes from | What it can write to | | --- | --- | --- | | Application key | Integrations → Server → Get this app's key | that one application, and nothing else | | Workspace key | shown at the top of the Integrations card | whichever application's production URL matches the page the report came from |

A server has no page, so a server must use an application key. A browser may use either.


Next.js

Two files, both optional on their own — wire the one you need, or both.

// instrumentation.ts  — the server: rendering, Route Handlers, Server Actions, the proxy
export { register, onRequestError } from "keelnest/next";
// instrumentation-client.ts  — the browser, before React hydrates
import { init } from "keelnest";

init({
  key: process.env.NEXT_PUBLIC_KEELNEST_KEY,
  host: process.env.NEXT_PUBLIC_KEELNEST_HOST,
});
# .env
KEELNEST_KEY=pk_live_…              # the application key; server side, never in the bundle
KEELNEST_HOST=https://app.example.com
NEXT_PUBLIC_KEELNEST_KEY=pk_live_…  # the key the browser sends; public by design
NEXT_PUBLIC_KEELNEST_HOST=https://app.example.com

register runs once per server instance, in each runtime Next boots. onRequestError is the part that matters most: Next catches an error thrown while rendering a Server Component or inside a Route Handler before it ever reaches uncaughtException, so a process-level handler alone would never see it. To pass options that cannot come from the environment, call configure first:

import { configure, register, onRequestError } from "keelnest/next";
configure({ maxPerMinute: 10, beforeSend: (e) => (e.message.includes("AbortError") ? null : e) });
export { register, onRequestError };

Any browser app — Vite, Astro, CRA, plain JavaScript

import { init } from "keelnest";

init({ key: import.meta.env.VITE_KEELNEST_KEY, host: "https://app.example.com" });

Call it once, as early in your entry file as you can: errors thrown before init() are not captured, because nothing was listening yet.

A script tag, for an app you cannot add a dependency to

Some applications are not yours to add a dependency to: a site a client edits by hand, a builder that owns package.json, a page inside a CMS. For those, Keelnest serves a script of its own at <host>/e.js and the Integrations card gives you the tag with your key already in it.

The package ships the same reporter as a single self-configuring file, dist/keelnest.js, for when you would rather serve it yourself or load it from a CDN:

<script async src="https://unpkg.com/keelnest/dist/keelnest.js"
        data-key="pk_live_…" data-host="https://app.example.com"></script>

Attributes: data-key (required), data-host (defaults to wherever the script was served from, which is why it is spelled out above), data-sample (0 to 1, default 1), data-requests ("true" to also report failed requests). Afterwards window.Keelnest.captureError(e) reports something the page caught itself.

Node — Express, Fastify, a worker, a cron job

import { init } from "keelnest";

init({ key: process.env.KEELNEST_KEY, host: process.env.KEELNEST_HOST });

Cloudflare Workers and other edge runtimes

Edge runtimes do not reliably deliver a global error event, so wrap the handler:

import { init, wrap } from "keelnest";

init({ key: env.KEELNEST_KEY, host: "https://app.example.com" });

export default {
  fetch: wrap(async (request: Request, env: Env) => {
    // …
  }),
};

wrap reports what the function throws and then throws it on. Deciding what to do about an error stays with your application, exactly as it was before you added this dependency.


What it sends

One POST to <host>/api/ingest/errors, batched, at most 20 events per request:

{
  "key": "pk_live_…",
  "page": "https://client.example.com/checkout",
  "events": [
    {
      "kind": "error",
      "message": "Cannot read properties of undefined (reading 'total')",
      "source": "https://client.example.com/_next/static/chunks/page-4f2.js:1:9821",
      "stack": "TypeError: Cannot read properties of undefined…",
      "runtime": "browser",
      "at": "2026-09-16T09:41:02.118Z"
    }
  ]
}

That is the whole payload. There is no field in it that Keelnest does not use.

It never sends form values, request bodies, request headers, cookies, localStorage, environment variables, or user identifiers. page is the URL with the query string removed, because a search term is not ours to store. Messages are clipped to 300 characters and stacks to 4,000 before they leave, and the ingest route blanks anything token-shaped and any email address again on arrival.

Nothing it sends is trusted. The fingerprint that decides which errors are the same error, and the npm package a stack frame sits in, are both computed on the server from the message and the stack. There is no field this package could fill in to merge its errors into another workspace's group, or to blame a dependency it was not in. That matters because the key is public by design — it ships inside a browser bundle, where anyone can read it. A key is a name, not a secret: an application key can only ever write to the one application it names, and a workspace key can only write to an application whose production URL matches the page that sent the report. If a key is being abused, rotate it in Integrations; sites carrying the old one stop reporting from their next load.

What it will not do to your application

An error reporter that throws, blocks, or floods is worse than no error reporter. So:

  • It cannot throw into you. Every entry point is wrapped. captureError in a catch block returns immediately and never adds a second failure to the first. init() on a runtime it does not recognise, or with no key, produces a reporter that does nothing and says nothing.
  • It does not block. Sends are fire-and-forget with a 3-second deadline. Timers are unref'd, so a Node process still exits when it is finished. A page unloading sends with sendBeacon.
  • It does not flood. At most 30 events per minute per client (the ingest door keeps 60 per application), batches of 20, and one fault repeating inside 10 seconds is one report rather than sixty. A crash loop costs you one event.
  • It does not retry forever. A batch that fails is dropped, not queued. Three failures in a row and it pauses, doubling up to half an hour. A 4xx answer — a wrong key, a malformed body — stops it for the life of the process, because sending the same thing again would only be wrong again. Twelve failed sends in total and it gives up. If Keelnest is down, your application does not notice.
  • It does not change how your errors already behave. This is the part most reporters get wrong:
    • In a browser it adds error and unhandledrejection listeners, never window.onerror, which has one slot and would evict whatever was in it. It never calls preventDefault(), so the console still logs and your error boundaries still run.
    • In Node it uses uncaughtExceptionMonitor, not uncaughtException. Attaching a listener to uncaughtException stops Node exiting, which turns a crash into a process that is still running and no longer working. The monitor sees the same errors and changes nothing.
    • It attaches no signal handlers. A SIGTERM listener stops the default termination, and a container that will not stop is a worse bug than the one being reported.
    • unhandledRejection is the one hook with no observe-only variant, and merely listening to it suppresses Node's default, which is to raise the rejection as an uncaught exception. So when this package is the only listener, it puts that default back on the next tick. Pass restoreDefault: false if you would rather it did not, and know that you are choosing to let rejections stop crashing your process.
    • It does not wrap fetch unless you ask for it (requests: true). Replacing a global is the one thing a dependency can do that you cannot undo, and a wrapper with a bug in it breaks every request in your application rather than losing one report.

One thing it will not promise: the very last error before a process dies may not arrive. When Node is on its way out, this package asks for the send and does not wait for it, because holding a dying process open to file a report is exactly the blocking it refuses to do.

Source maps

Keelnest does not accept source map uploads and does not symbolicate stacks. Frames arrive the way the runtime produced them. If your browser bundle is minified, they will read like page-4f2.js:1:9821, and no amount of serving .map files next to the bundle changes that — a browser applies source maps in DevTools, but error.stack, which is what this package reads, stays minified.

What actually helps, in order of how much:

  • On a server, run Node with --enable-source-maps. Node rewrites error.stack itself, so the frames that arrive at Keelnest are your real file names and line numbers. This works today and costs nothing. Next.js enables it for you in development, and you can add it in production with NODE_OPTIONS=--enable-source-maps.
  • Keep function names in the browser bundle. Minifiers can mangle everything except function names (esbuild keepNames, terser keep_fnames, Vite esbuild.keepNames). The file and line stay meaningless, but at checkoutTotal in the stack is most of the diagnosis.
  • Nothing at all. Keelnest groups errors on the shape of the message rather than the exact words, and reads the npm package a frame sits in from the node_modules/<name> in its path, which still works on a minified bundle that kept its module paths. So a dependency failing still reads as a dependency failing.

If symbolication matters to you, say so — it is not built, and this file will say so until it is.


API

init(options?): Reporter

Starts reporting and attaches the handlers for the current runtime. Safe to call twice: the second call returns the reporter the first one made rather than attaching a second set of handlers, even across two copies of this package in one dependency tree.

| Option | Default | | | --- | --- | --- | | key | KEELNEST_KEY, then NEXT_PUBLIC_KEELNEST_KEY | the workspace or application key | | host | KEELNEST_HOST, then NEXT_PUBLIC_KEELNEST_HOST | origin of your Keelnest | | endpoint | <host>/api/ingest/errors | the full ingest URL, when it is somewhere else | | attach | "auto" | "browser", "node", "edge", or "none" to wire it yourself | | runtime | detected | "browser" or "server"; how the error is filed | | sample | 1 | fraction of clients that report at all, decided once at init | | maxPerMinute | 30 | events one client may send per minute | | batchSize | 20 | events per request; 20 is the most the door reads from one body | | flushAfterMs | 3000 | how long a partial batch waits for company | | timeoutMs | 3000 | how long one send may take before it is abandoned | | beforeSend | — | last look at an event; return null to drop it | | requests | false | browser only: also report requests that fail or answer 5xx | | restoreDefault | true | Node only: keep Node's crash-on-unhandled-rejection behaviour | | onDebug | — | where the reporter says what it is doing; silent without it |

captureError(thrown, kind?, extra?): void   // report something you caught yourself
wrap(fn, kind?): typeof fn                  // report what a function throws, then throw it on
flush(): Promise<void>                      // send what is queued; never rejects
close(): Promise<void>                      // stop, and remove every handler this package attached
reporter(): Reporter | null                 // the reporter init() made, or null

kind is one of "error", "rejection", "request", "resource", "unhandled", "uncaught".

Sizes

| | raw | gzipped | | --- | --- | --- | | dist/keelnest.js — the script tag, minified here | 8.3 KB | 3.1 KB | | import { init } from "keelnest" — source, before your bundler minifies it | 18.1 KB | 5.9 KB |

The published ESM and CommonJS builds are deliberately not minified: your bundler does that better than we can, and a readable dependency is one you can step through when it misbehaves. The package is marked side-effect free apart from the script build, so a bundler drops the runtimes you do not use.

Requirements

Node 18 or newer, any modern browser, Cloudflare Workers, Vercel's edge runtime, Deno, Bun. It uses fetch, AbortController and navigator.sendBeacon where they exist and does without where they do not.

Licence

MIT.