O bug que só existe numa configuração que ainda ninguém usa
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
Analytics que pergunta antes: construindo uma biblioteca de rastreamento só opt-in
O script de rastreamento não existe até você consentir. Não desativado. Não pausado. Ausente. É assim que funciona esta biblioteca de analytics só opt-in.
Criando uma biblioteca compartilhada de links de afiliados para dois sites
Conformidade por construção: um plugin remark que falha o build quando um link de afiliado sai sem sua divulgação, empacotado do mesmo jeito que os componentes de blog compartilhados.
Entregando um recurso de pacote compartilhado em dois repositórios para uma única rota
Um feed RSS que se dividiu num helper puro no pacote compartilhado e numa rota local por site, além da disciplina de release que uma dependência de tarball fixada por tag exige.
Você também pode achar útil
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 maisAdGuard 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 maisRackNerd VPS
Hospedagem VPS econômica para serviços leves que funcionam continuamente.
Como afiliado da RackNerd, ganho com compras qualificadas.
Saiba mais