0034. Corpo del record fallito nel report, dietro opt-in esplicito
- Stato: Accettata
- Data: 2026-07-27
- Decisori: Team BI/ETL
Contesto
CaughtErrorSummary — il riepilogo degli errori catturati da un guard che finisce nel report e in UI — è sanitizzato per costruzione: tipo, messaggio troncato, nodo sorgente, stage e, da ADR-0033, il recordId sintetico. Mai il payload, mai i details grezzi. La ragione è che il report viene persistito e mostrato in UI, e i payload di una pipeline reale possono contenere dati personali.
Questo però rende il pannello del guard poco utilizzabile per il caso più comune: capire cosa aveva quel record che non andava. Il recordId dice quale record, non cosa conteneva — e per una demo che deve raccontare la feature, o per un flusso su dati non sensibili, vedere il corpo è esattamente ciò che serve.
Decisione
Il corpo del record entra nel report solo su dichiarazione esplicita, per singolo nodo try-catch:
.tryCatch("enrich", "guard", { includeRawInReport: true })Default false. A flag spento il report è byte-identico a prima: il test di sanitizzazione scritto per ADR-0033 continua a passare senza modifiche, ed è questo il criterio con cui l'opt-in è stato verificato essere davvero tale.
Perché opt-in e non default
Il default sicuro è quello che protegge quando nessuno ha pensato al problema. Invertirlo significherebbe che ogni pipeline già esistente comincia a persistere payload nel report senza che nessuno abbia deciso nulla. Il costo dell'opt-in è una riga in più nelle pipeline che lo vogliono; il costo del default opposto è un incidente silenzioso in quelle che non ci hanno pensato.
Perché per-guard e non per-pipeline
Valutata l'alternativa graph(name, { includeRaw: true }). Scartata per due ragioni. La prima è che introdurrebbe l'oggetto opzioni su graph(), superficie API che #78 aveva deliberatamente evitato di aprire. La seconda è che un guard è già l'unità naturale: pipeline diverse hanno guard su flussi con sensibilità diversa, e un interruttore unico costringerebbe ad allinearsi al più permissivo.
Il contro è l'auditabilità: la risposta a «quali pipeline espongono payload?» è un grep su includeRawInReport, non la lettura di un unico registro. Accettabile perché il nome del flag è autodescrittivo e greppabile.
Perché una stringa e non un oggetto
raw è il payload serializzato in JSON e troncato a CAUGHT_ERROR_RAW_MAX_LEN (2000 caratteri), non l'oggetto. Il report è un confine serializzabile: infilarci un oggetto arbitrario aprirebbe un canale per dimensioni e tipi imprevedibili. Una stringa capata è misurabile, e insieme a caughtErrorsLimit dà un tetto calcolabile a quanto un guard può aggiungere al report.
La troncatura lascia un marcatore esplicito (… (troncato)), così chi legge sa di avere un frammento. La UI riformatta il JSON quando è valido e mostra la stringa tale e quale quando la troncatura l'ha spezzato — meglio un frammento leggibile che un errore di parsing.
Serializzazione difensiva
JSON.stringify solleva su strutture cicliche e BigInt. serializeRawForReport cattura e degrada a [payload non serializzabile]: un payload ostile non deve poter abbattere una run, tanto meno una run che stava già gestendo un errore.
Solo le catture per-record
Il corpo è quello di DeadLetter.raw, cioè il record esatto responsabile. Le catture in circuit-breaker non ne hanno uno, e restano senza corpo anche con il flag acceso. lastConsumed (ADR-0033) non viene esposto nemmeno con l'opt-in: la sua semantica è più debole e mescolarla al corpo esatto renderebbe impossibile, per chi legge il report, sapere quale delle due cose ha davanti.
Alternative considerate
Redazione o mascheramento dei campi sensibili — includere il corpo con i campi noti come sensibili oscurati. Scartata per ora: richiede che qualcuno dichiari quali campi sono sensibili, e una dichiarazione incompleta dà una falsa sicurezza, che è peggio di un opt-in onesto tutto-o-niente. Resta la naturale evoluzione se il flag dovesse servire su flussi reali.
Esporre il corpo solo in UI e non nel report persistito — non c'è un canale separato: la UI legge il report. Avrebbe richiesto un secondo percorso dati solo per questo, sproporzionato.
Conseguenze
Positive
- Il pannello del guard mostra finalmente cosa conteneva il record respinto;
orders-enriched-dailylo attiva e diventa raccontabile. - Il default resta sicuro: nessuna pipeline esistente cambia comportamento.
- Il tetto sul report è calcolabile:
caughtErrorsLimit × CAUGHT_ERROR_RAW_MAX_LEN.
Negative / da tenere d'occhio
- Il rischio è reale e resta in capo a chi scrive la pipeline. Accendere il flag su un flusso con dati personali significa persisterli nel report. Nessun controllo automatico lo impedisce; solo il nome del flag e questa ADR.
- Nessuna redazione: è tutto-o-niente per guard.
- Due classi di errori catturati nel report, con e senza corpo, a seconda della modalità di cattura. Documentato sul campo.
Non toccato
GRAPH_REPORT_SCHEMA_VERSIONresta3:rawè additivo e opzionale.- Retention e cifratura del report: dove finisce il report non cambia, cambia solo cosa può contenere.

