Como coletar erros que acontecem no navegador com Javascript
28
0

Como coletar erros que acontecem no navegador com Javascript

Esse artigo te ensinará de forma fácil como criar um módulo para coletar logs de erros no navegador e explicar por que isso pode ser útil.

Felipe Schieber
6 min
28
0
Email image

Esse artigo te ensinará de forma fácil como criar um módulo para coletar logs de erros no navegador e explicar por que isso pode ser útil.

Assume-se que a sua aplicação tenha um servidor próprio para receber os logs que forem coletados no navegador e que você tenha conhecimento básico de Javascript e do ECMAScript 6.

Parte 1 - Por quê coletar erros do navegador

Caso você já tenha motivos o suficiente e apenas esteja interessado no código ou em como fazer, pule para a próxima parte.

Coletar erros que acontecem no navegador não é uma prática tão comum no desenvolvimento de sites/aplicações. Porém, conforme projetos crescem utilizando das mais diversas ferramentas - e dependências - , alguns problemas podem passar despercebidos mesmo que você desenvolva bem os seus testes.

Imagine essa situação: o seu site possui um botão que, ao ser clicado, faz uma consulta numa API pública mantida por terceiros. Por algum motivo fora de seu controle, essa API decide apresentar um erro em determinado momento. O usuário do seu site ao clicar no botão, verá uma mensagem de erro ou talvez nenhuma informação e provavelmente tomará uma ação das duas mais comuns: enviará um email/chat para o suporte ou desistirá de usar o seu site que não está funcionando como deveria.

Caso o seu usuário ainda envie uma mensagem de suporte, você olhará o log do servidor e não encontrará nenhum problema. Pedir para que o usuário envie logs do navegador geralmente não é fácil e nada sofisticado. Você não sabe o motivo do problema. O que você sabe é que precisa dar uma resposta para o usuário e precisa resolver esse problema rapidamente para que não aconteça novamente. Essa situação é pior ainda quando o usuário nem reporta o problema e simplesmente desiste de usar o seu produto. Você perde um usuário e nem sabe o porquê.

Vamos resolver isso e criar uma forma de saber exatamente o que aconteceu no navegador quando aquele botão não funcionou naquele momento.

Parte 2 - Como coletar erros do navegador

Agora iremos adentrar no nosso pequeno projeto e partir para o código. A ideia é que esse módulo utilize apenas Javascript e nenhuma dependência, tornando-o fácil de ser usado em diversos casos.

O projeto

O nosso objetivo é ser capaz de analisar todos os erros que aconteçam no navegador, enviá-los para o servidor e armazená-los em um local de fácil acesso. Algumas considerações importantes antes de começar.

O módulo precisa ser capaz de:

  1. Coletar erros tratados e não tratados.
  2. Indicar o arquivo de origem do erro, a linha em que aconteceu e a mensagem de erro.
  3. Indicar qual o navegador está sendo utilizado no momento do erro.
  4. Ser fácil de implementar em novas páginas depois de pronto.

Requisitos da aplicação que irá receber esse módulo:

  1. Ter um servidor próprio para receber os logs e algum meio de armazená-los, como um banco de dados.

Diagrama do projeto

Email image

Parte 3 - Escrevendo o código

Para começar, vamos configurar um servidor básico no Node.js para receber as requisições do navegador.

const express = require('express')
const app = express()
const port = 8080
app.use(express.static("public"));
app.use(express.json());
app.get('/', (req, res) => {
res.sendFile(__dirname + "/public/index.html")
})
app.post('/logs', (req, res) => {
console.log(req.body);
});
app.listen(port, () => {
console.log(`Example app listening at http://localhost:${port}`)
})

Agora vamos criar o arquivo index.html com nada mais do que uma simples página HTML com um formulário que recebe o nome e sobrenome do usuário e carrega o nosso script.

<!DOCTYPE html>
<head>
<title>Error Logger Sample</title>
</head>
<body>
<form id="formulario">
<label for="nome">Seu nome:</label><br>
<input type="text" id="nome" name="nome"><br>
<label for="sobrenome">Seu sobrenome:</label><br>
<input type="text" id="sobrenome" name="sobrenome">
<input type="submit" value="Enviar">
</form>
<script type="module" src="./app.js" async></script>
</body>
</html>
<br>

Escrevemos o script que usaremos para manipular o formulário e gerar erros intencionais.

const form = document.getElementById('formulario');
form.addEventListener("submit", (e) => {
e.preventDefault();
let nome = document.getElementById('nome').value;
let sobrenome = document.getElementById('sobrenome').value;
let mensagem = "O seu nome é " + primeiro_nome + " " + sobrenome;
alert(mensagem);
}, false);

