Nuestra aplicación no es perfecta, y por más que nuestros tests abarquen todas las situaciones posibles, la computadora del cliente es un mundo aparte y para ellos no hay nada más frustrante que utilizar una aplicación/página y que la misma deje de funcionar sin motivo aparente. Por eso nos interesa saber:
Esa es la información que necesitamos para poder reproducir los errores en nuestro ambiente de desarrollo. Dependiendo de cada caso, pueden existir otros datos también de interés, por ejemplo, alguno relativo a nuestra aplicación como la versión/release en que sucede.
A grandes rasgos, la arquitectura estará compuesta por dos partes...
- La parte de front-end se dedicará a capturar información sobre errores que suceden en el cliente e inmediatamente enviarlos al servidor.
- En back-end necesitamos una aplicación que se encargue de recibir los errores y los recopile en algún lugar para posterior análisis. Esta aplicación puede filtrar los errores, agruparlos o guardarlos en un log con algún formato común.
Vamos a hablar un poco sobre la parte de front-end, en nuestro código JavaScript, tenemos dos formas de capturar los errores que sucedan:
Usando try...catch
No tiene mucha ciencia, consiste en utilizar el bloque try...catch en los lugares que nosotros pensamos que puede suceder algún error:
try{
//Función que devuelve error
funcionErronea();
} catch(error){
//Sucedió un error!
}
La información que llega en el catch(error)
es un objeto Error que posee las siguientes propiedades:
message
: Mensaje de error que el intérprete JavaScript del navegador devolvió;
fileName
: Archivo en donde sucedió el error;
lineNumber
: Línea en que sucedió el error, en el archivo indicado.
Hasta acá todo bien, tenemos la información que necesitamos, sin embargo el problema de los try...catch
es que si se utilizan mal o en lugares no indicandos pueden terminar perjudicando la performance de nuestro código.
Usando window.onerror
Por otro lado, window.onerror es un evento que ejecuta automáticamente una función dada cuando sucede un error JavaScript en cualquier parte de nuestro código. La diferencia es justamente esa, es un evento automático, y al no ser intrusivo no perjudica la performance:
window.onerror = function errorHandler(message, fileName, lineNumber) {
//Sucedió un error!
}
En el ejemplo, errorHandler
sobre-escribe la función predeterminada. Dicha función recibe tres parámetros, que son los mismos que las propiedades del objeto Error
que vimos en el try...catch
.
Un ejemplo real con window.error
Veamos un ejemplo real de implementación:
window.onerror = function(message, fileName, lineNumber) {
//Url del servicio en donde se enviará la información del error
var serviceUrl = "/log-service/";
//Se rellena un array con la información
var data = [];
data.push("message=" + encodeURIComponent(message));
data.push("file=" + encodeURIComponent(fileName));
data.push("line=" + encodeURIComponent(lineNumber));
//Se forma un string
data = data.join("&");
data = data.substring(1);
//Se crea una nueva imagen poniendo como src la URL del servicio
//y pasando como parámetros la información del error
( new Image() ).src = serviceUrl + "?" + data;
};
El script no tiene mucha ciencia:
- Se recopila la información, se encodea y luego se crea una nueva imagen poniendo como src todos los datos (URL del servicio y datos).
- Del lado del backend, cada vez que se hace una petición a la URL del servicio y se pasan los datos, los manipula de alguna forma para luego analizarlos (como comenté más arriba). El servicio debe tener algún tipo de validador, además que debe obtener el
HTTP referer
para saber en que página sucedió el error.
Los problemas de window.error
Todo parece sencillo, pero en mi experiencia window.error
posee dos problemas, los cuales dependen de ciertas situaciones:
- si el error sucede en un archivo que no cumple la regla de Same origin policy (mismo dominio, URL, puerto, etc) los parámetros serán nulos (en verdad el mensaje será
"Script Error" on line 0
). Es decir, si nosotros servimos los JS desde, por ejemplo, un CDN, no tendremos la información que necesitamos;
- si nuestros JS están minimizados y concatenados, el único dato interesante será la del mensaje, ya que
fileName
será el archivo minimizado (por ser un paquete de archivos concatenados) y la línea la número 1 (por estar todo minimizado), por lo tanto perderemos información de valor.
En mi caso concreto, la aplicación que necesitaba analizar errores se encontraba en la dos situaciones, servía los scripts minimizados, concatenados y desde un CDN. Para manejar la situación se ideó un esquema:
- Modificamos la aplicación para tener dos configuraciones: una que permita habilitar/deshabilitar cuando los scripts se sirven desde el CDN y otra que inyecte o no el script que envia errores al servicio. Además, en conjunto con los scripts minimizados, se subían una versión sin minimizar.
- Cada vez que se hacía un nuevo release de la aplicación y se subía a producción, durante un lapso de tiempo (digamos, algunas horas) se desactivaba el CDN, la aplicación apuntaba a los scripts sin minimizar y se inyectaba en los páginas el script que envía los errores. Durante ese lapso de tiempo la aplicación de backend logueaba los errores y los guardaba (la aplicación era una versión modificada de ErrorBoard)
El esquema depende de la situación de cada uno, en nuestra experiencia, con algunas horas bastaba para tener un gran número de errores para analizar (la aplicación recibía varios millones de visitas mensuales, por lo cual era suficiente). Claro que durante esas horas la aplicación cargaba un poco más lenta (ya que los JS no eran minimizados y se servían desde servidores internos). Otra manera también podría ser hacer una especie de test A/B y que sólo algunos usuarios tengan habilitada la configuración y otros no.
Algo interesante de ErrorBoard es que al ser open source permite modificarlo a gusto, y posee características interesantes: Agrupación de errores, posibilidad de marcar los que estén solucionados, filtro por navegador, versión y una interfaz sencilla.
También comentar que existe una solución para poder obtener la información de un error si sucede en un script por fuera de nuestro dominio, la misma radica en habilitar CORS en los servidores y en la aplicación, en teoría esta manera permite que Firefox y Chrome comiencen a leer la información del error, sin embargo la última vez que lo probé no tuve suerte y la información seguía sin aparecer (más información: Catching Cross-Domain JS Errors).
En conclusión, por más que nuestra aplicación sea chica, mediana o grande, capturar errores JavaScript es una buena práctica, como decía arriba, para el cliente no hay nada mas frustrante que una página deje de funcionar sin motivo alguno y quien dice que esa persona termine siendo un cliente frecuente.