- PVSM.RU - https://www.pvsm.ru -

Мониторинг JavaScript-ошибок с помощью window.onerror

Материал, перевод которого мы сегодня публикуем, посвящён обработке JS-ошибок с помощью window.onerror. Это — особое событие браузера, которое вызывается при появлении неперехваченных ошибок. Здесь мы поговорим о том, как перехватывать ошибки с помощью обработчика события onerror, и о том, как отправлять сведения о них на сервер разработчика веб-сайта. Этот обработчик можно использовать в качестве основы собственной системы сбора и анализа информации об ошибках. Кроме того, он является одним из важнейших механизмов, применяемых в библиотеках, ориентированных на работу с ошибками, таких, как raven-js [1].

image [2]

Прослушивание события window.onerror

Прослушивать событие onerror можно, назначив window.onerror функцию, играющую роль обработчика ошибок:

window.onerror = function(msg, url, lineNo, columnNo, error) {
  // ... обработка ошибки ...
  return false;
}

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

  • msg — сообщение ошибки. Например — Uncaught ReferenceError: foo is not defined.
  • url — адрес скрипта или документа, в котором произошла ошибка. Например — /dist/app.js.
  • lineNo — номер строки, в которой произошла ошибка (если поддерживается).
  • columnNo — номер столбца строки (если поддерживается).
  • error — объект ошибки (если поддерживается).

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

Объект Error и свойство Error.prototype.stack

На первый взгляд в объекте Error нет ничего особенного. Он содержит три вполне стандартных свойства — message, fileName и lineNumber. Эти данные, учитывая сведения, передаваемые в обработчик события window.onerror, можно считать избыточными.

Настоящую ценность в данном случае представляет собой нестандартное свойство Error.prototype.stack. Это свойство даёт доступ к стеку вызовов (стеку ошибки), позволяет узнать о том, что происходило в программе на момент возникновения ошибки, вызов каких функций предшествовал её появлению. Трассировка стека вызовов может оказаться важнейшей частью процесса отладки. И, несмотря на то, что свойство stack стандартным не является, оно доступно во всех современных браузерах.

Вот как выглядит свойство stack объекта ошибки в Chrome 46.

"Error: foobarn    at new bar (<anonymous>:241:11)n    at foo (<anonymous>:245:5)n    at <anonymous>:250:5n    at <anonymous>:251:3n    at <anonymous>:267:4n    at callFunction (<anonymous>:229:33)n    at <anonymous>:239:23n    at <anonymous>:240:3n    at Object.InjectedScript._evaluateOn (<anonymous>:875:140)n    at Object.InjectedScript._evaluateAndWrap (<anonymous>:808:34)"

Перед нами — неотформатированная строка. Когда содержимое этого свойства представлено в таком виде, работать с ним неудобно. Вот как то же самое будет выглядеть после форматирования.

Error: foobar
  at new bar (<anonymous>:241:11)
  at foo (<anonymous>:245:5)
  at callFunction (<anonymous>:229:33)
  at Object.InjectedScript._evaluateOn (<anonymous>:875:140)
  at Object.InjectedScript._evaluateAndWrap (<anonymous>:808:34)

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

Однако, и тут не всё гладко. Свойство stack не стандартизировано, оно по-разному реализовано в различных браузерах. Вот, например, как выглядит стек ошибки в Internet Explorer 11.

Error: foobar
  at bar (Unknown script code:2:5)
  at foo (Unknown script code:6:5)
  at Anonymous function (Unknown script code:11:5)
  at Anonymous function (Unknown script code:10:2)
  at Anonymous function (Unknown script code:1:73)

Можно заметить, в сравнении с предыдущим примером, то, что здесь не только используется другой формат представления стековых кадров, но и то, что здесь приведено меньше данных по каждому кадру. Например, Chrome выявляет случаи использования ключевого слова new и даёт более подробные сведения о других событиях (в частности, о вызовах функций._evaluateOn и ._evaluateAndWrap). При этом тут мы сравнили лишь то, что выдают IE и Chrome. В других браузерах использованы собственные подходы к выводу данных о стеке и к подбору сведений, включаемых в эти данные.

