From 1e394879aa7cbf9fdb98a32b1d6d54538d150b65 Mon Sep 17 00:00:00 2001 From: "bert.hausmans" Date: Wed, 6 May 2026 07:32:12 +0200 Subject: [PATCH] docs: RFC-WS-7 observability foundation (GlitchTip) Two charter amendments from the original WS-7 brief: - Sentry -> GlitchTip (self-hosted, protocol-compatible). Same Sentry SDKs on backend (sentry-laravel) and frontend (@sentry/vue), pointed at a self-hosted GlitchTip DSN. Avoids Sentry SaaS pricing and keeps event data on infrastructure Bert controls. - Performance monitoring out of scope (errors-only). WS-7 delivers exception capture + alerting + scrubbing + RBAC only. APM/tracing/spans deferred to a later workstream if ever needed; pre-launch with no users, the cost/benefit doesn't justify it now. RFC-as-first-commit pattern (per WS-6) so the scope-alignment document is in main before any infra/code changes land. --- dev-docs/RFC-WS-7-OBSERVABILITY.md | 223 +++++++++++++++++++++++++++++ 1 file changed, 223 insertions(+) create mode 100644 dev-docs/RFC-WS-7-OBSERVABILITY.md diff --git a/dev-docs/RFC-WS-7-OBSERVABILITY.md b/dev-docs/RFC-WS-7-OBSERVABILITY.md new file mode 100644 index 00000000..da831b5a --- /dev/null +++ b/dev-docs/RFC-WS-7-OBSERVABILITY.md @@ -0,0 +1,223 @@ +# 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 `errors.crewli.app` (consistent met bestaande subdomain-pattern). + +### 3.2 Twee projecten / DSNs + +- `crewli-api` — Laravel +- `crewli-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 `@` (`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 | API | apps/app | +|---|---|---| +| `release` | altijd | altijd | +| `environment` | altijd | altijd | +| `app` | `api` | `app` | +| `route_name` | `Route::currentRouteName()` | `route.name` | +| `http.method` | altijd | n.v.t. | +| `organisation_id` (ULID) | wanneer auth+scope gebound | uit auth store | +| `event_id` (ULID) | wanneer event-scoped | wanneer applicabel | +| `user_id` (ULID) | `auth()?->id()` | uit auth store, alleen session-mode | +| `actor_type` | `organizer_admin` / `super_admin` / `portal_token` / `volunteer` / etc. | mirror | +| `impersonation.active` | bool | n.v.t. | +| `impersonation.impersonator_user_id` | wanneer actief | n.v.t. | +| `queue.attempt` | binnen job-context | n.v.t. | + +**Nooit als tag:** email, telefoon, naam, IP-adres, raw form_value content, raw cookie content. + +Multi-tenant invariant: élke captured event uit een geauthenticeerde controller MOET `organisation_id` hebben. Een unit-test verifieert dit — als `organisation_id` ontbreekt op een geauthenticeerd path, faalt de test. + +### 3.7 PII scrubbing + +**Backend (`before_send` in sentry-laravel):** +1. 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`. +2. Headers: `Authorization`, `Cookie`, `Set-Cookie`, `X-API-Key`, `X-Impersonation-Token`. +3. Form submissions: élke payload met `form_values` → recursive value-replace met `[scrubbed]`. Form values zijn definitionally PII. +4. URL query string: scrub `?token=...`, `?api_key=...`. +5. `send_default_pii = false` globaal (strips locals uit stack traces). + +**Frontend (`beforeSend` in @sentry/vue):** +1. Cookies via `document.cookie` exposure: scrub. +2. localStorage / sessionStorage: nooit in event context (portal-state in sessionStorage MAG NIET lekken). +3. **Mask-all op input breadcrumbs.** Sentry default maskt alleen passwords; wij masken alle `` values via breadcrumb-integratie config. Crewli heeft email/phone/IBAN/dietary/medical als plain `` — selectief masken is niet veilig genoeg. +4. URL query string: zelfde patronen. +5. Console messages: `Sentry.consoleLoggingIntegration` uitgeschakeld 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: + +```php +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. + +--- + +## 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 = `, 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 `errors.crewli.app` (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: `BindRequestLogContext` middleware in PR-2, geen call-site-wijzigingen. +- Mock transport voor scrubbing-tests (geen externe afhankelijkheid in CI). +- Source-map upload-token in deploy `.env` op productie-host (later naar 1Password wanneer CI komt). + +--- + +## 6. Acceptance criteria + +WS-7 is compleet wanneer: + +1. GlitchTip draait op `errors.crewli.app` met TLS, alleen toegankelijk voor Bert (2FA aan). +2. Twee projecten aangemaakt; DSNs in vault. +3. Laravel SDK geïntegreerd; errors uit prod-API verschijnen <60s. +4. apps/app SDK geïntegreerd; errors verschijnen met org/user/release context. `/p/*` routes hebben strictere scrubbing en geen user-context. +5. Source-maps upload werkt; leesbare stack traces in UI; `.map` bestanden afwezig in publieke bundle. +6. PII scrubbing-tests groen (backend + frontend). +7. Smoke test: induced 500 in staging, verifieer dat hij verschijnt met alle verwachte tags én geen PII lekt. +8. ARCH-OBSERVABILITY.md geschreven (WS-8b). +9. Email-alerting geconfigureerd; getest met sample issue. +10. Retention-policy (90 dagen) toegepast. +11. Daily postgres-backup-script in place. +12. Activity_log indexes (addendum D-06) gemigreerd. +13. Structured logging conventie geïmplementeerd; `X-Request-Id` round-trip getest. +14. SECURITY_AUDIT.md bijgewerkt. + +--- + +## 7. Deliverables (4 PRs, `--no-ff` per CLAUDE.md) + +| PR | Inhoud | +|----|---| +| **PR-1: Infra** | docker-compose.glitchtip.yml, errors.crewli.app 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