# 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://<clé>@<host-ou-IP-tailnet>:<port>/<id-projet>
# 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 <glitchtip-tailnet-name-ou-IP>
curl -sS -o /dev/null -w "%{http_code}\n" http://<glitchtip-IP-tailnet>:<port>/_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/<id>/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://<clé>@100.x.y.z:<port>/<id-sirh-api>
```
```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).
