presseportale/dev/betrieb/monitoring-plan.md
Kevin Adametz 32e1349517 dev: Baukasten Git-Deploy und Betriebsüberwachung für andere Laravel-Projekte (dev/git-deploy-monitoring) — Anleitungen, Checkliste, Vorlagen für Laravel, deploy.sh und Monitoring-Server
Vorlagen in einem frischen Laravel 12 eingebaut (30 Tests grün), deploy.sh
gegen ein simuliertes Repository durchgespielt (Deploy, Abbruch bei
Migrationen, Drift, Rollback, fehlender Worker). GlitchTip-Aufbewahrung heißt
seit Version 6 GLITCHTIP_RETENTION_DAYS — in der Projekt-Doku korrigiert.

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

316 lines
18 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_RETENTION_DAYS=90`, vor GlitchTip 6 `GLITCHTIP_MAX_EVENT_LIFE_DAYS`). 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`).
### 06.10.2026 — Nachtrag 1.7: fehlgeschlagene Anmeldungen
Befund auf Live (05.10. 15:26 – 06.10. 16:25): rund 250 Fehlversuche aus
53 Quellen, **alle** `unknown_account`, kein falsches Passwort, keine Sperre,
gleichmäßig auch nachts — verteiltes, langsames Durchprobieren fremder
Zugangsdaten („low and slow“, jede Quelle bleibt unter der Sperre von
5/Minute). Kein Konto betroffen. Die Einträge verdeckten im Verlauf die
relevanten Fälle. Das Nginx-Zugriffsprotokoll der Site ist bei Ploi aus,
der Einstieg ließ sich so nicht klären.
Umgesetzt:
- `meta.entry` an jedem Fehlversuch, jeder Sperre und jedem 2FA-Fehler:
`livewire` (Formular), `fortify` (`POST /login`, alle Domains, auch JSON),
`two-factor`, sonst `other`.
- Unbekannte Adressen nicht mehr im Verlauf, sondern in `failed_login_tallies`
(Tag × Einstieg × IP-Hash → Versuche, erster/letzter Zeitpunkt). Löschfrist
90 Tage über `model:prune` (täglich 04:05).
- Systemstatus-Kachel „Anmeldeversuche mit unbekannter Adresse“ (heute/gestern,
Versuche und Quellen); `warn` ab `MONITORING_FAILED_LOGIN_WARN_PER_DAY` (1000).
Auswertung auf Live (nach dem Deploy):
```bash
php artisan tinker --execute '
DB::table("failed_login_tallies")
->where("day", ">=", now()->subDays(7)->toDateString())
->selectRaw("day, entry, count(*) as quellen, sum(attempts) as versuche")
->groupBy("day", "entry")->orderBy("day")->orderBy("entry")
->get()
->each(fn ($r) => print(sprintf("%s %-10s %4d Quellen %5d Versuche\n", $r->day, $r->entry, $r->quellen, $r->versuche)));
'
```
Danach entscheiden: überwiegend `fortify` → `POST /login` auf den Hub
beschränken bzw. je IP drosseln (vorher klären, wer den JSON-Login nutzt);
überwiegend `livewire` → Honeypot/Turnstile im Formular.
### Externer Monitor auf `/health` — Einrichtung (erledigt 06.10.2026, status.adametz.media)
Ziel: Ausfälle melden, die die App selbst nicht melden kann (Server weg,
PHP-FPM hängt, Scheduler steht, Platte voll). **Uptime Kuma** auf dem
GlitchTip-Server (`178.105.154.106`); die App läuft auf `178.105.240.70` —
ein anderer Server, also echte Außensicht. ntfy liegt auf demselben Server
wie Uptime Kuma; fällt dieser aus, kommt kein Push (siehe Schritt 6).
1. **Installation** (Docker, neben GlitchTip, hinter demselben Reverse Proxy,
z. B. `status.adametz.media`):
```yaml
services:
uptime-kuma:
image: louislam/uptime-kuma:1
restart: unless-stopped
volumes:
- ./uptime-kuma:/app/data
ports:
- "127.0.0.1:3001:3001"
```
Admin-Konto beim ersten Aufruf anlegen, 2FA in den Einstellungen aktivieren.
2. **Benachrichtigung** (Settings ▸ Notifications ▸ ntfy): Server
`https://push.adametz.media`, Topic `pressekonto-ops`, Priorität 4,
Authentifizierung „Access Token“ mit dem Token von `pressekonto-alerts`.
Zusätzlich SMTP-Mail an das Ops-Postfach.
3. **Monitor „pressekonto /health“**: Typ HTTP(s), URL
`https://pressekonto.com/health`, Methode GET, Header (JSON)
`{"Authorization": "Bearer <MONITORING_HEALTH_TOKEN aus der Live-.env>"}`,
Intervall 60 s, Wiederholungen 2 (Retry-Intervall 60 s), akzeptierte
Statuscodes `200-299`. 503 = mindestens ein Check kritisch, Details in der
JSON-Antwort.
4. **Monitore „Domains“** ohne Token, Typ HTTP(s) - Keyword oder HTTP(s):
`https://presseecho.de/up`, `https://businessportal24.com/up`,
`https://pressekonto.com/up` — prüft Nginx/SSL/PHP je Domain, Intervall 5 min.
Zertifikatsablauf-Warnung aktivieren (Settings ▸ „Certificate Expiry Notification“, 14 Tage).
5. **Test:** Einen zweiten Monitor mit absichtlich falschem Token anlegen —
`/health` antwortet dann 404, der Alarm muss auf dem Handy und per Mail
ankommen. Danach diesen Test-Monitor löschen. Beim nächsten Deploy zeigt
der echte Monitor die Wartungsphase (`down` → 503) als kurzen Ausfall.
6. **Zweite Ebene (optional, empfohlen):** ein kostenloser externer Dienst
(z. B. UptimeRobot) mit einem einfachen Monitor auf `https://pressekonto.com/up`
und Mail-Benachrichtigung. Er meldet, wenn der Monitoring-Server selbst
ausfällt oder beide Server gemeinsam nicht erreichbar sind.
Nach der Einrichtung: Punkt 29 in `deploy-v1.1.0.md` und Phase 1 hier als erledigt eintragen.
### 06.10.2026 — Worker-Ausfall und Folgen (v1.3.2)
Nach dem Deploy von `v1.3.0` (16:51) lief über eine Stunde kein Queue-Worker:
Supervisor war gestoppt, `queue:restart` beendete den Worker, niemand startete
ihn neu. Sichtbar war es nur in der Systemstatus-Kachel; einen Alarm gab es
nicht — `/health` hätte 503 geliefert, der externe Monitor war noch nicht
eingerichtet. Beim Abarbeiten scheiterten drei Veröffentlichungsmails vom
03./08.09., die erneut eingestellt worden waren (GlitchTip #23, alte
Serialisierung ohne `$viaSyndication`) — entfernt, nicht nachgesendet.
- **Externer Monitor** läuft seit 06.10.2026 (`status.adametz.media`, Uptime
Kuma). Er deckt einen stehenden Worker ab: `/health` meldet `queue`
kritisch ab 5 Minuten Wartezeit. Ein zusätzlicher Alarm aus dem Scheduler
wurde deshalb **bewusst nicht** gebaut (wäre doppelt).
- **Worker-Prüfung im Deploy** (`monitoring:queue-probe`, Ende von
`deploy.sh`, Exit 3 ohne Worker).
- **Runbook:** kein pauschales `queue:retry all` mehr; Vorgehen bei stehendem Worker.
### 07.10.2026 — Allgemeine Fassung für andere Projekte
Git-Deploy und Betriebsüberwachung als übertragbarer Baukasten mit Vorlagen
(Laravel-Dateien, `deploy.sh`, Docker-Compose für GlitchTip/ntfy/Uptime Kuma):
[`../git-deploy-monitoring/`](../git-deploy-monitoring/README.md). In einem
frischen Laravel 12 eingebaut und getestet.