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

@@ -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 (
<BrowserRouter>
<AuthProvider>
{/* PWA Install Prompt */}
<InstallPWA />
<AnalyticsWrapper>
{/* PWA Install Prompt */}
<InstallPWA />
{/* Cookie Consent Banner */}
<CookieConsent />
{/* Cookie Consent Banner */}
<CookieConsent />
{/* Toast Notifications */}
<Toaster
@@ -246,6 +269,7 @@ function App() {
{/* 404 Not Found - Catch all unmatched routes */}
<Route path="*" element={<NotFoundPage />} />
</Routes>
</AnalyticsWrapper>
</AuthProvider>
</BrowserRouter>
);

View File

@@ -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 = () => {

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