Javascript.RU

Серверное логирование клиентских ошибок в JavaScript

- Ты суслика видишь?
- Нет.
- И я нет. А он есть.
(с) ДМБ

Аксиома. В любой программе есть ошибки.
(с) закон Мерфи

Чуть переформулировав: если программа выполняется без ошибок, это еще не означает, что их нет.

К чему я это. Не всегда любое ПО можно оттестировать идеально, что бы в процессе эксплуатации не возникало ошибок. Но если они возникают - их нужно как-то отлавливать. Тем более, если они возникают _уже_ в процессе эксплуатации, а не разработки/отладки.

В PHP скриптах это достигается анализом лог файлов веб сервера. Но как быть с ошибками в JavaScript, который выполняется на стороне пользователя? В данном случае нет никаких лог файлов. Кажется, всё потеряно? Нет. Есть два метода для достижения этих целей.

Сразу сделаю оговорку. Кто-то может не найти в этой статье ничего нового. А для кого-то она может оказаться почти панацеей.

Алгоритм

1. Отловить ошибку браузером клиента и не дать ему возможность ее увидеть. Любым приемлимым способом;
2. Отловленную ошибку необходимо передать на серверную сторону;
3. На стороне сервера с ошибкой можно выполнять любые операции. Нас будет интересовать логгинг в БД и отсылка на почту.

Отлов ошибок

Существует два способа (сделаю поправку: два известных мне способа), как отловить ошибку в JavaScript. Первый - с помощью связки try-catch, второй с помощью переопределения обработчика события onerror. Рассмотрим ниже оба способа чуть подробнее

Нужный участок кода оборачивается этой связкой, в нем происходит отлов выбрасываемых ошибок. Можно спокойно применять для каких-нибудь отладочных целей. Но используется, в основном, для отлова ошибок, типа, деление на ноль и прочих, т.к. этой связкой оборачивается конкретный кусок кода. Если кода написано много - это будет долгим и не эффективным занятием.

Плюсы данного метода:
* Не нужно переопределять стандартный обработчик ошибок window.onerror

Минусы данного метода:
* Используется, в основном, для отлова "локальных" ошибок. Необходимо оборачивать этим блоком каждый код, в котором мы хотим отловить ошибку.
* Немного не то, что нужно изначально.

try {
} catch (e) {
 // логгинг
}

Позволяет назначить свой обработчик на событие onerror.

Плюсы этого метода:
* Обработчик назначается один раз.
* Если кода написано много, и ошибка может быть где угодно - достаточно одного переопределения в начале скрипта.

Минусов, кажется, нет.

// функция, которая будет выполняться при событии window.onerror. 
// на входе она имеет три параметра:
// - сообщение об ошибке;
// - файл, в котором произошла ошибка;
// - номер строки с ошибкой.
function myErrHandler(message, url, line)
{
   alert ('Error: '+message+' at '+url+' on line '+line);

// что бы не выскочило окошко с сообщение об ошибке - 
// функция должна возвратить true
   return true;
}

// назначаем обработчик для события onerror
window.onerror = myErrHandler;

Итак, ошибка поймана на клиентской стороне. Можно переходить ко второму пункту.

Передача ошибки на серверную сторону

Ошибку нужно передавать на сервер без перезагрузки страницы, без всплывающих и закрывающихся попапов, скрытых фреймов и прочего. Для этого воспользуемся технологией AJAX (Асинхронный JavaScript и XML).

Код ниже желательно вставить первой строчкой в тег body, что бы обработчик был создан до того, как будут выполняться последующие скрипты.

// Обеспечиваем поддержу XMLHttpRequest`а в IE
var xmlVersions = new Array(
    "Msxml2.XMLHTTP.6.0",
    "MSXML2.XMLHTTP.3.0",
    "MSXML2.XMLHTTP",
    "Microsoft.XMLHTTP"
    );
if( typeof XMLHttpRequest == "undefined" ) XMLHttpRequest = function() {
	for(var i in xmlVersions)
	{
		try { return new ActiveXObject(xmlVersions[i]); }
		catch(e) {}
	}
	throw new Error( "This browser does not support XMLHttpRequest." );
};


