- 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>
805 lines
22 KiB
Markdown
805 lines
22 KiB
Markdown
# 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)
|