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

# Storefront Tracker

> The client-side tracking contract: events, anonymous identity, view dedupe, consent gating, transport, and the Live View Counter block.

Tracking ships as a theme app extension with two blocks: an **app embed**
(the tracker, enabled once per theme) and the **Live View Counter** section
block (added to the product template). The embed collects events; the counter
only reads an aggregate count and tracks nothing itself. Merchants enable both
in the theme editor — see [Theme setup](https://topten.mintlify.site/getting-started/theme-setup.md).

## Event types

| Event | Trigger | Payload |
| - | - | - |
| `view` | Once per product page load per tab, after consent resolves | session id, product id or handle, variant id, device |
| `heartbeat` | Every 5 s while the tab is visible; also immediately when a tracked product page is revisited in the same tab or the tab becomes visible again | session id, product id or handle, device |
| `add_to_cart` | A cart mutation increases a line quantity (detected by diffing cart state before and after the request) | product id, variant id, quantity delta, cart token |
| `remove_from_cart` | A cart mutation decreases a quantity or removes a line | product id, variant id, quantity delta, cart token |

Cart detection wraps both `fetch` and `XMLHttpRequest` for cart mutation URLs,
then compares the cart state before and after the call. Legacy non-AJAX form
posts to cart add URLs are beaconed optimistically. Heartbeats are skipped
when no product identity exists on the page.

## Anonymous identity

* The session id is a client-generated UUID stored in `localStorage` and
  regenerated after **30 minutes of inactivity**. Pages that block storage
  fall back to a per-page random id.
* Product identity is the numeric product id when the theme exposes it,
  otherwise the URL handle from `/products/<handle>`. The variant id is
  attached when present.
* Device class (`mobile`, `tablet`, `desktop`) derives from the user agent.
* Referrer host and UTM parameters are attached **only to the first event of a
  session**; later events carry no source data.

## View dedupe (two layers)

1. **Client, per tab** — a `sessionStorage` flag records that this tab already
   reported a view of this product. The flag is written only after the send
   actually succeeds, so a blocked or failed send stays retryable.
2. **Server, per shop + session + product** — a configurable window (default
   **30 minutes**, merchant-adjustable 5–1440 in Settings) prevents duplicate
   view events. The server-side window is what keeps counts honest across
   reloads and repeated navigation.

## Consent gating

Collection is gated by the Shopify Customer Privacy API and only runs when
analytics processing is allowed:

* At boot the tracker waits up to \~4 s for the consent API, polling and
  explicitly loading the consent feature if needed.
* A `visitorConsentCollected` subscription restarts evaluation when the
  visitor answers the banner, with a slow fallback re-check for late flips.
* **Every** outgoing batch re-checks consent before sending.
* A first-party opt-out flag in `localStorage` (`_lpi_opt_out = 1`) blocks
  sending regardless of the API state.
* Storefronts without the consent API (unmanaged) proceed; an API error also
  fails open rather than breaking the page.

```mermaid theme={null}
sequenceDiagram
  participant B as Browser (tracker)
  participant C as Customer Privacy API
  participant P as App proxy
  B->>C: boot: wait up to 4 s for consent state
  alt analytics allowed
    C-->>B: allowed
    B->>B: ensure session UUID (localStorage, 30 min TTL)
    B->>P: POST view event
    loop while tab visible
      B->>P: POST heartbeat every 5 s
    end
  else denied or opted out
    C-->>B: not allowed
    B->>B: stay silent; subscribe to consent changes
  end
  C-->>B: consent flips to allowed
  B->>P: POST view event (retryable - flag only written on success)
```

## Transport

* Events are batched (`POST`, JSON body `{ "events": [...] }`, up to **25 per
  batch**) to the app-proxy endpoint with the shop domain as a query parameter.
* Non-cart events are debounced \~900 ms; cart events flush immediately.
* `pagehide` and tab-hide flush the queue via `sendBeacon`; the request is
  `keepalive` and same-origin.
* Failed flushes retry up to 5 times with increasing backoff, then drop the
  queue rather than retry forever.

> [!NOTE]
>
> The app proxy strips cookies, so requests carry no server-side identity —
> the HMAC-signed `shop` parameter and the anonymous session UUID are all the
> identity that exists.

## Live View Counter block

* Polls the [stats endpoint](https://topten.mintlify.site/architecture/app-proxy-api.md) immediately, once
  more after \~3 s (so the visitor's own first heartbeat is counted), then every
  5 s; it also refreshes when the tab becomes visible or the section
  re-renders.
* Renders **only when the response is `visible: true` and `count > 0`**; in
  every other case it hides itself. It never displays zero or a placeholder
  number.
* Supports singular and plural wording automatically. The label text and the
  dot, text, and background colors are merchant-configurable in the theme
  editor; the label default is "people are viewing this right now".
* Per-product suppression and the minimum-views threshold are enforced
  server-side (see [Settings](https://topten.mintlify.site/settings.md)), so the block cannot be tricked into
  showing a suppressed product.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.