Um bug silencioso cegou o nosso funil de vendas | Excelsi
[T-04-pt]2026-10-15•Excelsi

Um bug silencioso cegou o nosso funil de vendas

Durante meses, a nossa qualificação de leads mediu 'Dor Digital' sem nunca a ter recolhido. O bug não deu erro: devolveu None. Contamos o que o causou e o teste de regressão que fecha a classe.

⏱5 min de leitura

Um None num relatório parece falta de dados. Quase nunca é. É, muito mais vezes, um bug que ninguém viu – porque falhou em silêncio.

O que o pipeline faz

Temos um serviço interno que recolhe registos de empresas e sinais sobre elas, e depois pontua cada registo. O resultado é uma lista ordenada de leads para prospeção. Nada disto é sofisticado: são coletores que escrevem linhas, e um scorer que lê essas linhas e produz um número.

Há coletores para diretórios de empresas, para dados geográficos abertos, e um para auditar o próprio site da empresa – este último é o que interessa aqui.

O bug: registado não é o mesmo que agendado

Havia um coletor, o site_auditor, que fazia exatamente o que o nome diz: visita o site da empresa e mede três coisas – se tem TLS, se é um site antigo e se é responsivo em telemóvel. Essas três coisas alimentam uma componente do score chamada Dor Digital, que vale no máximo 30 pontos de um score composto de 80 (números do nosso projeto, não benchmarks de mercado).

O coletor estava construído. Estava registado no arranque do serviço. Tinha testes. Faltava uma coisa: estava registado, mas não estava na lista de jobs diários. O agendador e o arranque derivavam ambos da mesma lista, e essa lista simplesmente não o incluía.

Consequência direta: o job de auditoria nunca era enfileirado. Nenhum registo recebia os sinais do auditor – ficavam todos None – e a componente de Dor Digital era pontuada a zero, não por ser zero, mas por não existir matéria-prima para a medir.

Não consigo datar com rigor quando isto começou: o código não deixou testemunho da data exata e o sinal nunca chegou a ser preenchido, por isso não há série histórica para trás. O que sei é que esteve assim por todo o tempo em que o pipeline correu com esta versão.

Porque é que não deu erro

Duas razões, ambas banais e ambas perigosas.

A primeira é que um campo por preencher não é uma exceção. has_ssl = None não rebenta nada. O scorer olha para o None, não encontra sinal, e devolve zero. O sistema funciona. O relatório sai. Está tudo partido e tudo parece bem.

A segunda é a forma como o score está desenhado: ele soma o que tem. Uma componente em falta não reduz o total nem marca a linha – simplesmente não entra. Um score de 0 e um score de “não sei medir isto” são indistinguíveis à saída. Foi preciso ir ao código para ver a diferença.

O segundo bug, encontrado ao ligar as peças

Quando fomos agendar o auditor, descobrimos que ligar só o agendamento não resolvia o problema. O scorer decidia se havia site a auditar com base no URL estar preenchido (bool(signal.website)), e não com base em o site responder de facto (o campo website_found, que é precisamente o que o auditor preenche).

Ou seja: um site morto – o sinal mais forte que temos, porque uma empresa sem site funcional tem uma dor digital óbvia – continuava a medir Dor Digital a zero, porque o URL “existia”. O auditor podia correr à vontade que o resultado não passava.

Isto só apareceu porque corremos o auditor sobre sites reais com o resultado esperado escrito antes de o ver. Se tivéssemos assumido que “agendar” era o fix completo, teríamos fechado o ticket com o bug a meio.

O que mudou

Três coisas, por ordem de importância:

  1. O auditor passou a estar no agendamento diário, com prioridade baixa para correr depois dos coletores que alimentam a lista de sites.
  2. A lista de sites a auditar é resolvida no momento do disparo, a partir dos registos que os outros coletores já gravaram (URLs distintos, até 500 por job). Resolver no arranque não servia: no arranque a base de dados ainda está vazia, e a biblioteca de agendamento congela os argumentos no momento em que o job é criado.
  3. O scorer passou a usar website_found quando o sinal existe, e só cai para “o URL está preenchido” quando o coletor não preenche esse campo. Um site morto passou a medir Dor Digital 30.

Fizemos ainda uma limpeza: removemos um coletor que, por desenho do próprio código, violava os termos de serviço da sua fonte. Não é o tema deste artigo, mas é o tipo de coisa que aparece quando se revisita o registo de coletores.

O teste que fecha a classe

A correção é fácil. O que impede a repetição é o teste.

Escrevemos um teste de regressão – poucas linhas – que faz uma pergunta simples: todos os coletores registados estão no agendamento diário? Compara o conjunto de coletores que o serviço constrói com o conjunto de coletores que o agendador realmente agenda. Se houver um registado e não agendado, falha, e – isto é a parte útil – di-lo pelo nome.

Contra o código original, o teste falha assim:

AssertionError: Coletores registados em build_collectors() mas ausentes do agendamento diário: ['maps', 'site_auditor']

Contra o código corrigido, passa. A beleza deste teste é que ele não verifica este bug; verifica a classe de bug. Um coletor novo que alguém registe e se esqueça de agendar deixa de poder passar despercebido: o CI apanha-o antes do humano.

A lição

Má qualidade de leads raramente vem de pouca recolha. Vem de pouca verificação.

Recolhemos imenso e verificamos pouco. A parte que produzia sinal estava construída, testada e desligada – e nenhum teste perguntava se estava ligada. Um sistema de dados não se mede pelo que é capaz de recolher, mas pelo que consegue provar que recolheu.

O que continua em aberto

Sem falso otimismo: dois dos coletores continuam partidos e não foram tocados. Um está bloqueado por captcha a partir de datacenter (precisa de proxy residencial ou resolução de captcha). Outro precisa de uma chave de API e de um enriquecimento dos registos que ainda não existe. Sem esses dois, o topo da lista de leads ainda não povoa. O bug que aqui descrevemos está fechado; o funil, ainda não.