Меня зовут Станислав Решетнев, я руковожу отделом разработки в компании Sape по направлению Link Building (инструменты для продвижения в поисковых системах). В моем ведении находятся команды продуктовой разработки и команда внутренней Платформы. Чтобы лучше понимать эффективность сотрудников и видеть проблемные места, я создал инфополе с метриками продуктивности команд.
В этой статье хочется передать тем руководителям и командам, которые растут и оцифровывают свои достижения, наши наработки. Построить подобное инфополе можно достаточно быстро и легко с помощью стандартных инструментов — Asana и Jira.
Суть наработки
В нашей компании мы применяем Asana, чтобы планировать работу всех сотрудников в масштабах месяцев. В Asana можно быстро создавать и редактировать задачи, выстраивать взаимосвязи между отделами, добавлять произвольные поля (например, количество часов разработки). Еще Asana позволяет смотреть на проекты в разных разрезах: видеть как списки задач, так и временную шкалу выполнения, график занятости ресурсов. Разработчики же более привычны к Jira — она больше адаптирована под их работу. Таким образом задачи с уровня Asana в какой-то момент превращаются в технические задачи в Jira. Поэтому планировать проекты в целом и ставить планы на спринты мы будем в Asana, а часть задач, которые уходят функциональным разработчикам, будут слинкованы с задачами в Jira.
Точкой входа для управления процессом разработки у нас будет Asana-проект «Цели команд: планирование и контроль». Здесь есть секция «Спринты», где границы и названия спринтов определены в виде задач. Завершенные спринты представлены как завершенные задачи:

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

Будем считать, что задачи в пределах спринта — это цели команды. Поскольку мы придерживаемся методологии Scrum, цели должны выполняться в пределах спринта и не пересекать его границ. Значит, если задача пересекла границу (пришлось удлинить срок выполнения), то цель не достигнута. Также мы можем на встрече по планированию спринта отменять цели-задачи, которые не удалось выполнить, но они будут перепланированы.
Техстек для системы построения метрик
Теперь определимся с техстеком для системы построения метрик. Мы остановимся на наиболее простых и доступных технологиях. Метрики выполнения задач будем собирать в таблицах Google Spreadsheets. Связующим звеном между Asana, Jira и таблицами, а также агрегатором метрик, выступит n8n. Он поддерживает множество интеграционных нод и позволяет выстраивать из них рабочие процессы практически без написания кода. Благодаря низкому порогу входа, рабочие процессы в n8n хорошо понятны как разработчикам, так и менеджерам.
Создадим новую таблицу. Для каждого вида метрик заведем отдельную вкладку. В каждой вкладке мы заранее настроим шапку таблицы, а рабочий процесс n8n по каждой из метрик будет ее регулярно обновлять:

Продуктовые метрики
Теперь разберем построение всех основных продуктовых метрик. Они непосредственно влияют на время разработки, качество продукта и опыт пользователя.
Скорость разработки. Результативность спринтов
Первой возьмем метрику «Скорость разработки. Результативность спринтов». Как бизнесу, так и разработчикам важно видеть, какую часть целей по продуктовому производству достигают команды. Необходимо отслеживать и число внеплановых целей. Они возникают, если команда понимает, что для достижения результата нужно сделать какие-то дополнительные действия. Например, изначально не учли технические нюансы интеграции по API или для раскатки новой фичи необходимо обновить лендинг. В нашей упрощенной схеме мы не увеличиваем оценку в Story Points, а отслеживаем только цели в виде дополнительно появляющихся задач.
Краткий алгоритм сбора метрик такой:
Очищаем уже заполненные данные в таблице.
Получаем список спринтов из Asana (скажем, последние 10) и проходим в цикле по каждому из спринтов.
Получаем список команд разработки и проходим по ним.
Получаем задачи спринта для команды.
Фильтруем организационные задачи.
Записываем статистику в таблицу.
Результат будет выглядеть примерно так:

Благодаря цветовой окраске ячеек по значениям, можно подсветить, где, например, выполнимость целей у команды оказалась не такой высокой, как у коллег.
Мы используем подобную таблицу на ретроспективе по каждой из команд, в таком формате:
Успешный спринт. В прошедшем спринте наша команда достигла всех целей, молодцы! Спринт был достаточно предсказуемым, внеплановых целей не появилось — все учли.
Не совсем успешный спринт. Мы выполнили почти все цели, процент выполнения — 90%. Не выполнили одну цель. Смотрим, что за цель. Предположим, причина в инфраструктурной проблеме — аналитик данных не смог составить отчет в BI из-за сбоя во внешней системе. Форс-мажорная ситуация, обойти ее не могли.
Настройка процесса в n8n
Обсудим техническую сторону настройки рабочего процесса в n8n. Нам понадобится триггер с регулярным выполнением:

