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

@syntarie/tracking

v0.3.0

Published

Syntarie tracking SDK — browser-first, tiny, privacy-respecting analytics.

Readme

@syntarie/tracking

Browser-first, tiny, privacy-respecting analytics SDK for leatmap.

  • < 5 kB gzip core (CP §1.5).
  • Opt-in modules for click, scroll, engaged-time, vitals, errors, offline queue, retries.
  • Lazy-loaded replay sub-bundle for session and error replay (~30 kB gzip, fetched only when imported).
  • ESM only. Targets Safari 15+ and every evergreen browser.

Performance budgets (enforced in CI — TRK-305)

These are hard gates. A PR that exceeds them fails the sdk-perf job and cannot merge.

Bundle size (gzip, per entry)

| chunk | budget | what it ships | | --- | ---: | --- | | index.js (core) | 5200 B | init, queue, transport, pageview, consent, identify, session, sampler, region resolver | | vitals.js | 1024 B | LCP / FID / CLS web-vitals | | errors.js | 1024 B | window.onerror + unhandledrejection | | network-errors.js | 1500 B | fetch + XHR wrap | | clicks.js | 1500 B | click delegation + selector builder | | scroll.js | 1500 B | scroll-depth buckets | | heatmap.js | 2000 B | click coords + scroll buckets for heatmap rendering | | engaged-time.js | 1500 B | visible time + idle pause | | offline-queue.js | 1500 B | IndexedDB persistence | | retry.js | 1500 B | lazy retry orchestrator | | feedback.js | 4000 B | NPS + CSAT widget (shadow-DOM, ARIA) | | feature-flags.js | 500 B | subpath re-export wrapper | | feature-flags-init.js | 3000 B | eval engine + cache + fetch loop (lazy) | | replay.js | 35 000 B | rrweb + recorder + redact + chunker + sampler (lazy) |

Aggregate gate: every customer-loadable entry combined (excluding replay.js and the lazy feature-flag init chunk) must stay under 12 000 B gzip. This stops a future change from stacking five "1.5 kB opt-in" subpaths into a customer-visible regression that no per-entry check would catch.

Init time (cold + warm, measured via headless Chromium)

| metric | budget | measurement | | --- | ---: | --- | | Cold init() | < 50 ms | first call on a freshly loaded page, median of 5 samples | | Warm init() | < 10 ms | second call short-circuits via config !== null; median of 20 samples |

Both gates run against the built artifact under dist/, not source. Bundle size is asserted by sdk/scripts/check-bundle-size.ts (also from sdk/test-built/bundle.test.ts during pnpm test); init time runs under the CI sdk-perf job via sdk/scripts/check-init-time.ts.

The size gate runs in two modes:

  • CI (sdk-perf job) — builds the PR and a sibling origin/main checkout, then renders the PR comment as a gzip / main / Δ table so reviewers see signed byte deltas vs the merge target. A core-bundle Δ above +250 B raises a ::warning annotation but does not fail the build; only an absolute budget breach blocks merge.
  • Local (pnpm size) — no main to compare against, so the script falls back to headroom = budget − current. Same gate, fewer columns.
# Local re-runs (assume a fresh build):
pnpm -F @syntarie/tracking build
pnpm -F @syntarie/tracking size      # bundle size (headroom mode)
pnpm -F @syntarie/tracking bench:init # init time (requires `playwright install chromium`)

Install

pnpm add @syntarie/tracking

Initialise

Get an API key from app.leatmap.com → Settings → API keys. Your site id is shown next to it on the install page (for a fresh workspace it equals your workspace id).

import { init, grantConsent } from '@syntarie/tracking';

init({
  apiKey: 'lk_live_xxx',
  host: 'https://collect.leatmap.com',
  siteId: 'a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d',
});

// The hosted collector enforces consent server-side and rejects
// requests without a non-empty consent token. See Consent below.
grantConsent('granted');

apiKey is the value the leatmap dashboard issues you. It is sent verbatim as the bearer on every collector request; the bearer is stored as a hash server-side; the collector resolves your workspace from the matching hash on each request.

siteId is the site's UUID (36 characters). When set alongside apiKey, the SDK sends it as the X-Site-Id header on every request. The hosted collector requires that header on keyed requests and answers 401 missing_site_id without it, so omitting siteId means no event ever lands.

