Copilot/setup sentry nextjs (#58)

* Revise portfolio: warm brown theme, elegant typography, optimized analytics tracking (#55)

* Initial plan

* Update color theme to warm brown and off-white, add elegant fonts, fix analytics tracking

Co-authored-by: denshooter <44590296+denshooter@users.noreply.github.com>

* Fix 404 page integration with warm theme, update admin console colors, fix font loading

Co-authored-by: denshooter <44590296+denshooter@users.noreply.github.com>

* Address code review feedback: fix navigation, add utils, improve tracking

Co-authored-by: denshooter <44590296+denshooter@users.noreply.github.com>

* Fix accessibility and memory leak issues from code review

Co-authored-by: denshooter <44590296+denshooter@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: denshooter <44590296+denshooter@users.noreply.github.com>

* chore: Code cleanup, add Sentry.io monitoring, and documentation (#56)

* Initial plan

* Remove unused code and clean up console statements

Co-authored-by: denshooter <44590296+denshooter@users.noreply.github.com>

* Remove unused components and fix type issues

Co-authored-by: denshooter <44590296+denshooter@users.noreply.github.com>

* Wrap console.warn in development check

Co-authored-by: denshooter <44590296+denshooter@users.noreply.github.com>

* Integrate Sentry.io monitoring and add text editing documentation

Co-authored-by: denshooter <44590296+denshooter@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: denshooter <44590296+denshooter@users.noreply.github.com>

* Initial plan

* feat: Add Sentry configuration files and example pages

- Add sentry.server.config.ts and sentry.edge.config.ts
- Update instrumentation.ts with onRequestError export
- Update instrumentation-client.ts with onRouterTransitionStart export
- Update global-error.tsx to capture exceptions with Sentry
- Create Sentry example page at app/sentry-example-page/page.tsx
- Create Sentry example API route at app/api/sentry-example-api/route.ts

Co-authored-by: denshooter <44590296+denshooter@users.noreply.github.com>

* feat: Update middleware to allow Sentry example page and fix deprecated API

- Update middleware to exclude /sentry-example-page from locale routing
- Remove deprecated startTransaction API from Sentry example page
- Use consistent DSN configuration with fallback values

Co-authored-by: denshooter <44590296+denshooter@users.noreply.github.com>

* refactor: Improve Sentry configuration with environment-based sampling

- Add comments explaining DSN fallback values
- Use environment-based tracesSampleRate (10% in production, 100% in dev)
- Address code review feedback for production-safe configuration

Co-authored-by: denshooter <44590296+denshooter@users.noreply.github.com>

---------

Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
This commit is contained in:
denshooter
2026-01-22 10:05:43 +01:00
committed by GitHub
parent 33f6d47b3e
commit 377631ee50
33 changed files with 3219 additions and 539 deletions

View File

@@ -1,58 +1,73 @@
'use client';
import { useEffect } from 'react';
import { useEffect, useRef, useCallback } from 'react';
import { useWebVitals } from '@/lib/useWebVitals';
import { trackEvent, trackPageLoad } from '@/lib/analytics';
import { debounce } from '@/lib/utils';
interface AnalyticsProviderProps {
children: React.ReactNode;
}
export const AnalyticsProvider: React.FC<AnalyticsProviderProps> = ({ children }) => {
const hasTrackedInitialView = useRef(false);
const hasTrackedPerformance = useRef(false);
const currentPath = useRef('');
// Initialize Web Vitals tracking - wrapped to prevent crashes
// Hooks must be called unconditionally, but the hook itself handles errors
useWebVitals();
// Track page view - memoized to prevent recreation
const trackPageView = useCallback(async () => {
if (typeof window === 'undefined') return;
const path = window.location.pathname;
// Only track if path has changed (prevents duplicate tracking)
if (currentPath.current === path && hasTrackedInitialView.current) {
return;
}
currentPath.current = path;
hasTrackedInitialView.current = true;
const projectMatch = path.match(/\/projects\/([^\/]+)/);
const projectId = projectMatch ? projectMatch[1] : null;
// Track to Umami (if available)
trackEvent('page-view', {
url: path,
referrer: document.referrer,
timestamp: Date.now(),
});
// Track to our API - single call
try {
await fetch('/api/analytics/track', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
type: 'pageview',
projectId: projectId,
page: path
})
});
} catch (error) {
// Silently fail
if (process.env.NODE_ENV === 'development') {
console.error('Error tracking page view:', error);
}
}
}, []);
useEffect(() => {
if (typeof window === 'undefined') return;
// Wrap entire effect in try-catch to prevent any errors from breaking the app
try {
// Track page view
const trackPageView = async () => {
const path = window.location.pathname;
const projectMatch = path.match(/\/projects\/([^\/]+)/);
const projectId = projectMatch ? projectMatch[1] : null;
// Track to Umami (if available)
trackEvent('page-view', {
url: path,
referrer: document.referrer,
timestamp: Date.now(),
});
// Track to our API
try {
await fetch('/api/analytics/track', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
type: 'pageview',
projectId: projectId,
page: path
})
});
} catch (error) {
// Silently fail
if (process.env.NODE_ENV === 'development') {
console.error('Error tracking page view:', error);
}
}
};
// Track page load performance - wrapped in try-catch
try {
trackPageLoad();
@@ -66,8 +81,12 @@ export const AnalyticsProvider: React.FC<AnalyticsProviderProps> = ({ children }
// Track initial page view
trackPageView();
// Track performance metrics to our API
// Track performance metrics to our API - only once
const trackPerformanceToAPI = async () => {
// Prevent duplicate tracking
if (hasTrackedPerformance.current) return;
hasTrackedPerformance.current = true;
try {
if (typeof performance === "undefined" || typeof performance.getEntriesByType !== "function") {
return;
@@ -98,7 +117,7 @@ export const AnalyticsProvider: React.FC<AnalyticsProviderProps> = ({ children }
si: 0 // Speed Index - would need to calculate
};
// Send performance data
// Send performance data - single call
await fetch('/api/analytics/track', {
method: 'POST',
headers: {
@@ -117,7 +136,7 @@ export const AnalyticsProvider: React.FC<AnalyticsProviderProps> = ({ children }
console.warn('Error collecting performance data:', error);
}
}
}, 2000); // Wait 2 seconds for page to stabilize
}, 2500); // Wait 2.5 seconds for page to stabilize
} catch (error) {
// Silently fail
if (process.env.NODE_ENV === 'development') {
@@ -130,26 +149,26 @@ export const AnalyticsProvider: React.FC<AnalyticsProviderProps> = ({ children }
if (document.readyState === 'complete') {
trackPerformanceToAPI();
} else {
window.addEventListener('load', trackPerformanceToAPI);
window.addEventListener('load', trackPerformanceToAPI, { once: true });
}
// Track route changes (for SPA navigation)
const handleRouteChange = () => {
setTimeout(() => {
trackPageView();
trackPageLoad();
}, 100);
};
// Track route changes (for SPA navigation) - debounced
const handleRouteChange = debounce(() => {
// Track new page view (trackPageView will handle path change detection)
trackPageView();
trackPageLoad();
}, 300);
// Listen for popstate events (back/forward navigation)
window.addEventListener('popstate', handleRouteChange);
// Track user interactions
const handleClick = (event: MouseEvent) => {
// Track user interactions - debounced to prevent spam
const handleClick = debounce((event: unknown) => {
try {
if (typeof window === 'undefined') return;
const target = event.target as HTMLElement | null;
const mouseEvent = event as MouseEvent;
const target = mouseEvent.target as HTMLElement | null;
if (!target) return;
const element = target.tagName ? target.tagName.toLowerCase() : 'unknown';
@@ -168,7 +187,7 @@ export const AnalyticsProvider: React.FC<AnalyticsProviderProps> = ({ children }
console.warn('Error tracking click:', error);
}
}
};
}, 500);
// Track form submissions
const handleSubmit = (event: SubmitEvent) => {
@@ -191,10 +210,10 @@ export const AnalyticsProvider: React.FC<AnalyticsProviderProps> = ({ children }
}
};
// Track scroll depth
// Track scroll depth - debounced
let maxScrollDepth = 0;
const firedScrollMilestones = new Set<number>();
const handleScroll = () => {
const handleScroll = debounce(() => {
try {
if (typeof window === 'undefined' || typeof document === 'undefined') return;
@@ -223,7 +242,7 @@ export const AnalyticsProvider: React.FC<AnalyticsProviderProps> = ({ children }
console.warn('Error tracking scroll:', error);
}
}
};
}, 1000);
// Add event listeners
document.addEventListener('click', handleClick);
@@ -270,7 +289,12 @@ export const AnalyticsProvider: React.FC<AnalyticsProviderProps> = ({ children }
// Cleanup
return () => {
try {
// Remove load handler if we added it
// Cancel any pending debounced calls to prevent memory leaks
handleRouteChange.cancel();
handleClick.cancel();
handleScroll.cancel();
// Remove event listeners
window.removeEventListener('load', trackPerformanceToAPI);
window.removeEventListener('popstate', handleRouteChange);
document.removeEventListener('click', handleClick);
@@ -290,7 +314,7 @@ export const AnalyticsProvider: React.FC<AnalyticsProviderProps> = ({ children }
// Return empty cleanup function
return () => {};
}
}, []);
}, [trackPageView]);
// Always render children, even if analytics fails
return <>{children}</>;

View File

@@ -1,5 +0,0 @@
'use client';
export const LiquidCursor = () => {
return null;
};

View File

@@ -91,7 +91,9 @@ const ModernAdminDashboard: React.FC<ModernAdminDashboardProps> = ({ isAuthentic
});
if (!response.ok) {
console.warn('Failed to load projects:', response.status);
if (process.env.NODE_ENV === 'development') {
console.warn('Failed to load projects:', response.status);
}
setProjects([]);
return;
}

View File

@@ -1,139 +0,0 @@
'use client';
import { useState, useEffect } from 'react';
import { trackEvent } from '@/lib/analytics';
interface PerformanceData {
timestamp: string;
url: string;
metrics: {
LCP?: number;
FID?: number;
CLS?: number;
FCP?: number;
TTFB?: number;
};
}
export const PerformanceDashboard: React.FC = () => {
const [performanceData, setPerformanceData] = useState<PerformanceData[]>([]);
const [isVisible, setIsVisible] = useState(false);
useEffect(() => {
// This would typically fetch from your Umami instance or database
// For now, we'll show a placeholder
const mockData: PerformanceData[] = [
{
timestamp: new Date().toISOString(),
url: '/',
metrics: {
LCP: 1200,
FID: 45,
CLS: 0.1,
FCP: 800,
TTFB: 200,
},
},
];
setPerformanceData(mockData);
}, []);
const getPerformanceGrade = (metric: string, value: number): string => {
switch (metric) {
case 'LCP':
return value <= 2500 ? 'Good' : value <= 4000 ? 'Needs Improvement' : 'Poor';
case 'FID':
return value <= 100 ? 'Good' : value <= 300 ? 'Needs Improvement' : 'Poor';
case 'CLS':
return value <= 0.1 ? 'Good' : value <= 0.25 ? 'Needs Improvement' : 'Poor';
case 'FCP':
return value <= 1800 ? 'Good' : value <= 3000 ? 'Needs Improvement' : 'Poor';
case 'TTFB':
return value <= 800 ? 'Good' : value <= 1800 ? 'Needs Improvement' : 'Poor';
default:
return 'Unknown';
}
};
const getGradeColor = (grade: string): string => {
switch (grade) {
case 'Good':
return 'text-green-600 bg-green-100';
case 'Needs Improvement':
return 'text-yellow-600 bg-yellow-100';
case 'Poor':
return 'text-red-600 bg-red-100';
default:
return 'text-gray-600 bg-gray-100';
}
};
if (!isVisible) {
return (
<button
onClick={() => {
setIsVisible(true);
trackEvent('dashboard-toggle', { action: 'show' });
}}
className="fixed bottom-4 right-4 bg-white text-stone-700 border border-stone-200 px-4 py-2 rounded-lg shadow-md hover:bg-stone-50 transition-colors z-50"
>
📊 Performance
</button>
);
}
return (
<div className="fixed bottom-4 right-4 bg-white border border-gray-200 rounded-lg shadow-xl p-6 w-96 max-h-96 overflow-y-auto z-50">
<div className="flex justify-between items-center mb-4">
<h3 className="text-lg font-semibold text-gray-800">Performance Dashboard</h3>
<button
onClick={() => {
setIsVisible(false);
trackEvent('dashboard-toggle', { action: 'hide' });
}}
className="text-gray-500 hover:text-gray-700"
>
</button>
</div>
<div className="space-y-4">
{performanceData.map((data, index) => (
<div key={index} className="border-b border-gray-100 pb-4">
<div className="text-sm text-gray-600 mb-2">
{new Date(data.timestamp).toLocaleString()}
</div>
<div className="text-sm font-medium text-gray-800 mb-2">
{data.url}
</div>
<div className="grid grid-cols-2 gap-2">
{Object.entries(data.metrics).map(([metric, value]) => {
const grade = getPerformanceGrade(metric, value);
return (
<div key={metric} className="flex justify-between items-center">
<span className="text-xs font-medium text-gray-600">{metric}:</span>
<div className="flex items-center space-x-2">
<span className="text-xs font-mono">{value}ms</span>
<span className={`text-xs px-2 py-1 rounded ${getGradeColor(grade)}`}>
{grade}
</span>
</div>
</div>
);
})}
</div>
</div>
))}
</div>
<div className="mt-4 pt-4 border-t border-gray-100">
<div className="text-xs text-gray-500">
<div>🟢 Good: Meets recommended thresholds</div>
<div>🟡 Needs Improvement: Below recommended thresholds</div>
<div>🔴 Poor: Significantly below thresholds</div>
</div>
</div>
</div>
);
};

View File

@@ -13,7 +13,6 @@ import {
Github,
RefreshCw
} from 'lucide-react';
// Editor is now a separate page at /editor
interface Project {
id: string;