# Tracking scripts gated on consent

- **Pattern:** `ab-002581` (`project-snapshot.legal.cookie-consent-before-tracking`)
- **Severity:** high
- **Lifecycle:** active
- **Last modified:** 2026-04-25
- **Canonical URL:** https://auditbuffet.com/patterns/ab-002581
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-002581)

## Why it matters

Loading PostHog, Google Analytics, Segment, Mixpanel, or Hotjar before an EU/UK visitor clicks "accept" violates GDPR Art. 7 and the ePrivacy Directive — France's CNIL fined Google €150M and Facebook €60M in 2022 for non-compliant cookie consent, the EDPB added a €390M fine against Meta in 2023, and EU courts have held that cookies set before user opt-in on any tracking script are per-se unlawful under the ePrivacy Directive. AI coding tools ship the classic failing shape: a `posthog.init()` or `<Script src="googletagmanager.com/...">` dropped into `app/layout.tsx` during setup with no consent gate wired in, because the assistant knew how to install the SDK but not the regulatory dependency. The user-facing failure is silent — the site works, cookies get dropped, and the liability accrues until a complaint or audit surfaces it. California CPRA, Quebec Law 25, and Brazil's LGPD impose parallel prior-consent requirements.

## Severity rationale

High because the exposure scales per-visitor and regulators have issued nine-figure fines (CNIL €150M v. Google, EDPB €390M v. Meta) specifically for tracking-before-consent — the remediation is narrow but the pre-fix exposure compounds with every page load.

## Remediation

Add a consent banner that gates tracking init:
  ```ts
  if (consent === 'granted') posthog.init(KEY, { autocapture: true })
  ```
  Use a library like `react-cookie-consent` or `vanilla-cookieconsent` for the UI.

Deeper remediation guidance and cross-reference coverage for this check lives in the `gdpr-readiness` Pro audit — run that after applying this fix for a more exhaustive pass on the same topic.

## Detection

- **ID:** `cookie-consent-before-tracking`
- **Severity:** `high`
- **What to look for:** Enumerate tracking-SDK deps in `package.json`: `posthog-js`, `@vercel/analytics`, `@vercel/speed-insights`, `react-ga`/`react-ga4`, `gtag`, `googletagmanager`, `next-plausible`, `mixpanel-browser`, `@segment/analytics-next`, `hotjar-react`/`@hotjar/browser`, `@intercom/messenger-js-sdk`, `@microsoft/clarity`, `amplitude-js`/`@amplitude/analytics-browser`, `meta-pixel`/`react-facebook-pixel`, `fbq`. Also grep source for script strings: `googletagmanager.com`, `google-analytics.com`, `fbevents.js`, `static.hotjar.com`, `cdn.mxpnl.com`. For each SDK, LOCATE THE INIT CALL SITE — `.init()`/`.load()`/`inject()`, `new PostHog(...)`/`new Mixpanel(...)`, or provider-component mount (`<Analytics/>`, `<GoogleAnalytics/>`). Verify it is runtime-gated: an `if (consent === 'granted')` branch, a `useEffect(() => { if (getConsent()) {...} })`, Google Consent Mode (`gtag('consent', 'default', { analytics_storage: 'denied' })` followed by a later `'update'`), or a function only called from inside a consent-accept handler. Quote init + gate line per SDK.
- **Pass criteria:** No SDKs detected, OR every init is runtime-gated before cookies or network sends. Google Consent Mode satisfies this when `default: 'denied'` precedes tag load AND `update` is wired to the consent handler.
- **Fail criteria:** Any SDK init runs unconditionally — `posthog.init(KEY)` in `app/layout.tsx`, `<Analytics/>` rendered unconditionally, `<Script src="googletagmanager.com/...">` without consent mode, `mixpanel.init(TOKEN)` in module scope. A `<CookieBanner>` rendered in layout while the SDK init fires unconditionally elsewhere is the most common failure shape — banner presence is not a gate. Banner that only sets a cookie on accept while SDK init never reads it: fails. Gate is client-only but SDK loads via `<Script strategy="beforeInteractive">` (before hydration): fails.
- **Skip (N/A) when:** No tracking SDKs detected. Quote: `"No tracking-SDK deps in package.json; no googletagmanager.com / google-analytics.com / fbevents.js script tags; no detected init sites."`
- **Report even on pass:** `"SDKs: {list}. Init sites: {file:line each}. Gates: {mechanism per SDK}. All N gated before cookie or network send."`
- **Detail on fail:** `"posthog.init(KEY) at app/layout.tsx:14 runs unconditionally; <CookieBanner /> at :22 but init fires on every page load regardless of consent state"`.
- **Cross-reference:** For full GDPR coverage (data-subject rights, DPAs, breach notification), run `gdpr-readiness`.
- **Remediation:**
  ```ts
  if (consent === 'granted') posthog.init(KEY, { autocapture: true })
  ```
  Use `react-cookie-consent` or `vanilla-cookieconsent` for the UI.

Taxons: project-snapshot, regulatory-conformance

HTML version: https://auditbuffet.com/patterns/ab-002581
