Files
crewli/dev-docs/runbooks/observability-triage.md
bert.hausmans bf89090850 docs: observability triage + erasure runbooks
PR-4 commit 2. Both runbooks live under dev-docs/runbooks/ as the first
entries in that directory.

- observability-triage.md (270 lines): incoming-issue procedure. Tags
  inspectie (actor_scope, release, actor_type, organisation_id,
  impersonation), triage classes (P0–P3), reproductie via request_id
  correlation naar laravel.log, common patterns (validation leakage,
  runaway errors, multi-tenant invariant violations, CSP black-silence),
  resolution + audit trail.
- observability-erasure.md (293 lines): GDPR Art. 17 procedure.
  Trigger voorwaarden (upstream eerst), pre-checks, handmatige
  psql-procedure met counts vóór delete, post-checks, automation
  BACKLOG verwijzing, edge cases (no-events-in-window,
  impersonation-target, queued events, mass-erasure batch).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 19:46:49 +02:00

271 lines
10 KiB
Markdown

# Observability triage runbook
> Praktische gids voor wanneer Bert (of een latere op-call) een
> GlitchTip alert / email krijgt of zelf de Issues-pagina opent.
>
> **Audience:** maintainer met repo-toegang en ssh-toegang naar de
> productie-host.
>
> **Cross-references:**
> [`ARCH-OBSERVABILITY.md`](../ARCH-OBSERVABILITY.md) (architectuur en
> tag-taxonomie), [`GLITCHTIP.md`](../GLITCHTIP.md) (operationele
> stack — boot, backup, restore),
> [`observability-erasure.md`](./observability-erasure.md) (Art. 17
> procedure als triage in een GDPR-deletion-verzoek omdraait).
---
## §1 Eerste 60 seconden — issue inspecteren
Open de issue in GlitchTip. Vóór je begint na te denken over de
exception zelf, check de tags. Het kost 30 seconden en bepaalt vaak
direct de prioriteit.
### 1.1 `actor_scope` — wat is de blast radius?
| Waarde | Wat het betekent voor triage |
|---|---|
| `organisation` | Eén klant geraakt. Tag `organisation_id` zegt welke. |
| `platform` | super_admin op een platform-route. Bug in admin-tooling, niet in een klant-flow. |
| `user` | Authenticated user op een non-org route (`/me/*`, `/portal/profiel`). Vaak account-settings of auth-flow bug. |
| `portal` | Token-based portal flow (artist advance, public form fill). Geen `user_id` aanwezig — debug via `request_id` correlation. |
| `anonymous` | Public pagina, niet ingelogd. Login-flow / public-form bug. |
### 1.2 `release` — actuele deploy of oud residual?
`release=crewli-api@<sha>` of `crewli-app@<sha>`. Vergelijk met
`git log --oneline -5` op `main`. Als de SHA matcht het laatste deploy:
het probleem zit in de huidige codebase. Als de SHA achterloopt: kan
een long-running browser-session zijn die nog op een oude bundle
draait, of een achtergrondservice die nog niet herstart is na deploy.
### 1.3 `actor_type` — wie is geraakt?
`super_admin` / `organizer_admin` / `org_member` / `portal_token` /
`unauthenticated`. Een `super_admin`-only bug is laag-prio voor
klanten; een `org_member`-bug raakt vrijwilligers en moet sneller fix.
### 1.4 `organisation_id` — één klant of platform-breed?
| Aanwezig op één issue | Eén klant. Reach out direct als blocker. |
| Aanwezig met variable values across events binnen 1 issue | Multi-tenant bug; spike in events is een P0-signaal. |
| Afwezig | Verwacht voor `actor_scope=platform` / `user` / `anonymous`. Niet aanwezig zijn betekent dus niet automatisch "platform-breed" — check `actor_scope` eerst. |
### 1.5 `impersonation.active` — admin-context?
Bijna altijd `'false'`. Wanneer `'true'`, dan was een super_admin
aan het impersonating. Bug kan in de target-user's data zitten of
in de impersonation-flow zelf. Crosscheck:
`impersonation.impersonator_user_id` (admin's ULID) en
`impersonation.session_id` voor lookup in `impersonation_audit_logs`.
### 1.6 `request_id` — correlation naar Laravel logs
Elke gecaptured event draagt `request_id` als tag of in de extra
context (gezet door `BindRequestLogContext` middleware). Pak die ULID,
ssh naar productie-host en grep:
```bash
grep "01HX...." storage/logs/laravel.log | head -50
```
Logregels die door dezelfde request flowden delen die ULID. Geeft
context die GlitchTip's eigen breadcrumbs niet hebben (bijv. SQL
queries, queue-job dispatches).
---
## §2 Triage-classificatie
| Klasse | Signaal | Actie |
|---|---|---|
| **P0 — productie down** | Spike (>10 events / 5 min) op meerdere users; OF auth-flow bug die nieuwe logins blokkeert; OF spike op `actor_type=super_admin` (admin tooling kapot) | Direct fix-deploy. Notify klanten als > 30 min downtime verwacht. |
| **P1 — single-user blocker** | Repeating events same `user_id`, geen workaround. User kan niet doorwerken. | Reach out to user, fix in current sprint, mark issue assigned in GlitchTip. |
| **P2 — degraded UX** | Cosmetisch (bv. broken UI-state na specifieke actie), geen data-loss, workaround beschikbaar. | BACKLOG entry, plan in volgende sprint. Mark issue ignored in GlitchTip met BACKLOG-link in comment. |
| **P3 — known-tolerated** | Bekende edge case, gedocumenteerd in BACKLOG of een ARCH-doc. Niet fix-baar zonder grotere refactor. | Mark issue resolved/ignored in GlitchTip met inline comment naar de tracking-locatie. |
**Volume-thresholds zijn richtlijnen, niet hard rules.** Eén event op
een super_admin-route die data corrupt zou maken is P0 ondanks count=1.
Honderd events op een rate-limited public form-spam attempt is geen
P0 ondanks volume.
---
## §3 Reproductie-stappen
### 3.1 Pak het stack-trace + tags
GlitchTip toont stack trace met sourcemap-resolved frames (zie
[`ARCH-OBSERVABILITY.md §8`](../ARCH-OBSERVABILITY.md)). Open de
exception, klik op het top frame, lees de regel.
### 3.2 Cross-correlate met laravel.log
```bash
ssh crewli@<prod-host>
cd /home/crewli/crewli/api
grep "<request_id-ULID>" storage/logs/laravel.log
```
Geeft je: alle log-regels die door deze request flowden, in volgorde.
Zoek naar: SQL-query timing (langzame query?), queue-dispatch
(achtergrond-job triggered?), warning-logs vóór de exception.
### 3.3 Lokale reproductie
```bash
make services # start mysql + redis + mailpit + glitchtip
make api # start Laravel dev server
make app # start Vue dev server
```
Voor backend-only state-bugs: `php artisan tinker` om de specifieke
state te recreëren.
```php
// Voorbeeld: reproduceer een form-submission bug
$schema = \App\Models\FormBuilder\FormSchema::find('01HX...');
$submission = \App\Models\FormBuilder\FormSubmission::find('01HY...');
event(new \App\Events\FormBuilder\FormSubmissionSubmitted($submission));
```
Voor frontend-state-bugs: gebruik de Vue DevTools om Pinia store-state
te dump'en, recreëer de state via `useAuthStore().$patch({...})` in de
console.
### 3.4 Breadcrumbs
GlitchTip's breadcrumbs tonen de event-historie vóór de exception.
Frontend: route-changes, fetch-calls, console-warnings (input-text
masked). Backend: query-events (sentry-laravel built-in), job-dispatch.
Het patroon "user navigated → fetch failed → exception" is vaak
genoeg om de bug te lokaliseren zonder lokale repro.
---
## §4 Common patronen
### 4.1 "ValidationException doorbreekt naar GlitchTip"
Mag niet gebeuren per RFC §3.10. `ignore_exceptions` in
`config/sentry.php` zou ValidationException moeten filteren. Check:
```bash
grep -A 6 "ignore_exceptions" api/config/sentry.php
```
Als de class ontbreekt: regression — voeg toe en commit. Als de class
er staat: misschien een subclass die niet matcht? `ignore_exceptions`
matcht via `is_a($exception, $class)` dus subclasses werken normaal,
maar verifieer.
### 4.2 "Same exception fingerprint, multiple events"
GlitchTip dedupliceert via stack-trace-fingerprint. Honderd events
binnen een minuut met dezelfde fingerprint = runaway error. Mogelijke
oorzaken:
- Cron-job die elke minuut faalt → check supervisord / cron-logs op
productie-host.
- Queue-worker die een job retry'd zonder backoff → check `queue.attempt`
tag op de events; als die monoton stijgt, is retry de oorzaak.
- Frontend infinite-loop in een Vue component (bijv. setup() throws
in een loop) → check `release` tag, mogelijk net-deployed code.
### 4.3 "actor_scope=organisation maar organisation_id ontbreekt"
Multi-tenant invariant violation. Per [ARCH §3.3](../ARCH-OBSERVABILITY.md):
wanneer `actor_scope=organisation`, MOET `organisation_id` aanwezig
zijn als valide ULID. Zo niet: file-level bug in
`AuthScopeContextListener::resolveTenantContext()`. P1 — fix in current
sprint en breidt
`AuthScopeContextListenerTest::test_organisation_id_present_when_actor_scope_is_organisation`
uit met de specifieke trigger-route.
### 4.4 "Frontend events zonder user_id terwijl user is ingelogd"
Twee mogelijke oorzaken:
- `actor_scope=portal` — verwacht gedrag voor token-based portal flows.
Geen bug.
- Pinia auth-store niet geïnitialiseerd op het moment van capture.
Vaak na hard-refresh op een protected route waar `useAuthStore` nog
niet `setUser()` heeft gehad. Check `useAuthStore().isInitialized`.
### 4.5 "Backend events zonder organisation_id op een org-scoped route"
Per ARCH §4.1: auth-listener fires bij `Authenticated` /
`TokenAuthenticated`, route-context komt uit `BindSentryRouteContext`.
Als de listener vóór route-binding draait, kan organisation_id
ontbreken. Realistisch: dit is geen probleem in de huidige
implementatie omdat `request()` binnen de listener al de gebonden
route ziet — maar als een refactor het pattern breekt, vangt
`AuthScopeBindingHttpFlowTest` het.
### 4.6 "GlitchTip krijgt geen events meer (zwarte stilte)"
Frontend: check CSP-violation in DevTools Console (zie
[ARCH §7](../ARCH-OBSERVABILITY.md)). Bij nieuwe environment / domein
moet de ingest-host opnieuw whitelisted worden.
Backend: check `Integration::handles($exceptions)` in
`api/bootstrap/app.php`. Per [BACKLOG OBS-6](../BACKLOG.md) is dit een
bekende silent-failure-mode bij sentry-laravel install. Test:
```bash
cd api && php artisan tinker
\Sentry\captureMessage('triage smoke test')
```
Refresh GlitchTip Issues; als er geen event verschijnt, zit het
probleem in de SDK-config zelf (DSN, network) niet in
`Integration::handles`.
---
## §5 Resolutie
### 5.1 Mark as resolved
Na fix-deploy: open de issue in GlitchTip, klik **Resolve**.
GlitchTip's auto-resolve-on-version werkt niet altijd betrouwbaar in
zelf-hosted setups; doe het handmatig zodra je de deploy ziet draaien
op de prod-host.
Als de issue terugkomt op een latere `release` tag: GlitchTip markeert
'm automatisch als **regressed**. Behandel als nieuwe P1 (de bug die
je dacht gefixed te hebben is niet geheel weg).
### 5.2 Mark as ignored
Voor P2/P3 issues die niet in deze sprint worden gefixt: klik
**Ignore** met een comment die wijst naar de BACKLOG-entry of een
GitHub-issue. Voorkomt dat dezelfde issue opnieuw triage-aandacht
krijgt.
### 5.3 BACKLOG update
Wanneer de root cause een architectural pattern aanpassing vereist
(geen quick fix), maak een nieuwe BACKLOG entry met `OBS-` prefix
en cross-link naar de GlitchTip issue. Volg de format van bestaande
[OBS-* entries](../BACKLOG.md).
---
## §6 Audit trail
Elke triage-actie laat sporen na:
- **GlitchTip:** issue-state changes (resolved / ignored / regressed)
met user (Bert) en timestamp.
- **Git:** fix-commits met issue-link in de body indien van
toepassing.
- **BACKLOG.md:** entries voor architectural follow-ups.
- **`runbooks/observability-erasure.md`** als de triage in een
GDPR-deletion-verzoek omdraait.
Voor incidents waar 1+ klant geraakt is, log een externe
incident-summary (klant, impact, timeline, fix) in een aparte
incident-tracker — niet in GlitchTip zelf, want GlitchTip data wordt
na 90 dagen gepurged.