Skip to main content
Antes da v0.18.16, as verificações de sanidade de campanha eram ad-hoc — exportações de CSV e scripts pontuais executados manualmente antes de um envio de risco. O preflight de segurança de campanha (BORD-699/700/701) substitui isso por um registro de banco de dados durável e auditável do que foi verificado, quando, e contra qual conjunto exato de destinatários — e uma trava de lançamento opt-in que pode se recusar a lançar uma campanha cujo preflight não passou.
O preflight é desativado por padrão. Campanhas existentes são lançadas exatamente como antes, a menos que os metadados de uma campanha explicitamente ativem o opt-in via launchSafety.requireSafetyPreflight.

O que uma execução de preflight faz

runCampaignSafetyPreflightInSupabase executa uma única passagem determinística sobre os destinatários enfileirados de um batch de campanhas:
  1. Resolve as campanhas dentro do escopo — ou todas as campanhas que compartilham uma batchKey (uma “família” de campanhas, correlacionada via campaigns.metadata->>'batchKey'), ou uma lista explícita de ids de campanha.
  2. Carrega os campaign_recipients enfileirados dessas campanhas, unidos (joined) às colunas de contato relevantes para segurança (consent_state, subscriber_status, deleted_at, deletion_requested_at, metadata).
  3. Abre uma linha de audit (campaign_safety_audits, status running).
  4. Executa todos os gates determinísticos registrados sobre o conjunto de destinatários e coleta os findings.
  5. Persiste os findings (campaign_safety_audit_findings) e completa o audit — gravando um recipient_set_hash sobre os ids de destinatários ordenados e as contagens de severidade consolidadas, então muda o status para completed.
  6. Em caso de qualquer erro após a abertura da linha de audit, o audit é marcado como failed com o motivo do erro, e o erro é relançado (re-thrown) — um preflight nunca reporta sucesso silenciosamente em caso de falha (crash).

Gate disponível: DNC

O único gate ativo hoje é o dncGate — uma verificação autocontida, baseada apenas em dados do Switchbord. Ele marca um destinatário como do-not-contact (severidade dnc, tipo de finding dnc_opted_out) quando qualquer uma destas condições é verdadeira, e registra quais delas corresponderam como evidência:
  • consent_state === "opted_out"
  • subscriber_status === "unsubscribed"
  • o contato tem deleted_at definido
  • o contato tem deletion_requested_at definido
O adapter tem um ponto de extensão documentado para gates de identidade expandida (Travio/Neo4j/UDB de contato prévio, hits de status de viagem, correspondência fuzzy de pax — rastreados como BORD-702/703/704/705). Esses gates precisam de contexto de identidade resolvida mais rico do que apenas o conjunto de destinatários fornece, e estão intencionalmente fora do escopo deste preflight inicial.

Escada de severidade

Os findings carregam uma de sete severidades, da mais para a menos grave:
Apenas findings hard_block bloqueiam o lançamento hoje. O gate DNC disponível deliberadamente usa dnc, não hard_block — um audit completado apenas com findings DNC ainda passa pela trava de lançamento. Trate findings DNC como um sinal de revisão do operador, não como um bloqueador de lançamento, até que o BORD-706 (modo apply-withholds) seja lançado.

A trava de lançamento

Uma campanha ativa o opt-in para a trava via seus próprios metadados (launchSafety.requireSafetyPreflight: true) ou por quem chama a função passando explicitamente requireSafetyPreflight: true na chamada de lançamento. Quando habilitada, launchCampaignInSupabase se recusa a lançar a menos que todas as condições abaixo sejam válidas:
  1. Existe um audit completado para a batchKey desta campanha (ou id da campanha, se não houver batch key).
  2. O hard_block_count desse audit é exatamente zero.
  3. O recipient_set_hash do audit corresponde a um hash em tempo real (live) recalculado sobre os destinatários enfileirados atuais da campanha no momento do lançamento.
Se qualquer verificação falhar, launchCampaign lança uma exceção com um motivo específico (no completed campaign safety preflight audit found, ... has N hard-block finding(s), ou recipient set changed since audit ...; rerun preflight) e marca a tentativa de lançamento como falha — ela nunca lança parcialmente.

Por que a verificação de hash existe

O recipient_set_hash é um digest prefixado com sha256: dos ids de campaign_recipient enfileirados e ordenados que o audit efetivamente cobriu. Recalcular e comparar esse hash no momento do lançamento fecha uma lacuna óbvia: se destinatários forem adicionados ou alterados depois que o preflight foi executado (uma audiência maior, um batch rematerializado, uma edição manual de destinatário), o audit é evidência obsoleta e não deve ser confiado. A trava força uma nova execução de preflight em vez de lançar contra uma audiência que ninguém de fato verificou.
Para um audit de batchKey, o hash é calculado sobre a união de todos os destinatários enfileirados em toda a família de batch — correspondendo à população que o próprio preflight hasheou. Um audit por id de campanha hasheia apenas os destinatários enfileirados daquela campanha específica. A trava resolve a mesma população que o audit usou antes de comparar, de modo que os dois hashes são sempre comparáveis entre si (apples-to-apples).

Fail-closed por design

A trava de lançamento foi reforçada contra alguns caminhos de contorno específicos durante a revisão adversarial:
  • A flag de habilitação requireSafetyPreflight é lida a partir do JSON bruto de metadados da campanha, não através do esquema analisado/validado — de modo que um campo de metadados malformado e não relacionado em outro lugar da campanha nunca pode desabilitar silenciosamente o gate.
  • O hash em tempo real de um audit de batchKey é recalculado sobre a família de batch inteira, não sobre uma única campanha, de modo que nunca pode corresponder trivialmente a uma população mais estreita.
  • Um audit vazio ou um conjunto de destinatários em tempo real vazio é recusado diretamente, em vez de passar “vacuamente” — um preflight de nada não é evidência de que algo foi verificado.

Modelo de dados

Duas tabelas com escopo de workspace, protegidas por RLS (via app.has_workspace_access): campaign_safety_audits — uma linha por execução de preflight. campaign_safety_audit_findings — N linhas por audit, uma por hit de (destinatário, tipo de finding).

Executando um preflight

Ainda não há uma superfície dedicada na UI — runCampaignSafetyPreflightInSupabase(admin, { workspaceId, batchKey | campaignIds, mode?, config? }) é chamado diretamente a partir de ferramentas operacionais antes de um lançamento protegido pela trava. Passe uma batchKey (preferível para uma família com múltiplas campanhas) ou um array explícito de campaignIds; a chamada lança uma exceção se nenhum dos dois for utilizável, de modo que um audit nunca pode ser iniciado contra uma população não delimitada ou ambígua.
Execute o preflight novamente sempre que os destinatários mudarem após uma execução anterior — a trava de lançamento vai rejeitar um audit obsoleto de qualquer forma, mas executar novamente de forma proativa evita uma tentativa de lançamento falha no momento do go-live.

Triagem

Veja também