/** * FIX #18, #19, #20, #22, #25: Logger structuré pour le frontend * - Support de la corrélation avec request_id (FIX #22) * - Logs structurés en JSON en production (FIX #25: Standardisé) * - Format texte en développement pour lisibilité * - Filtrage selon l'environnement (FIX #21, #24) * - Contexte global pour corrélation (FIX #19, #22) * - Intégration Sentry pour error tracking (FIX #20) * * FIX #19: Logger structuré complet avec : * - Format JSON optionnel ✅ * - Corrélation avec request_id ✅ * - Envoi vers endpoint de logging (optionnel - via VITE_LOG_ENDPOINT) * * FIX #20: Error tracking avec Sentry : * - Capture automatique des erreurs React ✅ * - Enrichissement avec contexte (request_id, user_id) ✅ * - Intégration avec le logger structuré ✅ */ interface LogContext { request_id?: string; user_id?: string; component?: string; action?: string; [key: string]: unknown; } interface Logger { debug: (message: string, context?: LogContext, ...args: unknown[]) => void; info: (message: string, context?: LogContext, ...args: unknown[]) => void; warn: (message: string, context?: LogContext, ...args: unknown[]) => void; error: (message: string, context?: LogContext, ...args: unknown[]) => void; } const isDev = import.meta.env.DEV; const isProd = import.meta.env.PROD; // FIX #21, #24: Configuration du niveau de log via variable d'environnement // FIX #24: Standardiser sur LOG_LEVEL (avec fallback sur VITE_LOG_LEVEL pour compatibilité) const logLevel = ( import.meta.env.VITE_LOG_LEVEL || import.meta.env.LOG_LEVEL || (isDev ? 'DEBUG' : 'WARN') ).toUpperCase(); // Contexte global pour la corrélation let globalContext: LogContext = {}; /** * FIX #22: Définir le contexte global (request_id, user_id, etc.) */ export function setLogContext(context: LogContext): void { globalContext = { ...globalContext, ...context }; } /** * FIX #22: Obtenir le contexte global */ export function getLogContext(): LogContext { return { ...globalContext }; } /** * FIX #22: Effacer le contexte global */ export function clearLogContext(): void { globalContext = {}; } /** * FIX #19: Formater un log structuré avec support JSON et corrélation */ function formatLog( level: string, message: string, context?: LogContext, ...args: unknown[] ): void { const logContext = { ...globalContext, ...context }; const timestamp = new Date().toISOString(); // FIX #25: Standardiser sur JSON en production pour faciliter l'agrégation if (isProd) { // En production : JSON structuré (standardisé pour agrégation) const logEntry = { timestamp, level, message, ...logContext, ...(args.length > 0 && { data: args }), }; const jsonLog = JSON.stringify(logEntry); console.log(jsonLog); // FIX #19: Envoi optionnel vers endpoint de logging (si configuré) // Par défaut, utiliser l'endpoint backend si VITE_LOG_ENDPOINT n'est pas défini const logEndpoint = import.meta.env.VITE_LOG_ENDPOINT || (import.meta.env.VITE_API_URL ? `${import.meta.env.VITE_API_URL}/api/v1/logs/frontend` : null); // Envoyer tous les logs (pas seulement les erreurs) vers le backend pour archivage if (logEndpoint) { sendLogToEndpoint(logEndpoint, logEntry).catch(() => { // Ignorer les erreurs silencieusement pour ne pas bloquer l'application }); } } else { // En développement : format lisible const contextStr = Object.keys(logContext).length > 0 ? ` ${JSON.stringify(logContext)}` : ''; const argsStr = args.length > 0 ? ` ${args.map((a) => JSON.stringify(a)).join(' ')}` : ''; console.log(`[${level}] ${message}${contextStr}${argsStr}`); } } /** * FIX #19: Envoyer un log vers un endpoint de logging (optionnel) */ async function sendLogToEndpoint( endpoint: string, logEntry: Record, ): Promise { try { // Utiliser sendBeacon pour un envoi non-bloquant if (navigator.sendBeacon) { const blob = new Blob([JSON.stringify(logEntry)], { type: 'application/json', }); navigator.sendBeacon(endpoint, blob); } else { // Fallback sur fetch si sendBeacon n'est pas disponible await fetch(endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(logEntry), keepalive: true, // Permet l'envoi même après navigation }); } } catch (error) { // Ignorer silencieusement pour ne pas bloquer l'application // Les logs sont déjà affichés dans la console } } /** * FIX #21: Vérifier si un niveau de log doit être affiché */ function shouldLog(level: string): boolean { const levelOrder = ['DEBUG', 'INFO', 'WARN', 'ERROR']; const currentLevelIndex = levelOrder.indexOf(logLevel); const messageLevelIndex = levelOrder.indexOf(level); // Si le niveau n'est pas trouvé, autoriser par défaut (sécurité) if (currentLevelIndex === -1 || messageLevelIndex === -1) { return true; } // Logger si le niveau du message est >= au niveau configuré return messageLevelIndex >= currentLevelIndex; } /** * Logger structuré qui supporte la corrélation * FIX #21: Respecte le niveau de log configuré via VITE_LOG_LEVEL */ export const logger: Logger = { debug: (message: string, context?: LogContext, ...args: unknown[]) => { if (shouldLog('DEBUG')) { formatLog('DEBUG', message, context, ...args); } }, info: (message: string, context?: LogContext, ...args: unknown[]) => { if (shouldLog('INFO')) { formatLog('INFO', message, context, ...args); } }, warn: (message: string, context?: LogContext, ...args: unknown[]) => { if (shouldLog('WARN')) { formatLog('WARN', message, context, ...args); } }, error: (message: string, context?: LogContext, ...args: unknown[]) => { if (shouldLog('ERROR')) { formatLog('ERROR', message, context, ...args); // FIX #20: Envoyer les erreurs à Sentry si disponible if (import.meta.env.PROD && import.meta.env.VITE_SENTRY_DSN) { try { // Import dynamique pour éviter les erreurs si Sentry n'est pas configuré import('@sentry/react') .then((Sentry) => { const logContext = { ...globalContext, ...context }; const error = new Error(message); Sentry.captureException(error, { contexts: { application: logContext, }, tags: logContext.request_id ? { request_id: String(logContext.request_id) } : undefined, user: logContext.user_id ? { id: String(logContext.user_id) } : undefined, }); }) .catch(() => { // Ignorer silencieusement si Sentry n'est pas disponible }); } catch { // Ignorer silencieusement } } } }, }; /** * Export par défaut pour faciliter l'import */ export default logger;