Voltar ao blog
13 de agosto de 2026Sergei Solod15 min de leitura

Eu alertava por tudo: como transformei o ruído de erros do navegador em monitoramento útil de produção

Meu sistema de reporte no frontend tratava bloqueios de anúncios, falhas do GTM, AbortError, Script error opacos e erros reais de chunks do Next.js da mesma forma. Reconstruí os alertas em torno da propriedade do recurso, do impacto no usuário, da qualidade da evidência, da correlação e da recuperação.

Monitoramento de erros do navegadorObservabilidade frontendErros JavaScriptMonitoramento de produçãoNext.jsPerformance web

Eu criei um sistema de reporte de erros no cliente porque queria enxergar problemas de usuários reais que eu não conseguia reproduzir com confiança localmente. O sistema de reporte fazia exatamente o que eu tinha pedido: capturava falhas e me enviava.

O problema era que quase tudo parecia igualmente importante.

Um script externo de analytics não carregou? Alerta vermelho. Um script de publicidade foi bloqueado? Alerta vermelho. Um rastreador não conseguiu buscar Google Analytics? Alerta vermelho. Um prévia de vídeo chamou play() e foi pausado antes de a Promise terminar? Alerta vermelho. Chegou um Script error. opaco, sem source ou stack útil? Outro alerta vermelho.

No mesmo fluxo estavam falhas que realmente podiam quebrar o produto: uma URL da própria aplicação que por engano virou https://example.comhttps://example.com/... e um arquivo Next.js da própria aplicação em /_next/static/chunks/... que um navegador não conseguiu carregar.

A coleta funcionava. O monitoramento não.

Essa diferença mudou minha forma de pensar observabilidade de frontend. Um evento de erro é evidência de que algo aconteceu. Ainda não é um diagnóstico, uma severidade nem um incidente.

O primeiro erro foi transformar a palavra “error” em prioridade

Meu modelo inicial era praticamente:

o navegador relata um erro
        ↓
enviar CLIENT ERROR
        ↓
o desenvolvedor precisa agir

Isso misturava perguntas diferentes. É código meu? A rota ativa quebrou? Foi um cancelamento esperado? O navegador tinha informação suficiente sobre a origem? A aplicação se recuperou? Dez mensagens representam dez incidentes ou dez sintomas do mesmo incidente?

Enquanto essas perguntas estiverem abertas, um evento não deveria virar alerta automaticamente.

Em uma análise inicial eu tinha cerca de dezoito mensagens. A maioria era ruído de terceiros ou do ciclo de vida normal. Dois casos se destacavam: a URL da própria aplicação malformada era objetivamente errada e o chunk JavaScript da própria aplicação poderia ter deixado código necessário indisponível para a página. Mesmo assim, o sistema de reporte mostrava quase tudo com a mesma urgência de um script publicitário bloqueado.

Foi aí que parei de tratar “coletar todos os erros do navegador” e “construir monitoramento de produção” como o mesmo problema. A coleta preserva evidências. O monitoramento precisa transformar essas evidências em decisões.

O navegador não tem um único canal universal de erro

Falhas diferentes no cliente chegam com semânticas diferentes.

O evento error de window cobre erros síncronos de script e também participa de falhas de recursos. Uma Promise rejeitada sem handler segue outro caminho, via unhandledrejection. Elementos que carregam scripts, imagens ou mídia podem emitir seu próprio error. React e Next.js ainda adicionam error boundaries do framework.

window.error
→ um erro síncrono de script pode ter escapado

unhandledrejection
→ uma Promise rejeitada estava sem tratamento naquele momento

error de elemento
→ um recurso não pôde ser carregado ou usado

framework boundary
→ renderização ou execução chegaram a uma fronteira de erro

Um hook global observa um sintoma numa fronteira do sistema. Ele não conhece necessariamente toda a cadeia causal.

Depois que aceitei isso, parei de normalizar imediatamente tudo para um único Error genérico com a mesma criticidade.

Quem é dono do recurso é o primeiro filtro útil

A primeira separação realmente produtiva é descobrir de quem é o código ou recurso que falhou.

