/** * Analytics & Performance Monitoring * Supports Google Analytics, Plausible, and custom events */ // Types export interface PageViewEvent { url: string title: string referrer?: string } export interface CustomEvent { name: string properties?: Record } export interface PerformanceMetrics { fcp?: number // First Contentful Paint lcp?: number // Largest Contentful Paint fid?: number // First Input Delay cls?: number // Cumulative Layout Shift ttfb?: number // Time to First Byte } /** * Track page view */ export function trackPageView(event: PageViewEvent) { // Google Analytics (gtag) if (typeof window !== 'undefined' && (window as any).gtag) { ;(window as any).gtag('config', process.env.NEXT_PUBLIC_GA_ID, { page_path: event.url, page_title: event.title, }) } // Plausible Analytics (privacy-friendly) if (typeof window !== 'undefined' && (window as any).plausible) { ;(window as any).plausible('pageview', { u: event.url, props: { title: event.title, ...(event.referrer && { referrer: event.referrer }), }, }) } // Custom analytics endpoint (optional) if (process.env.NEXT_PUBLIC_ANALYTICS_ENDPOINT) { fetch(process.env.NEXT_PUBLIC_ANALYTICS_ENDPOINT, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ type: 'pageview', ...event, timestamp: new Date().toISOString(), }), }).catch(() => {}) // Silent fail } } /** * Track custom event */ export function trackEvent(event: CustomEvent) { // Google Analytics if (typeof window !== 'undefined' && (window as any).gtag) { ;(window as any).gtag('event', event.name, event.properties || {}) } // Plausible if (typeof window !== 'undefined' && (window as any).plausible) { ;(window as any).plausible(event.name, { props: event.properties || {} }) } // Custom endpoint if (process.env.NEXT_PUBLIC_ANALYTICS_ENDPOINT) { fetch(process.env.NEXT_PUBLIC_ANALYTICS_ENDPOINT, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ type: 'event', ...event, timestamp: new Date().toISOString(), }), }).catch(() => {}) } } /** * Track search query */ export function trackSearch(query: string, results: number) { trackEvent({ name: 'search', properties: { query, results, }, }) } /** * Track domain view */ export function trackDomainView(domain: string, price?: number) { trackEvent({ name: 'domain_view', properties: { domain, ...(price && { price }), }, }) } /** * Track listing inquiry */ export function trackInquiry(domain: string) { trackEvent({ name: 'listing_inquiry', properties: { domain, }, }) } /** * Track signup */ export function trackSignup(method: 'email' | 'google' | 'github') { trackEvent({ name: 'signup', properties: { method, }, }) } /** * Track subscription */ export function trackSubscription(tier: 'scout' | 'trader' | 'tycoon', price: number) { trackEvent({ name: 'subscription', properties: { tier, price, }, }) } /** * Measure Web Vitals (Core Performance Metrics) */ export function measureWebVitals() { if (typeof window === 'undefined') return // Use Next.js built-in web vitals reporting const reportWebVitals = (metric: PerformanceMetrics) => { // Send to Google Analytics if ((window as any).gtag) { ;(window as any).gtag('event', metric, { event_category: 'Web Vitals', non_interaction: true, }) } // Send to custom endpoint if (process.env.NEXT_PUBLIC_ANALYTICS_ENDPOINT) { fetch(process.env.NEXT_PUBLIC_ANALYTICS_ENDPOINT, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ type: 'web_vital', ...metric, timestamp: new Date().toISOString(), }), }).catch(() => {}) } } // Measure FCP (First Contentful Paint) const paintEntries = performance.getEntriesByType('paint') const fcpEntry = paintEntries.find((entry) => entry.name === 'first-contentful-paint') if (fcpEntry) { reportWebVitals({ fcp: fcpEntry.startTime }) } // Observe LCP (Largest Contentful Paint) if ('PerformanceObserver' in window) { const observer = new PerformanceObserver((list) => { const entries = list.getEntries() const lastEntry = entries[entries.length - 1] reportWebVitals({ lcp: lastEntry.startTime }) }) observer.observe({ entryTypes: ['largest-contentful-paint'] }) } // Measure CLS (Cumulative Layout Shift) if ('PerformanceObserver' in window) { let clsValue = 0 const observer = new PerformanceObserver((list) => { for (const entry of list.getEntries()) { if (!(entry as any).hadRecentInput) { clsValue += (entry as any).value } } reportWebVitals({ cls: clsValue }) }) observer.observe({ entryTypes: ['layout-shift'] }) } // Measure TTFB (Time to First Byte) const navigationEntry = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming if (navigationEntry) { reportWebVitals({ ttfb: navigationEntry.responseStart - navigationEntry.requestStart }) } } /** * Initialize analytics */ export function initAnalytics() { if (typeof window === 'undefined') return // Measure web vitals on load if (document.readyState === 'complete') { measureWebVitals() } else { window.addEventListener('load', measureWebVitals) } // Track page views on navigation const handleRouteChange = () => { trackPageView({ url: window.location.pathname + window.location.search, title: document.title, referrer: document.referrer, }) } // Initial page view handleRouteChange() // Listen for route changes (for SPA navigation) window.addEventListener('popstate', handleRouteChange) return () => { window.removeEventListener('popstate', handleRouteChange) } } /** * Error tracking */ export function trackError(error: Error, context?: Record) { trackEvent({ name: 'error', properties: { message: error.message, stack: error.stack, ...context, }, }) // Also log to console in development if (process.env.NODE_ENV === 'development') { console.error('Error tracked:', error, context) } } /** * A/B Test tracking */ export function trackABTest(testName: string, variant: string) { trackEvent({ name: 'ab_test', properties: { test: testName, variant, }, }) } /** * Consent management */ export function hasAnalyticsConsent(): boolean { if (typeof window === 'undefined') return false const consent = localStorage.getItem('analytics_consent') return consent === 'true' } export function setAnalyticsConsent(consent: boolean) { if (typeof window === 'undefined') return localStorage.setItem('analytics_consent', String(consent)) if (consent) { initAnalytics() } }