// Собственно, сам наш обработчик. 
function myErrHandler(message, url, line)
{
	var server_url = window.location.toString().split("/")[2];
	var params = "logJSErr=logJSErr&message="+message+'&url='+url+'&line='+line;
	var req =  new XMLHttpRequest();
	req.open('POST', 'http://'+server_url+'/ajax_chain.php', true)
	req.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
	req.setRequestHeader("Content-length", params.length);
	req.setRequestHeader("Connection", "close");
	req.send(params); 
	// Чтобы подавить стандартный диалог ошибки JavaScript, 
	// функция должна возвратить true
	return true;
}

//назначаем обработчик для события onerror
window.onerror = myErrHandler;

Ошибку на сервер передали, пользователь ни о чем не догадался. Переходим к плану "b" следующему пункту. (тег STRIKE не работает?)

Логгинг ошибки

Что-то расписывать подробно нет смысла. Ошибка приходит из JS-скрипта в $_POST массиве. Нам необходимо ее забрать, записать в базу и отослать на почту. См реализацию.

p.s. запись в файл была добавлена после того, как в базе стали появляться записи с message=="". немного поразбиравшись нашел в лог файлах апача такую запись: "PHP Notice: iconv(): Detected illegal character in input string in ..." и я предположил, что на вход iconv приходила строка в не UTF кодировке. а просматривая содержимое файла можно было бы явно определить, что было не так.

файл ajax_chain.php, находится в корне.

<?php
// Инициируем соединение с базой. Используется библиотека http://dklab.ru/lib/DbSimple/ 
// Можно было бы, конечно, переписать с использованием нативных функций PHP, 
// но уклон, как-бы, идет не на процесс взаимодействия с БД.
require_once($_SERVER['DOCUMENT_ROOT']."/conf.php");
require_once($_SERVER['DOCUMENT_ROOT']."/dbsimple.php");

$db = DbSimple_Generic::connect($cfg['db_dsn']);
if (!empty($cfg['db_prefix'])) {
	define("TABLE_PREFIX", $cfg['db_prefix']);
	$db->setIdentPrefix(TABLE_PREFIX);
}
$db->setErrorHandler('dbErrorHandler');
set_sql_codepage($db);


