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

zulogs

v0.3.0

Published

Zulogs SDK for Node.js and the browser — automatic error capture and structured logging, with no runtime dependencies.

Readme

zulogs

The Zulogs SDK for JavaScript. Catches unhandled errors on its own, and gives you explicit methods for everything else. No runtime dependencies.

zulogs is the Node half. The browser half is zulogs/browser from the same package — one install, one version, see In the browser.

npm install zulogs

Quick start

Call init() as early as possible, so the error handlers are installed before anything can throw.

import * as zulogs from 'zulogs';

zulogs.init({
	key: process.env.ZULOGS_KEY,
	environment: process.env.NODE_ENV,
	release: process.env.GIT_SHA,
});

// Uncaught exceptions are reported automatically. Everything else is explicit:
zulogs.info('checkout started', { cartId });

try {
	await charge(order);
} catch (error) {
	zulogs.captureException(error, { orderId: order.id });
}

key falls back to ZULOGS_KEY. Events go to hosted Zulogs, so there is nothing else to point at.

Levels

trace, debug, info, warn, error, fatal — each is a function taking a message and an optional context object:

zulogs.warn('payment gateway slow', { ms: 842 });
zulogs.log('error', 'dynamic level', { via: 'variable' });

Events below minLevel (default info) are never sent. captureException() ignores that threshold on purpose: an exception is the reason the SDK exists, and dropping one because the log threshold is high would be a trap.

Request scope

A server handles many requests at once, so attaching a user to "the SDK" would tag the wrong request. withScope() binds data to one async call tree via AsyncLocalStorage, and everything captured inside it — however deeply nested — carries that data.

import { withScope } from 'zulogs';

app.use((req, res, next) => withScope({ user: { id: req.userId }, traceId: req.id }, next));

For process-wide data there is setUser(), setTags() and configureScope(). Scopes merge, with the innermost value winning.

Automatic error capture

uncaughtException is handled by default. The SDK reports the error, flushes, and then restores what would have happened without it: if no other listener exists, the process prints the error and exits 1, exactly as Node would.

Unhandled promise rejections are covered by the same handler, because Node re-raises them as uncaught exceptions. There is a separate captureUnhandledRejections option, but it is off by default and rarely correct: registering an unhandledRejection listener is what tells Node the rejection was handled, so turning it on stops rejections from terminating your process. Only enable it when running with --unhandled-rejections=warn.

Set captureConsole: true to additionally mirror console.error and console.warn. The console keeps printing exactly as before.

Keeping the local log

By default the SDK is write-only: zulogs.info() reaches Zulogs and leaves nothing behind in the service log, which is the wrong trade while you are attached to a container. mirrorToConsole prints every captured event locally as well:

zulogs.init({ key: process.env.ZULOGS_KEY, mirrorToConsole: true });

zulogs.warn('payment gateway slow', { ms: 842 });
// 2026-08-16T20:44:33.001Z WARN  payment gateway slow { ms: 842 }

Levels map onto console.debug, console.info, console.warn and console.error, and the context object is passed through unstringified so the console renders it and a circular reference cannot throw. Printing happens after beforeSend, so a redacted field never reappears locally and a dropped event is not printed either. minLevel applies as usual: what gets sent is what gets shown.

The output goes through the console as it was at import time, so enabling this together with captureConsole cannot feed the mirror back into itself.

Delivery

Events are buffered and sent in batches, at most every flushIntervalMs (default 2000) or as soon as maxBatchSize (default 100) has accumulated. Failed requests are retried three times with exponential backoff and jitter; a 4xx other than 429 is not retried, since repeating a malformed batch or a dead key only repeats the rejection.

The buffer is bounded at maxQueueSize (default 1000). Beyond it the oldest events are dropped — during an incident the newest ones describe what is happening now — and the number dropped travels with the next batch, so the counts you see are honest rather than silently short.

The flush timer is unref'd, so an idle logger never keeps a CLI alive.

Short-lived processes

Serverless handlers and scripts should flush before they return:

export async function handler(event) {
	try {
		return await work(event);
	} finally {
		await zulogs.flush();
	}
}

close() flushes, removes the handlers and shuts the client down.

Redacting

beforeSend sees every event just before it is queued. Return null to drop it, or a modified event to strip fields:

zulogs.init({
	beforeSend(event) {
		if (event.context?.password) delete event.context.password;
		return event;
	},
});

Options

