Статьи

Регистрация ошибок в клиентских приложениях

Эта статья была рецензирована Panayiotis «pvgr» Велисаракос , Джеймс Райт и Стефан Макс . Спасибо всем рецензентам SitePoint за то, что сделали контент SitePoint как можно лучше!

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

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

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

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

В этой статье я рассмотрю некоторые способы, с помощью которых вы можете реализовать ведение журнала в клиентском приложении; особенно в JavaScript-одностраничном приложении (SPA).

Консоль

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

Реализация console не всегда последовательна, особенно в IE, что неудивительно, но в целом вам доступны четыре основных метода:

 console.log() console.info() console.warn() console.error() 

Вывод каждого из этих четырех методов несколько отличается, и большинство реализаций веб-консоли (т. Е. Dev Tools) позволяют фильтровать сообщения на основе используемого метода; то есть уровень ведения журнала.

Чтобы уменьшить различия между браузерами, вы можете использовать функцию-обертку — такую, как эта, от Paul Irish . WHATWG пытается стандартизировать консольный API , но спецификация все еще находится на ранней стадии и вряд ли будет реализована в течение некоторого времени.

Совет: Если вы обнаружите, что ваш код изобилует операторами console.log() , вы можете найти такие инструменты, как grunt-remove-logging или grunt-strip для Grunt или gulp-strip-debug для Gulp, полезные при перемещении применение в производство.

Улучшение консоли

Есть несколько библиотек, которые вы можете использовать для «перезарядки» консоли.

Logdown

Logdown — это небольшая библиотека, которая предоставляет несколько улучшений для консоли. Вы найдете демо здесь .

Выход из системы позволяет указать префиксы при создании экземпляра; Одним из возможных вариантов использования этого является разделение ваших сообщений журнала по модулю, например:

 var uiLogger = new Logdown({prefix: 'MyApp:UI'}); var networkServiceLogger = new Logdown({prefix: 'MyApp:Network'}); 

Затем вы можете включить или отключить регистраторы по их префиксу, например:

 Logdown.disable('MyApp:UI'); Logdown.enable('MyApp:Network'); Logdown.disable('MyApp:*'); // wildcards are supported, too 

Отключение логгера эффективно заставляет его замолчать.

После того, как вы инстанцировали один или несколько регистраторов, вы можете регистрировать сообщения, используя методы log() , warn() , info() и error() :

 var logger = new Logdown(); logger.log('Page changed'); logger.warn('XYZ has been deprecated in favour of 123'); logger.info('Informational message here'); logger.error('Server API not available!'); 

Logdown также обеспечивает поддержку Markdown:

 var logger = new Logdown({markdown: true}); // Technically "markdown: true" isn't required; it's enabled by default logger.warn('_XYZ_ has been *deprecated* in favour of _123_'); 

console.message

console.message — это еще одна библиотека для украшения вывода консоли.

Вот небольшая анимация из документации, демонстрирующая некоторые ее особенности:

console.message в действии

По сути, библиотека предоставляет цепной интерфейс с методами, которые позволяют форматировать текст, группировать сообщения и делать их разборными, отправлять интерактивные элементы или объекты DOM в журнал — и даже включать изображения.

Ограничения консоли

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

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

Другие вещи для рассмотрения

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

Захват глобальных ошибок

По крайней мере, стоит фиксировать и регистрировать любые необработанные исключения. Вы можете сделать это, используя window.onerror . Вот действительно простой пример:

 window.onerror = function(message, file, line) { console.log('An error occured at line ' + line + ' of ' + file + ': ' + message); }; 

Следы стека

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

TraceKit

TraceKit позволяет вам вставлять трассировки стека в исключения и что-то делать с ними (например, отправлять их на серверный компонент журналирования), подписываясь на них.

