# 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](https://analytics.specvista.com/docs/get-a-project-key.md).
- A privacy page on the site. The banner links to `/privacy` unless you set `data-privacy-url`.

## 1. Add the script

Paste before `</body>`, replacing `<project key>`:

```html
<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`:

```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 in `sessionStorage`;
- 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_x` parameter 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

```js
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`:

```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

1. Open the site in an ordinary browser window, not a headless or automated one. Events are stored within seconds.
2. 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.
3. Or, with a bearer token, call `GET /api/v1/analytics/{projectId}/overview` and check that `total` is above 0. See [Read reports](https://analytics.specvista.com/docs/read-reports.md).

## 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` |
