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

@atrix.dev/analytics-web

v0.1.1

Published

Atrix Analytics for the browser: events, pageviews, identity, consent. Tiny, cookieless-capable, never throws.

Readme

@atrix.dev/analytics-web

Atrix Analytics for the browser. It is 7.7 KB gzipped with its core included. It never throws, it does nothing when it isn't configured, and it can run without cookies.

bun add @atrix.dev/analytics-web      # or npm i / pnpm add
import * as atrix from "@atrix.dev/analytics-web";

atrix.init("atx_pk_eu_production_XXXXXXXXXXXXXXXXXXXXXX");   // region comes from the key → eu.i.analytics.atrix.dev

atrix.track("checkout_completed", { amount: 1999, currency: "AED", booking_id: "bk_42" }); // money: integer minor units
atrix.identify(user.id, { plan: "pro" });   // your user id (never an email or phone number)
atrix.group("company", "acme", { name: "Acme" });
atrix.register({ environment: "production" });   // stamped on every later event, persisted
atrix.reset();                                   // on sign-out

$pageview is recorded automatically when the page loads and on every SPA navigation (pushState, replaceState and popstate). Call atrix.page() yourself if you turn that off.

API

| Call | Notes | | --- | --- | | init(publicKey, options?) → AtrixWeb | Starts the default client. Calling it again replaces the client. On the server (SSR) and with an invalid key it is a no-op. | | createAtrix(publicKey, options?) | A separate client, for several projects on one page. | | track(event, properties?, { $set?, $set_once?, timestamp? }?) | Event names starting with $ are reserved for system events. | | page(properties?) | A manual $pageview. | | identify(distinctId, $set?, $set_once?) | One-way. Once identified, a different id is ignored until reset(). Junk ids (guest, null, anonymous, …) are refused. There is no alias(): identified people are never merged on the client. | | setRevenueCustomer(customerId, { provider? }?) | Links a billing customer id (Stripe cus_…, RevenueCat app_user_id, …) to the current user by sending $revenue_customer, so server-side revenue webhooks are attributed to them. provider is stripe, revenuecat, app_store or google_play; an unknown value is dropped. An empty id or one over 200 characters is ignored. | | reset() | New anonymous id and session; clears super-properties and groups. | | group(type, key, properties?) | Up to 5 group types. Later events carry groups. | | register(props) / registerOnce(props) / unregister(key) | Persisted super-properties. | | setConsent("granted" \| "pending" \| "denied"), optIn(), optOut(), getConsent() | See Privacy below. | | flush(), shutdown() | Both return promises that never reject. | | getDistinctId(), getInstance() | |

The instance returned by init() has the same methods, plus snapshot() for diagnostics and destroy(), which stops the client and removes its listeners without flushing.

Feature flags

atrix.isEnabled("new-checkout");            // false until flags load (lazily, on first use)
atrix.getVariant("pricing");                // "test" | "control" | null
atrix.getPayload("pricing");
atrix.onFlags((flags) => render(flags));    // now (if known) and after every load
init(KEY, { bootstrap: { flags: { "new-checkout": true } }, preloadFlags: true });

Flags are cached in the same storage as identity, reload on identify/group/reset, and each read sends one $feature_flag_called per flag and value per session (sendFeatureFlagEvents: false turns that off).

Options

