CodeGym /Corsi /ChatGPT Apps /Audit & lifecycle: audit logs, data retention, cancel...

Audit & lifecycle: audit logs, data retention, cancellazione su richiesta, continuità/backup

ChatGPT Apps
Livello 15 , Lezione 4
Disponibile

1. Perché vi servono audit & lifecycle in una ChatGPT App

Finché scrivete un prototipo, l’utente siete voi, il DB è un SQLite locale e gli «incidenti» si curano con git reset --hard, tutto sembra carino e domestico.

Ma appena il vostro GiftGenius (o un’altra app ChatGPT (App)) ottiene utenti reali, soprattutto con pagamenti e PII, compaiono all’improvviso:

  • i security officer del cliente con la domanda «chi può vedere i nostri ordini e chi li ha modificati?»;
  • i legali con la domanda «per quanto tempo conservate i dati e come eseguite la richiesta “cancellami”?»;
  • la realtà di produzione con la domanda «cosa succede se uno sviluppatore droppa una tabella in prod?».

In questa lezione analizzeremo quattro blocchi fondamentali:

  1. Log di audit — un livello di logging separato per sicurezza e audit.
  2. Data retention — tempi di vita per diversi tipi di dati e come implementarli.
  3. Cancellazione su richiesta dell’utente — il «diritto all’oblio» in pratica.
  4. Business continuity & backups — come sopravvivere ai guasti senza perdere faccia e dati.

Tutti gli esempi saranno, dove possibile, legati alla nostra App didattica (GiftGenius su Next.js + Apps SDK + MCP).

2. Log di audit: chi, cosa, quando e con quale esito

In cosa i log di audit differiscono dai log applicativi

I normali log applicativi sono messaggi amichevoli per lo sviluppatore. Ci vivono gli stack trace, le informazioni di debug, valori strani di variabili, i console.log("qui sicuramente non dovrebbe esserci null") di debug. Vivono poco e vengono letti dagli ingegneri.

I log di audit sono un altro universo. Il loro pubblico principale sono i security, gli auditor, talvolta i legali. Non serve loro la riga «NullPointer alla riga 55», serve una voce del tipo «l’utente X ha modificato le impostazioni di pagamento dell’organizzazione Y in tale momento, risultato — successo». I record di audit di solito vivono molto più a lungo (anni) e sono considerati prova in caso di indagini.

Differenze chiave:

Caratteristica Application Logs Audit Logs
Scopo Debug, diagnostica Sicurezza, compliance, investigazioni
Pubblico Sviluppatori, SRE Security, legali, a volte autorità
Contenuto dei dati Dettagli tecnici, stack trace Chi/cosa/quando/su quale risorsa/con quale esito
Periodo di conservazione Settimane–1 mese Anni (spesso ≥ 1 anno)
Operazioni sui log Si possono eliminare/sovrascrivere Preferibilmente solo append, senza UPDATE/DELETE

OWASP e linee guida simili sottolineano: è meglio tenere i log di audit in un archivio o tabella separati, e non mescolarli con i log applicativi.

Cosa loggare nel contesto di un’app ChatGPT

Per un’app ChatGPT, soprattutto con componenti commerciali, un minimo ragionevole di audit è:

  • eventi di autenticazione: login, logout, tentativi di accesso;
  • operazioni su dati critici: creazione/aggiornamento/eliminazione di profili, ordini, impostazioni di pagamento;
  • azioni amministrative: cambio ruoli, modifica delle impostazioni del tenant;
  • invocazioni di strumenti sensibili MCP/Agents: create_order, charge_customer, cancel_subscription ecc.

Una buona intuizione: tutto ciò che durante un incidente vorreste poter chiedere in stile «chi lo ha fatto e attraverso cosa?» dovrebbe finire nell’audit.

Struttura di un evento di audit

Un utile modello mentale: ogni record è «chi / quale azione / su cosa / in quale contesto / con quale esito». Spesso viene formulato come struttura who, action, resource, context, outcome.

Per il nostro GiftGenius definiamo un’interfaccia in TypeScript:

// lib/audit.ts
export type AuditAction =
  | "auth.login"
  | "auth.logout"
  | "order.create"
  | "order.cancel"
  | "account.delete"
  | "giftidea.generate";

