company_banner

Как искать баги на фронтенде: 4 основных этапа


    В этой статье я рассмотрю вопрос выявления и обработки ошибок, возникающих на фронтенде (браузер или web-view).

    На фронтенде код JS выполняется в браузере. JavaScript не является компилируемым языком, поэтому всегда существует вероятность ошибки исполнения при непосредственном использовании программы. Ошибка исполнения блокирует код, расположенный после места ошибки, и пользователи программы рискуют остаться с нефункциональным экраном приложения, который можно будет лишь перезагрузить или закрыть. Но есть методы выявления ошибок и их безопасного сопровождения, позволяющие избежать таких ситуаций.

    1. Средства языка JavaScript


    Блоки try/catch


    Функции, которые могут выполниться с ошибкой, оборачивают в блоки try/catch. Сначала программа пытается выполнить код в блоке try. Если по каким-то причинам выполнение кода сломалось, программа переходит в блок catch, где доступны три параметра:

    • name — стандартизированное имя ошибки;
    • message — сообщение о деталях ошибки;
    • stack — текущий стек вызова, в котором произошла ошибка.

    То есть:

    try {
    
       callFunc();
    
    } catch (e) {
    
       console.log(e.name) // ReferenceError
    
       console.log(e.message) // callFunc is not defined
    
       console.log(e.stack) // ReferenceError: callFunc is not defined at window.onload
    
    }

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

    С помощью блока try/catch также можно вызывать собственные ошибки, например при проверке данных:

    const user = {
    
      name : «Mike»
    
    };
    
    try {
    
       if (!user.age) throw new SyntaxError(«User age is absent!»);
    
    } catch (e) {
    
       console.log(e.name) // SyntaxError
    
       console.log(e.message) // User age is absent!
    
       console.log(e.stack) // SyntaxError: User age is absent! at window.onload
    
    }

    Наконец, можно расширить эту инструкцию еще одним блоком — finally, который выполняется всегда: и в случае, если в try не было ошибки, и в случае, если управление перешло в блок catch:

    try {
    
       callFunc();
    
    } catch (e) {
    
       console.log(e)
    
    } finally {
    
       ...
    
    }

    Иногда используют инструкцию try/finally (без catch), чтобы была возможность продолжать использование кода без обработки конкретных ошибок.

    Событие window.onerror


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

    У глобального объекта window существует событие onerror (использовать его надо аккуратно: в разных браузерах реализация может отличаться!):

    window.onerror = function(message, url, line, col, error) {
    
       console.log(`${message}\n В ${line}:${col} на ${url}`);
    
    };

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

    Однако полная информация доступна только для скриптов, которые были загружены с того же домена. Если сломанный скрипт загружен с другого домена, window.onerror сработает, однако подробностей ошибки не будет.

    Компоненты фреймворков


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

    class ErrorBoundary extends React.Component {
    
       constructor(props) {
    
           super(props);
    
           this.state = { hasError: false };
    
       }
    
       static getDerivedStateFromError(error) {
    
           // Обновить состояние с тем, чтобы следующий рендер показал запасной UI.
    
           return { hasError: true };
    
       }
    
       componentDidCatch(error, errorInfo) {
    
           // Можно также сохранить информацию об ошибке в соответствующую службу журнала ошибок
    
           logErrorToMyService(error, errorInfo);
    
       }
    
       render() {
    
           if (this.state.hasError) {
    
               // Можно отрендерить запасной UI произвольного вида
    
               return <h1>Что-то пошло не так.</h1>;
    
           }
    
           return this.props.children;
    
       }
    
    }
    
    <ErrorBoundary>
    
       <MyWidget />
    
    </ErrorBoundary>

    Фактически, компонент React оборачивается специальным компонентом, который обрабатывает ошибки. Это напоминает оборачивание функций с помощью конструкции try/catch.

    2. Средства сборки проекта


    Современные JS-скрипты, как правило, делаются транспилируемыми. То есть разработка ведется с использованием последних стандартов ES. А затем код разработчика с помощью сборщика проектов (такого как Webpack) преобразуется в код, который будет гарантированно работать в выбранном числе браузеров.

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

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

    3. Тестирование


    Еще один способ не допустить ошибок в коде – тестировать его. Во фронтенде есть инструменты для эффективного использования юнит-тестов. Обычно используют фреймворки, такие как Jest, Karma, Mocha, Jasmine. Вместе с тестовыми фреймворками часто используют расширения, такие как Enzyme, React Testing Library, Sinon и другие, позволяющие обогатить тесты с помощью мокирования, функций-шпионов и других инструментов.

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

    const func = (data) => {
    
       return JSON.parse(data)
    
    }
    
    func('{«a»:1}')
    

    Однако он сломается, если подать ему неверное значение:

    func() // Uncaught SyntaxError: Unexpected token u in JSON at position 0.


    Этот код тоже проходит валидацию при сборке:

    const obj = {
    
       outer : {
    
           last : 9
    
       }
    
    }
    
    if (obj.outer.inner.last) {
    
       console.log(«SUCCESS»)
    
    }

    Однако он также сломается во время исполнения. После тестирования разработчик наверняка сделает дополнительные проверки:

    if (obj.outer?.inner?.last) {
    
       console.log(«SUCCESS»)
    
    }

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

    4. Логирование


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

    Смысл простой: на каждое событие window.onerror или в каждый переход исполнения кода в блок catch выполняется простой AJAX-запрос на специально выделенный адрес сервера, в тело которого кладется информация об ошибке. Далее потребуется инструмент, который быстро оповестит техподдержку и разработчиков о наличии новых ошибок и позволит эффективно работать с ними. Самый популярный из таких инструментов для фронтенда — Sentry.

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

    Подключать Sentry можно как непосредственно в HTML-файле, так и в компонентах, выполненных на одном из популярных фреймворков: React, Vue, Angular, Ember и других.

    Для подключения возможностей логирования прямо в браузере в секции загружаем скрипт:

    <script
    
       src=«https://browser.sentry-cdn.com/5.13.0/bundle.min.js»
    
       integrity=«sha384-ePH2Cp6F+/PJbfhDWeQuXujAbpil3zowccx6grtsxOals4qYqJzCjeIa7W2UqunJ»
    
       crossorigin="anonymous"></script>


    Далее в коде JS инициализируем:

    Sentry.init({
    
       dsn: 'https://<your account key here>@sentry.io/<your project id here>'
    
    });

    Всё. Если и когда в коде ниже этой строчки произойдет ошибка, Sentry выполнит ее логирование. Логи будут записаны даже тогда, когда ошибка произошла по вине скриптов с других доменов:



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

    Sentry.init({
    
       dsn: 'https://<your account key here>@sentry.io/<your project id here>',
    
       release: '2020.03.06.1'
    
    });

    С помощью Sentry в статистику можно передать контекст ошибки, например, информацию о клиенте, fingerprint, уровень ошибки (fatal, error, warning, info, debug), проставить теги.

    Есть возможность записывать в статистику пользовательские события. Например, можно поставить на отслеживание изменение размеров окна браузера или совершение AJAX-запроса. Также у Sentry есть собственный виджет с окном обратной связи, которое можно показать пользователю при ошибке. Это даст дополнительную информацию для расследования обстоятельств возникновения ошибки.

    Для развертывания Sentry вместе с фреймворками достаточно просто установить пакет и подключить:

    # Using yarn
    
    yarn add @sentry/browser
    
    # Using npm
    
    npm install @sentry/browser


    Делаем инициализацию в главном скрипте проекта (для React и Angular):

    import * as Sentry from «@sentry/browser»;
    
    Sentry.init({ dsn: 'https://<your account key here>@sentry.io/<your project id here>' });


    Для Vue и Ember передаем еще одну обязательную строку конфигурации:

    # Vue
    
    Sentry.init({
    
       dsn: '<your account key here>@sentry.io/<your project id here>',
    
       integrations: [new Integrations.Vue({Vue, attachProps: true})],
    
    });
    
    # Ember
    
    Sentry.init({
    
       dsn: '<your account key here>@sentry.io/<your project id here>',
    
       integrations: [new Integrations.Ember()]
    
    });

    Пакет integrations устанавливается отдельно:

    # Using yarn
    
    yarn add @sentry/integrations
    
    # Using npm
    
    npm install @sentry/integrations

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

    import { BrowserClient } from «@sentry/browser»;
    
    const client = new BrowserClient({
    
       dsn: '<your account key here>@sentry.io/<your project id here>',
    
    });
    
    client.captureException(new Error('example'));

    На сайте проекта есть подробная документация с примерами использования: https://docs.sentry.io.

    Статья подготовлена при поддержке облачной платформы Mail.ru Cloud Solutions.

    Что еще почитать по теме:

    1. Переиспользуемые компоненты React: как перестать писать одно и то же.
    2. Как избежать ошибок при разработке на React.
    3. Наш телеграм-канал с новостями о цифровой трансформации.
    Mail.ru Group
    Строим Интернет

    Комментарии 0

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

    Самое читаемое