Files
Portfolio/blog-posts/80-performance-monitoring-analytics.md
T
damjan_savicandClaude Opus 4.5 43484c5023 Add blog posts, cleanup unused files, update components
- Add 100 blog posts covering AI, development, and tech topics
- Add .env.example for environment configuration
- Add accessibility and lighthouse audit scripts
- Remove obsolete SEO reports and temporary files
- Remove dev-dist build artifacts and backup files
- Remove unused portrait images (moved/consolidated elsewhere)
- Update contact form and component improvements

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-25 11:42:11 +01:00

22 KiB

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:

  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