Uma falha em /_next/static/chunks/app/... não é equivalente a um SDK de publicidade em outra origem. Uma URL quebrada pelo meu próprio código não é igual a uma requisição de analytics bloqueada. Código de extensão do navegador é uma categoria diferente.

  • própria aplicação: meu JavaScript, CSS, APIs, mídia e URLs geradas;
  • framework e tempo de execução: Next.js ou React quando fazem parte do caminho de execução;
  • integrações externas: analytics, publicidade, widgets e SDKs;
  • ambiente: extensões, rastreadores, estado de rede, ferramentas de privacidade e particularidades do browser.

Isso não significa ignorar terceiros. Um provedor de pagamento ou autenticação pode ser crítico. Uma falha de publicidade pode ter impacto financeiro. Mas a saúde da integração não equivale automaticamente a uma falha da aplicação.

Se tudo vai para o mesmo canal urgente, o canal perde o significado.

Duas falhas da própria aplicação me ensinaram o que realmente exige ação

A URL errada era o caso simples:

https://example.comhttps://example.com/resource

Não precisava teorizar sobre AdBlock, VPN ou comportamento do navegador. A URL era inválida. Em algum ponto meu código estava concatenando a origem com um valor que já era absoluto.

Esse evento era acionável porque a evidência era específica, o recurso era meu e o problema apontava para código sob meu controle.

A falha do chunk Next.js era diferente:

Failed to load script:
/_next/static/chunks/9253.647385b4be0958e4.js

Também era um recurso da própria aplicação e podia quebrar a página, mas o evento não provava a causa. Um cliente antigo podia pedir um recurso de uma implantação anterior. A requisição podia exceder o tempo limite. Um proxy reverso ou CDN podia falhar. A conexão podia cair. O arquivo podia realmente não existir.

A reação correta não era “eu já sei a causa”, e sim “essa classe merece prioridade alta e mais contexto”.

Uma falha pode ser grave mesmo quando a certeza sobre sua causa é baixa.

Script error. é uma pista, não um rastreamento de pilha

Error: Script error.
filename: unknown
line: 0
column: 0

Parece dramático e traz quase nenhuma informação.

Navegadores limitam intencionalmente detalhes de erros de scripts cross-origem. A MDN explica que, sem a configuração CORS adequada, window.onerror recebe informações limitadas; o comportamento de crossorigin em <script> influencia diretamente os detalhes disponíveis.

Por isso eu não interpreto automaticamente um Script error. opaco como “minha aplicação caiu”. Pode ser meu código, código de terceiros, código injetado ou simplesmente um erro cujos detalhes o navegador não pode expor.

Eu retenho o evento, correlaciono com página, build, navegador e eventos próximos, mas um único 0:0 não dispara alerta urgente. Um cluster em torno de uma versão ou rota muda a prioridade.

Desconhecido não significa inofensivo. Também não significa crítico.

Um AbortError pode ser real e ainda assim esperado

Os prévias de vídeo mostraram isso melhor que qualquer definição:

AbortError:
The play() request was interrupted by a call to pause()

HTMLMediaElement.play() retorna uma Promise, e essa Promise pode ser rejeitada. Operações do ciclo de vida da mídia também podem cancelar uma reprodução pendente; a MDN documenta que load() aborta Promises pendentes de play() com AbortError.

Em uma grade de prévias isso pode acontecer sem qualquer bug visível: o elemento entra no viewport, o código chama play(), o usuário rola a página, o elemento sai e é pausado ou substituído antes de o início terminar.

A rejeição da Promise é real. O incidente para o usuário pode não existir.

A correção deve ficar perto da chamada: tratar a Promise e separar cancelamento esperado de falha real de reprodução. unhandledrejection é uma rede de segurança, não o primeiro lugar onde o ciclo de vida normal do componente deve ser entendido.

Falhas de terceiros precisam de um modelo de saúde separado

Meus primeiros logs continham muitos erros de analytics e publicidade. Alguns vinham de navegadores focados em privacidade, outros de rastreadores. Um caso especialmente inútil como alerta urgente era um rastreador incapaz de carregar Google Analytics.

