feat(sentry): Ajoute Sentry pour le suivi des erreurs

Ajoute Sentry pour le suivi des erreurs en production et améliore la
gestion des erreurs côté client et serveur.
```
This commit is contained in:
Serreau Jovann
2025-11-19 17:41:07 +01:00
parent 87e4dc6f8e
commit 7389c5f02b
5 changed files with 96 additions and 20 deletions

View File

@@ -75,6 +75,7 @@
STRIPE_SK=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
STRIPE_WEBHOOKS_SIGN=whsec_wNHtgjypqbfP7erAqifCOzZvW8kW9oB7
MAILER_DSN=ses+smtp://AKIAWTT2T22CWBRBBDYN:BBdgb6KxRQ8mNcpWFJsZCJxbSGNdgLhKFiITMErfBlQP@default?region=eu-west-3
SENTRY_DSN="https://4f43769e7c483f14da26e05824a482d0@o4509563601092608.ingest.de.sentry.io/4510392636473424"
dest: "{{ path }}/.env.local"
when: ansible_os_family == "Debian"

View File

@@ -2,6 +2,7 @@ import './app.scss'
import * as Turbo from "@hotwired/turbo"
import {PaymentForm} from './PaymentForm'
import * as Sentry from "@sentry/browser";
// --- CLÉS DE STOCKAGE ET VAPID ---
const VAPID_PUBLIC_KEY = "BKz0kdcsG6kk9KxciPpkfP8kEDAd408inZecij5kBDbQ1ZGZSNwS4KZ8FerC28LFXvgSqpDXtor3ePo0zBCdNqo";
@@ -427,6 +428,24 @@ document.addEventListener('DOMContentLoaded', ()=>{
// Gère le bandeau de cookies (bottom-right)
handleCookieBanner()
Sentry.init({
dsn: "https://129785624e32fc0d4d7d8002a03b77b7@o4509563601092608.ingest.de.sentry.io/4510392640340048",
// Setting this option to true will send default PII data to Sentry.
// For example, automatic IP address collection on events
sendDefaultPii: true,
integrations: [
Sentry.browserTracingIntegration(),
Sentry.replayIntegration()
],
tunnel: "/tunnel",
// Tracing
tracesSampleRate: 1.0, // Capture 100% of the transactions
// Set 'tracePropagationTargets' to control for which URLs distributed tracing should be enabled
tracePropagationTargets: ["localhost", /^https:\/\/yourserver\.io\/api/],
// Session Replay
replaysSessionSampleRate: 0.1, // This sets the sample rate at 10%. You may want to change it to 100% while in development and then sample at a lower rate in production.
replaysOnErrorSampleRate: 1.0 // If you're not already sampling the entire session, change the sample rate to 100% when sampling sessions where errors occur.
});
const env = document.querySelector('meta[name="env"]')
if(env.getAttribute('content') == "prod") {
if (typeof navigator.serviceWorker !== 'undefined') {

View File

@@ -2,23 +2,7 @@ when@prod:
sentry:
dsn: '%env(SENTRY_DSN)%'
options:
# Add request headers, cookies, IP address and the authenticated user
# see https://docs.sentry.io/platforms/php/data-management/data-collected/ for more info
# send_default_pii: true
ignore_exceptions:
- 'Symfony\Component\ErrorHandler\Error\FatalError'
- 'Symfony\Component\Debug\Exception\FatalErrorException'
# If you are using Monolog, you also need this additional configuration to log the errors correctly:
# https://docs.sentry.io/platforms/php/guides/symfony/#monolog-integration
# register_error_listener: false
# register_error_handler: false
# monolog:
# handlers:
# sentry:
# type: sentry
# level: !php/const Monolog\Logger::ERROR
# hub_id: Sentry\State\HubInterface
# fill_extra_context: true # Enables sending monolog context to Sentry
# process_psr_3_messages: false # Disables the resolution of PSR-3 placeholders
# Specify a fixed sample rate
traces_sample_rate: 1.0
# Set a sampling rate for profiling - this is relative to traces_sample_rate
profiles_sample_rate: 1.0

View File

@@ -22,6 +22,7 @@
"@grafikart/drop-files-element": "^1.0.9",
"@hotwired/turbo": "^8.0.13",
"@preact/preset-vite": "^2.10.2",
"@sentry/browser": "^10.26.0",
"@tailwindcss/vite": "^4.1.13",
"autoprefixer": "^10.4.21",
"body-scroll-lock": "^4.0.0-beta.0",

View File

@@ -17,6 +17,7 @@ use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Twig\Environment;
class HomeController extends AbstractController
@@ -39,4 +40,74 @@ class HomeController extends AbstractController
'city' => $city,
]);
}
const SENTRY_HOST = 'o4509563601092608.ingest.de.sentry.io';
const SENTRY_PROJECT_IDS = ['4510392640340048'];
#[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];
// 3. Parse the header (which is JSON)
$header = json_decode($piece, true);
if (!isset($header['dsn'])) {
throw new \Exception("Missing DSN in envelope header.");
}
// 4. Extract and validate DSN and Project ID
$dsnUrl = parse_url($header['dsn']);
$dsnHostname = $dsnUrl['host'] ?? null;
$dsnPath = $dsnUrl['path'] ?? '/';
// 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, [
'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'),
],
]);
// 7. Return the status from the upstream Sentry response
return new JsonResponse([], $response->getStatusCode());
} 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);
}
}
}