Compare commits

...

3 Commits

Author SHA1 Message Date
denshooter
80f2ac61ac Fix type error in KernelPanic404: update currentMusic type to match return type 2026-01-10 16:55:01 +01:00
denshooter
a980ee8fcd Fix runtime errors: PerformanceObserver, localStorage, crypto.randomUUID, hydration issues, and linting errors 2026-01-10 16:54:28 +01:00
denshooter
ca2ed13446 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.
2026-01-10 16:53:06 +01:00
16 changed files with 593 additions and 289 deletions

View File

@@ -38,7 +38,8 @@ export async function GET(request: NextRequest) {
cls: ((p.performance as Record<string, unknown>)?.coreWebVitals as Record<string, unknown>)?.cls as number || 0
}));
const avgLighthouse = projectsWithPerformance.length > 0
// Calculate average lighthouse score (currently unused but kept for future use)
const _avgLighthouse = projectsWithPerformance.length > 0
? Math.round(projectsWithPerformance.reduce((sum, p) => sum + p.lighthouse, 0) / projectsWithPerformance.length)
: 0;

View File

@@ -152,6 +152,7 @@ export async function POST(request: NextRequest) {
}
}
}
}
// Track session data (for bounce rate calculation)
if (type === 'session' && session) {

View File

@@ -57,8 +57,16 @@ export default function ActivityFeed() {
const [isTrackingEnabled, setIsTrackingEnabled] = useState(() => {
// Check localStorage for tracking preference
if (typeof window !== "undefined") {
try {
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;
});
@@ -1385,7 +1393,14 @@ export default function ActivityFeed() {
const newValue = !isTrackingEnabled;
setIsTrackingEnabled(newValue);
if (typeof window !== "undefined") {
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
if (!newValue) {

View File

@@ -20,21 +20,47 @@ interface Message {
}
export default function ChatWidget() {
// Prevent hydration mismatch by only rendering after mount
const [mounted, setMounted] = useState(false);
const [isOpen, setIsOpen] = useState(false);
const [messages, setMessages] = useState<Message[]>([]);
const [inputValue, setInputValue] = useState("");
const [isLoading, setIsLoading] = useState(false);
const [conversationId, setConversationId] = useState(() => {
// Generate or retrieve conversation ID
if (typeof window !== "undefined") {
const [conversationId, setConversationId] = useState<string>("default");
useEffect(() => {
setMounted(true);
// Generate or retrieve conversation ID only on client
try {
const stored = localStorage.getItem("chatSessionId");
if (stored) return stored;
const newId = crypto.randomUUID();
localStorage.setItem("chatSessionId", newId);
return newId;
if (stored) {
setConversationId(stored);
return;
}
return "default";
// 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);
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)}`);
}
}, []);
const messagesEndRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<HTMLInputElement>(null);
@@ -62,6 +88,7 @@ export default function ChatWidget() {
// Load messages from localStorage
useEffect(() => {
if (typeof window !== "undefined") {
try {
const stored = localStorage.getItem("chatMessages");
if (stored) {
try {
@@ -74,7 +101,24 @@ export default function ChatWidget() {
})),
);
} catch (e) {
console.error("Failed to load chat history", 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
@@ -87,13 +131,35 @@ export default function ChatWidget() {
},
]);
}
} catch (error) {
// 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([
{
id: "welcome",
text: "Hi! I'm Dennis's AI assistant. Ask me anything about his skills, projects, or experience! 🚀",
sender: "bot",
timestamp: new Date(),
},
]);
}
}
}, []);
// Save messages to localStorage
useEffect(() => {
if (typeof window !== "undefined" && messages.length > 0) {
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]);
@@ -204,6 +270,11 @@ export default function ChatWidget() {
]);
};
// Don't render until mounted to prevent hydration mismatch
if (!mounted) {
return null;
}
return (
<>
{/* Chat Button */}

View File

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

View File

@@ -28,18 +28,18 @@ export default function KernelPanic404() {
let audioCtx: AudioContext | null = null;
let systemFrozen = false;
let currentMusic: any = null;
let currentMusic: { stop: () => void } | null = null;
let hawkinsActive = false;
let fsocietyActive = false;
// Timers storage to clear on unmount
const timers: (NodeJS.Timeout | number)[] = [];
const interval = (fn: Function, ms: number) => {
const interval = (fn: () => void, ms: number) => {
const id = setInterval(fn, ms);
timers.push(id);
return id;
};
const timeout = (fn: Function, ms: number) => {
const timeout = (fn: () => void, ms: number) => {
const id = setTimeout(fn, ms);
timers.push(id);
return id;
@@ -49,7 +49,7 @@ export default function KernelPanic404() {
function initAudio() {
if (!audioCtx) {
const AudioContextClass =
window.AudioContext || (window as any).webkitAudioContext;
window.AudioContext || (window as typeof window & { webkitAudioContext?: typeof AudioContext }).webkitAudioContext;
if (AudioContextClass) {
audioCtx = new AudioContextClass();
}
@@ -444,6 +444,7 @@ export default function KernelPanic404() {
}
/* --- FILE SYSTEM --- */
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const fileSystem: any = {
home: {
type: "dir",
@@ -551,7 +552,7 @@ export default function KernelPanic404() {
let currentPath = fileSystem.home.children.guest;
let pathStr = "~";
let commandHistory: string[] = [];
const commandHistory: string[] = [];
let historyIndex = -1;
/* --- UTILS --- */
@@ -671,7 +672,7 @@ export default function KernelPanic404() {
// Clear initial output
output!.innerHTML = "";
for (let msg of bootMessages) {
for (const msg of bootMessages) {
printLine(msg.t, msg.type);
await sleep(msg.d);
}
@@ -794,7 +795,7 @@ export default function KernelPanic404() {
if (suggestions.length === 0) {
try {
playSynth("beep");
} catch (e) {}
} catch {}
return;
}
@@ -817,13 +818,13 @@ export default function KernelPanic404() {
input.setSelectionRange(input.value.length, input.value.length);
try {
playSynth("beep");
} catch (e) {}
} catch {}
} else {
// Multiple matches
printLine(`Possible completions: ${suggestions.join(" ")}`, "log-dim");
try {
playSynth("beep");
} catch (e) {}
} catch {}
}
}
@@ -832,7 +833,7 @@ export default function KernelPanic404() {
if (systemFrozen || !input) {
try {
playSynth("beep");
} catch (e) {}
} catch {}
return;
}
@@ -871,7 +872,7 @@ export default function KernelPanic404() {
args.includes("-a") || args.includes("-la") || args.includes("-l");
const longFormat = args.includes("-l") || args.includes("-la");
let items = Object.keys(currentPath.children).filter(
const items = Object.keys(currentPath.children).filter(
(n) => !n.startsWith(".") || showHidden,
);
@@ -1177,7 +1178,7 @@ export default function KernelPanic404() {
overlay.style.display = "none";
overlay.innerHTML = "";
const sporeInterval = interval(() => {
const _sporeInterval = interval(() => {
const spore = document.createElement("div");
spore.className = "spore";
spore.style.left = Math.random() * 100 + "%";
@@ -1187,7 +1188,7 @@ export default function KernelPanic404() {
setTimeout(() => spore.remove(), 3000);
}, 300);
const glitchInterval = interval(() => {
const _glitchInterval = interval(() => {
if (!hawkinsActive) return;
body.style.filter = "hue-rotate(180deg) contrast(1.3) brightness(0.9)";
setTimeout(
@@ -1412,7 +1413,7 @@ export default function KernelPanic404() {
try {
playSynth("key");
} catch (e) {}
} catch {}
if (e.key === "ArrowUp" && historyIndex > 0) {
historyIndex--;

View File

@@ -2,7 +2,7 @@
import { useState, useEffect } from "react";
import { motion, Variants } from "framer-motion";
import { ExternalLink, Github, Layers, ArrowRight, ArrowLeft, Calendar } from "lucide-react";
import { ExternalLink, Github, ArrowRight, Calendar } from "lucide-react";
import Link from "next/link";
import Image from "next/image";

View File

@@ -59,7 +59,7 @@ function EditorPageContent() {
const [isSaving, setIsSaving] = useState(false);
const [isCreating, setIsCreating] = useState(!projectId);
const [showPreview, setShowPreview] = useState(false);
const [isTyping, setIsTyping] = useState(false);
const [_isTyping, setIsTyping] = useState(false);
const [history, setHistory] = useState<typeof formData[]>([]);
const [historyIndex, setHistoryIndex] = useState(-1);
const [originalFormData, setOriginalFormData] = useState<typeof formData | null>(null);

View File

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

View File

@@ -1,12 +1,11 @@
"use client";
import { motion } from 'framer-motion';
import { ExternalLink, Calendar, Tag, ArrowLeft, Github as GithubIcon, Share2 } from 'lucide-react';
import { ExternalLink, Calendar, ArrowLeft, Github as GithubIcon, Share2 } from 'lucide-react';
import Link from 'next/link';
import { useParams } from 'next/navigation';
import { useState, useEffect } from 'react';
import ReactMarkdown from 'react-markdown';
import Image from 'next/image';
interface Project {
id: number;

View File

@@ -4,9 +4,7 @@ import { useState, useEffect, useCallback } from 'react';
import { motion } from 'framer-motion';
import {
BarChart3,
TrendingUp,
Eye,
Heart,
Zap,
Globe,
Activity,

View File

@@ -65,18 +65,19 @@ export const AnalyticsProvider: React.FC<AnalyticsProviderProps> = ({ children }
// Wait for page to fully load
setTimeout(async () => {
const navigation = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming;
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[lcpEntries.length - 1];
const lcp = lcpEntries.length > 0 ? lcpEntries[lcpEntries.length - 1] : undefined;
const performanceData = {
loadTime: navigation ? navigation.loadEventEnd - navigation.fetchStart : 0,
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 : 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
@@ -95,6 +96,12 @@ export const AnalyticsProvider: React.FC<AnalyticsProviderProps> = ({ children }
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
@@ -124,8 +131,13 @@ export const AnalyticsProvider: React.FC<AnalyticsProviderProps> = ({ children }
// Track user interactions
const handleClick = (event: MouseEvent) => {
const target = event.target as HTMLElement;
const element = target.tagName.toLowerCase();
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;
@@ -135,23 +147,48 @@ export const AnalyticsProvider: React.FC<AnalyticsProviderProps> = ({ children }
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) => {
const form = event.target as HTMLFormElement;
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 / (document.documentElement.scrollHeight - window.innerHeight)) * 100
(window.scrollY / (scrollHeight - innerHeight)) * 100
);
if (scrollDepth > maxScrollDepth) {
@@ -168,6 +205,12 @@ export const AnalyticsProvider: React.FC<AnalyticsProviderProps> = ({ children }
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
@@ -177,20 +220,36 @@ export const AnalyticsProvider: React.FC<AnalyticsProviderProps> = ({ children }
// Track errors
const handleError = (event: ErrorEvent) => {
try {
if (typeof window === 'undefined') return;
trackEvent('error', {
message: event.message,
filename: event.filename,
lineno: event.lineno,
colno: event.colno,
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(),
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);

View File

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

View File

@@ -157,7 +157,7 @@ const ModernAdminDashboard: React.FC<ModernAdminDashboardProps> = ({ isAuthentic
const stats = {
totalProjects: projects.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,
avgPerformance: (() => {
// Only show real performance data, not defaults
@@ -172,9 +172,9 @@ const ModernAdminDashboard: React.FC<ModernAdminDashboardProps> = ({ isAuthentic
}, 0) / projectsWithPerf.length);
})(),
systemHealth: (systemStats?.status as string) || 'unknown',
totalUsers: (analytics?.metrics?.totalUsers as number) || (analytics?.totalUsers as number) || 0,
bounceRate: (analytics?.metrics?.bounceRate as number) || (analytics?.bounceRate as number) || 0,
avgSessionDuration: (analytics?.metrics?.avgSessionDuration as number) || (analytics?.avgSessionDuration as number) || 0
totalUsers: ((analytics?.metrics as Record<string, unknown>)?.totalUsers as number) || (analytics?.totalUsers as number) || 0,
bounceRate: ((analytics?.metrics as Record<string, unknown>)?.bounceRate as number) || (analytics?.bounceRate as number) || 0,
avgSessionDuration: ((analytics?.metrics as Record<string, unknown>)?.avgSessionDuration as number) || (analytics?.avgSessionDuration as number) || 0
};
useEffect(() => {

View File

@@ -57,11 +57,13 @@ 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;
try {
const navigationEntries = performance.getEntriesByType('navigation');
const navigation = navigationEntries[0] as PerformanceNavigationTiming | undefined;
if (navigation) {
if (navigation && navigation.loadEventEnd && navigation.fetchStart) {
trackPerformance({
name: 'page-load',
value: navigation.loadEventEnd - navigation.fetchStart,
@@ -72,19 +74,38 @@ export const trackPageLoad = () => {
// 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),
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,11 +13,15 @@ interface Metric {
// Simple Web Vitals implementation (since we don't want to add external dependencies)
const getCLS = (onPerfEntry: (metric: Metric) => void) => {
if (typeof window === 'undefined' || typeof PerformanceObserver === 'undefined') return null;
try {
let clsValue = 0;
let sessionValue = 0;
let sessionEntries: PerformanceEntry[] = [];
const observer = new PerformanceObserver((list) => {
try {
for (const entry of list.getEntries()) {
if (!(entry as PerformanceEntry & { hadRecentInput?: boolean }).hadRecentInput) {
const firstSessionEntry = sessionEntries[0];
@@ -42,28 +46,64 @@ const getCLS = (onPerfEntry: (metric: Metric) => void) => {
}
}
}
} 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 });
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) => {
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: (entry as PerformanceEntry & { processingStart?: number }).processingStart! - entry.startTime,
delta: (entry as PerformanceEntry & { processingStart?: number }).processingStart! - entry.startTime,
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 });
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) => {
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({
@@ -74,32 +114,67 @@ const getFCP = (onPerfEntry: (metric: Metric) => void) => {
});
}
}
} catch (error) {
if (process.env.NODE_ENV === 'development') {
console.warn('FCP tracking error:', error);
}
}
});
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) => {
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 });
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) => {
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,
@@ -108,9 +183,22 @@ const getTTFB = (onPerfEntry: (metric: Metric) => void) => {
});
}
}
}
} catch (error) {
if (process.env.NODE_ENV === 'development') {
console.warn('TTFB tracking error:', error);
}
}
});
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 {
// Silently fail
}
});
window.removeEventListener('load', handleLoad);
};
}, []);