Очистка данных в таблице потребует настройки credentials в Google Cloud Console (нужно будет создать там проект и разрешить Google Sheets API):

Похожим образом получим список спринтов из секции со спринтами в Asana:

Кстати, ID секции, как и прочие параметры, можно получить по ID задачи, создав временный тестовый узел (ID секции тут будет в memberships.section.gid):

На мой взгляд, для записи в документ удобнее всего использовать операцию “Append Row”, поскольку она не привязана к фиксированному числу строк. Это позволит записывать в таблицу отчет произвольного размера. Но именно это заставило нас выводить метрики в различных вкладках, поскольку, получается, у каждой метрики может быть произвольное число строк и мы не знаем, где границы по таблице той или иной метрики.
На скриншоте мы выбрали в “Values to Send” связь значения с названием столбца («спринт»). В Options указали, что заголовок таблицы находится в строке 3 (отсчет ведется с первой строки):

Для хранения меняющихся данных удобно использовать функционал в n8n Data Tables. В них можно хранить список команд и соответствующие им ID секции в Asana.
Часть задач из секций команд мы не хотим учитывать в статистике. Это могут быть организационные задачи или вехи. Их мы можем отфильтровать:

Чтобы подготовить статистику по каждой из задач, делаем следующее:
Сначала при помощи узла типа “Edit Fields” добавляем поля с признаками того, как задача вписывается в спринт (начата до спринта, заканчивается в спринте, заканчивается после окончания спринта):

2. Затем узлом такого же типа добавляем к задаче признаки достижения целей (цель достигнута, цель не достигнута, цель добавлена вне плана):

3. И, наконец, суммируем итог команды по спринту при помощи узла типа Summarize:

Теперь остается только записать вычисленную статистику:

Важный технический нюанс. В API Spreadsheets есть ограничение на число обращений в секунду (WPS), поэтому полезно использовать задержку в виде узла типа Wait:

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

Столбцы означают:
Создано и подтверждено — количество не отмененных на момент формирования отчета критичных багов.
Потрачено времени на решение, часов — зафиксированное время на решение, в часах.
Задачи — список тикетов в Jira.
В таком виде по метрикам мы можем индикативно понять, как проходили спринты, с каким числом критичных проблем мы столкнулись. Более подробный разбор по фиксации багов мы увидим в метрике MTTR, описанной ниже.
Здесь источником сведений будет в основном Jira. Данные мы будем получать непосредственно из БД Jira, в обход API, для большей гибкости.
Первая часть рабочего процесса останется прежней. Мы также перебираем спринты из Asana, но внутри цикла будем получать выборку из БД Jira:

