Set VITE_STORYBOOK=true for storybook dev/build so the logger never sends POST to /logs/frontend in the isolated UI environment. Prevents 94+ failed network requests in audit and keeps Storybook hermetic. Co-authored-by: Cursor <cursoragent@cursor.com>
228 lines
7.1 KiB
TypeScript
228 lines
7.1 KiB
TypeScript
/**
|
|
* 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é)
|
|
// Skip sending in Storybook so no network calls run in isolated UI environment
|
|
const isStorybook = import.meta.env.VITE_STORYBOOK === 'true';
|
|
const logEndpoint = isStorybook
|
|
? null
|
|
: (import.meta.env.VITE_LOG_ENDPOINT ||
|
|
(import.meta.env.VITE_API_URL
|
|
? `${import.meta.env.VITE_API_URL}/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<string, unknown>,
|
|
): Promise<void> {
|
|
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;
|