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
- L'utente apre la view Report Bug dal cluster utility del launcher.
- L'utente compila titolo, descrizione, categoria e opzionalmente trascina fino a 3 screenshot.
- 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. - Al submit, il main process chiama la Edge Function
report-bugcon 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. - La Edge Function persiste la riga del report e restituisce
{ id, created_at }. - L'
INSERTscatena il trigger Postgresbug_reports_to_linear, che invoca la edge functionbug-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 inbug_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 |
|---|---|
| Trigger | bug_reports_to_linear (AFTER INSERT su public.bug_reports) |
| Edge function | bug-report-to-linear (verify_jwt: false, auth tramite shared secret) |
| Linear team | EC |
| Linear project | Community Bug Reporting |
| Stato iniziale | Backlog |
| Label | Bug, from-app |
| Formato titolo | [{category}] {title} |
| Body della issue | description + embed degli screenshot + tabella versioni + hw_config collassabile + electron_runtime collassabile |
| Commento cross-ref | link alla riga Supabase + nota di auto-creazione |
| Policy di retry | 3 tentativi, backoff 1s / 4s / 16s per chiamata Linear |
| Upload immagini | ciascuna immagine caricata individualmente; fallimenti parziali non bloccano la creazione della issue |
| Idempotency | skip se linear_ticket IS NOT NULL |
| Writeback dell'identificatore | UPDATE 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:
| Secret | Dove | Scopo |
|---|---|---|
LINEAR_OAUTH_TOKEN | Edge Function secret | Token applicativo OAuth2 (workspace-scoped) usato per chiamare la GraphQL API di Linear. |
BUG_REPORT_WEBHOOK_SECRET | Edge Function secret | Shared secret presentato come x-webhook-secret dal trigger. La function rifiuta le chiamate che non corrispondono. |
bug_report_webhook_secret | vault.secrets | Stesso 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:
- Esegui il deploy della edge function
bug-report-to-linear. - Aggiungi i tre secret (
LINEAR_OAUTH_TOKEN,BUG_REPORT_WEBHOOK_SECRETnei secret della Edge Function,bug_report_webhook_secretnel Vault). Le due voci del webhook-secret devono avere lo stesso valore. - Applica la migration
20260516280000_bug_reports_to_linear_webhook.sqlper 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:
- Aggiorna la env var della Edge Function dalla dashboard.
- 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 IPC | UPLOAD_BUG_REPORT_IMAGE |
| Storage bucket | bug-report-images (privato) |
| Object path | {userId}/{uuid}.{ext} |
| Max immagini per report | 3 |
| Dimensione massima per immagine | 5 MB |
| MIME type ammessi | image/png, image/jpeg, image/webp, image/gif |
| TTL del signed URL | 15 minuti (il worker bug-report-to-linear rifirma con TTL 7 giorni quando carica su Linear) |
| Modalità di upload | upsert: 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 sebucket_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 IPC | SUBMIT_BUG_REPORT |
| Edge Function | report-bug (verify_jwt: true) |
| Campi obbligatori | title, description, category |
| Categorie | ui, recording, performance, onboarding, other |
| Lunghezza massima titolo | 200 caratteri |
| Lunghezza massima descrizione | 10.000 caratteri |
| Contesto auto-allegato | installer_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_IMAGErifiuta 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/pngma contengono byte HTML / SVG / eseguibili vengono rifiutati conbad_mime. - Estensione derivata solo dal MIME sniffato. Il
fileNameoriginale 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 chiamatopwn.htmlverrebbe altrimenti salvato come.htmlsotto 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(daplayroll-coreGET /api/hw/snapshot) eelectron_runtime(daapp.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.warncon 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
| Codice | Quando viene emesso | Comportamento UI |
|---|---|---|
unauthorized | Nessuna sessione Supabase, o token rifiutato dall'edge | Submit bloccato, l'utente deve ri-autenticarsi |
rate_limited | La edge function ritorna un errore di rate-limit | Submit bloccato, chiede all'utente di riprovare più tardi |
validation_failed | Mancano title/description, categoria non valida | Submit bloccato con hint a livello di campo |
invocation_failed | Edge function 5xx o response malformata | Feedback di errore generico |
too_large | Immagine > 5 MB | Immagine non aggiunta alla draft, le altre proseguono |
bad_mime | MIME fuori dalla whitelist dei 4 tipi | Immagine rifiutata, le altre proseguono |
upload_failed | Errore di rete, errore storage, configurazione mancante | Immagine 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:
- Purge time-based. Una Edge Function schedulata (cron) elimina gli object in
bug-report-imagespiù vecchi di 90 giorni dalcreated_at. Default raccomandato. - Purge status-driven. Quando il bug report collegato transisce a
resolvedoclosed, 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(cercaSUBMIT_BUG_REPORT,UPLOAD_BUG_REPORT_IMAGE) - Edge function:
report-bug(Supabase) - Storage bucket:
bug-report-images(privato)