EN

Mario Rocafull / Albin

Programador freelance en Valencia

9 agosto 2023 · Código

Cómo crear logs desde el javascript del navegador

Explico cómo trazo las acciones y los errores del usuario durante el transcurso de su visita.

Cuando programamos en el servidor tenemos disponibles los propios log del sistema para ver si ha habido algún error de sintaxis, de ejecución, … Sin embargo en el cliente estamos vendidos, nada de lo que se escribe en la consola suele ir hasta el servidor para que podamos revisarlo.

Básicamente nos interesa interceptar tres situaciones:

  • Consola, ya sean todos los mensajes que se escribe o únicamente aquellos que sean warnings, errores, ….
  • Erores, errores lanzados por el lenguaje, no deliberadamente escritos por el código usando
    console
    .
  • Log, registro de mensajes y datos que queramos envia al servidor.

Consola

Aprovechando las virtudes de Javascript podemos cambiar la consola del sistema por la nuestra propia guardándonos una referencia a la original y entonces sobrescribimos los métodos que nos interesen y hacemos un puente a los originales que queramos mantener inalterados.

La razón de crear un error y obtener su pila de llamadas es para tener una mínima trazabilidad de dónde se originó el problema (mirando qué función llamó a la función que crea este falso error).

Luego solo hay que enviar esta información al servidor. Yo la codifico en base64 porque trabajo con WebEmpresa por su firewall pero en ocasiones puede ser un granito en el culo (falsos positivos) y prefiero ocultar un poco el contenido del mensaje y que no parezca XSS (Cross Side Scripting).

var console = (function(exconsole) {
	return {
		log: function() {
			exconsole.log(...arguments);
		},
		info: function () {
			exconsole.info(...arguments);
		},
		warn: function () {
			exconsole.warn(...arguments);
		},
		error: function () {
			exconsole.error(...arguments);
			const error = new Error();
			const stack = JSON.stringify(error.stack);
			const args  = JSON.stringify(arguments);
			const fdata = new FormData();
			fdata.append('stack',   btoa(stack));
			fdata.append('args',    btoa(args));
			fetch(`js-error-console.php`, { 'method': 'post', 'body': fdata });
		},
		// Se debería puentear cualquier otra función como time, timeEnd, …
		assert:    exconsole.assert;
		time:      exconsole.time;
		timeEnd:   exconsole.timeEnd;
		timeLog:   exconsole.timeLog;
		timeStamp: exconsole.timeStamp;
	};
}(window.console));

window.console = console;

En el servidor recopilaríamos la información así (EngineFwk son mis librerías cuando hago programación a medida):

header('Content-Type: text/plain');
include('ssi/common.php');

$logger = \EngineFwk\Logger::get('javascript');
$logger->line('CONSOLE');
$logger->line('ip',      USER_IP);
$logger->line('stack',   base64_decode($_POST['stack'] ?? null));
$logger->line('args',    base64_decode($_POST['args']  ?? null));

Errores

El lenguaje te permite suscribirte al evento error para ser notificado de cada vez que algo va mal y te proporciona información útil como el fichero de origen, la línea y la columna (caracter) a parte del mensaje descriptivo. Entonces solo hay que enviarlo esta información servidor.

window.onerror = function(event, source, lineno, colno, error) {
	const stack = JSON.stringify(error.stack);
	const fdata = new FormData();
	fdata.append('event',  btoa(JSON.stringify(event)));
	fdata.append('error',  btoa(JSON.stringify(error)));
	fdata.append('source', btoa(JSON.stringify(source)));
	fdata.append('lineno', btoa(JSON.stringify(lineno)));
	fdata.append('colno',  btoa(JSON.stringify(colno)));
	fdata.append('stack',   stack);
	fetch(`js-error-window.php`, { 'method': 'post', 'body': fdata });
};

En el servidor recopilaríamos la información así:

header('Content-Type: text/plain');
include('ssi/common.php');

$logger = \EngineFwk\Logger::get('javascript');
$logger->line('WINDOW');
$logger->line('ip',     USER_IP);
$logger->line('event',  json_decode(base64_decode($_POST['event']  ?? null)));
$logger->line('error',  json_decode(base64_decode($_POST['error']  ?? null)));
$logger->line('source', json_decode(base64_decode($_POST['source'] ?? null)));
$logger->line('lineno', json_decode(base64_decode($_POST['lineno'] ?? null)));
$logger->line('colno',  json_decode(base64_decode($_POST['colno']  ?? null)));

Logger

Ya puestos a poder escribir logs en el servidor desde el javascript del cliente, por qué no permitirnos también escribir lo que consideremos interesante, y no únicamente los errores y mensajes ajenos.

El log se va acumulando y se envía al servidor cada cierto tiempo, cuando supera cierta cantidad de líneas o cuando el usuario parece abandonar la página. Los dos primeros pueden ir al gusto e incluso ser excluyentes.

const oLogger = {
	hto: null,
	lines: [],
	init: function () {
		this.hto = setInterval(this.send.bind(this), 30000);
		window.addEventListener('visibilitychange', this.beacon.bind(this));
	},
	log: function () {
		const line = [];
		for(argument of arguments) {
			line.push( JSON.stringify(argument) );
		}
		this.lines.push(line.join(' | '));
		if(this.lines.length>50) this.send();
	},
	send: function () {
		if(this.lines.length) {
			const lines = [...this.lines]; this.lines = [];
			const fdata = new FormData();
			fdata.append('lines', Base64.encode(lines.join('
')));
			fetch(`js-logger.php?beacon=false`, { 'method': 'post', 'body': fdata }).then(resp => resp.text()).then(plain => {
				if(plain!='Ok') this.lines = [...lines, ...this.lines];
			}).catch(error => {
				console.log(error);
			});
		}
	},
	beacon: function () {
		if(this.lines.length) {
			const lines = [...this.lines]; this.lines = [];
			const fdata = new FormData();
			fdata.append('lines', Base64.encode(lines.join('
')));
			navigator.sendBeacon(`js-logger.php?beacon=true`,  fdata);
		}
	},
};
oLogger.init();

En el servidor recopilaríamos la información así:

header('Content-Type: text/plain');
include('ssi/common.php');

$logger = \EngineFwk\Logger::get('javascript');
$logger->line('FETCH');
$logger->line('ip',    USER_IP);
$logger->line('url',   base64_decode($_POST['url']   ?? null));
$logger->line('url',   base64_decode($_POST['error'] ?? null));
$logger->line('stack', json_decode(base64_decode($_POST['stack'] ?? null)));
$logger->line('args',  json_decode(base64_decode($_POST['args']  ?? null)));
$logger->line('body',  (base64_decode($_POST['body'] ?? null));
$logger->line('plain', base64_decode($_POST['plain'] ?? null));

Resumen

No tenemos porqué seguir estando ciegos, sobre todo cuando queremos depurar desde un móvil.

Lógicamente, esto solo es válido para los programadores que hacemos código limpio y sería una locura en proyectos donde se integran librerías muy verbose.