fix: namespace rate limit buckets per endpoint, remove custom analytics
- Add `prefix` param to checkRateLimit/getRateLimitHeaders so each endpoint
has its own bucket (previously all shared `admin_${ip}`, causing 429s when
analytics/track incremented past n8n endpoints' lower limits)
- n8n/hardcover/currently-reading → prefix 'n8n-reading'
- n8n/status → prefix 'n8n-status'
- analytics/track → prefix 'analytics-track'
- Remove custom analytics system (AnalyticsProvider, lib/analytics,
lib/useWebVitals, all /api/analytics/* routes) — was causing 500s in
production due to missing PostgreSQL PageView table
- Remove analytics consent toggle from ConsentBanner/ConsentProvider
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,321 +0,0 @@
|
||||
'use client';
|
||||
|
||||
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 load performance - wrapped in try-catch
|
||||
try {
|
||||
trackPageLoad();
|
||||
} catch (error) {
|
||||
// Silently fail
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.warn('Error tracking page load:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Track initial page view
|
||||
trackPageView();
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
// Get current page path to extract project ID if on project page
|
||||
const path = window.location.pathname;
|
||||
const projectMatch = path.match(/\/projects\/([^\/]+)/);
|
||||
const projectId = projectMatch ? projectMatch[1] : null;
|
||||
|
||||
// Wait for page to fully load
|
||||
setTimeout(async () => {
|
||||
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.length > 0 ? lcpEntries[lcpEntries.length - 1] : undefined;
|
||||
|
||||
const performanceData = {
|
||||
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 ? 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
|
||||
};
|
||||
|
||||
// Send performance data - single call
|
||||
await fetch('/api/analytics/track', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
type: 'performance',
|
||||
projectId: projectId,
|
||||
page: path,
|
||||
performance: performanceData
|
||||
})
|
||||
});
|
||||
} catch (error) {
|
||||
// Silently fail - performance tracking is not critical
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.warn('Error collecting performance data:', error);
|
||||
}
|
||||
}
|
||||
}, 2500); // Wait 2.5 seconds for page to stabilize
|
||||
} catch (error) {
|
||||
// Silently fail
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.error('Error tracking performance:', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Track performance after page load
|
||||
if (document.readyState === 'complete') {
|
||||
trackPerformanceToAPI();
|
||||
} else {
|
||||
window.addEventListener('load', trackPerformanceToAPI, { once: true });
|
||||
}
|
||||
|
||||
// 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 - debounced to prevent spam
|
||||
const handleClick = debounce((event: unknown) => {
|
||||
try {
|
||||
if (typeof window === 'undefined') return;
|
||||
|
||||
const mouseEvent = event as MouseEvent;
|
||||
const target = mouseEvent.target as HTMLElement | null;
|
||||
if (!target) return;
|
||||
|
||||
const element = target.tagName ? target.tagName.toLowerCase() : 'unknown';
|
||||
const className = target.className;
|
||||
const id = target.id;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}, 500);
|
||||
|
||||
// Track form submissions
|
||||
const handleSubmit = (event: SubmitEvent) => {
|
||||
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 - debounced
|
||||
let maxScrollDepth = 0;
|
||||
const firedScrollMilestones = new Set<number>();
|
||||
const handleScroll = debounce(() => {
|
||||
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 / (scrollHeight - innerHeight)) * 100
|
||||
);
|
||||
|
||||
if (scrollDepth > maxScrollDepth) maxScrollDepth = scrollDepth;
|
||||
|
||||
// Track each milestone once (avoid spamming events on every scroll tick)
|
||||
const milestones = [25, 50, 75, 90];
|
||||
for (const milestone of milestones) {
|
||||
if (maxScrollDepth >= milestone && !firedScrollMilestones.has(milestone)) {
|
||||
firedScrollMilestones.add(milestone);
|
||||
trackEvent('scroll-depth', { depth: milestone, 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);
|
||||
}
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
// Add event listeners
|
||||
document.addEventListener('click', handleClick);
|
||||
document.addEventListener('submit', handleSubmit);
|
||||
window.addEventListener('scroll', handleScroll, { passive: true });
|
||||
|
||||
// Track errors
|
||||
const handleError = (event: ErrorEvent) => {
|
||||
try {
|
||||
if (typeof window === 'undefined') return;
|
||||
trackEvent('error', {
|
||||
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() || '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('unhandledrejection', handleUnhandledRejection);
|
||||
|
||||
// Cleanup
|
||||
return () => {
|
||||
try {
|
||||
// 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);
|
||||
document.removeEventListener('submit', handleSubmit);
|
||||
window.removeEventListener('scroll', handleScroll);
|
||||
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 () => {};
|
||||
}
|
||||
}, [trackPageView]);
|
||||
|
||||
// Always render children, even if analytics fails
|
||||
return <>{children}</>;
|
||||
};
|
||||
Reference in New Issue
Block a user