Для того чтобы привести всё это к единообразному виду, можно воспользоваться сторонними инструментами. Например, в raven-js для этого используется TraceKit. Той же цели служит stacktrace.js и некоторые другие проекты.

Особенности поддержки window.onerror различными браузерами

Событие windows.onerror существует в браузерах уже довольно давно. В частности, его можно обнаружить в IE6 и в Firefox 2. Проблема здесь заключается в том, что все браузеры реализуют windows.onerror по-разному. Например, это касается количества и структуры аргументов, передаваемых обработчикам этого события.

Вот таблица, в которой приведены сведения об аргументах, передаваемых обработчику onerror в основных браузерах.

Браузер message url lineNo colNo errorObj
Firefox Есть Есть Есть Есть Есть
Chrome Есть Есть Есть Есть Есть
Edge Есть Есть Есть Есть Есть
IE 11 Есть Есть Есть Есть Есть
IE10 Есть Есть Есть Есть Нет
IE 9,8 Есть Есть Есть Нет Нет
Safari 10 и выше Есть Есть Есть Есть Есть
Safari 9 Есть Есть Есть Есть Нет
Android Browser 4.4 Есть Есть Есть Есть Нет

Вероятно, нет ничего удивительного в том, что Internet Explorer 8, 9, и 10 имеют ограниченную поддержку onerror. Однако необычным может показаться то, что в браузере Safari поддержка объекта ошибки появилась лишь в 10-й версии, вышедшей в 2016 году. Кроме того, существуют устаревшие мобильные устройства, использующие стандартный браузер Android, который также не поддерживает объект ошибки. В современных версиях Android этот браузер заменён на Chrome Mobile.

Если в нашем распоряжении нет объекта ошибки, то нет и данных о трассировке стека. Это означает, что браузеры, не поддерживающие объект ошибки, не дают, в стандартном сценарии использования обработчика onerror, сведений о стеке. А это, как мы уже говорили, очень важно.

Разработка полифилла для window.onerror с использованием конструкции try/catch

Для того чтобы получить сведения о стеке в браузерах, не поддерживающих передачу в обработчик onerror объекта ошибки, можно воспользоваться следующим приёмом. Код можно обернуть в конструкцию try/catch и перехватывать ошибки самостоятельно. Полученный в результате объект ошибки будет содержать, во всех современных браузерах, то, что нам нужно — свойство stack.
Взгляните на код вспомогательного метода invoke(), который вызывает заданный метод объекта, передавая ему массив аргументов.

function invoke(obj, method, args) {
  return obj[method].apply(this,args);
}

Вот как им пользоваться.

invoke(Math, 'max', [1,2]); // вернёт 2

Вот тот же invoke(), но теперь вызов метода обёрнут в try/catch, что позволяет перехватывать возможные ошибки.

function invoke(obj, method, args) {
  try {
    return obj[method].apply(this,args);
  } catch(e) {
    captureError(e);// сообщить об ошибке
    throw e;// повторно выбросить ту же ошибку
  }
}

invoke(Math,'highest',[1,2]); // выбрасывает ошибку, сигнализирующую об отсутствии метода Math.highest

Конечно, весьма накладно вручную добавлять подобные конструкции во все места, где они могут понадобиться. Эту задачу можно упростить, создав универсальную вспомогательную функцию.

function wrapErrors(fn) {
  //не оборачивайте функции больше одного раза
  if(!fn.__wrapped__) {
    fn.__wrapped__ = function() {
      try{
        return fn.apply(this,arguments);
      }catch(e){
        captureError(e);// сообщить об ошибке
        throw e;// повторно выбросить ту же ошибку
      }
    };
  }

  return fn.__wrapped__;
}

var invoke = wrapErrors(function(obj, method, args) {
  returnobj[method].apply(this,args);
});

