Files
crewli/dev-docs/ARCH-OBSERVABILITY.md
bert.hausmans 754222f74d docs: ARCH-OBSERVABILITY.md (WS-8b)
Replaces the WS-6 skeleton with a full post-implementation reference
for the observability stack. Eleven sections covering scope, component
overview, tag taxonomy (replacing RFC §3.6 as source-of-truth), tag
binding architecture, scrubbing semantics, runtime context split, CSP
whitelist, sourcemap upload, GDPR + privacy, maintenance + extension
guidance, plus cross-references.

Form Builder exception classification from the old skeleton §3 is
preserved in §5.4 — concrete answer for which Crewli exception
classes do or do not go to GlitchTip.

Lengte: 730 regels markdown. Closes WS-7 acceptance criterion 8.

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

37 KiB
Raw Permalink Blame History

ARCH — Observability (v1.0)

Source of truth for Crewli's observability implementation (sentry-laravel + @sentry/vue + GlitchTip). This document supersedes RFC-WS-7-OBSERVABILITY.md for tag taxonomy, binding semantics, and operational patterns. The RFC remains the historical implementation-spec; ARCH is the post-implementation reference for developers maintaining or extending the stack.

Status: WS-7 implementation complete. Code criteria 3, 4, 5, 6, 11, 12, 13 satisfied; documentation criteria 8, 14 satisfied via this ARCH plus the runbooks under runbooks/. Manual closure criteria (1, 2, 7, 9, 10) remain on Bert's checklist.

Version: 1.0 (initial post-implementation reference, mei 2026, after PR-1 → PR-4 landed in feat/ws-7-observability).

Pre-WS-7 skeleton: earlier versions of this document (v0.1, april 2026) carried placeholder sections for log levels, metrics, alerting and dashboards. Those decisions were taken during WS-7 and implemented as code; this document captures the as-built outcome. The historical skeleton sections about metrics (§5), alerting (§6), and dashboards (§7) are intentionally not carried forward — Crewli has settled on errors-only observability via GlitchTip; no Statsd / Prometheus / Grafana stack is planned (RFC §2 amendment B).


§1 Doel & scope

Observability in Crewli levert geautomatiseerde error-detection en service-availability monitoring. Stack traces, tags, breadcrumbs en release-correlation worden verzameld via GlitchTip, self-hosted op monitoring.hausdesign.nl. GlitchTip is binary-compatible met het Sentry event-protocol; we gebruiken sentry/sentry-laravel op de backend en @sentry/vue op de frontend ongewijzigd.