Вот как может выглядеть код:

 TraceKit.report.subscribe(function yourLogger(errorReport) { //send via ajax to server, or use console.error in development //to get you started see: https://gist.github.com/4491219 }); 

Затем в вашем приложении:

 try { /* * your application code here * */ throw new Error('oops'); } catch (e) { TraceKit.report(e); //error with stack trace gets normalized and sent to subscriber } 

stacktrace.js

stacktrace.js , цитируя документацию, является «независимой от фреймворка микро-библиотекой для получения трассировки стека во всех веб-браузерах».

Он предоставляет метод с именем printStackTrace() который можно использовать в обработчике ошибок, чтобы добавить трассировку стека в функцию ведения журнала. Например, мы могли бы улучшить наш серверный регистратор следующим образом:

 function log(data, level) { $.post( 'https://your-app.com/api/logger', { context : navigator.userAgent, level : level || 'error', data : data, stack_trace : printStackTrace() } ); } 

Ведение клиентских ошибок на сервере

Отправка записей журнала на сервер имеет ряд преимуществ:

  1. Вы можете захватывать записи журнала из вашего приложения, не находясь физически за компьютером (идеально в производстве)
  2. Вы можете управлять журналами на стороне сервера и на стороне клиента в одном месте, потенциально используя одни и те же инструменты
  3. Вы можете настроить оповещения (например, Slack уведомление или SMS при возникновении критической ошибки)
  4. Там, где консоль недоступна или ее трудно просматривать (например, при использовании веб-браузера на мобильном телефоне), легче увидеть, что происходит

Давайте посмотрим на несколько подходов к этому.

Роллинг свой собственный серверный регистратор

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

Вот чрезвычайно минимальный пример клиентской части, использующей jQuery:

 function log(data, level) { $.post( 'https://your-app.com/api/logger', { context : navigator.userAgent, level : level || 'error', data : data } ); } 

Некоторые примеры использования:

 try { // some function } catch (e) { log({ error : e.message }); } 
 log('Informational message here', 'info'); 

Имея это в виду, вот базовый серверный компонент, сопровождающий этот пример, созданный с использованием Node.js с Express вместе с превосходной библиотекой журналов Winston :

 /** * Load the dependencies */ var express = require( 'express' ); var bodyParser = require('body-parser'); var winston = require( 'winston' ); /** * Create the Express app */ var app = express(); app.use(bodyParser.urlencoded({ extended: true })); /** * Instantiate the logger */ var logger = new ( winston.Logger )({ transports: [ new ( winston.transports.Console )( { level: 'error' } ), new ( winston.transports.DailyRotateFile )( { filename: 'logs/client.log', datePattern: '.yyyy-MM-dd' } ) ] }); app.post ('/api/logger', function( req, res, next ) { logger.log( req.body.level || 'error', 'Client: ' + req.body.data ); return res.send( 'OK' ); }); var server = app.listen( 8080, function() { console.log( 'Listening on port %d', server.address().port ); }); 

На практике существуют некоторые фундаментальные ограничения для этого упрощенного регистратора:

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

Лучший способ справиться со второй проблемой — это буферизовать записи в журнале и отправлять их партиями. Обычный подход заключается в использовании localStorage для хранения записей журнала, а затем отправки их через определенные промежутки времени — будь то на основе времени, когда достигнут определенный порог в количестве ожидающих записей, или когда пользователь закрывает окно или удаляется от вашего приложение, используя событие window.onbeforeunload .

Чтобы обойти эти проблемы, давайте посмотрим на готовое решение для регистрации из приложений JS.

log4javascript

log4javascript основан на вездесущем log4j , Java-каркасе, который также был портирован на PHP , поэтому, если вы работаете на серверном фоне, вы, возможно, уже знакомы с ним.

log4javascript использует концепцию appenders , которая определяет, что происходит, когда вы вызываете один из его методов ведения журнала. По умолчанию, PopUpAppender , возможно, довольно избыточен, если у вас есть инструменты разработки, предоставляемые большинством современных браузеров.

Вероятно, более полезным является AjaxAppender , который вы можете использовать для отправки записей журнала обратно на сервер. Вы можете настроить AjaxAppender для отправки записей партиями через определенные интервалы времени с помощью setTimed() , определенного числа с помощью setBatchSize() или когда окно выгружается с помощью setSendAllOnUnload() .

log4javascript доступен для загрузки с Sourceforge , или аналогичный Log4js доступен на Github . Вы можете обратиться к Quickstart, чтобы быстро начать работу.

Вот пример:

 var log = log4javascript.getLogger(); var ajaxAppender = new log4javascript.AjaxAppender('http://example.com/api/logger'); ajaxAppender.setThreshold(log4javascript.Level.ERROR); ajaxAppender.setBatchSize(10); // send in batches of 10 ajaxAppender.setSendAllOnUnload(); // send all remaining messages on window.beforeunload() log.addAppender(ajaxAppender); 

Кроме того, для отправки сообщений через определенный интервал:

 ajaxAppender.setTimed(true); ajaxAppender.setTimerInterval(10000); // send every 10 seconds (unit is milliseconds) 

Другие библиотеки

Если ваш проект использует jQuery, вы можете заглянуть в jquery logger, который позволяет вам войти через Ajax; однако он не поддерживает пакеты. Тем не менее, он прекрасно интегрируется с Airbrake в качестве бэк-энда.

loglevel — это легковесная и расширяемая среда ведения журналов на основе JS, которая поддерживает Ajax через отдельный плагин serverSend .

Сверните свой собственный пакетно-совместимый регистратор

Вот простое доказательство концепции регистратора, который отправляет сообщения партиями. Он написан с использованием ванильного JavaScript с функциями ES6.

 "use strict"; class Logger { // Log levels as per https://tools.ietf.org/html/rfc5424 static get ERROR() { return 3; } static get WARN() { return 4; } static get INFO() { return 6; } static get DEBUG() { return 7; } constructor(options) { if ( !options || typeof options !== 'object' ) { throw new Error('options are required, and must be an object'); } if (!options.url) { throw new Error('options must include a url property'); } this.url = options.url; this.headers = options.headers || [ { 'Content-Type' : 'application/json' } ]; this.level = options.level || Logger.ERROR; this.batch_size = options.batch_size || 10; this.messages = []; } send(messages) { var xhr = new XMLHttpRequest(); xhr.open('POST', this.url, true); this.headers.forEach(function(header){ xhr.setRequestHeader( Object.keys(header)[0], header[Object.keys(header)[0]] ); }); var data = JSON.stringify({ context : navigator.userAgent, messages : messages }); xhr.send(data); } log(level, message) { if (level <= this.level) { this.messages.push({ level : level, message : message }); if (this.messages.length >= this.batch_size) { this.send(this.messages.splice(0, this.batch_size)); } } } error(message) { this.log(Logger.ERROR, message); } warn(message) { this.log(Logger.WARN, message); } info(message) { this.log(Logger.INFO, message); } debug(message) { this.log(Logger.DEBUG, message); } } 

Использование простое:

 var logger = new Logger({ url : 'http://example.com/api/batch-logger', batch_size : 5, level : Logger.INFO }); logger.debug('This is a debug message'); // No effect logger.info('This is an info message'); logger.warn('This is a warning'); logger.error('This is an error message'); logger.log(Logger.WARN, 'This is a warning'); 

Опции на основе собственного сервера

Errbit

Errbit — это автономное решение с открытым исходным кодом для регистрации ошибок. Он реализован на Ruby и использует MongoDB для хранения.

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

Чтобы войти в онлайн-демо, используйте электронную почту demo@errbit-demo.herokuapp.com и пароль password .

Опции SaaS-сервера

Существует несколько решений SaaS для ведения журнала. К ним относятся Loggly , track.js , ErrorCeption , Airbrake и New Relic .

Давайте кратко рассмотрим несколько таких решений.

Loggly

Loggly — одно из многих решений SaaS. Я собираюсь использовать его в качестве примера, потому что это легко и бесплатно, чтобы начать. С бесплатным планом вы можете войти до 200 МБ в день, а данные хранятся в течение 7 дней.

Чтобы использовать Loggly из клиентского приложения, вам необходимо включить следующий фрагмент:

 <script type="text/javascript" src="http://cloudfront.loggly.com/js/loggly.tracker.js" async></script> <script> var _LTracker = _LTracker || []; _LTracker.push({'logglyKey': 'YOUR-LOGGING-KEY', 'sendConsoleErrors' : true }); </script> 

Примечание. Вам нужно заменить YOUR-LOGGING-KEY на значение, специфичное для вашего приложения, которое вы получите, войдя в систему и выполнив вход в систему, перейдя в Source Setup .

Если вы изучите этот код, то увидите, что объект _LTracker изначально создается как массив. Это метод «shim», используемый во многих аналитических библиотеках, что означает, что вы можете вызвать push() для него до загрузки библиотеки. Любые ошибки или сообщения, которые вы помещаете в этот массив, будут поставлены в очередь, когда библиотека станет доступной.

Использование простое:

 _LTracker.push(data); 

Вы можете использовать его для отправки фрагмента текста:

 _LTracker.push( 'An error occured: ' + e.message ); 

Или, возможно, более полезно, вы можете использовать JSON — например:

 try { // some operation } catch (e) { _LTracker.push({ level : 'error', message : e.message, trace : e.trace, context : navigator.userAgent }); } 

Хотя это довольно простое решение, вы можете просто использовать следующий код для регистрации ошибок:

 window.onerror = function(message, file, line) { _LTracker.push({ context: navigator.userAgent, error: message, file: file, line: line }); }; 

У этого подхода есть некоторые ограничения. Номера строк практически бесполезны, если у вас немного разные сборки или вы минимизируете свой код JS.

В приведенном выше фрагменте sendConsoleErrors вы также заметите, что для sendConsoleErrors установлено значение TRUE , которое автоматически регистрирует определенные ошибки, не отправляя их вручную. Например, следующее будет отправлено Loggly, если истечет время ожидания RequireJS:

 { "category": "BrowserJsException", "exception": { "url": "http://example.com/js/require.js", "message": "Uncaught Error: Load timeout for modules: main\nhttp://requirejs.org/docs/errors.html#timeout", "lineno": 141, "colno": 15 }, "sessionId": "xyz-123-xyz-123" } 

{} track.js

{track.js} — еще одно SaaS-решение для ведения журнала.

Они предлагают бесплатный план; он ограничен 10 ошибками в минуту, 10 000 обращений в месяц, а ваши данные хранятся только 24 часа. Самый базовый платный тариф — 29,99 долларов в месяц. Более подробную информацию вы найдете на его странице с ценами .

Примечание: «попадание» записывается всякий раз, когда библиотека инициализируется.

Установить его просто:

 <!-- BEGIN TRACKJS --> <script type="text/javascript">window._trackJs = { token: 'YOUR-TOKEN-HERE' };</script> <script type="text/javascript" src="//d2zah9y47r7bi2.cloudfront.net/releases/current/tracker.js" crossorigin="anonymous"></script> <!-- END TRACKJS --> 

После того, как вы загрузили соответствующий файл и инициализировали библиотеку, вы можете использовать такие методы, как track() :

 /** * Directly invokes an error to be sent to TrackJS. * * @method track * @param {Error|String} error The error to be tracked. If error does not have a stacktrace, will attempt to generate one. */ trackJs.track("Logical error: state should not be null"); try { // do something } catch (e) { trackJs.track(e); } 

Или используйте консоль, которая будет отправлять сообщения в веб-сервис:

 trackJs.console.debug("a message"); // debug severity trackJs.console.log("another message"); // log severity 

С помощью {track.js} вы можете сделать гораздо больше — ознакомьтесь с документацией для получения дополнительной информации.

В итоге

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

Как вы обрабатываете вход в свои клиентские приложения? Вы разработали свой собственный подход? Используете ли вы что-то не покрытое здесь Дай мне знать в комментариях.