'use client'; import { useEffect } from 'react'; import { useWebVitals } from '@/lib/useWebVitals'; import { trackEvent, trackPageLoad } from '@/lib/analytics'; interface AnalyticsProviderProps { children: React.ReactNode; } export const AnalyticsProvider: React.FC = ({ children }) => { // Initialize Web Vitals tracking - wrapped to prevent crashes // Hooks must be called unconditionally, but the hook itself handles errors useWebVitals(); useEffect(() => { if (typeof window === 'undefined') return; // Wrap entire effect in try-catch to prevent any errors from breaking the app try { // Track page view const trackPageView = async () => { const path = window.location.pathname; const projectMatch = path.match(/\/projects\/([^\/]+)/); const projectId = projectMatch ? projectMatch[1] : null; // Track to Umami (if available) trackEvent('page-view', { url: path, referrer: document.referrer, timestamp: Date.now(), }); // Track to our API try { await fetch('/api/analytics/track', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ type: 'pageview', projectId: projectId, page: path }) }); } catch (error) { // Silently fail if (process.env.NODE_ENV === 'development') { console.error('Error tracking page view:', error); } } }; // Track page load performance - wrapped in try-catch try { trackPageLoad(); } catch (error) { // Silently fail if (process.env.NODE_ENV === 'development') { console.warn('Error tracking page load:', error); } } // Track initial page view trackPageView(); // Track performance metrics to our API const trackPerformanceToAPI = async () => { try { // Get current page path to extract project ID if on project page const path = window.location.pathname; const projectMatch = path.match(/\/projects\/([^\/]+)/); const projectId = projectMatch ? projectMatch[1] : null; // Wait for page to fully load setTimeout(async () => { try { const navigation = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming | undefined; const paintEntries = performance.getEntriesByType('paint'); const lcpEntries = performance.getEntriesByType('largest-contentful-paint'); const fcp = paintEntries.find((e: PerformanceEntry) => e.name === 'first-contentful-paint'); const lcp = lcpEntries.length > 0 ? lcpEntries[lcpEntries.length - 1] : undefined; const performanceData = { loadTime: navigation && navigation.loadEventEnd && navigation.fetchStart ? navigation.loadEventEnd - navigation.fetchStart : 0, fcp: fcp ? fcp.startTime : 0, lcp: lcp ? lcp.startTime : 0, ttfb: navigation && navigation.responseStart && navigation.fetchStart ? navigation.responseStart - navigation.fetchStart : 0, cls: 0, // Will be updated by CLS observer fid: 0, // Will be updated by FID observer si: 0 // Speed Index - would need to calculate }; // Send performance data await fetch('/api/analytics/track', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ type: 'performance', projectId: projectId, page: path, performance: performanceData }) }); } catch (error) { // Silently fail - performance tracking is not critical if (process.env.NODE_ENV === 'development') { console.warn('Error collecting performance data:', error); } } }, 2000); // Wait 2 seconds for page to stabilize } catch (error) { // Silently fail if (process.env.NODE_ENV === 'development') { console.error('Error tracking performance:', error); } } }; // Track performance after page load if (document.readyState === 'complete') { trackPerformanceToAPI(); } else { window.addEventListener('load', trackPerformanceToAPI); } // Track route changes (for SPA navigation) const handleRouteChange = () => { setTimeout(() => { trackPageView(); trackPageLoad(); }, 100); }; // Listen for popstate events (back/forward navigation) window.addEventListener('popstate', handleRouteChange); // Track user interactions const handleClick = (event: MouseEvent) => { try { if (typeof window === 'undefined') return; const target = event.target as HTMLElement | null; if (!target) return; const element = target.tagName ? target.tagName.toLowerCase() : 'unknown'; const className = target.className; const id = target.id; trackEvent('click', { element, className: (typeof className === 'string' && className) ? className.split(' ')[0] : undefined, id: id || undefined, url: window.location.pathname, }); } catch (error) { // Silently fail - click tracking is not critical if (process.env.NODE_ENV === 'development') { console.warn('Error tracking click:', error); } } }; // Track form submissions const handleSubmit = (event: SubmitEvent) => { try { if (typeof window === 'undefined') return; const form = event.target as HTMLFormElement | null; if (!form) return; trackEvent('form-submit', { formId: form.id || undefined, formClass: form.className || undefined, url: window.location.pathname, }); } catch (error) { // Silently fail - form tracking is not critical if (process.env.NODE_ENV === 'development') { console.warn('Error tracking form submit:', error); } } }; // Track scroll depth let maxScrollDepth = 0; const handleScroll = () => { try { if (typeof window === 'undefined' || typeof document === 'undefined') return; const scrollHeight = document.documentElement.scrollHeight; const innerHeight = window.innerHeight; if (scrollHeight <= innerHeight) return; // No scrollable content const scrollDepth = Math.round( (window.scrollY / (scrollHeight - innerHeight)) * 100 ); if (scrollDepth > maxScrollDepth) { maxScrollDepth = scrollDepth; // Track scroll milestones if (scrollDepth >= 25 && scrollDepth < 50 && maxScrollDepth >= 25) { trackEvent('scroll-depth', { depth: 25, url: window.location.pathname }); } else if (scrollDepth >= 50 && scrollDepth < 75 && maxScrollDepth >= 50) { trackEvent('scroll-depth', { depth: 50, url: window.location.pathname }); } else if (scrollDepth >= 75 && scrollDepth < 90 && maxScrollDepth >= 75) { trackEvent('scroll-depth', { depth: 75, url: window.location.pathname }); } else if (scrollDepth >= 90 && maxScrollDepth >= 90) { trackEvent('scroll-depth', { depth: 90, url: window.location.pathname }); } } } catch (error) { // Silently fail - scroll tracking is not critical if (process.env.NODE_ENV === 'development') { console.warn('Error tracking scroll:', error); } } }; // Add event listeners document.addEventListener('click', handleClick); document.addEventListener('submit', handleSubmit); window.addEventListener('scroll', handleScroll, { passive: true }); // Track errors const handleError = (event: ErrorEvent) => { try { if (typeof window === 'undefined') return; trackEvent('error', { message: event.message || 'Unknown error', filename: event.filename || undefined, lineno: event.lineno || undefined, colno: event.colno || undefined, url: window.location.pathname, }); } catch (error) { // Silently fail - error tracking should not cause more errors if (process.env.NODE_ENV === 'development') { console.warn('Error tracking error event:', error); } } }; const handleUnhandledRejection = (event: PromiseRejectionEvent) => { try { if (typeof window === 'undefined') return; trackEvent('unhandled-rejection', { reason: event.reason?.toString() || 'Unknown rejection', url: window.location.pathname, }); } catch (error) { // Silently fail - error tracking should not cause more errors if (process.env.NODE_ENV === 'development') { console.warn('Error tracking unhandled rejection:', error); } } }; window.addEventListener('error', handleError); window.addEventListener('unhandledrejection', handleUnhandledRejection); // Cleanup return () => { try { window.removeEventListener('popstate', handleRouteChange); document.removeEventListener('click', handleClick); document.removeEventListener('submit', handleSubmit); window.removeEventListener('scroll', handleScroll); window.removeEventListener('error', handleError); window.removeEventListener('unhandledrejection', handleUnhandledRejection); } catch { // Silently fail during cleanup } }; } catch (error) { // If anything fails, log but don't break the app if (process.env.NODE_ENV === 'development') { console.error('AnalyticsProvider initialization error:', error); } // Return empty cleanup function return () => {}; } }, []); // Always render children, even if analytics fails return <>{children}; };