iOS-Crash-Reporting-Vertrag (MetricKit, selbstgehostet)
SILV-344 (Sub von SILV-127, Jaqueline-Rollout). Eigener Endpoint, kein Drittanbieter-SDK (Sentry o. Ä.) — selbstgehostet im Projekt-Geist. Macht Crashes/Hangs auf Familien-Geräten sichtbar, ohne Kabel/Xcode (Jaquelines iPhone hängt nie am Rechner).
Auth (gelockt, sicherheitsbegründet)
Session-Cookie der eingeloggten App (ss_session, ADR-16) — der globale
auth_guard schützt /api/* ohnehin, kein Sonder-Handling. KEIN
X-Internal-Token im Client: der ist ein Server-Secret; ihn ins App-Binary zu
legen würde ihn leaken, und ein Public-Internet-Client ist die falsche
Trust-Boundary. Die native App trägt das ss_session-Cookie aus dem
App-Group-Keychain. Ohne Anmeldung → 401 (kein offener Endpoint).
Datensparsamkeit (gelockt)
Nur Crash-/Hang-Stack + Versionen — keine Nutzerdaten/Diary-Inhalte.
MetricKit-Diagnose-Payloads enthalten Stacks/Symbole/Metriken, keinen
Nutzerinhalt; was gesendet wird, bestimmt der Client. payload wird als
JSON-Text gespeichert und bei 256 KB hart gedeckelt.
Endpoints
| Methode + Pfad | Tut |
|---|---|
POST /api/ios-crashes |
Diagnose-Payload annehmen. Body s. u. Return {ok, id, type}. Best-effort (MetricKit liefert verzögert + gebündelt beim nächsten App-Start). |
GET /api/ios-crashes?limit=50 |
Sichtbarkeit für Dennis: jüngste Reports schlank (ohne payload-Body, dafür payload_bytes + user_name). limit 1..200. |
GET /api/ios-crashes/{id} |
Voller Report inkl. payload (Stack als JSON-Text). 404 unbekannt. |
POST-Body (IosCrashIn, alle Felder optional außer type-Default)
{
"type": "crash", // crash|hang|cpu_exception|disk_write_exception|app_launch|diagnostic
// unbekannter Wert → serverseitig auf "diagnostic" normalisiert (rät nicht)
"app_version": "1.4.0",
"build": "142",
"os_version": "iOS 26.0",
"device_model": "iPhone14,5", // optional, nicht-personenbezogen
"occurred_at": "2026-06-21T08:30:00", // ISO, clientseitig gesammelt (optional)
"payload": { … } // schlankes MXDiagnosticPayload.jsonRepresentation()
// bzw. Crash-/Hang-Stack; dict|list|string; ≤256 KB (sonst gekappt)
}
user_id setzt der Server aus der Session (welches Familien-Gerät; null bei
internem Token). received_at = Server-Zeit. Kein harter Dedup (MetricKit
ist verzögert/gebündelt → best-effort); weicher Schlüssel zur manuellen Sicht =
occurred_at + type + build.
Wo landen die Reports
Tabelle ios_crashes in app/data/silverscale.db (im 03:30-DB-Backup +
Off-Site-Backup enthalten). Ansehen:
TOK=$(grep -E '^INTERNAL_API_TOKEN=' .env | cut -d= -f2-)
curl -s -H "X-Internal-Token: $TOK" http://127.0.0.1:9300/api/ios-crashes | python3 -m json.tool
curl -s -H "X-Internal-Token: $TOK" http://127.0.0.1:9300/api/ios-crashes/<id> # voller Stack
(Der X-Internal-Token-Weg ist nur die VPS-interne Sicht; die App selbst sendet
per Session-Cookie.) Eine UI-Fläche ist bewusst nicht Teil von SILV-344 —
erst Sichtbarkeit, UI später bei Bedarf.
Client (MacClaude, SILV-127)
MXMetricManager-Subscriber registrieren; didReceive diagnosticPayloads →
POST /api/ios-crashes mit dem schlanken JSON (best-effort, beim nächsten
App-Start). Auth = vorhandenes ss_session-Cookie der App.