habrahabr

Техподдержка: как научиться жить без Jira

  • вторник, 31 января 2023 г. в 00:42:08
https://habr.com/ru/company/arenadata/blog/712988/
  • Блог компании Arenadata
  • JavaScript
  • Help Desk Software
  • Service Desk


Привет! Меня зовут Савр, я работаю инженером технической поддержки Arenadata.

В прошлом году нам, как и многим другим компаниям, использовавшим зарубежное ПО, пришлось переходить на российские аналоги. В частности, с болью в сердце мы отказались от Jira Service Management (далее SM) — нашей системы управления обращениями заказчиков и основного инструмента службы поддержки. Мы были вынуждены перейти на российскую разработку SimpleOne.

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

Как мы выбирали замену

Изначально мы изучили несколько отечественных решений: SimpleOne, Итилиум, Naumen, Яндекс Трекер, Osticket, ЮзДеск, Okdesk, Kaiten. Выбирали по следующим критериям:

  1. Полностью российские разработчик и продукт.

  2. Наличие клиентского портала с возможностью заведения обращений, сохранения истории обращений, авторизации пользователей.

  3. Встроенная база знаний.

  4. Возможность регистрации по email.

  5. Наличие базовой автоматизации ITIL-процессов.

  6. Простота автоматизации (zero code / low code).

  7. Внутренний BI-модуль для отчётности.

  8. Модуль учёта трудозатрат.

  9. Возможность интеграции с внешними системами, API.

  10. Возможность миграции данных из Jira Service Management.

  11. Удобство интерфейса для пользователей.

  12. Потребление сервиса из облака на территории РФ.

  13. Стоимость, близкая к стоимости Jira Service Management.

Мы отобрали пять кандидатов и провели их оценку по разным параметрам*:

* Необходимо отметить, что оценка параметров была актуальна на март 2022-го и субъективна. Данная информация носит оценочный характер и демонстрирует исключительно наш подход к выбору решения.

В результате из двух кандидатов мы выбрали решение от SimpleOne. Связались с вендором и опробовали продукт в тестовом режиме. На наш взгляд, это позволяет получить хорошее представление об ограничениях и особенностях, примерно на 80–85%. Действительно, продемонстрированные возможности этого продукта во многом соответствовали требуемым, и мы приняли решение на нём остановиться.

Начав работать с SimpleOne, мы, однако, столкнулись с тем, что «из коробки» в этом продукте недоступны некоторые важные для нас функции. Мы не стали ждать, когда разработчик их реализует, платить за это подрядчику не хотелось, поэтому «засучили рукава» и сделали всё самостоятельно.

Что и как мы доработали

Оценка удовлетворённости

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

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

Благодаря гибкости SimpleOne удалось реализовать привычное решение с помощью изменения кода виджета оценки. Первым делом мы его стилизовали. С помощью HTML и CSS сверстали внешний вид шкалы, а JS-скрипты на клиенте и сервере переписали в нужных местах.

HTML

<div class="assessment__caller selection">
            <div class="selection__title text_h3">{data.translation.areYouSatisfiedServiceQuality}</div>
            <div class="selection__options">
                    <div event-click="s_widget_custom.setAssessment('caller',5)"
                        class="option__icon 5"></div>
                    <div event-click="s_widget_custom.setAssessment('caller',4)"
                        class="option__icon 4"></div>
                    <div event-click="s_widget_custom.setAssessment('caller',3)"
                        class="option__icon 3"></div>
                    <div event-click="s_widget_custom.setAssessment('caller',2)"
                        class="option__icon 2"> </div>
                    <div event-click="s_widget_custom.setAssessment('caller',1)"
                        class="option__icon 1"></div>
            </div>
        </div>

JS Client