Isso prova que uma requisição falhou. Quase não diz se um usuário humano conseguia usar o produto.

O erro não era armazenar o evento. Era colocá-lo no mesmo fluxo de um chunk JavaScript da própria aplicação que não carregou.

  • A aplicação está quebrada para o usuário?
  • A integração externa está saudável?

Um script de anúncios bloqueado pode alimentar métricas de entrega de anúncios. Uma falha de analytics pode medir cobertura de analytics. Nenhum dos dois deveria acordar alguém como “falha do frontend” se a função principal não depende dessa integração.

Separados, problemas de terceiros ficam até mais fáceis de analisar por provedor, navegador e região.

navigator.onLine é contexto, não prova de conectividade

Também passei a registrar se o navegador se considerava online. Isso ajuda, desde que seja apenas uma dica.

Eu tive eventos classificados como falha de rede com:

Online: true

Não há contradição. A MDN alerta que navigator.onLine depende de heurísticas do navegador e do sistema operacional. Uma máquina pode enxergar uma LAN e ainda não alcançar minha origem. VPN, firewall, DNS e falhas parciais complicam ainda mais o quadro.

online === false
→ forte indício de problema ambiental

online === true
→ NÃO prova que origin ou recurso estavam acessíveis

Essa distinção impede que uma dica vire diagnóstico.

Uma única falha raiz pode produzir vários eventos

Com uma coleta melhor surgiu outro problema: um incidente pode gerar várias mensagens.

Um chunk pode primeiro emitir resource.error, depois o loader lança ChunkLoadError, React ou Next.js chega a um error boundary e a lógica de recuperação agenda um reload. Se cada camada alerta separadamente, uma única ação do usuário parece várias panes.

Cinco mensagens parecem cinco usuários afetados, mesmo quando vêm de uma sessão e um arquivo.

Deduplicação por texto não basta. Preciso de correlação do incidente:

session
+ janela curta de tempo
+ classe de erro normalizada
+ recurso first-party
+ client build
+ route

Guardo os eventos brutos, mas mostro à pessoa a representação mais informativa. Se um limite de erro do framework já traz uma pilha da própria aplicação e a URL exata do chunk, o erro genérico de recurso anterior não precisa gerar outro alerta urgente.

Alertar incidentes. Armazenar eventos.

O contexto ao redor do erro se tornou mais valioso que a mensagem

A telemetria posterior ficou muito mais estruturada:

clientBuildId
resource URL
resourceResponseStatus
resourceTransferSize
resourceDurationMs
serviceWorkerVersion
serviceWorkerController
serviceWorkerState
chunkRecoveryScheduled
online
stack / component stack

Para mídia eu também coletava código de erro, status HTTP quando era possível verificá-lo de forma independente, o Content-Type recebido e se a falha parecia HTTP ou network.

Isso permitiu perguntas que uma simples mensagem de exceção não responde: começou em um build específico? Houve resposta HTTP? Um Service Worker controlava a página? A recuperação já foi iniciada? Vários eventos apontam para o mesmo recurso? A rota ativa realmente parou de funcionar?

A Resource Timing API pode fornecer duração, informações de transferência e, onde suportado e permitido, response status. Há limitações: dados cross-origem são restritos, recursos em cache podem ter transferSize: 0 e responseStatus não existe em todos os browsers. Por isso null e 0 devem continuar sendo estados honestos, não desculpas para inventar certeza.

ChunkLoadError é um sintoma, não um detector de 404

Um evento posterior mudou bastante minha interpretação.

O navegador reportou um ChunkLoadError do Next.js para um layout chunk, enquanto a telemetria também continha:

resourceResponseStatus: 200
resourceDurationMs: 170523
serviceWorkerState: activated
chunkRecoveryScheduled: true

A medição registrada ficou em aproximadamente 170 segundos. Qualquer que tenha sido a causa raiz exata, isso já era suficiente para rejeitar:

ChunkLoadError === servidor respondeu 404

