/** * Service de monitoring avancé côté client */ export interface PerformanceMetric { name: string; value: number; timestamp: number; tags?: Record; } export interface ErrorMetric { error: string; stack?: string; url: string; userAgent: string; timestamp: number; userId?: string; } export interface UserAction { action: string; component: string; timestamp: number; duration?: number; metadata?: Record; } export class MonitoringService { private static metrics: PerformanceMetric[] = []; private static errors: ErrorMetric[] = []; private static userActions: UserAction[] = []; private static isEnabled = true; /** * Initialise le service de monitoring */ static init(): void { if (typeof window === 'undefined') return; // Monitoring des erreurs JavaScript window.addEventListener('error', (event) => { this.recordError({ error: event.message, stack: event.error?.stack, url: window.location.href, userAgent: navigator.userAgent, timestamp: Date.now() }); }); // Monitoring des promesses rejetées window.addEventListener('unhandledrejection', (event) => { this.recordError({ error: `Unhandled Promise Rejection: ${event.reason}`, url: window.location.href, userAgent: navigator.userAgent, timestamp: Date.now() }); }); // Monitoring des performances de navigation if ('performance' in window) { window.addEventListener('load', () => { setTimeout(() => { this.recordNavigationMetrics(); }, 0); }); } // Envoi périodique des métriques setInterval(() => { this.sendMetrics(); }, 60000); // Toutes les minutes } /** * Enregistre une métrique de performance */ static recordMetric(name: string, value: number, tags?: Record): void { if (!this.isEnabled) return; this.metrics.push({ name, value, timestamp: Date.now(), tags }); // Limiter le nombre de métriques en mémoire if (this.metrics.length > 1000) { this.metrics = this.metrics.slice(-500); } } /** * Enregistre une erreur */ static recordError(error: ErrorMetric): void { if (!this.isEnabled) return; this.errors.push(error); // Limiter le nombre d'erreurs en mémoire if (this.errors.length > 100) { this.errors = this.errors.slice(-50); } } /** * Enregistre une action utilisateur */ static recordUserAction(action: string, component: string, metadata?: Record): void { if (!this.isEnabled) return; this.userActions.push({ action, component, timestamp: Date.now(), metadata }); // Limiter le nombre d'actions en mémoire if (this.userActions.length > 500) { this.userActions = this.userActions.slice(-250); } } /** * Mesure le temps d'exécution d'une fonction */ static async measureTime( name: string, fn: () => Promise, tags?: Record ): Promise { const start = performance.now(); try { const result = await fn(); const duration = performance.now() - start; this.recordMetric(`${name}.duration`, duration, tags); this.recordMetric(`${name}.success`, 1, tags); return result; } catch (error) { const duration = performance.now() - start; this.recordMetric(`${name}.duration`, duration, tags); this.recordMetric(`${name}.error`, 1, tags); throw error; } } /** * Enregistre les métriques de navigation */ private static recordNavigationMetrics(): void { if (!('performance' in window) || !window.performance.timing) return; const timing = window.performance.timing; const navigation = window.performance.navigation; // Temps de chargement de la page const pageLoadTime = timing.loadEventEnd - timing.navigationStart; this.recordMetric('page.load_time', pageLoadTime, { url: window.location.pathname }); // Temps de réponse du serveur const serverResponseTime = timing.responseEnd - timing.requestStart; this.recordMetric('page.server_response_time', serverResponseTime); // Temps de rendu DOM const domRenderTime = timing.domContentLoadedEventEnd - timing.domLoading; this.recordMetric('page.dom_render_time', domRenderTime); // Type de navigation const navigationType = navigation.type === 0 ? 'navigate' : navigation.type === 1 ? 'reload' : navigation.type === 2 ? 'back_forward' : 'unknown'; this.recordMetric('page.navigation_type', 1, { type: navigationType }); } /** * Envoie les métriques au serveur */ private static async sendMetrics(): Promise { if (this.metrics.length === 0 && this.errors.length === 0 && this.userActions.length === 0) { return; } try { const payload = { metrics: [...this.metrics], errors: [...this.errors], userActions: [...this.userActions], timestamp: Date.now(), url: window.location.href, userAgent: navigator.userAgent }; // Envoyer au serveur (endpoint à implémenter) await fetch('/api/monitoring', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); // Vider les buffers après envoi réussi this.metrics = []; this.errors = []; this.userActions = []; } catch (error) { console.warn('Échec de l\'envoi des métriques:', error); } } /** * Obtient les statistiques actuelles */ static getStats(): { metricsCount: number; errorsCount: number; userActionsCount: number; isEnabled: boolean; } { return { metricsCount: this.metrics.length, errorsCount: this.errors.length, userActionsCount: this.userActions.length, isEnabled: this.isEnabled }; } /** * Active ou désactive le monitoring */ static setEnabled(enabled: boolean): void { this.isEnabled = enabled; } /** * Vide tous les buffers */ static clear(): void { this.metrics = []; this.errors = []; this.userActions = []; } } // Initialisation automatique if (typeof window !== 'undefined') { MonitoringService.init(); } // Hook React pour le monitoring des composants export function useMonitoring(componentName: string) { const recordAction = (action: string, metadata?: Record) => { MonitoringService.recordUserAction(action, componentName, metadata); }; const measureAsync = async ( name: string, fn: () => Promise ): Promise => { return MonitoringService.measureTime(`${componentName}.${name}`, fn); }; return { recordAction, measureAsync }; }