Tracking scripts gated on consent
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:
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: anif (consent === 'granted')branch, auseEffect(() => { 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 ANDupdateis wired to the consent handler. - Fail criteria: Any SDK init runs unconditionally —
posthog.init(KEY)inapp/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:
Useif (consent === 'granted') posthog.init(KEY, { autocapture: true })react-cookie-consentorvanilla-cookieconsentfor the UI.
Taxons
History
- 2026-04-22·v1.0.0·Initial import from project-snapshot via Phase 8.1 bundling·by phase-8-1-bundle-project-snapshot
- 2026-04-22·v2.0.0·Phase 9 consequence-first restructure — moved to new section slug, added news/incident references, severity reviewed.·by phase-9-stack-scan-v3
- 2026-04-23·v2.1.0·Phase 9.1 tighten — renamed to `Tracking scripts gated on consent`; check now targets the SDK init call site (not banner-component presence); explicit failure mode documented when a banner exists but SDK init runs unconditionally.·by phase-9-1-stack-scan-v3-1
- 2026-04-25·v2.1.1·v3.1.0 pre-ship trim — prose compression for under-80K MCP cap; merged overlapping Fail-criteria / Do-NOT-pass-when sections; compressed enumeration prose; one remediation example per pattern. No semantic change; anti-sycophancy guards preserved.·by phase-9-1-stack-scan-v3-1