[LOGGING] Fix #20: Intégration Sentry pour error tracking frontend - Capture automatique, enrichissement contexte, intégration logger
This commit is contained in:
parent
0813aa3ad2
commit
7b85cb2e57
7 changed files with 362 additions and 8 deletions
153
apps/web/package-lock.json
generated
153
apps/web/package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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 = () => {
|
||||
|
|
|
|||
|
|
@ -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
144
apps/web/src/lib/sentry.ts
Normal 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);
|
||||
}
|
||||
|
||||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in a new issue