Привет! Меня зовут Савр, я работаю инженером технической поддержки Arenadata.
В прошлом году нам, как и многим другим компаниям, использовавшим зарубежное ПО, пришлось переходить на российские аналоги. В частности, с болью в сердце мы отказались от Jira Service Management (далее SM) — нашей системы управления обращениями заказчиков и основного инструмента службы поддержки. Мы были вынуждены перейти на российскую разработку SimpleOne.
Поскольку наша команда привыкла к предыдущей функциональности, после миграции мы сделали ряд доработок нового сервиса. В этой статье я расскажу о некоторых из них: почему мы решили это исправить и как именно реализовали. Сразу оговорюсь, что мы не претендуем на статус великих специалистов или консультантов по SimpleOne, а лишь хотим поделиться своим опытом и идеями с теми, кто тоже рассматривает этот инструмент как альтернативу существующему решению.
Как мы выбирали замену
Изначально мы изучили несколько отечественных решений: SimpleOne, Итилиум, Naumen, Яндекс Трекер, Osticket, ЮзДеск, Okdesk, Kaiten. Выбирали по следующим критериям:
Полностью российские разработчик и продукт.
Наличие клиентского портала с возможностью заведения обращений, сохранения истории обращений, авторизации пользователей.
Встроенная база знаний.
Возможность регистрации по email.
Наличие базовой автоматизации ITIL-процессов.
Простота автоматизации (zero code / low code).
Внутренний BI-модуль для отчётности.
Модуль учёта трудозатрат.
Возможность интеграции с внешними системами, API.
Возможность миграции данных из Jira Service Management.
Удобство интерфейса для пользователей.
Потребление сервиса из облака на территории РФ.
Стоимость, близкая к стоимости 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 об изменениях статусов инцидентов и считали длительность каждого статуса. А для текущего статуса, который ещё не успел смениться, мы считали длительность как разницу между временем перехода в статус и текущим временем. Таким образом мы получали актуальную хронологию жизни каждого инцидента и могли отслеживать те, которым нужно уделить больше остальных ради удовлетворения потребностей заказчиков.
Реализация этой функциональности заняла у нас около недели чистого времени, включая корректирование требований к отчёту в процессе работы и исправление ошибочного поведения. В итоге мы получили такую таблицу:
В результате получаем отчёт по статусам, который можно анализировать в различных разрезах.
Скрипт сбора данных по времени активности по каждому статусу для всех тикетов
(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 не содержала всех данных. Также некоторые соответствия необходимо будет проверять на уровне сравнения отдельных обращений.
На сегодняшний день разрыв между функциональностью устоявшегося продукта и аналогов достаточно велик для того, чтобы возникала потребность в доработке. Именно в доработке, а не настройке. С другой стороны, альтернативные решения позволяют использовать их в качестве промышленных, без потери ключевых функций.
Процесс доработки (добавления новой функциональности) вышел за рамки проекта внедрения и перешёл в режим постоянного. Для обеспечения такой разработки необходимы выделенные люди, цели, план и подход к управлению.
Что мы планируем сделать в будущем:
Изменить внешний вид портала для пользователей. Сейчас существует набор проблем с отображением, снижающий удобство пользования.
Реализовать механизм follow для портала.
Автоматизировать некоторые внутренние процессы.
Мы вошли во вкус, и бэклог доработок и исправлений постоянно расширяется, на текущий момент у нас уже более 40 задач.