[LOGGING] Fix #20: Intégration Sentry pour error tracking frontend - Capture automatique, enrichissement contexte, intégration logger

This commit is contained in:
senke 2025-12-27 01:58:49 +01:00
parent 0813aa3ad2
commit 7b85cb2e57
7 changed files with 362 additions and 8 deletions

View file

@ -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",

View file

@ -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",

View file

@ -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<Props, State> {
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 = () => {

View file

@ -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

144
apps/web/src/lib/sentry.ts Normal file
View file

@ -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<string, unknown>): void {
Sentry.setContext('application', context);
}
/**
* Capturer une exception manuellement
*/
export function captureException(
error: Error,
context?: Record<string, unknown>,
): 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, unknown>,
): 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);
}

View file

@ -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: {

View file

@ -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
}
}
}
},
};