Ao abrir a página no navegador, digitar o seu nome  e clicar em "Enviar", você perceberá que o navegador jogará um erro.

<br>

Agora que temos um site bugado para trabalhar, vamos escrever o código do módulo que irá coletar esse e outros erros.

Começamos declarando a classe e o construtor para o nosso módulo. O código deve ser autoexplicativo e possui comentários para facilitar a compreensão.

class ErrorLogger {
constructor(params = {
source: null,
interval: 10000,
log_uncaught: true,
app_version: null,
api_url: null,
}) {
this.source = params.source;
this.interval = params.interval * 1000 || 10000; // recebe os intervalos em segundos e converte para millisegundos ou seta 10000ms como padrão
this.app_version = params.app_version;
this.log_uncaught = params.log_uncaught === false ? false : true; // caso o parâmetro log_uncaught não seja configurado, seta como true por padrão
this.api_url = params.api_url;
this.error_buffer = []; // array para armazenar os logs coletados antes de enviar para o servidor
this.init();
if (!this.api_url) {
console.error("Nenhuma API fornecida. Os logs não serão enviados para o servidor.")
}
}
. . .

Em seguida definimos o nosso método init que irá iniciar o módulo.

init() {
this.setBrowser();
if (this.log_uncaught) {
this.uncaughtLogger();
}
this.startBuffering();
}
. . .

O método startBuffering inicia o nosso buffer que irá enviar os logs coletados no intervalo especificado, ao invés de criar uma requisição para cada erro que acontecer.

startBuffering() {
setInterval(async () => {
if (this.error_buffer.length) {
try {
let xhr = new XMLHttpRequest();
xhr.open("POST", this.api_url);
xhr.setRequestHeader("Accept", "application/json");
xhr.setRequestHeader("Content-Type", "application/json");
xhr.send(JSON.stringify(this.error_buffer));
while (this.error_buffer.pop()) { }
console.log("buffer flushed");
} catch (error) {
console.log('error in buffering', error)
}
}
}, this.interval);
}
. . .

Criamos um método que coleta qual navegador está sendo utilizado pelo usuário. Fazemos uma comparação de recursos com um backup para o UserAgent. Esse método não é 100% eficaz para todos os navegadores, mas é uma boa referência.

setBrowser() {
if ((!!window.opr && !!opr.addons) || !!window.opera || navigator.userAgent.indexOf(' OPR/') >= 0) {
this.browser = "opera";
} else if (typeof InstallTrigger !== 'undefined') {
this.browser = "firefox"
} else if (/constructor/i.test(window.HTMLElement) || (function (p) { return p.toString() === "[object SafariRemoteNotification]"; })(!window['safari'] || (typeof safari !== 'undefined' && window['safari'].pushNotification))) {
this.browser = "safari"
} else if (false || !!document.documentMode) {
this.browser = "IE"
} else if (!!window.chrome && (!!window.chrome.webstore || !!window.chrome.runtime)) {
this.browser = "chrome"
}
if (!this.browser) {
let browser_name = undefined;
let isIE = false || !!document.documentMode;
let isEdge = !isIE && !!window.StyleMedia;
if (navigator.userAgent.indexOf("Chrome") != -1 && !isEdge) {
browser_name = 'chrome';
}
else if (navigator.userAgent.indexOf("Safari") != -1 && !isEdge) {
browser_name = 'safari';
}
else if (navigator.userAgent.indexOf("Firefox") != -1) {
browser_name = 'firefox';
}
else if ((navigator.userAgent.indexOf("MSIE") != -1) || (!!document.documentMode == true))
{
browser_name = 'ie';
}
else if (isEdge) {
browser_name = 'edge';
}
this.browser = browser_name;
}
}
. . .

Os próximos métodos fazem o log dos erros e os insere no nosso buffer.

O método uncaughtLogger cria o evento que vai monitorar erros não tratados.

uncaughtLogger() {
window.onerror = (message, source, lineno, colno, error) => {
if (this.isWhitelisted(message)) return;
let newError = {
message: message,
source: this.source,
line: lineno,
column: colno,
app_version: this.app_version,
browser: this.browser,
}
this.error_buffer.push(newError);
console.log("Uncaught exception on", newError);
}
}
. . .

Agora é o método que utilizaremos para logar os erros tratados. O desafio aqui é identificar a linha em que o erro ocorreu. Diferentes navegadores e diferentes frameworks vão apresentar essa informação de forma distinta entre si. O código que desenvolvi abaixo acessa a propriedade stack do objeto error e tenta identificar a linha que gerou o erro.

logError(error, objects = null) {
if (this.isWhitelisted(error.message)) return;
for (let i in objects) {
if (typeof (objects[i]) === "object") {
objects[i] = JSON.stringify(objects[i]);
}
}
if (objects && objects.length && typeof (objects) === "object") {
objects = JSON.stringify(objects);
}
let lineno, colno;
let stack = error.stack.split("\n");
let error_stack;
for (let i in stack) {
if (stack[i].includes(".js:")) {
error_stack = stack[i];
break;
}
}
error_stack = error_stack.split(":");
lineno = Array.from(error_stack[error_stack.length - 2]);
colno = Array.from(error_stack[error_stack.length - 1]);
lineno = lineno.filter(x => !isNaN(x));
colno = colno.filter(x => !isNaN(x));
lineno = lineno.join('');
colno = colno.join('');
let newError = {
message: error.message,
source: this.source,
line: lineno,
column: colno,
app_version: this.app_version,
browser: this.browser,
objects: objects
}
this.error_buffer.push(newError);
console.log("Caught error on ", newError);
}
. . .

Você provavelmente reparou que os nossos dois métodos de log possuem uma checagem de whitelist. Adicionei essa opção depois de perceber que algumas dependências podem jogar alguns erros que não são importantes. A whitelist serve para evitar que o nosso servidor fique armazenando essas mensagens de erro.

isWhitelisted(message) {
let allowed = ["FocusTrap: Element must have at least one focusable child"];
for (let i in allowed) {
if (message.includes(allowed[i])) {
return true;
}
}
return false;
}
. . .

Aqui na whitelist simplesmente criamos uma Array para armazenar uma lista de mensagens/strings que queremos ignorar. Caso o nosso erro contenha essa exata string em algum ponto, ele será ignorado. Caso tenha uma whitelist grande, é interessante que a coloque em um arquivo separado, claro.

Voltando ao método logError, você provavelmente notou que ele possui um parâmetro objects. Esse parâmetro pode ser usado para que você passe uma array de objetos que sejam úteis no seu código. Por exemplo, caso esteja acessando uma API na hora do erro, você pode passar uma array com o objeto da resposta da API para analisar o seu conteúdo.

E o nosso módulo para coleta de erros está pronto :)

