Files
Classeo/frontend/src/lib/monitoring/webVitals.ts
Mathias STRASSER d3c6773be5 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
2026-02-04 12:59:12 +01:00

116 lines
3.3 KiB
TypeScript

/**
* 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
});
}
}
};
}