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

# Consent mode

> How Traceten respects cookie consent signals from OneTrust, Cookiebot, IAB TCF v2.2, and GPC.

Traceten uses no local storage, session storage, or IndexedDB. It uses only first-party cookies, and only once consent is resolved, and it integrates with your existing consent management platform (CMP). No configuration is required for supported CMPs: the snippet detects them automatically.

## How it works

The snippet uses a four-state consent machine:

| State     | Trigger                             | Cookies written | Events fired |
| --------- | ----------------------------------- | --------------- | ------------ |
| `pending` | Page load, CMP not yet resolved     | No              | No           |
| `granted` | CMP or GPC grants analytics consent | Yes             | Yes          |
| `denied`  | CMP or GPC denies analytics consent | No              | No           |
| `no_cmp`  | No CMP detected after 300 ms        | Yes             | Yes          |

On each page load the snippet:

1. Checks Global Privacy Control (GPC) immediately. If set, state is `denied` permanently.
2. Looks for a loaded CMP (TCF v2.2, OneTrust, Cookiebot).
3. Waits up to 300 ms for any CMP that loads via a tag manager after the snippet.
4. If no CMP resolves: falls back to `no_cmp` (cookies written, DNT still honoured).

<Note>
  **EU sites without a supported CMP:** if you serve EU/EEA visitors and use a custom cookie banner,
  you must use the [manual integration](#manual-integration) below. Otherwise the snippet enters
  `no_cmp` mode and writes cookies before consent is obtained, which is an ePrivacy violation.
</Note>

## Supported CMPs

### IAB TCF v2.2

Detected automatically via `window.__tcfapi`. The snippet waits for the consent string to be ready and checks IAB Purpose 1 (Store and/or access information on a device).

If `gdprApplies === false` (visitor is outside GDPR jurisdiction), the snippet falls back to `no_cmp`.

**No configuration required.**

### OneTrust

Detected via `window.OnetrustActiveGroups`. Analytics consent maps to group `C0002` (Analytics Cookies).

**No configuration required.**

### Cookiebot

Detected via `window.Cookiebot.consent.statistics`. Consent is granted when `statistics === true`.

**No configuration required.**

### Global Privacy Control (GPC)

Detected via `navigator.globalPrivacyControl`. If `true`, the snippet immediately enters `denied` and no events are ever fired for that visitor. Required under CPRA (California).

**No configuration required.**

## Consent flip

If a visitor initially denies but later clicks "Accept All", the snippet detects the change via the CMP callback and:

1. Writes `_traceten_sid` and `_traceten_vid` cookies.
2. Fires a single `session_start` event with the anonymous session ID from before consent, so the backend can join pre-consent traffic to the now-identified visitor.

## Manual integration

If you use a custom cookie banner (not OneTrust, Cookiebot, or a TCF-compliant CMP), you need two separate pieces of integration. They control different things, so do both.

### 1. Gate tracking until consent (required for EU/EEA visitors)

Set `consentDefault: "pending"` on the pre-load stub, before the snippet script tag. This tells the snippet to skip CMP auto-detection and the 300 ms `no_cmp` fallback entirely, and hold all tracking (no cookies, no events) until your banner explicitly resolves it:

```html theme={null}
<script>
  window.traceten = window.traceten || {
    q: [],
    track: function () {
      (this.q = this.q || []).push(arguments);
    },
  };
  window.traceten.consentDefault = "pending";
</script>
<script
  async
  src="https://cdn.traceten.com/tt.min.js"
  data-site="ttid_7Rb4TrC1dTbnD8w3s1TS12"
  data-cookie-domain="example.com"
></script>
```

Both values come from your site's install page. `data-site` is your site key and `data-cookie-domain` is your own domain, so do not paste `example.com`: a value the page does not belong to is ignored and cookies quietly stay host-only.

Then, inside your banner's own Accept/Reject handlers, call the snippet's public `resolveConsent()`:

```html theme={null}
<script>
  function onAccept() {
    window.traceten.resolveConsent("granted");
  }

  function onReject() {
    window.traceten.resolveConsent("denied");
  }
</script>
```

`resolveConsent("denied")` writes a 30-day first-party opt-out cookie (exempt under ePrivacy Recital 66 as a user-initiated preference) so the visitor isn't re-prompted every page load, and drops any queued events. `resolveConsent("granted")` starts tracking and, if the visitor had previously denied, fires a single `session_start` so pre-consent and post-consent traffic can be joined.

<Warning>
  `window.traceten.resolveConsent` is only defined once the snippet script has finished loading. If
  your banner can be accepted before that (a fast click on a slow connection), queue the call
  yourself and replay it from the script tag's `onload` handler. The pre-load stub above does not
  know how to queue `resolveConsent` the way it queues `track()`.
</Warning>

Without this step, a custom banner with no `consentDefault` set gets no gating at all: the snippet finds no CMP, waits 300 ms, and falls back to `no_cmp` (tracking on) regardless of what your visitor chose in your banner.

<Warning>
  Denial must be re-asserted on every page load. The opt-out cookie that `resolveConsent("denied")`
  writes suppresses the persistent visitor cookie and the higher-entropy device signals, but it does
  not feed back into the consent state. On the next page load the state starts as `pending` and
  falls to `no_cmp` after 300 ms unless your banner calls `resolveConsent("denied")` again. If you
  are relying on consent state to honor a GDPR Article 21 objection, make sure your banner
  re-asserts the denial on every load, not just the one where the visitor clicked. See [Opt-out and
  Do Not Track](/privacy/data-collected#opt-out-and-do-not-track).
</Warning>

### 2. Unlock fingerprint signals (optional, additive)

Separately from the gate above, you can opt a visitor into two additional detection signals (canvas entropy, WebGL) once they've explicitly granted consent:

```html theme={null}
<script>
  function onAccept() {
    window.traceten.resolveConsent("granted");
    window.traceten.consent = "granted";
  }
</script>
```

<Warning>
  `window.traceten.consent = "granted"` only unlocks the fingerprint signals above. It is **not** a
  substitute for `resolveConsent()` and does not gate event firing on its own. Skipping it does not
  break tracking; it means detection runs without those two signals for that visitor.
</Warning>

If your site spans multiple subdomains and you want this consent decision to carry across all of them, see [Consent across subdomains](/install/subdomains#consent-across-subdomains). The same broadening has a real tradeoff, so it is documented there rather than repeated here.

## Privacy guarantees

* No persistent cookies (`_traceten_sid`, `_traceten_vid`, `_traceten_cart`) are written while state is `pending` or `denied`.
* GPC is always honoured, regardless of CMP configuration, and it cannot be overridden.
* All consent detection is read-only: the snippet never modifies your CMP state.
* Canvas entropy and WebGL signals are never collected in `pending` or `denied` states.

For a full list of what data is collected, see [Data collected](/privacy/data-collected).

## Testing your integration

1. Open your browser's **Network** tab and filter by `ingest.traceten.com`.
2. **Consent denied:** no requests should appear.
3. **Consent granted:** a `pageview` event should appear within a few hundred milliseconds.
4. **Consent flip (denied → granted):** a `session_start` event should appear, followed by a `pageview`.
