Tracking pipeline
Two halves: the browser side, where the theme renders one GTM snippet and a Consent Mode default and pushes events to the dataLayer, and the server side, where Evolve records conversions and delivers them to the GA4 Measurement Protocol and the Meta Conversions API. The webmaster view is on Tracking and consent.
The two halves
Emission
Evolve::emitsTracking() (core/src/Evolve.php) decides whether anything leaves the site: evolve.tracking.emit (TRACKING_EMIT) when set, otherwise EnvironmentType::emitsTracking(), which is true for live only. The theme partials and TrackingManager::record() both consult it.
Browser side
theme/views/head.blade.php renders, in order, the verification metas, preconnects (when emitting), the include of theme::consentMode, the reCAPTCHA script when a site key exists, and the GTM snippet when emitting and services.google.gtm-id is set. theme/views/consentMode.blade.php defines dataLayer and gtag(), reads the evolve_consent cookie, exposes window.EVOLVE_CONSENT and window.evolveConsentAllows(category), and calls gtag('consent', 'default', ...) with analytics_storage, ad_storage, ad_user_data, ad_personalization granted or denied per the policy, plus wait_for_update: 500 for opt-in. The same view defines window.evolveConsentUpdate({analytics, marketing}), the bridge for consent decided outside the banner (#300): it writes evolve_consent in the banner's format, sets EVOLVE_CONSENT.state, sends gtag('consent', 'update') only when managedBy is evolve, pushes consent_updated and dispatches evolve:consent-updated; unchanged choices are skipped. With evolve.tracking.consent.cmp = cookiebot it also listens to CookiebotOnConsentReady, CookiebotOnAccept and CookiebotOnDecline. theme/views/consentPlatform.blade.php, included last in head.blade.php, renders Cookiebot's uc.js (synchronous, data-blockingmode="auto") when the driver and the ID are set. theme/views/trackingConfig.blade.php renders the deprecated window.*_TRACKING globals behind TRACKING_LEGACY_VARIABLES and the Hotjar snippet, both only when emitting.
In the starter theme, theme/theme/src/lib/cookie-consent.ts holds the Alpine store consent (analytics, marketing, acceptAll(), rejectAll(), save()), writes the cookie for cookieLifetimeDays, calls gtag('consent', 'update', ...) and pushes consent_analytics and consent_marketing to the dataLayer. theme/theme/src/lib/event-tracking.ts is the event layer: genericTrack(event, payload) pushes to the dataLayer, window.gtmTrack aliases it, helpers build generate_lead, form_submit, purchase and the funnel events, and [x-track] attributes are wired on DOM ready. theme/gtm_container.json is the matching container export.
Server side
core/src/Tracking/:
| Class | Role |
|---|---|
TrackingManager | record(TrackingEvent): snapshots the request (IP, user agent, _ga, _fbp, _fbc cookies, consent) into a UserContext and dispatches one Jobs\SendServerSideTrackingEvent per enabled driver on evolve.tracking.queue |
TrackingEvent | name, event ID, params, user context, timestamp; factory generateLead(Form, Request, eventId) |
UserContext, ConsentState, Consent, ConsentPolicy, ConsentCategory | the consent snapshot taken at record time; ConsentPolicy::OptOut grants by default, OptIn denies |
Drivers\GoogleAnalyticsMeasurementProtocolDriver | enabled when measurement ID and ANALYTICS_API_SECRET exist; posts to the collect endpoint, or to the debug endpoint with TRACKING_DEBUG |
Drivers\MetaConversionsApiDriver | enabled when pixel ID and FACEBOOK_CONVERSIONS_API_KEY exist; adds FACEBOOK_TEST_EVENT_CODE when set |
Support\HashesPersonalData | SHA-256 of normalised email and digits-only phone before they leave |
Support\EventId | the shared ID that lets the browser and server copy of one event deduplicate |
Callers: Http\Controllers\Frontend\FormController records generate_lead after a stored lead and hands the event ID to the redirect so the theme pushes the browser copy with the same ID; shopify/src/.../SendPurchaseTrackingEvent listens to OrderUpdatedOrCreated. The consent snapshot gates every delivery, so a visitor who declined marketing produces no Meta event. Drivers without credentials disable themselves, which is why TRACKING_SERVER_SIDE=true is a safe default.
EncryptCookies::except() in the core provider keeps evolve_consent, _ga, _fbp and _fbc unencrypted; otherwise PHP could not read them.
Configuration
core/config/evolve.php → tracking (server-side, emit, queue, debug, legacy-variables, consent.policy, consent.via-cmp, consent.cmp, consent.cookiebot-id, consent.cookie-name, consent.cookie-lifetime-days), evolve.cookie-consent for the banner; platform IDs in core/config/services.php; the attribution cookie in core/config/tracking.php (footprints, duration, blacklisted paths). Http\Middleware\AttributionTracking writes footprints only when Consent::allows(Marketing); before that the token sits in the session under footprints_token, and AttributionTracking::currentToken() (cookie, else session token) is what Visit::previousVisits() and TracksAttribution match leads by (#302). Settings\TrackingSettings overlays the public IDs, the policy, server_side and cookie_banner.
Observability
Health\Checks\TrackingCheck fails a live site with missing or malformed GTM or GA4 IDs or a silencing TRACKING_EMIT, warns on a staging site left emitting, on an opt-in site whose only consent source is via-cmp (the Evolve cookie is never written) and on the Cookiebot driver without an ID. HealthCheckController returns the tracking block (type, emits, override, IDs, secrets-set booleans, consent, sources) that Fleet compares per environment. Tests: core/tests/Feature/Tracking/*.