> ## Documentation Index
> Fetch the complete documentation index at: https://docs.stage.systems/llms.txt
> Use this file to discover all available pages before exploring further.

# Tracking

> How a page view becomes a row, what the site includes to make that happen, the consent gate, the engagement beacon, and the hooks a site can call.

Stage's analytics are first-party and recorded at the edge. A site does three things: re-exports the SDK middleware, includes the SDK's inline scripts in its base layout, and renders a consent banner wired to the SDK's consent API. The template ships all three.

## Where a page view is recorded

The middleware, not a browser script. For a top-level document request it classifies the user agent (human, known bot, AI crawler), applies the consent rule, reads the session cookie, the UTM parameters (from the URL, else from the campaign cookie), the referrer and the path, and posts the visit to the API's ingest route after the response is sent. The API derives the org from the verified origin and writes the row. Nothing in the edge or the browser holds a database key.

Excluded: API paths, assets, non-document fetches, and visitors carrying the admin's preview cookie.

## Sessions

The `s` cookie: first-party, 30 minutes, `SameSite=Lax`, readable by script because the beacon and forms send it. A campaign landing sets the same value before redirecting, so the click and the landing share a session. Forms and the contact overlay send it with a submission as the anonymous id, which is how an enquiry joins its visits.

## Consent

The middleware decides the region from the edge's country header (EU, EEA, UK). For those visitors nothing is written until a `consent=1` cookie exists; `consent=0` writes nothing ever. Elsewhere views are recorded immediately. Bots and AI crawlers are recorded regardless.

The SDK ships the decision core; the banner is the site's. In the base layout:

```astro theme={null}
<ConsentBanner audience={consentAudience} />
<Fragment set:html={consentActivationScript({ bannerId: "consentBanner", audience: consentAudience })} />
```

| Hook                                   | Does                                                                                                                         |
| -------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| `window.__stageConsent.accept(prefs?)` | Sets the consent cookie (one year) and activates gated scripts                                                               |
| `window.__stageConsent.decline()`      | Sets `consent=0`                                                                                                             |
| `geo=eu` cookie                        | Short-lived flag the middleware sets so a static page knows whether to show the banner. `audience="all"` shows it everywhere |

Google Analytics, if configured, goes through `gaScript(gaId, { gated })`. Gated, it ships inert and makes no connection until acceptance.

## Engagement

`engageBeaconScript()` in the base layout collects, per page: seconds on page, maximum scroll depth, a click count per `data-track` value, and the session. It sends once on `visibilitychange` to hidden via the beacon API, skipping trivial hits. The API updates the existing visit row: scroll depth clamped to 0 to 100, time to an hour. It can only update a row that exists, which is why the beacon needs no consent check of its own.

| Hook                       | Use                                                                                                                                                                                                                                                                                                                                                          |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `data-track="nav:contact"` | Name any element. Clicks on it are counted per name and surface as the most-clicked element                                                                                                                                                                                                                                                                  |
| `window._engageSend()`     | On a site that navigates without full page loads, call it before changing the path. It sends the engagement for the page being left and resets for the next. It does **not** create a page view: views are recorded per full document request, so a client-routed site records one view per full load. Use full navigations for Stage pages you want counted |

## What is never stored

The raw address is used in transit to derive two keys and dropped. `ip_key` is a keyed hash scoped to the org with no date, stable over time, for joining a returning visitor to a later enquiry. `ip_hash` rotates daily and only deduplicates within a day. No fingerprinting, no third-party cookies.

## Reading the numbers

The Performance tool asks the API for one payload per range. The API serves a per-org cache and refreshes in the background when older than 15 minutes. The database aggregation functions are executable only by the API. The "on site now" count comes from a small set of indexed counts polled every 25 seconds. Day buckets are on a fixed calendar timezone in the current release.

## Next steps

<CardGroup cols={2}>
  <Card title="Campaign landing" href="/developers/analytics/campaign-landing">
    The 30-day attribution cookie.
  </Card>

  <Card title="Custom funnels" href="/developers/analytics/custom-funnels">
    Your own stages.
  </Card>
</CardGroup>