Wel in scope:

  • Programmer errors uit Laravel controllers en queue jobs (Throwable, RuntimeException, TypeError, QueryException etc.)
  • Infrastructure failures (database connection drop, redis unavailable, external HTTP timeouts in Crewli's eigen client-code)
  • Frontend runtime errors uit Vue componenten en composables
  • Unhandled promise rejections in de SPA
  • Vue Router navigatie-context als breadcrumbs

Niet in scope (per RFC §3.10):

  • Performance / tracing / profiling. Hard-pinned op 0.0 sample rate in zowel config/sentry.php als apps/app/src/observability/sentry.ts.
  • Verwachte business-uitkomsten. ValidationException, AuthenticationException, AuthorizationException, sub-500 HttpExceptions worden bewust niet gecaptured. Die hebben eigen audit-paden (form_submission_action_failures, activity_log).
  • Audit trails. activity_log, impersonation_audit_logs, form_webhook_deliveries blijven authoritative voor security en compliance audit. GlitchTip is voor defectdetectie.
  • Replay / Web Vitals / User Feedback. GlitchTip ondersteunt dit niet; we gebruiken het ook niet als roadmap-item.
  • Metrics / dashboards / alerting beyond GlitchTip's email. Geen Statsd / Prometheus / Grafana. Alerting initieel email-naar-Bert via GlitchTip's eigen rule-engine; Slack-integratie staat op BACKLOG.

Boundary met bestaande systemen:

Systeem Wat het doet Wat GlitchTip NIET overneemt
Telescope (/telescope) Dev-only debugging dashboard. Local + testing. GlitchTip is voor production-incidents; Telescope blijft voor lokale debug.
activity_log (Spatie) Audit trail van user-acties op tenant data. Authoritative voor "wie deed wat wanneer." GlitchTip captured nooit business-events zoals form.submitted.
form_webhook_deliveries Webhook delivery audit met retry / dead-letter. Bij dead-letter NIET via GlitchTip; alleen als de dispatcher zelf een programmer-error gooit.
form_submission_action_failures Apply-pipeline failures per submission, action, en organisatie. Org-admin operational handling via WS-6 admin UI. GlitchTip ziet runtime apply-pipeline exceptions (zie §5.4) parallel — engineering visibility, niet operational fix UI.
impersonation_audit_logs Wie impersoneerde wie wanneer (security audit). GlitchTip tagt actieve impersonation als context op gecaptured events; vervangt audit niet.
Laravel default log channel Operationele runtime logs (info/warning/error). Beide systemen krijgen dezelfde events; correlation via request_id.

§2 Componenten-overzicht

┌─────────────────────────────────────────────────────────────────────────┐
│                       Crewli production / dev                            │
│                                                                          │
│  ┌─────────────────────┐         ┌──────────────────────────┐            │
│  │  Laravel API        │         │  apps/app SPA (Vue 3)    │            │
│  │  (api.crewli.app)   │         │  (crewli.app)            │            │
│  │                     │         │                          │            │
│  │  ┌───────────────┐  │         │  ┌────────────────────┐  │            │
│  │  │sentry-laravel │  │         │  │@sentry/vue 10.x    │  │            │
│  │  │4.25 SDK       │  │         │  │                    │  │            │
│  │  └───────┬───────┘  │         │  └─────────┬──────────┘  │            │
│  │          │          │         │            │             │            │
│  │  ┌───────▼───────┐  │         │  ┌─────────▼──────────┐  │            │
│  │  │SentryEvent    │  │         │  │scrubEvent          │  │            │
│  │  │Scrubber (PHP) │  │         │  │(TypeScript)        │  │            │
│  │  └───────┬───────┘  │         │  └─────────┬──────────┘  │            │
│  │          │          │         │            │             │            │
│  └──────────┼──────────┘         └────────────┼─────────────┘            │
│             │                                 │                          │
│             │  HTTPS POST /api/<n>/envelope/  │                          │
│             └────────────┬────────────────────┘                          │
│                          ▼                                               │
│         ┌───────────────────────────────────────┐                        │
│         │      GlitchTip                        │                        │
│         │  monitoring.hausdesign.nl  (prod)     │                        │
│         │  localhost:8200            (dev)      │                        │
│         │                                       │                        │
│         │  ┌─────────┐  ┌─────────┐  ┌───────┐  │                        │
│         │  │ web     │  │ worker  │  │ pg/   │  │                        │
│         │  │ (django)│  │ (celery)│  │ redis │  │                        │
│         │  └─────────┘  └─────────┘  └───────┘  │                        │
│         └───────────────────────────────────────┘                        │
│                                                                          │
└──────────────────────────────────────────────────────────────────────────┘

Twee projecten in één GlitchTip-instance:

  • crewli-api — Laravel events (app=api tag)
  • crewli-app — SPA events (app=app tag)

DSN per project; beide keys liggen in 1Password vault onder Crewli / GlitchTip / DSNs. Backend leest SENTRY_DSN_BACKEND, frontend VITE_SENTRY_DSN_FRONTEND.

CSP connect-src whitelist voor de ingest-host is verplicht — zonder deze whitelist blokkeert de browser elke @sentry/vue-egress stilletjes. Zie §7.


§3 Tag-taxonomie

Deze tabel vervangt de tabel in RFC-WS-7-OBSERVABILITY.md §3.6 als source-of-truth. Wanneer een tag wordt toegevoegd of de bron-locatie wijzigt, wordt deze tabel bijgewerkt; de RFC blijft historisch document.

3.1 Backend tags (sentry-laravel)

Tag Locatie Always / conditional Bron
app initial scope (config/sentry.php) of BindSentryRouteContext always constant 'api'
release sentry-laravel built-in when SENTRY_RELEASE env set crewli-api@<short-sha> injected by deploy.sh
environment sentry-laravel built-in always APP_ENV
route_name BindSentryRouteContext middleware conditional (named routes only) $request->route()->getName()
http.method BindSentryRouteContext middleware always (HTTP requests) $request->method()
actor_scope AuthScopeContextListener always (authenticated events) resolution chain — see §3.3
actor_type AuthScopeContextListener always (authenticated events) ActorType::resolve() — see §3.4
user_id AuthScopeContextListener (overridden by HandleImpersonation) always (authenticated) ULID
username AuthScopeContextListener user object always (authenticated) ULID — RFC §3.8: never email
organisation_id AuthScopeContextListener conditional — only when actor_scope=organisation route param / portal-token resolution — see §3.3
event_id AuthScopeContextListener (via {event} route param) conditional route binding
impersonation.active AuthScopeContextListener baseline + HandleImpersonation override always (authenticated) binary 'true'/'false' — RFC §3.6 invariant
impersonation.impersonator_user_id HandleImpersonation middleware conditional — only when impersonating ULID
impersonation.session_id HandleImpersonation middleware conditional — only when impersonating ULID
queue.attempt TagJobAttemptOnSentry listener conditional — within queue jobs $event->job->attempts()

3.2 Frontend tags (@sentry/vue)

Tag Locatie Always / conditional Bron
app initial scope (sentry.ts) always constant 'app'
release sentry-vue built-in when VITE_SENTRY_RELEASE set crewli-app@<short-sha> injected by deploy.sh build-time
environment sentry-vue built-in always import.meta.env.MODE
route_name installContextBinding Vue Router guard always route.name ?? 'unnamed'
actor_scope installContextBinding always one of organisation / platform / user / portal / anonymous
actor_type installContextBinding always super_admin / organizer_admin / org_member / portal_token / unauthenticated
user_id installContextBinding conditional — never when actor_scope=portal useAuthStore().user.id
organisation_id installContextBinding conditional — only when actor_scope=organisation useOrganisationStore().activeOrganisationId

http.method is afwezig in de frontend-tabel; Vue Router-routes zijn page-level navigation events, niet HTTP-requests. De backend-tabel heeft http.method per request; de frontend laat die over aan fetch-breadcrumbs die @sentry/vue automatisch attached.

3.3 actor_scope resolution

Beide implementaties volgen dezelfde priority chain. De backend doet de resolution in AuthScopeContextListener::resolveTenantContext(), de frontend in contextBinding.ts::bindScope().

Priority Backend signaal Frontend signaal Resulterende actor_scope
1 {organisation} route-param route binding op /organisations/:id organisation
2 {event} route-param {event} route-param organisation (via event.organisation_id)
3 portal_event request attribute (set by PortalTokenMiddleware) route.meta.public === true && route.meta.context === 'portal' backend: organisation, frontend: portal
4 super_admin role + route name starts with admin. super_admin role + route.path starts with /platform platform (no organisation_id)
5 Authenticated, no org context Authenticated, no activeOrganisationId user (no organisation_id)
6 Unauthenticated Unauthenticated (backend: not bound; frontend: anonymous)

Belangrijke noot voor frontend portal-zone: actor_scope=portal kent geen user_id of username. RFC §3.7 frontend-block punt 5 expliciet — token-based flows (artist advance, public form fill) krijgen geen user-context omdat de identifier (ULID-token) zelf gevoelig is en de bezoeker niet permanent met Crewli is verbonden.

Multi-tenant invariant: wanneer actor_scope=organisation MOET organisation_id aanwezig zijn als valide ULID. Wanneer actor_scope=platform, user, of anonymous, IS organisation_id afwezig. Niet "altijd aanwezig" maar "altijd correct gerelateerd aan actor_scope." Geverifieerd in AuthScopeContextListenerTest::test_organisation_id_present_when_actor_scope_is_organisation.

3.4 actor_type enum

Backend: App\Enums\Observability\ActorType (PHP enum). Frontend: inline string-mapping in contextBinding.ts::resolveActorType(). Beide geven dezelfde waarden:

Waarde Wanneer
super_admin User has Spatie role super_admin
organizer_admin User has Spatie role org_admin
org_member Authenticated user, no admin role (covers volunteers — Crewli has no dedicated volunteer role today; see BACKLOG OBS-1)
portal_token Token-based portal request (portal_event attribute / route.meta.public + context=portal)
unauthenticated No auth (e.g. login page, public form fill)

§4 Tag-binding architectuur

Drie patronen die we bewust hebben gekozen tijdens WS-7 implementation; gedocumenteerd hier zodat toekomstige uitbreidingen consistent zijn.

4.1 Backend split — middleware × event-listener

Route-scope tags binden per HTTP request via middleware. Auth-scope tags binden per authenticatie-event via een listener. Reden: route-context bestaat alleen tijdens HTTP handling, auth-context wordt geëmit door élke authenticator (Laravel's SessionGuard, Sanctum's bearer-token Guard, toekomstige authenticators).

Concern Implementatie
Route-scope (app, route_name, http.method) App\Http\Middleware\BindSentryRouteContext — registered globally on the api group via $middleware->api(prepend: [...]) in bootstrap/app.php
Auth-scope (user_id, actor_type, actor_scope, organisation_id) App\Listeners\Observability\AuthScopeContextListener — listens to BOTH Illuminate\Auth\Events\Authenticated (SessionGuard) AND Laravel\Sanctum\Events\TokenAuthenticated (Sanctum)
Impersonation override + escalation App\Http\Middleware\HandleImpersonation — re-binds Sentry scope after the user-swap, sets impersonation.active='true' plus impersonator/session ids
Queue context (queue.attempt) App\Listeners\Observability\TagJobAttemptOnSentry — listens to Illuminate\Queue\Events\JobProcessing

Waarom dual-event listener? Crewli's HTTP-flow is bearer-token via CookieBearerToken middleware → auth:sanctum → Sanctum's Guard fires only TokenAuthenticated, NOT Authenticated. Listening only to the Authenticated event would silently miss every authenticated HTTP request. Discovered by the live smoke test that PR-3 follow-up fixed (commit adab3be).

4.2 Frontend split — Vue Router guard + Pinia store reads

Vue heeft geen Sentry-equivalent van Laravel's Authenticated event; de natuurlijke tag-binding momenten zijn route-transitions. Een router.beforeEach guard in apps/app/src/observability/contextBinding.ts:

  1. Roept Sentry.getCurrentScope().clear() aan op elke navigatie. Voorkomt cross-zone leakage (e.g. user logt uit in portal-zone maar Sentry houdt user_id van de organizer-context vast).
  2. Leest useAuthStore() en useOrganisationStore() voor identity en tenant-context.
  3. Past dezelfde resolution chain toe als de backend (§3.3).

4.3 Default-in-listener / override-in-middleware pattern

Voor binary tags die altijd aanwezig moeten zijn maar door specifieke middleware-stappen worden geëscaleerd, gebruiken we een twee-fase pattern:

AuthScopeContextListener::bindForUser()   →  scope.setTag('impersonation.active', 'false')
                                          ↓
HandleImpersonation::handle()             →  scope.setTag('impersonation.active', 'true')
                                              scope.setTag('impersonation.impersonator_user_id', $admin->id)
                                              scope.setTag('impersonation.session_id', $session->id)

De listener seedt altijd een baseline ('false'). Wanneer impersonation actief is, draait HandleImpersonation ná auth en overschrijft de scope met de target user en de escalation-tags. Als toekomstige refactors per actor_scope branch shortcuts maken die de baseline overslaan, vangt AuthScopeContextListenerTest::test_impersonation_active_default_false_across_every_actor_scope_branch de regressie.

Dit pattern is herbruikbaar voor andere binary signals; tot nu toe alleen toegepast op impersonation.active.

4.4 Listener registration discipline

Laravel 12's listener auto-discovery is uitgeschakeld in bootstrap/app.php via ->withEvents(discover: false). Reden: auto-discovery + explicit Event::listen() veroorzaakt silent double-registration (vandaag idempotent door scope-tag overwrite semantics, morgen niet meer wanneer een listener additive operations doet). Gevangen door tests/Feature/Observability/EventListenerRegistrationTest.

Voor élke nieuwe observability-listener:

  1. Maak listener-class in app/Listeners/Observability/.
  2. Registreer expliciet in AppServiceProvider::boot() met array-callable form [Class::class, 'method']. Class-string vorm verbergt method-binding in php artisan event:list.
  3. Voeg een case toe aan EventListenerRegistrationTest::test_*_listener_registered_exactly_once met de juiste event-class + method-naam.

§5 Scrubbing semantics

5.1 Backend — App\Services\Observability\SentryEventScrubber

Geregistreerd als before_send hook in config/sentry.php via array-callable static-method notation. Stateless; geen container-resolution per event.

Wat wordt gescrubt:

  1. Request body keys (recursief, key-name match, depth-limited): password, password_confirmation, current_password, token, api_key, secret, webhook_secret, dsn, signature, authorization, cookie, bearer, iban, bic, passport_number, bsn. Replace value met [scrubbed].

  2. Request headers (case-insensitive): authorization, cookie, set-cookie, x-api-key, x-impersonation-token. Replace met [scrubbed].

  3. Form submissions: élke payload-key form_values wordt wholesale replaced met [scrubbed_form_values]. Reden: Crewli's form-builder genereert dynamische form-values waar elke key PII kan zijn (email, telefoon, dietary, medical). Selectief op key matchen is niet veilig.

  4. URL query string: token=, api_key= worden gescrubt.

  5. Cookies wholesale: event.request.cookies wordt vervangen door [scrubbed].

  6. Max-depth guard op recursie: na 10 levels wordt subtree replaced met ['[max_depth]'] om malicious deeply-nested payloads te beperken.

Sub-500 HttpException filter: wanneer $hint?->exception instanceof HttpException && $hint->exception->getStatusCode() < 500, returnt de scrubber null → event wordt niet gestuurd. Reden: 404, 403, 422 etc. zijn verwachte business-uitkomsten (RFC §3.10), niet programmer-errors. ignore_exceptions in config/sentry.php doet class-only filtering; status-based filtering moet hier.

5.2 Frontend — apps/app/src/observability/scrubber.ts

TypeScript port van de backend-scrubber met identieke semantics. Plus:

  1. Storage context strip: event.contexts.storage wordt gestript. Sentry doesn't add this by default but defensively. RFC §3.7 frontend point 2 — localStorage / sessionStorage never in event context (Crewli's portal-state in sessionStorage MAG NIET lekken).

  2. event.user.cookies strip: als sentry's BrowserSession integration document.cookie exposure via user-context injecteert, wordt het weggehaald.

  3. Cookies wholesale (typed shape): event.request.cookies is typed Record<string, string> in @sentry/vue. Replace met { scrubbed: '[scrubbed]' } in plaats van een string — preserves the typed shape.

