Files
portfolio/lib/analytics.ts
Cursor Agent abfb710c4b Fix: guard Umami tracking and web vitals performance APIs
Avoid calling undefined umami.track, add safe checks for Performance APIs, and clean up load listeners to prevent .call() crashes in Chrome.
2026-01-14 02:09:22 +00:00

145 lines
4.2 KiB
TypeScript

// Analytics utilities for Umami with Performance Tracking
declare global {
interface Window {
umami?: {
track: (event: string, data?: Record<string, unknown>) => void;
};
}
}
export interface PerformanceMetric {
name: string;
value: number;
url: string;
timestamp: number;
userAgent?: string;
}
export interface WebVitalsMetric {
name: 'CLS' | 'FID' | 'FCP' | 'LCP' | 'TTFB';
value: number;
delta: number;
id: string;
url: string;
}
// Track custom events to Umami
export const trackEvent = (event: string, data?: Record<string, unknown>) => {
if (typeof window === "undefined") return;
const trackFn = window.umami?.track;
if (typeof trackFn !== "function") return;
try {
trackFn(event, {
...data,
timestamp: Date.now(),
url: window.location.pathname,
});
} catch (error) {
// Silently fail - analytics must never break the app
if (process.env.NODE_ENV === "development") {
console.warn("Error tracking Umami event:", error);
}
}
};
// Track performance metrics
export const trackPerformance = (metric: PerformanceMetric) => {
trackEvent('performance', {
metric: metric.name,
value: Math.round(metric.value),
url: metric.url,
userAgent: metric.userAgent,
});
};
// Track Web Vitals
export const trackWebVitals = (metric: WebVitalsMetric) => {
trackEvent('web-vitals', {
name: metric.name,
value: Math.round(metric.value),
delta: Math.round(metric.delta),
id: metric.id,
url: metric.url,
});
};
// Track page load performance
export const trackPageLoad = () => {
if (typeof window === 'undefined' || typeof performance === 'undefined') return;
try {
const navigationEntries = performance.getEntriesByType('navigation');
const navigation = navigationEntries[0] as PerformanceNavigationTiming | undefined;
if (navigation && navigation.loadEventEnd && navigation.fetchStart) {
trackPerformance({
name: 'page-load',
value: navigation.loadEventEnd - navigation.fetchStart,
url: window.location.pathname,
timestamp: Date.now(),
userAgent: navigator.userAgent,
});
// Track individual timing phases
trackEvent('page-timing', {
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),
status,
url: window.location.pathname,
});
};
// Track user interactions
export const trackInteraction = (action: string, element?: string) => {
if (typeof window === 'undefined') return;
trackEvent('interaction', {
action,
element,
url: window.location.pathname,
});
};
// Track errors
export const trackError = (error: string, context?: string) => {
if (typeof window === 'undefined') return;
trackEvent('error', {
error,
context,
url: window.location.pathname,
});
};