Использование системы отслеживания js-ошибок трудно переоценить. Даже на покрытом тестами сайте возникают js-ошибки, важно их найти и починить. Расскажу как искал подходящее решение.


Существует три вида систем отслеживания js-ошибок.


Первый вид


Самописная система. Добавляем компактный js-код на сайт:


window.onerror = function(msg, file, line, col) {
    new Image().src = '/jserrors/?msg=' + msg + ...;
}

Делаем «ручку» для сохранения ошибок, парсим логи. В лучшем случае, пишем свой интерфейс для анализа ошибок. Потом занимаемся доработкой и поддержкой.


Второй вид


Платный сервис с расширенными возможностями, но с ограничениями, например, на количество js-ошибок в день или месяц. На любой сайт поставить эту систему не получится, придётся каждый раз выбирать какой из ваших сайтов «достоин» платного сервиса. И не забывать оплачивать услуги.


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


Третий вид


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


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


Решение


Объединим первый и третий вид. Ошибки будем вручную отправлять в Яндекс.Метрику с помощью компактного js-кода. В Метрике есть необходимые отчёты и инструменты для анализа данных, нет особых ограничений на количество счётчиков и собираемых данных, и она бесплатна.


Для сбора ошибок подойдёт отчёт «Параметры визитов».


  1. Заведём отдельный счётчик.


  2. Добавим на страницу перед всеми скриптами компактный код:


    <script>
    // Не более 320 байт после минификации.
    window.onerror = function(msg, file, line, col, err) {
        // Отсекаем совсем старые браузеры.
        if (!window.JSON) { return; }
    
        var counterId = 12345, // Ваш номер счётчика Метрики.
            siteInfo = {},
            pointer = siteInfo,
            // Список параметров визитов.
            path = [
                'JS errors', // 1 уровень
                msg, // 2 уровень
                err && err.stack || (file + ':' + line + ':' + col) // 3 уровень
                // Не хватает параметров? Добавьте ещё!
            ];
    
        // Преобразуем параметры из плоского в древовидный вид для отчёта.
        for (var i = 0; i < path.length - 1; i++) {
            var item = path[i];
            pointer[item] = {};
            pointer = pointer[item];
        }
    
        pointer[path[i]] = 1;
    
        new Image().src = 'https://mc.yandex.ru/watch/' + counterId +
            '/?site-info=' + encodeURIComponent(JSON.stringify(siteInfo))
            '&rn=' + Math.random();
    };
    </script>

  3. Не забываем указать в коде свой номер счётчика (counterId).


  4. Получаем примерно такой отчёт:


Структуру и порядок параметров в отчёте можно менять на лету, а также добавлять новые параметры. Давайте с помощью кнопки «Группировки» добавим браузер и ОС в отчёт.



И ещё один момент, если скрипты на сайте загружаются с другого домена (CDN), то в отчёте, скорее всего, будут видны сообщения вида «Script error» и без стека.
Чтобы вернуть сообщениям нормальный вид, необходимо добавить к скриптам атрибут crossorigin="anonymous" и HTTP-заголовок Access-Control-Allow-Origin:"*".


<script src="https://mycdn.com/folder/file.js" crossorigin="anonymous"></script>

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


Дополнительно добавим ограничение на собираемое количество ошибок (не более 5) на странице. Например, однотипные ошибки, возникающие при движении мышки, могут создать сотни запросов в Метрику.


В современных браузерах данные будем отправлять через sendBeacon.


<script>
window.onerror = function handler(msg, file, line, col, err) {
    if (!window.JSON || handler.count > 5) { return; }

    var counterId = 12345, // Ваш номер счётчика Метрики.
        siteInfo = {},
        pointer = siteInfo,
        stack = err && err.stack,
        path = [
            // Укажите в регулярном выражении домены, с которых загружаются ваши скрипты и сайт.
            'JS ' + (!file || /mysite\.ru|cdn\.com/.test(file) ? 'in' : 'ex') + 'ternal errors',
            'message: ' + msg,
            stack ?
                'stack: ' + stack :
                (file ? 'file: ' + file + ':' + line + ':' + col : 'nofile'),
            'href: ' + location.href
        ];

    for (var i = 0; i < path.length - 1; i++) {
        var item = path[i];
        pointer[item] = {};
        pointer = pointer[item];
    }

    pointer[path[i]] = 1;

    var url = 'https://mc.yandex.ru/watch/' + counterId + '/' +
            '?site-info=' + encodeURIComponent(JSON.stringify(siteInfo)) +
            '&rn=' + Math.random();

    if (typeof navigator.sendBeacon === 'function') {
        navigator.sendBeacon(url, ' ');
    } else {
        new Image().src = url;
    }

    if (handler.count) {
        handler.count++;
    } else {
        handler.count = 1;
    }
};
</script>

И ещё, данные по ошибкам можно получить с помощью API и сделать с ними всё что угодно.


Не забудьте добавить ссылки на отчёты по ошибкам в свою документацию на видное место.
И дать доступ к отчётам остальным разработчикам из группы, чтобы исправление ошибок превратилось в соревнование.


