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>
9.3 KiB
Observability erasure runbook (GDPR Art. 17)
Procedure voor het verwijderen van een gebruiker's data uit GlitchTip bij een geldig "right to erasure" verzoek.
Audience: Bert (in zijn rol als controller per Crewli's processing register), eventueel met legal-input voor het beoordelen of het verzoek valid is.
Trigger: een Art. 17 verzoek bevestigd door legal én Crewli's primaire datastores zijn al geërased. GlitchTip is downstream — pas erase'en als upstream klaar is, anders kunnen nieuwe events de net- verwijderde user-id opnieuw introduceren.
Cross-references:
ARCH-OBSERVABILITY.md §9(GDPR sectie),SECURITY_AUDIT.md(processing register entry voor GlitchTip),GLITCHTIP.md(operational context — hoe je in de postgres komt).
§1 Trigger
Crewli's privacy-policy verplicht de controller (Bert) om bij een geldig Art. 17-verzoek alle persoonsgegevens te verwijderen. Voor GlitchTip betekent dat:
- Verwijder élk event waarin de
user_idof (indien van toepassing)impersonation.impersonator_user_idvan de betreffende gebruiker voorkomt. - Verwijder ook events waarin de gebruiker als
impersonation.impersonator_user_idstaat (super_admin die ooit deze user impersoneerde — events bevatten de admin's ULID, en een verzoek van een admin om eigen data te wissen valt ook onder de procedure).
Pas uitvoeren nadat: de gebruiker is verwijderd uit Crewli's primaire datastores (users tabel, soft-deleted state, gerelateerde records). Anders kan een achterblijvende background job de net- verwijderde user-id opnieuw introduceren in GlitchTip via een exception event.
§2 Pre-checks
Vóór je SSH'd naar productie:
-
Bevestig de user-id (ULID). Open Crewli's platform-admin UI → activity log → zoek de gebruiker. Noteer:
user_id(ULID, 26 chars)- Of de gebruiker ooit super_admin was (zo ja, ook
impersonation.impersonator_user_idmoet gepurged) - Of de gebruiker target is geweest van impersonation (zoek in
impersonation_audit_logs— als targets/admins gemixt zijn, erase je de admin's ULID via diens eigen request, niet via deze).
-
Documenteer ticket-referentie. Crewli's processing register eist auditable trail. Noteer in een veilige notitie:
- Datum verzoek
- Ticket-ID / referentie
- User-id
- Wie heeft het verzoek beoordeeld (Bert + legal indien van toepassing)
-
Check retention-window. GlitchTip purgt na 90 dagen (zie ARCH §9.3). Als de gebruiker > 90 dagen geen events heeft gegenereerd, is er waarschijnlijk niets meer te wissen. Doe deze check eerst zodat je geen onnodige delete uitvoert.
§3 Handmatige procedure
3.1 SSH en open psql
ssh crewli@<monitoring-host> # monitoring.hausdesign.nl
docker exec -it glitchtip-postgres psql -U postgres -d glitchtip
3.2 Counts vóór delete (verifieer scope)
-- Direct events met user_id tag matchend
SELECT COUNT(*) FROM issues_event
WHERE tags @> ARRAY[ARRAY['user_id', '<ULID>']]::text[][];
-- Events waar deze user impersonator was
SELECT COUNT(*) FROM issues_event
WHERE tags @> ARRAY[ARRAY['impersonation.impersonator_user_id', '<ULID>']]::text[][];
-- Issues die uitsluitend uit deze user's events bestaan (kandidaten
-- voor full issue-delete)
SELECT i.id, i.title, COUNT(e.id) AS events_for_user, i.times_seen
FROM issues_issue i
JOIN issues_event e ON e.issue_id = i.id
WHERE e.tags @> ARRAY[ARRAY['user_id', '<ULID>']]::text[][]
GROUP BY i.id, i.title, i.times_seen
HAVING COUNT(e.id) = i.times_seen;
Schema-noot: GlitchTip versies kunnen verschillen in tabel-namen (
issues_eventvsevents_event) en tag-storage (array vs jsonb). Check eerst\dtom de actuele tabel-namen te zien, en\d issues_eventvoor de tag-kolom-definitie. De queries hier zijn voor v6.x mettagsals array-of-2-arrays; pas aan voor andere versies. Overweeg de queries een keer te verifiëren tegen test-data vóór je een echte delete uitvoert.
Schrijf de output op (counts) zodat je in §4 kunt verifiëren dat de deletes succesvol waren.
3.3 Delete events
-- Direct user_id events
DELETE FROM issues_event
WHERE tags @> ARRAY[ARRAY['user_id', '<ULID>']]::text[][];
-- Impersonator-as-admin events (alleen als verzoek dit dekt)
DELETE FROM issues_event
WHERE tags @> ARRAY[ARRAY['impersonation.impersonator_user_id', '<ULID>']]::text[][];
3.4 Cleanup van orphan issues
Issues die nu 0 events hebben moeten ook weg (anders blijft de issue- title — die in theorie geen PII zou bevatten, maar de aggregate-counts worden onnauwkeurig):
DELETE FROM issues_issue
WHERE id NOT IN (SELECT DISTINCT issue_id FROM issues_event);
3.5 Bevestig vacuum
GlitchTip gebruikt postgres standaard auto-vacuum; voor bulk-deletes kan een handmatige vacuum nodig zijn om disk-space terug te winnen:
VACUUM FULL issues_event;
VACUUM FULL issues_issue;
VACUUM FULL blokkeert reads/writes — doe dit in een laag-traffic
window, of skip als de gewiste user weinig events had.
3.6 Exit psql
\q
§4 Post-checks
4.1 GlitchTip UI search
Open https://monitoring.hausdesign.nl → projects → search:
user_id:<ULID>
Verwacht: 0 resultaten.
impersonation.impersonator_user_id:<ULID>
Verwacht: 0 resultaten.
4.2 Postgres re-count
docker exec glitchtip-postgres psql -U postgres -d glitchtip -c \
"SELECT COUNT(*) FROM issues_event WHERE tags @> ARRAY[ARRAY['user_id', '<ULID>']]::text[][];"
Verwacht: 0.
4.3 Audit trail
Update de notitie uit §2 met:
- Datum / tijd van uitvoering
- Aantal events verwijderd (uit §3.2 vóór-counts)
- Eventuele afwijkingen / partial successes
- Bevestiging van §4.1 + §4.2 zero-results
Bewaar in een audit-locatie waar legal + Bert het kunnen terugvinden voor minimaal 6 jaar (per Crewli's algemene compliance-bewaartermijn).
§5 Toekomstige automation
Geautomatiseerd erasure-script staat op BACKLOG. Wanneer geïmplementeerd:
- Mogelijk als een Laravel artisan command:
php artisan glitchtip:erase --user-id=<ULID> - Of als een GlitchTip API-endpoint integratie (GlitchTip heeft een REST API; uit Laravel een HTTPS-call doen die alle matchende events delete).
- In beide gevallen: audit-log entry naar
activity_logmetsubject_type='User',event='glitchtip.erasure', properties{requested_at, ticket_ref, count_deleted}.
Tot dat erin zit: gebruik deze handmatige procedure.
§6 Edge cases
6.1 User had geen events in 90-day window
Niets te doen. GlitchTip's eigen retention-loop heeft alles al gepurged. Documenteer in audit trail dat erasure compleet was zonder delete-actie ("user had no events within retention window").
6.2 User_id is impersonation-target
Een event met actor_type=org_admin, user_id=<TARGET-ULID>,
impersonation.active=true, impersonation.impersonator_user_id=<ADMIN-ULID>
betekent: een super_admin was de organizer aan het impersoneren toen
het event vuurde. Voor een verzoek van de TARGET user: deletes lopen
via §3.3 op de TARGET ULID — de impersonator-tag blijft (admin-PII die
niet onder de target's verzoek valt).
Voor een verzoek van de ADMIN user (e.g. een uitgetreden super_admin
die zijn eigen data wil wissen): deletes lopen via §3.3 op de ADMIN
ULID over BEIDE tag-kolommen (user_id als de admin zelf gebruiker
was, en impersonation.impersonator_user_id voor sessies waar hij
impersoneerde).
6.3 Events in glitchtip-worker queue maar nog niet in postgres
Celery-worker batches events. Een event die nog in de queue staat wordt niet door §3.3 delete'd; het komt later alsnog binnen.
Mitigation: wacht 5 minuten na de laatste user-activiteit vóór je de erasure-run start. Crewli's user-soft-delete + de Crewli-API-side deletes geven natuurlijk al een idle-window; gebruik dat.
Als een laat-binnenkomend event de net-gewiste ULID herintroduceert: draai §3.3 nogmaals na een tweede 5-minuten-window. Documenteer in audit trail.
6.4 Multiple ULIDs (mass-erasure)
Voor een batch erasure (bv. een deelorganisatie wordt opgeheven): gebruik een tijdelijke tabel:
CREATE TEMP TABLE erasure_targets (ulid char(26));
\copy erasure_targets FROM '/tmp/erasure-ulids.csv' WITH CSV;
DELETE FROM issues_event
WHERE EXISTS (
SELECT 1 FROM erasure_targets t
WHERE issues_event.tags @> ARRAY[ARRAY['user_id', t.ulid]]::text[][]
);
Document in audit trail: aantal ULIDs, totaal aantal events verwijderd, ticket-batch-referentie.
6.5 Partial failure mid-procedure
Als psql een error gooit halverwege §3.3 (bv. lock conflict): de DELETE statements draaien standalone (niet in een transactie). Een geslaagde eerste DELETE blijft committed; een gefaalde tweede moet opnieuw. Verifieer met §4.2 counts welke ULIDs / kolommen al gewist zijn voordat je opnieuw runt.
Voor consistentie kun je beide DELETEs in een transactie wikkelen:
BEGIN;
DELETE FROM issues_event WHERE tags @> ARRAY[ARRAY['user_id', '<ULID>']]::text[][];
DELETE FROM issues_event WHERE tags @> ARRAY[ARRAY['impersonation.impersonator_user_id', '<ULID>']]::text[][];
COMMIT;
— Maar overweeg dat dit langere lock-times veroorzaakt op een live GlitchTip postgres en kan andere event-ingest blokkeren tijdens de transactie. Voor low-volume erasures (één user) is statement-by- statement OK.