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

@@ -126,29 +126,30 @@ export async function POST(request: NextRequest) {
await prisma.project.update({ await prisma.project.update({
where: { id: projectIdNum }, where: { id: projectIdNum },
data: { data: {
performance: { performance: {
...perf, ...perf,
lighthouse: lighthouseScore, lighthouse: lighthouseScore,
loadTime: performance.loadTime || perf.loadTime || 0, loadTime: performance.loadTime || perf.loadTime || 0,
firstContentfulPaint: fcp || perf.firstContentfulPaint || 0, firstContentfulPaint: fcp || perf.firstContentfulPaint || 0,
largestContentfulPaint: lcp || perf.largestContentfulPaint || 0, largestContentfulPaint: lcp || perf.largestContentfulPaint || 0,
cumulativeLayoutShift: cls || perf.cumulativeLayoutShift || 0, cumulativeLayoutShift: cls || perf.cumulativeLayoutShift || 0,
totalBlockingTime: performance.tbt || perf.totalBlockingTime || 0, totalBlockingTime: performance.tbt || perf.totalBlockingTime || 0,
speedIndex: performance.si || perf.speedIndex || 0, speedIndex: performance.si || perf.speedIndex || 0,
coreWebVitals: { coreWebVitals: {
lcp: lcp || (perf.coreWebVitals as Record<string, unknown>)?.lcp || 0, lcp: lcp || (perf.coreWebVitals as Record<string, unknown>)?.lcp || 0,
fid: fid || (perf.coreWebVitals as Record<string, unknown>)?.fid || 0, fid: fid || (perf.coreWebVitals as Record<string, unknown>)?.fid || 0,
cls: cls || (perf.coreWebVitals as Record<string, unknown>)?.cls || 0 cls: cls || (perf.coreWebVitals as Record<string, unknown>)?.cls || 0
},
lastUpdated: new Date().toISOString()
}, },
lastUpdated: new Date().toISOString() analytics: {
}, ...analytics,
analytics: { lastUpdated: new Date().toISOString()
...analytics, }
lastUpdated: new Date().toISOString()
} }
} });
}); }
} }
} }
} }

View File

