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

watchfire

v1.3.0

Published

Self-hosted error tracking for Next.js. A library, not a service.

Readme

Watchfire

Self-hosted error tracking for Next.js. A library, not a service.

CI npm license

Watchfire captures JavaScript errors in your users' browsers, resolves the minified stack traces back to your original source using privately held source maps, gives each error a stable fingerprint for grouping, and hands the result to a callback in your code. Error data stays on your infrastructure, and there's no extra service to run.

npm install watchfire

Why

Hosted error tracking sends stack traces and user context to a third party, which for many teams means a subprocessor agreement and a vendor review. Self-hosting a full platform (Sentry's stack is Kafka, ClickHouse, Redis, and Postgres) is a lot of infrastructure for what is usually a few dozen events a day.

The hard part of error tracking is resolving minified stack traces against source maps without making the maps public. That fits in a library. Storage, alerting, and dashboards are left to the tools you already run: watchfire delivers each error as a structured event, and you decide what to store and when to alert.

There's no UI, no issue workflow, and no analytics.

How it works

browser error ──POST──▶ /api/errors (a route in your app)
                              │
                              ▼
                validate → rate-limit → scrub → parse → resolve → fingerprint
                              │
                              ▼
                        onEvent(event)   ← your code: a DB insert, a log line

One package, six subpath exports and a CLI:

| Import | Contents | | --- | --- | | watchfire/browser | Client SDK: capture, breadcrumbs, batching, repeat suppression, flush. 5 kB minified, 2.3 kB gzipped | | watchfire/ingest | The route handler, a standard (Request) => Response function | | watchfire/next | withWatchfire, wraps next.config to wire source maps and the release build id | | watchfire/react | reportBoundaryError, for error boundaries | | watchfire/stack | Stack parsing (Chrome, Safari, and Firefox formats) and fingerprinting | | watchfire/sourcemaps | Map stores, the runtime resolver, boot-time registration |

The build tooling targets Next.js; the runtime is framework-neutral.

Setup

1. The build

In next.config.ts, wrap your config:

import { withWatchfire } from "watchfire/next";

const nextConfig = { /* your config */ };

export default withWatchfire(nextConfig);

The wrapper enables source map generation and returns your release id as Next's build id, so maps are stored under the same name the browser reports. The release comes from NEXT_PUBLIC_RELEASE (pass release to override); without one, Next falls back to its own generated id, so local development needs no configuration. A generateBuildId you define yourself is respected and not overwritten.

In package.json, run watchfire maps after the build:

"scripts": {
  "build": "next build && watchfire maps"
}

next build writes a .map file beside every chunk. watchfire maps moves the maps into a private directory inside the server output, removes the public pointers, and fails the build if any map remains publicly reachable. Maps are stored under a release id, which defaults to Next's build id.

2. The server route

Create app/api/errors/route.ts:

import { createIngestHandler, filesystemStore, defaultMapsDir } from "watchfire/ingest";

export const POST = createIngestHandler({
  maps: filesystemStore(defaultMapsDir()),
  onEvent: async (event) => {
    await db.insertInto("client_errors").values({
      fingerprint: event.fingerprint,
      message: event.message,
      frames: JSON.stringify(event.frames),
    }).execute();
  },
});

onEvent receives the finished event: parsed, resolved to original source, fingerprinted.

3. The browser

Create instrumentation-client.ts at the project root. Next runs any file with that name in the browser before your app code starts, so nothing needs to import it:

import { init } from "watchfire/browser";

init({ endpoint: "/api/errors", release: process.env.NEXT_PUBLIC_RELEASE });

release must match the id the maps were stored under in step 1. withWatchfire reads the same NEXT_PUBLIC_RELEASE variable, so setting one env var at build time covers both. A report with an unknown release is still delivered, but without resolved source positions.

Reports are batched, and the queue is sent when the page is hidden or torn down. If your own code destroys the page, call flush() first, or the report explaining why is the one you lose:

import { captureError, flush } from "watchfire/browser";

captureError(new Error("StaleBundle: reloading"));
flush();
location.reload();

Source maps at runtime

Resolution reads maps from local disk, which always matches the release the server is running. This covers errors from browsers on the current bundle.

Browsers on an older bundle (tabs opened before your last deploy) produce stacks that reference the previous build. To resolve those too, configure a shared store and register each release's maps on boot:

import { layeredStore, s3Store, filesystemStore, registerMaps, defaultMapsDir } from "watchfire/sourcemaps";

const shared = s3Store({ bucket: "my-app-maps", client, commands });
void registerMaps({ release, localDir: `${defaultMapsDir()}/${release}`, store: shared });

registerMaps only uploads what's missing, so every server can call it on every boot. Without a shared store, stale-bundle errors fall back to function names instead of source positions.

Default filtering

The default ignore list contains only errors that are noise in any application: ResizeObserver loop notices, cross-origin Script error events (which carry no usable information), and errors thrown from browser-extension code, identified by extension URLs in the stack.

Some extension errors arrive with no stack at all, leaving no frame to identify anyone by. A short list of those is matched on the message instead, and the bar for being on it is that the message names an extension API the page cannot reach (chrome.runtime errors, and the Firefox pair produced when an extension hands React a privileged event target). A generic TypeError is never matched this way.

Chunk-load failures, network errors, and aborted requests are reported, since they're often signal: a chunk-load spike measures how many open tabs a deploy broke, and a network-error spike can indicate an outage. To drop or reroute them, use ignoreErrors (substrings or regexes, matched against both message and stack) or classify them in onEvent.

Privacy defaults

  • Route changes and clicks: recorded as path patterns and CSS selectors, never text content
  • Network breadcrumbs: method, origin, and status; fetchFullPath: true adds the path, and query strings are always stripped (callback URLs can contain OAuth codes)
  • Console breadcrumbs: off by default
  • Input values: never captured; there's no option to enable this

Breadcrumbs

Each report carries a short trail of what happened in the tab beforehand: fetch (method, path, status), click (a CSS selector), navigation (route changes), and console if you opt in. capture.limit bounds it, default 30.

Eviction is not first-in-first-out. An app that polls makes requests the overwhelming majority of everything recorded, and dropping the oldest entry would discard the rarer clicks and navigations first, which are the ones that say what the person was doing. So when the buffer is full the oldest entry of the largest kind is dropped. Request chatter evicts request chatter, and a click from a minute ago outlives a hundred polls.

The trade is worth knowing: the trail is the recent history of each kind in chronological order, not strictly the last N events.

Grouping

Every event carries a fingerprint; grouping by it gives an issue list. Two properties are enforced by tests:

  • Stable across deploys. The fingerprint is built from resolved source paths and lines, not generated chunk names, so a rebuild doesn't split an issue. Variable parts of the message (ids, numbers, quoted strings, URLs) are normalized out.
  • Stable across engines. Only the top application frame contributes. Engines disagree about deeper framework frames (V8 reports frames JSC elides), which would split one bug into several issues.

When a stack yields nothing at all, grouping falls back to the request that failed. Safari reports a dropped fetch as TypeError: Load failed with no stack, so every dropped request in an application would otherwise share one key; the breadcrumb trail still knows which endpoint it was. Only the most recent request counts, and only if it failed.

Watchfire's own frames are removed from the stack before it reaches onEvent. Breadcrumb capture patches window.fetch, so without this the wrapper is frame 0 of every network error, ahead of the code that made the call.

Repeat suppression runs in the browser: at most three reports per distinct error per page load, with the suppressed count attached to the next flush.

Status

v1.3.0. Covered by 195 unit tests and 33 end-to-end tests that run Chromium, WebKit, and Firefox against a Next 16 build. Parser fixtures are captured from the engines rather than written by hand; the source map decoder is tested against real bundler output. Watchfire is in production at Fieldwork.

Out of scope: a hosted UI and Sentry protocol compatibility, both of which would turn the library into a service.

License

MIT