Fix white screen: add error boundaries and improve error handling in AnalyticsProvider and useWebVitals

This commit is contained in:
2026-01-10 17:07:00 +01:00
parent 2a260abe0a
commit 832b468ea7
4 changed files with 91 additions and 43 deletions

View File

@@ -4,6 +4,7 @@ import React, { useEffect, useState } from "react";
import { usePathname } from "next/navigation"; import { usePathname } from "next/navigation";
import dynamic from "next/dynamic"; import dynamic from "next/dynamic";
import { ToastProvider } from "@/components/Toast"; import { ToastProvider } from "@/components/Toast";
import ErrorBoundary from "@/components/ErrorBoundary";
import { AnalyticsProvider } from "@/components/AnalyticsProvider"; import { AnalyticsProvider } from "@/components/AnalyticsProvider";
// Dynamic import with SSR disabled to avoid framer-motion issues // Dynamic import with SSR disabled to avoid framer-motion issues
@@ -46,13 +47,20 @@ export default function ClientProviders({
}; };
}, [pathname]); }, [pathname]);
// Wrap in multiple error boundaries to isolate failures
return ( return (
<AnalyticsProvider> <ErrorBoundary>
<ToastProvider> <ErrorBoundary>
{mounted && <BackgroundBlobs />} <AnalyticsProvider>
<div className="relative z-10">{children}</div> <ErrorBoundary>
{mounted && !is404Page && <ChatWidget />} <ToastProvider>
</ToastProvider> {mounted && <BackgroundBlobs />}
</AnalyticsProvider> <div className="relative z-10">{children}</div>
{mounted && !is404Page && <ChatWidget />}
</ToastProvider>
</ErrorBoundary>
</AnalyticsProvider>
</ErrorBoundary>
</ErrorBoundary>
); );
} }

View File

@@ -9,12 +9,16 @@ interface AnalyticsProviderProps {
} }
export const AnalyticsProvider: React.FC<AnalyticsProviderProps> = ({ children }) => { export const AnalyticsProvider: React.FC<AnalyticsProviderProps> = ({ children }) => {
// Initialize Web Vitals tracking // Initialize Web Vitals tracking - wrapped to prevent crashes
// Hooks must be called unconditionally, but the hook itself handles errors
useWebVitals(); useWebVitals();
useEffect(() => { useEffect(() => {
if (typeof window === 'undefined') return; if (typeof window === 'undefined') return;
// Wrap entire effect in try-catch to prevent any errors from breaking the app
try {
// Track page view // Track page view
const trackPageView = async () => { const trackPageView = async () => {
const path = window.location.pathname; const path = window.location.pathname;
@@ -49,8 +53,15 @@ export const AnalyticsProvider: React.FC<AnalyticsProviderProps> = ({ children }
} }
}; };
// Track page load performance // Track page load performance - wrapped in try-catch
trackPageLoad(); try {
trackPageLoad();
} catch (error) {
// Silently fail
if (process.env.NODE_ENV === 'development') {
console.warn('Error tracking page load:', error);
}
}
// Track initial page view // Track initial page view
trackPageView(); trackPageView();
@@ -255,16 +266,29 @@ export const AnalyticsProvider: React.FC<AnalyticsProviderProps> = ({ children }
window.addEventListener('error', handleError); window.addEventListener('error', handleError);
window.addEventListener('unhandledrejection', handleUnhandledRejection); window.addEventListener('unhandledrejection', handleUnhandledRejection);
// Cleanup // Cleanup
return () => { return () => {
window.removeEventListener('popstate', handleRouteChange); try {
document.removeEventListener('click', handleClick); window.removeEventListener('popstate', handleRouteChange);
document.removeEventListener('submit', handleSubmit); document.removeEventListener('click', handleClick);
window.removeEventListener('scroll', handleScroll); document.removeEventListener('submit', handleSubmit);
window.removeEventListener('error', handleError); window.removeEventListener('scroll', handleScroll);
window.removeEventListener('unhandledrejection', handleUnhandledRejection); 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}</>; return <>{children}</>;
}; };

View File

@@ -22,17 +22,19 @@ export default class ErrorBoundary extends React.Component<
render() { render() {
if (this.state.hasError) { if (this.state.hasError) {
return ( // Still render children to prevent white screen - just log the error
<div className="p-4 m-4 bg-red-50 border border-red-200 rounded text-red-800"> if (process.env.NODE_ENV === 'development') {
<h2>Something went wrong!</h2> return (
<button <div>
className="mt-2 px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700" <div className="p-2 m-2 bg-yellow-50 border border-yellow-200 rounded text-yellow-800 text-xs">
onClick={() => this.setState({ hasError: false })} Error boundary triggered - rendering children anyway
> </div>
Try again {this.props.children}
</button> </div>
</div> );
); }
// In production, just render children silently
return this.props.children;
} }
return this.props.children; return this.props.children;

View File

@@ -206,12 +206,14 @@ export const useWebVitals = () => {
useEffect(() => { useEffect(() => {
if (typeof window === 'undefined') return; if (typeof window === 'undefined') return;
// Store web vitals for batch sending // Wrap everything in try-catch to prevent errors from breaking the app
const webVitals: Record<string, number> = {}; try {
const path = window.location.pathname; // Store web vitals for batch sending
const projectMatch = path.match(/\/projects\/([^\/]+)/); const webVitals: Record<string, number> = {};
const projectId = projectMatch ? projectMatch[1] : null; const path = window.location.pathname;
const observers: PerformanceObserver[] = []; const projectMatch = path.match(/\/projects\/([^\/]+)/);
const projectId = projectMatch ? projectMatch[1] : null;
const observers: PerformanceObserver[] = [];
const sendWebVitals = async () => { const sendWebVitals = async () => {
if (Object.keys(webVitals).length >= 3) { // Wait for at least FCP, LCP, CLS if (Object.keys(webVitals).length >= 3) { // Wait for at least FCP, LCP, CLS
@@ -319,16 +321,28 @@ export const useWebVitals = () => {
window.addEventListener('load', handleLoad); window.addEventListener('load', handleLoad);
} }
return () => { return () => {
// Cleanup all observers // Cleanup all observers
observers.forEach(observer => { observers.forEach(observer => {
try {
observer.disconnect();
} catch {
// Silently fail
}
});
try { try {
observer.disconnect(); window.removeEventListener('load', handleLoad);
} catch { } catch {
// Silently fail // Silently fail
} }
}); };
window.removeEventListener('load', handleLoad); } catch (error) {
}; // If Web Vitals initialization fails, don't break the app
if (process.env.NODE_ENV === 'development') {
console.warn('Web Vitals initialization failed:', error);
}
// Return empty cleanup function
return () => {};
}
}, []); }, []);
}; };