Outras falhas não tinham status observável. Algumas eram tempo limites; outras tinham dados de transferência. Mesma classe de erro, evidências diferentes.

Isso importa especialmente no Next.js porque arquivos em /_next/static/ normalmente têm hash de conteúdo e são tratados como immutable. A documentação atual de self-hosting do Next.js descreve cache headers longos para esses recursos. Portanto, um ChunkLoadError pode envolver implantação skew, clientes antigos, rede, proxy reverso, CDN, cache, Service Worker ou artefatos realmente ausentes.

A camada de alertas não deveria inventar qual hipótese é verdadeira. Ela deve preservar evidências para a investigação.

Erros de mídia ensinaram a mesma lição por outra camada

Em alguns casos o media element reportava:

MEDIA_ELEMENT_ERROR: Format error

Isso parece um problema de codec.

Mas uma verificação adicional da entrega mostrava, para alguns eventos:

HTTP status: 410
Content-Type: text/html; charset=utf-8
failure kind: http

O navegador pediu vídeo e recebeu uma resposta HTTP de erro contendo HTML. O media element não conseguia decodificar HTML como vídeo e, por isso, o sintoma externo era “Format error”. O diagnóstico útil estava na entrega.

A camada que detecta o sintoma não precisa ser a camada que causou o problema.

“Erro de codec”, “queda de rede”, “bug de cache” e “chunk ausente” são conclusões. A telemetria deveria começar por observações.

Eu avalio falhas do navegador em cinco dimensões

1. Propriedade

É própria aplicação, framework/runtime, integração externa ou ambiente?

2. Impacto no usuário

Quebrou a rota ativa, rendering, autenticação, chat, checkout ou outro fluxo essencial? Ou falhou apenas anúncio, analytics, preload ou prévia opcional?

3. Qualidade da evidência

Tenho stack da própria aplicação, URL do recurso, status HTTP, build ID e pilha de componentes? Ou somente Script error. em 0:0?

4. Recorrência e alcance

É uma sessão ou a mesma assinatura aparece em usuários, rotas e navegadores diferentes após a mesma versão?

5. Recuperação

A app se recuperou? Reload de chunk foi agendado? Fallback funcionou? O usuário continua bloqueado?

claramente first-party
+ alto impacto
+ forte evidência
+ várias sessões
+ sem recovery
= incidente urgente

third-party
+ função opcional
+ evidência fraca
+ isolado
+ usuário não afetado
= métrica ou baixa prioridade

A supressão deve ser conservadora

Quando o ruído irrita, uma blacklist enorme parece tentadora. É perigoso.

Suprimir todo AbortError pode esconder requisições reais da aplicação que foram abortadas. Apagar todo Script error. pode esconder um cluster específico de navegador. Ignorar todas as falhas de terceiros pode esconder um provedor de pagamento, autenticação ou consentimento quebrado.

ALERT
→ incidente forte que exige atenção

RETAIN / AGGREGATE
→ guardar e contar; alertar quando formar cluster

METRIC / SAMPLE
→ ruído esperado ou de baixo impacto; preservar tendência e exemplos

Assim o sistema fica mais silencioso sem ficar cego.

Um classificador útil é principalmente política escrita em código

O código abaixo não é copiado do meu projeto em produção. É uma representação compacta da política que eu preferiria ter desde o início:

function classifyClientEvent(event) {
  const owner = classifyOwner(event);

  if (isExpectedMediaCancellation(event)) {
    return { severity: "metric", reason: "expected-cancellation" };
  }

  if (owner === "first-party" && breaksActiveRoute(event)) {
    return { severity: "alert", reason: "first-party-user-impact" };
  }

  if (isActiveFirstPartyChunkFailure(event)) {
    return { severity: "alert", reason: "application-chunk" };
  }

  if (owner === "third-party") {
    return { severity: "aggregate", reason: "integration-health" };
  }

  if (isOpaqueScriptError(event)) {
    return { severity: "aggregate", reason: "insufficient-evidence" };
  }

  return { severity: "aggregate", reason: "needs-correlation" };
}

