'use client'; import { useEffect, useRef, useCallback } from 'react'; import { useWebVitals } from '@/lib/useWebVitals'; import { trackEvent, trackPageLoad } from '@/lib/analytics'; import { debounce } from '@/lib/utils'; interface AnalyticsProviderProps { children: React.ReactNode; } export const AnalyticsProvider: React.FC = ({ children }) => { const hasTrackedInitialView = useRef(false); const hasTrackedPerformance = useRef(false); const currentPath = useRef(''); // Initialize Web Vitals tracking - wrapped to prevent crashes // Hooks must be called unconditionally, but the hook itself handles errors useWebVitals(); // Track page view - memoized to prevent recreation const trackPageView = useCallback(async () => { if (typeof window === 'undefined') return; const path = window.location.pathname; // Only track if path has changed (prevents duplicate tracking) if (currentPath.current === path && hasTrackedInitialView.current) { return; } currentPath.current = path; hasTrackedInitialView.current = true; 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 - single call 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); } } }, []); useEffect(() => { if (typeof window === 'undefined') return; // Wrap entire effect in try-catch to prevent any errors from breaking the app try { // 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 - only once const trackPerformanceToAPI = async () => { // Prevent duplicate tracking if (hasTrackedPerformance.current) return; hasTrackedPerformance.current = true; try { if (typeof performance === "undefined" || typeof performance.getEntriesByType !== "function") { return; } // 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 - single call 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); } } }, 2500); // Wait 2.5 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, { once: true }); } // Track route changes (for SPA navigation) - debounced const handleRouteChange = debounce(() => { // Track new page view (trackPageView will handle path change detection) trackPageView(); trackPageLoad(); }, 300); // Listen for popstate events (back/forward navigation) window.addEventListener('popstate', handleRouteChange); // Track user interactions - debounced to prevent spam const handleClick = debounce((event: unknown) => { try { if (typeof window === 'undefined') return; const mouseEvent = event as MouseEvent; const target = mouseEvent.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); } } }, 500); // 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 - debounced let maxScrollDepth = 0; const firedScrollMilestones = new Set(); const handleScroll = debounce(() => { 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 each milestone once (avoid spamming events on every scroll tick) const milestones = [25, 50, 75, 90]; for (const milestone of milestones) { if (maxScrollDepth >= milestone && !firedScrollMilestones.has(milestone)) { firedScrollMilestones.add(milestone); trackEvent('scroll-depth', { depth: milestone, 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); } } }, 1000); // 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 { // Cancel any pending debounced calls to prevent memory leaks handleRouteChange.cancel(); handleClick.cancel(); handleScroll.cancel(); // Remove event listeners window.removeEventListener('load', trackPerformanceToAPI); 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 () => {}; } }, [trackPageView]); // Always render children, even if analytics fails return <>{children}; };