// собственно, основная часть. проверяем, пришла ли POST-запросом строка logJSErr, 
// если да - то это то, что мы послали из JS скрипта с помощью AJAX`а
if (isset($_POST['logJSErr']))  {
	$notSet = "notSet";
	// Необходимо знать, в каком браузере произошла ошибка и на какой странице
	$httpUserAgent  = isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : $notSet;
	$httpReferer    = isset($_SERVER['HTTP_REFERER'])    ? $_SERVER['HTTP_REFERER']    : $notSet;

	// Так же может быть интересно, с какого IP адреса зашел пользователь
	$servRemoteAddr = isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : $notSet;

	// Прочие переменные, которые могут пригодиться. А могут и нет.
	$httpXForwarded = isset($_SERVER['HTTP_X_FORWARDED_FOR']) ? $_SERVER['HTTP_X_FORWARDED_FOR'] : $notSet;
	$httpXRealIP    = isset($_SERVER['HTTP_X_REAL_IP'])  ? $_SERVER['HTTP_X_REAL_IP']  : $notSet;
	$servHttpVia    = isset($_SERVER['HTTP_VIA']) ? $_SERVER['HTTP_VIA'] : $notSet;

	// Экранируем пришедший контент - защита "от дурака". 
	// Ибо переменные отсылаются из JS`а в явном виде
	$message = $_POST['message'];
	$url	 = $_POST['url'];
	$line	 = $_POST['line'];

	$message	 = iconv('UTF-8','Windows-1251',$message);
	$message_e   = mysql_escape_string ($message); 
	$url_e  	 = mysql_escape_string ($url);
	$line_e  	 = mysql_escape_string ($line);



// защита от DOS-атак.
// алгоритм работы: 
// имеется массив $limits, определяющий количество разрешенных "сообщений" в единицу 
// времени для конкретного IP адреса. если это корпоративный сайт, и сотрудники выходят
// с NATовского ip-адреса - можно внести его в этот массив, убрав ограничение.
// для всех остальных существует "IP"=='default' - параметры по умолчанию для
// "осталных" пользователей. 
// для каждого IP адреса, с которого приходит "ошибка" - производится запись в сессию
// с временем появления ошибки. при последующем появлении сообщения - сравнивается 
// разница между текущим временем и временем появления прошлой ошибки. если эта разница 
// меньше чем secs/msgs - то не даем "ошибке" пробиться дальше.

	// запускаем сессию
	session_start();
	// при желании можно дописать корректную работу с подсетями. но это не основная задача.
	// и времени на реализацию нет. 
	$limits = array(
				'1.1.1.1' 	 => array('msgs'=>0, 'secs'=>1), // unlimited for 1.1.1.1 ip
				'default'	 => array('msgs'=>2, 'secs'=>1)  // 2 message per second (default)
				);
	// $limits[$ip]['msgs'] - number of messages (0==unlimited)
	// $limits[$ip]['secs'] - per seconds

	// приходит POST. смотрим в сессию. встречался ли данный IP ранее.
	if (isset($_SESSION[$servRemoteAddr]))
	{
		// если он встречался - берем дефолтные или нужные значения msgs & secs
		$msgs = $limits['default']['msgs'];
		$secs = $limits['default']['secs'];
		if (isset($limits[$servRemoteAddr]))
		{
			$msgs = $limits[$servRemoteAddr]['msgs'];
			$secs = $limits[$servRemoteAddr]['secs'];
		}
		// если $msgs == 0 == unlim -> выходим
		if ($msgs == 0)
			break;
		// сравниваем разницу текущего времени и последнего записанного в базу с ($secs/$msgs)
		$delta = microtime(true)-$_SESSION[$servRemoteAddr]['microtime'];
		// если разница меньше чем ($secs/$msgs), то происходит попытка залогировать данные сверх предоставленного лимита
		if ($delta<($secs/$msgs))
			break;
	}
	// если он (IP) не встречался - заносим новое значение. а если встречался - по сути - перезаписываем.
	$_SESSION[$servRemoteAddr]['microtime'] = microtime(true);



	/* temporary section that provides dumping input content to log-file */
	$fname = $_SERVER["DOCUMENT_ROOT"]."logs/js_err.txt";
	$fhandle = fopen($fname, 'ab');
	$content = "";
	$content .= "message: '".$_POST['message'] . "'\n url: '".$_POST['url'] . "'\n line: '".$_POST['line'] . "'\n";
	$content .= "UA: '".$httpUserAgent . "'\n Referer: '".$httpReferer . "'\n RemoteAddr: '".$servRemoteAddr .
																		 "'\n LocalAddr: '".$httpXForwarded . "'\n";
	$content .= "date: ".date("Y-m-d H:i:s")."\n\n";
	fwrite($fhandle, $content);
	fclose($fhandle);
	/* end of temporary section */



	// Пишем ошибку в БД
	$query_dt = date("Y-m-d H:i:s");
	$query = "INSERT INTO `log_javascript` (	`id`, `message`, `url`, `line`, 
				`HTTP_USER_AGENT`, `HTTP_REFERER`, `REMOTE_ADDR`, `HTTP_X_REAL_IP`, `HTTP_X_FORWARDED_FOR`, `HTTP_VIA`, `dt`  )
			VALUES (						NULL , '$message_e', '$url_e', '$line_e', 
		'$httpUserAgent', '$httpReferer', '$servRemoteAddr', '$httpXRealIP', '$httpXForwarded', '$servHttpVia', '$query_dt' 
			); ";
	$logUpdate  = $db->select($query); 


	// И отсылаем ее на email. 
	// Думаю, ничего страшного не будет, если отсылать $message в unescaped-виде.
	ini_set("sendmail_from", "username@ema.il");
	ini_set("SMTP", "smtp.ema.il");
	$subject = 'JavaScript Error';
	$who = "JavaScript master"; 	// по аналогии с Webmaster
	$who_from = "JavaScript Master";

	$rcpt_to = "=?koi8-r?B?" .base64_encode(convert_cyr_string($who, "w","k")). "?= <username@ema.il>";
	$subject = "=?koi8-r?B?" .base64_encode(convert_cyr_string($subject, "w","k"))."?=";

	$message = "
	<html>
	<head>
	  <title>У меня где-то ошибка</title>
	</head>
	<body>
	  <p>Здравствуйте, %username%! <br><br>
	В процессе моего выполнения возникла ошибка: '$message',<br>
	по адресу '$url' на строчке '$line'.<br><br>
	Прочие переменные:<br>
	Referer: $httpReferer;<br>
	User-Agent: $httpUserAgent;<br>
	Remote-Addr: $servRemoteAddr.<br><br>
	Local-Addr: $httpXForwarded.<br><br>
	Время моего запуска: $query_dt<br><br>
	--<br>
	С уважением, <br>выполнявшийся скрипт.<br>
	  </p>
	</body>
	</html>
	";
	$message = convert_cyr_string($message, "w", "k"); 

	$headers  = "Content-Type: text/html; charset=koi8-r" . "\n";
	$headers .= "From: =?koi8-r?B?" .base64_encode(convert_cyr_string($who_from, "w","k")). "?= <username@ema.il>" . "\n";
	$headers .= "Reply-To: =?koi8-r?B?" .base64_encode(convert_cyr_string($who, "w","k")). "?= <username@ema.il>" . "\n";
	$headers .= "X-Mailer: PHP mail tool /" . phpversion();

	mail($rcpt_to, $subject, $message, $headers);

}