Запрос в Jira мы делаем при помощи ноды типа Mysql, операция “Execute MySQL” с запросом вида:
SELECT
ticket.ID,
CONCAT(p.pkey, '-', ticket.issuenum) AS ticket_code,
pr.pname AS ticket_status,
u.lower_user_name AS ticket_assignee,
IF(na_be.SOURCE_NODE_ID IS NOT NULL, 'BE', IF(na_fe.SOURCE_NODE_ID IS NOT NULL, 'FE', '-')) AS ticket_be_fe,
ticket.CREATED AS ticket_created_at,
cg.CREATED AS first_in_progress_at,
ticket.RESOLUTIONDATE AS ticket_resolved_at,
TIMESTAMPDIFF(MINUTE, ticket.CREATED, ticket.RESOLUTIONDATE) AS total_execution_time_in_minutes,
TIMESTAMPDIFF(MINUTE, ticket.CREATED, cg.CREATED) AS time_before_in_progress_in_minutes,
TIMESTAMPDIFF(MINUTE, cg.CREATED, ticket.RESOLUTIONDATE) AS execution_time_after_in_progress_in_minutes,
ROUND(ticket.TIMESPENT / 60) AS time_spent_in_minutes
FROM jiraissue ticket
INNER JOIN project p ON p.ID = ticket.PROJECT
INNER JOIN changegroup cg ON cg.issueid = ticket.ID
INNER JOIN changeitem ci ON cg.ID = ci.groupid AND ci.FIELD = 'status'
INNER JOIN priority pr ON pr.ID = ticket.PRIORITY
INNER JOIN app_user u ON u.user_key = ticket.ASSIGNEE
LEFT JOIN nodeassociation na_fe ON na_fe.SOURCE_NODE_ID = ticket.ID AND na_fe.SINK_NODE_ENTITY = 'Component' AND na_fe.SINK_NODE_ID IN (100, 200) /* component.cname = 'Frontend' */
LEFT JOIN nodeassociation na_be ON na_be.SOURCE_NODE_ID = ticket.ID AND na_be.SINK_NODE_ENTITY = 'Component' AND na_be.SINK_NODE_ID IN (300, 400) /* component.cname = 'Backend' */
WHERE
ticket.issuetype = 10000 /* Bug - issuetype */
AND ticket.PRIORITY IN (10001 /* Urgent */, 10002 /* Immediate */)
AND ticket.CREATED >= '{{ $('Loop Over Sprints').item.json.start_on }} 00:00:00'
AND ticket.CREATED <= '{{ $('Loop Over Sprints').item.json.due_on }} 23:59:59'
AND p.pkey IN ('PROJECT1', ‘PROJECT2’)
AND ticket.issuestatus IN (
5 /* Resolved */,
6 /* Closed */
)
GROUP BY ticket.ID
ORDER BY ticket.ID DESC
;
То есть, мы выбираем баг-тикеты в пределах спринта в высоких приоритетах, принадлежащие Jira-проектам (обычно в Jira-проект выделяется конкретный продукт). К ним добавляем первый переход статуса тикета, взятый из таблиц changegroup и changeitem. MySQL в случае группировки по ID тикета выберет первую запись из связанной таблицы. Этот переход будет означать статус “In Progress”, так как это первый возможный переход из статуса “Backlog”.
Кроме того, нас интересует исполнитель по тикету и маркировка Frontend / Backend. Их мы определяем по соответствующим компонентам тикета.
Время восстановления после инцидентов
Теперь мы знаем, сколько критичных багов возникает. Но остается вопрос — насколько эффективно они решаются? Найти ответ поможет метрика «время восстановления после инцидентов» (Mean Time To Recovery, MTTR).
Впрочем, мы хотим видеть и сроки по конкретным этапам решения багов, чтобы обнаружить проблемные места:
Общее время решения, в минутах. То самое время, которое заложено в SLA. Сколько минут было истрачено с момента постановки задачи до ее решения.
Время до первого взятия в работу, в минутах. Сколько задача ждала до того, как над ней начали работать. Метрика помогает оценить, как быстро реагирует разработчик и насколько эффективна система уведомления о критичных проблемах системы.
Время решения после взятия в работу и до закрытия, в минутах. Остальная часть работы по багу (фидбеки, ревью, выкат и т. п.) Можно провести декомпозицию выполнения и по этой части, если время решения оказывается слишком большим.
Залогировано времени на решение, минут. Срок выполнения не связан напрямую с залогированным временем, так как есть время ожидания до взятия в работу, а также задача может ожидать фидбека или в ее решении могут участвовать несколько человек. Поэтому через это поле мы увидим ресурсоемкость решения бага.
Таблица будет выглядеть примерно так:

Эти показатели также разбираются на ретроспективе команд. Уточню, что тут речь идет именно о реагировании на инциденты. То есть мы сосредотачиваемся на том, как ускорить процесс решения критичных багов.
Процессные метрики
Далее разберем некоторые процессные метрики. Они показывают, насколько соблюдаются процессы в наших командах.
Мы можем писать отличные, проверенные временем рабочие инструкции и даже периодически про них напоминать участникам команд. Проблема в том, что без единого представления в виде четких, проверяемых метрик инструкции не будут работать.
Допустим, мы хотим, чтобы разработчики в командах уделяли примерно половину своего рабочего времени на написание кода, а около четверти времени — на решение багов из бэклога. Если обязанностью тимлида будет внимательно следить за соблюдением этой договоренности, вскоре он перестанет заниматься чем-либо еще. Все время будет уходить на микроменеджмент разработчиков. Эффективнее будет иметь посчитанные фактические соотношения перед глазами и приводить в качестве иллюстрации на ретроспективе по спринту и на 1-1 с сотрудниками.
Мы считаем, что развитие тайм-менеджмента важно для сотрудника и он сам должен следить за тем, чтобы время распределялось равномерно. Поэтому метрики по распределению времени будут полезны в качестве личного KPI.
Выглядеть будет примерно так:

Цветовая индикация подсвечивает в виде градиентов наши пожелания: чем больше процент времени на продуктовые задачи, тем темнее окраска. Чем меньше процент времени, тем темнее окраска. С процентом времени на баги логикв окраски обратная.
Рабочий процесс похож на предыдущие:

Основная выборка происходит в ноде типа MySQL, операция “Execute SQL” такого вида:
SELECT
u.lower_user_name AS jira_user_name,
ROUND(SUM(wl.timeworked) / 60 / 60, 2) AS total_work_logged_in_hours,
ROUND(SUM(IF(ticket.issuetype IN (1000 /* Change request */, 1001 /* Improvement */, 1002 /* New Feature */), wl.timeworked, 0)) / 60 / 60, 2) AS product_work_logged_in_hours,
ROUND((SUM(IF(ticket.issuetype IN (1000 /* Change request */, 1001 /* Improvement */, 1002 /* New Feature */), wl.timeworked, 0)) / SUM(wl.timeworked)) * 100) AS product_work_percent,
ROUND(SUM(IF(ticket.issuetype = 100 /* Bug */, wl.timeworked, 0)) / 60 / 60, 2) AS bugs_work_logged_in_hours,
ROUND((SUM(IF(ticket.issuetype = 100 /* Bug */, wl.timeworked, 0)) / SUM(wl.timeworked)) * 100) AS bugs_percent
FROM project p
INNER JOIN jiraissue ticket ON ticket.PROJECT = p.ID
INNER JOIN worklog wl ON wl.issueid = ticket.ID
INNER JOIN app_user u ON u.user_key = wl.AUTHOR AND u.lower_user_name IN (
{{ $('Aggregate Jira User Names').item.json.jira_user_name.map(item => "'" + item + "'") }}
)
WHERE
p.pkey IN ('PROJECT1', 'PROJECT2')
AND wl.CREATED >= '{{ $('Loop Over Sprints').item.json.start_on }} 00:00:00'
AND wl.CREATED <= '{{ $('Loop Over Sprints').item.json.due_on }} 00:00:00'
GROUP BY wl.AUTHOR
;
Мы выбираем тикеты по проектам в пределах спринта, к которым приджойниваем общее залогированное время из таблицы worklog для поля total_work_logged_in_hours, залогированное время на продуктовые задачи и на баги. Также считаем процент по ним от всего залогированного времени. Выборку делаем только для списка линейных разработчиков, полученных из Data Table.
Другие процессные метрики
Пройдусь еще кратко по тем метрикам, которые также могут быть полезны и реализуются аналогичным образом:
Скорость реакции на фидбеки. Число задач в ожидании фидбека, минимальное/максимальное и медианное время ожидания, с разбивкой по спринтам и сотрудникам.
Ответ на вопросы: не застревают ли задачи на ответе исполнителю? Не замедляют ли общее время выполнения?Своевременность оценки задач. Содержит метрику по количеству неоцененных задач в бэклоге, находящихся в спринте (фактически это очередь на оценку), и время оценки задач (сколько уже оцененные задачи ждали оценки, с момента появления на исполнителе).
Ответ на вопрос: не застревают ли задачи на оценке у исполнителей?
Своевременная оценка задач необходима, чтобы понимать объем бэклога в часах и планировать ресурсы.
Время на обеспечение процесса. Как распределяется время на оргвопросы, по типам встреч.
Ответ на вопросы: не отнимают ли встречи слишком много времени? Не требуют ли процессы оптимизации для более эффективного перераспределения времени?
В заключение
В этой статье мне хотелось показать, что построение метрик не является сложной задачей, зато позволяет наглядно видеть качество работы и аргументированно обсуждать возникающие проблемы с сотрудниками. Благодаря n8n у нас есть возможность буквально за час добавить недостающие метрики, чтобы прояснять все этапы в процессах. Причем сам подсчет прозрачен для всех участников и подлежит простому исправлению при необходимости.
Все в наших руках. Отлаженные процессы – результат кропотливого и целенаправленного труда. Желаю успехов в налаживании работы ваших команд!