5.3 Boundary: business outcomes vs programmer/infra errors

Exception class Backend behaviour Reden
Throwable, RuntimeException, TypeError Captured Programmer error
QueryException, PDOException Captured Infra error
ValidationException NOT captured (ignore_exceptions) Verwacht user-input error
AuthenticationException NOT captured (ignore_exceptions) Verwacht user-state error
AuthorizationException NOT captured (ignore_exceptions) Verwacht user-permission error
HttpException status < 500 NOT captured (scrubber returns null) Verwacht 4xx outcome
HttpException status >= 500 Captured Genuine server error

Integration::handles($exceptions) in bootstrap/app.php is niet auto-registered door sentry-laravel 4.x. Zonder deze regel runt report($e) alleen door Laravel's default reporter (logs to channel) en bereikt het Sentry niet. Gedekt door tests/Feature/Observability/ExceptionReportingTest. Zie ook BACKLOG OBS-6.

Voor élke nieuwe $exceptions->render(...) handler in bootstrap/app.php: Laravel's flow is report()render(). Als de handler een Throwable consumeert en een Response retourneert, zorgt de framework-flow voor report() automatisch. Render handlers MOGEN NIET report() hand-rollen of vroegtijdig short-circuiten — zie BACKLOG OBS-7 voor expansion plan.

