From 101990dfbd0fade0b96ae9770ded37f2b59b8bae Mon Sep 17 00:00:00 2001 From: Serreau Jovann Date: Thu, 15 Jan 2026 20:42:55 +0100 Subject: [PATCH] =?UTF-8?q?```=20=E2=9C=A8=20feat(Sentry):=20Initialise=20?= =?UTF-8?q?Sentry=20pour=20le=20suivi=20des=20erreurs=20et=20performances.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ajoute l'initialisation de Sentry avec tunnel, suivi des performances et replay. ``` --- assets/admin.js | 43 ++++++++-------- assets/app.js | 16 ++++++ package.json | 1 + src/Controller/HomeController.php | 81 ++++++++----------------------- 4 files changed, 60 insertions(+), 81 deletions(-) diff --git a/assets/admin.js b/assets/admin.js index e3ea47f..9f0d648 100644 --- a/assets/admin.js +++ b/assets/admin.js @@ -1,15 +1,29 @@ import './admin.scss' -import * as Turbo from "@hotwired/turbo" +import * as Sentry from "@sentry/browser"; +import * as Turbo from "@hotwired/turbo"; + +// --- INITIALISATION SENTRY (En premier !) --- +Sentry.init({ + dsn: "https://803814be6540031b1c37bf92ba9c0f79@sentry.esy-web.dev/24", + tunnel: "/sentry-tunnel", + sendDefaultPii: true, + integrations: [ + Sentry.browserTracingIntegration(), + Sentry.replayIntegration() + ], + tracesSampleRate: 1.0, + tracePropagationTargets: ["localhost", "esy-web.dev"], // Remplace par ton domaine réel + replaysSessionSampleRate: 0.1, + replaysOnErrorSampleRate: 1.0 +}); /** * Initialise les composants de l'interface d'administration. - * Cette fonction est appelée à chaque chargement de page par Turbo. */ function initAdminLayout() { const sidebar = document.getElementById('sidebar'); const overlay = document.getElementById('sidebar-overlay'); const toggleBtn = document.getElementById('sidebar-toggle'); - const settingsToggle = document.getElementById('settings-toggle'); const settingsSubmenu = document.getElementById('settings-submenu'); @@ -30,17 +44,11 @@ function initAdminLayout() { if (settingsToggle && settingsSubmenu) { const settingsChevron = settingsToggle.querySelector('svg:last-child'); - /** - * Alterne l'état du dropdown avec une animation de glissement. - * @param {boolean} show - Forcer l'ouverture ou la fermeture - * @param {boolean} animate - Activer ou non la transition CSS - */ const toggleDropdown = (show, animate = true) => { if (!animate) settingsSubmenu.style.transition = 'none'; if (show) { settingsSubmenu.classList.remove('hidden'); - // scrollHeight permet de calculer la hauteur réelle du contenu settingsSubmenu.style.maxHeight = settingsSubmenu.scrollHeight + "px"; settingsChevron?.classList.add('rotate-180'); localStorage.setItem('admin_settings_open', 'true'); @@ -49,7 +57,6 @@ function initAdminLayout() { settingsChevron?.classList.remove('rotate-180'); localStorage.setItem('admin_settings_open', 'false'); - // On cache l'élément après l'animation pour l'accessibilité if (animate) { setTimeout(() => { if (settingsSubmenu.style.maxHeight === "0px") { @@ -62,30 +69,27 @@ function initAdminLayout() { } if (!animate) { - // Forcer un recalcul pour réactiver la transition proprement - settingsSubmenu.offsetHeight; + settingsSubmenu.offsetHeight; // force redraw settingsSubmenu.style.transition = ''; } }; - // Événement de clic settingsToggle.onclick = (e) => { e.preventDefault(); const isClosed = settingsSubmenu.style.maxHeight === "0px" || settingsSubmenu.classList.contains('hidden'); toggleDropdown(isClosed); }; - // --- PERSISTANCE --- - // On vérifie si on est sur une page appartenant au menu ou si l'utilisateur l'avait laissé ouvert + // PERSISTANCE const isSettingsRoute = window.location.pathname.includes('/crm/administrateur') || window.location.pathname.includes('/crm/logs'); const wasOpen = localStorage.getItem('admin_settings_open') === 'true'; if (isSettingsRoute || wasOpen) { - toggleDropdown(true, false); // Ouverture immédiate sans animation + toggleDropdown(true, false); } - // --- HIGHLIGHT DU LIEN ACTIF --- + // HIGHLIGHT settingsSubmenu.querySelectorAll('a').forEach(link => { if (window.location.pathname === link.getAttribute('href')) { link.classList.add('text-blue-600', 'dark:text-blue-400', 'font-semibold'); @@ -94,7 +98,7 @@ function initAdminLayout() { }); } - // --- 3. GESTION DES MESSAGES FLASH (Auto-suppression) --- + // --- 3. GESTION DES MESSAGES FLASH --- document.querySelectorAll('.flash-message').forEach((flash) => { setTimeout(() => { flash.classList.add('opacity-0', 'translate-x-10'); @@ -104,7 +108,6 @@ function initAdminLayout() { } // --- CORRECTIF DATA-TURBO-CONFIRM --- -// Turbo 7+ intercepte les clics, on réimplémente une confirmation native simple document.addEventListener("turbo:click", (event) => { const message = event.target.closest("[data-turbo-confirm]")?.getAttribute("data-turbo-confirm"); if (message && !confirm(message)) { @@ -112,10 +115,8 @@ document.addEventListener("turbo:click", (event) => { } }); -// Exécution au chargement initial et à chaque navigation Turbo document.addEventListener('turbo:load', initAdminLayout); -// Nettoyage avant la mise en cache de Turbo (évite les bugs visuels au retour arrière) document.addEventListener('turbo:before-cache', () => { const sidebar = document.getElementById('sidebar'); const overlay = document.getElementById('sidebar-overlay'); diff --git a/assets/app.js b/assets/app.js index a5910fb..2e86eec 100644 --- a/assets/app.js +++ b/assets/app.js @@ -1,6 +1,22 @@ import './app.scss' import * as Turbo from "@hotwired/turbo" +import * as Turbo from "@hotwired/turbo"; + +// --- INITIALISATION SENTRY (En premier !) --- +Sentry.init({ + dsn: "https://803814be6540031b1c37bf92ba9c0f79@sentry.esy-web.dev/24", + tunnel: "/sentry-tunnel", + sendDefaultPii: true, + integrations: [ + Sentry.browserTracingIntegration(), + Sentry.replayIntegration() + ], + tracesSampleRate: 1.0, + tracePropagationTargets: ["localhost", "esy-web.dev"], // Remplace par ton domaine réel + replaysSessionSampleRate: 0.1, + replaysOnErrorSampleRate: 1.0 +}); // --- INITIALISATION DES COMPOSANTS APRÈS TURBO/CHARGEMENT --- document.addEventListener('DOMContentLoaded', ()=>{ diff --git a/package.json b/package.json index 0253a89..991f392 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "@grafikart/drop-files-element": "^1.0.9", "@hotwired/turbo": "^8.0.20", "@preact/preset-vite": "^2.10.2", + "@sentry/browser": "^10.34.0", "@tailwindcss/vite": "^4.1.17", "autoprefixer": "^10.4.22", "body-scroll-lock": "^4.0.0-beta.0", diff --git a/src/Controller/HomeController.php b/src/Controller/HomeController.php index c535512..8d73118 100644 --- a/src/Controller/HomeController.php +++ b/src/Controller/HomeController.php @@ -146,80 +146,41 @@ class HomeController extends AbstractController 'email' => $email ]); } - const SENTRY_HOST = ''; - const SENTRY_PROJECT_IDS = ['']; + // Remplace par ton DSN exact pour la vérification + private const ALLOWED_DSN = 'https://803814be6540031b1c37bf92ba9c0f79@sentry.esy-web.dev/24'; + private const SENTRY_HOST = 'sentry.esy-web.dev'; + private const PROJECT_ID = '24'; - #[Route('/uptime',name: 'app_uptime',options: ['sitemap' => false], methods: ['GET'])] - public function app_uptime(Request $request,HttpClientInterface $httpClient): Response + #[Route('/sentry-tunnel', name: 'sentry_tunnel', methods: ['POST'])] + public function tunnel(Request $request, HttpClientInterface $httpClient): Response { - return $this->json([]); - } - - #[Route('/tunnel',name: 'app_tunnel',options: ['sitemap' => false], methods: ['POST'])] - public function tunnel(Request $request,HttpClientInterface $httpClient): Response - { - $envelope = $request->getContent(); - if (empty($envelope)) { - return $this->json([]); - } - try { - // 2. Extract the header piece (first line) - $pieces = explode("\n", $envelope, 2); - $piece = $pieces[0]; + $envelope = $request->getContent(); + $pieces = explode("\n", $envelope); - // 3. Parse the header (which is JSON) - $header = json_decode($piece, true); + // La première ligne de l'enveloppe Sentry contient le header en JSON + $header = json_decode($pieces[0], true); - if (!isset($header['dsn'])) { - throw new \Exception("Missing DSN in envelope header."); + // --- SÉCURITÉ : On vérifie que le DSN correspond bien au tien --- + if (isset($header['dsn']) && $header['dsn'] !== self::ALLOWED_DSN) { + return new Response('Invalid DSN', 401); } - // 4. Extract and validate DSN and Project ID - $dsnUrl = parse_url($header['dsn']); - $dsnHostname = $dsnUrl['host'] ?? null; - $dsnPath = $dsnUrl['path'] ?? '/'; + // Construction de l'URL finale vers ton instance Sentry + $url = "https://" . self::SENTRY_HOST . "/api/" . self::PROJECT_ID . "/envelope/"; - // Remove leading/trailing slashes from the path to get the project_id - $projectId = trim($dsnPath, '/'); - - - if ($dsnHostname !== self::SENTRY_HOST) { - throw new \Exception("Invalid sentry hostname: {$dsnHostname}"); - } - - if (empty($projectId) || !in_array($projectId, self::SENTRY_PROJECT_IDS)) { - throw new \Exception("Invalid sentry project id: {$projectId}"); - } - - // 5. Construct the upstream Sentry URL - $upstreamSentryUrl = "https://" . self::SENTRY_HOST . "/api/" . $projectId . "/envelope/"; - - // 6. Forward the request using an HTTP client (e.g., Guzzle) - - $response = $httpClient->request("POST",$upstreamSentryUrl, [ + // On renvoie l'enveloppe à Sentry + $httpClient->request('POST', $url, [ 'body' => $envelope, 'headers' => [ - // Sentry expects this content type - 'Content-Type' => 'application/x-sentry-envelope', - // Forward the content encoding if present, though often not needed - // 'Content-Encoding' => $request->headers->get('Content-Encoding'), + 'Content-Type' => 'text/plain;charset=UTF-8', ], ]); - // 7. Return the status from the upstream Sentry response - return new JsonResponse([], $response->getStatusCode()); - + return new Response('', 204); } catch (\Exception $e) { - // Log the error for server-side debugging - error_log("Error tunneling to Sentry: " . $e->getMessage()); - - // Return a success status (200/202) or a non-specific 500 to the client. - // Returning a non-error status (like 200) is often preferred for tunnels - // to avoid triggering ad-blockers on failures. - return new JsonResponse([ - 'error' => 'An error occurred during tunneling.' - ], JsonResponse::HTTP_INTERNAL_SERVER_ERROR); + // En cas d'erreur du tunnel, on reste discret pour ne pas polluer la console client + return new Response('Tunnel Error', 500); } } }