export interface AuditEvent {
  eventId: string;          // uuid
  timestamp: string;        // ISO
  actor: {
    userId: string | null;  // può essere null prima del login
    tenantId?: string | null;
    ip?: string | null;
    client: "chatgpt-app" | "admin-panel" | string;
  };
  action: AuditAction;
  resource?: {
    type: string;           // "order", "user", ...
    id?: string;
  };
  context?: {
    mcpTool?: string;
    requestId?: string;
  };
  outcome: {
    status: "success" | "failure";
    reason?: string | null;
  };
}

Notate che nell’evento non finiscono e‑mail complete, numeri di carta e altre PII, che nelle lezioni precedenti abbiamo imparato a mascherare e a non loggare senza necessità.

Dove e come conservare i log di audit

Requisiti minimi per l’archiviazione:

  • tabella separata o persino un DB separato dai log applicativi, così è più difficile cancellare qualcosa per errore;
  • se possibile, modalità append‑only: tecnicamente può essere semplicemente la policy «non facciamo mai UPDATE/DELETE su questa tabella», più un ruolo DB che ha solo i permessi INSERT e SELECT;
  • accesso limitato: non tutto il team tecnico deve poter leggere l’audit completo.

Se usate PostgreSQL tramite Prisma/Drizzle, il modello può essere così (esempio semplificato):

CREATE TABLE audit_events (
  event_id   uuid PRIMARY KEY,
  created_at timestamptz NOT NULL DEFAULT now(),
  actor_user_id text,
  actor_tenant_id text,
  actor_ip      inet,
  action        text NOT NULL,
  resource_type text,
  resource_id   text,
  context_mcp_tool text,
  context_request_id text,
  outcome_status text NOT NULL,
  outcome_reason text
);

Lo schema si adatta alle vostre esigenze, ma l’importante è la struttura. Il JSON spazzatura in una riga lo maledirete in seguito.

Implementazione dell’audit nella nostra App

Creiamo un piccolo helper nell’app Next.js (ambiente Node, ad esempio nel server MCP o in un route API):

// lib/audit.ts
import { randomUUID } from "crypto";
import { db } from "./db"; // il tuo client al DB

export async function logAudit(event: Omit<AuditEvent, "eventId" | "timestamp">) {
  const full: AuditEvent = {
    ...event,
    eventId: randomUUID(),
    timestamp: new Date().toISOString(),
  };

  // Nella vita reale — tramite una coda/processo in background, qui solo insert
  await db.insertInto("audit_events").values({
    event_id: full.eventId,
    created_at: full.timestamp,
    actor_user_id: full.actor.userId,
    action: full.action,
    outcome_status: full.outcome.status,
    outcome_reason: full.outcome.reason ?? null,
    // ...altri campi
  });
}

Ora aggiungiamo la chiamata nel handler che crea un ordine (supponiamo sia un MCP tool o un endpoint server‑side):

// app/api/orders/route.ts
export async function POST(req: Request) {
  const user = await requireUser(req); // dal modulo di autenticazione
  const body = await req.json();
  const order = await createOrderInDb(user, body);

  await logAudit({
    actor: { userId: user.id, client: "chatgpt-app" },
    action: "order.create",
    resource: { type: "order", id: order.id },
    context: { mcpTool: "create_order_tool" },
    outcome: { status: "success" },
  });

  return Response.json(order);
}

Lo stesso si può fare attorno alle operazioni rischiose — annullare un ordine, modificare i dati di pagamento, eliminare un account.

Abbiamo ottenuto un livello di audit separato e strutturato — ottimo. La domanda successiva, naturale, è: per quanto tempo tutti questi eventi (e gli altri dati utente) devono vivere e cosa farne alla scadenza?

3. Data retention: quanto vivono i vostri dati

Perché non si può conservare tutto per sempre

L’istinto ingegneristico «magari servirà» nel contesto dei dati degli utenti è molto pericoloso.

Primo, più a lungo e più dati accumulate, più gravi sono le conseguenze in caso di violazione: più grande è la botte di benzina, più spaventoso l’incendio. Molte linee guida sulla protezione dei dati definiscono i dati un «asset tossico»: vanno conservati con uno scopo, ma minimizzando volume e durata.

