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

@jterrazz/analytics

v1.0.0

Published

Vendor-neutral product analytics for Node.js and TypeScript apps, with pluggable adapters.

Readme

@jterrazz/analytics

Vendor-neutral product analytics for Node.js and TypeScript apps, with pluggable adapters.

Installation

npm install @jterrazz/analytics

Usage

import { OpenPanelAnalyticsAdapter } from '@jterrazz/analytics';

const analytics = new OpenPanelAnalyticsAdapter({
    apiUrl: 'https://analytics.jterrazz.com/api', // self-hosted instance
    clientId: process.env.OPENPANEL_CLIENT_ID!,
    clientSecret: process.env.OPENPANEL_CLIENT_SECRET, // server-side only
});

await analytics.track('article_shared', {
    profileId: 'user-1',
    properties: { channel: 'x', slug: 'hello-world' },
});

Typed event catalogue

Declare your tracking plan as a type and get compile-time safety:

import type { AnalyticsEvents, AnalyticsPort } from '@jterrazz/analytics';

interface AppEvents extends AnalyticsEvents {
    app_link_opened: { platform: 'android' | 'desktop' | 'ios'; slug: string };
    user_signed_up: { plan: string };
}

const analytics: AnalyticsPort<AppEvents> = new OpenPanelAnalyticsAdapter<AppEvents>({ ... });

await analytics.track('app_link_opened', { properties: { platform: 'ios', slug: 'x' } }); // ✅
await analytics.track('unknown_event'); // ❌ compile error

Event naming convention: object_action, snake_case, past tense (user_signed_up), static names, data in properties.

Request scopes (server-side)

Backends derive geolocation from the IP and device/browser/OS from the user-agent. On the server, bind a scope to the incoming request so events are attributed to the real user instead of your server:

const requestAnalytics = analytics.child({
    ip: request.ip,
    userAgent: request.headers['user-agent'],
    profileId: session?.userId, // links server events to the active web session
});

await requestAnalytics.track('subscription_started', { properties: { plan: 'pro' } });

Page views

Send the full URL — the backend extracts path, query and UTM attribution:

await analytics.page({
    referrer: 'https://news.ycombinator.com/',
    title: 'Hello World',
    url: 'https://example.com/articles/hello?utm_source=hn',
});

Sessions, bounce rate and page duration are computed by the backend from page views; they need no input from this API.

Revenue

await analytics.revenue(49.9, { profileId: 'user-1' }); // EUR by default
await analytics.revenue(100, { currency: 'USD', properties: { product: 'pro-plan' } });

Profiles

Codified traits follow the Segment identify spec; anything else goes into properties:

await analytics.identify({
    profileId: 'user-1', // your database id, never the email
    email: '[email protected]',
    firstName: 'Jean',
    createdAt: new Date('2026-01-15'),
    plan: 'pro',
    properties: { referral: 'friend' },
});

Adapters

OpenPanelAnalyticsAdapter

Sends events to OpenPanel — cloud or self-hosted.

const analytics = new OpenPanelAnalyticsAdapter({
    apiUrl: 'https://analytics.example.com/api', // omit for OpenPanel cloud
    clientId: 'your-client-id',
    clientSecret: 'your-client-secret', // required for server-side tracking and revenue
    globalProperties: { app: 'my-app' }, // sent with every event
});

NoopAnalyticsAdapter

Implements the port but sends nothing. Use it in local development and tests.

import { NoopAnalyticsAdapter } from '@jterrazz/analytics';

const analytics = new NoopAnalyticsAdapter();

API

| Method | Description | | --------------------------------------------------------- | ------------------------------------------------ | | track(event, { profileId?, properties? }) | Track a named event, optionally scoped to a user | | page({ url, title?, referrer? }, { profileId? }) | Track a page view | | revenue(amount, { currency?, profileId?, properties? }) | Track revenue (EUR by default) | | identify({ profileId, ...traits, properties? }) | Attach identity and traits to a profile | | increment(property, { profileId, value? }) | Increment a numeric profile property | | decrement(property, { profileId, value? }) | Decrement a numeric profile property | | setGlobalProperties(properties) | Set properties sent with every subsequent event | | child(context) | Create a request-scoped analytics instance |

Port Interface

Depend on the port, inject the adapter:

import type { AnalyticsPort } from '@jterrazz/analytics';

class SignupService {
    constructor(private readonly analytics: AnalyticsPort) {}

    async signup(email: string): Promise<void> {
        // ...create the user...
        await this.analytics.track('user_signed_up', {
            profileId: user.id,
            properties: { plan: 'free' },
        });
    }
}

License

MIT