Add tracking to a website
One script tag records pageviews, including route changes in single-page apps, and shows a consent banner. Works with any framework.
Prerequisites
- A web property and its key. See Get a project key.
- A privacy page on the site. The banner links to
/privacyunless you setdata-privacy-url.
1. Add the script
Paste before </body>, replacing <project key>:
<script src="https://analyticsapi.specvista.com/api/v1/sdk.js" data-api-key="<project key>" async></script>Next.js App Router, in app/layout.tsx:
import Script from "next/script";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
{children}
<footer>
<a href="#" data-sa-consent-settings>Cookie and analytics settings</a>
</footer>
<Script src="https://analyticsapi.specvista.com/api/v1/sdk.js" data-api-key={process.env.NEXT_PUBLIC_SA_API_KEY} strategy="afterInteractive" />
</body>
</html>
);
}A web key is public by design, so a NEXT_PUBLIC_ variable or a literal key are both fine.
Without a key: if the property's domain is set, leave out data-api-key. The script then identifies the site by its hostname, with www. ignored.
Expected result: the page loads sdk.js with status 200, and a request to https://analyticsapi.specvista.com/api/v1/ingest returns 202.
2. Understand consent
Until a visitor chooses, and after Only essential, the script sends one pageview per route change with only the path, the referrer host, the device type and consent: "essential". These still count as pageviews in reports. Custom events and identify calls are dropped. After Accept analytics and personalisation, the script:
- stores a first-party visitor id in
localStorage["sa_aid"]and the cookie_sa_aid, and a session id insessionStorage; - sends full events with the URL, referrer, browser, operating system, screen size, language, time zone and UTM parameters;
- adds a hashed browser fingerprint, built from canvas and font measurements, to help join visits;
- adds an
_sa_xparameter to links that go to other domains registered on the same account, so a visitor moving between your sites is counted once.
Mention these in your privacy policy.
| Attribute | Default | Effect |
|---|---|---|
data-api-key | none | Project key |
data-domain | the page hostname | Domain to match when there is no key |
data-api-url | https://analyticsapi.specvista.com/api/v1 | API base |
data-privacy-url | /privacy | Link in the banner |
data-consent-mode | ask | essential never shows the banner, only sends essential pageviews and ignores data-sa-consent-settings clicks |
Let visitors change their choice: give any element data-sa-consent-settings, for example a footer link labelled "Cookie and analytics settings".
3. Send custom events and identify people
window.saTrack?.("checkout.completed", { plan: "pro" });
window.saIdentify?.("user_123", { email: "person@example.com", name: "Ada" });Both only send once the visitor has accepted. The functions exist only after sdk.js has run, and calls made earlier are not queued, so use ?.. In React, call them from client components ("use client") or event handlers. For TypeScript, declare them once, for example in types/specvista.d.ts:
declare global {
interface Window {
saTrack?: (event: string, properties?: Record<string, unknown>) => void;
saIdentify?: (userId: string, traits?: Record<string, unknown>) => void;
saConsent?: { open: () => void; choice: () => "full" | "essential" | null };
}
}
export {};Name events object.action in lower case. Never put passwords, payment details, messages or other free text in properties.
Other globals: window.saConsent.open() reopens the banner and window.saConsent.choice() returns "full", "essential" or null. Each change fires a sa:consent event on window with detail.choice.
4. Confirm success
- Open the site in an ordinary browser window, not a headless or automated one. Events are stored within seconds.
- Open the property in the dashboard. While no event has arrived, the page says the property has not sent an event yet and checks again every 5 seconds.
- Or, with a bearer token, call
GET /api/v1/analytics/{projectId}/overviewand check thattotalis above 0. See Read reports.
Common errors
| Symptom | Cause | Fix |
|---|---|---|
ingest returns 401 with code unregistered_sender | Wrong key, or no key and the hostname is not a property's domain | Copy the key again from Properties, or set the property's domain |
sdk.js returns 404 | The URL is missing /api/v1 | Use https://analyticsapi.specvista.com/api/v1/sdk.js |
| Requests return 202 but reports stay empty | Your own browser is ignored. The dashboard header has a switch that reads Track my visits while your browser is tracked and Ignoring my visits while it is ignored; clicking it toggles an sa_ignore cookie. New browsers are tracked | Open https://analyticsapi.specvista.com/api/v1/ingest/ignore-status in that browser. If tracking is false, click Ignoring my visits in the dashboard header so it reads Track my visits |
| Events from automated tests never appear | Headless browsers, Playwright, Puppeteer, Selenium, curl and similar clients are stored as bot traffic and left out of reports | Test in an ordinary browser |
| Visits from Apple's offices are missing | Requests from Apple's network (17.0.0.0/8) are stored as internal, mainly to keep App Review out of reports | Expected; this applies to every property |
saTrack calls do nothing | The visitor has not accepted analytics | Accept in the banner, or check window.saConsent.choice() |
| No script request at all | A content blocker or a Content Security Policy blocks it | Allow analyticsapi.specvista.com in script-src and connect-src |