feat: Observabilité et monitoring complet
Implémentation complète de la stack d'observabilité pour le monitoring de la plateforme multi-tenant Classeo. ## Error Tracking (GlitchTip) - Intégration Sentry SDK avec GlitchTip auto-hébergé - Scrubber PII avant envoi (RGPD: emails, tokens JWT, NIR français) - Contexte enrichi: tenant_id, user_id, correlation_id - Configuration backend (sentry.yaml) et frontend (sentry.ts) ## Metrics (Prometheus) - Endpoint /metrics avec restriction IP en production - Métriques HTTP: requests_total, request_duration_seconds (histogramme) - Métriques sécurité: login_failures_total par tenant - Métriques santé: health_check_status (postgres, redis, rabbitmq) - Storage Redis pour persistance entre requêtes ## Logs (Loki) - Processors Monolog: CorrelationIdLogProcessor, PiiScrubberLogProcessor - Détection PII: emails, téléphones FR, tokens JWT, NIR français - Labels structurés: tenant_id, correlation_id, level ## Dashboards (Grafana) - Dashboard principal: latence P50/P95/P99, error rate, RPS - Dashboard par tenant: métriques isolées par sous-domaine - Dashboard infrastructure: santé postgres/redis/rabbitmq - Datasources avec UIDs fixes pour portabilité ## Alertes (Alertmanager) - HighApiLatencyP95/P99: SLA monitoring (200ms/500ms) - HighErrorRate: error rate > 1% pendant 2 min - ExcessiveLoginFailures: détection brute force - ApplicationUnhealthy: health check failures ## Infrastructure - InfrastructureHealthChecker: service partagé (DRY) - HealthCheckController: endpoint /health pour load balancers - Pre-push hook: make ci && make e2e avant push
This commit is contained in:
115
frontend/src/lib/monitoring/webVitals.ts
Normal file
115
frontend/src/lib/monitoring/webVitals.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
/**
|
||||
* Web Vitals monitoring for frontend performance.
|
||||
*
|
||||
* Captures Core Web Vitals (LCP, FID, CLS) and sends to analytics.
|
||||
* These metrics are critical for user experience and SEO.
|
||||
*
|
||||
* @see Story 1.8 - T8.4: Web Vitals (FCP, LCP, TTI)
|
||||
*/
|
||||
|
||||
import { onCLS, onFCP, onINP, onLCP, onTTFB, type Metric } from 'web-vitals';
|
||||
|
||||
type VitalsReporter = (metric: VitalMetric) => void;
|
||||
|
||||
export interface VitalMetric {
|
||||
name: 'CLS' | 'FCP' | 'INP' | 'LCP' | 'TTFB';
|
||||
value: number;
|
||||
rating: 'good' | 'needs-improvement' | 'poor';
|
||||
delta: number;
|
||||
id: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Web Vitals thresholds (Core Web Vitals 2024 standards).
|
||||
*
|
||||
* - LCP (Largest Contentful Paint): < 2.5s good, < 4s needs improvement
|
||||
* - FCP (First Contentful Paint): < 1.8s good, < 3s needs improvement
|
||||
* - INP (Interaction to Next Paint): < 200ms good, < 500ms needs improvement
|
||||
* - CLS (Cumulative Layout Shift): < 0.1 good, < 0.25 needs improvement
|
||||
* - TTFB (Time to First Byte): < 800ms good, < 1.8s needs improvement
|
||||
*/
|
||||
const THRESHOLDS = {
|
||||
LCP: { good: 2500, needsImprovement: 4000 },
|
||||
FCP: { good: 1800, needsImprovement: 3000 },
|
||||
INP: { good: 200, needsImprovement: 500 },
|
||||
CLS: { good: 0.1, needsImprovement: 0.25 },
|
||||
TTFB: { good: 800, needsImprovement: 1800 }
|
||||
} as const;
|
||||
|
||||
function getRating(
|
||||
name: VitalMetric['name'],
|
||||
value: number
|
||||
): 'good' | 'needs-improvement' | 'poor' {
|
||||
const threshold = THRESHOLDS[name];
|
||||
if (value <= threshold.good) return 'good';
|
||||
if (value <= threshold.needsImprovement) return 'needs-improvement';
|
||||
return 'poor';
|
||||
}
|
||||
|
||||
function createVitalMetric(metric: Metric): VitalMetric {
|
||||
return {
|
||||
name: metric.name as VitalMetric['name'],
|
||||
value: metric.value,
|
||||
rating: getRating(metric.name as VitalMetric['name'], metric.value),
|
||||
delta: metric.delta,
|
||||
id: metric.id
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize Web Vitals collection.
|
||||
*
|
||||
* @param reporter - Callback function to report metrics (e.g., to analytics)
|
||||
*/
|
||||
export function initWebVitals(reporter: VitalsReporter): void {
|
||||
// Core Web Vitals
|
||||
onLCP((metric) => reporter(createVitalMetric(metric)));
|
||||
onCLS((metric) => reporter(createVitalMetric(metric)));
|
||||
onINP((metric) => reporter(createVitalMetric(metric)));
|
||||
|
||||
// Other Web Vitals
|
||||
onFCP((metric) => reporter(createVitalMetric(metric)));
|
||||
onTTFB((metric) => reporter(createVitalMetric(metric)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Default reporter that logs to console (dev) or sends to backend (prod).
|
||||
*/
|
||||
export function createDefaultReporter(options: {
|
||||
endpoint?: string;
|
||||
debug?: boolean;
|
||||
}): VitalsReporter {
|
||||
return (metric: VitalMetric) => {
|
||||
// Log in development
|
||||
if (options.debug) {
|
||||
console.log(`[WebVitals] ${metric.name}: ${metric.value.toFixed(2)} (${metric.rating})`);
|
||||
}
|
||||
|
||||
// Send to analytics endpoint in production
|
||||
if (options.endpoint) {
|
||||
// Use sendBeacon for reliability during page unload
|
||||
const body = JSON.stringify({
|
||||
metric: metric.name,
|
||||
value: metric.value,
|
||||
rating: metric.rating,
|
||||
delta: metric.delta,
|
||||
id: metric.id,
|
||||
timestamp: Date.now(),
|
||||
url: window.location.href
|
||||
});
|
||||
|
||||
if (navigator.sendBeacon) {
|
||||
navigator.sendBeacon(options.endpoint, body);
|
||||
} else {
|
||||
fetch(options.endpoint, {
|
||||
method: 'POST',
|
||||
body,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
keepalive: true
|
||||
}).catch(() => {
|
||||
// Silently fail - vitals are best-effort
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user