From 7b85cb2e57cda57e68d9db5a174e626d3cd00ae4 Mon Sep 17 00:00:00 2001 From: senke Date: Sat, 27 Dec 2025 01:58:49 +0100 Subject: [PATCH] =?UTF-8?q?[LOGGING]=20Fix=20#20:=20Int=C3=A9gration=20Sen?= =?UTF-8?q?try=20pour=20error=20tracking=20frontend=20-=20Capture=20automa?= =?UTF-8?q?tique,=20enrichissement=20contexte,=20int=C3=A9gration=20logger?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/package-lock.json | 153 ++++++++++++++++++++++ apps/web/package.json | 1 + apps/web/src/components/ErrorBoundary.tsx | 24 +++- apps/web/src/config/env.ts | 11 +- apps/web/src/lib/sentry.ts | 144 ++++++++++++++++++++ apps/web/src/main.tsx | 5 + apps/web/src/utils/logger.ts | 32 ++++- 7 files changed, 362 insertions(+), 8 deletions(-) create mode 100644 apps/web/src/lib/sentry.ts diff --git a/apps/web/package-lock.json b/apps/web/package-lock.json index bce8f4dd1..f24a375e4 100644 --- a/apps/web/package-lock.json +++ b/apps/web/package-lock.json @@ -26,6 +26,7 @@ "@radix-ui/react-switch": "^1.0.3", "@radix-ui/react-tabs": "^1.0.4", "@radix-ui/react-toast": "^1.1.5", + "@sentry/react": "^10.32.1", "@tanstack/react-query": "^5.17.0", "@tanstack/react-virtual": "^3.13.12", "@types/dompurify": "^3.0.5", @@ -4031,6 +4032,117 @@ "win32" ] }, + "node_modules/@sentry-internal/browser-utils": { + "version": "10.32.1", + "resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-10.32.1.tgz", + "integrity": "sha512-sjLLep1es3rTkbtAdTtdpc/a6g7v7bK5YJiZJsUigoJ4NTiFeMI5uIDCxbH/tjJ1q23YE1LzVn7T96I+qBRjHA==", + "license": "MIT", + "dependencies": { + "@sentry/core": "10.32.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry-internal/browser-utils/node_modules/@sentry/core": { + "version": "10.32.1", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-10.32.1.tgz", + "integrity": "sha512-PH2ldpSJlhqsMj2vCTyU0BI2Fx1oIDhm7Izo5xFALvjVCS0gmlqHt1udu6YlKn8BtpGH6bGzssvv5APrk+OdPQ==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry-internal/feedback": { + "version": "10.32.1", + "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-10.32.1.tgz", + "integrity": "sha512-O24G8jxbfBY1RE/v2qFikPJISVMOrd/zk8FKyl+oUVYdOxU2Ucjk2cR3EQruBFlc7irnL6rT3GPfRZ/kBgLkmQ==", + "license": "MIT", + "dependencies": { + "@sentry/core": "10.32.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry-internal/feedback/node_modules/@sentry/core": { + "version": "10.32.1", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-10.32.1.tgz", + "integrity": "sha512-PH2ldpSJlhqsMj2vCTyU0BI2Fx1oIDhm7Izo5xFALvjVCS0gmlqHt1udu6YlKn8BtpGH6bGzssvv5APrk+OdPQ==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry-internal/replay": { + "version": "10.32.1", + "resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-10.32.1.tgz", + "integrity": "sha512-KKmLUgIaLRM0VjrMA1ByQTawZyRDYSkG2evvEOVpEtR9F0sumidAQdi7UY71QEKE1RYe/Jcp/3WoaqsMh8tbnQ==", + "license": "MIT", + "dependencies": { + "@sentry-internal/browser-utils": "10.32.1", + "@sentry/core": "10.32.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry-internal/replay-canvas": { + "version": "10.32.1", + "resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-10.32.1.tgz", + "integrity": "sha512-/XGTzWNWVc+B691fIVekV2KeoHFEDA5KftrLFAhEAW7uWOwk/xy3aQX4TYM0LcPm2PBKvoumlAD+Sd/aXk63oA==", + "license": "MIT", + "dependencies": { + "@sentry-internal/replay": "10.32.1", + "@sentry/core": "10.32.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry-internal/replay-canvas/node_modules/@sentry/core": { + "version": "10.32.1", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-10.32.1.tgz", + "integrity": "sha512-PH2ldpSJlhqsMj2vCTyU0BI2Fx1oIDhm7Izo5xFALvjVCS0gmlqHt1udu6YlKn8BtpGH6bGzssvv5APrk+OdPQ==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry-internal/replay/node_modules/@sentry/core": { + "version": "10.32.1", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-10.32.1.tgz", + "integrity": "sha512-PH2ldpSJlhqsMj2vCTyU0BI2Fx1oIDhm7Izo5xFALvjVCS0gmlqHt1udu6YlKn8BtpGH6bGzssvv5APrk+OdPQ==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry/browser": { + "version": "10.32.1", + "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-10.32.1.tgz", + "integrity": "sha512-NPNCXTZ05ZGTFyJdKNqjykpFm+urem0ebosILQiw3C4BxNVNGH4vfYZexyl6prRhmg91oB6GjVNiVDuJiap1gg==", + "license": "MIT", + "dependencies": { + "@sentry-internal/browser-utils": "10.32.1", + "@sentry-internal/feedback": "10.32.1", + "@sentry-internal/replay": "10.32.1", + "@sentry-internal/replay-canvas": "10.32.1", + "@sentry/core": "10.32.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry/browser/node_modules/@sentry/core": { + "version": "10.32.1", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-10.32.1.tgz", + "integrity": "sha512-PH2ldpSJlhqsMj2vCTyU0BI2Fx1oIDhm7Izo5xFALvjVCS0gmlqHt1udu6YlKn8BtpGH6bGzssvv5APrk+OdPQ==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@sentry/core": { "version": "6.19.7", "resolved": "https://registry.npmjs.org/@sentry/core/-/core-6.19.7.tgz", @@ -4136,6 +4248,32 @@ "dev": true, "license": "0BSD" }, + "node_modules/@sentry/react": { + "version": "10.32.1", + "resolved": "https://registry.npmjs.org/@sentry/react/-/react-10.32.1.tgz", + "integrity": "sha512-/tX0HeACbAmVP57x8txTrGk/U3fa9pDBaoAtlOrnPv5VS/aC5SGkehXWeTGSAa+ahlOWwp3IF8ILVXRiOoG/Vg==", + "license": "MIT", + "dependencies": { + "@sentry/browser": "10.32.1", + "@sentry/core": "10.32.1", + "hoist-non-react-statics": "^3.3.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "react": "^16.14.0 || 17.x || 18.x || 19.x" + } + }, + "node_modules/@sentry/react/node_modules/@sentry/core": { + "version": "10.32.1", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-10.32.1.tgz", + "integrity": "sha512-PH2ldpSJlhqsMj2vCTyU0BI2Fx1oIDhm7Izo5xFALvjVCS0gmlqHt1udu6YlKn8BtpGH6bGzssvv5APrk+OdPQ==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@sentry/types": { "version": "6.19.7", "resolved": "https://registry.npmjs.org/@sentry/types/-/types-6.19.7.tgz", @@ -9275,6 +9413,21 @@ "integrity": "sha512-E3a5VwgXimGHwpRGV+WxRTKeSp2DW5DI5MWv34ulL3t5UNmyJWCQ1KmLEHbYzcfThfXG8amBL+fCYPneGHC4VA==", "license": "Apache-2.0" }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "license": "BSD-3-Clause", + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/hoist-non-react-statics/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, "node_modules/hoopy": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/hoopy/-/hoopy-0.1.4.tgz", diff --git a/apps/web/package.json b/apps/web/package.json index 96aaa5495..a3c17fe3e 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -52,6 +52,7 @@ "@radix-ui/react-switch": "^1.0.3", "@radix-ui/react-tabs": "^1.0.4", "@radix-ui/react-toast": "^1.1.5", + "@sentry/react": "^10.32.1", "@tanstack/react-query": "^5.17.0", "@tanstack/react-virtual": "^3.13.12", "@types/dompurify": "^3.0.5", diff --git a/apps/web/src/components/ErrorBoundary.tsx b/apps/web/src/components/ErrorBoundary.tsx index a99eda6d5..3690e7c10 100644 --- a/apps/web/src/components/ErrorBoundary.tsx +++ b/apps/web/src/components/ErrorBoundary.tsx @@ -1,4 +1,5 @@ import { Component, type ErrorInfo, type ReactNode } from 'react'; +import * as Sentry from '@sentry/react'; import { Button } from '@/components/ui/button'; import { Card, @@ -8,6 +9,7 @@ import { CardTitle, } from '@/components/ui/card'; import { AlertTriangle, RefreshCw } from 'lucide-react'; +import { logger, getLogContext } from '@/utils/logger'; interface Props { children: ReactNode; @@ -30,14 +32,30 @@ export class ErrorBoundary extends Component { return { hasError: true, error }; } - componentDidCatch(error: Error, errorInfo: ErrorInfo) { + override componentDidCatch(error: Error, errorInfo: ErrorInfo) { this.setState({ error, errorInfo, }); - // Log l'erreur pour le monitoring - console.error('ErrorBoundary caught an error:', error, errorInfo); + // FIX #20: Logger l'erreur avec le logger structuré et Sentry + const logContext = getLogContext(); + logger.error('[ErrorBoundary] React error caught', { + error: error.message, + stack: error.stack, + componentStack: errorInfo.componentStack, + ...logContext, + }); + + // Envoyer à Sentry avec contexte enrichi + Sentry.captureException(error, { + contexts: { + react: { + componentStack: errorInfo.componentStack, + }, + application: logContext, + }, + }); } handleReset = () => { diff --git a/apps/web/src/config/env.ts b/apps/web/src/config/env.ts index 8bc7e196c..e7285d781 100644 --- a/apps/web/src/config/env.ts +++ b/apps/web/src/config/env.ts @@ -17,6 +17,8 @@ const envSchema = z.object({ .transform((val) => val === '1' || val === 'true') .default('0'), VITE_FCM_VAPID_KEY: z.string().optional(), + // FIX #20: Configuration Sentry pour error tracking + VITE_SENTRY_DSN: z.string().url().optional(), }); // Validation et parsing des variables d'environnement @@ -29,9 +31,10 @@ const parseEnv = () => { VITE_UPLOAD_URL: import.meta.env.VITE_UPLOAD_URL, VITE_APP_NAME: import.meta.env.VITE_APP_NAME, VITE_DEBUG: import.meta.env.VITE_DEBUG, - VITE_USE_MSW: import.meta.env.VITE_USE_MSW, - VITE_FCM_VAPID_KEY: import.meta.env.VITE_FCM_VAPID_KEY, - }); + VITE_USE_MSW: import.meta.env.VITE_USE_MSW, + VITE_FCM_VAPID_KEY: import.meta.env.VITE_FCM_VAPID_KEY, + VITE_SENTRY_DSN: import.meta.env.VITE_SENTRY_DSN, + }); } catch (error) { if (error instanceof z.ZodError) { console.error('❌ Invalid environment variables:', error.errors); @@ -58,6 +61,8 @@ export const env = { DEBUG: validatedEnv.VITE_DEBUG, USE_MSW: validatedEnv.VITE_USE_MSW, FCM_VAPID_KEY: validatedEnv.VITE_FCM_VAPID_KEY, + // FIX #20: Configuration Sentry + SENTRY_DSN: validatedEnv.VITE_SENTRY_DSN, } as const; // Type pour les variables d'environnement diff --git a/apps/web/src/lib/sentry.ts b/apps/web/src/lib/sentry.ts new file mode 100644 index 000000000..ee0c2a3a7 --- /dev/null +++ b/apps/web/src/lib/sentry.ts @@ -0,0 +1,144 @@ +/** + * FIX #20: Configuration Sentry pour error tracking + * - Intégration avec le logger structuré + * - Capture automatique des erreurs React + * - Enrichissement avec contexte (request_id, user_id, etc.) + */ + +import * as Sentry from '@sentry/react'; +import { logger, getLogContext } from '@/utils/logger'; + +/** + * Initialiser Sentry avec configuration + */ +export function initSentry(): void { + const dsn = import.meta.env.VITE_SENTRY_DSN; + const environment = import.meta.env.MODE || 'development'; + const enabled = import.meta.env.PROD && dsn; // Activer uniquement en production si DSN configuré + + if (!enabled) { + logger.debug('[Sentry] Error tracking disabled', { + reason: !dsn ? 'DSN not configured' : 'Not in production', + environment, + }); + return; + } + + Sentry.init({ + dsn, + environment, + integrations: [ + Sentry.browserTracingIntegration(), + Sentry.replayIntegration({ + maskAllText: true, + blockAllMedia: true, + }), + ], + // Performance Monitoring + tracesSampleRate: environment === 'production' ? 0.1 : 1.0, // 10% en prod, 100% en dev + // Session Replay + replaysSessionSampleRate: 0.1, // 10% des sessions + replaysOnErrorSampleRate: 1.0, // 100% des sessions avec erreur + // Filtrage des erreurs + beforeSend(event, hint) { + // Enrichir avec le contexte du logger + const logContext = getLogContext(); + if (logContext.request_id) { + event.tags = { ...event.tags, request_id: logContext.request_id }; + } + if (logContext.user_id) { + event.user = { ...event.user, id: String(logContext.user_id) }; + } + + // Logger l'erreur avec le logger structuré + const error = hint.originalException; + logger.error('[Sentry] Error captured', { + error: error instanceof Error ? error.message : String(error), + sentry_event_id: event.event_id, + ...logContext, + }); + + return event; + }, + // Ignorer certaines erreurs + ignoreErrors: [ + // Erreurs réseau courantes + 'NetworkError', + 'Network request failed', + 'Failed to fetch', + // Erreurs de résolution DNS + 'Resolving timed out', + // Erreurs de CORS + 'CORS', + // Erreurs de script tiers + 'Script error', + 'Non-Error promise rejection captured', + ], + // Ignorer certaines URLs + denyUrls: [ + // Extensions de navigateur + /extensions\//i, + /^chrome:\/\//i, + /^chrome-extension:\/\//i, + // Scripts tiers + /cdn\./i, + ], + }); + + logger.info('[Sentry] Error tracking initialized', { + environment, + dsn_configured: !!dsn, + }); +} + +/** + * Enrichir le contexte Sentry avec les informations du logger + */ +export function setSentryContext(context: Record): void { + Sentry.setContext('application', context); +} + +/** + * Capturer une exception manuellement + */ +export function captureException( + error: Error, + context?: Record, +): string { + if (context) { + Sentry.setContext('custom', context); + } + + // Enrichir avec le contexte du logger + const logContext = getLogContext(); + if (logContext.request_id) { + Sentry.setTag('request_id', String(logContext.request_id)); + } + if (logContext.user_id) { + Sentry.setUser({ id: String(logContext.user_id) }); + } + + return Sentry.captureException(error); +} + +/** + * Capturer un message personnalisé + */ +export function captureMessage( + message: string, + level: Sentry.SeverityLevel = 'info', + context?: Record, +): string { + if (context) { + Sentry.setContext('custom', context); + } + + // Enrichir avec le contexte du logger + const logContext = getLogContext(); + if (logContext.request_id) { + Sentry.setTag('request_id', String(logContext.request_id)); + } + + return Sentry.captureMessage(message, level); +} + diff --git a/apps/web/src/main.tsx b/apps/web/src/main.tsx index bc9776780..40720d832 100644 --- a/apps/web/src/main.tsx +++ b/apps/web/src/main.tsx @@ -7,11 +7,16 @@ import { App } from './app/App'; import './index.css'; // Initialize i18next before React renders import './lib/i18n'; +// FIX #20: Initialize Sentry for error tracking +import { initSentry } from './lib/sentry'; // FE-API-019: Initialize MSW for development if enabled import { env } from './config/env'; // HMR Force Update: 1765126900 +// FIX #20: Initialize Sentry before React renders +initSentry(); + const queryClient = new QueryClient({ defaultOptions: { queries: { diff --git a/apps/web/src/utils/logger.ts b/apps/web/src/utils/logger.ts index 2e91ca35f..3a759219b 100644 --- a/apps/web/src/utils/logger.ts +++ b/apps/web/src/utils/logger.ts @@ -1,15 +1,21 @@ /** - * FIX #18, #19, #22, #25: Logger structuré pour le frontend + * 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 - à implémenter si nécessaire) + * - 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 { @@ -171,6 +177,28 @@ export const logger: Logger = { 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 + } + } } }, };