docs: observability triage + erasure runbooks
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>
This commit is contained in:
293
dev-docs/runbooks/observability-erasure.md
Normal file
293
dev-docs/runbooks/observability-erasure.md
Normal file
@@ -0,0 +1,293 @@
|
||||
# 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-host> # 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', '<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_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', '<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):
|
||||
|
||||
```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:<ULID>
|
||||
```
|
||||
|
||||
Verwacht: 0 resultaten.
|
||||
|
||||
```
|
||||
impersonation.impersonator_user_id:<ULID>
|
||||
```
|
||||
|
||||
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', '<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_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=<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:
|
||||
|
||||
```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', '<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.
|
||||
Reference in New Issue
Block a user