Бизнес-метрики в Sentry или как сделать велосипед из самоката

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Привет! Меня зовут Врублевский Артур, и я занимаюсь frontend-разработкой на Angular в Страховом Доме ВСК. Так сложилось, что ранее, до работы в ВСК, я уже сталкивался с инструментом Sentry – занимался его настройкой на отлов ошибок. Наверно именно поэтому я был привлечен к неожиданным для меня работам, которые так же касались Sentry, его использования в необычной плоскости. Об этом и пойдет далее рассказ в 6 актах.

Стадия 1 — «Отрицание»

Думаю, многим Sentry известен как инструмент, позволяющий аккумулировать ошибки, отслеживать метрики производительности, делать тревожные оповещения о состоянии ваших приложений. И когда мне принесли задачу, звучащую как «Нам нужны продвинутые бизнес-метрики, можем ли мы это сделать в Sentry?», я сразу начал отрицать возможность такой затеи. Это казалось противоестественным действием, хотелось отправить постановщика в Яндекс Метрику, которая изначально была создана для подобных целей. Но со слов постановщика, решение Яндекса все же не давало необходимых показателей в нужном виде, нужно было больше гибкости.

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

«Нам нужны продвинутые бизнес-метрики, можем ли мы это сделать в Sentry?»

«Нам нужны продвинутые бизнес-метрики, можем ли мы это сделать в Sentry?»
«Нам нужны продвинутые бизнес-метрики, можем ли мы это сделать в Sentry?»

 

Стадия 2 – «Гнев»

Если звезды зажигают – значит – это кому-нибудь нужно? С такой мыслью я отправился на просторы интернета искать других энтузиастов, которым в голову могла прийти схожая мысль по использованию Sentry. Но, к сожалению, ни на русском, ни на английском языках ничего подобного найдено не было. В документации к самому Sentry тоже было сложно найти что-либо подходящее под поставленные цели, максимум инструментов для того, что заявлено из коробки, и ноль – для нужд бизнес-метрик (как мне казалось тогда).

Стадия 3 – «Торг»

При изучении документации Sentry я наткнулся на метод Sentry.captureMessage(), позволяющий отправлять в Sentry кастомные сообщения/события. К любому такому сообщению, как и к ошибке, можно добавить контекста в виде тегов Sentry.setTags(), а также информацию о пользователе при помощи Sentry.setUser().

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

1.      Issues. Данный раздел в Sentry хранит в себе ошибки и запись кастомных бизнес-метрик в это место приводит к тому, что среди реальных ошибок проекта появляются «мусорные» записи, которые не относятся к Issues, что мешает с ними работать.

2.      Время выполнения. Одной из второстепенных целей бизнес-метрики было измерение длительности события. Большинство событий происходит не мгновенно и имеет время выполнения. При использовании Sentry.captureMessage() приходилось вручную замерять время начала события и время окончания, что не очень удобно, а также ведет к усложнению настройки виджетов дашбордов. Почему к усложнению? Свойство будет кастомным, его придется добавлять к событию как тег.

3.      Статус события. Приходилось передавать отдельным тегом для возможности фильтровать события по статусу и настраивать виджеты дашбордов.

 

Стадия 4 – «Депрессия»

Радость от того, что удалось выполнить задачу по бизнес-метрикам в Sentry со стороны кода сменилась грустью от того, что виджеты при таком подходе настраиваются весьма плохо из-за их сильной зависимости от стоковых полей Sentry. А в случае с Sentry.captureMessage() большинство параметров кастомные, поэтому возможности настройки виджетов ограничены.

На момент демонстрации функционала заказчику другого выхода, кроме как реализации через Sentry.captureMessage(), я не видел. Поэтому уже начал мириться с такой реализацией.

Спасло ситуацию подключение к демонстрации сотрудника из SystemTeam (DevOps), который предложил реализовать этот функционал при помощи Sentry.transaction(), а не Sentry.captureMessage(). Да, предстояло все переписать заново, но оно того стоило!

 

