Script tag
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.