refactor: enhance error handling and performance tracking across components

- 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.
This commit is contained in:
2026-01-10 16:53:06 +01:00
parent 20f0ccb85b
commit ca2ed13446
10 changed files with 573 additions and 268 deletions

View File

@@ -57,34 +57,55 @@ export const trackWebVitals = (metric: WebVitalsMetric) => {
// Track page load performance
export const trackPageLoad = () => {
if (typeof window === 'undefined') return;
if (typeof window === 'undefined' || typeof performance === 'undefined') return;
const navigation = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming;
if (navigation) {
trackPerformance({
name: 'page-load',
value: navigation.loadEventEnd - navigation.fetchStart,
url: window.location.pathname,
timestamp: Date.now(),
userAgent: navigator.userAgent,
});
try {
const navigationEntries = performance.getEntriesByType('navigation');
const navigation = navigationEntries[0] as PerformanceNavigationTiming | undefined;
if (navigation && navigation.loadEventEnd && navigation.fetchStart) {
trackPerformance({
name: 'page-load',
value: navigation.loadEventEnd - navigation.fetchStart,
url: window.location.pathname,
timestamp: Date.now(),
userAgent: navigator.userAgent,
});
// Track individual timing phases
trackEvent('page-timing', {
dns: Math.round(navigation.domainLookupEnd - navigation.domainLookupStart),
tcp: Math.round(navigation.connectEnd - navigation.connectStart),
request: Math.round(navigation.responseStart - navigation.requestStart),
response: Math.round(navigation.responseEnd - navigation.responseStart),
dom: Math.round(navigation.domContentLoadedEventEnd - navigation.responseEnd),
load: Math.round(navigation.loadEventEnd - navigation.domContentLoadedEventEnd),
url: window.location.pathname,
});
// Track individual timing phases
trackEvent('page-timing', {
dns: navigation.domainLookupEnd && navigation.domainLookupStart
? Math.round(navigation.domainLookupEnd - navigation.domainLookupStart)
: 0,
tcp: navigation.connectEnd && navigation.connectStart
? Math.round(navigation.connectEnd - navigation.connectStart)
: 0,
request: navigation.responseStart && navigation.requestStart
? Math.round(navigation.responseStart - navigation.requestStart)
: 0,
response: navigation.responseEnd && navigation.responseStart
? Math.round(navigation.responseEnd - navigation.responseStart)
: 0,
dom: navigation.domContentLoadedEventEnd && navigation.responseEnd
? Math.round(navigation.domContentLoadedEventEnd - navigation.responseEnd)
: 0,
load: navigation.loadEventEnd && navigation.domContentLoadedEventEnd
? Math.round(navigation.loadEventEnd - navigation.domContentLoadedEventEnd)
: 0,
url: window.location.pathname,
});
}
} catch (error) {
// Silently fail - performance tracking is not critical
if (process.env.NODE_ENV === 'development') {
console.warn('Error tracking page load:', error);
}
}
};
// Track API response times
export const trackApiCall = (endpoint: string, duration: number, status: number) => {
if (typeof window === 'undefined') return;
trackEvent('api-call', {
endpoint,
duration: Math.round(duration),
@@ -95,6 +116,7 @@ export const trackApiCall = (endpoint: string, duration: number, status: number)
// Track user interactions
export const trackInteraction = (action: string, element?: string) => {
if (typeof window === 'undefined') return;
trackEvent('interaction', {
action,
element,
@@ -104,6 +126,7 @@ export const trackInteraction = (action: string, element?: string) => {
// Track errors
export const trackError = (error: string, context?: string) => {
if (typeof window === 'undefined') return;
trackEvent('error', {
error,
context,

View File

@@ -13,104 +13,192 @@ interface Metric {
// Simple Web Vitals implementation (since we don't want to add external dependencies)
const getCLS = (onPerfEntry: (metric: Metric) => void) => {
let clsValue = 0;
let sessionValue = 0;
let sessionEntries: PerformanceEntry[] = [];
if (typeof window === 'undefined' || typeof PerformanceObserver === 'undefined') return null;
try {
let clsValue = 0;
let sessionValue = 0;
let sessionEntries: PerformanceEntry[] = [];
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (!(entry as PerformanceEntry & { hadRecentInput?: boolean }).hadRecentInput) {
const firstSessionEntry = sessionEntries[0];
const lastSessionEntry = sessionEntries[sessionEntries.length - 1];
const observer = new PerformanceObserver((list) => {
try {
for (const entry of list.getEntries()) {
if (!(entry as PerformanceEntry & { hadRecentInput?: boolean }).hadRecentInput) {
const firstSessionEntry = sessionEntries[0];
const lastSessionEntry = sessionEntries[sessionEntries.length - 1];
if (sessionValue && entry.startTime - lastSessionEntry.startTime < 1000 && entry.startTime - firstSessionEntry.startTime < 5000) {
sessionValue += (entry as PerformanceEntry & { value?: number }).value || 0;
sessionEntries.push(entry);
} else {
sessionValue = (entry as PerformanceEntry & { value?: number }).value || 0;
sessionEntries = [entry];
if (sessionValue && entry.startTime - lastSessionEntry.startTime < 1000 && entry.startTime - firstSessionEntry.startTime < 5000) {
sessionValue += (entry as PerformanceEntry & { value?: number }).value || 0;
sessionEntries.push(entry);
} else {
sessionValue = (entry as PerformanceEntry & { value?: number }).value || 0;
sessionEntries = [entry];
}
if (sessionValue > clsValue) {
clsValue = sessionValue;
onPerfEntry({
name: 'CLS',
value: clsValue,
delta: clsValue,
id: `cls-${Date.now()}`,
});
}
}
}
if (sessionValue > clsValue) {
clsValue = sessionValue;
onPerfEntry({
name: 'CLS',
value: clsValue,
delta: clsValue,
id: `cls-${Date.now()}`,
});
} catch (error) {
// Silently fail - CLS tracking is not critical
if (process.env.NODE_ENV === 'development') {
console.warn('CLS tracking error:', error);
}
}
}
});
});
observer.observe({ type: 'layout-shift', buffered: true });
observer.observe({ type: 'layout-shift', buffered: true });
return observer;
} catch (error) {
if (process.env.NODE_ENV === 'development') {
console.warn('CLS observer initialization failed:', error);
}
return null;
}
};
const getFID = (onPerfEntry: (metric: Metric) => void) => {
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
onPerfEntry({
name: 'FID',
value: (entry as PerformanceEntry & { processingStart?: number }).processingStart! - entry.startTime,
delta: (entry as PerformanceEntry & { processingStart?: number }).processingStart! - entry.startTime,
id: `fid-${Date.now()}`,
});
}
});
if (typeof window === 'undefined' || typeof PerformanceObserver === 'undefined') return null;
try {
const observer = new PerformanceObserver((list) => {
try {
for (const entry of list.getEntries()) {
const processingStart = (entry as PerformanceEntry & { processingStart?: number }).processingStart;
if (processingStart !== undefined) {
onPerfEntry({
name: 'FID',
value: processingStart - entry.startTime,
delta: processingStart - entry.startTime,
id: `fid-${Date.now()}`,
});
}
}
} catch (error) {
if (process.env.NODE_ENV === 'development') {
console.warn('FID tracking error:', error);
}
}
});
observer.observe({ type: 'first-input', buffered: true });
observer.observe({ type: 'first-input', buffered: true });
return observer;
} catch (error) {
if (process.env.NODE_ENV === 'development') {
console.warn('FID observer initialization failed:', error);
}
return null;
}
};
const getFCP = (onPerfEntry: (metric: Metric) => void) => {
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.name === 'first-contentful-paint') {
onPerfEntry({
name: 'FCP',
value: entry.startTime,
delta: entry.startTime,
id: `fcp-${Date.now()}`,
});
if (typeof window === 'undefined' || typeof PerformanceObserver === 'undefined') return null;
try {
const observer = new PerformanceObserver((list) => {
try {
for (const entry of list.getEntries()) {
if (entry.name === 'first-contentful-paint') {
onPerfEntry({
name: 'FCP',
value: entry.startTime,
delta: entry.startTime,
id: `fcp-${Date.now()}`,
});
}
}
} catch (error) {
if (process.env.NODE_ENV === 'development') {
console.warn('FCP tracking error:', error);
}
}
}
});
});
observer.observe({ type: 'paint', buffered: true });
observer.observe({ type: 'paint', buffered: true });
return observer;
} catch (error) {
if (process.env.NODE_ENV === 'development') {
console.warn('FCP observer initialization failed:', error);
}
return null;
}
};
const getLCP = (onPerfEntry: (metric: Metric) => void) => {
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries();
const lastEntry = entries[entries.length - 1];
onPerfEntry({
name: 'LCP',
value: lastEntry.startTime,
delta: lastEntry.startTime,
id: `lcp-${Date.now()}`,
if (typeof window === 'undefined' || typeof PerformanceObserver === 'undefined') return null;
try {
const observer = new PerformanceObserver((list) => {
try {
const entries = list.getEntries();
const lastEntry = entries[entries.length - 1];
if (lastEntry) {
onPerfEntry({
name: 'LCP',
value: lastEntry.startTime,
delta: lastEntry.startTime,
id: `lcp-${Date.now()}`,
});
}
} catch (error) {
if (process.env.NODE_ENV === 'development') {
console.warn('LCP tracking error:', error);
}
}
});
});
observer.observe({ type: 'largest-contentful-paint', buffered: true });
observer.observe({ type: 'largest-contentful-paint', buffered: true });
return observer;
} catch (error) {
if (process.env.NODE_ENV === 'development') {
console.warn('LCP observer initialization failed:', error);
}
return null;
}
};
const getTTFB = (onPerfEntry: (metric: Metric) => void) => {
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.entryType === 'navigation') {
const navEntry = entry as PerformanceNavigationTiming;
onPerfEntry({
name: 'TTFB',
value: navEntry.responseStart - navEntry.fetchStart,
delta: navEntry.responseStart - navEntry.fetchStart,
id: `ttfb-${Date.now()}`,
});
if (typeof window === 'undefined' || typeof PerformanceObserver === 'undefined') return null;
try {
const observer = new PerformanceObserver((list) => {
try {
for (const entry of list.getEntries()) {
if (entry.entryType === 'navigation') {
const navEntry = entry as PerformanceNavigationTiming;
if (navEntry.responseStart && navEntry.fetchStart) {
onPerfEntry({
name: 'TTFB',
value: navEntry.responseStart - navEntry.fetchStart,
delta: navEntry.responseStart - navEntry.fetchStart,
id: `ttfb-${Date.now()}`,
});
}
}
}
} catch (error) {
if (process.env.NODE_ENV === 'development') {
console.warn('TTFB tracking error:', error);
}
}
}
});
});
observer.observe({ type: 'navigation', buffered: true });
observer.observe({ type: 'navigation', buffered: true });
return observer;
} catch (error) {
if (process.env.NODE_ENV === 'development') {
console.warn('TTFB observer initialization failed:', error);
}
return null;
}
};
// Custom hook for Web Vitals tracking
@@ -123,6 +211,7 @@ export const useWebVitals = () => {
const path = window.location.pathname;
const projectMatch = path.match(/\/projects\/([^\/]+)/);
const projectId = projectMatch ? projectMatch[1] : null;
const observers: PerformanceObserver[] = [];
const sendWebVitals = async () => {
if (Object.keys(webVitals).length >= 3) { // Wait for at least FCP, LCP, CLS
@@ -156,7 +245,7 @@ export const useWebVitals = () => {
};
// Track Core Web Vitals
getCLS((metric) => {
const clsObserver = getCLS((metric) => {
webVitals.CLS = metric.value;
trackWebVitals({
...metric,
@@ -165,8 +254,9 @@ export const useWebVitals = () => {
});
sendWebVitals();
});
if (clsObserver) observers.push(clsObserver);
getFID((metric) => {
const fidObserver = getFID((metric) => {
webVitals.FID = metric.value;
trackWebVitals({
...metric,
@@ -175,8 +265,9 @@ export const useWebVitals = () => {
});
sendWebVitals();
});
if (fidObserver) observers.push(fidObserver);
getFCP((metric) => {
const fcpObserver = getFCP((metric) => {
webVitals.FCP = metric.value;
trackWebVitals({
...metric,
@@ -185,8 +276,9 @@ export const useWebVitals = () => {
});
sendWebVitals();
});
if (fcpObserver) observers.push(fcpObserver);
getLCP((metric) => {
const lcpObserver = getLCP((metric) => {
webVitals.LCP = metric.value;
trackWebVitals({
...metric,
@@ -195,8 +287,9 @@ export const useWebVitals = () => {
});
sendWebVitals();
});
if (lcpObserver) observers.push(lcpObserver);
getTTFB((metric) => {
const ttfbObserver = getTTFB((metric) => {
webVitals.TTFB = metric.value;
trackWebVitals({
...metric,
@@ -205,6 +298,7 @@ export const useWebVitals = () => {
});
sendWebVitals();
});
if (ttfbObserver) observers.push(ttfbObserver);
// Track page load performance
const handleLoad = () => {
@@ -226,6 +320,14 @@ export const useWebVitals = () => {
}
return () => {
// Cleanup all observers
observers.forEach(observer => {
try {
observer.disconnect();
} catch (error) {
// Silently fail
}
});
window.removeEventListener('load', handleLoad);
};
}, []);