---
url: /site/tracking-and-consent.md
---
# Tracking and consent

Evolve renders one tag manager snippet, a Google Consent Mode default, an optional cookie banner, and sends two conversions from the server. Everything else about analytics and ads lives in the GTM container. This page is the webmaster's view: what fires where, which keys control it, and how to check. The container itself is on [Google Tag Manager](/integrations/google-tag-manager).

## What renders on a page

In the theme's head partial, in this order:

1. **Site verification** meta tags for Google, Bing and Pinterest, on every environment.
2. **Consent Mode default**: a small inline script that defines `dataLayer`, reads the `evolve_consent` cookie and calls `gtag('consent', 'default', ...)` with the visitor's state or the policy default. It renders on every environment, so the consent state is always defined before any tag.
3. **The GTM snippet**, only when the site emits tracking and `GTM_ID` is set.
4. **Hotjar**, only when the site emits and `HOTJAR_SITE_ID` is set.
5. **Legacy globals** (`window.GOOGLE_ANALYTICS_TRACKING` and friends), only when emitting and `TRACKING_LEGACY_VARIABLES=true`. Deprecated, for projects still on the pre-GTM adapter bundle.
6. **The consent platform script** (Cookiebot), last in the head partial, only when a [consent platform driver](#external-consent-platforms) and its ID are set. It renders on every environment, like the Evolve banner.

No GA4, Meta, LinkedIn or Pinterest script is loaded by Evolve. Those tags live in the GTM container and fire on the events the theme pushes to the `dataLayer`.

## When a site emits

Tracking follows the [environment type](/getting-started/environments) since 2.11: live emits, staging and development stay silent. A silent site still renders the consent script and the verification tags, but no GTM snippet, no Hotjar, no legacy globals, and the server sends no conversions. The same IDs can therefore sit in every `.env` without test traffic reaching the customer's properties.

| Variable | Default | Effect |
|---|---|---|
| `TRACKING_EMIT` | unset | `true` makes a staging site fire for a test run, `false` silences a live site. Unset lets the type decide. A developer sets it in the Developers tab. |

The tracking health check fails on live when GTM or GA4 is missing or malformed or `TRACKING_EMIT=false` silences the site, and warns when `TRACKING_EMIT=true` is left on staging. `HEALTH_CHECK_TRACKING=false` turns the check off for a site without tracking on purpose.

## Platform IDs

Public IDs are edited in the panel under **Settings > Tracking**. Since 2.18 the panel saves them to the [settings store](/panel/settings-store); the `.env` variable of the same setting is the fallback for an empty field, and the field's placeholder shows it. The GTM container must look like `GTM-…` and the GA4 ID like `G-…`, otherwise nothing is saved.

| Setting | `.env` fallback | Feeds |
|---|---|---|
| GTM container | `GTM_ID` | the snippet |
| GA4 measurement ID | `ANALYTICS_MEASUREMENT_ID` | server-side GA4 events, legacy globals, health check |
| Meta pixel | `FACEBOOK_PIXEL_ID` | server-side Meta events, legacy globals |
| Pinterest tag | `PINTEREST_TAG_ID` | legacy globals |
| LinkedIn partner ID, lead conversion ID | `LINKEDIN_PARTNER_ID`, `LINKEDIN_LEAD_CONVERSION_ID` | legacy globals |
| Hotjar site ID | `HOTJAR_SITE_ID` | the Hotjar snippet |
| Conversion data | `ANALYTICS_CONVERSION_DATA` (`goals`) | which analytics figures the panel dashboard shows |

Secrets never enter the store: `ANALYTICS_API_SECRET` and `FACEBOOK_CONVERSIONS_API_KEY` stay in the `.env`.

## Consent

Evolve implements Google Consent Mode v2 with two categories, analytics and marketing, mapped to `analytics_storage` and `ad_storage`, `ad_user_data`, `ad_personalization`.

| Variable | Default | Effect |
|---|---|---|
| `TRACKING_CONSENT_POLICY` | `opt-out` | `opt-out`: both categories granted until the visitor declines, the Swiss default under the DSG. `opt-in`: both denied until the visitor accepts, as the GDPR requires; tags wait up to 500 ms for the update. |
| `APP_COOKIE_CONSENT` | `false` | Show the Evolve cookie banner. |
| `TRACKING_CONSENT_VIA_CMP` | `false` | A consent management platform (Cookiebot, Usercentrics and the like) runs in the GTM container and sends the consent update (2.18). |
| `TRACKING_CMP` | unset | A consent platform Evolve loads itself and bridges into its own consent cookie. `cookiebot` is the only driver (2.19). Counts as a consent platform, like the option above. |
| `COOKIEBOT_ID` | unset | The domain group ID from the Cookiebot manager, used with `TRACKING_CMP=cookiebot` (2.19). |

All of them sit on the Consent card of **Settings > Tracking** (since 2.18; the banner switch was on the General tab before) and are saved to the settings store as `consent_policy`, `cookie_banner`, `consent_via_cmp`, `consent_cmp` and `cookiebot_id`. The variables are the fallback.

### Opt-in needs somebody who grants it&#x20;

With `opt-in`, Consent Mode starts denied. If neither the banner nor a consent platform can send the update, GA4 and Meta silently never measure. The panel therefore refuses to save opt-in with the banner off and the CMP option off, shows the error at the top of the form and writes nothing. An inherited value counts the same as a posted one.

What the consent script emits per combination:

| Policy | Banner | CMP option | Default | `wait_for_update` | `EVOLVE_CONSENT.managedBy` |
|---|---|---|---|---|---|
| opt-out | any | any | granted | no | `evolve`, or `cmp` with the option on |
| opt-in | on | off | denied | 500 ms | `evolve` (the banner grants) |
| opt-in | off | on | denied | 500 ms | `cmp` (the platform in GTM grants) |
| opt-in | on | on | denied | 500 ms | `cmp` (allowed, both ask) |
| opt-in | off | off | denied | 500 ms | `evolve`; only reachable through the `.env` |

The CMP mode keeps Evolve's denied defaults: if the platform is missing from the container, tags stay gated instead of firing ungated. The last row stays denied as well (no silent fallback to granted); the tracking health check warns about it, and with `APP_DEBUG=true` the browser console does too. `window.EVOLVE_CONSENT` also carries `banner: true|false`.

The banner is the theme component `x-common.cookie-consent`, included by the default layout when the banner is on. It offers accept all and decline, links to the privacy policy from the `evolve::cookieconsent` language strings, and writes the choice to the `evolve_consent` cookie for 365 days. On a change the theme calls `gtag('consent', 'update', ...)` and pushes `consent_analytics` and `consent_marketing` to the `dataLayer`, so GTM triggers can react. The policy, cookie name and lifetime sit in `config/evolve.php` under `tracking.consent`; only the policy has a variable.

With the banner off and `opt-out`, everything is granted and nobody can decline. That is a legal decision for the site owner, not a default to leave unexamined. The going-live checklist counts the cookie notice as done when the banner, the CMP option or a consent platform driver is on.

### External consent platforms&#x20;

Google Consent Mode is only half of the consent state. The other half is the `evolve_consent` cookie, which the server and a few scripts read: server-side conversions, Hotjar, the client tracking config and, since 2.19, the [footprints cookie](#attribution). The Evolve banner writes that cookie. A consent platform in the GTM container (`TRACKING_CONSENT_VIA_CMP` alone) does not, so under `opt-in` everything that reads the cookie stays denied for the whole visit, even after the visitor accepted.

**Cookiebot.** Choose Cookiebot as the consent platform on the Consent card of Settings > Tracking (or set `TRACKING_CMP=cookiebot`) and enter the Cookiebot ID (`COOKIEBOT_ID`). Evolve then:

1. renders Cookiebot's `uc.js` with `data-blockingmode="auto"`, synchronous (no `async`, no `defer`), as the last element of the head partial. That is after the Consent Mode default, the GTM snippet and the module globals, the order Cookiebot documents for Google Consent Mode: the Google tags follow Cookiebot's consent signals, everything rendered later is blocked until the visitor consents. Evolve's own scripts and styles carry `data-cookieconsent="ignore"`.
2. listens to `CookiebotOnConsentReady`, `CookiebotOnAccept` and `CookiebotOnDecline` and copies Cookiebot's statistics choice to analytics and its marketing choice to marketing in the `evolve_consent` cookie. A choice equal to the stored cookie is not written again, so the cookie does not change on every page load.
3. counts as a consent platform: opt-in without the Evolve banner can be saved, and `EVOLVE_CONSENT.managedBy` is `cmp`. Cookiebot sends the Consent Mode update itself, Evolve does not send a second one.

Do not load Cookiebot a second time, neither in the GTM container nor in a project partial. Leave the Evolve banner off; both would ask the visitor.

**Other platforms.** Any platform can feed the Evolve cookie by calling `window.evolveConsentUpdate({ analytics, marketing })` from its consent callback. The theme developer wires this up, see [Consent in the theme](/frontend/consent). Without that call (or the Cookiebot driver) the tracking health check warns on an opt-in site whose only consent source is `TRACKING_CONSENT_VIA_CMP`, and with `TRACKING_CMP=cookiebot` but no ID.

### Cookies Evolve sets&#x20;

Copy these into the cookie declaration of the consent platform. Cookies of the GTM tags (`_ga`, `_fbp` and the like) come from the container and are declared there.

| Cookie | Category | Purpose | Lifetime | Set |
|---|---|---|---|---|
| `evolve_consent` | necessary | Stores the visitor's consent choice (analytics, marketing) | 365 days | by the browser, when the visitor decides in the banner or the consent platform |
| `footprints` | marketing | Attribution: ties landing page, referrer, UTM parameters and `gclid` of each visit to a later lead | up to 400 days (the configured 5 years are capped by the browser) | by the server, only after marketing consent |
| Laravel session (`<app name>_session`, `config/session.php`) | necessary | Session, form protection, attribution data of the current visit before consent | `SESSION_LIFETIME` (120 minutes by default), ends with the browser session when `SESSION_EXPIRE_ON_CLOSE=true` | by the server, on every visit |
| `XSRF-TOKEN` | necessary | Protects forms and Livewire requests against cross-site request forgery | same as the session | by the server, on every visit |

## Server-side conversions

Two events leave the server, independent of the browser:

| Event | Fired when | Platforms |
|---|---|---|
| `generate_lead` | a form is submitted successfully | GA4 Measurement Protocol, Meta Conversions API |
| `purchase` | a Shopify order is created or updated | GA4 Measurement Protocol, Meta Conversions API |

| Variable | Default | Effect |
|---|---|---|
| `TRACKING_SERVER_SIDE` | `true` | Master switch. Drivers without credentials disable themselves, so leaving it on is safe. Settings > Tracking (settings store `server_side`, the variable is the fallback). |
| `ANALYTICS_API_SECRET` | unset | GA4 Measurement Protocol secret for the measurement ID. Without it the GA4 driver is off. |
| `FACEBOOK_CONVERSIONS_API_KEY` | unset | Meta CAPI access token for the pixel. Without it the Meta driver is off. |
| `FACEBOOK_TEST_EVENT_CODE` | unset | Routes Meta events to the test events view in Events Manager. |
| `TRACKING_QUEUE` | unset | Queue name for the delivery jobs. Unset uses the default queue. |
| `TRACKING_DEBUG` | `false` | Sends GA4 events to the validation endpoint instead of the live one and logs every delivery. |

Each event is recorded in the request with the consent state at that moment, then delivered through queued jobs. A visitor who declined marketing produces no Meta event. Personal data (email, phone) is normalised and SHA-256 hashed before it leaves. The browser receives the same event ID for its own `dataLayer` push, so GA4 and Meta can deduplicate the client and server copy of one conversion. With `QUEUE_CONNECTION=sync` the delivery runs inline; with a worker the queue health check reports a stalled worker.

## Events from the browser

The theme pushes canonical GA4-style events to the `dataLayer`: `generate_lead` and `form_submit` from the form component, `purchase` and the e-commerce funnel from the shop, and anything an element declares with an `x-track` attribute, for example the social icons' outbound clicks. The GTM container maps those events to the platform tags. The event catalogue and payloads are on the [Google Tag Manager](/integrations/google-tag-manager) page.

## Attribution

Independent of the tags above, every visit records its landing page, referrer, UTM parameters and `gclid`, so a lead can be attributed to its source in the panel. The visits of the current browser session sit in the session and are attached to a lead created in it. The `footprints` cookie carries a random token that links visits across sessions; browsers keep it for at most 400 days.

Since 2.19 the `footprints` cookie is only set once the visitor granted marketing consent. Until then the token stays in the session, so a lead submitted in the same visit keeps its attribution, and the first page after the consent click writes the cookie with that same token. A visitor who withdraws marketing consent has the cookie removed on the next page. Under `opt-out` consent is granted by default, so the cookie is set on the first page as before. A site with an external consent platform needs the [bridge](#external-consent-platforms), otherwise marketing consent is never seen and the cookie is never set.

Configuration is `config/tracking.php` (cookie name, duration, excluded paths such as `img`, `api`, `evolve`); it has no `.env` keys.

## Verifying

* On live, `curl -s https://<domain>/ | grep -c gtm.js` is `1`; on staging it is `0` unless `TRACKING_EMIT=true`.
* GTM Preview mode shows the container, the consent default and the `dataLayer` events as you click through.
* `/evolve/health` runs the tracking check. `GET /api/health-check` carries a `tracking` block with every ID, the emit decision, the consent policy, `cookie_banner`, `consent_via_cmp` and `consent_cmp`, whether the secrets are set, and `sources` saying whether a value came from the store or the config. Fleet reads that block for every site.
* With `TRACKING_DEBUG=true`, submit a form on staging with `TRACKING_EMIT=true` and read the GA4 validation response in the log.

::: tip Step by step
The recipe [Set up tracking and consent](/recipes/set-up-tracking-and-consent) walks through policy, container, IDs, a test run on staging and the live checks in order.
:::