Стадия 5 – «Принятие»

Итак, был создан сервис, который несет в себе следующие возможности:

1.      Свойство events. Сюда мы будем класть все новые события при их старте и удалять при окончании.

events: { [name: string]: { transaction: Sentry.Transaction } } = {};

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

private deadlineSubscriptions: { [name: string]: Subscription } = {};

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

/**
 * Запускает событие
 * @param {ClientPathEventName} name - название события
 */
startEvent(name: ClientPathEventName) {
  // Если событие уже запущено, не пытаемся запустить его снова.
  if (this.events[name]) {
    return;
  }
  // Запускаем событие в Sentry.
  // В передаваемом объекте name это название транзакции, а op - тип.
  // Тип можно задавать произвольной строкой.
  const transaction = Sentry.startTransaction(
    {
      name: name,
      op: 'clientPath',
    });
  // Помещаем созданную транзакцию в свойство events (пункт 1).
  this.events[name] = { transaction };

  // Запускаем таймер, чтоб завершать "зависшие" транзакции.
  this.deadlineSubscriptions[name] = timer(TRANSACTION_EXPIRATION_TIME).subscribe(() => {
    // За время TRANSACTION_EXPIRATION_TIME событие могло уже завершиться, поэтому прежде, чем завершить его
    // проверяем, что оно ещё запущено.
    if (this.events[name]) {
      this.endEvent(name, ClientPathEventStatus.DeadlineExceeded);
    }
  });
}

4.      Метод окончания события. Специфика Sentry.transaction() такова, что любая транзакция должна быть завершена со стороны кода, иначе такая транзакция не будет отображена в Sentry. Нам же завершение транзакций нужно для установки конечной точки события: будь то успешная загрузка всех данных пользователя, или произошедшая при этом ошибка. Именно тут, при завершении события, мы определим, было оно успешным, или возникли какие-то проблемы при попытке пользователя пройти определенный клиентский путь.

/**
 * Завершает событие
 * @param {ClientPathEventName} name - название события
 * @param {ClientPathEventStatus} status - статус события
 * @param {{ [key: string]: Primitive }} [additionalData = {}] - дополнительные данные по событию
 */
endEvent(name: ClientPathEventName, status: ClientPathEventStatus, additionalData: { [key: string]: Primitive } = {} ) {
  // Если события уже не существует, не пытаемся его завершить.
  if (!this.events[name]) {
    return;
  }
  // Отменяем подписку по таймауту если событие завершилось раньше.
  // Иначе может возникнуть ситуация, когда таймер на событие с именем foo уже был запущен ранее,
  // но событие foo за это время успело успешно завершиться и начаться заново. В таком случае возникали
  // ложные окончания событий с причиной DeadlineExceeded
  if (status !== ClientPathEventStatus.DeadlineExceeded) {
    this.deadlineSubscriptions[name]?.unsubscribe();
  }
  // Устанавливаем транзакции статус в соответствии с переданным.
  this.events[name].transaction.setStatus(status);
  // Тут же можем дополнить в транзакцию тегов, с помощью которых потом сможем искать нужные события
  // через discover или при настройке виджетов дашбордов.
  this.events[name].transaction.setTag('product', this.fifthElementService.schemaCode);
  this.events[name].transaction.setTag('applicationId', this.fifthElementService.applicationId);
  // Мы так же можем записать какую-либо дополнительную информацию в событие.
  // Например, если событие подразумевает запрос, который завершился с ошибкой,
  // данные об ошибке мы можем поместить в поле data транзакции 
  const metaData = Object.entries(additionalData);
  if (metaData?.length) {
    metaData.forEach((data) => this.events[name].transaction.setData(...data));
  }
  // Завершаем транзакцию Sentry.
  this.events[name].transaction.finish();
  // Удаляем событие из нашего свойства events.
  delete this.events[name];
}

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

