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

@collabland/analytics

v0.1.0

Published

Zero-dependency analytics SDK for browsers and Node — event tracking with automatic visitor/session identity, SPA pageview tracking, and Express/Next.js request middleware.

Readme

@collabland/analytics

A zero-dependency analytics SDK for browsers and Node. It never throws into your app — delivery failures, storage failures, and even a throwing onError callback are all swallowed internally. Full TypeScript definitions are included.

Install

npm install @collabland/analytics
# or
pnpm add @collabland/analytics
# or
yarn add @collabland/analytics

Requires Node >= 20 when used server-side. In the browser it works with no bundler configuration beyond standard ESM/CJS resolution.

Quickstart (browser)

import { createAnalytics } from '@collabland/analytics';

const analytics = createAnalytics({
  apiKey: 'pk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
  host: 'https://your-ingest-host.example.com', // see Configuration below — defaults to localhost
});

analytics.track('signup_completed', { plan: 'pro' });
analytics.page(); // no path given — auto-fills from location.pathname
analytics.identify('user_123', { email: '[email protected]' });

In browsers, identity is automatic and needs no setup:

  • anonymousId is minted once and persisted in localStorage under aa_anonymous_id.
  • sessionId is minted once per tab and persisted in sessionStorage under aa_session_id.
  • Both are stamped on every event automatically.
  • After you call identify(userId), that userId sticks — it's stamped on every subsequent event from this instance, not just the one you called identify on.
  • A bare analytics.page() (no path given) auto-fills path from location.pathname.

SPA route tracking

trackPageviews wires up automatic pageview tracking for client-side routers by patching the History API (pushState/replaceState/popstate) — no router-specific integration needed. Example for a Next.js App Router layout:

'use client';
import { useEffect } from 'react';
import { analytics } from './analytics';
import { trackPageviews } from '@collabland/analytics';

export function AnalyticsListener() {
  useEffect(() => trackPageviews(analytics), []);
  return null;
}

