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

# Consent & deferred mode

> How ABTestly respects EU ePrivacy / GDPR consent signals — and how to wire it to your CMP.

ABTestly supports two consent modes, configurable per site:

* **`always_on`** (default) — the snippet runs on every pageload regardless of consent state. Right for sites where ABTestly's purpose qualifies as strictly necessary, or sites without an EU/UK audience that need ePrivacy compliance.

* **`deferred`** — the snippet does **nothing observable** until your consent management platform (CMP) signals consent has been granted. No network requests, no `localStorage` writes, no cookies, no DOM mutations. When consent is granted mid-session, the snippet wakes up and runs normally.

Pick `deferred` if you're under EU/UK ePrivacy, GDPR, or any regime that requires prior consent before non-essential device storage or third-party requests.

## The guarantee, in writing

In **deferred mode** with consent **not granted**, the ABTestly inline snippet — the one you paste in your `<head>` — performs **zero** of the following:

| Side effect                                                                  | Pre-consent (deferred) |
| ---------------------------------------------------------------------------- | ---------------------- |
| Network request to `cdn.abtestly.com` (fetching the runtime)                 | ❌ Not made             |
| Network request to `api.abtestly.com` (analytics, error reporting)           | ❌ Not made             |
| `localStorage` write (uid, variant cache, version stamps)                    | ❌ Not made             |
| Cookie set (`__abtestly_uid`)                                                | ❌ Not made             |
| DOM mutation (anti-flicker hide class when enabled, variant CSS, variant JS) | ❌ Not made             |
| `window.dataLayer.push` (GA4 export)                                         | ❌ Not made             |
| `window.__abtestly` API exposure (`onApply`, `onCleanup`, `trackGoal`)       | ❌ Not made             |
| SPA `pushState`/`popstate`/`hashchange` listener wrapping                    | ❌ Not made             |
| `window.error` / `unhandledrejection` listener attached                      | ❌ Not made             |

The only thing the inline snippet does pre-consent is run a tiny polling check (`setInterval`) on the consent property you configured. That's a local JS timer — not observable to anything outside the page.

When `window[consentKey] === 'granted'` is observed, the snippet then performs the **same full flow** an `always_on` site would have performed at pageload — fetch the runtime, bucket the visitor, apply the variant, record the exposure. Exactly once, even if consent flips on and off again.

## Configure your site

In **Settings → SPA & consent** for your site:

* **Mode:** `deferred`
* **Consent key:** the `window.<name>` property your CMP writes when consent for analytics/A-B testing is granted. Example: `abtestly_consent`, `analytics_storage`, `cmp_consent_status`.

Save the site, then **re-copy the snippet** from the Install panel and replace the one in your `<head>`. The snippet is parameterized at copy time — switching modes requires a fresh copy.

## Wire your CMP

After ABTestly is configured for deferred mode and the snippet is installed, point your CMP's grant callback at the configured key:

### Generic example

```html theme={null}
<script>
  // When your CMP / banner grants consent:
  window.abtestly_consent = 'granted';
</script>
```

That's it. The snippet's poller (running at 200ms intervals) observes the value, clears itself, fetches the runtime, and runs the experiment flow exactly once.

### OneTrust example

```html theme={null}
<script>
  window.OptanonWrapper = function () {
    // Categorize ABTestly under "Performance" or whichever cookie category fits
    // your DPO's classification — example uses "C0003" for Performance.
    if (window.OnetrustActiveGroups && window.OnetrustActiveGroups.indexOf('C0003') >= 0) {
      window.abtestly_consent = 'granted';
    }
  };
</script>
```

### Osano example

```html theme={null}
<script>
  if (window.Osano) {
    Osano.cm.addEventListener('osano-cm-consent-saved', function (consent) {
      if (consent.ANALYTICS === 'ACCEPT') {
        window.abtestly_consent = 'granted';
      }
    });
  }
</script>
```

### CookieYes example

```html theme={null}
<script>
  document.addEventListener('cookieyes_consent_update', function (e) {
    if (e.detail && e.detail.accepted && e.detail.accepted.indexOf('analytics') >= 0) {
      window.abtestly_consent = 'granted';
    }
  });
</script>
```

### Custom in-house consent

```html theme={null}
<script>
  document.querySelector('#accept-cookies').addEventListener('click', function () {
    window.abtestly_consent = 'granted';
    // ...your own consent persistence
  });
</script>
```

## The anti-flicker trade-off

