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

trackhome

v4.0.10

Published

Tiny browser SDK for trackhome — a self-hosted, tags-only event/click tracker.

Readme

trackhome

Tiny browser SDK for trackhome — a self-hosted, tags-only event/click tracker.

  • One-line setup: init('YOUR_TRACKHOME_URL') and you're done.
  • Auto-collects page views on init and on every SPA route change (React Router, Next.js, Vue Router — all work without extra config).
  • Tags-only model: every event is { tags, ts?, value? } with exactly one event:<name> tag (e.g. event:page_view, event:click). No separate type field.
  • Privacy-friendly: visitor identity is a client-generated id (base58, stored in localStorage) sent via the X-Trackhome-Visitor header. No cookies. IP/UA are hashed on the server, never stored raw.
  • Zero dependencies, ~2 KB gzipped (UMD), works with or without a bundler.
  • Fail-safe: tracking APIs (init, track, event, page, identify, flush) never throw into your app — network/ingest errors are throttled to console.warn, events are re-queued (capped) and retried on the next flush.
  • Auto-enriched server-side: every event also gets device:*, os:*, country:*, lang:* tags derived from request headers, so dashboards break down by these for free.

Install

npm install trackhome
# or: pnpm add trackhome / yarn add trackhome

Quick start (3 ways)

1. Bundler (webpack, Vite, esbuild, …)

import { init } from 'trackhome';

// That's it. Fires event:page_view immediately + on every SPA navigation.
init('YOUR_TRACKHOME_URL');

2. Build-less <script> tag (from your trackhome server)

<script src="YOUR_TRACKHOME_URL/t.js"></script>
<script>
  trackhome.init('YOUR_TRACKHOME_URL');
</script>

Replace YOUR_TRACKHOME_URL with the base URL of your self-hosted trackhome instance (no trailing slash).

Custom events

import { track, event, identify } from 'trackhome';

// Primary API — tags must include exactly one event: tag
track(['event:signup_click', 'tier:pro', 'cta:header']);

// Ergonomic helper — adds event: prefix for you
event('purchase', ['plan:annual']);

identify('user_42'); // → event:identify + user:user_42

API

init(endpointOrConfig)

init('YOUR_TRACKHOME_URL')                              // shorthand

init({                                                       // full config
  endpoint: 'YOUR_TRACKHOME_URL',
  projectKey: 'YOUR_PROJECT_KEY',   // optional: multi-tenant project key
  tags: ['app:marketing'],          // persistent tags on every event
  autoPageView: true,               // default true — see below
  batchSize: 20,                    // max events per flush
  flushIntervalMs: 5_000,           // flush cadency in ms
  disabled: false,                  // kill switch for dev
})

Idempotent — calling init() twice is a no-op.

autoPageView (default: true) — fires event:page_view tagged page:/current/path:

  1. immediately on init
  2. on history.pushState / replaceState (catches React Router, Vue Router, Next.js navigation)
  3. on popstate (back/forward buttons)
  4. on hashchange

So for many sites, the one-liner init(url) is all you need — the SDK collects traffic without any manual track() calls. Pass autoPageView: false if you have your own router hook or want to call page() manually.

track(tags, value?)

track(['event:signup_click', 'tier:pro']);
track(['event:page_view']);                   // value optional

Tags must include exactly one event:<name> tag. Persistent tags (set via init({tags}) / setTags / addTag) are merged in. Duplicates are deduped.

event(name, tags?, value?)

event('signup_click', ['tier:pro']);
// equivalent to track(['event:signup_click', 'tier:pro'])

page(name?, tags?)

page()                  // → event:page_view with page:<current-path>
page('pricing')         // → event:page_view with page:pricing
page('/blog/post-1')    // → event:page_view with page:/blog/post-1

Useful when you want to control page-view timing yourself (e.g. after a custom route transition).

identify(userId, tags?)

identify('user_42');
// → emits tags: ['event:identify', 'user:user_42', ...persistentTags]

This is just tags — it's the same as event('identify', ['user:user_42']). The server doesn't have a separate user table; downstream dashboards can group by user:* tags.

setTags(tags) / addTag(tag)

Replace / append to the persistent tag set.

setTags(['app:marketing', 'variant:b']);
addTag('flow:onboarding');

flush({ useBeacon? })