/**
 * Создаёт мгновенное событие, которое начинается и сразу заканчивается
 * @param {ClientPathEventName} name - название события
 * @param {{ [key: string]: Primitive }} [additionalData = {}] - дополнительные данные по событию
 */
instantEvent(name: ClientPathEventName, additionalData: { [key: string]: Primitive } = {}) {
  // Начинаем событие.
  this.startEvent(name);
  // Сразу его завершаем.
  this.endEvent(name, ClientPathEventStatus.Ok, additionalData);
  // Подключаем sideEffects при необходимости.
  this.sideEffects(name);
}

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

/**
 * Применение сторонних эффектов
 * @param {ClientPathEventName} name - название события
 */
sideEffects(name: ClientPathEventName) {
  switch (name) {
    case ClientPathEventName.SomeEvent:
      // делаем что-то…
      break;
    default:
      break;
  }
}

В методах используется два кастомных типа данных:

1.      ClientPathEventName – перечисление строковых названий событий. Называть события можно как угодно. Это пригодится для «отсеивания» событий по названию в discover.

2.      ClientPathEventStatus – перечисление строковых статусов транзакций. В качестве статусов можно использовать только определенные значение, которые можно отправить в Sentry, назвать статус события по-своему не получится. Есть следующие статусы:

  • ок – для случаев успешного завершения события;

  • permission_denied – если событие завершилось из-за недостатка разрешений/прав на событие;

  • cancelled – у нас это используется в случаях, когда событие было завершено из-за действий пользователя.

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

  • unknown – на случай, когда произошло что-то такое, чего не должно было произойти, но оно произошло.

  • deadline_exceeded – если мы не дождались завершения события по вышеперечисленным причинам и завершили его из кода.

 

А как же этим всем пользоваться в коде? Например, нам нужно получить данные пользователя при нажатии на кнопку. После нажатия выполняется запрос на сервер и данные выводятся на страницу.

1.      Начало события. В нужном месте вызываем метод startEvent сервиса событий.

/**
 * Обработчик кнопки загрузки данных пользователя
 */
onClick() {
  this.clientPathService.startEvent(ClientPathEventName.GetUserData);
  this.showLoader = true;
  this.getUserData();
}

2.      Далее, в месте, где мы знаем, что получим ответ от сервера и выведем данные на страницу, закладываем завершение события.

/**
 * Метод загрузки данных пользователя
 */
getUserData() {
  this.httpService.get('/api/userData').subscribe(
    (userData: UserData) => {
      this.setUserData(userData);
      this.showLoader = false;
      // Если запрос выполнен успешно, данные добавлены на страницу и отключен лоадер,
      // значит можно завершать событие со статусом ок.
      this.clientPathService.endEvent(ClientPathEventName.GetUserData, ClientPathEventStatus.Ok);
    },
    (error) => {
      this.showLoader = false;
      this.alertService.showAlert(error);
      // При запросе данных пользователя возникла ошибка, соответственно завершаем событие со статусом
      // internal_error и так же добавляем в событие подробности о том, что произошло, передавая
      // аргументом additionalData данные об ошибке.
      this.clientPathService.endEvent(
        ClientPathEventName.GetUserData,
        ClientPathEventStatus.InternalError,
        { error: error.message },
      );
    },
  );
}

По итогу в Sentry будут отправлены данные, говорящие нам о том, успешно ли пользователь получил данные или что-то пошло не так.

Стадия 6 – «Профит»

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

Вот некоторые примеры виджетов, которые были придуманы нами для мониторинга продукта:

1.  Количество неуспешных событий по определенным страницам (продуктам). Для данного отображения была выбрана диаграмма. Ось y строится по количеству событий.

Самое важное тут, это настройка фильтрации, чтоб на графике были только те события, что нам нужны. Берем только транзакции, принадлежащие к кастомному клиентскому пути (transaction.op:clientPath) у которых статус не равен «ok» или «cancelled».

