- Improve localStorage access in ActivityFeed, ChatWidget, and AdminPage with try-catch blocks to handle potential errors gracefully. - Update performance tracking in AnalyticsProvider and analytics.ts to ensure robust error handling and prevent failures from affecting user experience. - Refactor Web Vitals tracking to include error handling for observer initialization and data collection. - Ensure consistent handling of hydration mismatches in components like BackgroundBlobs and ChatWidget to improve rendering reliability.
271 lines
9.3 KiB
TypeScript
271 lines
9.3 KiB
TypeScript
'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<AnalyticsProviderProps> = ({ children }) => {
|
|
// Initialize Web Vitals tracking
|
|
useWebVitals();
|
|
|
|
useEffect(() => {
|
|
if (typeof window === 'undefined') return;
|
|
|
|
// 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
|
|
trackPageLoad();
|
|
|
|
// 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 () => {
|
|
window.removeEventListener('popstate', handleRouteChange);
|
|
document.removeEventListener('click', handleClick);
|
|
document.removeEventListener('submit', handleSubmit);
|
|
window.removeEventListener('scroll', handleScroll);
|
|
window.removeEventListener('error', handleError);
|
|
window.removeEventListener('unhandledrejection', handleUnhandledRejection);
|
|
};
|
|
}, []);
|
|
|
|
return <>{children}</>;
|
|
};
|