Secondo, la normativa tipo GDPR/CCPA introduce il principio «non più a lungo del necessario per la finalità del trattamento». Cioè, i dati personali non si possono tenere all’infinito «per ogni evenienza». Per ciascun tipo di dato devono esserci tempi chiari di conservazione e procedure di eliminazione o anonimizzazione.

Terzo, lo storage cloud costa. Grandi tabelle di log e storici delle chat crescono rapidamente e dopo un anno capita che metà del conto del provider sia «spazzatura di ieri».

Dati diversi — tempi diversi

L’esperienza delle aziende e le linee guida pubbliche portano più o meno a questo quadro:

Tipo di dati Tempi tipici di conservazione
Log di debug, metriche tecniche da 1 a 12 mesi
Log di audit ≥ 12 mesi, talvolta 2–5 anni
Ordini, pagamenti, fatture 3–7 anni (per requisiti contabili/fiscali)
Sessioni, token temporanei ore–giorni
Chat grezze / richieste da alcune settimane a alcuni mesi, oppure non si conservano
Aggregati anonimi (analytics) più a lungo, poiché la PII non c’è più

Importante: non è consulenza legale, ma orientamento ingegneristico. Per un prodotto reale concorderete i tempi con i legali, ma dal punto di vista tecnico dovete già essere pronti a implementare TTL differenti.

Come implementare la retention nel codice

Il pattern più comune: la tabella ha created_at o expires_at, e avete un processo periodico che elimina o anonimizza i record vecchi.

Esempio: pulizia dei log ordinari più vecchi di 90 giorni.

// scripts/cleanup-logs.ts
import { db } from "../lib/db";

async function cleanup() {
  await db
    .deleteFrom("app_logs")
    .where("created_at", "<", new Date(Date.now() - 90 * 24 * 60 * 60 * 1000));
  console.log("Old logs removed");
}

cleanup().catch(console.error);

Uno script del genere può essere eseguito con cron, tramite GitHub Actions a orario, oppure con lo scheduler del cloud.

Per la PII al posto dell’eliminazione si esegue spesso l’anonimizzazione. Per esempio, gli ordini più vecchi di N anni perdono il collegamento con l’utente:

UPDATE orders
SET user_id = NULL
WHERE created_at < now() - interval '3 years';

In questo modo si conservano importi, articoli e altra «contabilità», ma sparisce il collegamento alla persona specifica.

Non dimenticate che anche le copie di backup devono avere propri tempi di vita. La periodicità e la durata della conservazione dei backup le discuteremo a parte nel blocco relativo ai backup, ma l’idea è la stessa: nemmeno gli archivi si possono tenere in eterno, altrimenti il «diritto all’oblio» diventa una finzione.

4. Cancellazione su richiesta dell’utente: «diritto all’oblio» nel codice

Da dove viene il requisito

Il GDPR europeo (e leggi analoghe) introduce il cosiddetto «diritto all’oblio»: l’utente può chiedere la cancellazione dei propri dati personali e l’azienda deve farlo senza ritardi ingiustificati.

Dal punto di vista dello sviluppatore ciò significa: prima o poi vi arriverà una richiesta «eliminate tutti i miei dati» (o inserirete voi stessi un pulsante «Delete my data»), e dovrete non solo eliminare il record nella tabella users, ma seguire tutta la traccia: ordini, sessioni, token, log delle azioni, CRM, pagamenti, ecc.

Ma ci sono anche leggi che vi impongono di conservare determinati dati: ad esempio le transazioni finanziarie. Quindi qui ci sono più complessità legali che tecniche.

Cosa esattamente va pulito

Set minimo per il nostro GiftGenius:

  • profilo utente (nome, e‑mail, impostazioni);
  • sessioni, refresh token, collegamenti con provider OAuth;
  • ordini, se non servono in forma «personalizzata» (o se possono essere anonimizzati);
  • log e record di audit dove è presente PII (ad esempio e‑mail in chiaro).

Rimangono invece i dati importanti per la reportistica, ma ormai anonimizzati — importi degli ordini, numero di transazioni, aggregati per paese, ecc.

Esempio di algoritmo di cancellazione

Schema dello scenario:

  1. L’utente (autenticato) preme «Elimina il mio account».
  2. Al server arriva la richiesta con il suo userId.
  3. Il server:
    • elimina/anonimizza i record dipendenti (ordini, sessioni, integrazioni);
    • pulisce la PII nel profilo;
    • scrive un record nel log di audit «richiesta di cancellazione dati elaborata».

