# 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<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();
    }
  });
}
```

```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 (
    <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

```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<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

```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 (
    <>
      <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

```typescript
// 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);
}
```

```tsx
// 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:

1. **Web Vitals**: Kontinuierliche RUM-Erfassung
2. **Error Tracking**: Sentry für schnelle Problemlösung
3. **Analytics**: User Behavior verstehen
4. **Alerting**: Proaktive Benachrichtigungen

Monitoring ist kein einmaliges Setup, sondern kontinuierlicher Prozess.

---

## Bildprompts

1. "Performance monitoring dashboard, real-time charts and metrics"
2. "Error tracking interface showing stack traces and user sessions"
3. "Web Vitals visualization, LCP/INP/CLS gauges with colors"

---

## Quellen

- [Vercel Analytics](https://vercel.com/docs/analytics)
- [Sentry Next.js](https://docs.sentry.io/platforms/javascript/guides/nextjs/)
- [Google Analytics 4](https://developers.google.com/analytics/devguides/collection/ga4)
- [Web Vitals Library](https://github.com/GoogleChrome/web-vitals)