Далее группировка результатов. Эта настройка нужна для группировки каждого отдельного пика на графике. Например, по продукту  Mortgage, событие PriceCalculation завершившееся со статусом internal_error будет отображаться на графике как отдельный сгруппированный по данным признакам пик.

2.      Детализация соотношения неуспешных событий и успешных по конкретному продукту. В данном примере был выбран вариант отображения таблица.

Далее настроены колонки и фильтры. В данном случае в качестве первой колонки используется название транзакции (события), во вторую идет общее количество событий, далее кол-во событий со статусом «ok», кол-во событий со статусом не равным «ok» и в конце идет колонка-уравнение, считается кол-во неуспешных событий в процентном соотношении.

Помимо этого, фильтрами тут отсекаются события со статусом «cancelled», так как это отмененные пользователем события и неверно было бы считать их ошибочными или успешными, поэтому данные события исключены из статистики.

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

 

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

4.      Список пользователей, у которых возникли проблемы. В данном виджете внешний вид так же настроен как таблица. Главная задача данного виджета – показать нам пострадавших пользователей, которые столкнулись с ошибками в определенном событии. Например, очень важно, чтоб пользователи могли без проблем перейти на страницу оплаты. Соответственно настраиваем колонки на идентификатор пользователя, статус события (была эта ошибка запроса или событие завершилось по достижению дедлайна).

Если в вашем Sentry будет отправляться иная информация (например, номер телефона или email), то это можно будет так же вывести сюда. Далее фильтром отсекаем нужное событие и статусы события и наша таблица готова.

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

Пара заключительных выводов

Итак, что же стало итогом проделанной работы? Мы получили систему, которая позволяет:

1.      отслеживать путь пользователя в процессе оформлению продукта;

2.      видеть весь необходимые данные на определенном шаге пользователя;

3.      выявлять места, которые мешают пользователю пройти пользовательский путь, ведут к потере клиента;

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

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

6.      собирать контакты пользователей (при наличии на определенном этапе клиентского пути), которые не прошли путь до финальной точки (будь то покупка или любой другой показатель успешного завершения клиентского пути) не по техническим причинам, узнавать у них причины «отказа» завершения клиентского пути.

Поделитесь своим мнением о статье в комментариях! А также своим опытом, приходилось ли вам работать с Sentry в нестандартной форме, настраивать бизнес метрики не в привычных системах вроде Яндекс метрики или Google аналитики (или даже с ними), развивать решения после результатов отслеживания таких метрик?


Чтобы оставаться в курсе всех IT-новостей Страхового Дома ВСК, подписывайся на наши социальные сети:

 

Сообщество VK: IT-ВСК

Блог на Хабр: Страховой Дом ВСК

YouTube-канал: ВСК Страховой Дом

Источник: https://habr.com/ru/companies/vsk_insurance/articles/798493/


Интересные статьи

Интересные статьи

Разбираемся как вкатиться в Unity со знаниями C# на примере простой игры - 3D-раннер с препятствиями и сбором красных мячиков.
Вы изучаете данные и хотите поделиться своим кодом Python с другими, не раскрывая исходный код и не требуя от них установки Python и других компонентов? Если да, то вас может быть интересна конвертаци...
В первой части статьи мы последовательно рассмотрели шаги по созданию и преобразования приложения для Android, необходимыми для реализации тестов, начиная от Unit-тестирования и заканчивая E2E-тестами...
Техническое развитие не стоит на месте и новые версии оборудования приходят на смену старым версиям, а старые версии снимаются с производства. В 2021 году пришла очередь контроллера LOGO! версии 6, вы...
Хотите узнать мнение организаторов «Цифрового прорыва» о том, как прошел конкурс? В этом посте не будет ничего про масштабы, книгу рекордов, первых лиц, неповторимые решения и безупречную организ...