# 02 — Fehlerprotokoll mit GlitchTip GlitchTip ist ein selbst gehostetes, Sentry-kompatibles Fehlerprotokoll. Die App spricht es über die **offiziellen Sentry-SDKs** an (`sentry/sentry-laravel`, `@sentry/browser`); ohne DSN sind beide ein No-op. Gleiche Fehler werden zu einem Eintrag gruppiert (Häufigkeit, betroffene URLs, Release), gemeldet wird nur ein neuer Fehlertyp. --- ## 1. GlitchTip-Server (einmalig, für alle Projekte) Vorlage: `vorlagen/server/glitchtip-compose.yml` (GlitchTip 6, Postgres 18, Valkey; gegen die offizielle `compose.sample.yml` abgleichen). 1. `SECRET_KEY` (`openssl rand -hex 32`), `GLITCHTIP_DOMAIN`, `EMAIL_URL`, `DEFAULT_FROM_EMAIL` setzen. Aufbewahrung: `GLITCHTIP_RETENTION_DAYS` (Standard 90 — ältere Versionen hießen `GLITCHTIP_MAX_EVENT_LIFE_DAYS`). 2. `docker compose -f glitchtip-compose.yml up -d`, Reverse Proxy mit TLS (`vorlagen/server/Caddyfile`). 3. Erstes Konto anlegen, danach `ENABLE_USER_REGISTRATION: "False"`, neu starten. 4. Organisation anlegen (Slug merken, z. B. `meine-org`). ## 2. Je Projekt: zwei GlitchTip-Projekte | Projekt | Plattform | `.env` | |---|---|---| | `-server` | PHP / Laravel | `SENTRY_LARAVEL_DSN` | | `-browser` | JavaScript | `SENTRY_BROWSER_DSN` | Getrennt, weil Browserfehler (Erweiterungen, alte Browser) eine andere Qualität haben als Serverfehler. Je Projekt eine **Alert-Regel**: Mail bei neuem Fehlertyp, wenn möglich nur für Umgebung `production`. Umgebungen: `SENTRY_ENVIRONMENT=production` auf Live, `local` lokal. Lokal die DSN nur zum Testen setzen. --- ## 3. Laravel-Anbindung (Server) ```bash composer require sentry/sentry-laravel ``` Dateien aus `vorlagen/laravel/` kopieren: | Datei | Zweck | |---|---| | `config/sentry.php` | gehärtete Konfiguration (ersetzt die veröffentlichte) | | `app/Support/Monitoring/ErrorReportScrubber.php` | `before_send`: nur User-ID, Tokens/Signaturen aus URL/Query/Referer, E-Mails aus Meldungen | | `app/Support/Monitoring/Release.php` | Version aus `APP_VERSION` oder Datei `RELEASE` | | `app/Support/Monitoring/ErrorSignal.php` | Warnung ohne Exception, gedrosselt (5 je 10 min und Ort) | | `app/Http/Middleware/ReportErrorSignals.php` | 404 mit eigenem Referrer, 403/429 für Angemeldete, 419 | `bootstrap/app.php`: ```php use App\Http\Middleware\ReportErrorSignals; use Sentry\Laravel\Integration; use Symfony\Component\Console\Exception\CommandNotFoundException; ->withMiddleware(function (Middleware $middleware): void { // Fehler ohne Exception (toter interner Link, 403/429 für Angemeldete, 419) — // global, weil 404 vor dem Routing entsteht $middleware->append(ReportErrorSignals::class); }) ->withExceptions(function (Exceptions $exceptions): void { // Gemeldete Exceptions an GlitchTip (ohne DSN ein No-op); // Laravels dontReport (404, Validierung, Auth) gilt weiter Integration::handles($exceptions); // Vertippte artisan-Befehle sind kein Anwendungsfehler $exceptions->dontReport(CommandNotFoundException::class); }) ``` `config/app.php`: ```php 'version' => App\Support\Monitoring\Release::current(env('APP_VERSION')), ``` `config/sentry.php` liest `release` aus derselben Quelle. ### Was die gehärtete `config/sentry.php` anders macht | Option | Wert | Warum | |---|---|---| | `send_default_pii` | `false` (fest) | keine IP, keine Cookies, keine Header mit Personenbezug | | `max_request_body_size` | `never` | Formular- und JSON-Inhalte verlassen den Server nie | | `before_send` | `ErrorReportScrubber::beforeSend` | User-ID statt Person, Tokens raus, E-Mails → `[email]` | | `breadcrumbs.logs`, `breadcrumbs.cache` | `false` | Lognachrichten tragen oft E-Mail-Adressen | | `enable_metrics` | `false` | versteht GlitchTip nicht | | `ignore_transactions` | `/up`, `/health` | Monitor-Aufrufe sind kein Signal | **Je Projekt anpassen:** `ErrorReportScrubber::AllowedQueryParameters` — welche Query-Parameter unverändert mitgehen dürfen (`utm_*` immer). Dieselbe Liste steht in `error-reporting.js`. --- ## 4. Browserfehler ```bash npm install @sentry/browser ``` | Datei | Zweck | |---|---| | `resources/js/error-reporting.js` | hört auf `error` und `unhandledrejection`; lädt das SDK erst beim ersten Fehler (1,5 kB statt 32 kB auf fehlerfreien Seiten); filtert URLs und E-Mails | | `resources/js/error-reporting-sdk.js` | Re-Export der benötigten SDK-Funktionen (Tree-Shaking) | | `resources/views/components/monitoring/browser-errors.blade.php` | `` mit DSN, Release, User-ID, Bereich — nur mit `SENTRY_BROWSER_DSN` | Vite-Eingang ergänzen (`vite.config.js`, `input`): ```js input: ['resources/css/app.css', 'resources/js/app.js', 'resources/js/error-reporting.js'], ``` Im `` jedes Layouts (Build-Ordner und Bereich anpassen): ```blade ``` `area` erscheint in GlitchTip als Tag (z. B. `web`, `portal`, `auth`). `config/monitoring.php` aus der Vorlage liefert `monitoring.browser.dsn`. --- ## 5. Tests — nie an das echte GlitchTip melden `phpunit.xml`: ```xml ``` Tests aus der Vorlage: `tests/Feature/Monitoring/ErrorReportingTest.php` (Datenschutzfilter, Wächtertest „the test suite never reports to the real glitchtip“), `ErrorSignalsTest.php`, `BrowserErrorReportingTest.php`, `tests/Unit/Monitoring/ReleaseTest.php`. Ereignisse fängt `tests/Support/FakeGlitchTip.php` ab (`FakeGlitchTip::install()`). Die Tests setzen `RefreshDatabase` voraus (in `tests/Pest.php` aktivieren) und eine `User`-Factory. ### Testdatenbank absichern (vor `RefreshDatabase`!) `RefreshDatabase` führt `migrate:fresh` aus und **löscht alle Tabellen** der Datenbank, auf die die Tests zeigen. In einer frischen Laravel-Installation sind die SQLite-Zeilen in `phpunit.xml` auskommentiert, und ohne `force="true"` haben vorhandene Umgebungsvariablen (Docker-/Container-`environment`, CI) Vorrang vor `phpunit.xml` und `.env`. So hat am 07.10.2026 das Testen dieser Vorlagen in einem Demo-Projekt die lokale Datenbank eines anderen Projekts geleert, das im selben Container lief. `phpunit.xml`, im ``-Block: ```xml ``` Guard in `tests/TestCase.php` — `setUpTraits()` läuft vor jedem Datenbank-Trait und bricht ab, bevor etwas geleert wird (auch bei einem gecachten `bootstrap/cache/config.php`): ```php protected function setUpTraits(): array { $default = (string) \Illuminate\Support\Facades\DB::getDefaultConnection(); $connection = (array) config('database.connections.'.$default, []); if (! empty($connection['url']) || ($connection['driver'] ?? null) !== 'sqlite' || ($connection['database'] ?? null) !== ':memory:') { throw new \RuntimeException("Tests laufen nicht gegen SQLite :memory: ({$default}) — abgebrochen, bevor RefreshDatabase etwas leert."); } return parent::setUpTraits(); } ``` Wer Vorlagen in einem Wegwerf-Projekt ausprobiert, startet die Tests zusätzlich explizit mit `DB_CONNECTION=sqlite DB_DATABASE=:memory: php artisan test`. --- ## 6. Regeln für den Code - **Keine variablen Zahlen in Fehlermeldungen**, wenn sie gruppiert werden sollen — GlitchTip gruppiert nach Text. Variable Werte in den Kontext oder einen Fingerprint setzen (Muster: `ErrorSignal`). - **Vorübergehende Fremdfehler nicht einzeln melden** (Timeouts, 5xx, 429 von externen APIs): einmal wiederholen, dann `Log::warning`; Alarm erst, wenn ein Lauf abbricht oder die Mehrheit scheitert. Muster: ```php try { $response = Http::timeout(20) ->retry(2, 3000, fn (Throwable $e) => $e instanceof ConnectionException || ($e instanceof RequestException && ($e->response->serverError() || $e->response->status() === 429)), throw: false) ->post($url, $payload); } catch (ConnectionException $e) { throw new ServiceTemporarilyUnavailable('… nicht erreichbar', previous: $e); // eigene Klasse } // Aufrufer: catch (ServiceTemporarilyUnavailable) → Log::warning, sonst report() ``` - **Keine Personenbezüge** in Meldungen bauen; der Filter ist das Netz, nicht der Plan. --- ## 7. Auswertung per API (für Entwickler und KI-Chats) Lese-Token in GlitchTip (Profil ▸ Auth Tokens): Scopes `project:read`, `event:read` — **kein** Schreibrecht. Lokal als `GLITCHTIP_API_TOKEN` in die `.env`, nie ausgeben, nie committen. Ohne `org:read` liefert die Organisationsliste 403; Projekte und Issues funktionieren. ```bash T=$(grep -E "^GLITCHTIP_API_TOKEN=" .env | cut -d= -f2- | tr -d "'\"") B=https://errors.example.com/api/0 ORG=meine-org # Projekte (IDs) curl -s -H "Authorization: Bearer $T" "$B/projects/" | python3 -c " import json,sys; [print(p['id'], p['slug']) for p in json.load(sys.stdin)]" # Offene Einträge eines Projekts, nur Live curl -s -H "Authorization: Bearer $T" \ "$B/organizations/$ORG/issues/?project=1&query=is:unresolved&environment=production&limit=100" \ | python3 -c " import json,sys for i in json.load(sys.stdin): print(i['id'], i['count'], i['firstSeen'][:16], i['lastSeen'][:16], '|', (i['title'] or '')[:100])" # Letztes Ereignis: Umgebung, Release, URL, Stacktrace curl -s -H "Authorization: Bearer $T" "$B/issues//events/latest/" | python3 -c " import json,sys e=json.load(sys.stdin); t={x['key']:x['value'] for x in e.get('tags',[])} print('env:',t.get('environment'),'| release:',t.get('release'),'| area:',t.get('area')) for en in e.get('entries',[]): if en['type']=='exception': for v in en['data'].get('values',[]): print('EXC',v.get('type'),':',(v.get('value') or '')[:300]) for f in ((v.get('stacktrace') or {}).get('frames') or [])[-8:]: print(' ',f.get('filename'),f.get('function'),f.get('lineNo')) if en['type']=='request': print('REQ',en['data'].get('url')) if en['type']=='breadcrumbs': for b in en['data'].get('values',[])[-6:]: print('BC',b.get('category'),(b.get('message') or '')[:120])" ``` ### Ablauf „schau in GlitchTip“ 1. Offene Einträge beider Projekte abrufen, zuerst `environment=production`. 2. Einordnen: | Klasse | Erkennen | Vorgehen | |---|---|---| | Test-/Entwicklungsrauschen | Umgebung `local`, Texte aus Tests | nichts am Code; auflösen | | Bedienfehler am Server | z. B. `CommandNotFoundException` | `dontReport` | | Externer Dienst vorübergehend | Timeouts/5xx fremder APIs | Muster aus Abschnitt 6 | | Echter Bug | Stacktrace in `app/`, `resources/`, Browserfehler mit eigener URL | roter Test → Fix → Deploy | | Unklar | z. B. `Error: [object Object]` | Erfassung verbessern, nächstes Auftreten abwarten | 3. Bei Browserfehlern zeigen die Frames oft nur `livewire.min.js` — der Hinweis steckt im Funktionsnamen (`[Alpine] __('…')` = Alpine-Ausdruck) → in `resources/views` danach suchen. Kann ein Muster wiederkommen: Wächtertest über alle Dateien. 4. Fix ausliefern (Hotfix-Tag), danach in GlitchTip auflösen (von Hand — Token ist nur lesend). Kommt der Fehler mit neuem Release wieder, öffnet GlitchTip den Eintrag neu. 5. **Breadcrumbs lesen**: Sie zeigen die SQL-Abfragen vor dem Fehler — oft erkennt man daran Job, Modell und Datensatz (z. B. `failed_jobs`-Insert → Fehler in einem Queue-Job; `payload.createdAt` verrät alte Jobs). Im Projekt eine Kurzfassung mit den echten Werten (Instanz, Org-Slug, Projekt-IDs) unter `dev/glitch-tip/README.md` ablegen und in `CLAUDE.md` verweisen — dann finden KI-Chats sie automatisch. --- ## 8. Datenschutz (vor dem Einschalten) - Datenschutzerklärung: Fehlerprotokollierung (Dienst selbst gehostet, Kategorien: User-ID, URL, Browser, Fehlermeldung; Frist = Aufbewahrung in GlitchTip, z. B. 90 Tage; Rechtsgrundlage berechtigtes Interesse am sicheren Betrieb). - Kein Auftragsverarbeiter, wenn GlitchTip auf eigenem Server läuft (Hoster ggf. AVV).