From a786b1d92d154e666fda33c3c463e1b54d4d1da0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rados=C5=82aw=20Gierwia=C5=82o?= Date: Fri, 5 Dec 2025 22:28:00 +0100 Subject: [PATCH] feat(analytics): integrate Google Analytics 4 with GDPR compliance Prepared the application for Google Analytics 4 tracking with full GDPR/RODO compliance. GA only loads after user explicitly accepts cookies. Features: - Automatic page view tracking on route changes - Custom event tracking for key user actions - Privacy-first: GA loads only after cookie consent - Easy configuration via environment variable - Comprehensive tracking utilities for common events Implementation: - Created analytics.js with GA initialization and event tracking functions - Created usePageTracking hook for automatic page view tracking - Integrated GA into App.jsx with AnalyticsWrapper component - Updated CookieConsent to initialize GA after user consent - Added VITE_GA_MEASUREMENT_ID to .env.example Custom events tracked: - login, sign_up (authentication) - match_request, match_accepted (matching) - webrtc_connection, file_transfer (WebRTC) - event_join, recording_suggestion (events/recording) - search (search functionality) Setup: 1. Add VITE_GA_MEASUREMENT_ID=G-XXXXXXXXXX to .env 2. Restart frontend container 3. GA will auto-load after user accepts cookies Documentation: - Created comprehensive setup guide in docs/GOOGLE_ANALYTICS_SETUP.md - Includes troubleshooting, debugging tips, and usage examples --- .env.example | 5 + docs/GOOGLE_ANALYTICS_SETUP.md | 143 ++++++++++++++++++ frontend/src/App.jsx | 32 +++- .../src/components/common/CookieConsent.jsx | 7 + frontend/src/hooks/usePageTracking.js | 18 +++ frontend/src/utils/analytics.js | 112 ++++++++++++++ 6 files changed, 313 insertions(+), 4 deletions(-) create mode 100644 docs/GOOGLE_ANALYTICS_SETUP.md create mode 100644 frontend/src/hooks/usePageTracking.js create mode 100644 frontend/src/utils/analytics.js diff --git a/.env.example b/.env.example index 8881f4d..0e21018 100644 --- a/.env.example +++ b/.env.example @@ -9,3 +9,8 @@ VITE_ALLOWED_HOSTS=localhost,spotlight.cam,.spotlight.cam # Cloudflare Turnstile (CAPTCHA) # Get your keys from: https://dash.cloudflare.com/ VITE_TURNSTILE_SITE_KEY=your-site-key-here + +# Google Analytics 4 +# Format: G-XXXXXXXXXX +# Get your measurement ID from: https://analytics.google.com/ +VITE_GA_MEASUREMENT_ID= diff --git a/docs/GOOGLE_ANALYTICS_SETUP.md b/docs/GOOGLE_ANALYTICS_SETUP.md new file mode 100644 index 0000000..e0524f1 --- /dev/null +++ b/docs/GOOGLE_ANALYTICS_SETUP.md @@ -0,0 +1,143 @@ +# Google Analytics Setup Guide + +This document explains how to configure Google Analytics 4 (GA4) for spotlight.cam. + +## Prerequisites + +1. Google Analytics account +2. GA4 property created for spotlight.cam + +## Setup Instructions + +### 1. Get your Measurement ID + +1. Go to [Google Analytics](https://analytics.google.com/) +2. Select your property +3. Go to **Admin** (bottom left) +4. Under **Property**, click **Data Streams** +5. Select your web stream (or create one) +6. Copy the **Measurement ID** (format: `G-XXXXXXXXXX`) + +### 2. Configure Environment Variable + +Add your Measurement ID to the `.env` file in the project root: + +```bash +VITE_GA_MEASUREMENT_ID=G-XXXXXXXXXX +``` + +Replace `G-XXXXXXXXXX` with your actual Measurement ID. + +### 3. Restart the Application + +For the changes to take effect: + +```bash +# Rebuild and restart the frontend container +docker compose up -d --build frontend +``` + +## How It Works + +### Automatic Tracking + +The application automatically tracks: + +- **Page views**: Every route change is tracked +- **User consent**: GA only loads if user accepts cookies +- **Custom events**: See below for list of tracked events + +### Privacy & GDPR Compliance + +- GA script loads **only after** user accepts cookies +- If user declines cookies, no tracking occurs +- Consent is stored in `localStorage` + +### Custom Events + +The following custom events are tracked: + +| Event | Description | Parameters | +|-------|-------------|------------| +| `login` | User logs in | `method` | +| `sign_up` | User registers | `method` | +| `match_request` | User sends match request | `event_slug` | +| `match_accepted` | User accepts match | `match_slug` | +| `webrtc_connection` | WebRTC connection status | `status` | +| `file_transfer` | File transfer via WebRTC | `file_size`, `success` | +| `event_join` | User joins event | `event_slug` | +| `recording_suggestion` | Recording suggestion action | `action` | +| `search` | User searches | `search_term` | + +### Using Analytics in Code + +Import and use the tracking functions: + +```javascript +import { trackEvent, trackLogin, trackMatchRequest } from './utils/analytics'; + +// Track custom event +trackEvent('button_click', { button_name: 'register' }); + +// Track login +trackLogin('email'); + +// Track match request +trackMatchRequest('event-slug-123'); +``` + +## Debugging + +### Check if GA is loaded + +Open browser console and run: + +```javascript +console.log(window.gtag); // Should be a function +console.log(window.dataLayer); // Should be an array +``` + +### Check cookie consent + +```javascript +console.log(localStorage.getItem('cookieConsent')); // 'accepted' or 'declined' +``` + +### View GA events in real-time + +1. Go to Google Analytics +2. Navigate to **Reports** → **Realtime** +3. You should see your page views and events as they happen + +## Troubleshooting + +### GA not loading + +1. Check that `VITE_GA_MEASUREMENT_ID` is set in `.env` +2. Verify user accepted cookies (check browser localStorage) +3. Check browser console for errors +4. Ensure frontend container was rebuilt after adding env variable + +### Events not showing up + +1. Wait 24-48 hours for events to appear in standard reports +2. Use **Realtime** view for immediate feedback +3. Check that events are being sent: `console.log(window.dataLayer)` + +### Ad blockers + +Note: Users with ad blockers may block Google Analytics. This is expected behavior. + +## Production Deployment + +When deploying to production: + +1. Set `VITE_GA_MEASUREMENT_ID` in production environment +2. Verify tracking works in production domain +3. Update GA4 property settings if domain changes +4. Configure data retention and user deletion settings in GA + +## References + +- [GA4 Documentation](https://developers.google.com/analytics/devguides/collection/ga4) +- [GDPR Compliance](https://support.google.com/analytics/answer/9019185) diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 9a69c50..7895e3a 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,6 +1,9 @@ import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'; import { Toaster } from 'react-hot-toast'; +import { useEffect } from 'react'; import { AuthProvider, useAuth } from './contexts/AuthContext'; +import { initGA } from './utils/analytics'; +import usePageTracking from './hooks/usePageTracking'; import HomePage from './pages/HomePage'; import LoginPage from './pages/LoginPage'; import RegisterPage from './pages/RegisterPage'; @@ -71,15 +74,35 @@ const PublicRoute = ({ children }) => { return children; }; +// Analytics wrapper component +const AnalyticsWrapper = ({ children }) => { + usePageTracking(); // Track page views on route changes + + useEffect(() => { + // Initialize Google Analytics when user consents to cookies + const measurementId = import.meta.env.VITE_GA_MEASUREMENT_ID; + if (measurementId) { + // Wait a bit for cookie consent banner to be processed + const timer = setTimeout(() => { + initGA(measurementId); + }, 1500); + return () => clearTimeout(timer); + } + }, []); + + return children; +}; + function App() { return ( - {/* PWA Install Prompt */} - + + {/* PWA Install Prompt */} + - {/* Cookie Consent Banner */} - + {/* Cookie Consent Banner */} + {/* Toast Notifications */} } /> + ); diff --git a/frontend/src/components/common/CookieConsent.jsx b/frontend/src/components/common/CookieConsent.jsx index f4265fa..a82f3e7 100644 --- a/frontend/src/components/common/CookieConsent.jsx +++ b/frontend/src/components/common/CookieConsent.jsx @@ -1,5 +1,6 @@ import { useState, useEffect } from 'react'; import { Cookie, X } from 'lucide-react'; +import { initGA } from '../../utils/analytics'; /** * GDPR/RODO compliant cookie consent banner @@ -20,6 +21,12 @@ const CookieConsent = () => { const handleAccept = () => { localStorage.setItem('cookieConsent', 'accepted'); setIsVisible(false); + + // Initialize Google Analytics after consent + const measurementId = import.meta.env.VITE_GA_MEASUREMENT_ID; + if (measurementId) { + setTimeout(() => initGA(measurementId), 500); + } }; const handleDecline = () => { diff --git a/frontend/src/hooks/usePageTracking.js b/frontend/src/hooks/usePageTracking.js new file mode 100644 index 0000000..ce506b9 --- /dev/null +++ b/frontend/src/hooks/usePageTracking.js @@ -0,0 +1,18 @@ +import { useEffect } from 'react'; +import { useLocation } from 'react-router-dom'; +import { trackPageView } from '../utils/analytics'; + +/** + * Hook to automatically track page views on route changes + * Usage: Add usePageTracking() to App.jsx + */ +const usePageTracking = () => { + const location = useLocation(); + + useEffect(() => { + // Track page view on route change + trackPageView(location.pathname + location.search, document.title); + }, [location]); +}; + +export default usePageTracking; diff --git a/frontend/src/utils/analytics.js b/frontend/src/utils/analytics.js new file mode 100644 index 0000000..6d526eb --- /dev/null +++ b/frontend/src/utils/analytics.js @@ -0,0 +1,112 @@ +/** + * Google Analytics utilities + * Handles GA4 tracking events and page views + */ + +// Initialize Google Analytics +export const initGA = (measurementId) => { + if (!measurementId) { + console.warn('Google Analytics: No measurement ID provided'); + return; + } + + // Check if user has consented to cookies + const cookieConsent = localStorage.getItem('cookieConsent'); + if (cookieConsent !== 'accepted') { + console.log('Google Analytics: Disabled - user has not consented to cookies'); + return; + } + + // Check if gtag already exists + if (window.gtag) { + console.log('Google Analytics: Already initialized'); + return; + } + + // Load GA script + const script = document.createElement('script'); + script.async = true; + script.src = `https://www.googletagmanager.com/gtag/js?id=${measurementId}`; + document.head.appendChild(script); + + // Initialize gtag + window.dataLayer = window.dataLayer || []; + window.gtag = function() { + window.dataLayer.push(arguments); + }; + window.gtag('js', new Date()); + window.gtag('config', measurementId, { + send_page_view: false, // We'll handle page views manually + }); + + console.log('Google Analytics: Initialized with ID', measurementId); +}; + +// Track page view +export const trackPageView = (path, title) => { + if (!window.gtag) { + return; + } + + window.gtag('event', 'page_view', { + page_path: path, + page_title: title, + }); +}; + +// Track custom event +export const trackEvent = (eventName, eventParams = {}) => { + if (!window.gtag) { + return; + } + + window.gtag('event', eventName, eventParams); +}; + +// Track user login +export const trackLogin = (method = 'email') => { + trackEvent('login', { method }); +}; + +// Track user registration +export const trackSignUp = (method = 'email') => { + trackEvent('sign_up', { method }); +}; + +// Track search +export const trackSearch = (searchTerm) => { + trackEvent('search', { search_term: searchTerm }); +}; + +// Track match request +export const trackMatchRequest = (eventSlug) => { + trackEvent('match_request', { event_slug: eventSlug }); +}; + +// Track match accepted +export const trackMatchAccepted = (matchSlug) => { + trackEvent('match_accepted', { match_slug: matchSlug }); +}; + +// Track WebRTC connection +export const trackWebRTCConnection = (status) => { + trackEvent('webrtc_connection', { status }); +}; + +// Track file transfer +export const trackFileTransfer = (fileSize, success) => { + trackEvent('file_transfer', { + file_size: fileSize, + success: success ? 'yes' : 'no', + }); +}; + +// Track event join +export const trackEventJoin = (eventSlug) => { + trackEvent('event_join', { event_slug: eventSlug }); +}; + +// Track recording suggestion +export const trackRecordingSuggestion = (action) => { + trackEvent('recording_suggestion', { action }); +};