Manually flush the pending queue. Auto-flushes on:

  • batch full (default 20 events)
  • timer (default every 5s)
  • visibilitychange to hidden
  • beforeunload
  • pagehide

You only need to call this yourself in tests or for at-exit guarantees. It never throws — failed ingests log a throttled warning and re-queue events (oldest dropped if the queue exceeds ~500 while ingest is down).

Error handling

All browser tracking calls are wrapped in internal try/catch and will not crash your app if:

  • the ingest endpoint is down or returns 4xx/5xx
  • fetch / sendBeacon fails (offline, CORS, ad blockers)
  • auto-page-view hooks or attribution parsing hit an edge case

Warnings are logged at most once every 30 seconds via console.warn('[trackhome] …'). The queue caps at ~500 events; if ingest stays down, oldest events are dropped to bound memory.

createLink() is server-side admin API — it may still throw on misconfiguration or API errors (call from Node with try/catch).

Server-side admin: create links

Provide secretKey_im_not_frontend (your trackhome SECRET_KEY) for admin calls. Never use in browser/frontend code.

import { init, createLink } from 'trackhome';

init({
  endpoint: 'YOUR_TRACKHOME_URL',
  secretKey_im_not_frontend: process.env.TRACKHOME_SECRET_KEY,
});

const link = await createLink({
  url: 'https://example.com/landing',
  tags: ['campaign:email'],
  expirationMs: 86_400_000,
});
// link.short_url, link.key, link.expires_at, ...

Or pass credentials per call via the second argument. Omit key for a random slug. Uses X-Secret-KeyPOST /api/links.

Auto-tags (server-side, zero-config)

Every event sent to the trackhome server is enriched with tags derived from the HTTP request — callers never set these:

| Tag | Source | Example | |---|---|---| | device:mobile | device:desktop | device:tablet | User-Agent regex | device:mobile | | os:android | os:ios | os:macos | os:windows | os:linux | os:chromeos | User-Agent | os:ios | | country:xx | cf-ipcountry / x-vercel-ip-country / x-aws-region (2-letter ISO) | country:us | | lang:xx | Accept-Language (primary prefix) | lang:en |

This means a dashboard can break down traffic by device, OS, country, language for free — no extra client config. Caller-supplied tags of the same name win on collision.

Server

This package is just the client SDK. Deploy your own trackhome server (Fastify + ClickHouse) to receive events and serve the dashboard. The server also hosts this SDK at /t.js.

Session recording (server-driven)

Session recording (DOM replay via rrweb) is always server-driven — the SDK has no static recording config. Recording starts/stops automatically based on rules configured in the dashboard:

  1. Every POST /ev response may include a recording: { sampleRate, maskAllInputs, ... } field (or omit it).
  2. When present, the SDK lazy-imports rrweb and starts recording with the descriptor's knobs.
  3. When absent on a subsequent response, the SDK stops any active recording.

Visitors who never match an admin rule never download rrweb — zero bytes on the wire, zero parse cost. The customer's init() call never touches recording config; admins manage rules in the dashboard (targeting selector + sample rate per rule).

Deterministic sampling: rules use fnv1a32(visitor_id, rule_id) % 100 < sampleRate, so the same visitor is always sampled the same way across sessions. The server pre-buckets and only sends the descriptor to sampled-in visitors; the SDK re-checks client-side as defense-in-depth.

PII safety defaults: maskAllInputs: true (form input values show as ***) and blockSelector: [data-block-record] (CSS selector for fully-excluded regions). Both are configurable per rule in the dashboard.

Recording dependencies (installed automatically)

rrweb ships as a regular dependency of this package — when you npm install trackhome, you get everything you need for recordings out of the box. No peer dep negotiation, no bundler IgnorePlugin configuration, no "install this if you want recordings" dance.

The dynamic import('rrweb') stays lazy — visitors who never match an admin recording rule never download the rrweb chunk. Tree-shaking still works: bundlers split rrweb into its own chunk that only loads on demand. Consumers who never enable recording rules pay only the ~14 KB base SDK cost.

Note on gzip: recording batches are compressed via the browser's native CompressionStream API (universal since Chrome 80+, Firefox 113+, Safari 16.4+). No fflate or other polyfill peer dep required.

For advanced manual control (rare), import from the sub-entry:

import { startRecording, stopRecording } from 'trackhome/recording';

This bypasses the server-driven model — use only if you know what you're doing.

License

MIT