| Option | Default | Meaning | | ---------------------------- | --------------- | --------------------------------------- | | key | ZULOGS_KEY | Ingest key | | environment | NODE_ENV | production, staging, … | | release | — | Version or commit, used for regressions | | serverName | os.hostname() | Reported with every event | | tags | — | Static tags on every event | | minLevel | info | Lowest level that is sent | | flushIntervalMs | 2000 | Background flush interval | | maxBatchSize | 100 | Events per request | | maxQueueSize | 1000 | Buffer ceiling before dropping oldest | | captureUncaughtExceptions | true | Handle uncaughtException | | captureUnhandledRejections | false | See the warning above | | captureConsole | false | Mirror console.error / console.warn | | mirrorToConsole | false | Also print every event locally | | debug | false | Print SDK diagnostics | | beforeSend | — | Redact or drop events |

Multiple clients

init() manages one default client. For several destinations, construct them directly — note that only one should install the global handlers:

import { ZulogsClient } from 'zulogs';

const audit = new ZulogsClient({ key, captureUncaughtExceptions: false });

In the browser

The browser half is the same package, imported from zulogs/browser:

import * as zulogs from 'zulogs/browser';

zulogs.init({
	key: 'zl_pub_live_…',
	release: __APP_VERSION__,
});

Call it in your entry file, before the application renders, so the handlers are installed before anything can throw.

The key is public

A browser key is a different kind of credential. It ships inside your bundle, where anyone can read it, so it is bound to the origins you list on the key in the dashboard and gets a much tighter rate limit than a server key. init() refuses a zl_live_… key outright: shipping a server credential to every visitor is the one mistake worth being loud about.

What it captures on its own

error and unhandledrejection on window, which together cover everything a page throws without catching. Both are one option, captureGlobalErrors, because a page has no process to keep alive and no crash to restore — the SDK reports and changes nothing else.

Stacks are parsed for all three engines: Chromium prints at fn (url:line:col), Firefox and Safari print fn@url:line:col. A parser for one of them would silently cost the stack of every Safari and Firefox user. Frames from your own origin are marked as application code; a CDN or an extension is not.

Until sourcemap support lands, frames stay minified, and Zulogs groups browser exceptions by type and message rather than by frame — a minified function name changes with every build, and grouping on it would file the same defect anew after each deploy.

Breadcrumbs

The last 20 things that happened before the error travel with it: navigation, fetch, XMLHttpRequest and clicks. A click records the element and its label, never the value someone typed. They are attached to exceptions only, because a log line needs no trail.

zulogs.addBreadcrumb({ category: 'click', message: 'checkout: pay' });

zulogs.init({
	maxBreadcrumbs: 20, // 0 switches them off entirely
	beforeBreadcrumb: crumb => (crumb.message.includes('token') ? null : crumb),
});

Noise

Three defaults, all overridable:

  • The same exception is sent once per five seconds. A render loop throws thousands of times and the hundredth copy says nothing the first one did not.
  • At most 30 automatically captured events per page load. Explicit captureException() calls are never capped.
  • Script error. and the ResizeObserver loop warnings are dropped, as is anything whose top frame is a browser extension. Your own ignoreErrors and denyUrls are added to that list, not swapped for it.

Delivery

Batches are smaller and rarer than on a server (20 events, every five seconds), because every request competes with the application's own. Whatever is still buffered goes out on pagehide and when the document becomes hidden.

Those last requests use fetch with keepalive, not navigator.sendBeacon: a beacon cannot carry the authorization header the ingest endpoint requires. The tradeoff is a 64 kB ceiling for all keepalive requests of a page, which is why batches stay small.

What the browser does not have

withScope(). It exists on Node because one process interleaves many requests and a mutable scope would attach the wrong user to an error. A page belongs to one visitor for its whole lifetime, so setUser(), setTags() and configureScope() are all it needs, and an async-looking withScope() would only be promising something it cannot keep.

Browser options

| Option | Default | Meaning | | --------------------- | ------- | --------------------------------------------- | | key | — | Public ingest key, zl_pub_live_…. Required | | release | — | Version or commit, used for regressions | | environment | key | Falls back to the key's environment | | captureGlobalErrors | true | Handle error and unhandledrejection | | captureConsole | false | Mirror console.error / console.warn | | maxBreadcrumbs | 20 | Breadcrumbs kept and attached to an exception | | beforeBreadcrumb | — | Redact or drop a breadcrumb | | ignoreErrors | — | Extra message patterns to drop | | denyUrls | — | Extra top-frame patterns to drop | | sendUrl | true | Report URL and referrer | | maxAutoEvents | 30 | Automatic reports per page load | | minLevel | info | Lowest level that is sent | | flushIntervalMs | 5000 | Background flush interval | | maxBatchSize | 20 | Events per request | | maxQueueSize | 100 | Buffer ceiling before dropping oldest | | mirrorToConsole | false | Also print every event locally | | beforeSend | — | Redact or drop events |

License

MIT