?>

Приложение A. SQL Section

CREATE TABLE `log_javascript` (
`id` smallint(5) unsigned NOT NULL auto_increment,
`message` text NOT NULL,
`url` text NOT NULL,
`line` text NOT NULL,
`HTTP_USER_AGENT` text NOT NULL,
`HTTP_REFERER` text NOT NULL,
`REMOTE_ADDR` varchar(255) NOT NULL,
`dt` timestamp NOT NULL default CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;

0

Автор: Kinweb, дата: 15 июля, 2009 - 14:45
#permalink

Не смог запустить отлов ошибок со второго варианта... Коннектинг к базе непонятный для меня - свой написал.. Куда код явы ставить тоже не понятно... Почему ошибки не ловило тоже не ясно...
Вобщем, спасибо за статью, но она явно не для начинающих)))


Автор: AzriMan, дата: 15 июля, 2009 - 15:14
#permalink

для коннектинга к базе у меня используется библиотека DbSimple. Я не стал время на расписывание через стандартные PHP-функции, ибо уклон не на это. Но если нужно - могу изменить тот кусок кода.
Код JavaScript (не путайте яву и яваскрипт) нужно вставить первой строчкой в .
А что означает "ошибки" не ловило? как это проявлялось?

--edited
добавил комментарий в тексте статьи, касающийся коннектинга к базе и места вставки JS кода. спасибо.


Автор: Kinweb, дата: 15 июля, 2009 - 16:01
#permalink

Жаль я не могу выложить сюда более подробное описание действий, несомненно в них где-то ошибка, так как не работает ничего, но попробую написать как я пытался подключить вышеизложенные скипты...

1. Подключился к БД, в которой заранее создал таблицу со всем необходимым.

$db = @mysql_connect("localhost", 'user', 'password'); 
if (!$db) { 
print  mysql_error();
} 
if (!@mysql_select_db('tablename', $db)) { 
print mysql_error(); 
}

2. Создал файл errors.php(в яваскрипте заменил конечно же название ссылки), положил на сервер и изменил в нем коннектинг к базе, вместо mail сделал print, что бы сразу видеть выдаётся что-либо вообще или нет.

3. JavaScript-код вставил в header на страницу, на которой я хочу отслеживать ошибки.

4. Создаю на этой странице ошибочный код и смотрю в БД, не появилась ли запись.

