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

@rantingjs/metrics

v0.1.0

Published

How a ranting app does in production: servers, usage, queues, cache, exceptions and slow requests, queries, jobs and outgoing requests

Readme

@rantingjs/metrics

How a ranting app is doing in production, at a glance: each server's CPU, memory and disks, the users making the most requests, slow requests, queries, jobs and outgoing requests, exceptions by class and place, queues, and cache hits.

Each process records in memory and writes at the end of each request, job and command. Every record goes to the buckets of the four periods the dashboard shows (1 hour, 6 hours, 24 hours, 7 days; 60 buckets each), so a card reads 60 rows, not every event. The raw entries are kept too, for the oldest, partial bucket of a period.

bun add @rantingjs/metrics
bunx ranting install @rantingjs/metrics --skip-add   # METRICS_ENABLED in .env, src/config/metrics.ts
bunx ranting migrate                               # metrics_entries, metrics_aggregates, metrics_values

The app needs DatabaseModule. Open /metrics; the cards refresh every 5 seconds.

Who may open it

The viewMetrics ability decides. Until the app defines its own, it lets everyone in while app.env is local and nobody elsewhere (an app without AuthModule gets that rule too). Define it where the app starts:

constructor(gate: Gatekeeper) {
  gate.define('viewMetrics', (user: User) => user.isAdmin);
}

It is an ordinary ability: the gate's before callbacks count, guests are refused without asking it (unless it is defined with { guests: true }, as for a monitoring token: () => request().header('x-metrics-token') === token), and a denial's own status is the answer (Gate.denyAsNotFound() gives 404).

Commands

bunx ranting metrics:check            # samples the server's CPU, memory and disks every 15 seconds; one per server
bunx ranting metrics:check --once     # one sample
bunx ranting metrics:work             # with ingest.driver redis: moves the stream into the database
bunx ranting metrics:restart          # running check and work processes stop after their current round
bunx ranting metrics:clear            # deletes everything (--type slow_query for one type; --force in production)

Old data is deleted on 1 in 1000 writes (ingest.trim.lottery), and by metrics:work every 10 minutes.

Config

| Key | Default | | |---|---|---| | enabled | METRICS_ENABLED, false | Record. ranting install writes METRICS_ENABLED=true to .env. | | domain | METRICS_DOMAIN | The dashboard only on this host (404 on others). | | path | METRICS_PATH, metrics | Where the dashboard lives. | | storage.database | { connection: METRICS_DB_CONNECTION, chunk: 1000 } | The tables' connection; rows per insert. | | ingest.driver | METRICS_INGEST_DRIVER, storage | storage writes directly, redis to a stream that metrics:work writes in bulk, null drops it. | | ingest.buffer | 5000 | A process writes early once it holds this many records. | | cache | METRICS_CACHE_DRIVER, the default | The store for the dashboard's 5-second card cache and metrics:restart. | | recorders | see below | What is recorded. | | dashboard | see below | The cards and their place on a 12-column grid. |

Each recorder takes enabled, sampleRate (0 to 1: record that share and scale the counts up, shown with ~) and ignore (patterns of what not to record). Thresholds are milliseconds, one number or per pattern: threshold: { default: 1000, patterns: [[/^\/reports/, 5000]] }.

| Recorder | Records | |---|---| | servers | CPU, memory and the use of directories, as serverName, from metrics:check. | | userRequests | Requests by each signed-in user. | | userJobs | Jobs queued by each signed-in user. | | slowRequests | Requests over threshold, by method, route as registered (/posts/:id) and controller method; files from public/ and the dashboard are left out. | | slowQueries | Queries over threshold, by SQL with its placeholders (never the values) and the line of the app that ran it (location); maxQueryLength cuts long SQL. | | slowJobs | Jobs over threshold, by class. | | slowOutgoingRequests | Http requests over threshold, by method and URL; groups turns URLs into one name. | | exceptions | Exceptions the app reports, by class and the first line of the app's code (location). | | queues | Jobs queued, processing, processed, released and failed, per connection:queue (not sync). | | cacheInteractions | Hits and misses by key; groups turns keys into one name (user:42 into user:*). |

Dashboard

dashboard: [
  { card: 'servers', cols: 'full', ignoreAfter: 3600 },  // leave out a server silent for an hour
  { card: 'usage', cols: 4, rows: 2 },                   // or type: 'requests' | 'slowRequests' | 'jobs'
  { card: 'queues', cols: 4 },
  { card: 'cache', cols: 4 },
  { card: 'slowQueries', cols: 8 },
  { card: 'exceptions', cols: 6 },
  { card: 'slowRequests', cols: 6 },
  { card: 'slowJobs', cols: 6 },
  { card: 'slowOutgoingRequests', cols: 6, expand: true },
  { card: 'deploys', cols: 6 },                          // a card of the app's own
],

The usage card shows users by name: from the auth.model config, or as the app says:

metrics.user((user) => ({ name: user.fullName, extra: user.email, avatar: user.photoUrl }));

A card of the app's own records with metrics.record() (count, sum, max, min, avg) or metrics.set(), and reads with storage into a table:

metrics.record('deploy', release, durationMs).max().count();
metrics.card('deploys', {
  title: 'Deploys',
  data: async ({ storage, period }) => ({
    columns: ['Release', 'Slowest', 'Count'],
    rows: (await storage.aggregate('deploy', ['max', 'count'], period)).map((row) => [row.key, row.max, row.count]),
  }),
});