PR-4 commit 3 — closure-bookkeeping nu de implementation-PRs en de twee runbooks gemerged zijn. - RFC-WS-7-OBSERVABILITY.md: nieuwe §9 Implementation status (mei 2026) vat samen welke acceptance criteria via PR-1..PR-4 zijn voldaan en welke (1, 2, 7, 9, 10) op Bert's deploy-checklist resteren. Pointer naar ARCH-OBSERVABILITY.md als levende reference; de RFC blijft historisch document. - SECURITY_AUDIT.md: nieuwe sectie 'WS-7 Observability — finale audit (mei 2026)' tussen A13-10 en Positive Findings. Bevat (1) acceptance criteria checklist met status per criterium, (2) processing register entry voor GlitchTip (controller-not-processor, retention 90 dagen, TLS+full-disk-encryption+2FA), (3) zeven security controls die WS-7 introduceert (PII scrubbing, CSP whitelist, sourcemap upload-only, listener registration discipline, runtime portal-context-split, multi-tenant tag invariant, impersonation.active binary signal), (4) pointer naar runbooks/observability-erasure.md voor Art. 17. - BACKLOG.md: status-overzicht-tabel boven de OBS-entries. Toegevoegd als entry: OBS-2 (early-pipeline log context, ✅ Resolved), OBS-3 (sentry-context middleware coverage, ✅ Resolved — opgevouwen in AuthScopeContextListener), OBS-5 (Crewli render handlers report() invariant, ✅ Resolved via48f2a00+ ExceptionReportingTest), en OBS-9 (Active — staging environment GlitchTip CSP whitelist follow-up bij staging-introductie). Bestaande OBS-1, 4, 6, 7 ongewijzigd (Active); OBS-8 staat al op Resolved sindsdee1401. - .claude-sync.conf: drie nieuwe doc-paths toegevoegd (ARCH-OBSERVABILITY.md, runbooks/observability-triage.md, runbooks/observability-erasure.md). Post-commit sync-claude-docs hook regenereert SYNC_MANIFEST.md met deze entries. Closes WS-7 documentation acceptance criteria 8 (ARCH) en 14 (SECURITY_AUDIT). Resterende criteria (1, 2, 7, 9, 10) zijn deploy-checklist door Bert. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
19 KiB
RFC-WS-7 — Observability foundation (GlitchTip)
Status: Approved — implementation-ready
Workstream: WS-7 (consolidatie-sprint mei 2026)
Voorgangers: ARCH-CONSOLIDATION-2026-04 §3 besluit 8, §6.7 — ARCH-CONSOLIDATION-ADDENDUM-2026-04-24 D-06
Opvolgers: ARCH-API-VALIDATION (geblokkeerd door WS-7), WS-8b (ARCH-OBSERVABILITY.md)
Doorlooptijd: 4-5 dagen (charter zei 2-3; revisie wegens PII-scrubbing scope)
1. Doel
Crewli draait in productie zonder geautomatiseerde error-detectie. Stack traces alleen via SSH+grep; frontend errors onzichtbaar tenzij gerapporteerd; geen release-correlatie of alerting. WS-7 levert die foundation.
2. Charter-amendementen
Twee afwijkingen van charter §3 besluit 8, beide bewust:
A. Sentry → GlitchTip. Self-hosted = data binnen perimeter (Crewli verwerkt special category data: dietary, medical, accreditation passport — geen externe processor toevoegen aan het processing register). GlitchTip implementeert het Sentry-event-protocol; sentry-laravel, @sentry/vue, @sentry/cli werken zonder modificatie. Switchen naar Sentry SaaS later is mogelijk zonder app-wijzigingen.
B. Performance monitoring uit scope. Charter zei "Sentry Performance in prod". Deze RFC scope = errors-only. Performance-tracing pakken we later op wanneer er een concrete vraag ontstaat.
3. Architectuur
3.1 Hosting
Self-hosted GlitchTip op productie VPS via Docker Compose (glitchtip-web, glitchtip-worker, glitchtip-postgres, glitchtip-redis). Reverse proxy via DirectAdmin Apache; SSL via DirectAdmin Let's Encrypt op monitoring.hausdesign.nl (consistent met bestaande subdomain-pattern).
Lokale ontwikkeling: dezelfde docker-compose.glitchtip.yml draait lokaal als make services (gecombineerd met de bestaande docker-compose.yml via -f). Web-UI op http://localhost:8200, e-mail naar Mailpit op bm_mailpit:1025. Dev-stack en prod-stack delen één compose-file zodat configuratie-drift uitgesloten is.
3.2 Twee projecten / DSNs
crewli-api— Laravelcrewli-app— apps/app SPA (single-SPA na WS-3)
Twee aparte projecten omdat issue-ownership (frontend vs backend) helder moet zijn én omdat scrubbing-rules per project verschillen.
Binnen crewli-app werken we met een runtime context-split via beforeSend-hook: routes onder /p/* (token-based, artist/supplier/press) krijgen strictere scrubbing en geen user-context. Organizer-routes en /platform/* krijgen volledige auth-context. Eén SDK, twee zones.
3.3 Environment gating
Lege DSN = SDK no-op (bevestigd voor zowel sentry-laravel als @sentry/vue). Geen runtime-check in app-code nodig.
# .env.development → DSNs leeg
# .env.staging → DSNs gevuld, GLITCHTIP_ENVIRONMENT=staging
# .env.production → DSNs gevuld, GLITCHTIP_ENVIRONMENT=production
3.4 Release identifier
Format <app>@<short-sha> (crewli-api@f41951a, crewli-app@f41951a). Bron: git rev-parse --short HEAD als build-time env var, geïnjecteerd in deploy.sh per app-build.
3.5 Source maps
vite build produceert sourcemaps → @sentry/cli sourcemaps upload push naar GlitchTip → .map bestanden verwijderd uit dist/ vóór nginx ze serveert. Geen public-mapped sources op productie. Per-project upload-only auth-token in deploy environment.
3.6 Context tagging
Tag-binding gebeurt op twee plekken: route-scope tags via BindSentryRouteContext middleware (op de api-group), auth-scope tags via AuthScopeContextListener op Illuminate\Auth\Events\Authenticated. De split volgt de data-bron: route-context is alleen tijdens HTTP-handling beschikbaar, auth-context wordt door elke authenticator (Sanctum, portal-token) ge-emit via het Authenticated event.
| Tag | API | apps/app | Bron / locatie |
|---|---|---|---|
release |
altijd | altijd | env, sentry-laravel built-in |
environment |
altijd | altijd | env, sentry-laravel built-in |
app |
api |
app |
route-middleware |
route_name |
altijd | altijd | route-middleware |
http.method |
altijd | n.v.t. | route-middleware |
actor_scope |
organisation/platform/user/anonymous |
mirror | auth-listener (zie hieronder) |
organisation_id (ULID) |
aanwezig wanneer actor_scope = organisation |
uit auth store | auth-listener |
event_id (ULID) |
wanneer event-scoped | wanneer applicabel | auth-listener (via {event} route-param) |
user_id (ULID) |
wanneer authenticated | uit auth store, alleen session-mode | auth-listener |
actor_type |
organizer_admin / super_admin / portal_token / org_member / unauthenticated |
mirror | auth-listener |
impersonation.active |
bool | n.v.t. | HandleImpersonation middleware (post-swap) |
impersonation.impersonator_user_id |
wanneer actief | n.v.t. | HandleImpersonation middleware |
impersonation.session_id |
wanneer actief | n.v.t. | HandleImpersonation middleware |
queue.attempt |
binnen job-context | n.v.t. | TagJobAttemptOnSentry listener |
Nooit als tag: email, telefoon, naam, IP-adres, raw form_value content, raw cookie content.
Multi-tenant invariant (verfijnd na PR-2 live smoke test):
actor_scope is altijd aanwezig op authenticated events. Wanneer actor_scope = organisation, MOET organisation_id aanwezig en valide ULID zijn. Wanneer actor_scope = platform, IS organisation_id afwezig — dat is correct gedrag voor super_admin platform-routes (geforceerde org-attribution zou misleidend zijn). Wanneer actor_scope = user (default authenticated zonder org-route-context), is organisation_id ook afwezig: Crewli's User↔Organisation is many-to-many, een single-org "current org" bestaat niet op user-niveau, en attribution aan een willekeurige org zou misleiden. Een unit-test in AuthScopeContextListenerTest::test_organisation_id_present_when_actor_scope_is_organisation verifieert deze invariant.
actor_scope-waarden:
| Waarde | Wanneer | Filtering use-case in GlitchTip |
|---|---|---|
organisation |
route met {organisation} of {event} param, of portal-token request | "Issues voor organisatie X" |
platform |
super_admin op admin.* named routes |
"Platform-bugs (niet org-specifiek)" |
user |
authenticated user op routes zonder org-scope (/me/*, /portal/my-shifts, /uploads/* etc.) |
"Issues op user-routes; geen org-attribution" |
anonymous |
unauthenticated requests | "Public-route issues" |
Wijziging t.o.v. originele RFC: de oorspronkelijke formulering "élke captured event uit een geauthenticeerde controller MOET organisation_id hebben" is verfijnd na bevinding dat super_admin platform-routes geen zinvolle org-context hebben en Crewli's many-to-many user-org model geen reliable single-org hint biedt. Het invariant is nu sterker: niet "altijd aanwezig" maar "altijd correct gerelateerd aan actor_scope."
3.7 PII scrubbing
Backend (before_send in sentry-laravel):
- Request body keys (recursief, key-name match):
password,password_confirmation,current_password,token,api_key,secret,webhook_secret,dsn,signature,authorization,cookie,bearer,iban,bic,passport_number,bsn. - Headers:
Authorization,Cookie,Set-Cookie,X-API-Key,X-Impersonation-Token. - Form submissions: élke payload met
form_values→ recursive value-replace met[scrubbed]. Form values zijn definitionally PII. - URL query string: scrub
?token=...,?api_key=.... send_default_pii = falseglobaal (strips locals uit stack traces).
Frontend (beforeSend in @sentry/vue):
- Cookies via
document.cookieexposure: scrub. - localStorage / sessionStorage: nooit in event context (portal-state in sessionStorage MAG NIET lekken).
- Mask-all op input breadcrumbs. Sentry default maskt alleen passwords; wij masken alle
<input>values via breadcrumb-integratie config. Crewli heeft email/phone/IBAN/dietary/medical als plain<input>— selectief masken is niet veilig genoeg. - URL query string: zelfde patronen.
- Console messages:
Sentry.consoleLoggingIntegrationuitgeschakeld in productie.
Verificatie — verplicht in CI:
tests/Feature/Observability/PiiScrubbingTest.php— mock transport, assert dat sensitive keys gescrubd zijn.apps/app/src/__tests__/sentry-scrub.test.ts— assert form-input breadcrumb-scrubbing en/p/*route context-strip.
Tests blokkeren merge als scrubbing regresseert. Geen "manuele inspectie" als verificatie.
3.8 User context
Sentry::setUser([...]): id = ULID, username = ULID (duplicate, nooit email), ip_address = null. send_default_pii = false.
3.9 Sampling
Errors 100% (self-hosted, geen kostenreden). Performance/Profile niet van toepassing (uit scope per §2 amendement B). Wel een rate-limit op event-volume per project (GlitchTip default 10k/min/project) tegen runaway-errors.
3.10 Boundary met bestaande systemen
GlitchTip captureert programmer errors en infrastructure failures. Het captureert niet verwachte business-uitkomsten (failed payment, failed identity match, dead-letter webhook delivery — die hebben eigen audit-tabellen). Bestaande systemen blijven onaangeroerd: Telescope (dev), activity_log (audit trail), impersonation_audit_logs (security audit), form_webhook_deliveries (delivery audit), Laravel default log (operationeel).
Concreet voor webhooks: form_webhook_deliveries.dead_letter is de audit-record. GlitchTip vuurt alleen bij programmeerfouten (TypeError, missing config), niet wanneer receiver een 500 retourneert.
3.11 Queue-worker integratie
Laravel SDK auto-captureert vanuit queue jobs. Job-attempt-number als tag (queue.attempt) voor "issues die uiteindelijk slagen" vs "altijd falend". Stack-trace-grouping (default) is correct gedrag voor idempotente retries — niet per attempt fingerprinten.
3.12 Alerting
Initieel: email-naar-Bert op nieuwe issue / regression / spike (>10 events in 5min, tune na eerste week). Slack-integratie naar BACKLOG.
3.13 Structured logging conventie (charter §6.7)
BindRequestLogContext middleware op de api route group:
Log::withContext([
'request_id' => $request->header('X-Request-Id') ?? Str::ulid(),
'organisation_id' => $request->user()?->organisation_id,
'user_id' => $request->user()?->id,
'route' => $request->route()?->getName(),
]);
request_id retour als response header X-Request-Id. apps/app axios interceptor genereert client-side een ULID per request en stuurt mee als header; server respecteert headerwaarde. Dit geeft front-to-back correlation in één klik vanuit GlitchTip-UI naar log-segment op de host.
3.14 Activity_log indexes (addendum D-06)
(subject_type, subject_id) en (causer_type, causer_id) composite indexes op activity_log. Infrastructure-housekeeping; geen functionele wijziging.
Status (mei 2026, na PR-2): Bij implementatie bleek dat de Spatie activitylog default-migratie via nullableMorphs('subject') en nullableMorphs('causer') deze composite indexes al aanmaakt (subject op (subject_type, subject_id), causer op (causer_type, causer_id)). Geen aparte migratie nodig — geverifieerd via information_schema.STATISTICS. Acceptance criterium 12 daarmee al voldaan vóór WS-7 begon. Regression-guard: tests/Feature/Database/ActivityLogIndexesTest.php faalt wanneer een toekomstige refactor deze indexes verwijdert.
4. Privacy / GDPR
Data na scrubbing: alleen ULIDs, stack traces, route-namen, gecureerde tags, request_id. Scrubbing per §3.7 is de primaire control; tests in §3.7 zijn merge-blockers.
Controller / processor: Self-hosted op Crewli-infra → Crewli is controller, niet processor. Geen DPA-uitbreiding.
Right to erasure (Art. 17): Zoek events op user.id = <ULID>, verwijder via GlitchTip-API of directe SQL op glitchtip-postgres. Initieel handmatig; geautomatiseerd erasure-script naar BACKLOG.
Retention: 90 dagen, daarna purged.
Processing register-entry: Crewli Error Tracking (GlitchTip) — defectdetectie en service-availability monitoring — pseudonieme identifiers + technische metadata — recipient: Bert — retention 90 dagen — TLS in transit, full-disk encryption at rest, SSH-key + 2FA op web-UI.
SECURITY_AUDIT.md update in PR-4: GlitchTip als processing entry, scrubbing-config als security control, erasure-procedure als GDPR-runbook.
5. Vastgelegde defaults
Geen open vragen meer; deze zijn vastgesteld:
- DirectAdmin Let's Encrypt voor
monitoring.hausdesign.nl(consistent met bestaande subdomains). - Retention: 90 dagen.
- Email-alerting initieel; Slack naar BACKLOG.
- Mask-all op frontend input breadcrumbs.
- 2FA verplicht op GlitchTip web-UI.
- Daily postgres-backup naar zelfde target als Crewli main-DB.
- Capture failures-to-write-activity-log (silent failures zijn slecht).
- Impersonation: zowel impersonator als target user_id getagd (per §3.6 tabel).
- Big-bang structured logging:
BindRequestLogContextmiddleware in PR-2, geen call-site-wijzigingen. - Mock transport voor scrubbing-tests (geen externe afhankelijkheid in CI).
- Source-map upload-token in deploy
.envop productie-host (later naar 1Password wanneer CI komt).
6. Acceptance criteria
WS-7 is compleet wanneer:
- GlitchTip draait op
monitoring.hausdesign.nlmet TLS, alleen toegankelijk voor Bert (2FA aan). - Twee projecten aangemaakt; DSNs in vault.
- Laravel SDK geïntegreerd; errors uit prod-API verschijnen <60s.
- ✅ apps/app SDK geïntegreerd (PR-3); errors verschijnen met org/user/release context. Token-based portal-routes (
/portal/advance/:token,/register/:public_token) hebben strictere scrubbing en geen user-context. Detectie viaroute.meta.public === true && route.meta.context === 'portal'. Implementatie:apps/app/src/observability/contextBinding.ts. - ✅ Source-maps upload werkt (PR-3);
deploy.shexporteertVITE_SENTRY_RELEASEbuild-time, uploadt via@sentry/clinavite build, en verwijdert daarna élke*.mapuitdist/(RFC §3.5: no public-mapped sources). Soft-fail: deploy gaat door als upload faalt, maar de map-strip stap loopt altijd. - ✅ PII scrubbing-tests groen (PR-2 backend
PiiScrubbingTest20 cases; PR-3 frontendscrubber.spec.ts18 cases mirror). Plus structurele coverage inEventListenerRegistrationTest+AuthScopeContextListenerTest+AuthScopeBindingHttpFlowTest. - Smoke test: induced 500 in staging, verifieer dat hij verschijnt met alle verwachte tags én geen PII lekt.
- ARCH-OBSERVABILITY.md geschreven (WS-8b).
- Email-alerting geconfigureerd; getest met sample issue.
- Retention-policy (90 dagen) toegepast.
- Daily postgres-backup-script in place.
Activity_log indexes (addendum D-06) gemigreerd.✓ — al voldaan door Spatie'snullableMorphsdefault in de originele activitylog migratie; zie §3.14 status-note. Regression-guard:tests/Feature/Database/ActivityLogIndexesTest.php.- Structured logging conventie geïmplementeerd;
X-Request-Idround-trip getest. - SECURITY_AUDIT.md bijgewerkt.
Voortgang (mei 2026, na PR-3):
- PR-1 ✅: criteria 1, 2, 11 — infra + projecten + backup-script.
- PR-2 ✅: criteria 3, 6 (backend), 12, 13 — sentry-laravel + scrubber + structured logging + listener-registration discipline (OBS-8).
- PR-3 ✅: criteria 4, 5, 6 (frontend) —
@sentry/vueSDK + scrubber + Vue Router context-binding + sourcemap upload indeploy.sh. - Resterend voor WS-7-closure (PR-4): criteria 7 (live smoke staging), 8 (ARCH-OBSERVABILITY.md), 9 (email-alerting), 10 (retention 90d), 14 (SECURITY_AUDIT.md update).
7. Deliverables (4 PRs, --no-ff per CLAUDE.md)
| PR | Inhoud |
|---|---|
| PR-1: Infra | docker-compose.glitchtip.yml, monitoring.hausdesign.nl DNS + TLS, twee projecten aangemaakt, DSNs in vault, daily-backup script |
| PR-2: Backend SDK + structured logging | sentry-laravel install + config + scrubbing + context tagging, BindRequestLogContext middleware, X-Request-Id round-trip, PII scrubbing test, activity_log indexes (D-06) |
| PR-3: Frontend SDK | @sentry/vue install + config + context tagging + scrubbing test + /p/* runtime-split + sourcemap upload-step in deploy.sh |
| PR-4: Docs + WS-8b | ARCH-OBSERVABILITY.md, runbook (triage + erasure + scrubbing-tuning), SECURITY_AUDIT.md update |
WS-7 closure = alle 4 PRs gemerged + acceptance criteria 1-14 afgevinkt.
8. Verwijzingen
- ARCH-CONSOLIDATION-2026-04 §3 besluit 8 — originele Sentry-keuze (deze RFC wijzigt naar GlitchTip).
- ARCH-CONSOLIDATION-2026-04 §6.7 — originele scope WS-7.
- ARCH-CONSOLIDATION-ADDENDUM-2026-04-24 D-06 — activity_log indexes.
- AUTH_ARCHITECTURE.md — per-app cookie naming, impersonation flow.
- SECURITY_AUDIT.md — bestaande audit-posture (te updaten in PR-4).
- BACKLOG.md — entries voor automated-erasure script, Slack alerting (post-WS-7).
- GlitchTip docs: https://glitchtip.com/documentation
- GlitchTip self-hosting: https://glitchtip.com/documentation/install
9. Implementation status (mei 2026)
WS-7 implementation is voltooid. Vier PRs gemerged in feat/ws-7-observability:
- PR-1 (Infra): GlitchTip Docker stack, lokale + productie compose, daily-backup script,
GLITCHTIP.mdrunbook. - PR-2 (Backend SDK): sentry-laravel + scrubber + structured logging +
BindSentryRouteContext+AuthScopeContextListener+ tenant resolution + impersonation discipline + listener registration discipline +ExceptionReportingTest+ActivityLogIndexesTest. - PR-3 (Frontend SDK):
@sentry/vue+ scrubber + Vue Router context-binding + sourcemap upload + CSPconnect-srcwhitelist. - PR-4 (Docs + WS-8b):
ARCH-OBSERVABILITY.md+ observability runbooks +SECURITY_AUDIT.mdupdate +BACKLOG.mdcleanup.
Code-implementation acceptance criteria voldaan: 3, 4, 5, 6, 11, 12, 13.
Documentatie acceptance criteria voldaan: 8, 14.
Resterende criteria — handmatige deploy-stappen door Bert:
- 1: GlitchTip op
monitoring.hausdesign.nlmet TLS + 2FA - 2: Twee projecten + DSNs in 1Password vault
- 7: Smoke test induced 500 in staging-omgeving
- 9: Email-alerting geconfigureerd + getest
- 10: Retention-policy 90 dagen toegepast in GlitchTip admin
Deze stappen zijn deel van WS-7 closure-checklist (door Bert handmatig uit te voeren), niet van toekomstige PRs.
Volledige tag-taxonomie en implementation-details: zie ARCH-OBSERVABILITY.md (post-implementation reference). Deze RFC blijft historisch document; ARCH is de levende referentie.
Operationele procedures: zie runbooks/observability-triage.md (triage incoming issues) en runbooks/observability-erasure.md (GDPR Art. 17 procedure).