Track custom events

import { track } from '@syntarie/tracking';

track('checkout_completed', { plan: 'pro', amount_cents: 1999 });

Identify a user

import { identify } from '@syntarie/tracking';

identify('user-1234', { email: '[email protected]', plan: 'pro' });

Consent

import { grantConsent, revokeConsent } from '@syntarie/tracking';

grantConsent('granted');   // start sending events, with a consent token
revokeConsent();           // purge queue and stop

When defaultConsent is 'unknown' (the default), events accumulate in memory but never leave the SDK until the host page calls grantConsent().

The hosted collector at collect.leatmap.com additionally requires a non-empty X-Consent token on every request and rejects requests without one. The SDK only attaches that header when grantConsent() was called with a token argument: grantConsent('granted') works, and so does passing the token your own consent UI produced (the collector stores it as proof of consent). Note that defaultConsent: 'granted' alone does NOT attach a token, so against the hosted collector you must still call grantConsent('<token>') once after init().

Coverage tracking (pageKey)

The SDK can stamp a server-anchored page_key on every event so the dashboard's coverage report (TRK-262) can show you what your current analytics tool is hiding — events that fired versus renders that never made it through.

Two ways to wire it:

// 1. Auto-detect from a meta tag your SSR layer renders.
//    Recommended — works with the @syntarie/tracking-node SSR helper.
init({
  apiKey: 'lk_live_xxx',
  host: 'https://collect.leatmap.com',
  siteId: 'a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d',
});
// init() reads <meta name="leatmap-page-key" content="..."> on its own.

// 2. Pass it explicitly.
init({
  apiKey: 'lk_live_xxx',
  host: 'https://collect.leatmap.com',
  siteId: 'a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d',
  pageKey: 'f47ac10b-58cc-4372-a567-0e02b2c3d479',
});

When neither source is available the field is dropped from the wire payload — events still flow, they just land in the dashboard's unknown bucket. See docs/coverage-tracking for the SSR template that mints + threads the key.

Opt-in modules

Each lives at its own subpath import so the core stays small.

import { installVitals } from '@syntarie/tracking/vitals';
import { installErrors } from '@syntarie/tracking/errors';
import { installClicks } from '@syntarie/tracking/clicks';
import { installScroll } from '@syntarie/tracking/scroll';
import { installEngagedTime } from '@syntarie/tracking/engaged-time';

init({
  apiKey: 'lk_live_xxx',
  host: 'https://collect.leatmap.com',
  siteId: 'a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d',
});
installVitals();
installErrors();
installClicks();
installScroll();
installEngagedTime();

Session replay (opt-in, lazy-loaded)

import { init, getAnonId, getSessionId } from '@syntarie/tracking';

init({
  apiKey: 'lk_live_xxx',
  host: 'https://collect.leatmap.com',
  siteId: 'a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d',
});

const { startReplay, startErrorReplay } = await import(
  '@syntarie/tracking/replay',
);

startReplay({
  apiKey: 'lk_live_xxx',
  host: 'https://collect.leatmap.com',
  anonId: getAnonId(),
  sessionId: getSessionId(),
  replaySampleRate: 0.05,
});

Migrating from siteId-as-key

Pre-v1 the SDK accepted siteId instead of apiKey, as the bearer. That legacy alias (a siteId WITHOUT an apiKey) keeps working for one release window — it triggers a one-time console.warn and will be removed in v2.0.0. Get an API key from the dashboard and rename the field:

-init({ siteId: 'abc123', host: 'https://collect.leatmap.com' });
+init({
+  apiKey: 'lk_live_xxx',
+  host: 'https://collect.leatmap.com',
+  siteId: 'a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d',
+});

Note the field name survived with a new meaning: a siteId passed ALONGSIDE apiKey is the modern contract, your site's 36-char UUID sent as the X-Site-Id header (required by the hosted collector, see Initialise above). Only the keyless form is deprecated.

Privacy

  • Default consent is 'unknown' — no network egress without an explicit grantConsent().
  • Honours the browser's Do Not Track signal by default. Disable with respectDnt: false.
  • cookieless mode swaps the persistent anon id for a daily-rotating fingerprint of UA, screen size, and timezone.

License

MIT.