Two things worth knowing:

  • The History patch installs once per page and stays installed for the page's lifetime — calling the returned unsubscribe function stops that subscriber's emissions, but it does not remove the underlying patch.
  • If you unsubscribe and re-subscribe while still on the same path (e.g. React strict-mode's effect → cleanup → effect cycle), the initial page is not re-fired — consecutive same-path navigations are deduped.

Node / server usage

There's no window in Node, so nothing is automatic: pass userId, anonymousId, and sessionId explicitly on each call.

import { createAnalytics } from '@collabland/analytics';

const analytics = createAnalytics({
  apiKey: process.env.ANALYTICS_API_KEY!,
  host: 'https://your-ingest-host.example.com',
});

// Identity forwarded from the browser (see ids() below) — pass it per call:
export function recordCheckout(
  order: { total: number },
  ids: { userId?: string; anonymousId?: string; sessionId?: string },
) {
  analytics.track('checkout_completed', { total: order.total }, { ...ids, revenue: order.total });
}

identify() does not stick server-side. A server-side analytics instance is typically a single shared module-level object handling requests from many concurrent users — if identify() made the userId sticky the way it does in browsers, one user's id would leak onto every other user's events. In Node, call identify() only when you also want to emit a one-off kind:'identify' event, and pass userId explicitly via opts.userId (or RequestEventInput.userId) on every other call.

To keep server-emitted events joined to the same visitor/session as your frontend events, forward the browser's identity on your API calls using ids():

// frontend
const { anonymousId, sessionId } = analytics.ids();
await fetch('/api/checkout', {
  method: 'POST',
  body: JSON.stringify({ anonymousId, sessionId /* ...your payload */ }),
});

Express middleware

import express from 'express';
import { createAnalytics } from '@collabland/analytics';
import { analyticsMiddleware } from '@collabland/analytics/express';

const analytics = createAnalytics({
  apiKey: process.env.ANALYTICS_API_KEY!,
  host: 'https://your-ingest-host.example.com',
});

const app = express();
app.use(analyticsMiddleware(analytics, {
  ignore: (pathname) => pathname === '/healthz',
}));

Emits exactly one kind:'request' (http_request) event per completed response, with the matched route template as path (e.g. /users/:id, not /users/42) when Express has resolved a route, falling back to the concrete pathname otherwise (e.g. 404s). ignore receives that concrete pathname. The middleware types are hand-rolled structural subsets of Express's own types — no @types/express dependency required.

Next.js route handlers

// app/api/ask/route.ts
import { createAnalytics } from '@collabland/analytics';
import { withAnalytics } from '@collabland/analytics/next';

const analytics = createAnalytics({
  apiKey: process.env.ANALYTICS_API_KEY!,
  host: 'https://your-ingest-host.example.com',
});

async function handler(req: Request) {
  return Response.json({ ok: true });
}

export const POST = withAnalytics(analytics, handler, { route: '/api/ask' });

Emits one kind:'request' event per invocation, using opts.route as the path (Next exposes no runtime route-template API, so pass it explicitly) or the concrete request pathname if you omit it. withAnalytics never throws anything of its own — if your handler throws, that error is recorded as status 500 and then re-thrown unchanged, so your existing error handling is untouched. Works on both the Node and Edge runtimes.

Consent / privacy

  • identity: 'off' disables all storage access — the SDK never reads or writes localStorage/sessionStorage, and stamps no automatic anonymousId/sessionId. A sticky userId from identify() still works in-memory for the life of the instance (browsers only).
  • disabled: true implies identity: 'off' and goes further: the instance becomes a total no-op — no events are ever enqueued, and nothing is ever written to storage.
  • reset() clears the sticky userId, and in the default 'auto' mode also mints and persists fresh anonymous/session ids — call it on logout.
// before consent is granted
const analytics = createAnalytics({ apiKey: 'pk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', identity: 'off' });

// on logout
analytics.reset();

Configuration reference

| Field | Type | Default | Notes | | --- | --- | --- | --- | | apiKey | string | (required) | Your ingest API key. | | host | string | 'http://localhost:3000' | Set this to your ingestion endpoint in production. If left at the default, events silently go to localhost — the only signal is a delivery error passed to onError, if you provided one. | | flushAt | number | 20 | Queue size that triggers an automatic flush. | | flushIntervalMs | number | 5000 | Interval (ms) between timer-driven flushes. | | sampleRate | number | 1 | Fraction of events kept, 01. Dropped events never enqueue. | | disabled | boolean | false | Total no-op when true — see Consent / privacy above. | | identity | 'auto' \| 'off' | 'auto' | 'auto': in browsers, persist and auto-stamp anonymousId/sessionId. 'off': never touch storage. Node stamps neither, regardless of mode. | | onError | (err: Error) => void | undefined | Called for delivery failures, per-event ingest rejections (IngestRejectionError), and dropped oversized/unserializable events. A throwing onError is itself caught — it can never crash your app. |

API reference

| Member | Signature | Description | | --- | --- | --- | | track | track(name: string, props?: Record<string, unknown>, opts?: { userId?: string; timestamp?: Date; eventId?: string; revenue?: number; anonymousId?: string; sessionId?: string }): void | Enqueue a kind:'track' event. | | page | page(props?: { path?: string; title?: string; referrer?: string }): void | Enqueue a kind:'page' event; omitting path in a browser auto-fills location.pathname. | | identify | identify(userId: string, traits?: Record<string, unknown>): void | Enqueue a kind:'identify' event. Sticky in browsers only — see Node / server usage above. | | request | request(info: RequestEventInput): void | Enqueue a kind:'request' event — what the Express/Next middlewares call under the hood; you can also call it directly. | | reset | reset(): void | Logout: clears the sticky userId and mints fresh anonymous/session ids (browser only; no-op in Node). | | ids | ids(): { anonymousId?: string; sessionId?: string; userId?: string } | Current identity snapshot ({} in Node) — forward to your backend so server-emitted events share the same identity. | | flush | flush(): Promise<void> | Force an immediate flush of the queue. | | shutdown | shutdown(): Promise<void> | Stop the flush timer and perform a final flush (via sendBeacon in browsers, when available) — call on process exit / page teardown. | | trackPageviews | trackPageviews(analytics: Analytics): () => void | Framework-free SPA route tracking — see SPA route tracking above. Returns an unsubscribe function. | | createHttpTransport | createHttpTransport(opts: { host: string; apiKey: string; fetchImpl?: typeof fetch; sleep?: (ms: number) => Promise<void>; now?: () => number }): Transport | Builds the default HTTP transport. Override fetchImpl/sleep/now for tests, or pass your own Transport implementation to createAnalytics({ transport }). | | uuidv7 | uuidv7(now: number = Date.now()): string | RFC 9562 UUIDv7 generator — used internally to mint eventId. | | HTTP_REQUEST_EVENT_NAME | 'http_request' | The event name the Express/Next middlewares emit. | | IngestRejectionError | class IngestRejectionError extends Error { readonly rejected: { index: number; reason: string; event?: WireEvent }[] } | Passed to onError when the ingest server accepted a batch overall but rejected individual events within it. |

Delivery semantics

  • Events queue in memory (cap: 1000 — the oldest is dropped first if you exceed it) and flush automatically once the queue reaches flushAt (default 20) or every flushIntervalMs (default 5000ms), whichever comes first.
  • Batches are packed greedily and byte-aware: at most 500 events per HTTP request, individual events capped at 32 KiB (oversized events are dropped client-side and reported via onError rather than sent), and each request body capped at 256 KiB.
  • Failed sends retry up to 3 times with jittered exponential backoff, honoring a Retry-After response header when the server sends one. Non-retryable failures (4xx other than 429) are not retried.
  • On pagehide (browsers) and on shutdown(), the final flush prefers navigator.sendBeacon, falling back to a normal fetch if the beacon call is rejected or unavailable.
  • Every event carries a client-generated UUIDv7 eventId (override it via opts.eventId on track() for your own idempotency control). The ingest server dedupes on eventId, so a retried batch never double-counts.

License

MIT © Abridged, Inc.