A dificuldade real está dentro de funções como breaksActiveRoute(): elas precisam de contexto da rota, propriedade do recurso, dados do boundary e às vezes conhecimento do produto.

A assinatura deve acompanhar o incidente, não o texto da mensagem

Comparar mensagens literalmente é uma deduplicação fraca. Mudam offsets minificados, hashes de chunks, IDs dinâmicos e redação entre browsers.

{
  errorClass,
  normalizedFirstPartyResource,
  routeFamily,
  clientBuild,
  sessionId,
  shortTimeBucket
}

Para um crash global pode importar o primeiro frame da própria aplicação; para chunk, o recurso normalizado; para mídia, a classe de entrega.

resource.error
→ ChunkLoadError
→ framework boundary
→ recovery scheduled

pode virar um único incidente com quatro observações, não quatro emergências.

O canal urgente precisa de uma função bem mais estreita

Eu reservaria alerta imediato para erros de execução da própria aplicação que quebram a rota, limites de erro React/Next.js com impacto real, JavaScript ou CSS necessários à aplicação, ChunkLoadError repetidos em várias sessões, falhas essenciais de API sem recuperação e violações claras de invariantes como URLs geradas incorretamente.

Eu reteria sem alertar imediatamente um Script error. isolado, falha da própria aplicação recuperada com sucesso, erros de mídia cuja camada raiz ainda é incerta e anomalias específicas de navegador que precisam de agregação.

Falhas conhecidas de publicidade ou analytics, cancelamentos esperados AbortError, ruído de terceiros exclusivo de rastreadores, fortes sinais de falta de conexão e recursos especulativos opcionais normalmente iriam para métricas ou amostragem.

Um alerta urgente deve representar impacto acionável no usuário, não o volume bruto de reclamações do navegador.

Prefiro medir incidentes a uma única “contagem de erros”

  • incidentes da própria aplicação por 1.000 sessões;
  • sessões afetadas por build ID;
  • error boundaries por rota;
  • falhas de chunks por recurso e implantação;
  • taxa de falha de integrações por provedor;
  • volume de cancelamentos esperados, para detectar mudanças bruscas;
  • taxa de recuperação bem-sucedida;
  • incidentes únicos separados do total de eventos brutos.

“Um evento aconteceu” raramente é um bom limiar. “O mesmo incidente da própria aplicação atinge várias sessões independentes no novo build e a recuperação falha” é muito mais útil.

O monitoramento no cliente ainda não prova a causa raiz

Telemetria do browser tem limites duros.

Um status HTTP ausente pode vir de falta de suporte, restrição cross-origin, cancelamento ou outra lacuna de observabilidade. Um Service Worker ativo não prova que ele serviu um recurso obsoleto. ChunkLoadError após uma implantação não prova desalinhamento entre versões. online: true não prova alcançabilidade.

A telemetria no cliente reduz o conjunto de hipóteses. Logs do servidor e do proxy reverso, manifestos de implantação, estado do cache e reprodução ainda podem ser necessários.

Também não quero que observabilidade se transforme em coleta ilimitada de dados de usuários. Cada campo precisa de uma justificativa diagnóstica.

Melhor telemetria não significa necessariamente mais telemetria; significa telemetria que distingue melhor modos de falha.

Minha regra hoje: coletar eventos, investigar incidentes, alertar pelo impacto

No começo eu queria responder apenas: “algo quebrou?”. Em produção essa pergunta é ampla demais. Sempre haverá um rastreador, filtro de privacidade, Promise de mídia, rede ou SDK externo falhando em algum lugar.

É nosso?
O usuário perdeu funcionalidade?
Quão forte é a evidência?
Está se repetindo?
A aplicação se recuperou?
São vários eventos ou um incidente?

Quando organizei o monitoramento em torno dessas perguntas, o fluxo de mensagens vermelhas virou uma ferramenta de engenharia.

Um erro do navegador é uma observação. Um incidente é uma explicação correlacionada do impacto. Um alerta é a decisão de que uma pessoa precisa agir.

Não quero mais tratar essas três coisas como sinônimos.