invoke(Math,'highest',[1,2]);//ошибка, нет метода Math.highest

Так как в JavaScript используется однопоточная модель выполнения кода, эту обёртку нужно использовать только с теми вызовами функций, которые находятся в начале новых стеков. Нет необходимости оборачивать в неё абсолютно все вызовы функций.

В результате оказывается, что эту функцию нужно использовать в следующих местах:

  • Там, где начинается работа приложения (например, при использовании jQuery, в функции $(document).ready)
  • В обработчиках событий (например, в addEventListener или в конструкциях вида $.fn.click)
  • В коллбэках, вызываемых по событиям таймера (например, это setTimeout или requestAnimationFrame)

Вот пример использования функции wrapErrors.

$(wrapErrors(function () {// начало работы приложения

  doSynchronousStuff1();// здесь обёртка не нужна

  setTimeout(wrapErrors(function () {
    doSynchronousStuff2();// здесь тоже не нужна обёртка
  }));

  $('.foo').click(wrapErrors(function () {
    doSynchronousStuff3();// и здесь не нужна
  }));

}));

Такие конструкции можно добавлять в код самостоятельно, но это слишком трудоёмкая задача. В качестве удобной альтернативы в подобных ситуациях можно рассмотреть библиотеки для работы с ошибками, которые, например, имеют механизмы, снабжающие addEventListener и setTimeout средствами для перехвата ошибок.

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

Итак, теперь в нашем распоряжении есть средства для перехвата сведений об ошибках либо с помощью windows.onerror, либо с помощью вспомогательных функций, основанных на try/catch. Эти ошибки возникают на стороне клиента, и, после их перехвата, нам хотелось бы с ними разобраться и принять меры к их устранению. Для этого их нужно передать на наш сервер. Для того чтобы это сделать, нужно подготовить веб-сервис, который принимал бы сведения об ошибках по HTTP, после чего каким-то образом сохранял бы их для дальнейшей обработки, скажем — писал бы в лог-файл или в базу данных.

Если этот веб-сервис расположен на том же домене, что и веб-приложение, достаточно будет воспользоваться XMLHttpRequest. В следующем примере показано использование функции для выполнения AJAX-запросов из jQuery для передачи данных на сервер.

function captureError(ex){

  var errorData = {
    name:ex.name,// например: ReferenceError
    message:ex.line,// например: x is undefined
    url:document.location.href,
    stack:ex.stack// строка трассировки стека; помните, в разных браузерах они разные!
  };

  $.post('/logger/js/',{
    data:errorData
  });

}

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

Итоги

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

  • Особенности события onerror и его поддержка в различных браузерах.
  • Использование механизма try/catch для получения сведений о стеке вызовов в тех случаях, когда onerror не поддерживает работу с объектом ошибки.
  • Передача данных об ошибках на сервер разработчика.

Узнав о том, как работают вышеописанные механизмы, вы обзавелись базовыми знаниями, которые позволят приступить к созданию собственной системы для работы с ошибками, уточнив в ходе работы дополнительные детали. Пожалуй, этот сценарий особенно актуален для тех случаев, когда речь идёт о некоем приложении, в котором, скажем, из соображений безопасности, не планируется использовать сторонние библиотеки. Если же ваше приложение допускает использование стороннего кода, вы вполне можете подобрать подходящий инструмент для мониторинга JS-ошибок. Среди таких инструментов — Sentry [3], Rollbar [4], TrackJS [5] и другие подобные проекты.

Уважаемые читатели! Какими инструментами для мониторинга JS-ошибок вы пользуетесь?

Автор: ru_vds

Источник [6]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/javascript/282035

Ссылки в тексте:

[1] raven-js: https://ravenjs.com/

[2] Image: https://habr.com/company/ruvds/blog/413173/

[3] Sentry: https://sentry.io/welcome/

[4] Rollbar: https://rollbar.com/

[5] TrackJS: https://trackjs.com/

[6] Источник: https://habr.com/post/413173/?utm_campaign=413173