presseportale/dev/betrieb/monitoring-plan.md

205 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Betriebsüberwachung, Bedienbarkeit, Nutzungsstatistik — Plan
**Stand:** 05.10.2026 · **Phase 1 live seit 05.10.2026 (v1.1.0)**
Ziel: Fehler fallen auf, bevor Kunden sie melden — Serverfehler aus der
Programmierung ebenso wie Bedienungs- und Usability-Probleme. Dazu eine
Nutzungsstatistik, aus der sich ablesen lässt, wer was tut und wo Arbeit
aufhört.
## Entscheidungen (Betreiber, 02.10.2026)
| Nr. | Frage | Entscheidung |
|---|---|---|
| M1 | Fehlerwerkzeug | **GlitchTip, selbst gehostet** (wie Umami auf sys.adametz.media), angebunden über die offiziellen Sentry-SDKs (`sentry/sentry-laravel`, `@sentry/browser`) |
| M2 | Umami im Portal | **ja, mit Zuordnung zum Nutzer** (`umami.identify` mit der internen User-ID, keine E-Mail, keine Namen) |
| M3 | Alarmweg | **Mail bleibt**, zusätzlich **ntfy** (HTTP-POST, JSON) |
Bei der Umsetzung getroffen (Bestätigung offen):
| Nr. | Frage | Entscheidung |
|---|---|---|
| M4 | Personenbezug in GlitchTip | Nur die interne User-ID, `send_default_pii` aus, Request-Bodys nie (`max_request_body_size = never`) |
| M5 | Browserfehler-SDK | Wird erst beim ersten Fehler nachgeladen (dynamischer Import) — kein Ladegewicht auf fehlerfreien Seiten, dafür ohne Breadcrumbs |
| M6 | Umami-Schalter | Getrennt: `UMAMI_ENABLED` (Webseiten), `UMAMI_PORTAL_ENABLED` (Kundenbereich), `UMAMI_IDENTIFY_USERS` (Zuordnung). Portal und Zuordnung erst nach DSE-Anpassung einschalten |
| M7 | Umami im Admin, unter Impersonation, auf Auth-Seiten | Nicht getrackt: Admin-Nutzung verfälscht die Kundenstatistik, Impersonation würde Admin-Klicks dem Kunden zuschreiben, Auth-Seiten tragen Tokens im Pfad (`/reset-password/{token}`) |
| M8 | Query-Parameter an Umami | Nur `utm_*`, `src`, `ref`, `q`, `page` gehen raus; alles andere (Signaturen, Tokens) filtert `data-before-send` — gilt auch für die Webseiten |
| M9 | Fehlgeschlagene Logins | Im Verlauf mit User-Bezug (falls Konto existiert) und gekürztem HMAC der IP statt Klartext-IP |
## Phase 1 — Fehler fallen sofort auf
| Schritt | Inhalt | Stand |
|---|---|---|
| 1.1 | GlitchTip-Anbindung Server: `sentry/sentry-laravel`, `Integration::handles()` in `bootstrap/app.php`, Release aus `SENTRY_RELEASE`, User-ID als einziger Personenbezug | erledigt |
| 1.2 | Browserfehler (JS-Fehler, unbehandelte Promises) auf Webseiten, Auth-Seiten und Portal; eigener Vite-Eintrag `resources/js/error-reporting.js`, SDK erst beim ersten Fehler | erledigt |
| 1.3 | Fehler ohne Exception als GlitchTip-Warnung: 404 mit Referrer der eigenen Domain (kaputte interne Links), 403 und 429 für angemeldete Nutzer, 419 (Seite abgelaufen, Arbeit verloren). Je Signal gedrosselt | erledigt |
| 1.4 | Scheduler-Herzschlag (`monitoring:heartbeat`, minütlich) und Check im Systemstatus | erledigt |
| 1.5 | Tiefer Health-Endpunkt `/health` mit Token: DB, Cache, Queue-Rückstau, Scheduler-Herzschlag, Plattenplatz → 200/503 JSON, für Uptime Kuma bzw. Ploi | erledigt |
| 1.6 | ntfy als zweiter Alarmweg in `OperationalAlert` | erledigt |
| 1.7 | Fehlgeschlagene Logins, Login-Sperren und 2FA-Fehler im Aktivitäts-Verlauf | erledigt |
| 1.8 | `queue:prune-failed` (30 Tage) im Scheduler | erledigt |
| 1.9 | Umami: Portal (Kundenbereich) mit `identify`, Query-Filter, Web Vitals; Schalter getrennt (M6) | erledigt |
Signale, die **nicht** im Browser erfasst werden, weil der Server sie schon
sieht: fehlgeschlagene Livewire-Requests mit 5xx (Exception in GlitchTip) und
419 (Schritt 1.3).
## Phase 2 — Bedienprobleme sichtbar machen
- Validierungsfehler zentral zählen: Formular/Komponente × Feld × Regel, nie der Wert.
- Schritt-Tracking für die Kernabläufe (Registrierung, Newsroom anlegen,
PM erstellen/einreichen, Checkout, Bildbestätigung): `step_viewed` /
`step_completed`, pseudonymer Ablauf-Schlüssel, eigene kleine Tabelle,
Schreiben per `defer()`.
- Benannte Umami-Events (`data-umami-event`) an den Kern-Buttons.
- „Problem melden“ im Portal: URL, Browser, letzte GlitchTip-Event-ID, Freitext.
- Geschäftstrichter aus vorhandenen Daten (Registrierung → erste PM →
eingereicht → bezahlt → veröffentlicht → zweite PM), ohne neues Tracking.
## Phase 3 — Rückgänge und Abwanderung erkennen
- Tägliche Kennzahlen-Tabelle (Datum × Kennzahl × Dimension → Anzahl), nächtlich verdichtet.
- Alarme bei ausbleibender Aktivität: Einlieferer ohne Einlieferung, Tage
ohne Registrierung/Login/Zahlung, Funktionen deutlich unter dem 4-Wochen-Mittel.
- Überfällig-Liste je Kunde (übliches PM-Intervall × 2).
- Admin-Seite „Betrieb & Nutzung“; `conversion:funnel` und `api:usage-report` mit Oberfläche.
- Morgendliche Sammelmail.
- Löschfristen für `activity_events` und `api_usage_logs` (Vorschlag 395 Tage).
## Datenschutz (Betreiber/Anwalt, vor dem Einschalten)
- DSE §5 (Umami): Portal-Nutzung mit Zuordnung zur Nutzer-ID ist personenbezogen
(pseudonym) — Abschnitt ergänzen, bevor `UMAMI_PORTAL_ENABLED`/`UMAMI_IDENTIFY_USERS` an.
- Neuer Abschnitt Fehlerprotokollierung (GlitchTip, selbst gehostet; User-ID,
URL, Browser, Fehlermeldung; Frist = GlitchTip-Aufbewahrung, Vorschlag 90 Tage).
- Aktivitäts-Verlauf mit fehlgeschlagenen Anmeldungen (IP nur als gekürzter HMAC).
- Unabhängig davon offen: `activity_events`/`api_usage_logs` ohne Löschfrist und ohne DSE-Erwähnung.
## Live-Schritte Phase 1
Ausgeführt mit Deploy v1.1.0 — vollständiger Ablauf in [`deploy-v1.1.0.md`](deploy-v1.1.0.md).
**Infrastruktur (einmalig)**
1. GlitchTip auf sys.adametz.media (Docker Compose: web, worker, Postgres,
Redis; `GLITCHTIP_MAX_EVENT_LIFE_DAYS=90`). Zwei Projekte anlegen:
`pressekonto-server` (PHP/Laravel) und `pressekonto-browser` (JavaScript).
Alert-Regel je Projekt: Mail an das Ops-Postfach bei neuem Fehlertyp.
2. ntfy: Server (ntfy.sh oder selbst gehostet), Topic mit Zugriffsschutz,
Token anlegen. Optional GlitchTip-Webhook an ntfy über dessen
Template-Funktion.
3. Uptime Kuma (oder Ploi-Monitor) auf `https://pressekonto.com/health` mit
Header `Authorization: Bearer <MONITORING_HEALTH_TOKEN>`, Intervall 1 min,
Benachrichtigung über ntfy und Mail.
**`.env` auf Live**
```
SENTRY_LARAVEL_DSN=… # Projekt pressekonto-server
SENTRY_ENVIRONMENT=production
SENTRY_RELEASE= # leer lassen, siehe RELEASE-Datei unten
SENTRY_BROWSER_DSN=… # Projekt pressekonto-browser
MONITORING_HEALTH_TOKEN=<zufällig, ≥ 32 Zeichen>
MONITORING_NTFY_URL=https://push.adametz.media
MONITORING_NTFY_TOPIC=pressekonto-ops
MONITORING_NTFY_TOKEN=…
UMAMI_ENABLED=true # Webseiten
UMAMI_PORTAL_ENABLED=false # erst nach DSE-Anpassung
UMAMI_IDENTIFY_USERS=false # erst nach DSE-Anpassung
```
Danach `config:cache`, `npm run build`, `queue:restart`.
**Bei jedem Deploy (SFTP/SSH, solange es kein Git-Deploy gibt):** lokal vor
dem Upload `git describe --tags --always --dirty > RELEASE` und die Datei `RELEASE`
mit hochladen, danach auf dem Server `php artisan config:cache`. GlitchTip
ordnet Fehler so dem Stand zu (`…-dirty` = mit nicht committeten Änderungen
deployt). Beim späteren Git-Deploy schreibt das Deploy-Skript dieselbe Datei.
GlitchTip-Instanz: `errors.adametz.media`, Projekt 1 = Server, Projekt 2 = Browser.
Umgebungen: `production` (Live), `local` (Entwicklung).
## Umsetzungsprotokoll
### 02.10.2026 — Phase 1 umgesetzt (Branch `technik/monitoring-phase-1`)
Neue Abhängigkeiten: `sentry/sentry-laravel` ^4.28 (Composer, nur neue
Pakete), `@sentry/browser` ^11.3 (npm).
| Schritt | Dateien |
|---|---|
| 1.1 | `config/sentry.php` (veröffentlicht, gehärtet), `app/Support/Monitoring/ErrorReportScrubber.php`, `bootstrap/app.php` (`Integration::handles`) |
| 1.2 | `resources/js/error-reporting.js` + `error-reporting-sdk.js` (Chunk 32 kB gzip, nur bei Fehler), `resources/views/components/monitoring/browser-errors.blade.php`, eingebunden in `partials/head` (Portal), `web/layouts/web-master`, `components/layouts/auth/pressekonto`; Vite-Einträge in beiden Configs |
| 1.3 | `app/Support/Monitoring/ErrorSignal.php`, `app/Http/Middleware/ReportErrorSignals.php` (global) |
| 1.4 | `app/Console/Commands/MonitoringHeartbeat.php`, Scheduler minütlich, Check `scheduler` im Systemstatus |
| 1.5 | `app/Services/Monitoring/HealthProbe.php`, `app/Http/Controllers/HealthController.php`, Route `/health` (ohne Gruppe, throttle 30/min), Basic-Auth-Ausnahme; Check `disk` im Systemstatus |
| 1.6 | `app/Support/OperationalAlert.php` (Mail und ntfy unabhängig, beide Best-Effort) |
| 1.7 | `ActivityType::LoginFailed/LoginLockout/TwoFactorFailed`, `RecordAuthActivity` (Grund `unknown_account`/`no_password`/`wrong_password`, `ip_hash`), Login-Komponente feuert `Failed` und `Lockout` mit Adresse |
| 1.8 | `queue:prune-failed --hours=720` täglich 03:55 |
| 1.9 | `components/web/umami.blade.php` (Web + Portal, `data-tag`, `data-before-send`, `data-performance`, `identify('user-<id>')`), `config/services.php` (`portal_enabled`, `identify_users`) |
Nebenbefund: Das Umami-Snippet der Webseiten schickte bisher die volle Query
mit — signierte Claim-Links (`?signature=…`) landeten so in Umami. Der Filter
(M8) gilt jetzt auch dort.
Tests: `tests/Feature/Monitoring/*` (neu), `tests/Feature/Auth/FailedLoginActivityTest.php`
(neu), `OperationalAlertTest` und `UmamiSnippetTest` erweitert. Gesamtsuite
2312 grün (1 übersprungen), Larastan ohne Meldung, Pint sauber.
Nicht im Browser geprüft (kein GlitchTip/ntfy angebunden): das tatsächliche
Einliefern an GlitchTip und die Darstellung dort. Erster Test nach dem
Aufsetzen: im Browser `setTimeout(() => { throw new Error('GlitchTip-Test') })`
auf einer Portalseite, serverseitig `php artisan tinker --execute 'report(new RuntimeException("GlitchTip-Test"));'`.
### 05.10.2026 — Composer-Update
`composer update` innerhalb der bestehenden Grenzen (48 Updates, u. a.
Laravel 12.69.3, Livewire 4.4.7, Flux 2.20.1, Fortify 1.40.0); damit sind die
beiden `league/commonmark`-Advisories behoben (2.10.3), `composer audit` sauber.
- Livewire 4.4.7 rendert `ModelNotFoundException` im Testhelfer als 404,
statt sie zu werfen — zwei Tests auf `assertNotFound()` umgestellt
(`CustomerCompanyContextTest`, `ContactAccessTest`). Laufzeitverhalten unverändert.
- **Deploy:** Liegt auf Live `public/vendor/livewire/` (veröffentlichte Assets),
nach `composer install` `php artisan vendor:publish --tag=livewire:assets --force`
ausführen — sonst läuft ein altes `livewire.js` gegen das neue Backend.
Lokal war die Kopie schon vor dem Update veraltet (Hash 3dabe98a statt 8ea5922c).
### 05.10.2026 — GlitchTip angebunden (lokal)
DSNs eingetragen (errors.adametz.media, Projekte 1 und 2), lokal
`SENTRY_ENVIRONMENT=local`. Release-Kennung ohne Git auf dem Server:
`App\Support\Monitoring\Release` liest `SENTRY_RELEASE`, sonst die beim
Deploy hochgeladene Datei `RELEASE` (in `.gitignore`). Test-Events lokal
abgeschickt: Server-Exception (`report()`), toter interner Link per HTTP.
### 05.10.2026 — ntfy angebunden (lokal)
ntfy auf `push.adametz.media` (gleicher Server wie GlitchTip),
`auth-default-access: deny-all` (anonymes Lesen → 403 geprüft). Eigener
Benutzer `pressekonto-alerts` mit `write-only` auf `pressekonto-ops`, Token in
`MONITORING_NTFY_TOKEN`; Lesen über den persönlichen Benutzer in der App.
Test-Alert aus `OperationalAlert` ohne Fehler angenommen.
### 05.10.2026 — Versionsnummer
Versionen sind Git-Tags nach SemVer (`v1.0.0` Go-Live 20.07., nächster Deploy
`v1.1.0`). `config('app.version')` = `APP_VERSION` oder Datei `RELEASE`
(`git describe --tags --always --dirty`); dieselbe Kennung als GlitchTip-Release,
in `/health` (`version`), im Admin-Systemstatus und in `php artisan about`.
Grundlage für die spätere Changelog-Seite.
### 05.10.2026 — Phase 1 live (v1.1.0)
Mit Deploy v1.1.0 ausgeliefert ([`deploy-v1.1.0.md`](deploy-v1.1.0.md)).
GlitchTip (`production`), ntfy und Scheduler-Herzschlag auf Live aktiv.
Offen aus Phase 1: externer Monitor auf `/health`; DSE-Absätze, danach
`UMAMI_PORTAL_ENABLED`/`UMAMI_IDENTIFY_USERS`.
### 06.10.2026 — Erste Auswertung von GlitchTip
Lesezugriff per API eingerichtet; Arbeitsanleitung, Regeln und Protokoll der
Einträge in [`../glitch-tip/README.md`](../glitch-tip/README.md). Befunde:
Testsuite meldete an die echte DSN (behoben, `2f02acc`), Search-Console-Lauf
robuster (`a90f24a`), Registrierung mit Alpine-Fehler, Promise-Ablehnungen
lesbar, Tippfehler bei artisan nicht mehr gemeldet (`16af374`).