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

10 KiB

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 (architectuur en tag-taxonomie), GLITCHTIP.md (operationele stack — boot, backup, restore), 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:

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). Open de exception, klik op het top frame, lees de regel.

3.2 Cross-correlate met laravel.log

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

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.

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

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: 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). 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 is dit een bekende silent-failure-mode bij sentry-laravel install. Test:

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.


§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.