Passa al contenuto principale

Segnalazione bug

Questo runbook documenta la funzionalità Report Bug in-app: come vengono inviate le segnalazioni, dove risiedono gli screenshot allegati e per quanto tempo vengono conservati.

Flusso end-to-end

  1. L'utente apre la view Report Bug dal cluster utility del launcher.
  2. L'utente compila titolo, descrizione, categoria e opzionalmente trascina fino a 3 screenshot.
  3. Ogni screenshot viene caricato individualmente tramite il canale IPC UPLOAD_BUG_REPORT_IMAGE. Il main process inoltra i byte a Supabase Storage usando il JWT dell'utente.
  4. Al submit, il main process chiama la Edge Function report-bug con i metadati della segnalazione, la lista degli URL delle immagini caricate e il contesto macchina (versioni, hardware) raccolto lato server, in modo che il renderer non possa manometterli.
  5. La Edge Function persiste la riga del report e restituisce { id, created_at }.
  6. L'INSERT scatena il trigger Postgres bug_reports_to_linear, che invoca la edge function bug-report-to-linear. Questa funzione rifirma ciascuno screenshot, lo carica su Linear come attachment nativo, crea una issue nel progetto Community Bug Reporting, aggiunge un commento di cross-reference che punta alla riga Supabase originaria e scrive l'identificatore Linear in bug_reports.linear_ticket.

Il triage avviene interamente su Linear. La riga Supabase è il system of record; Linear è il viewer / la queue.

Pipeline Linear

ProprietàValore
Triggerbug_reports_to_linear (AFTER INSERT su public.bug_reports)
Edge functionbug-report-to-linear (verify_jwt: false, auth tramite shared secret)
Linear teamEC
Linear projectCommunity Bug Reporting
Stato inizialeBacklog
LabelBug, from-app
Formato titolo[{category}] {title}
Body della issuedescription + embed degli screenshot + tabella versioni + hw_config collassabile + electron_runtime collassabile
Commento cross-reflink alla riga Supabase + nota di auto-creazione
Policy di retry3 tentativi, backoff 1s / 4s / 16s per chiamata Linear
Upload immaginiciascuna immagine caricata individualmente; fallimenti parziali non bloccano la creazione della issue
Idempotencyskip se linear_ticket IS NOT NULL
Writeback dell'identificatoreUPDATE bug_reports SET linear_ticket = '{identifier}' dopo successo

Se il writeback fallisce dopo che la issue Linear è stata creata, la function restituisce 207 con l'identificatore Linear nella response; un operatore deve incollare a mano l'identificatore in bug_reports.linear_ticket per evitare una issue duplicata al prossimo replay.

Gestione dei secret

Due secret della Edge Function e una voce del Vault alimentano questa pipeline:

SecretDoveScopo
LINEAR_OAUTH_TOKENEdge Function secretToken applicativo OAuth2 (workspace-scoped) usato per chiamare la GraphQL API di Linear.
BUG_REPORT_WEBHOOK_SECRETEdge Function secretShared secret presentato come x-webhook-secret dal trigger. La function rifiuta le chiamate che non corrispondono.
bug_report_webhook_secretvault.secretsStesso valore di BUG_REPORT_WEBHOOK_SECRET. Letto a runtime del trigger tramite vault.decrypted_secrets, così il valore non appare mai in pg_get_triggerdef() o nei file di migration.

Il trigger è definito nella migration 20260516280000_bug_reports_to_linear_webhook.sql. La trigger function (public.bug_reports_to_linear_dispatch) gira come SECURITY DEFINER, così può leggere da vault.decrypted_secrets indipendentemente dai grant del ruolo che esegue l'INSERT.

Provisioning una-tantum

Quando si applica questa pipeline a un progetto fresco:

  1. Esegui il deploy della edge function bug-report-to-linear.
  2. Aggiungi i tre secret (LINEAR_OAUTH_TOKEN, BUG_REPORT_WEBHOOK_SECRET nei secret della Edge Function, bug_report_webhook_secret nel Vault). Le due voci del webhook-secret devono avere lo stesso valore.
  3. Applica la migration 20260516280000_bug_reports_to_linear_webhook.sql per creare il trigger.

