Rastreamento de erros JavaScript em apps web pequenos: o que você precisa de verdade

novidades do produto

Para um app web pequeno, o rastreamento de erros JavaScript se resume a quatro coisas: capturar todo erro não tratado e toda promise rejeitada sem tratamento, agrupar as repetições para que cem falhas apareçam como um bug só, transformar stack traces minificadas de volta no seu próprio código e guardar o console e a atividade de rede do minuto anterior. Dashboards, regras de alerta e tracing de performance podem esperar até que olhar para eles seja o trabalho de alguém.

Este post passa pelas quatro, pelos detalhes do navegador por trás de cada uma e pelo ruído que dá para jogar fora. Acabei de lançar a captura de erros no BugDump, então os exemplos vêm dele, mas a mecânica é a mesma em qualquer ferramenta que você escolher.

Seus usuários não vão avisar

Quando um botão de finalizar compra não faz nada, a maioria das pessoas tenta de novo, suspira e vai embora. Algumas escrevem, geralmente dias depois, com uma descrição como «não funciona no meu celular». O erro estava lá o tempo todo, num console que ninguém olhava.

Esse é o argumento a favor do rastreamento de erros numa frase: ele transforma os bugs que ninguém relata numa lista que você consegue resolver. Para um app pequeno, a lista importa mais do que qualquer gráfico desenhado em cima dela.

1. Capturando erros: dois eventos

Os navegadores relatam falhas por dois eventos, e você precisa dos dois:

js
window.addEventListener('error', (event) => {
  report(event.error ?? new Error(event.message));
});

window.addEventListener('unhandledrejection', (event) => {
  report(event.reason);
});

error dispara para uma exceção que nada capturou: um erro de digitação, a leitura de uma propriedade em undefined, a chamada de uma função que não existe. Já unhandledrejection dispara quando uma promise falha sem ninguém escutando. Em código que é quase todo async e await, é nesse segundo evento que a maioria dos erros vai parar. Um fetch que recebe uma página de erro em HTML e chama response.json() nela nunca chega a window.onerror.

Três coisas que esses eventos não entregam:

  • Erros que você mesmo captura. Um try/catch que mostra uma mensagem amigável também esconde o erro dos dois eventos. Se você ainda quer saber dele, relate-o a partir do bloco catch.

  • Os detalhes de erros de outras origens. Um script de outro domínio, carregado sem CORS, falha como Script error., sem arquivo, linha nem stack. O navegador esconde isso de propósito. Carregue o script com crossorigin="anonymous" e sirva-o com um cabeçalho Access-Control-Allow-Origin, ou aceite que não dá para ver o que acontece dentro dele.

  • Erros antes de o rastreador carregar. Se o script de rastreamento carrega por último, as falhas durante a inicialização se perdem. Carregue-o cedo.

Os listeners são a parte fácil. O resto deste post trata do que acontece depois de report().

2. Agrupamento: mil eventos, um bug

Um erro que atinge todos os visitantes de uma página chega milhares de vezes. Você quer uma linha com uma contagem, então é o agrupamento que decide se a lista tem alguma utilidade.

A abordagem comum, e a que eu uso, monta uma impressão digital (fingerprint) a partir de três partes:

  • o tipo do erro, como TypeError;

  • a mensagem sem as partes variáveis: Order 58213 not found e Order 61007 not found são o mesmo erro, então números, URLs, e-mails e IDs viram marcadores;

  • os primeiros frames do seu próprio código, por arquivo e função, não por linha, para que uma edição sem relação mais acima no arquivo não divida o grupo.

A página Erros do projeto Acme Store: 29 eventos nos últimos 14 dias e três erros novos, cada um com o número de eventos, a versão e o local no código-fonte via source map

Funciona bem, mas não é perfeito. Os navegadores descrevem a mesma falha com palavras diferentes: o Chrome diz Cannot read properties of undefined (reading 'total'), o Safari diz undefined is not an object (evaluating 'cart.summary.total'). Eles também montam as stacks de um jeito diferente; o Firefox acrescenta um frame para o lugar onde um callback foi registrado. Então, de vez em quando, um bug chega como dois erros, um por navegador. Uma ferramenta deveria deixar você vincular os dois à mesma tarefa e seguir em frente, em vez de fingir que isso nunca acontece.