Per semplicità mostriamo una variante minima su un paio di tabelle. In un prodotto reale, attorno a questo nucleo aggiungerete entità aggiuntive (integrazioni, servizi di terze parti, ecc.).

Codice di servizio in Next.js (esempio semplificato):

// app/api/delete-me/route.ts
import { db } from "@/lib/db";
import { logAudit } from "@/lib/audit";

export async function POST(req: Request) {
  const user = await requireUser(req);

  await db.transaction(async (tx) => {
    await tx.deleteFrom("sessions").where("user_id", "=", user.id);
    await tx.deleteFrom("orders").where("user_id", "=", user.id);

    await tx.updateTable("users")
      .set({
        is_deleted: true,
        name: null,
        email: null,
      })
      .where("id", "=", user.id);

    await logAudit({
      actor: { userId: user.id, client: "chatgpt-app" },
      action: "account.delete",
      outcome: { status: "success" },
    });
  });

  return new Response(null, { status: 204 });
}

Nel mondo reale aggiungerete qui le chiamate alle API esterne (ad esempio Stripe — per scollegare il customer), e renderete la transazione più robusta. Ma il principio c’è: tutto in un unico punto, con record di audit.

Interazione con i backup

Con la parte insidiosa «e i backup?» sono legate molte domande interessanti. Anche se avete eliminato l’utente dal DB di produzione, i suoi dati possono restare negli snapshot notturni. Perché questo non si trasformi in «di fatto non eliminiamo mai nessuno», ci sono due approcci:

  1. I backup hanno a loro volta un tempo di vita limitato (per esempio, 30–90 giorni) e al suo termine scompaiono insieme ai dati. Dopo la scadenza della retention né il DB principale né gli archivi contengono l’utente.
  2. Se comunque risollevate il sistema da un backup, avete un registro degli ID «eliminati» e dopo il ripristino rieseguite gli script di cancellazione/anonimizzazione.

Nelle aziende grandi talvolta si pratica il crypto‑shredding: la PII dell’utente è cifrata con una chiave separata e, alla richiesta di cancellazione, si distrugge la chiave. Anche se da qualche parte restano copie dei dati cifrati (nei log, nei backup), senza la chiave sono spazzatura inutile. È fantastico, ma per una startup è un po’ «tecnologia missilistica».

Aspetto UX importante

Ricordate che la cancellazione non è solo SQL. L’utente si aspetta:

  • un modo comprensibile per inviare la richiesta (pulsante, form, e‑mail);
  • tempi ragionevoli di esecuzione (in pratica fino a 30 giorni);
  • una notifica di successo o un rifiuto motivato (ad esempio quando una parte dei dati deve essere conservata per legge).

Dal lato tecnico avete già tutto pronto: sapete eseguire la pulizia, loggare l’azione e non conservare inutilmente nei backup.

5. Business continuity & backup

Ora immaginate che tutto quanto sopra funzioni alla perfezione… finché non avviene un fatale DROP TABLE orders, un guasto nel cloud o la caduta di una regione. Servono meccanismi per riportare il servizio in vita in tempi ragionevoli e non perdere dati critici.

RTO e RPO — due lettere che definiscono il vostro dolore

Due parametri base del Disaster Recovery:

  • RTO (Recovery Time Objective) — quanto tempo potete permettervi di essere indisponibili. Per esempio, se RTO = 1 ora, significa che dopo un guasto serio dovete rialzare il sistema entro un’ora al massimo.
  • RPO (Recovery Point Objective) — quanti dati nel tempo siete pronti a perdere. Se RPO = 10 minuti, significa che al ripristino potete perdere gli ultimi 10 minuti di storia, ma non di più.

Quanto più critico è il prodotto (banking, sistemi di trading), tanto più entrambi i parametri si avvicinano allo zero. Per il nostro GiftGenius didattico si può vivere con RTO ~ poche ore e RPO ~ 15–60 minuti, ma anche questo va realizzato.

Cosa può andare storto nel vostro stack

