@apptrackx/sdk-web
v0.1.0
Published
AppTrackX Web SDK — install attribution, page views and event tracking for web properties
Maintainers
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-webimport { 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 siteVersioning
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.08andP2.ATTR.09are 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 budgetThe 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.
