import { trace } from '@opentelemetry/api'; import { ATTR_HTTP_ROUTE } from '@opentelemetry/semantic-conventions'; // Types and Interfaces interface AnalyticsMetadata { [key: string]: string | number | boolean | undefined; } // Constants const ANALYTICS_ENABLED_KEY = 'analyticsEnabled'; const LOCATION_CONSENT_KEY = 'locationTrackingConsent'; const SESSION_ID_KEY = 'sessionId'; const USER_ID_KEY = 'userId'; // Generate a unique session ID const generateSessionId = (): string => { return Date.now().toString(36) + Math.random().toString(36).substring(2); }; // Get current user ID (implement based on your auth system) export const getCurrentUserId = (): string | null => { // This is a placeholder - implement based on your authentication system // For example, you might get this from localStorage, cookies, or context return localStorage.getItem(USER_ID_KEY) || sessionStorage.getItem(USER_ID_KEY) || null; }; // Track page view export const trackPageView = (pathname: string): void => { // Check if analytics are enabled if (!isAnalyticsEnabled()) { return; } try { const tracer = trace.getTracer('bakery-frontend'); const user_id = getCurrentUserId(); const span = tracer.startSpan('page_view', { attributes: { [ATTR_HTTP_ROUTE]: pathname, 'user.id': user_id || 'anonymous', 'page.path': pathname, } }); // End the span immediately for page views span.end(); } catch (error) { console.error('Failed to track page view:', error); } }; // Check if analytics are enabled export const isAnalyticsEnabled = (): boolean => { return localStorage.getItem(ANALYTICS_ENABLED_KEY) !== 'false'; }; // Enable or disable analytics export const setAnalyticsEnabled = (enabled: boolean): void => { localStorage.setItem(ANALYTICS_ENABLED_KEY, enabled.toString()); }; // Check if location tracking consent is granted export const isLocationTrackingConsentGranted = (): boolean => { return localStorage.getItem(LOCATION_CONSENT_KEY) === 'granted'; }; // Set location tracking consent export const setLocationTrackingConsent = (granted: boolean): void => { localStorage.setItem(LOCATION_CONSENT_KEY, granted ? 'granted' : 'denied'); }; // Track user session export const trackSession = (): (() => void) => { // Check if analytics are enabled if (!isAnalyticsEnabled()) { console.log('Analytics disabled by user preference'); return () => {}; // Return no-op cleanup function } try { const tracer = trace.getTracer('bakery-frontend'); const sessionId = generateSessionId(); const userId = getCurrentUserId(); const span = tracer.startSpan('user_session', { attributes: { 'session.id': sessionId, 'user.id': userId || 'anonymous', 'browser.user_agent': navigator.userAgent, 'screen.width': window.screen.width.toString(), 'screen.height': window.screen.height.toString(), 'device.type': /mobile|tablet|ipad|iphone|ipod|android|silk/i.test(navigator.userAgent) ? 'mobile' : 'desktop' } }); // Store session ID in sessionStorage for later use sessionStorage.setItem(SESSION_ID_KEY, sessionId); // End span when session ends const handleBeforeUnload = () => { span.end(); }; window.addEventListener('beforeunload', handleBeforeUnload); // Clean up event listener when needed return () => { window.removeEventListener('beforeunload', handleBeforeUnload); }; } catch (error) { console.error('Failed to track session:', error); return () => {}; // Return no-op cleanup function } }; // Track user action export const trackUserAction = (action: string, metadata?: AnalyticsMetadata): void => { // Check if analytics are enabled if (!isAnalyticsEnabled()) { return; } try { const tracer = trace.getTracer('bakery-frontend'); const userId = getCurrentUserId(); const span = tracer.startSpan('user_action', { attributes: { 'user.action': action, 'user.id': userId || 'anonymous', ...metadata } }); span.end(); } catch (error) { console.error('Failed to track user action:', error); } }; // Track user location (with consent) export const trackUserLocation = async (): Promise => { // Check if analytics are enabled if (!isAnalyticsEnabled()) { return; } // Check if location tracking consent is granted if (!isLocationTrackingConsentGranted()) { console.log('Location tracking consent not granted'); return; } try { const position = await new Promise((resolve, reject) => { if (!navigator.geolocation) { reject(new Error('Geolocation not supported')); return; } navigator.geolocation.getCurrentPosition(resolve, reject, { enableHighAccuracy: false, timeout: 10000, maximumAge: 300000 // 5 minutes }); }); const tracer = trace.getTracer('bakery-frontend'); const userId = getCurrentUserId(); const span = tracer.startSpan('user_location', { attributes: { 'user.id': userId || 'anonymous', 'location.latitude': position.coords.latitude, 'location.longitude': position.coords.longitude, 'location.accuracy': position.coords.accuracy, 'location.altitude': position.coords.altitude ?? undefined, 'location.speed': position.coords.speed ?? undefined, 'location.heading': position.coords.heading ?? undefined } }); span.end(); } catch (error) { console.log('Location access denied or unavailable:', error); } }; // Initialize analytics tracking export const initializeAnalytics = (): (() => void) => { // Track initial session const cleanupSession = trackSession(); // Track initial page view trackPageView(window.location.pathname); // Listen for route changes (for SPA navigation) let previousUrl = window.location.href; // For hash-based routing const handleHashChange = () => { if (window.location.href !== previousUrl) { trackPageView(window.location.pathname + window.location.search); previousUrl = window.location.href; } }; // For history API-based routing (most common in React apps) // Use proper typing for history state methods const originalPushState = history.pushState.bind(history); const handlePushState = function ( this: History, data: unknown, unused: string, url?: string | URL | null ) { originalPushState(data, unused, url); setTimeout(() => { if (window.location.href !== previousUrl) { trackPageView(window.location.pathname + window.location.search); previousUrl = window.location.href; } }, 0); }; const originalReplaceState = history.replaceState.bind(history); const handleReplaceState = function ( this: History, data: unknown, unused: string, url?: string | URL | null ) { originalReplaceState(data, unused, url); setTimeout(() => { if (window.location.href !== previousUrl) { trackPageView(window.location.pathname + window.location.search); previousUrl = window.location.href; } }, 0); }; // Override history methods history.pushState = handlePushState; history.replaceState = handleReplaceState; // Add event listeners window.addEventListener('hashchange', handleHashChange); // Track user consent for location if needed if (isLocationTrackingConsentGranted()) { trackUserLocation(); } // Return cleanup function return () => { // Restore original history methods history.pushState = originalPushState; history.replaceState = originalReplaceState; // Remove event listeners window.removeEventListener('hashchange', handleHashChange); // Clean up session tracking cleanupSession(); }; }; // Function to track custom metrics using OpenTelemetry spans export const trackCustomMetric = ( name: string, value: number, attributes?: Record ): void => { // Check if analytics are enabled if (!isAnalyticsEnabled()) { return; } try { // Record metric as a span with the value as an attribute // This approach works well for browser-based metrics since // the OpenTelemetry metrics API in browsers sends to the same collector const tracer = trace.getTracer('bakery-frontend'); const userId = getCurrentUserId(); const span = tracer.startSpan('custom_metric', { attributes: { 'metric.name': name, 'metric.value': value, 'user.id': userId || 'anonymous', ...attributes } }); span.end(); } catch (error) { // Log error but don't fail - metrics are non-critical console.warn('Failed to track custom metric:', error); } };