Nel contesto di un’app ChatGPT su Vercel + DB cloud + API esterne, l’elenco tipico dei guai è questo:

  • OpenAI API non disponibile: la vostra App risponde con errori alle tool‑call.
  • Vercel (o qualcun altro) ha un guasto: il widget non riesce a raggiungere il vostro backend.
  • Il database è danneggiato o qualcosa è stato eliminato per errore (ad esempio, DROP TABLE).
  • Account compromesso o rotto che gestisce l’infrastruttura.

A tutto questo rispondete con una combinazione di backup, repliche e comportamento ragionevole dell’app in caso di guasto.

Strategie di backup

I moderni Postgres/DB cloud di solito offrono almeno tre opzioni:

  1. Backup completi + incrementali.
    Fare uno snapshot completo del DB una volta al giorno e in mezzo conservare le modifiche incrementali. Al ripristino — rollback a uno snapshot specifico più replay del log delle modifiche.
  2. Point‑in‑Time Recovery (PITR).
    Il database scrive il transaction log (WAL) e permette di ripristinare a un momento arbitrario (ad esempio «stato alle 14:03:00, prima che droppassimo la tabella»).
  3. Replica in un’altra regione.
    Tenere una replica passiva o attiva del DB in un’altra regione/cloud. Alla perdita della regione principale si può commutare l’app sulla replica, perdendo solo i dati non ancora replicati.

Per la nostra scala di solito basta abilitare PITR presso il provider del DB e backup periodici off‑site.

Esempio semplice: dump giornaliero per il DB locale/dev

Anche se in produzione vi affidate a un DB gestito, per staging/dev talvolta conviene avere un semplice script:

# scripts/backup.sh
#!/usr/bin/env bash
set -e
DATE=$(date +%F)
pg_dump "$DATABASE_URL" > "backups/backup-$DATE.sql"
echo "Backup created: backups/backup-$DATE.sql"

Si può eseguirlo con cron o GitHub Actions. L’importante — non dimenticare che anche i backup vanno eliminati alla scadenza.

Comportamento dell’App quando i servizi esterni sono down

Backup e PITR risolvono il problema «cosa fare se tutto è proprio rotto o i dati corrotti». Ma nella realtà business capitano più spesso guasti parziali — cade un’API esterna, si taglia la rete, s’impalla il sistema di pagamenti.

Quando l’OpenAI API o il sistema di pagamenti sono giù, la strategia peggiore è cadere con un 500 nudo e uno stack trace privo di senso nella risposta. Idealmente:

  • il backend restituisce un errore strutturato tipo { error: "upstream_unavailable" };
  • il widget mostra all’utente un messaggio comprensibile: «Servizio temporaneamente non disponibile, riprova più tardi»;
  • il sistema non continua a tempestarne l’API caduta con retry infiniti (i pattern Circuit Breaker ecc. li vedremo più nel dettaglio nel modulo sulla resilienza).

Esempio di handler MCP‑tool che tiene conto dell’errore esterno:

// mcp/tools/createGiftIdea.ts
export async function createGiftIdea(args: Input): Promise<Output> {
  try {
    return await callOpenAiModel(args);
  } catch (err) {
    await logAudit({
      actor: { userId: args.userId ?? null, client: "chatgpt-app" },
      action: "giftidea.generate",
      outcome: { status: "failure", reason: "openai_unavailable" },
    });
    throw new Error("UPSTREAM_UNAVAILABLE");
  }
}

Poi il vostro strato tra MCP e widget saprà mostrare con cura l’errore nell’UI.

Verifica del ripristino: un backup senza restore è solo un file

Anti‑pattern classico: si esegue il backup ogni giorno, tutti contenti… finché non si scopre che non è possibile ripristinarli (il formato è cambiato, la chiave è persa, lo spazio non bastava).

Piano minimo:

  • periodicamente (per esempio, una volta al mese) alzare un ambiente di staging da un backup;
  • percorrere gli scenari base: login, creazione ordine, funzionamento dell’App;
  • assicurarsi che i tempi di ripristino e la perdita di dati rientrino nei vostri RTO/RPO.

Senza trasformare la lezione in un corso di «religione DevOps», qui basta capire: i processi di backup fanno parte dell’architettura dell’App, non «qualcosa che farà qualcuno nel cloud».

6. Visualizzazione: ciclo di vita dei dati e degli eventi

Per non lasciare tutto solo in forma di parole, disegniamo due semplici schemi.

