diff --git a/.env b/.env index a979df5..017cafe 100644 --- a/.env +++ b/.env @@ -56,3 +56,10 @@ JWT_COOKIE_SAMESITE=lax JWT_TOKEN_TTL=86400 JWT_COOKIE_TTL=86400 ###< lexik/jwt-authentication-bundle ### + +###> sentry/sentry-symfony ### +# Error tracking backend → GlitchTip (projet "sirh-api"). Prod only, vide => inerte. +# À définir dans l'env_file du serveur, PAS ici. Format : +# SENTRY_DSN=http://@:/ +# SENTRY_DSN= +###< sentry/sentry-symfony ### diff --git a/CLAUDE.md b/CLAUDE.md index 64e6fde..17dcf77 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -221,6 +221,13 @@ - Système : entité `Notification` (table `notifications`, `recipient`/`actor`/`message`/`category`/`target`/`isRead`), cloche **admin-only** dans `AppTopNav.vue`, providers `/notifications/{unread,today,history}` + `POST /notifications/mark-all-read`. Création historique : `WorkHourSiteValidationProcessor` (1 notif/admin via `UserRepository::findAllAdmins`). - **Fin de contrat (J-1 ouvré)** : commande cron quotidienne `app:contract:end-notifications` (crontab prod, ~6h ; option `--date`). Notifie les admins sur le **dernier jour ouvré avant** `endDate` (inclusif) de la **dernière** période d'un employé (changement de contrat enchaîné exclu). Week-ends + fériés sautés (`WorkingDayCalculator`, via `getHolidaysDayByYears` → applique `EXCLUDED_PUBLIC_HOLIDAYS`, donc **Lundi de Pentecôte traité comme jour ouvré**, cohérent avec le reste de l'app). Fenêtre couverte un jour J = `]J ; prochain_jour_ouvré(J)]`. Message « Fin de {nature} de {Nom} le {date} », catégorie `Contrat`, target `/employees/{id}`, acteur null. Idempotent (`NotificationRepository::existsForRecipientCategoryTargetMessage`). Logique pure testée : `ContractEndNotificationPlanner` + `WorkingDayCalculator`. Front : `AppTopNav.vue` masque le span acteur si `actorName` vide. Doc : `doc/contract-end-notifications.md`. +## Error tracking (GlitchTip) +- Backend Symfony → GlitchTip (SDK Sentry), org `malio`, projet `sirh-api`. **Prod only**, **inerte sans `SENTRY_DSN`**. +- Config : `config/packages/sentry.yaml` (DSN runtime `%env(SENTRY_DSN)%`, release `%app.version%`, 4xx ignorés, `traces_sample_rate: 0`, handler Monolog ERROR+) ; `SentryBundle` enregistré `['prod' => true]` (`config/bundles.php`) ; handler `sentry` en `when@prod` (`config/packages/monolog.yaml`). DSN runtime via l'env_file serveur, jamais committé/baké. +- **Transport réseau** : GlitchTip est interne (bloqué Sophos), servi HTTPS sur `logs.malio-dev.fr` (cert auto-signé) ; SIRH sur VPS OVH → tunnel **Tailscale** entre les deux hôtes (GlitchTip `100.111.223.34`, OVH `100.93.52.45`). Topologie retenue (Option A) : DSN **inchangé** (`https://…@logs.malio-dev.fr/3`), hostname résolu vers l'IP tailnet via `extra_hosts` dans le `docker-compose` serveur, et **CA racine MALIO bakée** dans l'image (`deploy/docker/Dockerfile.prod` + `deploy/docker/malio-dev-root-ca.crt`). Frontend hors périmètre (ajout futur via proxy nginx `/ingest`). +- Test d'envoi : `php bin/console sentry:test` (prod-only, `SENTRY_DSN` requis) → Issue dans `sirh-api`. +- Doc : `doc/error-tracking.md`. + ## Backend Conventions - Prefer explicit DTOs over associative arrays - Business rules in backend (providers/processors/services), frontend is display/interaction only diff --git a/composer.json b/composer.json index f3e0a77..b9069ef 100644 --- a/composer.json +++ b/composer.json @@ -17,6 +17,7 @@ "nelmio/cors-bundle": "^2.6", "phpdocumentor/reflection-docblock": "^5.6", "phpstan/phpdoc-parser": "^2.3", + "sentry/sentry-symfony": "^5.10", "symfony/asset": "8.0.*", "symfony/console": "8.0.*", "symfony/dotenv": "8.0.*", diff --git a/composer.lock b/composer.lock index 0238fc4..f3399b1 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "bdc04f5145303388bac52809ea3f4b05", + "content-hash": "5fa560dba1bae2997c8f71afbbbfb4ab", "packages": [ { "name": "api-platform/doctrine-common", @@ -2515,6 +2515,185 @@ }, "time": "2026-01-02T16:01:13+00:00" }, + { + "name": "guzzlehttp/psr7", + "version": "2.12.3", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "7ec62dc3f44aa218487dbed81a9bf9bc647be55d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/7ec62dc3f44aa218487dbed81a9bf9bc647be55d", + "reference": "7ec62dc3f44aa218487dbed81a9bf9bc647be55d", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.1 || ^2.0", + "ralouphie/getallheaders": "^3.0", + "symfony/deprecation-contracts": "^2.5 || ^3.0", + "symfony/polyfill-php80": "^1.25" + }, + "provide": { + "psr/http-factory-implementation": "1.0", + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "http-interop/http-factory-tests": "1.1.0", + "jshttp/mime-db": "1.54.0.1", + "phpunit/phpunit": "^8.5.52 || ^9.6.34" + }, + "suggest": { + "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Psr7\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://sagikazarmark.hu" + } + ], + "description": "PSR-7 message implementation that also provides common utility methods", + "keywords": [ + "http", + "message", + "psr-7", + "request", + "response", + "stream", + "uri", + "url" + ], + "support": { + "issues": "https://github.com/guzzle/psr7/issues", + "source": "https://github.com/guzzle/psr7/tree/2.12.3" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7", + "type": "tidelift" + } + ], + "time": "2026-06-23T15:21:08+00:00" + }, + { + "name": "jean85/pretty-package-versions", + "version": "2.1.1", + "source": { + "type": "git", + "url": "https://github.com/Jean85/pretty-package-versions.git", + "reference": "4d7aa5dab42e2a76d99559706022885de0e18e1a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Jean85/pretty-package-versions/zipball/4d7aa5dab42e2a76d99559706022885de0e18e1a", + "reference": "4d7aa5dab42e2a76d99559706022885de0e18e1a", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2.1.0", + "php": "^7.4|^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.2", + "jean85/composer-provided-replaced-stub-package": "^1.0", + "phpstan/phpstan": "^2.0", + "phpunit/phpunit": "^7.5|^8.5|^9.6", + "rector/rector": "^2.0", + "vimeo/psalm": "^4.3 || ^5.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Jean85\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Alessandro Lai", + "email": "alessandro.lai85@gmail.com" + } + ], + "description": "A library to get pretty versions strings of installed dependencies", + "keywords": [ + "composer", + "package", + "release", + "versions" + ], + "support": { + "issues": "https://github.com/Jean85/pretty-package-versions/issues", + "source": "https://github.com/Jean85/pretty-package-versions/tree/2.1.1" + }, + "time": "2025-03-19T14:43:43+00:00" + }, { "name": "lcobucci/jwt", "version": "5.6.0", @@ -3361,6 +3540,114 @@ }, "time": "2019-01-08T18:20:26+00:00" }, + { + "name": "psr/http-factory", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-factory.git", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "shasum": "" + }, + "require": { + "php": ">=7.1", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories", + "keywords": [ + "factory", + "http", + "message", + "psr", + "psr-17", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-factory" + }, + "time": "2024-04-15T12:06:14+00:00" + }, + { + "name": "psr/http-message", + "version": "2.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-message/tree/2.0" + }, + "time": "2023-04-04T09:54:51+00:00" + }, { "name": "psr/link", "version": "2.0.1", @@ -3467,6 +3754,50 @@ }, "time": "2024-09-11T13:17:53+00:00" }, + { + "name": "ralouphie/getallheaders", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/ralouphie/getallheaders.git", + "reference": "120b605dfeb996808c31b6477290a714d356e822" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", + "reference": "120b605dfeb996808c31b6477290a714d356e822", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^5 || ^6.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/getallheaders.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" + } + ], + "description": "A polyfill for getallheaders.", + "support": { + "issues": "https://github.com/ralouphie/getallheaders/issues", + "source": "https://github.com/ralouphie/getallheaders/tree/develop" + }, + "time": "2019-03-08T08:55:37+00:00" + }, { "name": "sabberworm/php-css-parser", "version": "v9.1.0", @@ -3541,6 +3872,201 @@ }, "time": "2025-09-14T07:37:21+00:00" }, + { + "name": "sentry/sentry", + "version": "4.28.0", + "source": { + "type": "git", + "url": "https://github.com/getsentry/sentry-php.git", + "reference": "662cb7a01a342a7f33780fc955ff4a028d8b785a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/getsentry/sentry-php/zipball/662cb7a01a342a7f33780fc955ff4a028d8b785a", + "reference": "662cb7a01a342a7f33780fc955ff4a028d8b785a", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "ext-json": "*", + "ext-mbstring": "*", + "guzzlehttp/psr7": "^1.8.4|^2.1.1", + "jean85/pretty-package-versions": "^1.5|^2.0.4", + "php": "^7.2|^8.0", + "psr/log": "^1.0|^2.0|^3.0", + "symfony/options-resolver": "^4.4.30|^5.0.11|^6.0|^7.0|^8.0" + }, + "conflict": { + "raven/raven": "*" + }, + "require-dev": { + "carthage-software/mago": "1.30.0", + "friendsofphp/php-cs-fixer": "^3.4", + "guzzlehttp/promises": "^2.0.3", + "monolog/monolog": "^1.6|^2.0|^3.0", + "nyholm/psr7": "^1.8", + "open-telemetry/api": "^1.0", + "open-telemetry/exporter-otlp": "^1.0", + "open-telemetry/sdk": "^1.0", + "phpstan/phpstan": "^1.3", + "phpunit/phpunit": "^8.5.52|^9.6.34", + "spiral/roadrunner-http": "^3.6", + "spiral/roadrunner-worker": "^3.6" + }, + "suggest": { + "ext-excimer": "Enable Sentry profiling with the Excimer PHP extension.", + "monolog/monolog": "Allow sending log messages to Sentry by using the included Monolog handler." + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Sentry\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Sentry", + "email": "accounts@sentry.io" + } + ], + "description": "PHP SDK for Sentry (http://sentry.io)", + "homepage": "http://sentry.io", + "keywords": [ + "crash-reporting", + "crash-reports", + "error-handler", + "error-monitoring", + "log", + "logging", + "profiling", + "sentry", + "tracing" + ], + "support": { + "issues": "https://github.com/getsentry/sentry-php/issues", + "source": "https://github.com/getsentry/sentry-php/tree/4.28.0" + }, + "funding": [ + { + "url": "https://sentry.io/", + "type": "custom" + }, + { + "url": "https://sentry.io/pricing/", + "type": "custom" + } + ], + "time": "2026-06-11T12:22:38+00:00" + }, + { + "name": "sentry/sentry-symfony", + "version": "5.10.0", + "source": { + "type": "git", + "url": "https://github.com/getsentry/sentry-symfony.git", + "reference": "6f49255f4cdcfc43a3a283bd3a1f65d483e9192f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/getsentry/sentry-symfony/zipball/6f49255f4cdcfc43a3a283bd3a1f65d483e9192f", + "reference": "6f49255f4cdcfc43a3a283bd3a1f65d483e9192f", + "shasum": "" + }, + "require": { + "guzzlehttp/psr7": "^2.1.1", + "jean85/pretty-package-versions": "^1.5||^2.0", + "php": "^7.2||^8.0", + "sentry/sentry": "^4.23.0", + "symfony/cache-contracts": "^1.1||^2.4||^3.0", + "symfony/config": "^4.4.20||^5.0.11||^6.0||^7.0||^8.0", + "symfony/console": "^4.4.20||^5.0.11||^6.0||^7.0||^8.0", + "symfony/dependency-injection": "^4.4.20||^5.0.11||^6.0||^7.0||^8.0", + "symfony/event-dispatcher": "^4.4.20||^5.0.11||^6.0||^7.0||^8.0", + "symfony/http-kernel": "^4.4.20||^5.0.11||^6.0||^7.0||^8.0", + "symfony/polyfill-php80": "^1.22", + "symfony/psr-http-message-bridge": "^1.2||^2.0||^6.4||^7.0||^8.0", + "symfony/yaml": "^4.4.20||^5.0.11||^6.0||^7.0||^8.0" + }, + "require-dev": { + "doctrine/dbal": "^2.13||^3.3||^4.0", + "doctrine/doctrine-bundle": "^2.6||^3.0", + "friendsofphp/php-cs-fixer": "^2.19||^3.40", + "masterminds/html5": "^2.8", + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "1.12.5", + "phpstan/phpstan-phpunit": "1.4.0", + "phpstan/phpstan-symfony": "1.4.10", + "phpunit/phpunit": "^8.5.40||^9.6.21", + "symfony/browser-kit": "^4.4.20||^5.0.11||^6.0||^7.0||^8.0", + "symfony/cache": "^4.4.20||^5.0.11||^6.0||^7.0||^8.0", + "symfony/dom-crawler": "^4.4.20||^5.0.11||^6.0||^7.0||^8.0", + "symfony/framework-bundle": "^4.4.20||^5.0.11||^6.0||^7.0||^8.0", + "symfony/http-client": "^4.4.20||^5.0.11||^6.0||^7.0||^8.0", + "symfony/messenger": "^4.4.20||^5.0.11||^6.0||^7.0||^8.0", + "symfony/monolog-bundle": "^3.4||^4.0", + "symfony/phpunit-bridge": "^5.2.6||^6.0||^7.0||^8.0", + "symfony/process": "^4.4.20||^5.0.11||^6.0||^7.0||^8.0", + "symfony/security-core": "^4.4.20||^5.0.11||^6.0||^7.0||^8.0", + "symfony/security-http": "^4.4.20||^5.0.11||^6.0||^7.0||^8.0", + "symfony/twig-bundle": "^4.4.20||^5.0.11||^6.0||^7.0||^8.0", + "vimeo/psalm": "^4.3||^5.16.0" + }, + "suggest": { + "doctrine/doctrine-bundle": "Allow distributed tracing of database queries using Sentry.", + "monolog/monolog": "Allow sending log messages to Sentry by using the included Monolog handler.", + "symfony/cache": "Allow distributed tracing of cache pools using Sentry.", + "symfony/twig-bundle": "Allow distributed tracing of Twig template rendering using Sentry." + }, + "type": "symfony-bundle", + "autoload": { + "files": [ + "src/aliases.php" + ], + "psr-4": { + "Sentry\\SentryBundle\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Sentry", + "email": "accounts@sentry.io" + } + ], + "description": "Symfony integration for Sentry (http://getsentry.com)", + "homepage": "http://getsentry.com", + "keywords": [ + "errors", + "logging", + "sentry", + "symfony" + ], + "support": { + "issues": "https://github.com/getsentry/sentry-symfony/issues", + "source": "https://github.com/getsentry/sentry-symfony/tree/5.10.0" + }, + "funding": [ + { + "url": "https://sentry.io/", + "type": "custom" + }, + { + "url": "https://sentry.io/pricing/", + "type": "custom" + } + ], + "time": "2026-04-01T14:50:32+00:00" + }, { "name": "symfony/asset", "version": "v8.0.4", @@ -5616,6 +6142,77 @@ ], "time": "2025-12-08T08:00:13+00:00" }, + { + "name": "symfony/options-resolver", + "version": "v8.0.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/options-resolver.git", + "reference": "d2b592535ffa6600c265a3893a7f7fd2bad82dd7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/d2b592535ffa6600c265a3893a7f7fd2bad82dd7", + "reference": "d2b592535ffa6600c265a3893a7f7fd2bad82dd7", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\OptionsResolver\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an improved replacement for the array_replace PHP function", + "homepage": "https://symfony.com", + "keywords": [ + "config", + "configuration", + "options" + ], + "support": { + "source": "https://github.com/symfony/options-resolver/tree/v8.0.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-11-12T15:55:31+00:00" + }, { "name": "symfony/password-hasher", "version": "v8.0.4", @@ -6358,6 +6955,93 @@ ], "time": "2026-01-27T16:18:07+00:00" }, + { + "name": "symfony/psr-http-message-bridge", + "version": "v8.0.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/psr-http-message-bridge.git", + "reference": "94facc221260c1d5f20e31ee43cd6c6a824b4a19" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/psr-http-message-bridge/zipball/94facc221260c1d5f20e31ee43cd6c6a824b4a19", + "reference": "94facc221260c1d5f20e31ee43cd6c6a824b4a19", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "psr/http-message": "^1.0|^2.0", + "symfony/http-foundation": "^7.4|^8.0" + }, + "conflict": { + "php-http/discovery": "<1.15" + }, + "require-dev": { + "nyholm/psr7": "^1.1", + "php-http/discovery": "^1.15", + "psr/log": "^1.1.4|^2|^3", + "symfony/browser-kit": "^7.4|^8.0", + "symfony/config": "^7.4|^8.0", + "symfony/event-dispatcher": "^7.4|^8.0", + "symfony/framework-bundle": "^7.4|^8.0", + "symfony/http-kernel": "^7.4|^8.0", + "symfony/runtime": "^7.4|^8.0" + }, + "type": "symfony-bridge", + "autoload": { + "psr-4": { + "Symfony\\Bridge\\PsrHttpMessage\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "PSR HTTP message bridge", + "homepage": "https://symfony.com", + "keywords": [ + "http", + "http-message", + "psr-17", + "psr-7" + ], + "support": { + "source": "https://github.com/symfony/psr-http-message-bridge/tree/v8.0.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-30T15:14:47+00:00" + }, { "name": "symfony/routing", "version": "v8.0.4", @@ -11220,77 +11904,6 @@ ], "time": "2024-10-20T05:08:20+00:00" }, - { - "name": "symfony/options-resolver", - "version": "v8.0.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/options-resolver.git", - "reference": "d2b592535ffa6600c265a3893a7f7fd2bad82dd7" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/options-resolver/zipball/d2b592535ffa6600c265a3893a7f7fd2bad82dd7", - "reference": "d2b592535ffa6600c265a3893a7f7fd2bad82dd7", - "shasum": "" - }, - "require": { - "php": ">=8.4", - "symfony/deprecation-contracts": "^2.5|^3" - }, - "type": "library", - "autoload": { - "psr-4": { - "Symfony\\Component\\OptionsResolver\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Provides an improved replacement for the array_replace PHP function", - "homepage": "https://symfony.com", - "keywords": [ - "config", - "configuration", - "options" - ], - "support": { - "source": "https://github.com/symfony/options-resolver/tree/v8.0.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://github.com/nicolas-grekas", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2025-11-12T15:55:31+00:00" - }, { "name": "symfony/process", "version": "v8.0.5", diff --git a/config/bundles.php b/config/bundles.php index 1c5d047..e5acea1 100644 --- a/config/bundles.php +++ b/config/bundles.php @@ -9,6 +9,7 @@ use Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle; use Lexik\Bundle\JWTAuthenticationBundle\LexikJWTAuthenticationBundle; use Nelmio\CorsBundle\NelmioCorsBundle; use Symfony\Bundle\FrameworkBundle\FrameworkBundle; +use Sentry\SentryBundle\SentryBundle; use Symfony\Bundle\MonologBundle\MonologBundle; use Symfony\Bundle\SecurityBundle\SecurityBundle; use Symfony\Bundle\TwigBundle\TwigBundle; @@ -24,4 +25,5 @@ return [ LexikJWTAuthenticationBundle::class => ['all' => true], MonologBundle::class => ['all' => true], DoctrineFixturesBundle::class => ['dev' => true, 'test' => true], + SentryBundle::class => ['prod' => true], ]; diff --git a/config/packages/monolog.yaml b/config/packages/monolog.yaml index cf2b085..0e9ed6c 100644 --- a/config/packages/monolog.yaml +++ b/config/packages/monolog.yaml @@ -36,3 +36,9 @@ when@prod: type: stream channels: [deprecation] path: "%kernel.logs_dir%/deprecations.log" + # Remonte les logs ERROR+ vers GlitchTip en tant qu'Issues (service défini + # dans sentry.yaml). Envoi immédiat, indépendamment des handlers fichier. + sentry: + type: service + id: Sentry\Monolog\Handler + channels: ["!event", "!doctrine", "!deprecation", "!cron"] diff --git a/config/packages/sentry.yaml b/config/packages/sentry.yaml new file mode 100644 index 0000000..56c611d --- /dev/null +++ b/config/packages/sentry.yaml @@ -0,0 +1,30 @@ +# Error tracking → GlitchTip (compatible SDK Sentry). +# Actif uniquement en prod (bundle enregistré prod-only dans bundles.php). +# Si SENTRY_DSN est vide/non défini, le SDK est inerte (rien n'est envoyé). +when@prod: + parameters: + env(SENTRY_DSN): '' + + sentry: + dsn: '%env(SENTRY_DSN)%' + # Capture des erreurs fatales PHP via le handler. On DÉSACTIVE le listener + # kernel pour éviter les doublons avec le handler Monolog (les exceptions du + # kernel sont déjà logguées par Symfony → remontées via Monolog). + register_error_listener: false + register_error_handler: true + options: + environment: '%env(APP_ENV)%' + release: '%app.version%' + traces_sample_rate: 0.0 + ignore_exceptions: + - Symfony\Component\HttpKernel\Exception\NotFoundHttpException + - Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException + - Symfony\Component\Security\Core\Exception\AccessDeniedException + + # Handler Monolog → Sentry : remonte les logs niveau ERROR+ comme Issues GlitchTip. + services: + Sentry\Monolog\Handler: + arguments: + $hub: '@Sentry\State\HubInterface' + $level: !php/const Monolog\Level::Error + $bubble: true diff --git a/deploy/docker/Dockerfile.prod b/deploy/docker/Dockerfile.prod index 6b0a936..743d921 100644 --- a/deploy/docker/Dockerfile.prod +++ b/deploy/docker/Dockerfile.prod @@ -39,10 +39,17 @@ FROM php:8.4-fpm AS production RUN apt-get update && apt-get install -y \ libicu-dev libpq-dev libpng-dev libzip-dev libxml2-dev \ - nginx supervisor \ + nginx supervisor ca-certificates \ && docker-php-ext-install -j$(nproc) intl pdo_pgsql zip gd opcache \ && rm -rf /var/lib/apt/lists/* +# CA racine interne MALIO (auto-signée) — permet au SDK Sentry/HttpClient de joindre +# GlitchTip en HTTPS sur logs.malio-dev.fr (cert *.malio-dev.fr). Le host est résolu vers +# l'IP tailnet via `extra_hosts` dans le docker-compose du serveur (cf. doc/error-tracking.md). +# Sans cette CA approuvée, le SDK logue « Message not sent » et rien ne remonte. +COPY deploy/docker/malio-dev-root-ca.crt /usr/local/share/ca-certificates/malio-dev-root-ca.crt +RUN update-ca-certificates + # PHP production config RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini" COPY docker/php/config/php.ini "$PHP_INI_DIR/conf.d/99-app.ini" diff --git a/deploy/docker/malio-dev-root-ca.crt b/deploy/docker/malio-dev-root-ca.crt new file mode 100644 index 0000000..087d73d --- /dev/null +++ b/deploy/docker/malio-dev-root-ca.crt @@ -0,0 +1,31 @@ +-----BEGIN CERTIFICATE----- +MIIFZzCCA0+gAwIBAgIUOiZigxwgIgtLipnLnu4eSgItc5MwDQYJKoZIhvcNAQEL +BQAwQzELMAkGA1UEBhMCRlIxEjAQBgNVBAoMCU1BTElPLURFVjEgMB4GA1UEAwwX +TUFMSU8tREVWIExvY2FsIFJvb3QgQ0EwHhcNMjYwNjI1MTYxMjIwWhcNMzYwNjIy +MTYxMjIwWjBDMQswCQYDVQQGEwJGUjESMBAGA1UECgwJTUFMSU8tREVWMSAwHgYD +VQQDDBdNQUxJTy1ERVYgTG9jYWwgUm9vdCBDQTCCAiIwDQYJKoZIhvcNAQEBBQAD +ggIPADCCAgoCggIBALqHXVWEae9aKtveLfSpxYy9RS0Aslw2Ls9+LWI33lpMRs02 +QssE9wquf3WGjz8NnHUWl5RM0QHC0DOCCddcbnRBciDRJeTaU43IGdNg+TSY+7aM +3t/jysZrpc/eu/udlIs7npCPaOGnRiuGN68Fkf9Q70FtmaASpusUe7J3jKDinznr +R2hARplO4OF01tFauu039A4yudLrZTUFTldicuZ6a5U3NhajgfNZA+pyJqvL3tLT +lXG3KupPD9BsbWe4zSM96CmyHM22QNlcL+M5XG5+EtDtM07tkDcyxFOsREjQHvSQ +NH+7h6G/QBHHKkYJhdyiuvpj6b5tEJBM2PVgy1T2JX5TuOBOLx6HvHLbNjUY/JI5 +0sIjnHbeybQCOfnKNAwidtnqjAfVg+XJ9UZCiGJOeRJOdN5isvvqEKydsX4ouCTj +89kwBbfCJeCS6BiadvNFUwnM0PksV0ovnOiUEEAPHRiP74jZ3IvH95BEwiZzyLpy +tXiJMW7cJMaqlT3jNwq3P00irfrpJNy4S1Mg2cBQh5ucv+PcMBfQT8YiarzlTQJo +saksh/2C43WH+qIFAL2aeD+rKReVBZcGa1XOBI8FUJTu3rLd37+iS4N2BUKq4fWo +FttuX5NOfeU3BRDLlCJ2AXau7o0czVy896R9iZTfBJC95QWD07PdHgoctuexAgMB +AAGjUzBRMB0GA1UdDgQWBBRNU0WsMg/pqo5XF/WXx78GrAzD5TAfBgNVHSMEGDAW +gBRNU0WsMg/pqo5XF/WXx78GrAzD5TAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3 +DQEBCwUAA4ICAQBFXsuT7Rm2oJBlWT/RsJtmWr95NoFLHovVDycgM8Vjm+E8hv/m +AcSjPjZDmXQLOrN31T/XUAs0nURHxSFgVzdIKpq2gOlGgHkZRMAW/iTON9Cqjn81 +Arjp5fjAJyFkoCiT3eTOElpteF4NhL8xMFaOg1Y2CEfOYO9OZR7Z38HdB6IArVwr +W3Dxq3DPtarCeo1k8SHJmJzUduYCltV8urB43gIiI2Hqd7aAlpkTfDhruKxxr7sJ +3/TpemJDCN9m8XMv2QvxqpMwH6EXg/7oqit5k0MvD445f3xt9vZydmV/x6F7u/A/ +gJitN+ixA4AKv7Lw210vaupiChqdY+78TXgLoPJ2/l2QPWG/R7Fb4yNZ2rEd6lyt +KLPxHDcdZetFnyqyaoB2SNtLx9hNUE5G3udU6DkNhDfQlDhqEG4f7GAInOu/cMWE +2uiIUEjcGSLM+XrrTFRc1tdXy6hnu+sw5ckvhwJ+kjah/pVGz21/y5a0p42AUznI +iN7HBV8YaSkeJLvBPnfakUAat1R98e0l72DucHe8RF44NmZCywpaUBsTpNy+bO2f +atqp4/ZEGJJlJ38rLv9bAuwr6d8x6T+m0oHknqtJHcWfO0kr4l3Lxsd8mRpGgmBe +zOjqjrat4vSc04Rqic4UV2IEoWCiSS/TSiBx8JAB6Ck0+YR9dUgXVQsFFg== +-----END CERTIFICATE----- diff --git a/doc/error-tracking.md b/doc/error-tracking.md new file mode 100644 index 0000000..79416d6 --- /dev/null +++ b/doc/error-tracking.md @@ -0,0 +1,114 @@ +# Error tracking (GlitchTip) + +Les erreurs **backend** (Symfony) sont remontées vers **GlitchTip** (instance interne MALIO, +compatible SDK Sentry), org `malio`, projet `sirh-api`. **Prod uniquement**, **inerte sans DSN**. + +> Frontend hors périmètre (les erreurs front partent du navigateur RH ; ajout futur possible via +> un proxy nginx `/ingest`). + +## Contrainte réseau & transport + +GlitchTip est sur le réseau interne (bloqué par Sophos), servi en **HTTPS sur +`logs.malio-dev.fr`** (cert auto-signé par la CA interne « MALIO-DEV Local Root CA »). SIRH tourne +sur un **VPS OVH** public. Le lien passe par un **tunnel Tailscale** entre les deux hôtes. + +**Topologie retenue (Option A — HTTPS + hostname mappé sur le tailnet) :** +- Tailscale est installé **sur l'hôte GlitchTip** (IP tailnet `100.111.223.34`) **et sur le VPS + OVH** (IP tailnet `100.93.52.45`). +- Le **DSN reste inchangé** : `https://@logs.malio-dev.fr/` (même endpoint que le + navigateur → pas de souci `ALLOWED_HOSTS`, Host header et cert cohérents). +- Côté SIRH, le nom `logs.malio-dev.fr` est résolu vers l'**IP tailnet de GlitchTip** via + `extra_hosts` dans le `docker-compose` du serveur. +- La **CA racine MALIO** est bakée dans l'image SIRH (`deploy/docker/Dockerfile.prod`) pour que le + SDK accepte le TLS auto-signé. + +> Pré-requis : le nginx qui sert `logs.malio-dev.fr` en 443 doit écouter sur une interface +> joignable via le tailnet (typiquement `0.0.0.0:443` → joignable sur `100.111.223.34:443`). + +## Fichiers concernés + +| Fichier | Rôle | +|---|---| +| `config/packages/sentry.yaml` | conf backend (prod-only, DSN runtime, 4xx ignorés, release = `app.version`, handler Monolog ERROR+) | +| `config/bundles.php` | `SentryBundle` enregistré `['prod' => true]` | +| `config/packages/monolog.yaml` | handler `sentry` (service) en `when@prod` | +| `.env` | bloc documenté `SENTRY_DSN` (vide → inerte) | +| `deploy/docker/Dockerfile.prod` | bake la CA racine MALIO (`update-ca-certificates`) pour le TLS interne | +| `deploy/docker/malio-dev-root-ca.crt` | certificat **public** de la CA interne (aucune clé privée) | + +## Activation (runbook) + +1. **Tailscale sur les deux hôtes** (GlitchTip **et** VPS OVH) : + ```bash + curl -fsSL https://tailscale.com/install.sh | sh + sudo tailscale up # ou --authkey tskey-auth-XXXX (headless) + tailscale ip -4 # GlitchTip → 100.111.223.34 ; OVH → 100.93.52.45 + ``` +2. **Vérifier l'accès** depuis le VPS OVH (tunnel + nginx 443 de GlitchTip) : + ```bash + tailscale ping 100.111.223.34 + curl -sSk -o /dev/null -w "%{http_code}\n" https://100.111.223.34/ # réponse HTTP = tunnel OK + ``` +3. **Mapper le hostname vers l'IP tailnet** dans le `docker-compose` du serveur OVH (service `php`), + pour que le container résolve `logs.malio-dev.fr` : + ```yaml + extra_hosts: + - "logs.malio-dev.fr:100.111.223.34" + ``` +4. **Projet GlitchTip** : déjà créé (org `malio`, projet `sirh-api`, id `3`). DSN de base affiché + dans *Settings → Client Keys* : `https://@logs.malio-dev.fr/3`. +5. **Injecter le DSN tel quel** (hostname conservé) dans l'env_file serveur (pas dans l'image), + puis rebuild/redéployer l'image (la CA est bakée au build) : + ```env + SENTRY_DSN=https://@logs.malio-dev.fr/3 + ``` + ```bash + docker compose up -d + docker compose exec php php bin/console cache:clear --env=prod + ``` + +## Tester l'envoi + +Le bundle `sentry/sentry-symfony` fournit une commande qui envoie un événement de test et +confirme s'il est bien parti vers GlitchTip. Elle n'existe qu'en **prod** (bundle prod-only) et +nécessite `SENTRY_DSN` défini. + +```bash +# Sur le serveur, dans le container PHP (SENTRY_DSN doit être dans l'env) : +docker compose exec php sh -lc "APP_ENV=prod php bin/console sentry:test" +``` + +Sortie attendue : `Sending test message... done.` → une **Issue de test** apparaît dans le projet +`sirh-api` côté GlitchTip. Si l'envoi échoue (`Message not sent`), le problème est réseau +(Tailscale/route/port) ou DSN, pas applicatif. + +Pré-check connectivité depuis le VPS OVH (`-k` ignore le cert juste pour ce test) : + +```bash +tailscale ping 100.111.223.34 +curl -sSk -o /dev/null -w "%{http_code}\n" https://100.111.223.34/ # réponse HTTP = tunnel OK +# Avec résolution du hostname (comme le container) + validation par la CA : +curl --resolve logs.malio-dev.fr:443:100.111.223.34 \ + --cacert deploy/docker/malio-dev-root-ca.crt \ + -sS -o /dev/null -w "%{http_code}\n" https://logs.malio-dev.fr/ +``` + +Alternative sans commande dédiée : déclencher un `throw new \RuntimeException('glitchtip test')` +temporaire dans un endpoint, ou un `$logger->error('glitchtip test')` (niveau ERROR+ → Issue). + +## CA HTTPS interne (bakée dans l'image) + +GlitchTip est en HTTPS avec un cert auto-signé par la **CA interne MALIO**. Le SDK refuse un TLS +non approuvé (« Message not sent »). La CA publique (`deploy/docker/malio-dev-root-ca.crt`, aucune +clé privée) est donc installée dans le trust store de l'image au build (`deploy/docker/Dockerfile.prod`, +stage production) : + +```dockerfile +COPY deploy/docker/malio-dev-root-ca.crt /usr/local/share/ca-certificates/malio-dev-root-ca.crt +RUN update-ca-certificates +``` + +Combinée à l'`extra_hosts` (hostname → IP tailnet), le container fait confiance à +`logs.malio-dev.fr` et l'atteint via le tunnel. + +Design détaillé : `docs/superpowers/specs/2026-06-28-glitchtip-backend-error-tracking-design.md`. diff --git a/docs/superpowers/plans/2026-06-28-glitchtip-backend-error-tracking.md b/docs/superpowers/plans/2026-06-28-glitchtip-backend-error-tracking.md new file mode 100644 index 0000000..fbe48ca --- /dev/null +++ b/docs/superpowers/plans/2026-06-28-glitchtip-backend-error-tracking.md @@ -0,0 +1,255 @@ +# GlitchTip Backend Error Tracking Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Remonter les erreurs backend Symfony de SIRH vers GlitchTip (SDK Sentry), en prod uniquement, inerte sans DSN, le transport réseau étant assuré par Tailscale (infra, hors repo). + +**Architecture:** On installe `sentry/sentry-symfony`, enregistré **prod-only** dans `bundles.php`. Un fichier `config/packages/sentry.yaml` (bloc `when@prod`) configure le SDK (DSN runtime `%env(SENTRY_DSN)%`, release = `%app.version%`, 4xx ignorés, pas de tracing) et déclare un handler Monolog qui remonte les logs `ERROR+` comme Issues. Sans `SENTRY_DSN`, le SDK est un no-op. Aucun changement frontend ni CI. + +**Tech Stack:** PHP 8.4, Symfony 8, API Platform, `sentry/sentry-symfony` ^5.10, Monolog, Docker (prod), Tailscale (transport infra). + +## Global Constraints + +- **Prod only** : `SentryBundle` enregistré `['prod' => true]`, config sous `when@prod`. Dev/test : zéro impact. +- **Inerte sans DSN** : `env(SENTRY_DSN)` défaut `''` → SDK no-op si vide. +- **DSN runtime** : lu depuis l'env_file serveur, jamais baké dans l'image, jamais committé. +- **Version dépendance** : `sentry/sentry-symfony:^5.10` (identique au projet Lesstime, même stack). +- **Release** : `%app.version%` (déjà fourni par `config/version.yaml`, ex. `0.1.127`). +- **Pas de tracing/APM** : `traces_sample_rate: 0.0`. +- **Format commit** (hook SIRH) : Conventional Commits FR, ex. `feat : …`, `docs : …`. Terminer par la ligne `Co-Authored-By: Claude Opus 4.8 (1M context) `. +- **Pas de modif frontend** (`frontend/**`), **pas de modif CI** (`.gitea/workflows/**`), **pas de modif Dockerfile** (DSN runtime ; CA HTTPS hors périmètre). +- **Branche** : `feat/glitchtip-backend-error-tracking` (déjà créée). + +--- + +### Task 1: Câblage backend Sentry/GlitchTip (inerte sans DSN) + +**Files:** +- Modify: `composer.json`, `composer.lock` (via `composer require`) +- Modify: `config/bundles.php` +- Create: `config/packages/sentry.yaml` +- Modify: `config/packages/monolog.yaml` +- Modify: `.env` + +**Interfaces:** +- Consumes: paramètre `%app.version%` (`config/version.yaml`), variable d'env `SENTRY_DSN`, `%env(APP_ENV)%`. +- Produces: service `Sentry\Monolog\Handler` (handler Monolog niveau `Error`), variable d'env attendue `SENTRY_DSN` (runtime). Aucune API PHP consommée par d'autres tâches. + +- [ ] **Step 1: Installer la dépendance** + +Depuis le container PHP (`make shell` ou `docker compose exec php sh`) : +```bash +composer require sentry/sentry-symfony:^5.10 +``` +Expected : `composer.json` + `composer.lock` modifiés, paquet `sentry/sentry-symfony` 5.x installé. + +- [ ] **Step 2: Forcer l'enregistrement prod-only du bundle** + +Le recipe Flex peut écrire `['all' => true]` dans `config/bundles.php`. Corriger en prod-only. Contenu attendu de la ligne ajoutée : +```php +use Sentry\SentryBundle\SentryBundle; +// ... dans le tableau de retour : + SentryBundle::class => ['prod' => true], +``` +Si Flex a aussi généré un `config/packages/sentry.yaml`, il sera remplacé à l'étape suivante. + +- [ ] **Step 3: Écrire `config/packages/sentry.yaml`** + +Remplacer intégralement le contenu du fichier par : +```yaml +# Error tracking → GlitchTip (compatible SDK Sentry). +# Actif uniquement en prod (bundle enregistré prod-only dans bundles.php). +# Si SENTRY_DSN est vide/non défini, le SDK est inerte (rien n'est envoyé). +when@prod: + parameters: + env(SENTRY_DSN): '' + + sentry: + dsn: '%env(SENTRY_DSN)%' + # Capture des erreurs fatales PHP via le handler. On DÉSACTIVE le listener + # kernel pour éviter les doublons avec le handler Monolog (les exceptions du + # kernel sont déjà logguées par Symfony → remontées via Monolog). + register_error_listener: false + register_error_handler: true + options: + environment: '%env(APP_ENV)%' + release: '%app.version%' + traces_sample_rate: 0.0 + ignore_exceptions: + - Symfony\Component\HttpKernel\Exception\NotFoundHttpException + - Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException + - Symfony\Component\Security\Core\Exception\AccessDeniedException + + # Handler Monolog → Sentry : remonte les logs niveau ERROR+ comme Issues GlitchTip. + services: + Sentry\Monolog\Handler: + arguments: + $hub: '@Sentry\State\HubInterface' + $level: !php/const Monolog\Level::Error + $bubble: true +``` + +- [ ] **Step 4: Brancher le handler Monolog en prod** + +Dans `config/packages/monolog.yaml`, sous `when@prod:` → `monolog:` → `handlers:`, ajouter le handler `sentry` (à côté des handlers `cron`, `main`, `deprecation` existants, sans les modifier) : +```yaml + # Remonte les logs ERROR+ vers GlitchTip en tant qu'Issues (service défini + # dans sentry.yaml). Envoi immédiat, indépendamment des handlers fichier. + sentry: + type: service + id: Sentry\Monolog\Handler + channels: ["!event", "!doctrine", "!deprecation", "!cron"] +``` + +- [ ] **Step 5: Documenter la variable dans `.env`** + +Ajouter à la fin de `.env` le bloc commenté (valeur réelle injectée côté serveur uniquement) : +```env +###> sentry/sentry-symfony ### +# Error tracking backend → GlitchTip (projet "sirh-api"). Prod only, vide => inerte. +# À définir dans l'env_file du serveur, PAS ici. Format : +# SENTRY_DSN=http://@:/ +# SENTRY_DSN= +###< sentry/sentry-symfony ### +``` +> Si Flex a déjà inséré un bloc `###> sentry/sentry-symfony ###`, le remplacer par celui-ci (ne pas dupliquer). + +- [ ] **Step 6: Vérifier que la conf prod compile (DSN vide → inerte)** + +Depuis le container : +```bash +APP_ENV=prod php bin/console cache:clear --no-warmup +APP_ENV=prod php bin/console debug:container --env=prod 2>/dev/null | grep -i sentry | head +``` +Expected : +- `cache:clear` : `[OK] Cleared the cache.` (aucune erreur de config/DI). +- `debug:container` : au moins une ligne `Sentry\...` (services chargés). DSN vide → SDK inerte, aucun envoi. + +- [ ] **Step 7: Vérifier que le dev n'est pas impacté** + +```bash +php bin/console cache:clear # APP_ENV=dev par défaut +make test +``` +Expected : cache dev OK ; suite de tests verte (le bundle n'est pas chargé hors prod). + +- [ ] **Step 8: Commit** + +```bash +git add composer.json composer.lock config/bundles.php config/packages/sentry.yaml config/packages/monolog.yaml .env +git commit -m "$(cat <<'EOF' +feat : error tracking backend vers GlitchTip (prod-only, inerte sans DSN) + +Ajout du SDK sentry/sentry-symfony enregistré prod-only, config sentry.yaml +(DSN runtime, release app.version, 4xx ignorés, pas de tracing) et handler +Monolog ERROR+. Sans SENTRY_DSN le SDK est no-op. Transport réseau via +Tailscale (infra, hors repo). + +Co-Authored-By: Claude Opus 4.8 (1M context) +EOF +)" +``` + +--- + +### Task 2: Documentation (doc/ + CLAUDE.md) + +**Files:** +- Create: `doc/error-tracking.md` +- Modify: `CLAUDE.md` + +**Interfaces:** +- Consumes: néant (documentation). +- Produces: néant. + +- [ ] **Step 1: Créer `doc/error-tracking.md`** + +```markdown +# Error tracking (GlitchTip) + +Les erreurs **backend** (Symfony) sont remontées vers **GlitchTip** (instance interne MALIO, +compatible SDK Sentry), org `malio`, projet `sirh-api`. **Prod uniquement**, **inerte sans DSN**. + +> Frontend hors périmètre (les erreurs front partent du navigateur RH ; ajout futur possible via +> un proxy nginx `/ingest`). + +## Contrainte réseau & transport + +GlitchTip est sur le réseau interne (bloqué par Sophos). SIRH tourne sur un VPS OVH public. Le +container PHP joint GlitchTip via un **tunnel Tailscale** monté sur le host de prod. + +## Fichiers concernés + +| Fichier | Rôle | +|---|---| +| `config/packages/sentry.yaml` | conf backend (prod-only, DSN runtime, 4xx ignorés, release = `app.version`, handler Monolog ERROR+) | +| `config/bundles.php` | `SentryBundle` enregistré `['prod' => true]` | +| `config/packages/monolog.yaml` | handler `sentry` (service) en `when@prod` | +| `.env` | bloc documenté `SENTRY_DSN` (vide → inerte) | + +## Activation (runbook) + +1. **Tailscale sur le host prod OVH** : + \`\`\`bash + curl -fsSL https://tailscale.com/install.sh | sh + sudo tailscale up # ou --authkey tskey-auth-XXXX (headless) + tailscale status && tailscale ip -4 + \`\`\` +2. **Vérifier l'accès à GlitchTip** depuis le host : + \`\`\`bash + tailscale ping + curl -sS -o /dev/null -w "%{http_code}\n" http://:/_health/ + \`\`\` +3. **Routage container → tailnet** : pointer `SENTRY_DSN` sur l'**IP tailnet** de GlitchTip + (le container ne résout pas MagicDNS). Repli si non routé : sidecar `tailscale/tailscale` + + `network_mode: service:tailscale`. +4. **Créer le projet GlitchTip** `sirh-api` (plateforme `php-symfony`) dans l'org `malio`, + récupérer le DSN (Settings → Client Keys). +5. **Injecter le DSN** dans l'env_file serveur (pas dans l'image), puis redéployer : + \`\`\`env + SENTRY_DSN=http://@100.x.y.z:/ + \`\`\` + \`\`\`bash + docker compose up -d + docker compose exec php php bin/console cache:clear --env=prod + \`\`\` + +## CA HTTPS (conditionnel) + +Uniquement si le DSN cible l'HTTPS interne `logs.malio-dev.fr` (cert auto-signé) : baker la CA +racine MALIO dans `deploy/docker/Dockerfile.prod` (stage production). Recommandé : préférer +l'endpoint **HTTP** via le tailnet (déjà chiffré par WireGuard) → pas de CA. + +Design détaillé : `docs/superpowers/specs/2026-06-28-glitchtip-backend-error-tracking-design.md`. +``` + +- [ ] **Step 2: Ajouter une section à `CLAUDE.md`** + +Insérer une section (après « ## Notifications ») : +```markdown +## Error tracking (GlitchTip) +- Backend Symfony → GlitchTip (SDK Sentry), org `malio`, projet `sirh-api`. **Prod only**, **inerte sans `SENTRY_DSN`**. +- Config : `config/packages/sentry.yaml` (DSN runtime `%env(SENTRY_DSN)%`, release `%app.version%`, 4xx ignorés, `traces_sample_rate: 0`, handler Monolog ERROR+) ; `SentryBundle` enregistré `['prod' => true]` (`config/bundles.php`) ; handler `sentry` en `when@prod` (`config/packages/monolog.yaml`). DSN runtime via l'env_file serveur, jamais committé/baké. +- **Transport réseau** : GlitchTip est interne (bloqué Sophos), SIRH sur VPS OVH → tunnel **Tailscale** sur le host prod. Frontend hors périmètre (ajout futur via proxy nginx `/ingest`). +- Doc : `doc/error-tracking.md`. +``` + +- [ ] **Step 3: Commit** + +```bash +git add doc/error-tracking.md CLAUDE.md +git commit -m "$(cat <<'EOF' +docs : documentation error tracking GlitchTip (doc/ + CLAUDE.md) + +Co-Authored-By: Claude Opus 4.8 (1M context) +EOF +)" +``` + +--- + +## Notes d'exécution + +- **TDD non applicable** ici : ce sont des changements de **configuration** (pas de logique métier unitairement testable). La vérification se fait par commandes console (`cache:clear` prod, `debug:container`) + non-régression de la suite existante (`make test`). Ne pas ajouter de test PHPUnit factice. +- Les valeurs `<…>` du runbook (IP tailnet, port GlitchTip, clé DSN) sont renseignées au **déploiement**, hors repo. +- Après les deux tasks : pousser la branche et ouvrir la MR (`git push -u origin feat/glitchtip-backend-error-tracking`). diff --git a/docs/superpowers/specs/2026-06-28-glitchtip-backend-error-tracking-design.md b/docs/superpowers/specs/2026-06-28-glitchtip-backend-error-tracking-design.md new file mode 100644 index 0000000..aafcf02 --- /dev/null +++ b/docs/superpowers/specs/2026-06-28-glitchtip-backend-error-tracking-design.md @@ -0,0 +1,223 @@ +# Error tracking backend SIRH → GlitchTip (via Tailscale) + +> Date : 2026-06-28 +> Périmètre : **backend Symfony uniquement**, **prod only**, transport **Tailscale**. +> Référence pattern : projet **Lesstime** (`config/packages/sentry.yaml`, `README.md` § Error tracking). + +## 1. Contexte & contrainte + +GlitchTip (instance auto-hébergée MALIO, compatible SDK Sentry) vit sur le **réseau interne**, +bloqué par **Sophos**, sur le domaine interne `logs.malio-dev.fr` (DNS local, CA auto-signée). +SIRH tourne sur un **VPS OVH** (Internet public) → le container PHP ne peut pas joindre l'interne. + +**Décision** : on monte un **tunnel Tailscale** sur le host de prod OVH. Le container PHP atteint +GlitchTip par le tailnet. **Backend seulement** pour l'instant (les erreurs front partent du +navigateur RH, hors périmètre — pourra être ajouté plus tard via un proxy nginx `/ingest`). + +Flux retenu : + +| Flux | Source | Chemin vers GlitchTip | +|---|---|---| +| **Backend** Symfony | container PHP sur le VPS OVH | → host Tailscale → tailnet → GlitchTip ✅ | +| Frontend SPA | navigateur RH | **hors périmètre** (pas de SDK front) | + +## 2. Principes + +- **Prod only** : le bundle n'est enregistré que pour `prod`. En dev/test : zéro impact. +- **Inerte sans DSN** : si `SENTRY_DSN` est vide/absent, le SDK ne fait rien (no-op). +- **Runtime DSN** : le DSN est lu à l'exécution depuis l'`env_file` du serveur, jamais baké dans + l'image (pas de secret dans le repo ni dans l'image Docker). +- **Pas d'APM/tracing** (`traces_sample_rate: 0`) : on ne remonte que les erreurs. +- **Bruit filtré** : 4xx HTTP (404/405/AccessDenied) ignorés ; channels `event/doctrine/ + deprecation/cron` exclus du handler Monolog. + +## 3. Changements de code (repo SIRH) + +### 3.1 Dépendance +```bash +make shell # ou docker exec dans le container php +composer require sentry/sentry-symfony:^5.10 +``` +Met à jour `composer.json` + `composer.lock`. Version identique à Lesstime (stack PHP 8.4 / +Symfony 8 commune). + +### 3.2 `config/bundles.php` +Ajouter l'enregistrement **prod-only** : +```php +use Sentry\SentryBundle\SentryBundle; +// ... +SentryBundle::class => ['prod' => true], +``` +> `composer require` ajoute généralement la ligne `['all' => true]` via Flex — la corriger en +> `['prod' => true]`. + +### 3.3 `config/packages/sentry.yaml` *(nouveau fichier)* +```yaml +# Error tracking → GlitchTip (compatible SDK Sentry). +# Actif uniquement en prod (bundle enregistré prod-only dans bundles.php). +# Si SENTRY_DSN est vide/non défini, le SDK est inerte (rien n'est envoyé). +when@prod: + parameters: + env(SENTRY_DSN): '' + + sentry: + dsn: '%env(SENTRY_DSN)%' + # On capture les erreurs fatales PHP via le handler, mais on DÉSACTIVE le listener + # kernel pour éviter les doublons avec le handler Monolog (les exceptions du kernel + # sont déjà logguées par Symfony → remontées via Monolog). + register_error_listener: false + register_error_handler: true + options: + environment: '%env(APP_ENV)%' + release: '%app.version%' + traces_sample_rate: 0.0 + ignore_exceptions: + - Symfony\Component\HttpKernel\Exception\NotFoundHttpException + - Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException + - Symfony\Component\Security\Core\Exception\AccessDeniedException + + # Handler Monolog → Sentry : remonte les logs niveau ERROR+ comme Issues GlitchTip. + services: + Sentry\Monolog\Handler: + arguments: + $hub: '@Sentry\State\HubInterface' + $level: !php/const Monolog\Level::Error + $bubble: true +``` +> `release: '%app.version%'` réutilise `config/version.yaml` (`app.version`, ex. `0.1.127`). + +### 3.4 `config/packages/monolog.yaml` +Dans le bloc `when@prod.monolog.handlers`, ajouter : +```yaml + # Remonte les logs ERROR+ vers GlitchTip en tant qu'Issues (service défini dans + # sentry.yaml). Envoi immédiat, indépendamment des handlers fichier. + sentry: + type: service + id: Sentry\Monolog\Handler + channels: ["!event", "!doctrine", "!deprecation", "!cron"] +``` +> Les autres handlers (`main`, `cron`, `deprecation`) restent inchangés. + +### 3.5 `.env` (+ `.env.example` si présent) +Bloc documenté (valeur réelle injectée côté serveur uniquement) : +```env +###> sentry/sentry-symfony ### +# Error tracking backend → GlitchTip (projet "sirh-api"). Prod only, vide => inerte. +# À définir dans l'env_file du serveur, PAS ici. Format : +# SENTRY_DSN=http://@:/ +# SENTRY_DSN= +###< sentry/sentry-symfony ### +``` + +### 3.6 CI / Dockerfile +**Aucun changement requis** pour le backend : le DSN est runtime (env_file). La CI +(`.gitea/workflows/build-docker.yml`) ne build/push que l'image — rien à toucher. + +**CA TLS (conditionnel)** — voir §4.4 : nécessaire **uniquement** si le DSN cible l'HTTPS interne +`logs.malio-dev.fr`. Si on tape l'endpoint **HTTP** GlitchTip via le tailnet (recommandé), pas de +modif Dockerfile. + +## 4. Runbook infra (hors repo) — toutes les étapes & commandes + +### 4.1 Installer Tailscale sur le host de prod OVH +```bash +# Sur le serveur OVH (Debian/Ubuntu), en root/sudo : +curl -fsSL https://tailscale.com/install.sh | sh + +# Jointure du tailnet (ouvre une URL d'auth, ou utiliser une auth key headless) : +sudo tailscale up +# --- headless (CI/scripté) : +# sudo tailscale up --authkey tskey-auth-XXXXXXXXXXXX + +# Vérifier l'état et récupérer l'IP tailnet du serveur : +tailscale status +tailscale ip -4 +``` + +> **Si GlitchTip est sur une autre machine du tailnet** : noter son IP tailnet (`100.x.y.z`) ou son +> nom MagicDNS. **Si GlitchTip est derrière un subnet router** (LAN interne non tailnet) : ajouter +> `--accept-routes` au `tailscale up`, et s'assurer qu'un subnet router annonce le sous-réseau. + +### 4.2 Vérifier la connectivité host → GlitchTip via le tailnet +```bash +# Depuis le host OVH : +tailscale ping +curl -sS -o /dev/null -w "%{http_code}\n" http://:/_health/ # → 200 attendu +``` + +### 4.3 Rendre le tailnet joignable depuis le container PHP + +Le container PHP est sur le réseau bridge Docker, pas directement sur le tailnet. Deux options : + +**Option A — Host Tailscale + IP tailnet dans le DSN (recommandé, simple).** +L'egress du container est masqueradé par le host, qui route `100.x.y.z` via `tailscale0`. +→ Pointer `SENTRY_DSN` directement sur l'**IP tailnet** de GlitchTip (pas MagicDNS, que le +container ne résout pas). Optionnellement figer le nom via `extra_hosts` dans le compose : +```yaml + # docker-compose.yml (serveur) + extra_hosts: + - "glitchtip.tailnet:100.x.y.z" +``` +Prérequis : IP forwarding actif sur le host (`net.ipv4.ip_forward=1`, déjà posé par l'install +Tailscale). + +**Option B — Sidecar Tailscale (robuste, si A ne route pas).** +Service `tailscale/tailscale` dans le compose, et le container app en +`network_mode: service:tailscale` → l'app partage l'interface tailnet (MagicDNS dispo). +À retenir seulement si l'option A ne fonctionne pas. + +### 4.4 (Conditionnel) CA racine MALIO — uniquement si DSN = HTTPS interne +Si le DSN cible `https://logs.malio-dev.fr` (cert auto-signé), baker la CA dans l'image +(`deploy/docker/Dockerfile.prod`, stage `production`) — `ca-certificates` est déjà installé : +```dockerfile +COPY deploy/docker/malio-dev-root-ca.crt /usr/local/share/ca-certificates/malio-dev-root-ca.crt +RUN update-ca-certificates +``` +(Le `.crt` public est récupérable depuis le repo Lesstime : `infra/prod/malio-dev-root-ca.crt`.) +Vérification : +```bash +curl --cacert deploy/docker/malio-dev-root-ca.crt https://logs.malio-dev.fr/api//store/ +``` +> **Recommandation** : préférer l'endpoint **HTTP** via le tailnet (déjà chiffré par WireGuard) → +> on évite complètement la CA et cette modif Dockerfile. + +### 4.5 Créer le projet GlitchTip `sirh-api` +Dans l'UI GlitchTip (org `malio`) : **New Project** → plateforme `php-symfony` → nom `sirh-api`. +Récupérer le **DSN** dans *Settings → Client Keys (DSN)*. Adapter le host du DSN à l'IP/nom tailnet +si nécessaire. +> Le MCP GlitchTip est en lecture seule (pas de `create_project`) → création manuelle UI. + +### 4.6 Injecter le DSN sur le serveur +Ajouter à l'`env_file` du docker-compose serveur (PAS dans l'image), puis redéployer : +```env +SENTRY_DSN=http://@100.x.y.z:/ +``` +```bash +docker compose up -d # recharge l'env_file +docker compose exec php php bin/console cache:clear --env=prod +``` + +## 5. Documentation (règles SIRH) + +- **`doc/error-tracking.md`** *(nouveau)* : pattern back, activation, runbook Tailscale, CA, lien + vers ce spec. +- **`CLAUDE.md`** : nouvelle section « Error tracking (GlitchTip) » résumant le pattern + le fait + que c'est prod-only / inerte sans DSN / transport Tailscale. +- **In-app documentation** (`frontend/data/documentation-content.ts`) : **non concernée** — + infra invisible pour les utilisateurs RH (employé/chef de site/admin), aucun changement + fonctionnel UI. + +## 6. Vérification + +| Niveau | Test | Attendu | +|---|---|---| +| Dev (sans DSN) | `make test`, boot dev | aucune régression, SDK absent en dev | +| Prod config | build image + `APP_ENV=prod cache:clear` (DSN bidon) | bundle chargé, pas d'erreur de conf | +| Inerte | prod sans `SENTRY_DSN` | aucun envoi, no-op | +| End-to-end | une fois Tailscale + projet OK : déclencher une erreur ERROR+ | Issue visible dans GlitchTip `sirh-api` | + +## 7. Hors périmètre (explicite) + +- Frontend (SDK Nuxt, source maps, build-args CI) — ajout futur via proxy nginx `/ingest`. +- APM / tracing / performance (DuckDB-like) — non. +- Exposition publique de GlitchTip — non (tout passe par Tailscale). diff --git a/symfony.lock b/symfony.lock index e6d16c4..fe88c7b 100644 --- a/symfony.lock +++ b/symfony.lock @@ -112,6 +112,15 @@ "bin/phpunit" ] }, + "sentry/sentry-symfony": { + "version": "5.10", + "recipe": { + "repo": "github.com/symfony/recipes-contrib", + "branch": "main", + "version": "5.0", + "ref": "aac2bc5220e9ab5b9e3838a7a4da90e7f74e6148" + } + }, "symfony/console": { "version": "8.0", "recipe": {