# Performance Monitoring & Analytics **Meta-Description:** Web Performance Monitoring mit Next.js. Real User Monitoring, Analytics Integration, Error Tracking und Dashboards. **Keywords:** Performance Monitoring, RUM, Analytics, Error Tracking, Web Vitals, Sentry, Google Analytics, Vercel Analytics --- ## Einführung **Performance Monitoring** geht über einmalige Tests hinaus. **Real User Monitoring (RUM)** erfasst echte Nutzerdaten, **Error Tracking** findet Probleme früh. Dieser Guide zeigt moderne Monitoring-Strategien für Next.js Anwendungen. --- ## Monitoring Overview ``` ┌─────────────────────────────────────────────────────────────┐ │ PERFORMANCE MONITORING STACK │ ├─────────────────────────────────────────────────────────────┤ │ │ │ Data Collection: │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ Browser │ │ │ │ ├── Web Vitals (LCP, INP, CLS) │ │ │ │ ├── Resource Timing │ │ │ │ ├── Navigation Timing │ │ │ │ ├── Long Tasks │ │ │ │ └── JS Errors │ │ │ │ │ │ │ │ Server │ │ │ │ ├── Response Times │ │ │ │ ├── Server Errors │ │ │ │ ├── Database Queries │ │ │ │ └── External API Calls │ │ │ └─────────────────────────────────────────────────────┘ │ │ │ │ Tools & Services: │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ Analytics: │ │ │ │ ├── Vercel Analytics / Speed Insights │ │ │ │ ├── Google Analytics 4 │ │ │ │ └── Plausible / Fathom (Privacy-focused) │ │ │ │ │ │ │ │ Error Tracking: │ │ │ │ ├── Sentry │ │ │ │ ├── Bugsnag │ │ │ │ └── LogRocket │ │ │ │ │ │ │ │ APM (Application Performance): │ │ │ │ ├── Datadog │ │ │ │ ├── New Relic │ │ │ │ └── Grafana Cloud │ │ │ └─────────────────────────────────────────────────────┘ │ │ │ │ Alerting: │ │ ├── Slack / Discord notifications │ │ ├── Email alerts │ │ ├── PagerDuty integration │ │ └── Custom webhooks │ │ │ └─────────────────────────────────────────────────────────────┘ ``` --- ## Web Vitals Monitoring ```typescript // lib/monitoring/web-vitals.ts import { onLCP, onINP, onCLS, onFCP, onTTFB, Metric } from 'web-vitals'; interface VitalsData { name: string; value: number; rating: 'good' | 'needs-improvement' | 'poor'; delta: number; id: string; navigationType: string; url: string; timestamp: number; } type ReportCallback = (data: VitalsData) => void; const thresholds: Record = { LCP: [2500, 4000], INP: [200, 500], CLS: [0.1, 0.25], FCP: [1800, 3000], TTFB: [800, 1800] }; function getRating(name: string, value: number): 'good' | 'needs-improvement' | 'poor' { const [good, poor] = thresholds[name] || [0, 0]; if (value <= good) return 'good'; if (value <= poor) return 'needs-improvement'; return 'poor'; } function createReporter(callback: ReportCallback) { return (metric: Metric) => { callback({ name: metric.name, value: metric.value, rating: getRating(metric.name, metric.value), delta: metric.delta, id: metric.id, navigationType: metric.navigationType || 'unknown', url: window.location.href, timestamp: Date.now() }); }; } export function initWebVitals(callback: ReportCallback) { const reporter = createReporter(callback); onLCP(reporter); onINP(reporter); onCLS(reporter); onFCP(reporter); onTTFB(reporter); } // Aggregated reporting (batch) let vitalsBuffer: VitalsData[] = []; let flushTimeout: NodeJS.Timeout | null = null; export function initBatchedWebVitals(endpoint: string) { const flush = () => { if (vitalsBuffer.length === 0) return; const data = [...vitalsBuffer]; vitalsBuffer = []; // Use sendBeacon for reliability if (navigator.sendBeacon) { navigator.sendBeacon(endpoint, JSON.stringify(data)); } else { fetch(endpoint, { method: 'POST', body: JSON.stringify(data), headers: { 'Content-Type': 'application/json' }, keepalive: true }); } }; initWebVitals((data) => { vitalsBuffer.push(data); // Flush after 5 seconds or when buffer is full if (flushTimeout) clearTimeout(flushTimeout); if (vitalsBuffer.length >= 10) { flush(); } else { flushTimeout = setTimeout(flush, 5000); } }); // Flush on page unload window.addEventListener('visibilitychange', () => { if (document.visibilityState === 'hidden') { flush(); } }); } ``` ```typescript // app/api/vitals/route.ts - Vitals API Endpoint import { NextResponse } from 'next/server'; interface VitalsPayload { name: string; value: number; rating: string; url: string; timestamp: number; } export async function POST(request: Request) { try { const data: VitalsPayload[] = await request.json(); // Validate data const validData = data.filter(item => item.name && typeof item.value === 'number' ); // Store in database or forward to analytics await storeVitals(validData); // Check for poor performance and alert const poorVitals = validData.filter(v => v.rating === 'poor'); if (poorVitals.length > 0) { await sendAlert(poorVitals); } return NextResponse.json({ success: true }); } catch (error) { console.error('Vitals API error:', error); return NextResponse.json({ error: 'Failed to process vitals' }, { status: 500 }); } } async function storeVitals(data: VitalsPayload[]) { // Example: Store in InfluxDB, PostgreSQL, or analytics service // await db.vitals.createMany({ data }); } async function sendAlert(poorVitals: VitalsPayload[]) { // Example: Send Slack notification const message = poorVitals.map(v => `${v.name}: ${v.value.toFixed(2)} (${v.rating}) on ${v.url}` ).join('\n'); // await fetch(process.env.SLACK_WEBHOOK_URL, { // method: 'POST', // body: JSON.stringify({ text: `Poor Web Vitals detected:\n${message}` }) // }); } ``` --- ## Vercel Analytics Integration ```typescript // app/layout.tsx - Vercel Analytics import { Analytics } from '@vercel/analytics/react'; import { SpeedInsights } from '@vercel/speed-insights/next'; export default function RootLayout({ children }: { children: React.ReactNode }) { return ( {children} ); } // Custom event tracking // components/Button.tsx 'use client'; import { track } from '@vercel/analytics'; export function CTAButton({ label, action }: { label: string; action: string }) { const handleClick = () => { track('cta_click', { label, action, page: window.location.pathname }); }; return ( ); } ``` --- ## Sentry Error Tracking ```typescript // sentry.client.config.ts import * as Sentry from '@sentry/nextjs'; Sentry.init({ dsn: process.env.NEXT_PUBLIC_SENTRY_DSN, // Performance Monitoring tracesSampleRate: 0.1, // 10% of transactions // Session Replay replaysSessionSampleRate: 0.1, replaysOnErrorSampleRate: 1.0, integrations: [ Sentry.replayIntegration({ maskAllText: true, blockAllMedia: true }), Sentry.browserTracingIntegration() ], // Environment environment: process.env.NODE_ENV, // Release tracking release: process.env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA, // Ignore specific errors ignoreErrors: [ 'ResizeObserver loop limit exceeded', 'Non-Error exception captured' ], // Before send hook beforeSend(event, hint) { // Filter out sensitive data if (event.request?.headers) { delete event.request.headers['authorization']; delete event.request.headers['cookie']; } return event; } }); // sentry.server.config.ts import * as Sentry from '@sentry/nextjs'; Sentry.init({ dsn: process.env.SENTRY_DSN, tracesSampleRate: 0.1, environment: process.env.NODE_ENV }); // sentry.edge.config.ts import * as Sentry from '@sentry/nextjs'; Sentry.init({ dsn: process.env.SENTRY_DSN, tracesSampleRate: 0.1 }); ``` ```typescript // lib/monitoring/sentry.ts - Custom Sentry Helpers import * as Sentry from '@sentry/nextjs'; // Capture custom error with context export function captureError( error: Error, context?: Record ) { Sentry.withScope(scope => { if (context) { Object.entries(context).forEach(([key, value]) => { scope.setExtra(key, value); }); } Sentry.captureException(error); }); } // Set user context export function setUser(user: { id: string; email?: string; username?: string }) { Sentry.setUser(user); } // Clear user on logout export function clearUser() { Sentry.setUser(null); } // Custom breadcrumb export function addBreadcrumb( category: string, message: string, data?: Record ) { Sentry.addBreadcrumb({ category, message, data, level: 'info' }); } // Performance transaction export function startTransaction(name: string, op: string) { return Sentry.startInactiveSpan({ name, op, forceTransaction: true }); } // Usage example export async function fetchDataWithMonitoring( url: string, options?: RequestInit ): Promise { const span = startTransaction(`fetch ${url}`, 'http.client'); try { addBreadcrumb('http', `Fetching ${url}`); const response = await fetch(url, options); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } return await response.json(); } catch (error) { captureError(error as Error, { url, options }); throw error; } finally { span?.end(); } } ``` --- ## Google Analytics 4 ```typescript // lib/analytics/gtag.ts declare global { interface Window { gtag: (...args: unknown[]) => void; dataLayer: unknown[]; } } export const GA_MEASUREMENT_ID = process.env.NEXT_PUBLIC_GA_ID; // Initialize GA4 export function initGA() { if (!GA_MEASUREMENT_ID) return; window.dataLayer = window.dataLayer || []; window.gtag = function gtag() { window.dataLayer.push(arguments); }; window.gtag('js', new Date()); window.gtag('config', GA_MEASUREMENT_ID, { page_path: window.location.pathname, anonymize_ip: true }); } // Page view export function pageview(url: string) { if (!GA_MEASUREMENT_ID) return; window.gtag('config', GA_MEASUREMENT_ID, { page_path: url }); } // Custom event export function event( action: string, category: string, label?: string, value?: number ) { if (!GA_MEASUREMENT_ID) return; window.gtag('event', action, { event_category: category, event_label: label, value: value }); } // E-commerce events export function purchaseEvent(transaction: { id: string; value: number; currency: string; items: Array<{ id: string; name: string; price: number; quantity: number; }>; }) { if (!GA_MEASUREMENT_ID) return; window.gtag('event', 'purchase', { transaction_id: transaction.id, value: transaction.value, currency: transaction.currency, items: transaction.items }); } // Web Vitals to GA4 export function sendWebVitalToGA(metric: { name: string; value: number; id: string; }) { if (!GA_MEASUREMENT_ID) return; window.gtag('event', metric.name, { event_category: 'Web Vitals', value: Math.round(metric.name === 'CLS' ? metric.value * 1000 : metric.value), event_label: metric.id, non_interaction: true }); } ``` ```tsx // components/GoogleAnalytics.tsx 'use client'; import Script from 'next/script'; import { usePathname, useSearchParams } from 'next/navigation'; import { useEffect, Suspense } from 'react'; import { GA_MEASUREMENT_ID, pageview } from '@/lib/analytics/gtag'; function GAPageTracker() { const pathname = usePathname(); const searchParams = useSearchParams(); useEffect(() => { if (pathname) { const url = pathname + (searchParams?.toString() ? `?${searchParams}` : ''); pageview(url); } }, [pathname, searchParams]); return null; } export function GoogleAnalytics() { if (!GA_MEASUREMENT_ID) return null; return ( <>