A consent banner that exists as a UI component but does not actually block analytics from firing provides zero legal protection. Under GDPR Art. 7, consent must be freely given before processing begins — not after. This is the most common AI-built project failure: analytics loads unconditionally in the root layout, the banner is purely decorative, and every page view constitutes an unlawful processing event under GDPR and eprivacy Art. 5(3). Data regulators consider cosmetic consent banners an aggravating factor in enforcement.
Critical because analytics firing before consent is granted means every page load is an unlawful processing event under GDPR Art. 6, regardless of whether the banner UI is present.
Move analytics initialization out of the root layout and into a client component that reads consent state before executing. In Next.js App Router:
// components/ConsentGatedAnalytics.tsx
'use client'
import { useEffect } from 'react'
export function ConsentGatedAnalytics() {
useEffect(() => {
const consent = localStorage.getItem('analytics_consent')
if (consent === 'true') initAnalytics()
}, [])
return null
}
For GA4, implement Consent Mode v2 in app/layout.tsx — set analytics_storage: 'denied' before the gtag script tag, then call gtag('consent', 'update', { analytics_storage: 'granted' }) inside the accept handler. Remove any unconditional <Script src="gtag"> tags from app/layout.tsx.
ID: marketing-analytics.privacy-compliance.analytics-gated-by-consent
Severity: critical
What to look for: Even if a consent banner exists, the analytics must not actually run until consent is granted. Check whether:
localStorage, a cookie, or a React context for consent)if (hasAnalyticsConsent()) { initAnalytics() }analytics_storage: 'denied' before the tag fires)This is the most commonly broken pattern in AI-built projects: the consent banner exists as a UI component but the analytics script loads unconditionally in the root layout, meaning consent is cosmetic.
Pass criteria: Count every analytics script or initialization call in the codebase. Each one is demonstrably gated on consent state: either via an explicit conditional check, GA4 Consent Mode with default denied, or the analytics SDK is loaded only after the user accepts cookies. 100% of initialization paths must be gated. Cookie-free analytics (Plausible, Fathom, Vercel Analytics) passes automatically since they don't require consent.
Fail criteria: At least 1 analytics script loads unconditionally in the root layout (e.g., a <Script> tag with no conditional rendering, or initAnalytics() called at module load time with no consent check), even if a consent banner UI component exists elsewhere.
Skip (N/A) when: No analytics is present. No consent banner is required (cookie-free analytics tools only).
Detail on fail: "GA4 script loads unconditionally in app/layout.tsx via <Script> tag. Consent banner component exists in components/CookieBanner.tsx but analytics fires on every page load regardless of user choice — consent is purely cosmetic."
Remediation: Gate analytics initialization on consent state. The pattern for Next.js App Router:
// components/analytics-provider.tsx
'use client'
import { useEffect } from 'react'
export function AnalyticsProvider() {
useEffect(() => {
const consent = localStorage.getItem('analytics_consent')
if (consent === 'true') {
initAnalytics() // Only runs if user accepted
}
}, [])
return null
}
For GA4, implement Consent Mode v2 — set analytics_storage: 'denied' before the gtag script loads, then update to 'granted' when the user accepts:
// Before gtag.js loads:
window.dataLayer = window.dataLayer || []
function gtag() { dataLayer.push(arguments) }
gtag('consent', 'default', { analytics_storage: 'denied' })
// After user accepts:
gtag('consent', 'update', { analytics_storage: 'granted' })