Ссылки:


Поделиться с друзьями
-->

Комментарии (21)


  1. webschik
    22.03.2017 11:40
    -2

    Может все-таки Sentry? :)


  1. IvanSCM
    22.03.2017 12:41
    +13

    Напомнило


  1. madkite
    22.03.2017 16:31

    А "номер счётчика" вы как, в коде всегда хардкодите? Или при build-е как-то вставляете?


    1. hcodes
      22.03.2017 17:04

      Да, захардкодить, при сборке или с помощью шаблонизатора.

      //...
      var counterId = '{{MY_COUNTER_ID}}';
      //...
      


  1. vitalets
    23.03.2017 09:07
    +2

    Отличная статья, мы тоже именно так делаем!


    Несколько дополнений:


    1. Кроме подписки на ошибки еще стоит подписаться на непойманные промисы через onunhandledrejection.


    2. Саму подписку лучше делать через window.addEventListener, а то какая-нибудь сторонняя библиотека может переопределить window.onerror.


    3. Чтобы подписаться сразу и на ошибки и на промисы есть библиотечка uncaught.


    4. URL для метрики можно не конструировать вручную, а использовать метод hit из API счетчика, передавая данные в params.


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


    1. hcodes
      23.03.2017 22:22

      1. Малая поддержка в браузерах, только Chrome.
      2. У IE8 нет addEventListener. А так, конечно, лучше addEventListener.
      3. Зависимость, которая тянет за собой дополнительный код. Код сбора ошибок инлайнится перед всеми скриптами и должен быть лаконичным.
      4. Про метод знаю, но реализация в статье не зависит от кода отслеживания Метрики.
      5. Метрике на заметку. :)


      1. justboris
        24.03.2017 13:47

        1. Малая поддержка в браузерах, только Chrome.

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


        1. hcodes
          24.03.2017 16:14

          Уговорили, добавлю.


      1. justboris
        24.03.2017 13:52

        1. Про метод знаю, но реализация в статье не зависит от кода отслеживания Метрики.

        На самом деле вы делаете еще хуже. Потому что названия урлов это часть приватного API, и Метрика может его поменять и все вам сломать.


        А метод hit задокуметирован и поддерживается, а значит без предупреждения не изменится.


        1. hcodes
          24.03.2017 16:26

          Не совсем так. Этот урл можно увидеть у картинки, которая находится в теге NOSCRIPT в коде установки Метрики. Урл не поменяется, с таким кодом Метрика установлена на многих сайтах.

          <!-- Yandex.Metrika counter -->
          <script type="text/javascript"> (function (d, w, c) { (w[c] = w[c] || []).push(function() { try { w.yaCounter43574444 = new Ya.Metrika({ id:43574444, clickmap:true, trackLinks:true, accurateTrackBounce:true }); } catch(e) { } }); var n = d.getElementsByTagName("script")[0], s = d.createElement("script"), f = function () { n.parentNode.insertBefore(s, n); }; s.type = "text/javascript"; s.async = true; s.src = "https://mc.yandex.ru/metrika/watch.js"; if (w.opera == "[object Opera]") { d.addEventListener("DOMContentLoaded", f, false); } else { f(); } })(document, window, "yandex_metrika_callbacks");</script>
          
          <noscript><div><img src="https://mc.yandex.ru/watch/43574444" style="position:absolute; left:-9999px;" alt="" /></div></noscript> <!-- /Yandex.Metrika counter -->
          


          1. justboris
            24.03.2017 16:29

            Параметра site-info, который вы используете, там нет.
            Так что сломается, если он внезапно станет siteInfo, например.


            1. hcodes
              24.03.2017 16:47
              +1

              Для этого случая сделаю отдельную версию с использованием API.


      1. inoyakaigor
        27.03.2017 00:14

        Не хочу начинать этот холивар, но… IE8? Серьёзно?


        1. Kolyaj
          27.03.2017 09:42

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


  1. Kolyaj
    23.03.2017 09:54

    Денис, привет!

    Я пару лет назад даже сервис поверх api метрики писать начал http://jserrs.com, но как-то всё застопорилось :-)


    1. hcodes
      23.03.2017 10:25

      Привет! Посмотрел, понравилась идея пробрасывать версию js-кода сайта.


  1. Semigradsky
    23.03.2017 09:59

    К слову, может быть полезно также отслеживать не обработанные ошибки в промисах, с помощью события unhandledrejection. Работает правда тольо в Хроме и производных.


  1. sosnovskyas
    23.03.2017 10:00

    судя по всему закралась ошибка.

    var counterId = 12345, 
            siteInfo = {},
            pointer = siteInfo; // ТУТ ЗАПЯТАЯ
            path = [.....
    


    1. hcodes
      23.03.2017 10:00

      Спасибо, поправил.


  1. inoyakaigor
    27.03.2017 00:17

    А что насчёт производительности сайта? Две метрики, да и ещё перед всеми скриптами (т.е. многовероятно, что в самом начале страницы) мне кажется приведут к печальным результатам


    1. capfsb
      27.03.2017 09:24

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