5.4 Form Builder runtime exceptions (concrete classification)

Form Builder is Crewli's grootste runtime-domein met eigen exception-hierarchy (zie ARCH-FORM-BUILDER.md). De classificatie tussen "expected business outcome" en "programmer / infra error" voor deze classes is concreet vastgelegd:

Wel naar GlitchTip (programmer/infra errors):

  • App\Exceptions\FormBuilder\PersonProvisioningException — runtime failure during the apply pipeline. Caught by ApplyBindingsOnFormSubmit and recorded as FormSubmissionActionFailure, but the engineering team needs visibility into recurring patterns across orgs.
  • App\Exceptions\FormBuilder\PurposeSubjectResolutionException — runtime resolution failure (no portal token, no auth user, etc.). Same dual-handling rationale: action-failures table for org-admin operational handling; GlitchTip for engineering visibility.
  • App\Exceptions\FormBuilder\FormBindingApplicatorException — runtime applicator failure (no_transaction, no_schema, unknown_purpose). These should never happen in production; if they do, they're systemic bugs — GlitchTip is the correct destination.

Niet naar GlitchTip (expected business outcomes):

  • App\Exceptions\FormBuilder\PublishGuardViolationException — publish-time validation: schema fails a guard. Returned as 422 with field-level errors. Not a system bug.
  • App\Exceptions\FormBuilder\PurposeRequirementsNotMetException — schema lacks required bindings for its purpose. Returned as 422. Not a system bug.
  • App\Exceptions\FormBuilder\IdempotencyConflictException — duplicate idempotency key on submission. Returned as 409. Not a system bug.

