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

@xeonr/forrest

v0.1.0

Published

Client SDK for Forrest — send logs, metrics and analytics events from anything that runs JavaScript.

Readme

@xeonr/forrest

Send logs, metrics and analytics events to Forrest from anything that runs JavaScript — browsers, Node, Deno, Bun, React Native, Cloudflare Workers.

Zero dependencies. One class.

import { Forrest } from "@xeonr/forrest";

const forrest = new Forrest({ url: "https://forrest.prod.wtf", token });

forrest.info("user signed in");
forrest.error(new Error("upload failed"));
forrest.metric("fps", 59.5);
forrest.event("checkout_completed", { plan: "pro" });

That's the whole API. Records are queued and sent in batches in the background; nothing blocks and nothing throws — a telemetry client that can break the application it observes is worse than no telemetry. Delivery problems go to onError.

Getting a token

A device never holds your API key. Your backend holds it and hands out short-lived per-device tokens:

// server-side only
import { mintToken } from "@xeonr/forrest/server";

app.get("/telemetry-token", async (req, res) => {
  const token = await mintToken({
    apiKey: process.env.FORREST_API_KEY!,   // fk_<id>_<secret>
    clientId: req.user.deviceId,            // your own id for this device
    ttlSeconds: 3600,
    attrs: { plan: req.user.plan },         // stamped on every record
  });
  res.json({ token });
});

Minting lives at @xeonr/forrest/server — a separate entry point so it can never be pulled into a browser bundle by accident. Never ship your API key to a device.

Tokens expire, so pass a function and the client refreshes on its own:

const forrest = new Forrest({
  url: "https://forrest.prod.wtf",
  token: async () => (await fetch("/telemetry-token").then((r) => r.json())).token,
});

The token is a plain HS256 JWT with the key id in kid, so any language with an HMAC can mint one. mintToken just saves you reaching for a JWT library.

Options

| Option | Default | | |---|---|---| | url | — | Your forrest host. Required. | | token | — | A token, or a function returning one. Required. | | attrs | — | Merged into every record; per-record attrs win. | | logger | — | Sent as the logger on every log line. | | batchSize | 100 | Flush once this many records are queued. | | flushInterval | 5000 | Flush at least this often (ms). 0 disables. | | maxQueue | 10000 | Queue cap; past it the oldest are dropped. | | retries | 3 | Attempts per batch. | | onError | — | Delivery failures and dropped records. | | onResponse | — | The server's reply, including dropped field names. | | fetch | globalThis.fetch | Override for tests or unusual runtimes. |

Custom fields

Attributes must be declared on the project before they can be filtered on. Anything undeclared is dropped server-side and named in the response — sending it is never an error, so shipping a new field before declaring it will not break your fleet. To see what is being dropped:

new Forrest({
  url, token,
  onResponse: (r) => {
    if (r.droppedFields) console.warn("forrest dropped:", r.droppedFieldNames);
  },
});

forrest.error(err) puts the stack in the log body, the way console.error(err) does — not in an attribute, because undeclared attributes are dropped and a stack is not something you would filter by.

Catching crashes

const stop = forrest.captureErrors();  // uncaught errors + unhandled rejections

Opt-in on purpose: installing global handlers from a constructor is the kind of side effect that is miserable to debug when you did not know it happened. Call the returned function to remove them.

Shutting down

await forrest.flush();   // send what is queued
await forrest.close();   // flush and stop the background timer

In browsers the client also flushes on pagehide and when the tab is hidden, using sendBeacon so the batch survives the page going away. In Node the background timer is unref'd, so it never keeps a process alive.

Behaviour worth knowing

  • Retries are exponential with jitter, and honour Retry-After on a 429. A 4xx that is not 429 is not retried — a bad token does not fix itself.
  • The queue is capped. A long offline stretch drops the oldest records rather than growing until the tab or process dies.
  • Batches are all-or-nothing. A rejected batch is not partially applied.