Per RFC-WS-6 §Q3 v1.3 + ARCH-BINDINGS §11. Nieuwe runbook-sectie §7 (na §6 Audit trail) die de triage-flow documenteert wanneer GlitchTip een FormBindingApplicatorException event opbrengt: - §7.1 failure_response_code classificatie (schema_config_error / temporary_error / data_integrity_error / unknown_error) drijft het initiële triage-pad - §7.2 form_schema.has_public_token tag onderscheidt klant-zichtbare failures (alert-waardig) van organizer-driven failures (admin-UI only) - §7.3 retry/dismiss decision-matrix met form-failures:retry artisan command + DismissalReasonType enum cases - §7.4 severe-failure escalatie criteria (>10/uur op één schema = P1) - §7.5 cross-references naar RFC, ARCH-BINDINGS, en erasure-runbook Companion van de operationele GlitchTip alert-rule (apart geconfigureerd in de GlitchTip web UI op monitoring.hausdesign.nl). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
16 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.attempttag 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
releasetag, 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
useAuthStorenog nietsetUser()heeft gehad. CheckuseAuthStore().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.mdals 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.
§7 Form-builder binding failures
Context
FormBindingApplicator draait synchroon binnen
ApplyBindingsOnFormSubmit::handle voor elke form-submission. Als die
throwt — schema-misconfiguratie, infra-issue, data-integrity violation
of deadline-timeout — vangt de listener de throw op, schrijft een
form_submission_action_failures rij in de outer-transaction, en zet
apply_status='failed' op de parent-submission. GlitchTip vangt het
exception-event op met de crewli-api project-tags.
De alert-rule op apply_status=failed AND form_schema.has_public_token=true
brengt alleen de klant-zichtbare failures naar boven — organizer-driven
private-form failures zijn zichtbaar in de Form Failures admin-UI zonder
alert, omdat hun blast radius beperkt is tot één organisatie.
Per RFC-WS-6.md §Q3 v1.3 + ARCH-BINDINGS.md §6 (two-transaction pattern) en §11 (failures lifecycle).
7.1 Eerste check — failure_response_code
De submission-rij draagt een denormalised classification-token in
failure_response_code. Vier waardes:
| Token | Oorzaak | Triage-pad |
|---|---|---|
schema_config_error |
Organizer-config issue (renamed kolom, deleted target-entity, ontbrekende identity-key binding die publish-guards toch doorlieten) | Contact de organizer van het schema; NIET retryen — retry produceert dezelfde exception |
temporary_error |
Infra-issue (DB-connection, lock-for-update wait, deadline-wrapper timeout) | Retry via php artisan form-failures:retry --id={failure_ulid} of admin-UI; als een tweede retry óók faalt, escaleer als infra-incident |
data_integrity_error |
Data-shape violation (type-mismatch, FK-violation, soft-deleted target-entity) | Onderzoek de failing binding's target_entity + target_attribute in form_field_bindings; lost meestal op als een schema-config issue verkleed als data |
unknown_error |
Iets buiten de binding-applicator hiërarchie (raw \Throwable, IdentityMatchInvariantViolation) |
Triage via de GlitchTip exception-trace; dit is dev-investigation territory |
De exception-class op de action-failure rij is de canonical truth;
failure_response_code is de afgeleide classificatie die door de
response-renderer gebruikt wordt.
7.2 Tweede check — public-token presence
De GlitchTip event-tag form_schema.has_public_token onderscheidt:
| Tag-waarde | Betekenis | Severity |
|---|---|---|
true |
Public submission flow (vrijwilliger registratie-window, public form-fill) | Customer-impact bevestigd; mobiliseer binnen minuten tijdens active festival registratie |
false |
Organizer-driven private flow (managed crew roster, internal data import) | Blast radius beperkt tot één organisatie; onderzoek binnen uren |
7.3 Retry vs dismiss
Gebruik het artisan-command:
# Retry een enkele failure
php artisan form-failures:retry --id={failure_ulid}
# Retry alle open failures voor een submission
php artisan form-failures:retry --submission={submission_ulid}
# Dry-run eerst als onzeker
php artisan form-failures:retry --id={failure_ulid} --dry-run
Wanneer wel retryen:
temporary_error— ja, bijna altijd; de deadline-wrapper of infra-hiccup is waarschijnlijk inmiddels weg.unknown_error— alleen na dev-investigatie die bevestigt dat de throw transient was.
Wanneer dismissen:
schema_config_errornadat de organizer het schema heeft gefixt en opnieuw heeft gesubmit — de originele failure-rij blijft staan als audit, dismiss met redenschema_deletedofbinding_removedper het relevante geval.data_integrity_errornadat dev-investigatie het herleidt tot een one-off data-state die niet meer geldt — dismiss metdata_quality_issue.- Iedere failure voor een submission die de organizer expliciet als
duplicaat geclassificeerd heeft — dismiss met
duplicate_submission.
De DismissalReasonType enum heeft zes cases; OTHER vereist een
free-text note. Per ARCH-BINDINGS §11.3.
7.4 Severe-failure escalatie
Als GlitchTip >10 events in 1 uur toont gescoped op één
form_schema_id, behandel als P1:
- Acknowledge de alert in het team-channel.
- Identificeer het schema via
form_submission.form_schema_idop een willekeurige failed submission. - Pauzeer publieke toegang tot het schema tijdelijk — disable het public token tot de root-cause geïdentificeerd is.
- Standaard incident-triage geldt verder.
Dit patroon wijst bijna altijd op een publish-guard gap: een schema-configuratie die de guards hadden moeten weigeren is er toch doorheen geslipt, en elke submission ertegen produceert dezelfde exception.
7.5 Cross-references
RFC-WS-6.mdv1.3.1 — volledige architectuur, in het bijzonder §Q3 v1.3 additions.ARCH-BINDINGS.mdv1.2 — §6 two-transaction pattern, §11 failures lifecycle, §11.3 dismissal reasons.observability-erasure.md— Art. 17 procedure als triage in een GDPR-deletion-verzoek omdraait.- BACKLOG
TECH-CHANNEL-AUTH-ORG-ADMIN— bekende follow-up voor live-update channel-auth uitbreiding (org-admin scope).