Insert nel Vault (una-tantum, da eseguire dall'SQL editor con i permessi appropriati):

select vault.create_secret(
'<shared secret value>',
'bug_report_webhook_secret',
'Shared secret presented as x-webhook-secret by the bug_reports_to_linear trigger.'
);

Rotazione

Per ruotare BUG_REPORT_WEBHOOK_SECRET:

  1. Aggiorna la env var della Edge Function dalla dashboard.
  2. Aggiorna la riga del Vault:
update vault.secrets
set secret = '<new value>'
where name = 'bug_report_webhook_secret';

Non serve nessun re-deploy o replay della migration — il trigger ri-legge dal vault a ogni chiamata.

Upload delle immagini

ProprietàValore
Canale IPCUPLOAD_BUG_REPORT_IMAGE
Storage bucketbug-report-images (privato)
Object path{userId}/{uuid}.{ext}
Max immagini per report3
Dimensione massima per immagine5 MB
MIME type ammessiimage/png, image/jpeg, image/webp, image/gif
TTL del signed URL15 minuti (il worker bug-report-to-linear rifirma con TTL 7 giorni quando carica su Linear)
Modalità di uploadupsert: false (nessun overwrite)

L'upload viene eseguito con un client Supabase per-request che porta l'access token dell'utente, così le RLS dello storage trattano il chiamante come authenticated. Due policy su storage.objects limitano tutti gli accessi alla cartella propria dell'utente:

  • bug_report_images_insert_own — INSERT consentito solo se bucket_id = 'bug-report-images' AND (storage.foldername(name))[1] = auth.uid()::text.
  • bug_report_images_select_own — stessa condizione su SELECT.

Non esistono policy di UPDATE o DELETE — né il renderer né l'utente possono modificare o rimuovere uno screenshot caricato. La pulizia è guidata dall'operatore (vedi Retention dei dati).

Invio del report

ProprietàValore
Canale IPCSUBMIT_BUG_REPORT
Edge Functionreport-bug (verify_jwt: true)
Campi obbligatorititle, description, category
Categorieui, recording, performance, onboarding, other
Lunghezza massima titolo200 caratteri
Lunghezza massima descrizione10.000 caratteri
Contesto auto-allegatoinstaller_version, ui_version, core_version, hw_config (snapshot HW canonico da playroll-core), electron_runtime (auxAttributes di Chromium), connection (snapshot speedtest Cloudflare)

Il main process raccoglie le versioni tramite l'update manager. L'hardware viene recuperato da playroll-core via GET /api/hw/snapshot — lo stesso payload canonico che il core scrive in ogni _meta.json::hardware di ogni registrazione, così bug report e analytics delle registrazioni condividono uno schema unico. I dettagli runtime di Electron/Chromium (vulkanVersion, skiaBackendType, supportsDx12, …) sono raccolti separatamente da app.getGPUInfo("basic").auxAttributes e vivono in una colonna jsonb dedicata electron_runtime — producer diverso (processo GPU di Chromium), scopo diverso (debug UI/renderer, non inventario device). Il renderer non vede e non invia nulla di tutto questo; l'IPC handler compone e inoltra ogni cosa lato server. Vedi anche: Inventario HW dei device.

Postura di sicurezza

Il renderer è considerato non affidabile (la form è user input e un renderer compromesso potrebbe tentare di abusare dei canali IPC). Tutte le decisioni di trust vivono nel main process:

  • Controllo dimensione prima dell'allocazione. UPLOAD_BUG_REPORT_IMAGE rifiuta i payload sopra 5 MB prima di costruire qualsiasi byte view, limitando l'uso di memoria anche sotto renderer ostili.
  • Rate limit di upload per-utente. 20 upload per finestra scorrevole di 10 minuti per utente, enforced in-process. Il cap di 3 immagini nel renderer è cosmetico; il cap nell'IPC è la vera difesa contro il flooding dello storage.
  • Protezione contro MIME spoofing. Il MIME dichiarato deve (a) essere nella whitelist dei 4 tipi e (b) corrispondere ai magic bytes effettivi rilevati nei primi 12 byte del file. File che dichiarano image/png ma contengono byte HTML / SVG / eseguibili vengono rifiutati con bad_mime.
  • Estensione derivata solo dal MIME sniffato. Il fileName originale non viene mai usato per costruire l'object path nello storage; l'estensione è fissata dal MIME verificato (png, jpg, webp, gif). Questo previene attacchi di extension-spoofing in cui un file chiamato pwn.html verrebbe altrimenti salvato come .html sotto il dominio trusted.
  • Titolo / descrizione sanitizzati lato server. I tag HTML vengono rimossi, le mass-mention di Discord (@everyone, @here, mention di ruoli/utenti) vengono neutralizzate e i cap di lunghezza (200 / 10.000 caratteri) sono enforced nell'IPC handler — i cap nel renderer sono di comodità, non di sicurezza.
  • Signed URL a vita breve. I signed URL durano 15 minuti — abbastanza per far renderizzare al renderer una thumbnail e per portare il report nella surface dell'operatore, abbastanza brevi perché un URL trapelato (log, paste su Slack, dump CI) sia inutile dopo. Il tooling dell'operatore rifirma on demand al momento della visualizzazione.
  • Il contesto raccolto lato server non può essere forgiato. installer_version, ui_version, core_version, hw_config (da playroll-core GET /api/hw/snapshot) e electron_runtime (da app.getGPUInfo("basic").auxAttributes) vengono raccolti nel main process. Il renderer non può fare override tramite il payload IPC.
  • Messaggi di errore generici verso il renderer. Gli errori upstream da Supabase Storage e dalla edge function vengono loggati lato server (console.warn con prefisso [Playroll]) ma sostituiti con stringhe generiche "Upload failed." / "Could not submit report." prima di tornare al renderer. Path interni, hint SQL e nomi di bucket non vengono esposti.

Assunzione di trust (by design)

Un utente che segnala un bug dalla propria macchina può allegare qualsiasi screenshot, anche uno contenente contenuti di terze parti (es. UI di gioco visibile, altre app in background). Questo è trattato come responsabilità dell'utente — sta acconsentendo all'upload inviando il report. La feature non è una surface di prevenzione data-leak; è un canale di supporto.

Codici di errore

CodiceQuando viene emessoComportamento UI
unauthorizedNessuna sessione Supabase, o token rifiutato dall'edgeSubmit bloccato, l'utente deve ri-autenticarsi
rate_limitedLa edge function ritorna un errore di rate-limitSubmit bloccato, chiede all'utente di riprovare più tardi
validation_failedMancano title/description, categoria non validaSubmit bloccato con hint a livello di campo
invocation_failedEdge function 5xx o response malformataFeedback di errore generico
too_largeImmagine > 5 MBImmagine non aggiunta alla draft, le altre proseguono
bad_mimeMIME fuori dalla whitelist dei 4 tipiImmagine rifiutata, le altre proseguono
upload_failedErrore di rete, errore storage, configurazione mancanteImmagine marcata con overlay rosso; l'utente può rimuoverla e riprovare

Retention dei dati

Policy attuale: retention indefinita. Gli screenshot delle segnalazioni bug rimangono in bug-report-images finché non vengono rimossi manualmente. Il signed URL embedded in ogni report scade dopo 1 anno, ma l'object sottostante resta nello storage.

Questo è accettabile per il pilot (basso volume, finestra breve). Non è la policy di lungo termine.

Retention pianificata (post-pilot)

Quando i dati del pilot non sono più usati attivamente per il triage, gli screenshot vanno purgati. Due approcci equivalenti; sceglierne uno:

  1. Purge time-based. Una Edge Function schedulata (cron) elimina gli object in bug-report-images più vecchi di 90 giorni dal created_at. Default raccomandato.
  2. Purge status-driven. Quando il bug report collegato transisce a resolved o closed, un trigger o job elimina gli object referenziati dopo un periodo di grazia di 30 giorni.

In entrambi i casi il job deve usare la service-role key (l'unica credenziale che può fare DELETE — non esiste una policy di delete user-facing).

Eliminazione iniziata dall'operatore

Per eliminare gli screenshot di uno specifico utente (richiesta right-to-erasure / GDPR):

-- list objects for the user
select name, created_at, metadata->>'size' as bytes
from storage.objects
where bucket_id = 'bug-report-images'
and (storage.foldername(name))[1] = '<user-uuid>'
order by created_at desc;

-- delete (run from Supabase SQL editor with service role)
delete from storage.objects
where bucket_id = 'bug-report-images'
and (storage.foldername(name))[1] = '<user-uuid>';

Le righe bug_reports associate (se referenziano signed URL scoped a questo utente) vanno gestite nella stessa operazione secondo il runbook DSR pertinente.

Note sui costi

Supabase Storage sul piano Pro include 100 GB di storage e 250 GB di egress al mese. L'overage è fatturato a $0.021/GB/mese (storage) e $0.09/GB (egress). Alla scala del pilot (poche centinaia di utenti × ~2 screenshot × ~1 MB), la footprint del bucket è ben sotto 1 GB e il costo è di fatto zero. Il cap di purge a 90 giorni (una volta implementato) mantiene la footprint stazionaria limitata indipendentemente dal volume di segnalazioni.

Riferimenti

  • View del renderer: src/renderer/features/launcher/views/BugReportView.tsx
  • IPC handler: src/main/ipc/handlers.ts (cerca SUBMIT_BUG_REPORT, UPLOAD_BUG_REPORT_IMAGE)
  • Edge function: report-bug (Supabase)
  • Storage bucket: bug-report-images (privato)