Pular para o conteúdo
Development

O bug que só existe numa configuração que ainda ninguém usa

Por Victor Da Luz•
astroprivacyjavascriptdev-logastro-tools

Nenhum dos dois sites que rodam meu pacote de consentimento de analytics usa as view transitions do Astro. Nem uma página. Então, quando um item do backlog dizia “isso quebra sob ClientRouter,” minha primeira reação foi: isso importa?

Decidi que sim, e consertar isso me ensinou algo sobre a diferença entre um bug e um bug que você ainda não consegue ver.

A configuração que nunca foi testada

ConsentGate e ConsentPrompt se inicializam com uma pequena tag de script inline: importa uma função, chama ela uma vez. Isso funciona porque o Astro roda um script de módulo inline exatamente uma vez por carregamento completo de página. Em cada carregamento de página em produção, a função de boot roda, encontra os elementos do DOM, conecta os listeners. Simples, e correto em produção por meses.

<ClientRouter /> muda o que “uma vez por carregamento de página” significa. Ligue essa funcionalidade e o Astro passa a fazer soft navigations: clicar em um link troca o conteúdo da página sem um reload completo. Scripts de módulo inline não rodam de novo numa soft nav; para o sistema de módulos, eles já rodaram. Então os botões de aceitar e recusar do prompt de consentimento, vinculados a qualquer elemento do DOM que existia na primeira página, ficam presos a nada assim que uma soft nav troca por uma cópia nova.

Dois bugs, não um

Eu presumi que fosse um único problema de “listeners não se reconectam.” São dois, e o segundo é o interessante.

O bug óbvio: os próprios botões do prompt estão vinculados a uma instância específica de elemento. Somem depois de uma troca.

O sutil: existe um link no rodapé, “Analytics preferences,” que reabre o prompt sob demanda. O handler dele foi escrito uma vez no momento do boot e capturou uma referência ao elemento do prompt daquela página dentro de um closure:

document.addEventListener(OPEN_PROMPT_EVENT, () => {
  prompt.hidden = false; // `prompt` is whatever it was when this ran
});

O próprio document sobrevive a uma soft nav, é o mesmo objeto document o tempo todo. Então esse listener nunca é removido, nunca precisa se reconectar, e continua disparando para sempre. Só que ele dispara contra uma variável prompt que parou de apontar para qualquer coisa visível no momento em que aconteceu a primeira navegação. O listener não está quebrado. O dado dele é que está obsoleto.

Essa distinção mudou a correção. Meu primeiro instinto foi “rodar a função de boot inteira de novo a cada navegação,” o que registraria esse mesmo handler de novo a cada navegação, para sempre, cada um fechando sobre o que quer que o prompt fosse naquele momento. Inofensivo no sentido de que só o handler mais recente encontraria um elemento real para tocar, mas é uma pilha sem limite de closures mortos se acumulando pela duração de uma sessão. Um vazamento de memória disfarçado de correção.

O que realmente resolveu

Duas regras diferentes para dois tipos diferentes de listener. Qualquer coisa vinculada a um elemento específico do DOM, os próprios botões do prompt, precisa se reconectar a cada navegação, porque o elemento é de fato novo a cada vez. Qualquer coisa vinculada a document precisa ser vinculada uma única vez, e o handler dela deveria buscar o que quer que vá tocar no momento em que dispara, em vez de fechar sobre uma referência capturada no registro:

document.addEventListener(OPEN_PROMPT_EVENT, () => {
  const current = document.getElementById('oia-prompt'); // fresh, every time
  if (current) current.hidden = false;
});

Rodar a função de boot de novo no evento astro:page-load do Astro, que dispara no primeiro carregamento e em toda soft nav depois dele, resolve o lado da reconexão de elementos. Uma flag de uma linha em nível de módulo impede que os listeners em nível de document se registrem duas vezes.

Verificando um bug que não existe em produção

Aqui está a parte que eu não conseguia pular. Nenhum dos sites reais usa <ClientRouter />, então nenhuma página em produção exibia esse comportamento de fato. Testes unitários também não ajudariam, já que isso é conexão de eventos do DOM e o pacote é deliberadamente livre de dependências, sem jsdom.

Então adicionei temporariamente <ClientRouter /> ao layout de um dos sites, sem commitar, só para fabricar a condição de falha por tempo suficiente para provar a correção. Entrei na listagem do blog, cliquei num post (uma soft navigation de verdade, não um carregamento novo), e verifiquei que o prompt aparecia, o botão de aceitar funcionava e, o caminho específico que o bug do closure quebrava, que o link de reabertura no rodapé apontava para o prompt da página atual, e não para um fantasma da primeira. Depois desfiz completamente a mudança no layout.

É uma sensação estranha, ligar uma funcionalidade no seu próprio site só para criar um bug de propósito e poder se ver corrigindo ele. Mas não havia outro jeito de ver aquilo acontecer, e uma correção que você não vê falhar primeiro não está de fato verificada.

O que eu faria diferente

Eu ficaria mais desconfiado de “nenhum site usa X” como motivo para despriorizar algo. É verdade, e também é exatamente a condição sob a qual um bug fica parado numa base de código por meses sem que ninguém perceba, porque nada está exercitando o caminho de código que está errado. Prioridade baixa não é a mesma coisa que risco baixo.

Leitura relacionada

Você também pode achar útil

Airalo

eSIM Airalo

eSIM de dados local para viagens - sem necessidade de trocar um SIM físico.

Este é meu link de indicação da Airalo. Você recebe um desconto no seu primeiro eSIM e eu ganho crédito da Airalo para o meu.

Saiba mais
AdGuard

AdGuard para iOS

Bloqueio de anúncios e rastreadores em todo o sistema no iOS, sem necessidade de um servidor DNS separado.

Como afiliado da AdGuard, ganho com compras qualificadas.

Saiba mais
RackNerd

RackNerd VPS

Hospedagem VPS econômica para serviços leves que funcionam continuamente.

Como afiliado da RackNerd, ganho com compras qualificadas.

Saiba mais