s_widget_custom.setAssessment = function (type, level) {
        const levelsSatisfaction = document.querySelectorAll(`.assessment__${type} .option__icon`);
        levelsSatisfaction.forEach((el) => {
            if (el.classList.contains(`${level}`)) {
                const satisfactionValue = !el.classList.contains("option_picked") ? level : false;
                s_widget.setFieldValue(`${type}Satisfaction`, satisfactionValue);
                el.classList.toggle("option_picked");
            } else {
                el.classList.remove("option_picked");
            }
        });

И соответственно, со стороны сервера получаем данные и пишем в нужную колонку.

JS Server

if (taskRecord.getValue("caller") === ss.getUserID()) {
            data.response = true;
            data.taskState = taskRecord.state;
            // data.taskAgentSatisfaction = taskRecord.agent_satisfaction;
            // data.taskServiceSatisfaction = taskRecord.service_satisfaction;
            data.taskCallerSatisfaction = taskRecord.customer_satisfaction;
        } else {
            data.response = false;
        }

Теперь после принятия работ по заявке, клиент увидит такую форму:

Пока реализовывали это решение, в эксплуатации работал коробочный виджет от SimpleOne. Оценки выставлялись с помощью текста: Disappointed, Satisfied, Very Pleased. Чтобы не терять выставленные оценки, пришлось спроецировать трёхбалльную шкалу на пятибалльную. Предположили, что Disappointed соответствует оценке 1 из 3, а Very Pleased — 3 из 3. Соответственно, если у нас выставляется оценка Satisfied, то в новой шкале оценки это соответствует ~3,33. Округляем, конечно же, до целого числа. А исторические данные об оценках из Jira SM успешно залили в новую систему без трансформации и подготовки.

Интерфейс комментариев

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

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

Ещё одним недостатком оформления комментариев в SimpleOne была очень маленькая ширина поля для ввода текста: примерно четверть ширины экрана! Непонятно, зачем так было сделано, но это была прямо боль — листать записи шириной с листочек для заметок. Кстати, аналогичная проблема есть с отображением и на клиентском портале, её решение у нас в планах.

К счастью, обе проблемы удалось решить очень просто: с помощью CSS-правила мы добавили выделение жёлтым цветом для служебных комментариев, а ширину поля ввода увеличили до удобочитаемых 750 пикселей. С этого и начались наши доработки SimpleOne: мы начали активно изучать код программы и дорабатывать её под себя.  

Длительность и текущий статус инцидента

Длительность решения инцидента — не менее важный показатель, чем SLA, для технической поддержки. Поэтому я был удивлён, что «из коробки» SimpleOne не показывает такой полезный параметр, хотя для его расчёта есть абсолютно все данные. Вооружившись «костылями», я взялся за столь важную для команды задачу.

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

Решение в лоб: с помощью функциональности business rules записывать в таблицу такие параметры, как название статуса, время создания и время закрытия инцидента, а после записи вычислять длительность между датами создания и закрытия инцидента. Просто и сердито. Но мы ведь не ищем простых путей? И в процессе исследования родилась вторая реализация.

Оказалось, что есть системная таблица sys_history, которая фиксирует все изменения в инцидентах: от изменения исполнителя до смены статусов. Бинго! Но есть нюанс. Для отчётности нам необходима длительность, а не просто даты создания и закрытия инцидента. Поэтому необходимо вычислять разницу вручную и записывать в отдельное поле. Но из-за небольшого опыта работы с системой и осознания возможных последствий я не рискнул модифицировать системную таблицу. Возвращаться к первому варианту тоже не хотелось, зачем выполнять двойную работу, если эти данные уже собираются? Кроме того, я не мог гарантировать, что эти данные идентичны.

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

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

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

В результате получаем отчёт по статусам, который можно анализировать в различных разрезах.

Обозначения: 
Pending — задача находится в разработке Arenadata.
In progress — длительные исследования проблемы или поведения продуктов на стороне Arenadata.
Waiting for customer — ожидание обратной связи от заказчика.
Waiting for support — задача в работе у технической поддержки Arenadata.
Обозначения: Pending — задача находится в разработке Arenadata. In progress — длительные исследования проблемы или поведения продуктов на стороне Arenadata. Waiting for customer — ожидание обратной связи от заказчика. Waiting for support — задача в работе у технической поддержки Arenadata.
Скрипт сбора данных по времени активности по каждому статусу для всех тикетов
(function executeScheduleScript() {
  let history_record = new SimpleRecord('sys_history');
  let report_record = new SimpleRecord('itsm_arenadata_report_state_history');
  let task_record = new SimpleRecord('itsm_task');
  const nowDateTime = new SimpleDateTime();
  const lastLoadDateTime = new SimpleDateTime();
  lastLoadDateTime.addSeconds(-90000);
  history_record.addQuery('table_name', 'itsm_incident');
  history_record.addQuery('field_name', 'state');
  history_record.query();
  let num_inserted_recs = 0;

  // inserting tuples to report table
  while(history_record.next()) {
    task_record.get(history_record.getValue('record_id'));
    report_record.setValue('history_id', history_record.getValue('sys_id'));
    report_record.setValue('history_created_at', history_record.getValue('sys_created_at'));
    report_record.setValue('record_id', history_record.getValue('record_id'));
    report_record.setValue('task_display_number', task_record.getValue('number'));
    report_record.setValue('username', history_record.getValue('username'));
    report_record.setValue('company', task_record.getValue('company'));
    report_record.setValue('installation', task_record.getValue('installation'));
    report_record.setValue('urgency', task_record.getValue('urgency'));
    report_record.setValue('caller', task_record.getValue('caller'));
    report_record.setValue('customer_cluster', task_record.getValue('customer_cluster'));
    report_record.setValue('c_cluster', task_record.getValue('c_cluster'));
    report_record.setValue('old_value', history_record.getValue('old_value'));
    report_record.setValue('new_value', history_record.getValue('new_value'));
    report_record.setValue('subject', task_record.getValue('subject'));
    report_record.setValue('assignment_group', task_record.getValue('assignment_group'));
    report_record.setValue('assigned_user', task_record.getValue('assigned_user'));

    if (history_record.getValue('update_count') == 1) {
      report_record.setValue('state_update_count', 1);
    } else {
      report_record.setValue('state_update_count', 0);
    }

    let status = report_record.insert();

    if (status) {
      num_inserted_recs = num_inserted_recs + 1;
    }
  }
  ss.addInfoMessage('Inserted: ' + num_inserted_recs);
  ss.info('Inserted: ' + num_inserted_recs);

  //

  function onlyUnique(value, index, self) {
    return self.indexOf(value) === index;
  }

  let report_record_1 = new SimpleRecord('itsm_arenadata_report_state_history');
  let all_record_ids = [];
  let unique_record_ids = [];

  report_record_1.addQuery('state_update_count', 0);
  report_record_1.selectAttributes('record_id');
  report_record_1.query();
  while(report_record_1.next()) {
    all_record_ids.push(report_record_1.getValue('record_id'));
  }
  unique_record_ids = all_record_ids.filter(onlyUnique);

  unique_record_ids.forEach(item => {
    let max_state_update_count = 0;
    let report_record_2 = new SimpleRecord('itsm_arenadata_report_state_history');
    report_record_2.addQuery('record_id', String(item));
    report_record_2.addQuery('state_update_count', '!=', 0);
    report_record_2.selectAttributes('state_update_count');
    report_record_2.orderByDesc('state_update_count');
    report_record_2.setLimit(1);
    report_record_2.query();
    report_record_2.next();
    max_state_update_count = report_record_2.getValue('state_update_count');

    let report_record_3 = new SimpleRecord('itsm_arenadata_report_state_history');
    report_record_3.addQuery('record_id', String(item));
    report_record_3.addQuery('state_update_count', 0);
    report_record_3.orderBy('history_created_at');
    report_record_3.query();
    while(report_record_3.next()) {
      max_state_update_count = max_state_update_count + 1;
      report_record_3.setValue('state_update_count', max_state_update_count);
      report_record_3.update();
    }
  });

  //
  
  unique_record_ids.forEach(item => {
    let report_record_6 = new SimpleRecord('itsm_arenadata_report_state_history');
    report_record_6.addQuery('record_id', String(item));
    report_record_6.orderBy('state_update_count');
    report_record_6.addQuery('new_value', '!=', '5');
    report_record_6.query();
    let max_state_update_count_1 = report_record_6.getRowCount();
    while(report_record_6.next()) {
      let start = new SimpleDateTime(report_record_6.getValue('history_created_at'));
      let end = new SimpleDateTime();
      if (report_record_6.getValue('state_update_count') == max_state_update_count_1) {
        end.setValue(String(nowDateTime.getValue()));
      } else {
        let report_record_7 = new SimpleRecord('itsm_arenadata_report_state_history');
        report_record_7.selectAttributes(['record_id','state_update_count','history_created_at']);
        report_record_7.addQuery('record_id', String(item));
        report_record_7.addQuery('state_update_count', '=', report_record_6.getValue('state_update_count') + 1);
        report_record_7.setLimit(1);
        report_record_7.query();
        report_record_7.next();
        end.setValue(String(report_record_7.getValue('history_created_at')));
      }

      let duration = new SimpleDateTime().subtract(start, end);
      let so_duration = new SimpleDuration(duration.getDurationSeconds());
      so_duration = so_duration.getDurationSeconds() * 1000;
      report_record_6.setValue('duration', so_duration);
      report_record_6.update();
      ss.error(report_record_6.getErrors());
    }
  });

  //

  let report_record_8 = new SimpleRecord('itsm_arenadata_report_state_history');
  report_record_8.addQuery('duration', 'isempty');
  report_record_8.addQuery('new_value', '!=', '5');
  report_record_8.query();
  while(report_record_8.next()) {
    let start = new SimpleDateTime(report_record_8.getValue('history_created_at'));
    let end = new SimpleDateTime();
    end.setValue(String(nowDateTime.getValue()));
    let duration = new SimpleDateTime().subtract(start, end);
    let so_duration = new SimpleDuration(duration.getDurationSeconds());
    so_duration = so_duration.getDurationSeconds() * 1000;
    report_record_8.setValue('duration', so_duration);
    report_record_8.update();
  }
  //

  ss.addInfoMessage('Load completed: itsm_arenadata_report_state_history');
  ss.info('Load completed: itsm_arenadata_report_state_history' + '; Start time: ' + nowDateTime.getValue() + '; End time: ' + new SimpleDateTime().getValue());
})()

Дизайн уведомлений

Когда мы работали в Jira SM, то использовали бота для рассылки уведомлений в Slack. Они были красиво оформлены, сразу был понятен приоритет, данные были визуально разделены. В SimpleOne нас встретили уведомления в виде простого текста, без какого-либо форматирования. Это оказалось очень плохо, потому что дежурным инженерам первой линии поддержки приходит довольно много уведомлений, и среди потока невыразительных текстовых сообщений очень легко пропустить важные и срочные.

Мы исправили этот недостаток с помощью Markdown-разметки, которую поддерживает наш текущий корпоративный мессенджер Mattermost. Для этого мы с помощью веб-хуков формируем JSON-сообщения, которые отправляются прямиком в Mattermost, а там уже уведомлениям придаётся желаемый внешний вид.

У нас есть четыре категории приоритета, от low до emergency. Мы решили визуально выделять их с помощью эмодзи. Так как это делается в скрипте, мы смогли добавлять в текст уведомлений дополнительные данные. Мы собираем их из таблиц SimpleOne и отправляем в Mattermost:

  • компанию-заказчика, разместившую заявку,

  • обратившегося пользователя,

  • ответственного инженера.

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

Автоматическое заведение пользователей

В самом начале перехода на SimpleOne нам потребовалось добавлять на портал пользователей от наших заказчиков. На первых этапах делали всё вручную: запрашивали электронные почты и вносили их в базу. Это занимало много времени. Но поскольку это регулярный и однотипный процесс, мы его решили автоматизировать. Добавили на портал форму, через которую заказчики могут передать имя, номер телефона и электронный адрес пользователя. Как только запись появляется в таблице, формируется внутренняя задача на проверку информации. Этим у нас занимается первая линия поддержки. И как только ответственный сотрудник всё проверит и нажмёт кнопу «Одобрить», данные улетают в скрипт создания пользователя, а тому на почту уходит пароль.

Заключение

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

Для успешной миграции данных также необходим достаточно длительный доступ к старой системе. Мы столкнулись с тем, что стандартная резервная копия Jira SM не содержала всех данных. Также некоторые соответствия необходимо будет проверять на уровне сравнения отдельных обращений.

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

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

Что мы планируем сделать в будущем:

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

  2. Реализовать механизм follow для портала.

  3. Автоматизировать некоторые внутренние процессы.

  4. Мы вошли во вкус, и бэклог доработок и исправлений постоянно расширяется, на текущий момент у нас уже более 40 задач.

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Какую систему управления обращениями заказчиков вы используете?
38.46% Jira Service Management 5
23.08% SimpleOne 3
0% Итилиум 0
0% Naumen 0
30.77% Яндекс.Трекер 4
0% Osticket 0
0% ЮзДеск 0
0% Okdesk 0
0% Kaiten 0
30.77% Свой вариант (если несложно, напишите в комментариях) 4
Проголосовали 13 пользователей. Воздержались 8 пользователей.
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Какие механизмы оценки удовлетворённости заказчиков вы используете?
0% Опрос с помощью обзвона 0
0% Тайный покупатель 0
100% Периодические опросники с запросом по почте 3
66.67% Оценка качества решения конкретного обращения в системе исполнителя 2
0% Свой вариант (если несложно, напишите в комментариях) 0
Проголосовали 3 пользователя. Воздержались 8 пользователей.
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Участвуете ли вы сами в оценках качества работы исполнителей по заказанным услугам?
14.29% Всегда 1
71.43% Иногда 5
14.29% Не участвую (если несложно, напишите в комментариях, почему) 1
Проголосовали 7 пользователей. Воздержались 4 пользователя.