presseportale/dev/git-deploy-monitoring/02-fehlerprotokoll-glitchtip.md
Kevin Adametz fd8dd74d5f ops: Lokale Datenbank gegen fremde Testläufe absichern — DB_* nicht mehr als Container-Umgebung, Agenten-Regel, Testdatenbank-Pflicht im Baukasten
Am 07.10.2026 hat das RefreshDatabase eines frischen Demo-Projekts (Test der
Vorlagen aus dev/git-deploy-monitoring) die lokale Datenbank `pressekonto` auf
global-mysql vollständig geleert (Binärlog binlog.000009, Pos. 566161486).
Ursache: docker-compose.yml setzte DB_* als Container-Umgebungsvariablen, die
in Laravel Vorrang vor jeder .env haben; die phpunit.xml des Demo-Projekts
erzwang kein SQLite. Die eigene Suite war nicht beteiligt (force="true" und
Guard in tests/TestCase.php greifen; Gegenprobe mit MySQL-Variablen grün).

- docker-compose.yml: DB_* entfernt (Werte identisch in .env), wirksam nach
  Neuerstellen des Containers.
- Baukasten: „Testdatenbank absichern" (phpunit.xml mit force, Guard,
  Gegenprobe) als Pflicht vor RefreshDatabase.
- .ai/rules/general.md und CLAUDE.md: fremde/frische Laravel-Projekte nur mit
  SQLite :memory: testen; migrate hier erst nach migrate:status.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 16:09:25 +00:00

12 KiB

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
<app>-server PHP / Laravel SENTRY_LARAVEL_DSN
<app>-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)

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:

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:

'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

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 <meta name="pk-error-reporting"> mit DSN, Release, User-ID, Bereich — nur mit SENTRY_BROWSER_DSN

Vite-Eingang ergänzen (vite.config.js, input):

input: ['resources/css/app.css', 'resources/js/app.js', 'resources/js/error-reporting.js'],

Im <head> jedes Layouts (Build-Ordner und Bereich anpassen):

<x-monitoring.browser-errors build="build" area="web" />

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:

<!-- Tests provozieren Fehler absichtlich — nie an das echte GlitchTip -->
<env name="SENTRY_LARAVEL_DSN" value="" force="true"/>
<env name="SENTRY_DSN" value="" force="true"/>
<env name="SENTRY_BROWSER_DSN" value="" force="true"/>

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 <php>-Block:

<env name="DB_CONNECTION" value="sqlite" force="true"/>
<env name="DB_DATABASE" value=":memory:" force="true"/>
<env name="DB_URL" value="" force="true"/>
<server name="DB_CONNECTION" value="sqlite" force="true"/>
<server name="DB_DATABASE" value=":memory:" force="true"/>
<server name="DB_URL" value="" force="true"/>

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):

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:

    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.

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/<ID>/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).