# 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`](../ARCH-OBSERVABILITY.md) (GDPR sectie), > [`SECURITY_AUDIT.md`](../SECURITY_AUDIT.md) (processing register > entry voor GlitchTip), > [`GLITCHTIP.md`](../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_id` of (indien van toepassing) `impersonation.impersonator_user_id` van de betreffende gebruiker voorkomt. - Verwijder ook events waarin de gebruiker als `impersonation.impersonator_user_id` staat (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: 1. **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_id` moet 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). 2. **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) 3. **Check retention-window.** GlitchTip purgt na 90 dagen (zie [ARCH §9.3](../ARCH-OBSERVABILITY.md)). 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 ```bash ssh crewli@ # monitoring.hausdesign.nl docker exec -it glitchtip-postgres psql -U postgres -d glitchtip ``` ### 3.2 Counts vóór delete (verifieer scope) ```sql -- Direct events met user_id tag matchend SELECT COUNT(*) FROM issues_event WHERE tags @> ARRAY[ARRAY['user_id', '']]::text[][]; -- Events waar deze user impersonator was SELECT COUNT(*) FROM issues_event WHERE tags @> ARRAY[ARRAY['impersonation.impersonator_user_id', '']]::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', '']]::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_event` vs `events_event`) en tag-storage > (array vs jsonb). Check eerst `\dt` om de actuele tabel-namen te > zien, en `\d issues_event` voor de tag-kolom-definitie. > De queries hier zijn voor v6.x met `tags` als 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 ```sql -- Direct user_id events DELETE FROM issues_event WHERE tags @> ARRAY[ARRAY['user_id', '']]::text[][]; -- Impersonator-as-admin events (alleen als verzoek dit dekt) DELETE FROM issues_event WHERE tags @> ARRAY[ARRAY['impersonation.impersonator_user_id', '']]::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): ```sql 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: ```sql 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 ```sql \q ``` --- ## §4 Post-checks ### 4.1 GlitchTip UI search Open `https://monitoring.hausdesign.nl` → projects → search: ``` user_id: ``` Verwacht: 0 resultaten. ``` impersonation.impersonator_user_id: ``` Verwacht: 0 resultaten. ### 4.2 Postgres re-count ```bash docker exec glitchtip-postgres psql -U postgres -d glitchtip -c \ "SELECT COUNT(*) FROM issues_event WHERE tags @> ARRAY[ARRAY['user_id', '']]::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=` - 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_log` met `subject_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=`, `impersonation.active=true`, `impersonation.impersonator_user_id=` 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: ```sql 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: ```sql BEGIN; DELETE FROM issues_event WHERE tags @> ARRAY[ARRAY['user_id', '']]::text[][]; DELETE FROM issues_event WHERE tags @> ARRAY[ARRAY['impersonation.impersonator_user_id', '']]::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.