Plain HTML
No framework, no build step, no npm. One script tag.
Minimal page
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Acme</title>
<script
defer
src="https://saaspro.dev/spm.js"
data-key="spm_pub_prod_xxxxxxxx"
></script>
</head>
<body>
<h1>Acme</h1>
</body>
</html>
That is the whole integration: sessions, pageviews, outbound links, errors and
web vitals all start on load. See script-tag.md for every
data- attribute.
Tracking clicks without JavaScript
<button
data-spm-event="cta clicked"
data-spm-prop-position="hero"
data-spm-prop-plan="pro"
>
Start free
</button>
<a
href="/docs"
data-spm-event="nav clicked"
data-spm-prop-target="docs"
>Docs</a>
data-spm-prop-plan-name="x" becomes { planName: "x" } and "true" /
"false" become booleans. The nearest tagged ancestor wins, so you can tag a
whole card:
<article data-spm-event="card clicked" data-spm-prop-id="pricing">
<h2>Pro</h2>
<p>Everything you need.</p>
<button>Choose</button>
</article>
Forms
<form
action="/subscribe"
method="post"
data-spm-form="newsletter"
data-spm-prop-placement="footer"
>
<label for="email">Email</label>
<input id="email" name="email" type="email" required />
<button type="submit">Subscribe</button>
</form>
Submitting sends $form_submit with
{ form: "newsletter", formId: "", action: "/subscribe", placement: "footer" }.
Field values are never captured.
Tracking from your own scripts
Because the tag is deferred, queue calls rather than assuming window.spm
exists:
<script>
window.spmq = window.spmq || [];
window.spmq.push(["track", "hero viewed", { variant: "b" }]);
</script>
Once the page has loaded, call window.spm directly:
<script>
document.getElementById("buy").addEventListener("click", function () {
window.spm.track("purchase started", { plan: "pro" }, { value: 49, currency: "EUR" });
});
</script>
Identifying a signed-in visitor
Render the identity from your server:
<script>
window.spmq = window.spmq || [];
window.spmq.push([
"identify",
"{{ user.id }}",
{ email: "{{ user.email }}", plan: "{{ user.plan }}" },
]);
</script>
and on sign-out:
<script>window.spm && window.spm.reset();</script>
Cookie banner
<script
defer
src="https://saaspro.dev/spm.js"
data-key="spm_pub_prod_xxxxxxxx"
data-consent="pending"
></script>
<div id="banner" role="dialog" aria-label="Cookies">
<p>We use first-party analytics.</p>
<button id="accept">Accept</button>
<button id="reject">Reject</button>
</div>
<script>
document.getElementById("accept").onclick = function () {
window.spm.consent("granted");
document.getElementById("banner").hidden = true;
};
document.getElementById("reject").onclick = function () {
window.spm.consent("revoked");
document.getElementById("banner").hidden = true;
};
</script>
Until consent is granted nothing is sent and nothing is written to storage. If
you would rather never set a cookie at all, add data-cookieless — identifiers
then live only in localStorage.
Multi-page sites
Nothing extra to do: each page load starts with the stored anonymous id and continues the same session for 30 minutes of activity. Put the tag in a shared header partial so it appears on every page.
Content Security Policy
Content-Security-Policy: script-src 'self' https://saaspro.dev; connect-src 'self' https://saaspro.dev;
connect-src covers both fetch and navigator.sendBeacon.
Serving the file from your own origin
<script
defer
src="/assets/spm.js"
data-key="spm_pub_prod_xxxxxxxx"
data-host="https://saaspro.dev"
></script>
Copy node_modules/@saaspro/browser/dist/spm.js into your assets directory
and keep data-host pointing at your ingest host.
Checklist
- The tag is in
<head>withdefer - The key starts with
spm_pub_ - Inline scripts use
window.spmq.push([...]), not barespm.… - CSP allows
connect-src https://saaspro.dev - A visitor appears in App → Analytics → Live when you load the page