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

@apptrackx/sdk-web

v0.1.0

Published

AppTrackX Web SDK — install attribution, page views and event tracking for web properties

Readme

AppTrackX Web SDK

Page view and event tracking for web properties. TypeScript, no dependencies, 2.1 KB gzipped against a 10 KB budget the build enforces.

P2.SDKW, built in W4-41.


What is different about the browser

Worth reading before integrating, because two of these change what you can expect from the data.

It cannot sign its events. Every native SDK signs its payload with the app's ingest secret. A browser bundle cannot hold a secret — shipping it publishes it to anyone who opens DevTools — and navigator.sendBeacon cannot set a header at all. So the collector authenticates browser events by Origin, checked against the domains registered on the app.

That is a weaker guarantee, and the data says so. A browser sets Origin and page script cannot forge it, so this stops a rogue website spending your budget. It does not stop curl. Every browser event is therefore stored as ingest_source = 'browser' with the origin that admitted it, so a billing question can separate what was proven from what was merely plausible.

There is no device identifier. No GAID, no IDFA, nothing the browser will tell you. The visitor id this SDK mints and stores in localStorage is the entire basis for saying two page views came from one person.


Install

npm

npm install @apptrackx/sdk-web
import { init, track, setConsent } from '@apptrackx/sdk-web'

init({ appToken: 'your-app-id', endpoint: 'https://go.apptrackx.com' })

A script tag

The bundle is 2.3 KB gzipped and needs no build step. Pin the version — latest means a page can change behaviour without anybody deploying.

<script src="https://unpkg.com/@apptrackx/[email protected]/dist/index.global.js"></script>
<script>
  AppTrackX.init({
    appToken: 'your-app-id',
    endpoint: 'https://go.apptrackx.com',
  })
</script>

Self-hosted

Serving it yourself avoids a third-party origin on every page load, which some consent regimes and CSP policies require.

pnpm --filter @apptrackx/sdk-web build
# copy sdks/web/dist/index.global.js onto your own site

Versioning

0.x, and that is not modesty — the minor version may break. Pin an exact version in production and read the changelog before moving.


---

## Register your domain first

Under **Apps → Platforms → Web**, register the exact origin the site is served
from. An unregistered origin is refused with a 401 and the events are lost.

`https://shop.example.com` and `http://shop.example.com` are **different
origins**, and so is `https://www.shop.example.com`. Register each one you
actually serve from.

---

## Consent

Nothing is sent and nothing is stored until you say so.

```ts
banner.onAccept(() => AppTrackX.setConsent(true))
banner.onReject(() => AppTrackX.setConsent(false))

Events raised while the banner is unanswered are held in memory — never in localStorage, because writing behavioural data about someone who has not agreed to it is the thing the gate exists to prevent. They are delivered if consent is granted and discarded if it is refused. A page closed before an answer loses them, which is the correct trade.

Up to 50 events are held; beyond that the oldest are dropped.

If consent is already established by other means, start open:

AppTrackX.init({ appToken: '…', endpoint: '…', consent: 'granted' })

Withdrawing consent stops future events. It cannot recall what was already sent, and this SDK does not pretend otherwise.


Tracking

A page view is sent on init unless you pass autoPageView: false.

Single-page apps must call trackPageView() on each route change — no navigation happens, so nothing else will.

AppTrackX.trackPageView()

AppTrackX.track({
  token: 'purchase',
  revenue: '249.50',
  currency: 'INR',
  params: { plan: 'pro' },
})

revenue is a string. The column is NUMERIC(18,6); a JavaScript number cannot hold 19.99 exactly, and a revenue figure that drifts by a hundredth is one nobody can reconcile against a payment processor.


Carrying a visitor across your domains

localStorage is per-origin, so a visitor moving from example.com to shop.example.net is two visitors with two ids. Third-party cookies used to solve this and browsers have removed them; what is left is putting the id in the link.

AppTrackX.init({
  appToken: '…',
  endpoint: '…',
  stitchDomains: ['shop.example.net'],
})

link.href = AppTrackX.decorateUrl(link.href)

Only listed domains are decorated. Attaching the id to every outbound link would publish a first-party identifier to every site a visitor clicks through to, which turns it into something third parties can correlate on — so there is no "decorate everything" option.

The destination adopts the id on init automatically.


API

| | | | ----------------------- | -------------------------------------------------------------------- | | init(options) | Configure and start. Sends a page view unless autoPageView: false. | | setConsent(granted) | Grant or refuse. Granting flushes anything buffered. | | trackPageView(extra?) | A page view, with optional extra params. | | track(event) | { token, revenue?, currency?, params? }. | | decorateUrl(url) | The URL with the visitor id, if it points at a listed domain. | | getVisitorId() | The current id, or null before init. |

InitOptions

| | | | --------------- | -------------------------------------------------------- | | appToken | The app id. Public — it is in the page source by design. | | endpoint | The collector, e.g. https://go.apptrackx.com. | | consent | 'pending' (default), 'granted' or 'denied'. | | stitchDomains | Domains the visitor id may travel to. Empty by default. | | autoPageView | Send a page view on init. true by default. |


Delivery

navigator.sendBeacon first, falling back to XMLHttpRequest.

The beacon matters most in the case that is hardest to observe: a page being unloaded cancels its in-flight XHRs, so the last event of a session — often the one that completes a funnel — is the most likely to be lost. A beacon is handed to the browser and survives the page.

It is not sufficient alone. sendBeacon returns false rather than throwing when the browser declines to queue — the per-origin budget is exhausted, or the payload is too large — so the SDK checks the return value and falls back. Bodies over 60 KB skip the beacon entirely.

Requests are sent with application/json, which makes them non-simple and therefore preflighted. That is deliberate: the preflight is where the collector checks your origin.


Things this does not do

  • No automatic link decoration. A global click handler on someone else's page is a surprise, and single-page apps re-render links constantly.
  • No retry or offline queue. A failed delivery is lost. The native SDKs persist a queue to disk; doing the same here would mean writing behavioural data to localStorage, which the consent model rules out.
  • No web-to-app attribution. P2.ATTR.08 and P2.ATTR.09 are unbuilt, so a visitor moving from your site into your app is not yet joined up.

Development

pnpm --filter @apptrackx/sdk-web test     # 57 unit tests
pnpm --filter @apptrackx/sdk-web build    # esm, cjs, iife + the size budget

The build fails if the iife bundle exceeds 10 KB gzipped. That budget is enforced rather than documented for the reason the Android SDK's 350 KB one is: no single commit adds 10 KB, and by the time anyone measures, getting back under it is a rewrite rather than a revert.

There is no DOM test tooling in this repository — no jsdom, no happy-dom. So every decision lives in a pure module with its own tests (identity, consent, payload, transport, stitch), and index.ts is only the wiring that reads browser globals. Anything that can only be proven in a real browser is proven by running the built bundle in one, which is how the sendBeacon credentials bug was found.