From 9cd36a0b15f1102542e9847bbcef81ecdaf5d06b Mon Sep 17 00:00:00 2001 From: Tudor Date: Sun, 11 Jan 2026 15:19:05 +0000 Subject: [PATCH] Add Google Analytics 4 with cookie consent integration - Add GA4 measurement ID to config (default: G-J0PCVT14NY) - Add /api/config endpoint to expose GA ID to frontend - Update cookie consent with Analytics category (opt-in) - Load GA4 only after user consents to analytics cookies - Update CSP to allow Google Analytics domains Co-Authored-By: Claude Opus 4.5 --- backend/app.py | 14 ++++++++++--- backend/config.py | 3 +++ frontend/index.html | 48 +++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 60 insertions(+), 5 deletions(-) diff --git a/backend/app.py b/backend/app.py index 29ded85..8dc48fa 100644 --- a/backend/app.py +++ b/backend/app.py @@ -65,11 +65,11 @@ class SecurityHeadersMiddleware(BaseHTTPMiddleware): # Content Security Policy response.headers["Content-Security-Policy"] = ( "default-src 'self'; " - "script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://unpkg.com; " + "script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://unpkg.com https://www.googletagmanager.com; " "style-src 'self' 'unsafe-inline' https://fonts.googleapis.com https://cdn.jsdelivr.net https://unpkg.com; " "font-src 'self' https://fonts.gstatic.com; " - "img-src 'self' data: https://*.tile.openstreetmap.org https://unpkg.com; " - "connect-src 'self' https://cdn.jsdelivr.net https://*.tile.openstreetmap.org https://unpkg.com; " + "img-src 'self' data: https://*.tile.openstreetmap.org https://unpkg.com https://www.google-analytics.com; " + "connect-src 'self' https://cdn.jsdelivr.net https://*.tile.openstreetmap.org https://unpkg.com https://www.google-analytics.com https://analytics.google.com https://*.google-analytics.com; " "frame-ancestors 'none'; " "base-uri 'self'; " "form-action 'self';" @@ -199,6 +199,14 @@ async def serve_rankings(): return FileResponse(settings.frontend_dir / "index.html") +@app.get("/api/config") +async def get_config(): + """Return public configuration for the frontend.""" + return { + "ga_measurement_id": settings.ga_measurement_id + } + + @app.get("/api/schools") @limiter.limit(f"{settings.rate_limit_per_minute}/minute") async def get_schools( diff --git a/backend/config.py b/backend/config.py index 04cefa4..ce0b8dc 100644 --- a/backend/config.py +++ b/backend/config.py @@ -38,6 +38,9 @@ class Settings(BaseSettings): rate_limit_burst: int = 10 # Allow burst of requests max_request_size: int = 1024 * 1024 # 1MB max request size + # Analytics + ga_measurement_id: Optional[str] = "G-J0PCVT14NY" # Google Analytics 4 Measurement ID + class Config: env_file = ".env" env_file_encoding = "utf-8" diff --git a/frontend/index.html b/frontend/index.html index 0bc2c63..62b1d38 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -354,6 +354,38 @@ + + + +