Files
Classeo/frontend/src/lib/monitoring/webVitals.ts
Mathias STRASSER ff18850a43 feat: Configuration du mode de notation par établissement
Les établissements scolaires utilisent des systèmes d'évaluation variés
(notes /20, /10, lettres, compétences, sans notes). Jusqu'ici l'application
imposait implicitement le mode notes /20, ce qui ne correspondait pas
à la réalité pédagogique de nombreuses écoles.

Cette configuration permet à chaque établissement de choisir son mode
de notation par année scolaire, avec verrouillage automatique dès que
des notes ont été saisies pour éviter les incohérences. Le Score Sérénité
adapte ses pondérations selon le mode choisi (les compétences sont
converties via un mapping, le mode sans notes exclut la composante notes).
2026-02-07 02:32:20 +01:00

117 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) {
// eslint-disable-next-line no-console
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
});
}
}
};
}