3. Ruído: o que jogar fora primeiro

Alguns erros não são seus, e alguns nem são erros de verdade. Descarte-os antes que cheguem à sua lista, ou a lista deixa de valer a leitura.

O que você vê

Por que acontece

O que fazer

Script error.

um script de outra origem, escondido pelo navegador

descartar, ou adicionar crossorigin e cabeçalhos CORS para enxergá-lo

um frame em chrome-extension:// ou moz-extension://

uma extensão do navegador injetou código na sua página

descartar

ResizeObserver loop completed with undelivered notifications

um loop de layout inofensivo sobre o qual o navegador avisa

descartar

gtag is not defined

um bloqueador de anúncios removeu um script de analytics que seu código ainda chama

ignorar, ou proteger a chamada

AbortError

uma requisição que o seu próprio código cancelou

ignorar

Mais duas regras mantêm o volume sob controle sem perder nada:

  • Envie cada erro distinto uma vez por sessão. Uma falha dentro de um loop de renderização pode disparar centenas de vezes por segundo numa aba. O primeiro evento já diz tudo.

  • Limite a taxa por página. Dez por minuto é mais que suficiente para uma aba. Qualquer coisa acima disso é um loop, não informação nova.

A amostragem, que envia só uma parte dos erros, ajuda quando o tráfego é grande o bastante para que 10% dele ainda encontre todos os bugs. Num app pequeno, ela quase só esconde o erro raro que você mais precisava ver. Deixe em 100% até a contagem mensal dizer o contrário.

4. Stack traces legíveis sem publicar o seu código-fonte

O código de produção é minificado, então uma stack trace crua fica assim:

Plain Text
TypeError: Cannot read properties of undefined (reading 'subtotal')
    at T (https://acme-store.com/assets/app-L5L4OHQM.js:2:1095)
    at x (https://acme-store.com/assets/app-L5L4OHQM.js:2:2081)

Um source map transforma isso de volta em renderCartSummary em src/cart/cart-summary.ts, linha 6. O problema é onde os maps ficam. Servi-los ao lado do seu JavaScript funciona para o DevTools e também publica o seu código-fonte para qualquer pessoa que abrir a aba de rede.

O padrão melhor são os source maps ocultos (hidden). O bundler gera os arquivos .map sem apontar o seu JavaScript para eles, o seu CI os envia para o rastreador de erros, e eles nunca chegam ao seu servidor.

Depois, o rastreador precisa achar o map certo para cada frame. Associar pela URL do arquivo e pelo nome da release é o jeito antigo, e ele quebra fácil: uma CDN muda a URL, um nome de release não bate, dois builds compartilham um nome. O jeito mais confiável, para o qual o Sentry migrou e que eu também uso, é um debug ID. Uma etapa depois do build calcula um ID a partir do conteúdo de cada arquivo e o grava no arquivo e no map dele. Em tempo de execução, o arquivo registra seu ID, o erro o carrega, e o map é encontrado só pelo ID.

Na prática, são três comandos:

Bash
vite build --sourcemap hidden
npx @bugdump/cli sourcemaps inject dist
npx @bugdump/cli sourcemaps upload dist --release "$GIT_SHA" --delete-after

A página de um TypeError vinculado à tarefa AS-6, com a stack trace mapeada para src/cart/cart-summary.ts e a linha que falhou destacada no código-fonte original

5. O minuto antes do erro

Uma stack trace diz onde o código desistiu. Muitas vezes não diz por quê.

Um exemplo da loja de demonstração. O erro é SyntaxError: Unexpected token '<', "<!DOCTYPE "... is not valid JSON, lançado dentro de um pequeno helper getJson(). A stack aponta para JSON.parse, o que está correto e não ajuda em nada. O log de rede do mesmo evento explica: GET /api/recommendations voltou como uma página 502 do balanceador de carga, e o código tentou interpretar o HTML como JSON.

Eventos armazenados de um SyntaxError vindos de cinco visitas no Chrome e no Edge, com a aba Rede mostrando a requisição GET /api/recommendations que retornou 502

Por isso um evento de erro deveria trazer o último minuto de logs do console, requisições de rede e cliques, as mesmas evidências de um bom relatório de bug. Sem isso, metade dos seus erros acaba como «não consigo reproduzir».

A outra metade do contexto são os seus usuários. Na mesma loja de demonstração, a tarefa AS-1 foi escrita a partir de reclamações de clientes: «Checkout button does nothing on Safari». Isolado, é o relatório clássico com o qual ninguém consegue fazer nada. Na mesma tarefa agora está o erro capturado, visto quatro vezes, só no Safari e no Mobile Safari: window.requestIdleCallback is not a function. O Safari não tem requestIdleCallback, então o handler do clique lança o erro antes de o pedido ser enviado. O que os clientes viram e a linha que explica isso estão no mesmo card.

Tarefa AS-1, Checkout button does nothing on Safari, com o erro vinculado window.requestIdleCallback is not a function, visto quatro vezes no Safari e no Mobile Safari

Essa é a parte que um dashboard de erros separado perde. O bug que um usuário descreve e o erro que o código lança muitas vezes são o mesmo bug, e devem ficar no mesmo lugar.

Você precisa de uma ferramenta de monitoramento completa?

Nem sempre. Produtos dedicados de monitoramento de erros fazem muito mais do que capturar erros do navegador, e, se você precisa do que eles acrescentam, use um. Para um app pequeno, a comparação honesta é esta:

Captura de erros no seu bug tracker

Uma ferramenta de monitoramento dedicada

O que captura

erros e promises rejeitadas de JavaScript no navegador

erros do navegador, do servidor e de mobile, além de performance

Para onde vão os erros

o mesmo quadro dos relatórios de bug dos seus usuários

uma caixa de entrada separada, com links para o seu tracker

Configuração

o widget que você já tem

um segundo SDK, projeto e conjunto de alertas

Alertas

uma notificação para erros novos e que voltaram

regras, limites e roteamento de plantão

Combina com

apps web pequenos, poucos desenvolvedores

equipes com plantão, vários serviços e SLOs

Se o seu app é um frontend e uma API, e as mesmas duas ou três pessoas leem os relatórios de bug e corrigem os erros, um quadro só é menos coisa para configurar e menos coisa para checar. Quando você tem um backend para monitorar, um rodízio de plantão ou um orçamento de performance, uma ferramenta dedicada se paga.

Checklist para um app pequeno

  1. Escute error e unhandledrejection, e carregue o rastreador cedo.

  2. Relate os erros que você mesmo captura, quando ainda se importa com eles.

  3. Defina uma release em todo build, para distinguir um bug novo de uma aba antiga.

  4. Gere source maps ocultos no build e envie-os pelo CI. Nunca faça deploy dos maps.

  5. Descarte na origem os erros de extensões, o Script error. e os avisos inofensivos do navegador.

  6. Envie cada erro uma vez por sessão e deixe a amostragem desligada até o volume exigir.

  7. Transforme erros em tarefas ao lado dos seus relatórios de bug, não numa segunda caixa de entrada que ninguém abre.

Perguntas frequentes

O rastreamento de erros deixa o meu site mais lento? Não de um jeito que você consiga medir. Os dois listeners não fazem nada até algo falhar, e um rastreador razoável envia cada erro distinto uma vez por sessão, em segundo plano.

O que significa «Script error.»? Aconteceu um erro num script de outra origem e o navegador escondeu os detalhes. Adicione crossorigin="anonymous" a essa tag script e faça o servidor dele enviar Access-Control-Allow-Origin, ou descarte esses erros: sem detalhes, não há o que fazer.

É seguro enviar source maps? Enviá-los para o seu rastreador de erros não tem problema. O que expõe o seu código-fonte é servi-los publicamente. Gere maps ocultos, envie-os e apague-os antes do deploy.

Quantos erros um app pequeno vai enviar? Isso depende muito mais de quantos bugs distintos você tem do que do tráfego. Com cada erro enviado uma vez por sessão, uma página com um bug envia um evento por visita afetada, não um por falha.

Testando no BugDump

A captura de erros faz parte do widget do BugDump, em todos os planos, e vem ligada por padrão. O plano Grátis inclui 1.000 eventos de erro por mês por projeto. Comece por Capturar erros JavaScript automaticamente, depois veja Stack traces legíveis: enviando source maps e Trabalhando com os erros capturados.

Leia também