Ciclo di vita dei dati dell’utente

flowchart TD
  A["Creazione dei dati<br/>(registrazione, ordine)"] --> B["Conservazione e uso<br/>(prod DB)"]
  B --> C["Archivio/aggregazione<br/>(metriche anonime)"]
  B --> D[Richiesta di cancellazione]
  D --> E[Cancellazione/anonimizzazione<br/>nel DB di produzione]
  E --> F["Scadenza del periodo di vita dei backup<br/>(retention)"]
    

L’idea principale: il ciclo di vita dei dati non finisce nel DB di produzione — continua anche nei backup.

Flusso di audit per un’azione pericolosa

sequenceDiagram
  participant User as Utente
  participant ChatGPT as ChatGPT
  participant App as Il tuo backend/MCP
  participant DB as Database
  participant Audit as Archivio audit

  User->>ChatGPT: "Annulla l’ordine #123"
  ChatGPT->>App: callTool cancel_order
  App->>DB: UPDATE orders SET status='canceled'
  App->>Audit: INSERT audit_event {actor, action, resource, outcome}
  App-->>ChatGPT: Risultato dell’operazione
  ChatGPT-->>User: Messaggio sul risultato
    

7. Errori tipici in audit & lifecycle

Errore n. 1: Mescolare i log di audit con i log applicativi.
Quando tutti i messaggi finiscono in un unico index logs, dopo sei mesi nessuno distinguerà «l’utente ha modificato il ruolo di amministratore» da «abbiamo di nuovo un riferimento null». Nell’audit devono esserci eventi strutturati a livello business (vedi la sezione sulla struttura dell’evento di audit) e un archivio separato con accesso limitato.

Errore n. 2: Loggare la PII in audit e nei log di debug.
E‑mail complete, telefono, indirizzo di spedizione, ultime quattro cifre della carta — tutto questo spesso finisce per errore nei log. Aumenta il rischio di violazione e contraddice le raccomandazioni sulla privacy. Invece, loggate identificatori e valori mascherati.

Errore n. 3: Assenza di una policy di retention — «teniamo tutto per sempre».
In fase di MVP sembra «vabbè», ma dopo un anno le tabelle crescono a dismisura e qualsiasi query analitica diventa un DDoS sul DB. In più violate il principio di minimizzazione previsto dalle moderne leggi sui dati. I TTL minimi per tipo di dato vanno pensati e la pulizia automatizzata.

Errore n. 4: «Cancellazione su richiesta» == DELETE FROM users.
Se avete semplicemente eliminato la riga dell’utente, ma avete lasciato la sua PII in ordini, sessioni e log, in sostanza non avete eliminato nessuno. L’approccio corretto è passare in transazione su tutte le entità collegate; dove non si può eliminare — anonimizzare. E non dimenticare di loggare il fatto stesso della cancellazione come evento di audit.

Errore n. 5: Ignorare i backup durante la cancellazione dei dati.
Avete eliminato l’utente in prod — bene, ma i suoi dati vivono ancora per un anno negli snapshot vecchi. Al ripristino da questi tutto «risorge», e violate di nuovo le vostre promesse all’utente e nella Privacy Policy. Bisogna o limitare il tempo di vita dei backup, oppure avere una procedura di riapplicazione delle cancellazioni dopo il ripristino.

Errore n. 6: «Abbiamo i backup attivi, quindi tutto ok», ma nessuno ha provato il ripristino.
Un backup che non è mai stato provato nel restore è solo un file costoso. Senza verifiche periodiche di ripristino non conoscete né il RTO/RPO effettivo, né se il vostro piano di DR funzioni davvero. Minimo — un rialzo regolare di uno staging da backup con check‑list.

Errore n. 7: Disallineamento tra documentazione e realtà.
Nella Privacy Policy scrivete che conservate i log per 30 giorni e cancellate i dati su richiesta, ma nel codice tutto resta per sempre. Il marketplace di ChatGPT, i clienti enterprise e gli auditor lo scopriranno facilmente con domande tipo «mostrateci la tabella di retention» e «dimostrate la cancellazione di un utente specifico». Meglio prima fare, poi scrivere.

1
Sondaggio/quiz
Sicurezza, livello 15, lezione 4
Non disponibile
Sicurezza
Sicurezza e conformità
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION