2 min read
PerformancePerformance Monitoring & Analytics
Web Performance Monitoring mit Next.js. Real User Monitoring, Analytics Integration, Error Tracking und Dashboards.
Performance MonitoringRUMAnalyticsError TrackingWeb VitalsSentry

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
// 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<string, [number, number]> = {
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();
}
});
}// 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
// 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 (
<html lang="de">
<body>
{children}
<Analytics />
<SpeedInsights />
</body>
</html>
);
}
// 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 (
<button onClick={handleClick} className="btn-primary">
{label}
</button>
);
}Sentry Error Tracking
// 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
});// 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<string, unknown>
) {
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<string, unknown>
) {
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<T>(
url: string,
options?: RequestInit
): Promise<T> {
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
// 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
});
}// 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 (
<>
<Script
strategy="afterInteractive"
src={`https://www.googletagmanager.com/gtag/js?id=${GA_MEASUREMENT_ID}`}
/>
<Script
id="gtag-init"
strategy="afterInteractive"
dangerouslySetInnerHTML={{
__html: `
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', '${GA_MEASUREMENT_ID}', {
page_path: window.location.pathname,
anonymize_ip: true
});
`
}}
/>
<Suspense fallback={null}>
<GAPageTracker />
</Suspense>
</>
);
}Custom Analytics Dashboard
// lib/analytics/dashboard.ts
interface AnalyticsData {
pageViews: number;
uniqueVisitors: number;
avgSessionDuration: number;
bounceRate: number;
topPages: Array<{ path: string; views: number }>;
vitals: {
lcp: { avg: number; p75: number };
inp: { avg: number; p75: number };
cls: { avg: number; p75: number };
};
errors: Array<{
message: string;
count: number;
lastSeen: Date;
}>;
}
export async function getAnalyticsDashboardData(
startDate: Date,
endDate: Date
): Promise<AnalyticsData> {
// Aggregate data from various sources
const [vitals, pageViews, errors] = await Promise.all([
getVitalsData(startDate, endDate),
getPageViewsData(startDate, endDate),
getErrorsData(startDate, endDate)
]);
return {
pageViews: pageViews.total,
uniqueVisitors: pageViews.unique,
avgSessionDuration: pageViews.avgDuration,
bounceRate: pageViews.bounceRate,
topPages: pageViews.topPages,
vitals,
errors
};
}
// API Route for dashboard
// app/api/analytics/dashboard/route.ts
import { NextResponse } from 'next/server';
import { getAnalyticsDashboardData } from '@/lib/analytics/dashboard';
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const startDate = new Date(searchParams.get('start') || Date.now() - 7 * 24 * 60 * 60 * 1000);
const endDate = new Date(searchParams.get('end') || Date.now());
const data = await getAnalyticsDashboardData(startDate, endDate);
return NextResponse.json(data);
}// app/admin/analytics/page.tsx - Analytics Dashboard
'use client';
import { useState, useEffect } from 'react';
import { Card } from '@/components/ui/Card';
interface DashboardData {
pageViews: number;
uniqueVisitors: number;
vitals: {
lcp: { avg: number; p75: number };
inp: { avg: number; p75: number };
cls: { avg: number; p75: number };
};
}
export default function AnalyticsDashboard() {
const [data, setData] = useState<DashboardData | null>(null);
const [loading, setLoading] = useState(true);
const [dateRange, setDateRange] = useState('7d');
useEffect(() => {
fetchData();
}, [dateRange]);
const fetchData = async () => {
setLoading(true);
const start = getStartDate(dateRange);
const res = await fetch(`/api/analytics/dashboard?start=${start.toISOString()}`);
const data = await res.json();
setData(data);
setLoading(false);
};
if (loading) return <div>Loading...</div>;
if (!data) return <div>No data available</div>;
return (
<div className="p-6 space-y-6">
<h1 className="text-2xl font-bold">Analytics Dashboard</h1>
{/* Date Range Selector */}
<select
value={dateRange}
onChange={(e) => setDateRange(e.target.value)}
className="border rounded px-3 py-2"
>
<option value="24h">Last 24 hours</option>
<option value="7d">Last 7 days</option>
<option value="30d">Last 30 days</option>
</select>
{/* Overview Cards */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<MetricCard
title="Page Views"
value={data.pageViews.toLocaleString()}
trend="+12%"
/>
<MetricCard
title="Unique Visitors"
value={data.uniqueVisitors.toLocaleString()}
trend="+8%"
/>
<MetricCard
title="LCP (p75)"
value={`${data.vitals.lcp.p75}ms`}
status={getVitalStatus('LCP', data.vitals.lcp.p75)}
/>
<MetricCard
title="INP (p75)"
value={`${data.vitals.inp.p75}ms`}
status={getVitalStatus('INP', data.vitals.inp.p75)}
/>
</div>
{/* Web Vitals Chart */}
<Card title="Core Web Vitals">
<VitalsChart data={data.vitals} />
</Card>
</div>
);
}
function MetricCard({
title,
value,
trend,
status
}: {
title: string;
value: string;
trend?: string;
status?: 'good' | 'needs-improvement' | 'poor';
}) {
const statusColors = {
good: 'bg-green-100 text-green-800',
'needs-improvement': 'bg-yellow-100 text-yellow-800',
poor: 'bg-red-100 text-red-800'
};
return (
<div className="bg-white rounded-lg shadow p-4">
<h3 className="text-sm text-gray-500">{title}</h3>
<p className="text-2xl font-bold mt-1">{value}</p>
{trend && (
<span className="text-sm text-green-600">{trend}</span>
)}
{status && (
<span className={`text-xs px-2 py-1 rounded ${statusColors[status]}`}>
{status}
</span>
)}
</div>
);
}
function getVitalStatus(name: string, value: number): 'good' | 'needs-improvement' | 'poor' {
const thresholds: Record<string, [number, number]> = {
LCP: [2500, 4000],
INP: [200, 500],
CLS: [0.1, 0.25]
};
const [good, poor] = thresholds[name] || [0, 0];
if (value <= good) return 'good';
if (value <= poor) return 'needs-improvement';
return 'poor';
}Monitoring Tools Comparison
| Tool | Type | Pricing | Best For |
|---|---|---|---|
| **Vercel Analytics** | RUM | Free tier | Vercel deployments |
| **Google Analytics** | Analytics | Free | General analytics |
| **Sentry** | Error Tracking | Free tier | Error monitoring |
| **Datadog** | APM | Paid | Enterprise |
| **Plausible** | Analytics | Paid | Privacy-focused |
| **Grafana Cloud** | Observability | Free tier | Custom dashboards |
Fazit
Performance Monitoring umfasst:
- Web Vitals: Kontinuierliche RUM-Erfassung
- Error Tracking: Sentry für schnelle Problemlösung
- Analytics: User Behavior verstehen
- Alerting: Proaktive Benachrichtigungen
Monitoring ist kein einmaliges Setup, sondern kontinuierlicher Prozess.
Bildprompts
- "Performance monitoring dashboard, real-time charts and metrics"
- "Error tracking interface showing stack traces and user sessions"
- "Web Vitals visualization, LCP/INP/CLS gauges with colors"