Script tag
On this page
One line, no build step. The build enforces a 12 KiB gzip budget.
<script
defer
src="https://saaspro.dev/spm.js"
data-key="spm_pub_prod_xxxxxxxx"
></script>
Put it in <head>. On load it starts a session, sends a $pageview, follows SPA
route changes, captures errors and web vitals, and exposes window.spm.
Configuration attributes#
| Attribute | Default | Meaning |
|---|---|---|
data-key | — | Required. Your public key |
data-host | https://saaspro.dev | Ingest host |
data-auto-pageviews | on | $pageview on load and route changes |
data-auto-errors | on | window errors and unhandled rejections |
data-vitals | on | LCP, CLS, INP, FCP, TTFB |
data-outbound-links | on | $outbound on cross-host link clicks |
data-data-attributes | on | [data-spm-event] and [data-spm-form] |
data-click-ids | on | Ad click ids and _fbp / _fbc / _ttp cookies |
data-privacy-mode | persistent | ephemeral keeps tracking identity in memory; see privacy modes |
data-cross-domain-origins | unset | Comma-separated exact origins for anonymous journeys; requires matching Data settings |
data-cross-domain-auto-link | on | Decorate ordinary same-tab links to configured journey origins |
data-cookieless | off | localStorage only, no cookies |
data-respect-dnt | off | Disable when the browser sends Do Not Track |
data-consent | granted | pending buffers in memory until you grant |
data-session-timeout-minutes | 30 | Idle window before a new session |
data-batch-size | 20 | Events per automatic flush |
data-flush-interval-ms | 2000 | Flush timer |
data-release data-env data-app-version | — | Stamped on every event |
data-debug | off | Log every queued event |
A bare attribute counts as true:
<script defer src="https://saaspro.dev/spm.js"
data-key="spm_pub_prod_xxxxxxxx"
data-cookieless
data-auto-errors="false"
data-env="production"
data-release="site@2026-09-03"></script>
"", "true" and "1" mean true; anything else means false.
Tracking from your page#
<script>
spm.track("newsletter signup", { source: "footer" });
spm.identify("user_123", { email: "ada@example.com", plan: "pro" });
</script>
Because the tag is deferred, window.spm may not exist yet when your own
inline script runs. Queue calls instead:
<script>
window.spmq = window.spmq || [];
window.spmq.push(["identify", "user_123", { plan: "pro" }]);
window.spmq.push(["track", "hero viewed", { variant: "b" }]);
</script>
Each entry is [methodName, ...arguments]; the queue is replayed in order as
soon as the tag initialises, and spmq.push keeps working afterwards.
Declarative tracking#
No JavaScript needed:
<button
data-spm-event="cta clicked"
data-spm-prop-position="hero"
data-spm-prop-plan="pro"
>
Start free
</button>
<form data-spm-form="signup" action="/subscribe" data-spm-prop-variant="b">
<input name="email" type="email" required />
<button type="submit">Subscribe</button>
</form>
data-spm-prop-plan-name="x" becomes { planName: "x" }. "true" and
"false" are parsed as booleans. The nearest tagged ancestor wins, so wrapping a
whole card in data-spm-event works.
Cookie banner#
<script defer src="https://saaspro.dev/spm.js"
data-key="spm_pub_prod_xxxxxxxx"
data-consent="pending"></script>
<script>
function accept() { window.spm.consent("granted"); }
function reject() { window.spm.consent("revoked"); }
</script>
While consent is pending nothing is sent and nothing is written to storage.
Granting flushes what was buffered. Persistent mode stores identifiers; ephemeral mode keeps them in memory.
Content Security Policy#
script-src 'self' https://saaspro.dev;
connect-src 'self' https://saaspro.dev;
connect-src covers both fetch and navigator.sendBeacon. If you serve the
file from your own origin, point both at it and set data-host to your ingest host.
Verifying#
Open App → Analytics → Live in the console and load your page. A visitor
should appear within a couple of seconds. If not, add data-debug and watch the
browser console — every queued event and every request is logged.