@@ -57,8 +57,16 @@ export default function ActivityFeed() {
const [isTrackingEnabled, setIsTrackingEnabled] = useState(() => { const [isTrackingEnabled, setIsTrackingEnabled] = useState(() => {
// Check localStorage for tracking preference // Check localStorage for tracking preference
if (typeof window !== "undefined") { if (typeof window !== "undefined") {
const stored = localStorage.getItem("activityTrackingEnabled"); try {
return stored !== "false"; // Default to true if not set const stored = localStorage.getItem("activityTrackingEnabled");
return stored !== "false"; // Default to true if not set
} catch (error) {
// localStorage might be disabled
if (process.env.NODE_ENV === 'development') {
console.warn('Failed to read tracking preference:', error);
}
return true; // Default to enabled
}
} }
return true; return true;
}); });
@@ -1385,7 +1393,14 @@ export default function ActivityFeed() {
const newValue = !isTrackingEnabled; const newValue = !isTrackingEnabled;
setIsTrackingEnabled(newValue); setIsTrackingEnabled(newValue);
if (typeof window !== "undefined") { if (typeof window !== "undefined") {
localStorage.setItem("activityTrackingEnabled", String(newValue)); try {
localStorage.setItem("activityTrackingEnabled", String(newValue));
} catch (error) {
// localStorage might be full or disabled
if (process.env.NODE_ENV === 'development') {
console.warn('Failed to save tracking preference:', error);
}
}
} }
// Clear data when disabling // Clear data when disabling
if (!newValue) { if (!newValue) {

View File

@@ -20,21 +20,47 @@ interface Message {
} }
export default function ChatWidget() { export default function ChatWidget() {
// Prevent hydration mismatch by only rendering after mount
const [mounted, setMounted] = useState(false);
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const [messages, setMessages] = useState<Message[]>([]); const [messages, setMessages] = useState<Message[]>([]);
const [inputValue, setInputValue] = useState(""); const [inputValue, setInputValue] = useState("");
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [conversationId, setConversationId] = useState(() => { const [conversationId, setConversationId] = useState<string>("default");
// Generate or retrieve conversation ID
if (typeof window !== "undefined") { useEffect(() => {
setMounted(true);
// Generate or retrieve conversation ID only on client
try {
const stored = localStorage.getItem("chatSessionId"); const stored = localStorage.getItem("chatSessionId");
if (stored) return stored; if (stored) {
const newId = crypto.randomUUID(); setConversationId(stored);
return;
}
// Generate UUID with fallback for browsers without crypto.randomUUID
let newId: string;
if (typeof crypto !== "undefined" && crypto.randomUUID) {
newId = crypto.randomUUID();
} else {
// Fallback UUID generation
newId = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
const r = Math.random() * 16 | 0;
const v = c === 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}
localStorage.setItem("chatSessionId", newId); localStorage.setItem("chatSessionId", newId);
return newId; setConversationId(newId);
} catch (error) {
// localStorage might be disabled or full
if (process.env.NODE_ENV === 'development') {
console.warn('Failed to access localStorage for chat session:', error);
}
setConversationId(`session-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`);
} }
return "default"; }, []);
});
const messagesEndRef = useRef<HTMLDivElement>(null); const messagesEndRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<HTMLInputElement>(null); const inputRef = useRef<HTMLInputElement>(null);
@@ -62,22 +88,55 @@ export default function ChatWidget() {
// Load messages from localStorage // Load messages from localStorage
useEffect(() => { useEffect(() => {
if (typeof window !== "undefined") { if (typeof window !== "undefined") {
const stored = localStorage.getItem("chatMessages"); try {
if (stored) { const stored = localStorage.getItem("chatMessages");
try { if (stored) {
const parsed = JSON.parse(stored); try {
setMessages( const parsed = JSON.parse(stored);
parsed.map((m: Message) => ({ setMessages(
...m, parsed.map((m: Message) => ({
text: decodeHtmlEntities(m.text), // Decode HTML entities when loading ...m,
timestamp: new Date(m.timestamp), text: decodeHtmlEntities(m.text), // Decode HTML entities when loading
})), timestamp: new Date(m.timestamp),
); })),
} catch (e) { );
console.error("Failed to load chat history", e); } catch (e) {
if (process.env.NODE_ENV === 'development') {
console.error("Failed to parse chat history", e);
}
// Clear corrupted data
try {
localStorage.removeItem("chatMessages");
} catch {
// Ignore cleanup errors
}
// Add welcome message
setMessages([
{
id: "welcome",
text: "Hi! I'm Dennis's AI assistant. Ask me anything about his skills, projects, or experience! 🚀",
sender: "bot",
timestamp: new Date(),
},
]);
}
} else {
// Add welcome message
setMessages([
{
id: "welcome",
text: "Hi! I'm Dennis's AI assistant. Ask me anything about his skills, projects, or experience! 🚀",
sender: "bot",
timestamp: new Date(),
},
]);
} }
} else { } catch (error) {
// Add welcome message // localStorage might be disabled
if (process.env.NODE_ENV === 'development') {
console.warn("Failed to load chat history from localStorage:", error);
}
// Add welcome message anyway
setMessages([ setMessages([
{ {
id: "welcome", id: "welcome",
@@ -93,7 +152,14 @@ export default function ChatWidget() {
// Save messages to localStorage // Save messages to localStorage
useEffect(() => { useEffect(() => {
if (typeof window !== "undefined" && messages.length > 0) { if (typeof window !== "undefined" && messages.length > 0) {
localStorage.setItem("chatMessages", JSON.stringify(messages)); try {
localStorage.setItem("chatMessages", JSON.stringify(messages));
} catch (error) {
// localStorage might be full or disabled
if (process.env.NODE_ENV === 'development') {
console.warn("Failed to save chat messages to localStorage:", error);
}
}
} }
}, [messages]); }, [messages]);
@@ -204,6 +270,11 @@ export default function ChatWidget() {
]); ]);
}; };
// Don't render until mounted to prevent hydration mismatch
if (!mounted) {
return null;
}
return ( return (
<> <>
{/* Chat Button */} {/* Chat Button */}

View File

@@ -1,13 +1,21 @@
"use client"; "use client";
import React, { useEffect, useState, Suspense, lazy } from "react"; import React, { useEffect, useState } from "react";
import { usePathname } from "next/navigation"; import { usePathname } from "next/navigation";
import dynamic from "next/dynamic";
import { ToastProvider } from "@/components/Toast"; import { ToastProvider } from "@/components/Toast";
import { AnalyticsProvider } from "@/components/AnalyticsProvider"; import { AnalyticsProvider } from "@/components/AnalyticsProvider";
// Lazy load heavy components to avoid webpack issues // Dynamic import with SSR disabled to avoid framer-motion issues
const BackgroundBlobs = lazy(() => import("@/components/BackgroundBlobs")); const BackgroundBlobs = dynamic(() => import("@/components/BackgroundBlobs").catch(() => ({ default: () => null })), {
const ChatWidget = lazy(() => import("./ChatWidget")); ssr: false,
loading: () => null,
});
const ChatWidget = dynamic(() => import("./ChatWidget").catch(() => ({ default: () => null })), {
ssr: false,
loading: () => null,
});
export default function ClientProviders({ export default function ClientProviders({
children, children,
@@ -41,17 +49,9 @@ export default function ClientProviders({
return ( return (
<AnalyticsProvider> <AnalyticsProvider>
<ToastProvider> <ToastProvider>
{mounted && ( {mounted && <BackgroundBlobs />}
<Suspense fallback={null}>
<BackgroundBlobs />
</Suspense>
)}
<div className="relative z-10">{children}</div> <div className="relative z-10">{children}</div>
{mounted && !is404Page && ( {mounted && !is404Page && <ChatWidget />}
<Suspense fallback={null}>
<ChatWidget />
</Suspense>
)}
</ToastProvider> </ToastProvider>
</AnalyticsProvider> </AnalyticsProvider>
); );

View File

@@ -57,25 +57,42 @@ const AdminPage = () => {
// Check if user is locked out // Check if user is locked out
const checkLockout = useCallback(() => { const checkLockout = useCallback(() => {
const lockoutData = localStorage.getItem('admin_lockout'); if (typeof window === 'undefined') return false;
if (lockoutData) {
try { try {
const { timestamp, attempts } = JSON.parse(lockoutData); const lockoutData = localStorage.getItem('admin_lockout');
const now = Date.now(); if (lockoutData) {
try {
const { timestamp, attempts } = JSON.parse(lockoutData);
const now = Date.now();
if (now - timestamp < LOCKOUT_DURATION) { if (now - timestamp < LOCKOUT_DURATION) {
setAuthState(prev => ({ setAuthState(prev => ({
...prev, ...prev,
isLocked: true, isLocked: true,
attempts, attempts,
isLoading: false isLoading: false
})); }));
return true; return true;
} else { } else {
localStorage.removeItem('admin_lockout'); try {
localStorage.removeItem('admin_lockout');
} catch {
// Ignore errors
}
}
} catch {
try {
localStorage.removeItem('admin_lockout');
} catch {
// Ignore errors
}
} }
} catch { }
localStorage.removeItem('admin_lockout'); } catch (error) {
// localStorage might be disabled
if (process.env.NODE_ENV === 'development') {
console.warn('Failed to check lockout status:', error);
} }
} }
return false; return false;
@@ -197,7 +214,11 @@ const AdminPage = () => {
attempts: 0, attempts: 0,
isLoading: false isLoading: false
})); }));
localStorage.removeItem('admin_lockout'); try {
localStorage.removeItem('admin_lockout');
} catch {
// Ignore errors
}
} else { } else {
const newAttempts = authState.attempts + 1; const newAttempts = authState.attempts + 1;
setAuthState(prev => ({ setAuthState(prev => ({
@@ -208,10 +229,17 @@ const AdminPage = () => {
})); }));
if (newAttempts >= 5) { if (newAttempts >= 5) {
localStorage.setItem('admin_lockout', JSON.stringify({ try {
timestamp: Date.now(), localStorage.setItem('admin_lockout', JSON.stringify({
attempts: newAttempts timestamp: Date.now(),
})); attempts: newAttempts
}));
} catch (error) {
// localStorage might be full or disabled
if (process.env.NODE_ENV === 'development') {
console.warn('Failed to save lockout data:', error);
}
}
setAuthState(prev => ({ setAuthState(prev => ({
...prev, ...prev,
isLocked: true, isLocked: true,
@@ -252,7 +280,11 @@ const AdminPage = () => {
<p className="text-stone-500">Too many failed attempts. Please try again in 15 minutes.</p> <p className="text-stone-500">Too many failed attempts. Please try again in 15 minutes.</p>
<button <button
onClick={() => { onClick={() => {
localStorage.removeItem('admin_lockout'); try {
localStorage.removeItem('admin_lockout');
} catch (error) {
// Ignore errors
}
window.location.reload(); window.location.reload();
}} }}
className="mt-4 px-6 py-2 bg-stone-900 text-stone-50 rounded-xl hover:bg-stone-800 transition-colors" className="mt-4 px-6 py-2 bg-stone-900 text-stone-50 rounded-xl hover:bg-stone-800 transition-colors"

View File

@@ -65,36 +65,43 @@ export const AnalyticsProvider: React.FC<AnalyticsProviderProps> = ({ children }
// Wait for page to fully load // Wait for page to fully load
setTimeout(async () => { setTimeout(async () => {
const navigation = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming; try {
const paintEntries = performance.getEntriesByType('paint'); const navigation = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming | undefined;
const lcpEntries = performance.getEntriesByType('largest-contentful-paint'); 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[lcpEntries.length - 1]; 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 : 0, const performanceData = {
fcp: fcp ? fcp.startTime : 0, loadTime: navigation && navigation.loadEventEnd && navigation.fetchStart ? navigation.loadEventEnd - navigation.fetchStart : 0,
lcp: lcp ? lcp.startTime : 0, fcp: fcp ? fcp.startTime : 0,
ttfb: navigation ? navigation.responseStart - navigation.fetchStart : 0, lcp: lcp ? lcp.startTime : 0,
cls: 0, // Will be updated by CLS observer ttfb: navigation && navigation.responseStart && navigation.fetchStart ? navigation.responseStart - navigation.fetchStart : 0,
fid: 0, // Will be updated by FID observer cls: 0, // Will be updated by CLS observer
si: 0 // Speed Index - would need to calculate fid: 0, // Will be updated by FID observer
}; si: 0 // Speed Index - would need to calculate
};
// Send performance data // Send performance data
await fetch('/api/analytics/track', { await fetch('/api/analytics/track', {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
body: JSON.stringify({ body: JSON.stringify({
type: 'performance', type: 'performance',
projectId: projectId, projectId: projectId,
page: path, page: path,
performance: performanceData 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 }, 2000); // Wait 2 seconds for page to stabilize
} catch (error) { } catch (error) {
// Silently fail // Silently fail
@@ -124,48 +131,84 @@ export const AnalyticsProvider: React.FC<AnalyticsProviderProps> = ({ children }
// Track user interactions // Track user interactions
const handleClick = (event: MouseEvent) => { const handleClick = (event: MouseEvent) => {
const target = event.target as HTMLElement; try {
const element = target.tagName.toLowerCase(); if (typeof window === 'undefined') return;
const className = target.className;
const id = target.id; const target = event.target as HTMLElement | null;
if (!target) return;
trackEvent('click', {
element, const element = target.tagName ? target.tagName.toLowerCase() : 'unknown';
className: (typeof className === 'string' && className) ? className.split(' ')[0] : undefined, const className = target.className;
id: id || undefined, const id = target.id;
url: window.location.pathname,
}); 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 // Track form submissions
const handleSubmit = (event: SubmitEvent) => { const handleSubmit = (event: SubmitEvent) => {
const form = event.target as HTMLFormElement; try {
trackEvent('form-submit', { if (typeof window === 'undefined') return;
formId: form.id || undefined,
formClass: form.className || undefined, const form = event.target as HTMLFormElement | null;
url: window.location.pathname, 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 // Track scroll depth
let maxScrollDepth = 0; let maxScrollDepth = 0;
const handleScroll = () => { const handleScroll = () => {
const scrollDepth = Math.round( try {
(window.scrollY / (document.documentElement.scrollHeight - window.innerHeight)) * 100 if (typeof window === 'undefined' || typeof document === 'undefined') return;
);
if (scrollDepth > maxScrollDepth) {
maxScrollDepth = scrollDepth;
// Track scroll milestones const scrollHeight = document.documentElement.scrollHeight;
if (scrollDepth >= 25 && scrollDepth < 50 && maxScrollDepth >= 25) { const innerHeight = window.innerHeight;
trackEvent('scroll-depth', { depth: 25, url: window.location.pathname });
} else if (scrollDepth >= 50 && scrollDepth < 75 && maxScrollDepth >= 50) { if (scrollHeight <= innerHeight) return; // No scrollable content
trackEvent('scroll-depth', { depth: 50, url: window.location.pathname });
} else if (scrollDepth >= 75 && scrollDepth < 90 && maxScrollDepth >= 75) { const scrollDepth = Math.round(
trackEvent('scroll-depth', { depth: 75, url: window.location.pathname }); (window.scrollY / (scrollHeight - innerHeight)) * 100
} else if (scrollDepth >= 90 && maxScrollDepth >= 90) { );
trackEvent('scroll-depth', { depth: 90, url: window.location.pathname });
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);
} }
} }
}; };
@@ -177,20 +220,36 @@ export const AnalyticsProvider: React.FC<AnalyticsProviderProps> = ({ children }
// Track errors // Track errors
const handleError = (event: ErrorEvent) => { const handleError = (event: ErrorEvent) => {
trackEvent('error', { try {
message: event.message, if (typeof window === 'undefined') return;
filename: event.filename, trackEvent('error', {
lineno: event.lineno, message: event.message || 'Unknown error',
colno: event.colno, filename: event.filename || undefined,
url: window.location.pathname, 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) => { const handleUnhandledRejection = (event: PromiseRejectionEvent) => {
trackEvent('unhandled-rejection', { try {
reason: event.reason?.toString(), if (typeof window === 'undefined') return;
url: window.location.pathname, 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('error', handleError);

View File

@@ -27,7 +27,16 @@ const BackgroundBlobs = () => {
const x5 = useTransform(springX, (value) => value / 15); const x5 = useTransform(springX, (value) => value / 15);
const y5 = useTransform(springY, (value) => value / 15); const y5 = useTransform(springY, (value) => value / 15);
// Prevent hydration mismatch
const [mounted, setMounted] = useState(false);
useEffect(() => { useEffect(() => {
setMounted(true);
}, []);
useEffect(() => {
if (!mounted) return;
const handleMouseMove = (e: MouseEvent) => { const handleMouseMove = (e: MouseEvent) => {
const x = e.clientX - window.innerWidth / 2; const x = e.clientX - window.innerWidth / 2;
const y = e.clientY - window.innerHeight / 2; const y = e.clientY - window.innerHeight / 2;
@@ -37,14 +46,7 @@ const BackgroundBlobs = () => {
window.addEventListener("mousemove", handleMouseMove); window.addEventListener("mousemove", handleMouseMove);
return () => window.removeEventListener("mousemove", handleMouseMove); return () => window.removeEventListener("mousemove", handleMouseMove);
}, [mouseX, mouseY]); }, [mouseX, mouseY, mounted]);
// Prevent hydration mismatch
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
if (!mounted) return null; if (!mounted) return null;

View File

@@ -157,7 +157,7 @@ const ModernAdminDashboard: React.FC<ModernAdminDashboardProps> = ({ isAuthentic
const stats = { const stats = {
totalProjects: projects.length, totalProjects: projects.length,
publishedProjects: projects.filter(p => p.published).length, publishedProjects: projects.filter(p => p.published).length,
totalViews: (analytics?.overview?.totalViews as number) || (analytics?.totalViews as number) || projects.reduce((sum, p) => sum + (p.analytics?.views || 0), 0), totalViews: ((analytics?.overview as Record<string, unknown>)?.totalViews as number) || (analytics?.totalViews as number) || projects.reduce((sum, p) => sum + (p.analytics?.views || 0), 0),
unreadEmails: emails.filter(e => !(e.read as boolean)).length, unreadEmails: emails.filter(e => !(e.read as boolean)).length,
avgPerformance: (() => { avgPerformance: (() => {
// Only show real performance data, not defaults // Only show real performance data, not defaults
@@ -172,9 +172,9 @@ const ModernAdminDashboard: React.FC<ModernAdminDashboardProps> = ({ isAuthentic
}, 0) / projectsWithPerf.length); }, 0) / projectsWithPerf.length);
})(), })(),
systemHealth: (systemStats?.status as string) || 'unknown', systemHealth: (systemStats?.status as string) || 'unknown',
totalUsers: (analytics?.metrics?.totalUsers as number) || (analytics?.totalUsers as number) || 0, totalUsers: ((analytics?.metrics as Record<string, unknown>)?.totalUsers as number) || (analytics?.totalUsers as number) || 0,
bounceRate: (analytics?.metrics?.bounceRate as number) || (analytics?.bounceRate as number) || 0, bounceRate: ((analytics?.metrics as Record<string, unknown>)?.bounceRate as number) || (analytics?.bounceRate as number) || 0,
avgSessionDuration: (analytics?.metrics?.avgSessionDuration as number) || (analytics?.avgSessionDuration as number) || 0 avgSessionDuration: ((analytics?.metrics as Record<string, unknown>)?.avgSessionDuration as number) || (analytics?.avgSessionDuration as number) || 0
}; };
useEffect(() => { useEffect(() => {

View File

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

View File

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