Para utilizar o nosso módulo é bem simples, basta importarmos o módulo no nosso script e inicializá-los com as configurações desejadas.

import { ErrorLogger } from "./lib/errorlogger.js";
const logger = new ErrorLogger({
source: "app.js",
interval: 30,
log_uncaught: true,
app_version: "1.0.0",
api_url: "http://localhost:8080/logs"
});
const form = document.getElementById('formulario');
form.addEventListener("submit", (e) => {
e.preventDefault();
let nome = document.getElementById('nome').value;
let sobrenome = document.getElementById('sobrenome').value;
let mensagem = "O seu nome é " + primeiro_nome + " " + sobrenome;
alert(mensagem);
}, false);
. . .

Agora, ao recarregar a página e tentar enviar o formulário novamente, o módulo coletará o erro gerado pelo navegador e enviará para o servidor.

Console do navegador<br>
Console do navegador
Console do servidor<br>
Console do servidor

Para utilizar o módulo em erros tratados, podemos simplesmente chamar o método logError do módulo e passar o objeto do erro, como no exemplo abaixo.

. . .
form.addEventListener("submit", (e) => {
try {
e.preventDefault();
let nome = document.getElementById('nome').value;
let sobrenome = document.getElementById('sobrenome').value;
let mensagem = "O seu nome é " + primeiro_nome + " " + sobrenome;
alert(mensagem);
} catch (err) {
logger.logError(err);
}
}, false);
. . .

Agora basta configurar o seu servidor para tratar as requisições como desejar e terá todos os erros do navegador concentrados em um lugar com os principais detalhes sobre eles!

Conclusão

Nesse artigo criamos um módulo para coletar erros do navegador e enviá-los para uma API. Você pode configurar o seu servidor como desejar para que lide com os logs da melhor forma para o seu caso. No meu caso, armazeno-os em uma tabela do banco de dados para consultar depois. Usar esses dados para construir um dashboard legal é uma boa ideia.

Espero que tenha gostado e caso tenha alguma dúvida ou sugestão de melhoria, basta deixar um comentário!

Estrutura dos nossos arquivos:

├── app.js
├── public/
│ ├── app.js
│ ├── index.html
│ ├── lib/
│ │ │── errorlogger.js

Link do Github com o código completo desse projeto: https://github.com/FSchieber/ELM-Error-Logger-Module