Anti-flicker is opt-in — a checkbox when you copy your snippet, unchecked by default. This section only applies if you ticked it. If you didn't, the page is never hidden and nothing here changes your consent posture.

With anti-flicker enabled in `always_on` mode, the snippet hides the page (`opacity: 0`) as soon as it runs, while it decides which variant to apply, and reveals after 2 seconds regardless.

With anti-flicker enabled in `deferred` mode, we still **don't hide the page** until consent is granted. The legally cleanest posture is to do nothing pre-consent, which means visitors who eventually grant consent may see a brief flash of original content (FOOC) when the variant applies. This is the intentional trade-off: zero pre-consent action in exchange for a momentary flicker after grant. If your CMP grants consent very quickly (i.e. you have a "consent already given" cookie from a prior visit), the flicker is imperceptible.

## What about "granted at load"?

If your CMP has persisted consent from a previous visit and sets `window[consentKey] = 'granted'` synchronously BEFORE the ABTestly inline snippet runs, ABTestly detects this on first execution and behaves exactly like `always_on` — no polling delay, no flicker beyond what always\_on visitors already see.

This is the common case for returning visitors with a consent cookie. Make sure your CMP runs **before** the ABTestly snippet in your `<head>`, OR write the consent property from a synchronous inline script before ABTestly's snippet.

## What about revocation?

If a visitor grants consent, ABTestly runs, then they later revoke consent in the same session: ABTestly does **not** undo what it already did (uid in localStorage, variant rendered, impression recorded — those are already there). It also does **not** start any new side effects after revocation; the next pageload in the revoked state will treat the visitor as not-granted from the start.

Cleaning up data already written is out of scope for the snippet. Customers who need full data deletion on revocation should clear `localStorage.__abtestly_uid`, the `__abtestly_uid` cookie, and any `__abtestly_v_*` keys themselves — or contact us about a `right-to-erasure` endpoint at `support@abtestly.com`.

## Verifying the guarantee yourself

Before relying on deferred mode for compliance, verify the contract holds on your site:

1. Configure deferred mode + your consent key.
2. Open a fresh incognito window. Open DevTools → **Network** + **Application** tabs.
3. Load a page on your site. Confirm in the Network tab that **no request** to `cdn.abtestly.com` or `api.abtestly.com` is made. Confirm in Application → Local Storage that **no `__abtestly_*` keys** exist.
4. Trigger your CMP to grant consent. Within \~200ms you should see:
   * Network request to `cdn.abtestly.com/s/<your-site-id>.js`
   * `__abtestly_uid` in Local Storage
   * One exposure beacon to `api.abtestly.com/e` per running experiment that targets the page
5. Refresh the page (consent now persisted). Observe that the snippet runs immediately on pageload — no 200ms polling delay.

If any of steps 3 fail (network or storage activity appears before grant), file a ticket at `support@abtestly.com` — that's a contract violation and we treat it as a P0.

## What happens if you select deferred but forget the key?

The snippet **fails closed**. It does nothing — no fetch, no hide class, no storage. Your DevTools console will show a clear error:

> `[abtestly] deferred consent mode is configured but consentKey is empty — the snippet will NOT run until a key is set in your site settings.`

We deliberately chose fail-closed over fail-open here. You selected `deferred` because you wanted consent gating; honoring that intent (by tracking nothing) is safer than silently reverting to `always_on` (tracking everything pre-consent), which would be the exact opposite of what you asked for.

To fix: Settings → SPA & consent → set a non-empty Consent key → save → re-copy the snippet from the Install panel → replace it in your `<head>`.

## Default key choice

If you're unsure what key to use, we recommend **`abtestly_consent`** — vendor-specific so it doesn't collide with another tool's consent property, and obvious in what it's signalling.

```html theme={null}
<!-- Once granted: -->
<script>window.abtestly_consent = 'granted';</script>
```

## Need help?

Email `support@abtestly.com` with your CMP name and we'll help wire it up. If your CMP supports the [IAB TCF v2.2 framework](https://iabeurope.eu/transparency-consent-framework/), there's a standard way to map a TCF consent signal to `window[consentKey] = 'granted'` — we can provide a snippet.

***

## Start testing

<Card title="Create a free ABTestly account" icon="flask" href="https://app.abtestly.com/sign-up" horizontal>
  The free tier covers 3,000 monthly tracked users and one active experiment, no credit card. Edge-served snippet, with sample size and confidence intervals shown on every result.
</Card>