| Option | Default | | | --- | --- | --- | | apiHost | https://<region>.i.analytics.atrix.dev | A first-party proxy, e.g. https://example.com/_atx. The SDK posts to {apiHost}/e. | | persistence | "localStorage" | "cookie" keeps identity in a first-party cookie; set cookieDomain: ".example.com" to share it across subdomains. The queue stays in localStorage either way. "memory" keeps everything in memory only. | | cookieless | false | Uses no storage at all and tags events consent: "cookieless". | | consent | "granted" | The starting state when the visitor hasn't decided. A stored decision takes precedence. | | capturePageview | true | $pageview on load and on SPA navigation. | | autocapture | false | true records clicks on links and buttons as $autocapture (tag and link path only). Use { captureText: true } to include element text. | | beforeSend(event) | | Return the event (edited if you like), or null to drop it. If the hook throws, the event is dropped. | | propertyDenylist | [] | Keys removed from properties, $set and $set_once of every event, after beforeSend. | | superProperties | {} | Stamped on every event for the life of the page. Not persisted; register() is. | | flushAt / flushIntervalMs | 20 / 30 000 | Batching. Everything queued is also sent with sendBeacon when the page is hidden. | | maxQueueSize / maxQueueBytes | 1000 / 5 MB | When full, the oldest events are dropped and reported in $dropped_events. | | requestTimeoutMs, sessionIdleTimeoutMs | 10 s, 30 min | | | compression | true | Request bodies are gzipped. | | debug | false | Logs problems to the console. |

Privacy defaults

  • No IP addresses are stored. Capture looks up the country from the IP and then discards it.
  • URLs are cleaned. The values of query parameters named like token, secret, password, auth, key, code, session, sig, email, phone or otp are replaced with [redacted] before any URL leaves the page. UTM parameters are never redacted.
  • Autocapture is off. When you turn it on, it records no element text unless you ask for it, and anything inside [data-atx-no-capture] is skipped. Form inputs are never read.
  • Consent. pending keeps events in memory, sends nothing and writes nothing to storage. granted sends the held events, tagged consent: "pending", and starts persisting. denied never touches the network and deletes the stored identity and queue; it keeps only the decision.
  • Cookieless mode doesn't read or write cookies, localStorage or sessionStorage.
  • On the first visit, first-touch attribution is sent once as $set_once: $initial_utm_*, $initial_referrer and $initial_current_url. It is not stored on the device.

Delivery

Batches are gzipped and sent as text/plain with ?compression=gzip-js. That makes them a CORS "simple request", so there is no preflight round trip. When the page is hidden (visibilitychange or pagehide), the queue goes out through navigator.sendBeacon. Events that fail to send wait in localStorage and are retried with backoff, including after a reload. Every event has a UUIDv7 that capture uses to drop duplicates, so a retry never counts twice.

Migrating an existing wrapper

PlayO's PostHog wrapper becomes:

atrix.init(KEY, { superProperties: { environment: ENV } });
export const track = (event: AnalyticsEvent, props?: Props) => atrix.track(event, props);
export const identifyPlayer = (p: { id: string; city?: string | null; level?: number | null }) =>
  atrix.identify(p.id, { city: p.city, level: p.level });
export const resetAnalytics = () => atrix.reset();

The wrapper no longer needs clean() or a try/catch around each call: undefined values are dropped and nothing throws. For RentGari's GA4 wrapper, keep the per-event helpers and send integer paisa with currency: "PKR" instead of converting to rupees. Its consent opt-out maps to atrix.optOut().

Session replay

atrix.init(KEY, { replay: { sampleRate: 0.5, minDurationMs: 2000, triggers: { events: ["checkout_started"] } } });

Off by default. The recorder (rrweb) is a separate chunk (~27 KB gz) that is downloaded only when a session is actually recorded, and only while consent is granted: pending records nothing, denied stops and drops everything buffered. Privacy defaults: every input value is masked (password inputs always, even inside data-atrix-unmask), data-atrix-mask masks a subtree's text, data-atrix-unmask reveals one, data-atrix-block replaces a subtree with an empty box, canvas and iframes are never recorded, and requests are recorded as URL (secrets redacted), method, status and timing only — never headers or bodies. Console errors (300 chars) and uncaught exceptions are recorded for the player's timeline. Chunks of ~5 s go to POST /r with retry; sessions the project samples out, and sessions past 4 h, stop recording. startReplay() / stopReplay() switch it at runtime.

Development

bun test                 # unit tests (happy-dom) + the full sdks/spec conformance matrix
bun run size             # builds dist/ and fails if core+web is over 12 KB gz or the replay chunk over 30 KB gz
bun run conformance      # the conformance CLI on its own

MIT licensed.