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>
This commit is contained in:
@@ -0,0 +1,804 @@
|
||||
# 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)
|
||||
Reference in New Issue
Block a user