Menu
Back to Blog
2 min read
Performance

Performance Monitoring & Analytics

Web Performance Monitoring mit Next.js. Real User Monitoring, Analytics Integration, Error Tracking und Dashboards.

Performance MonitoringRUMAnalyticsError TrackingWeb VitalsSentry
Performance Monitoring & Analytics

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

ToolTypePricingBest For
**Vercel Analytics**RUMFree tierVercel deployments
**Google Analytics**AnalyticsFreeGeneral analytics
**Sentry**Error TrackingFree tierError monitoring
**Datadog**APMPaidEnterprise
**Plausible**AnalyticsPaidPrivacy-focused
**Grafana Cloud**ObservabilityFree tierCustom 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