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
This commit is contained in:
Radosław Gierwiało
2025-12-05 22:28:00 +01:00
parent 3523172ecb
commit a786b1d92d
6 changed files with 313 additions and 4 deletions

View File

@@ -9,3 +9,8 @@ VITE_ALLOWED_HOSTS=localhost,spotlight.cam,.spotlight.cam
# Cloudflare Turnstile (CAPTCHA) # Cloudflare Turnstile (CAPTCHA)
# Get your keys from: https://dash.cloudflare.com/ # Get your keys from: https://dash.cloudflare.com/
VITE_TURNSTILE_SITE_KEY=your-site-key-here 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=

View File

@@ -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)

View File

@@ -1,6 +1,9 @@
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'; import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
import { Toaster } from 'react-hot-toast'; import { Toaster } from 'react-hot-toast';
import { useEffect } from 'react';
import { AuthProvider, useAuth } from './contexts/AuthContext'; import { AuthProvider, useAuth } from './contexts/AuthContext';
import { initGA } from './utils/analytics';
import usePageTracking from './hooks/usePageTracking';
import HomePage from './pages/HomePage'; import HomePage from './pages/HomePage';
import LoginPage from './pages/LoginPage'; import LoginPage from './pages/LoginPage';
import RegisterPage from './pages/RegisterPage'; import RegisterPage from './pages/RegisterPage';
@@ -71,10 +74,30 @@ const PublicRoute = ({ children }) => {
return 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() { function App() {
return ( return (
<BrowserRouter> <BrowserRouter>
<AuthProvider> <AuthProvider>
<AnalyticsWrapper>
{/* PWA Install Prompt */} {/* PWA Install Prompt */}
<InstallPWA /> <InstallPWA />
@@ -246,6 +269,7 @@ function App() {
{/* 404 Not Found - Catch all unmatched routes */} {/* 404 Not Found - Catch all unmatched routes */}
<Route path="*" element={<NotFoundPage />} /> <Route path="*" element={<NotFoundPage />} />
</Routes> </Routes>
</AnalyticsWrapper>
</AuthProvider> </AuthProvider>
</BrowserRouter> </BrowserRouter>
); );

View File

@@ -1,5 +1,6 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { Cookie, X } from 'lucide-react'; import { Cookie, X } from 'lucide-react';
import { initGA } from '../../utils/analytics';
/** /**
* GDPR/RODO compliant cookie consent banner * GDPR/RODO compliant cookie consent banner
@@ -20,6 +21,12 @@ const CookieConsent = () => {
const handleAccept = () => { const handleAccept = () => {
localStorage.setItem('cookieConsent', 'accepted'); localStorage.setItem('cookieConsent', 'accepted');
setIsVisible(false); setIsVisible(false);
// Initialize Google Analytics after consent
const measurementId = import.meta.env.VITE_GA_MEASUREMENT_ID;
if (measurementId) {
setTimeout(() => initGA(measurementId), 500);
}
}; };
const handleDecline = () => { const handleDecline = () => {

View File

@@ -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;

View File

@@ -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 });
};