Dual-handling voor de eerste groep is intentional: org-admins fixen specifieke failures via de WS-6 admin UI; engineering identificeert systemic issues across all orgs via GlitchTip's aggregation. De "niet naar GlitchTip" groep is afgedekt door ignore_exceptions (voor PublishGuardViolationException etc. die HttpExceptionInterface implementeren via 422 response) en moet bij toevoeging van een nieuwe expected-outcome class expliciet worden uitgezonderd in config/sentry.php.


§6 Runtime context-split (frontend)

Vie zones, gedecide per route.path en route.meta:

6.1 actor_scope=organisation

  • Organizer routes met active org context (useOrganisationStore().activeOrganisationId !== null)
  • Tags: actor_scope=organisation, organisation_id=<ULID>, plus user-context
  • Voorbeelden: /organisations/:id/dashboard, /events/:id, /dashboard

6.2 actor_scope=platform

  • super_admin op /platform/* paths
  • Tags: actor_scope=platform, GEEN organisation_id
  • Geforceerde org-attribution zou misleidend zijn. Platform-mode events spannen impliciet over alle organisaties.

6.3 actor_scope=user

  • Authenticated user op routes zonder org-scope (/account-settings, /portal/profiel)
  • Tags: actor_scope=user, GEEN organisation_id
  • Reden: Crewli's User↔Organisation is many-to-many; geen reliable single-org hint zonder route-context.

6.4 actor_scope=portal

  • Token-based portal flows: route.meta.public === true && route.meta.context === 'portal'
  • Concrete routes: /portal/advance/:token (artist advance), /register/:public_token (public form fill)
  • Tags: actor_scope=portal, actor_type=portal_token
  • Geen user_id, geen username — RFC §3.7 frontend point 5. De ULID-token zelf is gevoelig; de bezoeker is niet permanent met Crewli verbonden.
  • Backend portal-token request resolves de organisation via de matching artist/event row; frontend events correleren via request_id back naar het backend-event dat wel organisation_id heeft.

6.5 actor_scope=anonymous

  • Public routes zonder auth: /login, /forgot-password, /register, /invitations/:token (acceptance flow)
  • Tags: actor_scope=anonymous, actor_type=unauthenticated

6.6 Cross-zone leakage prevention

Sentry.getCurrentScope().clear() wordt aangeroepen op élke route-transitie in installContextBinding. Voorbeeld: user logt uit in organizer-context, navigeert naar /login. Zonder clear zou het volgende anonymous error-event nog user_id van de uitgelogde gebruiker dragen. Met clear wordt het Sentry-scope reset; de unauthenticated event krijgt alleen de zojuist gebonden anonymous-tags.

Test: contextBinding.spec.ts::test_cross-zone_leak_guard.


§7 CSP whitelist (kritisch)

Crewli's strict CSP connect-src directive moet de GlitchTip ingest-host expliciet whitelisten. Zonder deze entry blokkeert de browser elke @sentry/vue POST stilletjes met "Refused to connect because it violates the following Content Security Policy directive" in DevTools Console — de SDK denkt dat het werkt, maar geen events bereiken GlitchTip.

Environment CSP-locatie connect-src entry
Dev apps/app/index.html meta tag http://localhost:8200
Prod organizer SPA deploy/nginx/csp-spa.conf (Report-Only én Enforce regels) https://monitoring.hausdesign.nl
API JSON responses api/config/security.php — geen update default-src 'none'; geen connect-src want JSON-context heeft geen fetch-origin

Bij introductie van een nieuwe environment (bijv. staging — zie BACKLOG OBS-9) MOET:

  1. De bijbehorende GlitchTip ingest-host worden toegevoegd aan de juiste CSP-locatie.
  2. tests/Feature/Security/CspConnectsToObservabilityTest worden uitgebreid met een staging-assertion zodat de regression-guard de nieuwe environment dekt.

§8 Sourcemap upload (frontend)

Vite produceert sourcemaps voor élke chunk (build.sourcemap=true in vite.config.ts). deploy.sh uploadt ze naar GlitchTip én verwijdert ze uit dist/ vóór nginx ze serveert. RFC §3.5: never public-mapped sources op productie.

vite build  →  apps/app/dist/assets/*.js + *.js.map
                                        │
                                        ▼
       sentry-cli sourcemaps upload --org $SENTRY_ORG \
                                    --project crewli-app \
                                    --release $VITE_SENTRY_RELEASE \
                                    --url-prefix "~/assets/" \
                                    apps/app/dist/assets
                                        │
                                        ▼
       find apps/app/dist -name '*.map' -type f -delete
                                        │
                                        ▼
                                  nginx serves dist/

Required env vars (deploy host alleen, niet committed):

Var Beschrijving
SENTRY_AUTH_TOKEN Per-project upload-only token in GlitchTip. Bert provisioned dit handmatig in crewli-app project settings.
SENTRY_ORG GlitchTip organisation slug. Default in deploy.sh: crewli.
VITE_SENTRY_DSN_FRONTEND Aanwezigheid is conditional — als deze ontbreekt skipt deploy.sh upload (soft fail) maar voert alsnog *.map strip uit.
VITE_SENTRY_RELEASE Build-time injected door deploy.sh: crewli-app@$(git rev-parse --short HEAD).

Soft-fail: als upload faalt (GlitchTip unreachable, expired token), gaat de deploy door en logt een warning. De find … -delete stap loopt altijd. Beter unmapped stack traces in GlitchTip dan een geblokkeerde deploy.


§9 GDPR & privacy

9.1 Processing register

Crewli is controller voor GlitchTip-data (self-hosted op Crewli-infra). Geen processor-relatie, geen DPA-uitbreiding nodig. Processing register entry: zie SECURITY_AUDIT.md, "WS-7 Observability — finale audit".

9.2 Data na scrubbing

Wat een GlitchTip-event nog kan bevatten:

  • ULIDs (user_id, organisation_id, event_id, request_id, session_id)
  • Stack traces (zonder locals — send_default_pii=false)
  • Route names en HTTP methods
  • Gecureerde tags (zie §3)
  • Breadcrumbs (input-text masked, console-integration off in prod)

Wat niet: emails, telefoonnummers, namen, IP-adressen, raw form_values, raw cookies, raw headers (Authorization etc.).

9.3 Retention

90 dagen, daarna purged door GlitchTip's eigen partition-maintenance loop (zie GLITCHTIP.md monitoring sectie). Configurable via GlitchTip admin UI (settings → environment-config).

9.4 Right to erasure (Art. 17)

Initieel handmatig. Procedure: zie runbooks/observability-erasure.md. Geautomatiseerd erasure-script blijft op BACKLOG (referentie in de RFC; nog niet als concrete entry in BACKLOG.md).


§10 Onderhoud & uitbreiding

10.1 Een nieuwe tag toevoegen

Bepaal eerst de bron van de tag. Drie patronen:

Bron Pattern Voorbeeld
HTTP request context (route, method, headers) Middleware BindSentryRouteContext
Auth context (user, role, org) Listener op Authenticated + TokenAuthenticated AuthScopeContextListener
Domain event (job processing, custom event) Listener op het domain event TagJobAttemptOnSentry
Static / build-time config/sentry.php initial scope app=api

Voor élke nieuwe tag:

  1. Voeg toe aan §3 tabel hierboven.
  2. Implementeer in de gekozen locatie.
  3. Bij listeners: registreer expliciet in AppServiceProvider::boot() met array-callable form, en voeg case toe aan EventListenerRegistrationTest.
  4. Schrijf een feature-test die de tag op een live HTTP flow asserteert (volg het pattern van AuthScopeBindingHttpFlowTest).
  5. Frontend mirror: voeg toe aan apps/app/src/observability/contextBinding.ts en aan contextBinding.spec.ts.

10.2 Een nieuwe scrubbing-rule toevoegen

  1. Backend: voeg key toe aan SENSITIVE_BODY_KEYS of SENSITIVE_HEADERS in app/Services/Observability/SentryEventScrubber.php.
  2. Frontend: identieke wijziging in apps/app/src/observability/scrubber.ts.
  3. Voeg test-case toe aan beide: tests/Feature/Observability/PiiScrubbingTest.php (PHP) en apps/app/src/observability/__tests__/scrubber.spec.ts (TypeScript).
  4. Beide testbestanden moeten de nieuwe key dekken — backend en frontend zijn semantisch gelijk en moeten dat blijven.

10.3 Een nieuwe $exceptions->render(...) handler

Per BACKLOG OBS-7: nieuwe render handlers MOGEN NIET short-circuiten zonder report($e). Laravel's flow is report()render() automatisch; render handlers die een Response retourneren hebben report al gehad.

Als de nieuwe handler een Throwable consumeert die niet via Integration::handles() zou gaan (e.g. een eigen $exception->report() methode op een custom exception), voeg een case toe aan ExceptionReportingTest die bewijst dat het event alsnog gecaptured wordt.

10.4 Een nieuwe environment (staging, demo, …)

Zie BACKLOG OBS-9. Vereist:

  1. GlitchTip-project provisioning + DSN naar 1Password.
  2. CSP whitelist update (apps/app/index.html voor dev-style env, of nieuwe nginx-config voor prod-style env).
  3. tests/Feature/Security/CspConnectsToObservabilityTest uitbreiden met assertion voor de nieuwe environment.
  4. deploy.sh aanpassen als de release-tag-vorm verandert (default: crewli-app@<short-sha>).

10.5 Een nieuwe Form Builder exception class

Zie §5.4. Bij toevoeging van een nieuwe FormBuilder exception:

  • Als het een expected business outcome is: voeg toe aan ignore_exceptions in config/sentry.php als de class niet via HttpException of ValidationException afhandeling al geignored wordt. Documenteer in §5.4.
  • Als het een programmer/infra error is: niets toevoegen, de class flowt automatisch via Integration::handles($exceptions).

§11 Verwijzingen

Implementatie:

Tests (regression guards):

  • tests/Feature/Observability/PiiScrubbingTest.php
  • tests/Feature/Observability/AuthScopeContextListenerTest.php
  • tests/Feature/Observability/AuthScopeBindingHttpFlowTest.php
  • tests/Feature/Observability/BindSentryRouteContextTest.php
  • tests/Feature/Observability/ExceptionReportingTest.php
  • tests/Feature/Observability/RequestIdRoundTripTest.php
  • tests/Feature/Observability/EventListenerRegistrationTest.php
  • tests/Feature/Database/ActivityLogIndexesTest.php
  • tests/Feature/Security/CspHeaderTest.php
  • tests/Feature/Security/CspConnectsToObservabilityTest.php
  • apps/app/src/observability/__tests__/scrubber.spec.ts
  • apps/app/src/observability/__tests__/contextBinding.spec.ts

Documenten: