Skip to content

Tracking script reference

The Statable tracking script is a small JavaScript bundle you embed once on every page you want to measure. It auto-tracks pageviews, engagement, scroll depth, outbound clicks, file downloads, and exposes window.statable.t() for custom events.

This page is the canonical reference for the bundle, HTML attributes, and install patterns. For the JavaScript API see JavaScript API. For wire format see Event payload reference.


The bundle

s.js, about 2 KB over the wire. Auto-tracks pageviews, engagement time, scroll depth, outbound link clicks, file downloads, form submits, and exposes window.statable.t() for custom events.

What a given s.js actually does depends on the modules enabled for that site under Site settings → Tracking Code. The list above is the full set; the Nano preset ships pageviews and SPA navigation only.

Embeddable widgets ship as their own bundles: luw.js (Live Users), mw.js (Visitor Map), gw.js (3D Globe), tcw.js (Top Countries). gp.js is a frozen legacy globe, still served so old embeds keep working. Documented under Embeddable widgets, not here.

The size a browser downloads depends on how the file is compressed on the way. Every bundle is prepared in three formats, zstd, brotli and gzip, and served through the Bunny CDN, which picks one per browser from the formats that browser says it can read, so no browser ever receives a format it cannot unpack. Current Chrome, Edge, Firefox and Safari 26.3 or later, roughly three visitors in four, get our zstd: 2,226 bytes for the Standard s.js as of September 2026. Older Safari and some in-app browsers get our brotli, 1,985 bytes. Brotli is smaller, but Bunny does not pass it to a browser that also accepts zstd, and the CDN offers no setting to change that. A client that accepts only gzip gets gzip compressed by Bunny itself, 2,251 bytes. To see which one your browser received, open DevTools → Network, select s.js and check the content-encoding response header.


Installation

Place the snippet in <head> with defer. defer ensures the script doesn't block HTML parsing and runs after the document is parsed.

<script defer src="https://statable.com/js/YOUR_SITE_ID/s.js"></script>

Replace YOUR_SITE_ID with the numeric Site ID from Site settings → Tracking Code. The script reads the ID from the URL path, so no data-id attribute is required on paid plans.


URL structure

PathUsed for
/js/{site_id}/s.jsStandalone tracker (paid plans)
/js/{hash}/{widget}.jsStandalone widget (paid plans) — gw, luw, mw, tcw
/js/{hash}/t/{widget}.jsWidget + tracker bundle (Hobby plan; tracker is bundled inside the widget)

The Hobby plan has no separate tracker — pick a widget and the install snippet is generated from /api/site/script-url.


Attributes reference

AttributeRequiredDefaultDescription
srcyes(none)URL of the bundle. Site ID is encoded in the path.
deferrecommended(none)Defer script execution until HTML is parsed
data-idnoderived from srcNumeric Site ID. Needed when the script URL doesn't carry it: Hobby widget bundles (/t/...) and a copy of s.js served from your own origin.
data-tracking-apino<script-origin>/api/eventOverride the ingest endpoint (proxy / self-hosted)
data-before-sendno(none)Name of a global function that mutates props before send
data-statable-{key}no(none)Sticky custom property attached to every event from this page load

data-tracking-api (optional)

By default the script posts events to <script-origin>/api/event. If your script is loaded from https://statable.com/js/YOUR_SITE_ID/s.js, the endpoint is https://statable.com/api/event.

Set it when the script is served from somewhere other than Statable, so events still go straight to Statable:

<script defer
        src="https://example.com/statable/s.js"
        data-id="YOUR_SITE_ID"
        data-tracking-api="https://statable.com/api/event"></script>

Events can't be relayed through your server

Statable reads the visitor's IP from the connection that reaches it. Events forwarded by your own server arrive with that server's IP, so every visitor gets its location, and visitors on the same browser merge into one. A copy of s.js on your own origin also misses changes to tracking features until you copy it again.

The endpoint must accept POST with Content-Type: text/plain and a JSON body. Exact shape: Event payload reference.


data-before-send (optional)

Enrich, redact, or sample pageviews before they leave the browser. The value is the name of a global function (not an inline expression). The function receives the current props object and must return the modified object, or false to drop the pageview.

<script>
  function statableEnrich(props) {
    // Attach the current user ID, plan, and feature flags
    if (window.currentUser) {
      props.userId = window.currentUser.id;
      props.plan = window.currentUser.plan;
    }
    return props;
  }
</script>

<script defer
        src="https://statable.com/js/YOUR_SITE_ID/s.js"
        data-before-send="statableEnrich"></script>

Fires once per pageview, called with merged custom props (data-statable-* attributes + any per-call props). If the function throws, the script swallows the error silently and continues with unmodified props.


data-statable-* (optional)

Any attribute prefixed with data-statable- becomes a sticky custom property attached to every event sent from this page load. Useful for cohort, environment, or experiment tags.

<script defer
        src="https://statable.com/js/YOUR_SITE_ID/s.js"
        data-statable-cohort="beta"
        data-statable-env="production"
        data-statable-app-version="2024.4.28"></script>

Keys are forwarded verbatim (lowercased after the prefix), so the example produces:

{ "cohort": "beta", "env": "production", "app-version": "2024.4.28" }

These props are merged before data-before-send runs, so your hook can override or remove them.


SPA support

The script wraps history.pushState, history.replaceState, and listens for popstate / pageshow (back-forward cache). No configuration required. Pageviews fire automatically on client-side navigation in React Router, Vue Router, Next.js, SvelteKit, Astro, and any router that uses the History API.

If your router doesn't use history.pushState (rare), call window.statable.t('pageview') manually after each transition.


Performance impact

  • Bundle: ~2.4 KB over the wire (s.js — 5140 B uncompressed; served zstd 2457 B, brotli 2181 B, gzip 2539 B).
  • Loaded with defer: zero render-blocking, runs after DOMContentLoaded.
  • Network calls use fetch with keepalive: true so unload events don't delay navigation.
  • The inactivity timeout runs in a Web Worker, so a backgrounded tab still closes its session on time when the browser throttles ordinary timers. Engagement time itself is arithmetic on the main thread.
  • Scroll tracking uses requestAnimationFrame. Fires at most once per frame.

See also


Ready to take control of your web analytics? Try Statable free for 30 days. No credit card required, full feature access, built for GDPR. Start your free trial or view a live demo.