Мне кажется, что errors.php не принимает POST'a, не выполняет скрипт внутри и поэтому естественно ничего не пишет в базу. Вот почему он его не принимает не понимаю...


Автор: AzriMan, дата: 15 июля, 2009 - 16:19
#permalink

1. попробуйте в ф-и myErrHandler сделать alert(message) - что бы проверить, отлавливается ли ошибка вообще
2. Вы никак и не увидите результат print`a в php.
можно сделать:

или что-то наподобии такого:
вместо

var req =  new XMLHttpRequest();
	req.open('POST', 'http://'+server_url+'/ajax_chain.php', true)
	req.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
	req.setRequestHeader("Content-length", params.length);
	req.setRequestHeader("Connection", "close");
	req.send(params);

вставить

var req =  new XMLHttpRequest();
	var server_url = window.location.toString().split("/")[2];
	req.open('POST', 'http://'+server_url+'/ajax_chain.php', true);

	//Send the proper header information along with the request
	req.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
	req.setRequestHeader("Content-length", params.length);
	req.setRequestHeader("Connection", "close");

	req.onreadystatechange = function()
	{
		if (req.readyState != 4) return
		clearTimeout(timeout)
		if(req.status == 200) {
				a = document.getElementById('result');
				a.innerHTML=req.responseText;
		} else	// req.status != 200
			alert('some err');
	}
	req.send(params);
	// Таймаут 30 секунд
	var timeout = setTimeout( function(){ req.abort(); alert('timeout err') }, 30000);

далее на странице создаете какой-нибудь

<div id='result'></div>

, что бы в него записался результат по возвращении с сервера.

в первом случае запрос просто отсылался на сервер и нам было безразлично, а вернется ли ответ. и клиенту не нужно знать, вернется ли ответ. скрипт отсылает ошибку "прозрачно" для пользователя.

а во втором случае мы вешаем обработчик на вернувшийся ответ с сервера. более подробно про AJAX можно почитать в статьях соответствующего раздела этого сайта.

или на PHP сделать создание какого-нибудь файла. если файл создался - Вы попадаете в него. но ни print ни echo Вы не увидите. Вы аяксом послали запрос в тот файл. и Вас не волнует что там происходит.

Да, кстати.

$db->select($query);

нужно тоже заменить, ибо тут используется метод класс DbSimple, а не нативный php


Автор: Kinweb, дата: 15 июля, 2009 - 16:42
#permalink

Заработало все. Теперь ошибки на компе юзера записываются в БД. Кое-что пришлось изменить в PHP, но основная функция захвата ошибок все же работала.
Спасибо большое за помошь и разъяснение.


Автор: AzriMan, дата: 15 июля, 2009 - 16:57
#permalink

(чисто интересно)

а что было не так?
покажите Ваши изменения.


Автор: Kinweb, дата: 15 июля, 2009 - 17:49
#permalink

Ну прежде всего я не использовал DbSimple, а просто коннектился к БД, поэтому например

$db->select($query);

работать не могло. Так же я ставил javascript код и PHP код в один файл(errors.php), что бы все было "в одном" - являлось ли это ошибкой правда точно не знаю, из за других ошибок не смог проверить. Так же немного не понимал, каким образом POST может идти на PHP скрипт, который не выполняется юзером.. Ну и по мелочи еще чтото было - просто не понимал принципа и думал что ошибки кроются в каких-нибудь более сложных вещах в яваскрипте.. Помогло мне то, что Вы подсказали проверить работает ли вообще функция отлова ошибок

(alert(message);)

, поняв, что она работает, я стал искать что же не так в PHP скрипте и когда под свой коннектинг все переделал и query правильно записал все заработало!


Автор: AzriMan, дата: 15 июля, 2009 - 18:29
#permalink

Эээээээ....
а каким образом Вы объединили JS & PHP в одном файле?
(есть способы как такое можно сделать, но как-бы это немного не логично...)


Автор: hogart, дата: 16 июля, 2009 - 09:04
#permalink

Мысль не новая, но статья полезная.


Автор: Гость (не зарегистрирован), дата: 18 июля, 2009 - 19:34
#permalink

Вопрос Автору, как предпологаете боротся с чем-то вроде этого?

for(var i=0;i<1000;i++) { myErrHandler('Привет это DOS атака :)','http://google.com/',666);

Ведь открыть FireBug и дописать подобную строчку в код дело на 5 секунд...


Автор: AzriMan, дата: 20 июля, 2009 - 15:58
#permalink

я думал над этим. в "новой" версии будет встроена защита от подобного. примерный черновой код уже набросан, но его нужно будет оптимизировать и подтесать напильником.

как Вы считаете, ограничения на одну запись в одну секунду достаточно?
какой лимит лучше на Ваш взгляд и почему?


Автор: AzriMan, дата: 21 июля, 2009 - 09:13
#permalink

Чуть переделал текст статьи. Надеюсь, будет более понятно для обычного новичка.


Автор: AzriMan, дата: 24 июля, 2009 - 10:24
#permalink

Добавил защиту от DOS-а.

Хочется внести еще логгинг названия вызванной функции и переданных ей параметров (актуально для IE, ибо указанные им имена файлов и строки ну никак не соответствуют действительности) - но нет времени дописать код и проверить всё. особенно сериализацию..

Это может быть полезно, но способ определения не совсем удачный, на мой взгляд. из arguments.callee берется имя ф-и, а из arguments[] берутся переданные параметры, которые сериализуются. это всё записывается в глобальную переменную а из myErrHandler() считываются. но это еще мелочи. весь нюанс в том, что этот код необходимо вставлять в _каждую_ функцию, которая есть в наших скриптах. если мне подскажут более красивое решение этого - я буду признателен.


Автор: Гость (не зарегистрирован), дата: 1 августа, 2009 - 06:44
#permalink

onerror - некроссбраузерен


Автор: Zenitchik, дата: 4 февраля, 2014 - 20:13
#permalink

>Минусов, кажется, нет.

Я нашёл минус window.onerror: нет ссылки на объект Error, следовательно и до stack не добраться.


Автор: Гость (не зарегистрирован), дата: 12 апреля, 2022 - 23:09
#permalink

Автор: Гость (не зарегистрирован), дата: 13 апреля, 2022 - 06:23
#permalink

Автор: Гость (не зарегистрирован), дата: 16 апреля, 2022 - 00:47
#permalink

Автор: Гость (не зарегистрирован), дата: 16 апреля, 2022 - 14:16
#permalink

Автор: Гость (не зарегистрирован), дата: 16 апреля, 2022 - 14:28
#permalink

Отправить комментарий

Приветствуются комментарии:
  • Полезные.
  • Дополняющие прочитанное.
  • Вопросы по прочитанному. Именно по прочитанному, чтобы ответ на него помог другим разобраться в предмете статьи. Другие вопросы могут быть удалены.
    Для остальных вопросов и обсуждений есть форум.
P.S. Лучшее "спасибо" - не комментарий, как все здорово, а рекомендация или ссылка на статью.
Содержание этого поля является приватным и не предназначено к показу.
  • Адреса страниц и электронной почты автоматически преобразуются в ссылки.
  • Разрешены HTML-таги: <strike> <a> <em> <strong> <cite> <code> <ul> <ol> <li> <dl> <dt> <dd> <u> <i> <b> <pre> <img> <abbr> <blockquote> <h1> <h2> <h3> <h4> <h5> <p> <div> <span> <sub> <sup>
  • Строки и параграфы переносятся автоматически.
  • Текстовые смайлы будут заменены на графические.

Подробнее о форматировании

CAPTCHA
Антиспам
2 + 3 =
Введите результат. Например, для 1+3, введите 4.
 
Поиск по сайту
Другие записи этого автора
Больше записей нет. Прокомментируйте эту запись - может быть, тогда он что-нибудь еще хорошее напишет ;)
Содержание

Учебник javascript

Основные элементы языка

Сундучок с инструментами

Интерфейсы

Все об AJAX

Оптимизация

Разное

Дерево всех статей

Популярные таги
Последние комментарии
Последние темы на форуме
Forum