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
116 lines
3.3 KiB
TypeScript
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
|
|
});
|
|
}
|
|
}
|
|
};
|
|
}
|