„Ожидание – корень всех душевных страданий.“ © Уильям Шекспир или что-то из буддизма

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

Знаете, кто ещё больше не любит ожидания? Бизнес. Бизнес очень не любит, когда ожидания копят важные проекты и инициативы. Согласно исследованиям средняя эффективность потока в Delivery составляет 35%, а всё остальное время — задачи ждут. (Данные на основе опросов специалистов — ссылка. Метаанализ тысячи workflow от Nave — ссылка ) Справедливо, что ключевая точка роста для ускорения поставки – уменьшение ожиданий.

Именно об этих "фантастических" ожиданиях и пойдёт речь в статье. Я расскажу о системной работе с блокировками и зависимостями, которые повинны в значительном количестве задержек. Мы погрузимся в необходимую теорию, рассмотрим наш успешный практический кейс в hh.ru и, что особенно ценно, я поделюсь конкретными пошаговыми инструкциями по настройке Jira & n8n, а также способами работать с визуализацией блокеров в удобных плагинах, чтобы вы могли применить этот подход у себя.

Этот материал будет полезен IT-менеджерам, тимлидам, руководителям проектов, delivery менеджерам и руководителям функций — всем, кто стремится более осознанно и эффективно распоряжаться временем и ресурсами.

Теория

Хорошая новость — ожиданиями можно управлять.

Для начала, их можно классифицировать по типам — кластеризовать. Конкретный тип — это и есть зависимость. А вот когда такая зависимость становится непреодолимым препятствием и останавливает непосредственно работу, мы фиксируем блокер, в которым указываем тип зависимости. Именно об этих фундаментальных понятиях и их связи пойдёт речь дальше.

Терминология: Зависимость и Блокер

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

Представьте, что каждая машина на трассе — это задача или проект, движущийся по вашему потоку поставки.

Зависимость — это риск, из-за которого движение может остановиться. Колесо отвалилось, бензин кончился, дорогу перекрыли, водитель заболел. Это ещё не произошло, но мы знаем, что так может быть.

Блокер — это фактическое происшествие, которое уже остановило вашу машину. Это уже случившаяся поломка в самой машине или внешнее препятствие которое мешает её движению. Конкретная проблема с которой мы столкнулись, например — пробка.

Как этим управлять?

Итак, как это работает на практике, рассмотрим на дорожной аналогии:

Наша задача — обеспечить максимально быстрое движение потока задач на конкретном участке трассы.

  1. Фиксация блокеров: Каждый факт остановки машины (блокер) анализируется и фиксируется с указанием детальной причины: "красный свет", "ДТП", "ремонт", "отсутствие топлива".

  2. Кластеризация зависимостей: Собранные блокеры группируются по типам зависимостей, к которым они относятся. Например, все "красные светофоры" относятся к функциональной зависимости от регулирования трафика; "отсутствие топлива" – к ресурсной. Цель — выявить наиболее частые типы зависимостей, приводящие к остановкам.

  3. Системное улучшение: На основе анализа выявленных типов зависимостей принимаются стратегические решения по их минимизации или устранению. Если “светофорные” блокеры доминируют, это сигнал для системных изменений в управлении трафиком (перенастройка светофора, строительство развязки). Цель — минимизировать возникновение блокеров данного типа и их влияние на поток.

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

Типизация зависимостей

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

Представим наше дорожное движение и классифицируем его "узкие места":

1) По локализации:

Можем ли мы контролировать эту зависимость?

  • Внутренние зависимости: Те, что возникают внутри нашей машины. Мы можем контролировать их напрямую.

    • Аналогия: Кончился бензин, спустило колесо, или водитель плохо себя чувствует.

  • Внешние зависимости: Те, что возникают вне нашей машины. Мы не можем контролировать их напрямую, но зависим от них.

    • Аналогия: Дорога перекрыта полицией, внезапная пробка, мост разрушен или ремонтные работы.

Проекты в компании без WIP лимитов, например
Проекты в компании без WIP лимитов, например

2) По характеру зависимости:

  • Ресурсные зависимости: Для движения задачи необходим ограниченный ресурс (люди, оборудование, инструмент, время), находящийся вне нашего доступа.

    • Аналогия: На заправке нет нужного топлива, или мост закрыт из-за нехватки строительной бригады.

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

    • Аналогия: Не можем проехать перекрёсток, пока не загорится зелёный свет, или ждём, пока другая машина завершит манёвр.

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

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

Сбор и анализ данных

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

1) Сбор данных: что фиксируем?

Для эффективного анализа недостаточно просто знать о факте блокировки. Важно фиксировать:

  • Начало и окончание блокировки: Это позволяет точно измерить продолжительность простоя.

  • Тип зависимости: К какому типу (внутренняя/внешняя, ресурсная/функциональная/организационная) относится данный блокер.

  • Контекст: Дополнительные данные, которые могут помочь в дальнейшем анализе (например, команда-владелец задачи, команда-поставщик, связанная задача).

На защите инициативы по изменению процессов, например
На защите инициативы по изменению процессов, например

2) Метрики: что измеряем?

На основе собранных данных мы можем получить следующие ключевые метрики:

  • Общее время всех блокировок за период: Суммарное время, которое все задачи провели в состоянии блокировки за определенный период (например, неделя, месяц). Это показывает общие "потери" потока.

    • Аналогия: Сколько часов суммарно все машины простояли в пробках на нашем участке трассы.

  • Время в блокировках в разрезе по типам зависимостей: Продолжительность простоя, сгруппированная по типам зависимостей.

    • Аналогия: Сколько времени машины стояли из-за "красных светофоров", сколько из-за "ДТП", сколько из-за "отсутствия топлива". Это показывает, какие типы "зависимостей" наиболее "дорогие".

  • Количество блокировок: Общее число зафиксированных блокировок за период.

    • Аналогия: Сколько раз машины останавливались из-за пробок.

  • Количество блокировок по типам зависимостей: Число блокировок, сгруппированное по типам зависимостей.

    • Аналогия: Сколько раз машины останавливались из-за "красных светофоров", сколько раз из-за "ДТП" и т.д. Это показывает, какие типы "зависимостей" встречаются чаще всего.

Помимо ключевых метрик можно ещё выделить всякие контекстуальные. Например: метрики блокеров в разрезе по типам задач, по командам…

3) Анализ и визуализация: как использовать?

Сырые данные и метрики бесполезны без анализа. Для этого необходимы:

  • Удобные инструменты: Системы для сбора и агрегации данных (например, Jira, специализированные BI-системы).

  • Дашборды и отчёты: Визуальное представление метрик, которое позволяет быстро выявлять тенденции, аномалии и узкие места. Дашборды показывают актуальную картину, отчёты — ретроспективу и динамику.

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

Управление изменениями

Итак, мы научились видеть и измерять зависимости и блокеры. Но сбор данных и метрик — это лишь первый шаг. Главная цель — запустить системные изменения, которые позволят минимизировать эти проблемы. Процесс управления изменениями сам по себе обширен, но в контексте зависимостей мы выделяем несколько ключевых принципов:

1) Гипотезы, а не гарантии: Мы не можем быть уверены, что каждое предложенное изменение решит проблему. Наш анализ метрик лишь указывает на потенциальные узкие места. Поэтому каждое изменение — это гипотеза, требующая проверки.

  • Аналогия: Мы предполагаем, что новая фаза светофора ускорит поток, но не знаем наверняка, пока не попробуем. Тем более не можем предсказать, насколько. 

2. Тестирование на малом масштабе: Запускать изменения сразу на весь "трафик" дорого и рискованно. Вместо этого выбираем узкую тестовую группу: конкретный тип задач (например, "грузовики"), небольшой участок трассы или отдельную команду. Это позволяет проверить гипотезу с минимальными издержками и сделать это быстро.

  • Аналогия: Сначала тестируем новую схему движения только на одном перекрестке или для определённого типа транспорта.

3. Механизм согласования и запуска: Выявление проблем и предложенные решения должны быть донесены до тех, кто может принимать решения. Определите, кому вы приносите свои идеи (например, руководителям направлений, владельцам продуктов), кто выступает спонсором изменений и даёт им "зелёный свет". Важен регулярный формат отчётности и предложений, основанный на анализе метрик (например, квартальные встречи с обзором "болезней трафика" и предложением "лечения").

  • Аналогия: На основе данных о "пробках" мы представляем план изменения дорожной разметки или установки новых знаков в Дорожную службу, которая утверждает эти новшества.

Инициатива по напоминаниям о просроченных согласованиях, например
Инициатива по напоминаниям о просроченных согласованиях, например

4. Анализ результатов и масштабирование: После тестового запуска необходим тщательный анализ результатов. Кто участвует в этом анализе? Как мы принимаем решение о масштабировании изменения на весь "трафик"? Используем те же метрики в динамике: действительно ли "время в блокировках" по данному типу зависимости снизилось? Важно помнить, что систему нельзя перегружать постоянными, не доведенными до конца изменениями.

  • Аналогия: После тестового запуска на перекрестке мы снова измеряем скорость потока. Если стало лучше, внедряем это решение на всех подобных перекрестках, но следим, чтобы не создавать новые "пробки" из-за слишком большого количества изменений одновременно.

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

Теория: Обобщение и выводы

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

Основные определения:

  • Зависимость (в потоке поставки ценности) – это тип риска, реализация которого может остановить движение конкретного рабочего элемента.

  • Блокер (в потоке поставки ценности) – это реализовавшийся риск зависимости, который фактически остановил работу над задачей.

Общая схема системного управления зависимостями:

Эффективная работа с зависимостями строится на системном подходе. Этот процесс начинается с подготовки, а затем переходит в непрерывный цикл улучшения (Continuous Improvement):

Этап 1: Подготовка к циклу

  1. Определение типов зависимостей: Разработка унифицированной таксономии зависимостей (например, по локализации: внутренние/внешние; по характеру: ресурсные, функциональные, организационные). 

  2. Внедрение механизма фиксации блокеров: Разработка инструментов и процессов для сбора данных о каждой конкретной остановке (блокер). Важно фиксировать длительность, тип зависимости и контекст блокировки.

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

Этап 2: Цикл системного управления

После подготовки запускается непрерывный цикл, направленный на снижение влияния зависимостей:

  1. Анализ данных и формирование гипотез: На основе собранных метрик выявляются наиболее частые и критичные типы зависимостей. Формируются гипотезы о том, какие изменения могут снизить их негативное влияние.

  2. Запуск пилотных изменений: Тестирование разработанных гипотез на ограниченном масштабе для проверки их эффективности и минимизации рисков.

  3. Измерение результатов: Оценка влияния пилотных изменений на метрики блокировок. Подтверждение или опровержение выдвинутых гипотез.

  4. Масштабирование изменений: Успешные пилотные изменения тиражируются на всю систему, становясь частью стандартных процессов. Если гипотеза была не успешная — откатываемся к старой версии процесса.

  5. Бонус Рефлексия: Постоянный анализ самого процесса управления изменениями: как мы могли бы провести это изменение быстрее, точнее или эффективнее в будущем?

Итак, мы рассмотрели теорию: что такое зависимости и блокеры, как их классифицировать, измерять и управлять изменениями.

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

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

Практика

As Is — Q1 2024

Общий контекст

Мы ведём все наши задачи в Jira. В процессе Delivery у нас участвует более 50 технических команд, работающих над продуктовыми фичами. Все эти фичи находятся в общем Jira-проекте Portfolio.

Как это работало

Флаг. Задача считалась заблокированной, если на неё ставили флаг (через контекстное меню).

Ставим задачу на флаг
Ставим задачу на флаг

Автоматическое создание блокера. Постановка флага на карточку фичи автоматически создавала связанную задачу-блокер в отдельном проекте "Blocker".

Автоматическое закрытие блокера. Снятие флага автоматически закрывало соответствующую связанную задачу-блокер.

Расчёт времени блокировки. Время блокировки рассчитывалось как время, в течение которого связанная задача-блокер находилась в активном статусе.

Как этим управляли. Флаги ставили участники команды на утренних Daily встречах. Вася уехал на конференцию — ставим задачу на флаг. Петя вернулся с больничного — снимаем его задачу с флага.

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

Проблематика

Кластеризация. Хотя возможность выбрать тип блокировки существовала, ею пользовались лишь 2 человека из 500+ участников Delivery-процесса. Ведь для этого нужно отдельно открыть задачу блокер… Нажать редактирование… Как следствие, полезной статистики распределения блокеров по типам фактически не существовало.

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

"И так сойдёт". Часто флаг ставился только после того, как менеджер на утреннем Daily-синке спрашивал о статусе задачи, и команда сообщала о блокировке. Фактически, блокер мог возникнуть ещё вчера. Это приводило к искажению реального времени простоя и некорректным данным по длительности блокировок.

Планирование блокеров. Система не позволяла заранее зафиксировать ожидаемые простои. Например, если Вася согласовал свой уход в отпуск, и его задача гарантированно начнёт простаивать с даты Х, поставить флаг заранее было невозможно, так как это немедленно начинало бы отсчёт времени блокировки.

Постфактум блокеры. Ретроспективный анализ или обзор сервиса поставки часто выявляли неочевидные задержки. Например, задача могла простаивать два месяца в ожидании действий от коллег, но поскольку флаг не ставился, никаких записей о такой блокировке не существовало. Учесть подобные кейсы в статистике постфактум было уже невозможно.

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

MVP Q2-2024

Цель изменений: В первую очередь нам нужно было получить кластеризацию зависимостей и корректный учёт времени блокировок.

Новый механизм постановки блокера. Мы добавили специальную кнопку “Блокер” в задачу. При нажатии на неё появлялась форма, в которой обязательно требовалось выбрать тип блокировки, что напрямую решало проблему отсутствия кластеризации.

Отдельная кнопка "Блокер"
Отдельная кнопка "Блокер"

Другой подход к расчёту времени. Также в форму фиксации блокера мы добавили два новых поля: “Дата блокировки” и “Дата разблокировки”. Эти поля копировались автоматизацией в задачу блокер и после этого очищались в задаче фичи. Время блокировки теперь рассчитывалось как разница этих полей.

Форма при нажатии на кнопку
Форма при нажатии на кнопку

Учёт будущих и прошлых блокировок. Для решения проблем планирования и постфактум блокеров поля “Дата блокировки” и “Дата разблокировки” были сделаны необязательными при первоначальной постановке. Если поле “Дата блокировки” не заполнялось, оно автоматически устанавливалось на now(). Если же “Дата блокировки” была в будущем, задача физически не блокировалась сразу. Вместо этого, раз в сутки отрабатывала CRON-автоматизация, которая проверяла такие задачи и инициировала их блокировку, когда наступала указанная дата.

Флаг — для визуализации. Мы изменили смысл флага: теперь он использовался исключительно для визуализации заблокированного состояния задачи на доске, а не для запуска автоматизации создания блокера. Это делало статус задачи прозрачным для команды.

Запрет на движение по Workflow. При создании блокера у задачи устанавливался скрытый признак is_blocked = true. Это запускало проверку, которая запрещала перемещение задачи по WorkFlow. При закрытии блокера этот признак снимался, и задачу снова можно было двигать по процессу. Это гарантировало, что заблокированные задачи не продвинутся без решения их проблем.

Новый механизм снятия блокировки. Мы добавили специальную кнопку “Снять блок” (видимую только для задач с признаком is_blocked). При её нажатии можно было уточнить тип блокировки и установить финальные “Дату блокировки” и “Дату разблокировки”.

Все остальные переходы не доступны, пока не снят блок
Все остальные переходы не доступны, пока не снят блок

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

Форма при снятии блока
Форма при снятии блока

Результаты MPV

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

Командам — удобство.

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

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

Менеджменту — данные.

Кластеризация. Этим решением мы одномоментно подняли конверсию в заполнение типа блокировки с 0.2% до 100%. Создать блокер без указания типа блокировки стало невозможно. Для случаев, когда ни один из предложенных типов не подходил, мы оставили пункт “Другое”, при выборе которого поле комментария становилось обязательным для заполнения.

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

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

Обратная связь на процесс

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

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

Данные прямо в задачах. Для анализа блокировок требовалась ручная выгрузка данных из Jira: необходимо было вычитать значение поля “Дата блокировки” из “Даты разблокировки”, чтобы получить финальную цифру. Это означало, что отчётность и графики приходилось строить исключительно в Excel, а использовать готовые решения, такие как Jira Metrics Plugin и Jira Custom Charts & Dashboards, было невозможно. Более того, JQL-фильтры нельзя было применять для фильтрации задач по фактическому времени блокировок.

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

Эти замечания оказались чрезвычайно полезными, а предложенный фронт работ – весьма обширным. Мы тщательно проанализировали, как можно интегрировать и реализовать эти идеи в рамках существующей системы. К процессу проработки решения на стороне IT была подключена не только команда администрирования Jira, но и инженер по автоматизации. Вот что в итоге у нас получилось.

Глубоко автоматизированный процесс (Q3-2024)

Часть решений мы смогли реализовать базовыми средствами Jira. Однако для реализации более сложных сценариев нам потребовался внешний инструмент автоматизации — no-code платформа n8n.io. Я писал про неё в одной из своих прошлых статей: “Автоматизируем бизнес — без кода и разработчиков”. Ниже расскажу, как это решение изменило процесс с точки зрения команд и менеджмента:

Проект, тип задачи и статус заблокированной сущности. Чтобы мастабировать механизм не только для фич в проекте Portfolio, но и, например, для атомарных задач на написание кода или для аналитических задач), мы реализовали копирование данных. Теперь в отдельные поля задачи-блокера автоматически копировались “Проект”, “Тип заблокированной задачи” и “Статус заблокированной задачи” из родительской заблокированной сущности.

Это позволило строить гибкие JQL-фильтры. Например: 

Project = "R&D :: Blocker" AND issuetype = Блокер AND Статус ~ "Decomposition :: In Progress" AND Тип_задачи ~ "Feature"
(С помощью такого запроса, мы можем получить все блокеры, для конкретного типа и статуса задач и понять, почему у нас застревают задачи по завершению декомпозиции)

Технические комментарии. При фиксации и снятии блокировки в задачу писались технические комментарии от пользователя-бота, в которых указывались все основные данные о блокировке. Эти комментарии также отправлялись участникам заблокированной задачи на почту, обеспечивая немедленное уведомление заинтересованных лиц.

Метрики внутри задачи. Мы пошли дальше простого подсчета времени блокировок и решили отображать ключевые метрики поставки непосредственно внутри каждой задачи в отдельных, не редактируемых пользователями полях. Система автоматизации на базе n8n.io раз в сутки по API получает все активные задачи, проводит для каждой из них необходимые подсчёты и обновляет эти данные. Пользователи могут найти все ключевые Delivery метрики прямо в задаче: Время поставки, время в блокировках, Flow Efficiency этой задачи..

Это, в свою очередь, открыло широкие возможности для построения детализированных отчётов с помощью стандартных и кастомных решений Jira. Все метрики стали доступными для оперирования в JQL-фильтрах. Теперь не проблема создать Kanban-доску с фичами, которые находятся в работе дольше 30 дней, или с задачами, накопившими более 14 дней ожидания в блоках. Удельное количество задач, заблокированных более 5 дней, стало легко отслеживать с помощью отчётов пропускной способности и быстрых фильтров по метрикам внутри задачи. Сильно проще стало настраивать логику автоматизаций и уведомлений на то, как долго задача заблокирована или находиться на каком-то этапе…

Практика: Общие выводы

Так что же это всё дало бизнесу? Самое главное, не в самих данных и статистике, не в отдельных практиках и визуализации. А в том, что все вместе это превращается в комплексный бизнес инструмент.

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

Middle management получил инструмент, который сопровождает их на всем жизненном цикле поставки — от планирования новых работ и до написания финальных отчётов. К слову о последних, отчёты теперь пестрят красноречивыми цифрами, сколько месяцев и лет ожиданий мы накопили в поставке за прошедший квартал.

Мы сформировали отдельную рабочую группу ВИНОР “Внедрение изменений на основе репортов” которая регулярно (раз в квартал) проводит анализ Product Delivery, в том числе в разрезе ожиданий, подсвечивает наиболее критичные точки и предлагает изменения на основе этих данных.

Топ-менеджмент (CEO-1-2) используя этот инструмент, могут получить больше визуализированных данных для принятия решений и при необходимости опираться на них для адаптации стратегии.

***

Причём, это инструмент применим не только для изменения процессов:

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

  • Планирование. А здесь можно смотреть, как часто мы переключались на другие задачи и откладывали то, что было запланировано изначально.

  • Коммуникации. Блокеры также указывают с кем из коллег у нас может быть слабо построено взаимодействие, упираемся ли мы в коллег из Legal, 1C, CyberSec? Где ситуация хуже и требуется строить мосты и координировать усилия.

***

Использование этого инструмента позволило нам качественно вырастить Product Delivery культуру в компании. И мне хочется иметь наглость считать, что в части управления зависимостями у нас сейчас один из самых зрелых процессов поставки на рынке.

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

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

Под капотом Jira

Ну что, если вы загорелись желанием начать использовать такой инструмент у себя в компании, то вот эту часть статьи надо принести вашим Jira админам / Инженерам автоматизации / IT-шникам.

Далее даю инструкцию (со своими небольшими правками) от коллег, которые непосредственно реализовывали эту систему в Jira & n8n.

Jira — вводные

Сразу хочется сказать, что базовой функциональности Jira для реализации такой штуковины не достаточно. Мы использовали сразу несколько плагинов:

  • Jira Misc Workflow Extensions (JMWE) — основа для автоматизации переходов и условий.

  • ScriptRunner — для валидаций и предзаполнения полей через Behaviours.

  • Automation for Jira — для фоновой логики (например, cron-обработка).

  • Alkaes JIRA Plugin — для раскрашивания кнопок (опционально).

В принципе, всё это можно сделать с помощью ScriptRunner, но мы предпочли использовать JWME — он показался нам удобнее для поддержки и визуальной настройки.

Начинаем конфигурацию — подготовка и установка блока

Программа минимум которая вам потребуется — это два типа задач. Задачи которые блокируются и собственно задачи-блокеры. Также, вам потребуется версия Jira 8.4 или выше, так как только с этой версии добавили возможность делать переходы петли “Из статуса в самого себя”.

Шаг 1 Создание кастомных полей и форм

Сначала создадим новые кастомные поля:

Название

Тип

Описание

Тип блокировки

Select List (Single)

Для указания типа блокировки

Дата блокировки

Date Picker

С какой даты началась блокировка

Дата разблокировки

Date Picker

С какой даты блокировка окончилась

is_blocked

Text Field (single)

Техническое поле для фиксации факта блокировки, скрытое от пользователей через Behaviours

После этого формируем из этих полей две формы (Screens)

  • Blocker Screen — Который будет показываться на экране блокировки

  • UnBlocker Screen — Который будет показываться на экране снятия блока

Скриншот
Ого, уже на 15 проектов масштабировали…
Ого, уже на 15 проектов масштабировали…

Шаг 2 Создание переходов

В WorkFlow блокируемых задач мы добавляем два новых Transition петли из “Any status” в “ItSelf”

  • Переход “Блокер”.  По этому переходу показываем форму, создаем связанную задачу, устанавливаем блок.

  • Переход “Снять блок”. По этому переходу показываем форму, закрываем связанную задачу, снимаем блоку.

Скриншоты
На схеме WorkFlow они будут выглядеть вот так
На схеме WorkFlow они будут выглядеть вот так
Добавление в свою очередь выглядит вот так
Добавление в свою очередь выглядит вот так

Шаг 3 Красим кнопки

Чтобы кнопки переходов были красными и зелёными, как на скриншоте выше, и стояли в самом начале списка, можно задать им properties (свойства перехода).

Свойство opsbar-sequence — отвечает за место расположения кнопки в списке. Чем меньше значение, тем раньше в списке этот переход будет показан. Зададим обоим переходам значение 1, так как одновременно они показываться не будут.

Свойство transitionbuttomstyle — работает за счёт плагина Alkaes JIRA Plugin и отвечает за стилизацию. По сути это применяемые к кнопке in-line css стили, однако, поддерживаются далеко не все стили, а только ограниченный набор. Зададим стили:

  • Красная — background-image: linear-gradient(#FFAAAA 100%, #205081 0%); color:#FFFFFF;

  • Зелёная — background-image: linear-gradient(#00885A 100%, #205081 0%); color:#FFFFFF;

Вот так выглядит в настройках:

Скриншот

Теперь можно начать настраивать логику этих переходов:

Настраиваем Блокировку

Начнём с валидации. По логике у нас два основных условия — обязательность поля “Тип блокировки” и то, что “Дата разблокировки” не может быть более ранней, чем “Дата блокировки”. Всё это мы делаем с помощью JMWE

С обязательностью “Типа блокировки” всё просто, его делаем через JMWE Fields Required Validator

А вот с датами интересней, для этого используем JMWE Build-your-own Validator, в который вписываем малюсенький скриптик:

if (issue.get("customfield_37313") && issue.get("customfield_35718")) { 
  if (issue.get("customfield_37313") > issue.get("customfield_35718")) {false} 
  else {true}
} else {true}

ВАЖНО! Здесь и далее у вас номера кастомных полей будут отличаться. Не копируйте примеры кода без правок — обязательно, указывайте свои правильные номера кастомных полей.

Скриншот

Теперь перейдём к постфункциям. Тут большая часть логики, будем двигаться по кусочкам.

Установка “Даты блокировки”. Поскольку поле дата блокировки не обязательное, его могут не заполнять, но данные нам нужны. Так что при незаполненном поле мы считаем, что блокировка произошла в момент её установки. Делаем это через JMWE Set issue fields, прописываем вот так:

new Date().format("dd.MM.y")
Скриншот

Создаём задачу Blocker. Мы это делаем в отдельном проекте Blocker, но вы можете и в текущем. После создания, связываем её с заблокированной задачей. Делаем это через JMWE Create / Clone issue(s) 

  • Тема: issue.get("summary")

  • Проект: issue.project.name

  • Описание: transientVars.comment

  • Статус: issue.status.name

  • Тип задачи: issue.issuetype.name

  • Исполнитель: currentUser

  • И остальные поля из задачи: Тип блокировки, Дата блокировки, Дата разблокировки и др.

Скриншот

Комментируем заблокированную задачу. Оставляем комментарий: кем и когда была задача заблокирована, и если указана дата разблокировки — когда она разблокируется. Делаем это через JMWE Comment issue вот так:

Код
C ${issue.get("customfield_37313").format("dd.MM.yyyy")} задача заблокирована пользователем - ${currentUser.displayName}. 
Тип блокировки - ${issue.get("customfield_31724")}
Описание: <% if (transientVars.comment) {print transientVars.comment} else {print "отсутствует"} %>.
Ориентировочная дата разблокировки - <% if (issue.get("customfield_35718")) {print (issue.get("customfield_35718").format("dd.MM.yyyy"))} else {print("не указана")}  %>.
Скриншот

Устанавливаем флаг и признак блокировки. Но с условием, что задача уже заблокирована, а не будет заблокирована в будущем. Делаем это через JMWE Set issue fields, условие:

Код
((issue.get("customfield_37313") <= new Date()) || !(issue.get("customfield_37313"))) &&
((issue.get("customfield_35718") > new Date()) || !(issue.get("customfield_35718")))
Скриншот

Очищаем поля. Поскольку форма у нас показывается на блокируемой задаче, то тип блокировки и даты блокировки по умолчанию сохраняются в неё, мы их копируем в задачу блокер, а после этого они в заблокированной задаче не нужны. Ведь возможно она будет заблокирована несколько раз, и тогда нам надо будет указать эти значения заново. Так что мы добавили условие, очищать эти поля если задача не блокируется прямо сейчас. Очищаем через JMWE Clear field(s):

Код
(issue.get("customfield_37313") > new Date() && issue.get("customfield_37313") != null) 
|| 
(issue.get("customfield_35718") < new Date() && issue.get("customfield_35718") != null)  
Скриншот

Очищаем комментарий. В любую форму перехода автоматически подставляется поле “комментарий”. Его значение становится описанием задачи-блокера. При этом если мы его не очистим в процессе работы автоматизации, то он попадет и в блокируемую задачу. А туда у нас уже публикуется авто-комментарий о том, кто, когда и по какому поводу поставил блокер. Чтобы избежать дублирования, мы очищаем данные поля “комментарий” с помощью ScriptRunner — Custom script post-function

transientVars.comment = ""
Скриншот

На этом мы закончили техническую часть установки блока. Будет ещё ряд работ которые надо сделать после настройки обоих переходов, но о них попозже, а теперь…

Настраиваем “Снять блок”

Что делает переход:

  • Проверяет, что задача заблокирована.

  • Проверяет корректность данных.

  • Копирует поля в задачу "Блокер" (если она найдена по JQL)

  • Если дата разблокировки пуста — устанавливает текущую дату.

  • Копируем комментарий из окна перехода в блокер.

  • Пишем комментарий о снятии блокировки для прозрачности.

  • Снимает признак блокировки и флаг с заблокированной задачи.

  • Переводит связанную задачу-блокер в статус “Блокировка снята”.

Условия перехода (Condition) — Важно, чтобы этот переход был доступен пользователям только в случаях, когда задача уже заблокирована. Для этого с помощью JMWE Build-your-own Condition указываем

issue.get("customfield_37318") == "true"
Скриншот

Валидация полей. Если при установке блокера обязательным было только поле “Тип блокировки”, то при снятии мы также проверяем “Дату блокировки” и “Дату разблокировки”, через JMWE Fields Required Validator

Скриншот

Валидация корректности даты. Все ещё коллеги могут указать, случайно например, что “Дата разблокировки” произошла раньше, чем “Дата блокировки”. Добавляем валидацию от человеческого фактора через JMWE Build-your-own Validator:

if (issue.get("customfield_37313") && issue.get("customfield_35718")) { 
  if (issue.get("customfield_37313") > issue.get("customfield_35718")) {false} else {true}
} else {true}
Скриншот

Копируем данные полей. Теперь когда мы уверены что данные введены корректно переходим к постфункциям. Копируем из заблокированной задачи в задачу-блокер эти данные. Нужную задачу-блокер находим через JQL с помощью JMWE Copy issue fields:

issueFunction in linkedIssuesOf("issuekey = $issue.key", "blocked by") and status = Заблокировано
Скриншот

Устанавливаем дату разблокировки. В задаче "Блокер" по JQL если Дата разблокировки пуста  — ставим now():

  • Получаем связанный блокер — issueFunction in linkedIssuesOf("issuekey = $issue.key", "blocked by") and status = Заблокировано

  • Устанавливаем его значение — (issue.get("customfield_35718")) ? issue.get("customfield_35718") : new Date().format("dd.MM.y")

Скриншот

Копируем комментарий из окна перехода. Коллеги могут захотеть поделиться деталями предпринятых мер, после которых блокер снимается. Его мы с экрана перехода копируем в связанную задачу-блокер. Делаем это с помощью JMWE Comment issue

  • Получаем связанный блокер — issueFunction in linkedIssuesOf("issuekey = $issue.key", "blocked by") and status = Заблокировано

  • Копируем комментарий — transientVars.comment

Скриншот

Очищаем комментарий перехода. Чтобы он не опубликовался в заблокированную задачу, а был только в задаче-блокере. Делаем это с помощью ScriptRunner, очень просто: transientVars.comment = ""

Скриншот

Оставляем технический комментарий. От имени бота, который сигнализирует о том, что блокировка была снята. Делаем это с помощью JMWE Comment issue.

Блокировка снята <%=issue.get("customfield_35718").format("dd.MM.yyyy")%> сотрудником <%=currentUser.displayName%>.
Время начала блокера <%=issue.get("customfield_37313").format("dd.MM.yyyy")%>. 
Общее время блокера составило <%=secondsBetween(issue.get("customfield_37313"), issue.get("customfield_35718"))/86400%> дня.
Скриншот

Очищаем поля перехода. На случай если задача ещё будет блокироваться в будущем и данные старой блокировки не подставлялись в экран перехода. Делаем это с помощью JMWE Clear field(s):

Скриншот

Снимаем флаг и признак блокировки. Просто устанавливаем этим полям пустое значение. Через JMWE Set issue fields

Скриншот

Закрываем задачу блокер. Переводим связанную задачу блокер в статус “Блокировка снята”. Делаем это через JMWE Transition Linked Issue

Скриншот

Настройка Поведения (Behaviours) в Script Runner

Ещё немного логики докидываем через SciptRunner, в частности прячем от пользователей с его помощью поле is_blocked с экранов и делаем комментарий обязательным при указании “Тип блокировки” = “Другое”

Общая логика:

Код
def typ = getFieldById("customfield_31724") // Тип блокировки
def bloc = getFieldById("customfield_37313") // Дата блокировки
def rloc = getFieldById("customfield_35718") // Дата разблокировки
def iloc = getFieldById("customfield_37318") // Флаг is_blocked
iloc.setHidden(true) // прячем поле is_blocked, чтоб оно не смущало пользователей и они случайно его не изменили
if (bloc.getValue() != "" && rloc.getValue() == "") { // если дата блокировки не пустая, а дата разблокировки пустая
    rloc.setFormValue(new Date().format("dd.MM.y")) // установить дату разблокировки - текущую
}
if (iloc.getValue() == "" && bloc.getValue() == "") { // если задача НЕ заблокирована и дата блокировки пустая
    bloc.setFormValue(new Date().format("dd.MM.y")) // установить дату блокировки - текущую
}
if (iloc.getValue() == 'true' && rloc.getValue() == "") { // если задача заблокирована и дата разблокировки пустая
    rloc.setFormValue(new Date().format("dd.MM.y")) // установить дату разблокировки - текущую
}
Скриншот

Обязательный комментарий при “Другое”:

if ((fieldScreen.name == "PORTFOLIO Blocker Screen" || fieldScreen.name == "Blocker Screen") && typ.getValue() == "Другое") {
    getFieldById("comment").setRequired(true)
}
Скриншот

Изменения WorkFlow блокируемых задач

Для того чтобы коллеги не забывали снимать блокировки, важно добавить ограничения на все остальные переходы которые есть в WorkFlow по признаку is_blocked

Condition на переходы. Делаем это через JWME Build-your-own Condition прописывая в нём условие issue.get("customfield_37318") != "true"

Важно, что это нужно сделать для ВСЕХ переходов, кроме “Снять Блок”. Поскольку таких переходов может быть очень много, хочу подсветить удобную функцию копирования настроек в JWME.

Скопированную в буфер обмена конфигурацию потом можно вставить в нужный переход в 1 клик, вот так:

Jira Automations для блокеров

После того как всю эту логику в WorkFlow мы реализовали, есть ещё несколько точечных настроек, которые нам нужно добавить через Jira Automations.

Планирование блокеров в будущем. Помните выше говорилось о том, что дата блокировки может быть в будущем и пока она не наступила, над задачей можно продолжать работать? Так вот, чтобы это корректно работало нам нужна автоматизация, которая будет раз в сутки получать все задачи без признака is_blocked, но с “Дата блокировки” = сегодня. И для каждой найденной:

  • Устанавливать блокировку и флаг

  • Оставить технический комментарий о факте блокировки

Скриншот

Копирование данных из Блокера в задачу. Задачу-блокер можно не только создать и закрыть, но и отредактировать. Вот скажем заболел сотрудник, мы поставили блокер. Он сходил к врачу, ему продлили больничный — блокер затягивается. В этот момент логично открыть блокер и внести в него корректировки. Но чтобы не делать это вручную и в заблокированной задаче — мы настроим автоматизацию, которая скопирует правки.

Скриншот

Очистка блокировок при клонировании. Вот скажем коллеги пилят очень похоже фичи, например, компоненты дизайн-системы с однотипным описанием. Конечно же лень заводить новый компонент, заполняя все данные вручную. Можно просто клонировать текущий и внести в него точечные правки. Но если текущий был заблокирован — то блокировка тоже клонируется. Чтобы так не происходило, мы добавили отдельную автоматизацию, очищающую признаки блокировки.

Скрытый текст

Нюансы реализации

Есть ещё несколько важных вещей, которые надо учитывать:

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

Мы посчитали, что в большинстве случаев люди работают со своими задачами и создают блокеры из них, так что мы просто через behaviours запретили создавать блокеры вручную.

Закрытие блокеров. У задач типа блокер есть свой собственный WorkFlow и порой коллеги могут нажимать кнопку “Снять блок” не в заблокированной задаче, а в самой задаче-блокере. Тут снова выбор: либо поддержать такое поведение, либо запретить.

В этом случае мы посчитали, что лучше поддержать и добавили в переход постфункцию Set issue fields (JMWE) которая очищает поля для всех связанных задач (ну вдруг кто к блокеру несколько задач привяжет).

Получаем их все через JQL: issueFunction in linkedIssuesOf("issuekey = $issue.key")

После чего очищаем все поля: Flagged, is_blocked, Тип блокировки, Дата блокировки, Дата разблокировки.

Автоматическое закрытие блокера. Ещё одна развилка. Если заранее известна дата разблокировки, можно реализовать CRON автоматизацию, которая будет получать все блокеры, у которых “Дата разблокировки” = сегодня. И закрывать их.

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

Итого

Мы понимаем, что весь описанный объем функций может выглядеть устрашающим. Но под капотом это не настолько сложный процесс в Jira: всего два новых перехода в WorkFlow того типа задач который блокируется, 1 новый тип задач и пяток полей + логика того как это всё взаимодействует.

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

На текущий момент в нашей компании процесс масштабирован примерно на 15 Jira проектов и порядка 25 типов задач. Возможно, в будущем мы придумаем как модернизировать инструмент для учёта сквозных блокировок, когда верхнеуровневый проект / инициативы блокируются на одну функцию, а та в свою очередь на что-то ещё. Условно, чтобы в задаче на стратегическую цель можно было увидеть, что все связанные с ней работы встали из-за того, что у нас заболел единственный тестировщик. Также мы хотели бы придумать, как учитывать суммарное время блокировок в родительских задачах, исходя из данных дочерних.

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

Под капотом n8n

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

Записать метрики в каждую задачу базовыми средствами Jira, к сожалению, невозможно, либо слишком трудозатратно. Так что мы использовали платформу визуального программирования n8n. 

Что это за зверь такой и как его готовить, я писал в своей предыдущей статье для Habr — “Автоматизируем бизнес — без кода и разработчиков”. Так что сконцентрируемся на том, а как непосредственно работает созданная нами автоматизация для записи метрик.

Для того, чтобы понимать происходящее в автоматизации, надо ещё дать контекст того, что мы говорим про продуктовую разработку фичей основного продукта  hh.ru. WorkFlow этих фичей мы побили на крупные этапы, в рамках которых каждый этап содержит несколько различных статусов задачи в Jira.

  • Backlog – когда задача была создана, описана, может быть ранжирована, но никаких работ над ней не проводилось

  • Discovery – этап на котором сначала происходят исследования и анализ, фиксация проблематики и ценности, а потом происходит выработка решения которое позволит проблему решить, а ценность принести 

  • Delivery – этап, когда происходит непосредственно реализация выработанного решения, написание кода, review, demo, релиз 

  • A/B Test – необязательный для всех фичей этап, когда происходит тестирование разработанного решения, сбор продуктовых метрик и оценка влияния фичи на продукт 

  • Postproduction – этап уже после разработки для формирования документации, подведения итогов, дебрифинга и проч. 

И вот для этих этапов мы считаем следующий набор метрик, которые хотим видеть непосредственно в задаче:

  • Discovery CycleTime — время, которое задача провела в статусах, которые относятся к группе Discovery 

  • Delivery CycleTime — время, которое задача провела в статусах, которые относятся к группе Delivery 

  • A/B Test CycleTime — (где есть) время в статусах группы CycleTime, 

  • PostProduction CycleTime — (где есть) время в статусах группы Postprod 

  • Total LeadTime — суммарное время в п.1-п.4 

  • BlockerTime — время, которое задача суммарно провела в блокерах всех групп п.1-п.4 

  • BlockerTime % — отношение времени задачи в блокерах (BlockerTime), по отношению к Total LeadTime 

  • Discovery Flow Efficiency — процент времени в активных статусах работы  минус время в блокерах к общему времени Discovery CycleTime.  

  • Delivery Flow Efficiency — процент времени в активных статусах работы  минус время в блокерах к общему времени Delivery CycleTime.

  • Total FlowEfficiency — отношение в процентах суммы всех статусов непосредственной работы п.1-п.4 за вычетом времени блокеров в этих статусах, к общему времени задачи в поставке (Total LeadTime).

Общая схема автоматизации

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

Занимательный факт: Автоматизация построена таким образом, что позволяет по одному WorkFlow обрабатывать как штучную задачу, так и пачку из 1000 задач Jira
Занимательный факт: Автоматизация построена таким образом, что позволяет по одному WorkFlow обрабатывать как штучную задачу, так и пачку из 1000 задач Jira

Давайте разберемся по блокам: 

  • Блок 1: Триггеры автоматизации. Их два типа: WebHooks от Jira, которые прилетают на обновление данных в задаче, и CRON, отрабатывающий раз в сутки. Кроме того, мы оставили дополнительный неактивный блок, если придется обновлять какую-то выборку задач вне очереди.

  • Блок 2: Сбор данных. Отвечает за получение информации из Changelog задач, а также за фильтрацию данных — чтобы дальше в автоматизацию пошли только переходы задач по нужным статусам и блокерам.

  • Блок 3: Обработка данных.  В нем происходят основные вычисления и группировки по статусам, причем сами группы статусов захардкожены в первой ноде (самой левой  этого блока).

  • Блок 4: Запись в задачу. Тут происходит обновление полей-метрик в задаче по Jira HTTP REST API. Мы также сделали дополнительную валидацию, чтобы не записывать LeadTime для только созданных задач, т.к. в первые 2-3 дня задача активно обновляется и без наших обновлений. 

  • Блок 5: Обработка ошибок. В него сливаются все ошибки возникающие в нодах WorkFlow и присылаются сообщениями в чатик в корп мессенджере Mattermost со ссылкой на флоу. Таким образом мы можем оперативно получить информацию об ошибке и в два клика переходить к её фиксу.

    • Отдельной веткой мы решили сделать получение уведомлений, если по какой-то причине на выходе получим отрицательное значение LeadTime — это позволит сразу понять, если что-то в алгоритме считается некорректно 

  • Блок 6 “Здесь ничего не происходит”. исключительно визуальный, но в случае обработки большой пачки задач можно сразу увидеть: сколько из них сработало вхолостую.

Теперь давайте разберёмся в деталях и исполняемом коде (да, мы всё-таки пишем код). Блоки 1 и 2 скипнем. Там либо хождение в API Jira (про него можно просто почитать в документации — https://docs.atlassian.com/software/jira/docs/api/REST/9.14.0/#api/2/search-searchUsingSearchRequest ), либо получение всех данных задачи из WebHook.

Но есть важный комментарий: мы хоть и используем стандартный GET-метод по получению Changelog задачи, дополнительно указываем параметры expand=changelog, а также указываем ряд конкретных полей, которые мы хотим посмотреть. Это важно, так как при обработке пачки из 1000 задач и нескольких сотнях активных полей, n8n начинает"захлебываться" в данных, т.к. в моменте оперирует текстом на >20 мегабайт между нодами. За счет указания вывода конкретных полей этот объем уменьшается как минимум вдвое.

Хардкод DB

По сути, это просто группировка статусов по этапам, в рамках которых нужно считать метрики. При это есть общее время, есть разделение на активное время работы и время ожидания, чтобы считать метрику Flow Efficiency. А ещё не пугайтесь тому, как много этих статусов. Дело в том что там есть как актуальные названия, так и старые, уже выведенные из использования, но в Changelog они все ещё записаны по старому. Ах да, ещё тут статусы сразу для нескольких типов задач и если в какой-то момент задачу мувнули из одного типа в другой — это тоже будет корректно учтено в рассчётах.

В общем, получается вот такая портянка:

Код
return { 
  'statusDB':[{ 
	"name": "Discovery", 
	"nonInitStatuses": [ 
  	"PROBLEM DISCOVERY: IN PROGRESS",  
  	"PROBLEM DISCOVERY: DONE", 
  	"SOLUTION DISCOVERY: IN PROGRESS", 
  	"SOLUTION DISCOVERY: DONE", 
  	"Проработка: Анализ проблемы", 
  	"Проработка: Генерация/Отсев Вариантов", 
  	"Проработка: Детализация Решения", 
  	"Проработка: готово" 
	], 
	"initiativeStatuses": [ 
  	"BRIEFING", 
  	"ОПИСАНИЕ",  
  	"READY FOR RESEARCH", 
  	"ОПИСАНИЕ ГОТОВО", 
  	"RESEARCH", 
  	"Исследование в работе", 
  	"READY FOR FEASIBILITY", 
  	"Исследование готово", 
  	"FEASIBILITY STUDY", 
  	"ПОИСК РЕШЕНИЙ", 
  	"Brifieng", 
  	"Анализ с командой", 
  	"Скоринг инициатив", 
  	"Инициатива: готово к работе" 
	], 
	"field": "customfield_39037" 
  }, 
  {  
	"name": "Delivery", 
	"nonInitStatuses": [ 
  	"DECOMPOSITION: IN PROGRESS",  
  	"DECOMPOSITION: DONE",  
  	"DEVELOPMENT: IN PROGRESS",  
  	"DEVELOPMENT: DONE",  
  	"Декомпозиция: в работе",  
  	"Декомпозиция: Готово",  
  	"Разработка: В работе",  
  	"Разработка: Готово" 
	], 
	'initiativeStatuses': [ 
  	"BACKLOG",  
  	"В беклог",  
  	"PLANNING",  
  	"ПЛАНИРОВАНИЕ",  
  	"READY FOR DELIVERY",  
  	"ГОТОВО К ПОСТАВКЕ", 
  	"DELIVERY",  
  	"В ПОСТАВКЕ",  
  	"Инициатива: в работе" 
	], 
	"field": "customfield_39038" 
  }, 
  { 
	"name": "A/B Test", 
	"nonInitStatuses": [ 
  	"AB-TEST: ACCUMULATION OF DATA",  
  	"AB-TEST: DONE",  
  	"AB-TEST: SUMMING RESULTS",  
  	"Тестирование на пользователях",  
  	"Тестирование на пользователях, AB: в работе" 
	], 
	'initiativeStatuses': [], 
	"field": "customfield_39039" 
  }, 
  { 
	"name": "Postproduction", 
	"nonInitStatuses": [ 
  	"POSTPRODUCTION: IN PROGRESS",  
  	"Удаление лишнего кода",  
  	"Удаление лишнего кода: в работе" 
	], 
	'initiativeStatuses': [ 
  	"DEBRIEFING",  
  	"Подведение итогов",  
  	"Debrifieng" 
	], 
	"field": "customfield_39040" 
  }, 
  { 
	"name": "LeadTime", 
	"nonInitStatuses": [], 
	"initiativeStatuses": [], 
	"field": "customfield_39036" 
  }], 
  'FEDB': [{ 
	"name": "Discovery", 
	"nonInitStatuses": [ 
  	"PROBLEM DISCOVERY: IN PROGRESS",  
  	"SOLUTION DISCOVERY: IN PROGRESS",  
  	"Проработка: Анализ проблемы",  
  	"Проработка: Генерация/Отсев Вариантов",  
  	"Проработка: Детализация Решения" 
	], 
	'initiativeStatuses': [ 
  	"RESEARCH",  
  	"Исследование в работе", 
  	"FEASIBILITY STUDY",  
  	"ПОИСК РЕШЕНИЙ", 
  	"Анализ с командой",  
  	"Скоринг инициатив" 
	] 
  }, 
  {  
	"name": "Delivery", 
	"nonInitStatuses": [ 
  	"DECOMPOSITION: IN PROGRESS",  
  	"DEVELOPMENT: IN PROGRESS",  
  	"Декомпозиция: в работе",  
  	"Разработка: В работе" 
	], 
	'initiativeStatuses': [ 
  	"PLANNING",  
  	"ПЛАНИРОВАНИЕ", 
  	"DELIVERY",  
  	"В ПОСТАВКЕ",  
  	"Инициатива: в работе" 
	] 
  }, 
  { 
	"name": "A/B Test", 
	"nonInitStatuses": [ 
  	"AB-TEST: ACCUMULATION OF DATA",  
  	"AB-TEST: SUMMING RESULTS",  
  	"Тестирование на пользователях",  
  	"Тестирование на пользователях, AB: в работе" 
	], 
	'initiativeStatuses': [] 
  }, 
  { 
	"name": "Postproduction", 
	"nonInitStatuses": [ 
  	"POSTPRODUCTION: IN PROGRESS",  
  	"Удаление лишнего кода",  
  	"Удаление лишнего кода: в работе" 
	], 
	'initiativeStatuses': [ 
  	"DEBRIEFING",  
  	"Подведение итогов",  
  	"Debrifieng" 
	] 
  }], 
  'activeDiscoveryCT': [{ 
	"nonInitStatuses": [ 
  	"PROBLEM DISCOVERY: IN PROGRESS",  
  	"SOLUTION DISCOVERY: IN PROGRESS",  
  	"Проработка: Анализ проблемы",  
  	"Проработка: Генерация/Отсев Вариантов",  
  	"Проработка: Детализация Решения" 
	], 
	'initiativeStatuses': [] 
  }], 
  'activeDeliveryCT': [{ 
	"nonInitStatuses": [ 
  	"DECOMPOSITION: IN PROGRESS",  
  	"DEVELOPMENT: IN PROGRESS",  
  	"Декомпозиция: в работе",  
  	"Разработка: В работе" 
	], 
	'initiativeStatuses': [] 
  }], 
  'activeDevCT': [{ 
	"nonInitStatuses": [ 
  	"DEVELOPMENT: IN PROGRESS", 
  	"Разработка: В работе" 
	], 
	'initiativeStatuses': [] 
  }] 
}

Рассчёт метрик

Когда все эти данные получены мы идем по каждому из событий в Changelog и выбираем только те, которые связаны с изменением статуса или с блокером (т.к. информация по блокерам нам нужна). Здесь мы фиксируем как сам event, так и его стартовое и окончательное время. 

Код ноды: 

Код
function saveParam(list, value, param="issuetype") { 
  for (item of list) { 
    if (!item[param]) { 
      item[param] = value; 
	} 
  } 
} 
  
let tempIssue = { 
  'key': $input.item.json.key, 
  'issueType': $input.item.json.fields.issuetype.name, 
  'created': $input.item.json.fields.created,  
  'changeList': [], //список изменений статусов 
  'blockList': [] //список блокеров 
}; 
  
for (history of $input.item.json.changelog.histories) { 
  for (elem of history.items) { 
	//цикл сохранения изменения статусов без учета переходов в самого себя 
    if (elem.field == "status") { 
      tempIssue.changeList.push({ 
    	'changeDate': history.created, 
    	'fromCode': elem.from, 
    	'fromStatus': elem.fromString 
  	}); 
      saveParam(tempIssue.blockList, tempIssue.changeList[tempIssue.changeList.length-1].fromStatus, "blockedStatus"); 
  	//дополнительно при смене статуса обновляем статус блокера 
      if (tempIssue.blockList.length > 0) { 
        if (!tempIssue.blockList[tempIssue.blockList.length-1].finish) { 
          tempIssue.blockList[tempIssue.blockList.length-1].finish = history.created; 
          tempIssue.blockList.push({ 
        	'start': history.created 
      	}); 
    	} 
  	} 
	} 
  } 
  
  //отдельным циклом смотрим блокеры, чтобы корректно проставлять статус блокера 
  for (elem of history.items) { 
	//цикл сохранения блокеров 
    if (elem.field == 'is_blocked') { 
  	//проверяем, что это открывающийся блокер 
      if (!elem.fromString) { 
        tempIssue.blockList.push({ 
      	'start': history.created 
    	}); 
  	//иначе это закрывающийся блокер и последнему открытому ставим дату закрытия 
  	} else { 
        if (tempIssue.blockList.length > 0 && !tempIssue.blockList[tempIssue.blockList.length-1].finish) 
          tempIssue.blockList[tempIssue.blockList.length-1].finish = history.created; 
  	} 
	} 
  } 
  //отдельной итерацией смотрим на изменение типа задачи, чтобы корректно атрибуцировать статусы к типу 
  for (elem of history.items) { 
	//проверяем наличие смены типа 
    if (elem.field == 'issuetype') { 
  	//24307 - issueType "инициатива" 
      if (elem.from == "24307" || elem.to == "24307") { 
        tempIssue.typeChanged = 'yes'; 
  	} 
  	//указываем тип задачи, в котором были все статусы "до" 
      saveParam(tempIssue.changeList, elem.fromString); 
  	//указываем тип задачи, в которых были все статусы "до" 
      saveParam(tempIssue.blockList, elem.fromString); 
	} 
  } 
} 
  
if (tempIssue.blockList.length) { 
  //если у последнего блокера нет даты снятия, то ставим текущую 
  if (!tempIssue.blockList[tempIssue.blockList.length-1].finish) { 
    tempIssue.blockList[tempIssue.blockList.length-1].finish = $now; 
  } 
} 
tempIssue.changeList.push({ 
  'changeDate': $now, 
  'fromCode': $input.item.json.fields.status.id, 
  'fromStatus': $input.item.json.fields.status.name 
}); 
//проставляем всем изменениям статусов и блокерам тип задачи 
saveParam(tempIssue.changeList, $input.item.json.fields.issuetype.name); 
saveParam(tempIssue.blockList, $input.item.json.fields.issuetype.name); 
  
//если вдруг не было изменений статусов, то всем блокерам ставим текущий 
saveParam(tempIssue.blockList, tempIssue.changeList[tempIssue.changeList.length-1].fromStatus, "blockedStatus"); 
  
return tempIssue;

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

[
  {
    "key": "PORTFOLIO-32240",
    "issueType": "Feature",
    "created": "2024-07-16T19:02:13.733+0300",
    "changeList": [
      {
        "changeDate": "2024-07-16T19:05:24.310+0300",
        "fromCode": "10304",
        "fromStatus": "Идея: в работе",
        "issuetype": "Feature"
      },
    ],
    "blockList": [
      {
        "start": "2025-03-19T18:51:47.063+0300",
        "blockedStatus": "Solution Discovery: In progress",
        "finish": "2025-03-25T16:15:02.767+0300",
        "issuetype": "Feature"
      },
    ]
  }
]

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

Сам алгоритм подсчета метрик выглядит так:

Код
function groupStatuses(toGroup, statusGroups, time, sdb = []) { 
  let totalTime = 0; 
  
  for (status of toGroup) { 
	//выбираем группу статусов в зависимости от типа задачи в моменте 
    let statusesType; 
    if (status.issuetype == "Инициатива") { 
      statusesType = "initiativeStatuses"; 
	} else { 
      statusesType = "nonInitStatuses"; 
	} 
    for (statusGroup of statusGroups) { 
      for (elem of statusGroup[statusesType]) { 
        if (status.status.toString().toLowerCase() == elem.toString().toLowerCase()) { 
          let k = 0; 
          for (item of sdb) { 
            if (item.name == statusGroup.name) { 
              if (item[time]) { 
                item[time] += status.time; 
          	} else { 
                item[time] = status.time; 
          	} 
          	k++; 
        	} 
      	} 
          if (k == 0 && statusGroup.name) { 
            sdb.push({ 
          	'name': statusGroup.name, 
          	'field': statusGroup.field, 
          	[time]: status.time 
        	}); 
      	} 
          totalTime += status.time; 
    	} 
  	} 
	} 
  } 
  if (time == "time") { 
    sdb.push({ 
  	'name': 'Total', 
  	'field': statusGroups[statusGroups.length-1].field, 
  	'time': totalTime 
	}); 
  } else { 
    sdb[sdb.length-1][time] = totalTime; 
  } 
  return sdb; 
} 
  
//считаем время в статусах. SDB - StatusDataBase, хранилище информации о статусах 
let sdb = groupStatuses( 
  $input.item.json.stateArray, $('SetStatusesDB').item.json.statusDB, "time" 
); 
//добавляем время блокеров 
sdb = groupStatuses( 
  $input.item.json.blockerTimeArray, $('SetStatusesDB').item.json.statusDB, "blockerTime", sdb 
); 
//добавляем информацию о фактическом рабочем времени 
sdb = groupStatuses( 
  $input.item.json.stateArray, $('SetStatusesDB').item.json.FEDB, "FEWorkTime", sdb 
); 
//добавляем время блокеров в фактических рабочих процессах 
sdb = groupStatuses( 
  $input.item.json.blockerTimeArray, $('SetStatusesDB').item.json.FEDB, "FEBlockerTime", sdb 
); 
//добавляем время активной разработки 
sdb = groupStatuses( 
   $input.item.json.stateArray, $('SetStatusesDB').item.json.activeDiscoveryCT, "activeDiscoveryTime", sdb 
); 
//добавляем время блокеров активной разработки 
sdb = groupStatuses( 
  $input.item.json.blockerTimeArray, $('SetStatusesDB').item.json.activeDiscoveryCT, "activeDiscoveryBlockerTime", sdb 
); 
//добавляем время активной разработки 
sdb = groupStatuses( 
   $input.item.json.stateArray, $('SetStatusesDB').item.json.activeDeliveryCT, "activeDeliveryTime", sdb 
); 
//добавляем время блокеров активной разработки 
sdb = groupStatuses( 
  $input.item.json.blockerTimeArray, $('SetStatusesDB').item.json.activeDeliveryCT, "activeDeliveryBlockerTime", sdb 
); 
//добавляем время активной разработки 
sdb = groupStatuses( 
   $input.item.json.stateArray, $('SetStatusesDB').item.json.activeDevCT, "activeDevelopmentCT", sdb 
); 
//добавляем время блокеров активной разработки 
sdb = groupStatuses( 
  $input.item.json.blockerTimeArray, $('SetStatusesDB').item.json.activeDevCT, "activeDevelopmentBlockerCT", sdb 
); 
  
return { 
  'key': $input.item.json.key, 
  'sdb': sdb 
};

Пример того, как это выглядит после подсчёта:

Код
[
  {
    "key": "PORTFOLIO-32240",
    "sdb": [
      {
        "name": "Discovery",
        "field": "customfield_39037",
        "time": 3860854826,
        "blockerTime": 3168286760,
        "FEWorkTime": 3860854826,
        "FEBlockerTime": 3168286760
      },
      {
        "name": "Delivery",
        "field": "customfield_39038",
        "time": 4908894184,
        "FEWorkTime": 4534645884
      },
      {
        "name": "Total",
        "field": "customfield_39036",
        "time": 8769749010,
        "blockerTime": 3168286760,
        "FEWorkTime": 8395500710,
        "FEBlockerTime": 3168286760,
        "activeDiscoveryTime": 3860854826,
        "activeDiscoveryBlockerTime": 3168286760,
        "activeDeliveryTime": 4534645884,
        "activeDeliveryBlockerTime": 0,
        "activeDevelopmentCT": 3818885810,
        "activeDevelopmentBlockerCT": 0
      }
    ]
  }
]

Вычисление метрики Flow Efficiency вынесено в отдельную ноду, так как этот расчёт опирается на ранее подсчитанные метрики. Вот его код:

Код
function factTime(workTime, blockerTime, res = "") { 
  if (workTime) { 
    if (blockerTime) { 
      return workTime - blockerTime; 
	} else { 
      return workTime; 
	} 
  } else return "";  
} 
  
function timeSum(resultName, timeToSum) { 
  if (total[resultName]) { 
    total[resultName] += timeToSum; 
  } else { 
    total[resultName] = timeToSum; 
  } 
} 
  
function calcPart(workTime, blockerTime, totalTime) { 
  let result = 0; 
  if (totalTime) { 
    if (workTime) { 
      result = (factTime(workTime, blockerTime) / totalTime * 100).toFixed(0); 
	} 
  } 
  return result; 
} 
  
let sdb = $input.item.json.sdb; 
let total = sdb[sdb.length-1]; 
  
for (statusGroup of sdb) { 
  //считаем %Blocker 
  statusGroup.blockerPart = calcPart(statusGroup.blockerTime, 0, statusGroup.time); 
  //Считаем FE 
  statusGroup.FEPart = calcPart(statusGroup.FEWorkTime, statusGroup.FEBlockerTime, statusGroup.time); 
  
  //отдельно считаем FE Production без учета группы статусов Production 
  if (statusGroup.name != "Postproduction" && statusGroup.name != "Total") { 
    timeSum("prodWorkTime", statusGroup.FEWorkTime); 
    timeSum("prodBlockerTime", statusGroup.FEBlockerTime); 
    timeSum("prodToClientTime", statusGroup.time); 
  } 
} 
  
//считаем FE Production (без учета Postproduction) 
total.FEProdPart = calcPart(total.prodWorkTime, total.prodBlockerTime, total.prodToClientTime); 
  
//считаем Active Discovery 
total.activeDiscoveryFact = factTime(total.activeDiscoveryTime, total.activeDiscoveryBlockerTime); 
//считаем Active Delivery 
total.activeDeliveryFact = factTime(total.activeDeliveryTime, total.activeDeliveryBlockerTime); 
//считаем Active Development 
total.activeDevelopmentFact = factTime(total.activeDevelopmentCT, total.activeDevelopmentBlockerCT); 
  
return $input.item.json;

Ну и наконец, у нас есть договорённость с коллегами сохранять результаты в днях, а не в секундах, поэтому перед записью ещё проводится конвертация значений в человеко-понятную форму.

Код
function convertToHuman(time, comms = 0) { 
  let msecToDays = 1000*60*60*24; 
  if (time) { 
    return (time / msecToDays).toFixed(comms); 
  } else return "0"; 
} 
  
for (item of $input.item.json.sdb) { 
  item.humanTime = convertToHuman(item.time); 
  if (item.name == "Total") { 
    item.humanTimeTwo = convertToHuman(item.time,2); 
    item.blockerHumanTime = convertToHuman(item.blockerTime); 
    item.activeDiscoveryHumanTime = convertToHuman(item.activeDiscoveryFact); 
    item.activeDeliveryHumanTime = convertToHuman(item.activeDeliveryFact); 
    item.activeDevelopmentHumanTime = convertToHuman(item.activeDevelopmentFact); 
  } 
} 
  
return $input.item.json;

Итоговый набор данных после всех расчётов и конвертации в итоге выглядит вот так:

Код
[
  {
    "key": "PORTFOLIO-32240",
    "sdb": [
      {
        "name": "Discovery",
        "field": "customfield_39037",
        "time": 3860854826,
        "blockerTime": 3168286760,
        "FEWorkTime": 3860854826,
        "FEBlockerTime": 3168286760,
        "blockerPart": "82",
        "FEPart": "18",
        "humanTime": "45"
      },
      {
        "name": "Delivery",
        "field": "customfield_39038",
        "time": 4908894184,
        "FEWorkTime": 4534645884,
        "blockerPart": 0,
        "FEPart": "92",
        "humanTime": "57"
      },
      {
        "name": "Total",
        "field": "customfield_39036",
        "time": 8769749010,
        "blockerTime": 3168286760,
        "FEWorkTime": 8395500710,
        "FEBlockerTime": 3168286760,
        "activeDiscoveryTime": 3860854826,
        "activeDiscoveryBlockerTime": 3168286760,
        "activeDeliveryTime": 4534645884,
        "activeDeliveryBlockerTime": 0,
        "activeDevelopmentCT": 3818885810,
        "activeDevelopmentBlockerCT": 0,
        "prodWorkTime": 8395500710,
        "prodBlockerTime": null,
        "prodToClientTime": 8769749010,
        "blockerPart": "36",
        "FEPart": "60",
        "FEProdPart": "96",
        "activeDiscoveryFact": 692568066,
        "activeDeliveryFact": 4534645884,
        "activeDevelopmentFact": 3818885810,
        "humanTime": "102",
        "humanTimeTwo": "101.50",
        "blockerHumanTime": "37",
        "activeDiscoveryHumanTime": "8",
        "activeDeliveryHumanTime": "52",
        "activeDevelopmentHumanTime": "44"
      }
    ]
  }
]

Тут мы делаем ту самую проверку, не получили ли мы какие-то странные значения, например отрицательный LeadTime. Если всё хорошо, идём дальше, а если нет, присылаем уведомления о том, что где-то наши подсчёты пошли не по плану.

Запись в задачу

Наконец, нам нужно подготовить эти данные для записи непосредственно в Jira, где каждому кастомному полю будет соответствовать значение метрики. Это сделает следующая нода:

Код
let body = {"fields": { }};
let sdb = $input.item.json.sdb;
for (item of sdb) {
  if (item.humanTime) {
    //cycleTime и LeadTime
    body.fields[item.field] = +item.humanTime;
  } 
  if ($('GetTransitions').item.json.issueType == "Feature" && +sdb[sdb.length-1].humanTime >= 3) {
    if (item.name == "Discovery") {
      //FlowEfficiency Discovery в %
      body.fields.customfield_40120 = +item.FEPart;
    } 
    if (item.name == "Delivery") {
      //FlowEfficiency Delivery в %
      body.fields.customfield_40121 = +item.FEPart;
    }
  }
  
  if (item.name == "Total") {
    //BlockerTime в днях
    body.fields.customfield_40118 = +item.blockerHumanTime;
    
    if (+sdb[sdb.length-1].humanTime >= 3) {
      //BlockerTime в процентах
      body.fields.customfield_40119 = +item.blockerPart;
      //FlowEfficiency Production - FE для всех статусов, кроме Postproduction
      body.fields.customfield_40122 = +item.FEProdPart;
      //FlowEfficiency Full - FE для всех статусов задачи из LeadTime
      body.fields.customfield_40124 = +item.FEPart;
    }
    if ($('GetTransitions').item.json.issueType == "Feature" || $('GetTransitions').item.json.issueType == "Story" || $('GetTransitions').item.json.issueType == "История") {
      body.fields.customfield_40311 = +item.activeDiscoveryHumanTime;
      body.fields.customfield_40312 = +item.activeDeliveryHumanTime;
      body.fields.customfield_40313 = +item.activeDevelopmentHumanTime; 
    }
  }
}
body.fields['customfield_36312'] = +$input.item.json.sdb[$input.item.json.sdb.length-1].humanTimeTwo;

let toClear = [
  'customfield_36312',
  "customfield_39036",
  'customfield_39037',
  'customfield_39038',
  'customfield_39039',
  'customfield_39040',
  'customfield_40118',
  'customfield_40119',
  'customfield_40120',
  'customfield_40121',
  'customfield_40122',
  'customfield_40124',
  'customfield_40311',
  'customfield_40312',
  'customfield_40313'
];
for (elem of toClear) {
  if ($('GetIssueChangelog').item.json.fields[elem] && !body.fields[elem]) {
    body.fields[elem] = null;
  }
}


return {
  "body": body,
  "key": $input.item.json.key,
}

А вот пример данных которые она возвращает:

[
  {
    "body": {
      "fields": {
        "customfield_39037": 45,
        "customfield_40120": 18,
        "customfield_39038": 57,
        "customfield_40121": 92,
        "customfield_39036": 102,
        "customfield_40118": 37,
        "customfield_40119": 36,
        "customfield_40122": 96,
        "customfield_40124": 60,
        "customfield_40311": 8,
        "customfield_40312": 52,
        "customfield_40313": 44,
        "customfield_36312": 101.5
      }
    },
    "key": "PORTFOLIO-32240"
  }
]

На этом все рассчёты заканчиваются, и в следующем блоке эти данные заливаются по REST API в задачку, о чём опять-таки уже можно почитать в документации — https://docs.atlassian.com/software/jira/docs/api/REST/9.14.0/#api/2/issue-editIssue

И если Jira вернула успешный ответ, то на этом автоматизация заканчивается, а вот если вернула ошибку — мы получаем об этом уведомление в Mattermost.

Итого

С точки зрения взаимодействия систем, это очень простая автоматизация: данные берутся из Jira, обрабатываются и кладуться обратно в Jira (тут даже нечего согласовывать с архитектурой или службой безопасности). Но вот внутренняя логика не совсем тривиальная, и в процессе реализации мы её несколько раз обсуждали со специалистами по Product Delivery.

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

Также, реализация этой автоматизации показала нам границы возможностей n8n по обработке значительных массивов данных. Например, мы остановились на “комфортных” объёмах пачками по 600 задач. Обработка одной такой пачки занимает порядка 3 минут.

Скриншот

Визуализация метрик по блокерам

Ну и вишенка на торте от моего коллеги из Product Operations: расскажем как строить графики и дашборды по этим метрикам.

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

Для регулярного анализа блокеров мы используем дашборды на базе Custom Charts for Jira. Это позволяет буквально в пару кликов собрать наглядный отчет: по оси X — этапы процесса (WorkFlow), по оси Y — суммарное время блокировки, а цветом выделяются типы блокеров. Такой виджет строится на основе JQL-фильтра, который выбирает блокировки за интересующий период. В результате сразу видно, где и какие блокировки “болят” чаще всего, и сколько времени на них теряется.

Как создать отчет по блокерам в Custom Charts?

Подготовьте JQL-фильтр

Сначала создайте фильтр, который выберет нужные задачи-блокеры за интересующий период.

Пример JQL:

issueFunction in linkedIssuesOf("project = 'R&D :: Портфель проектов' AND type in (Feature) AND 'Delivery Team' in (Команда_1, Команда_2) AND status = Fixed AND labels != nostat") 
AND project = "R&D :: Blocker" AND created >= 2025-01-01 AND created <= 2025-03-31

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

З.Ы. Функция issueFunction in linkedIssuesOf() работает за счёт плагина Script Runner, её нет в Jira “из коробки”.

Не забудьте сохранить этот фильтр под понятный именем, например: БЛОКЕРЫ В Q1 2025.

Добавьте виджет Custom Charts на Дашборд

  • Откройте нужный Dashboard в Jira

  • Нажмите “Add gadget” и выберите Custom Charts

  • В настройках виджета выберите источник данных:

    • SOURCE -> Saved filters -> выберите ваш фильтр (БЛОКЕРЫ В Q1 2025)

Настройте параметры визуализации

  • CHART TYPE: выберите “2D Stacked Bar Chart” (или другой тип, если нужен другой вид).

  • CHART BY: выберите поле “Статус” (это этапы WorkFlow, по оси X).

  • GROUP BY: выберите поле “Тип блокировки” (это цветовые сегменты столбцов).

  • CALCULATE: выберите “Sum” и далее “Blocker LeadTime” (это суммарное время блокировки, по оси Y).

  • Chart title: задайте понятное название, например “Статистика по блокерам в Q1”.

Дополнительные настройки

  • В разделе Chart By Options можно задать порядок отображения статусов (Custom Order), скрыть нулевые значения, настроить подписи и цвета.

  • В правой части виджета отображается Live Preview — сразу видно, как будет выглядеть итоговый график.

  • Проверьте, что легенда корректно отображает все типы блокировок (цвета и подписи).

Сохраните и разместите

Нажмите Save, и отчет появится на вашем Dashboard  — теперь вы видите, где и какие блокировки “болят” чаще всего и сколько времени на них теряется.

***

Важно: анализировать стоит не только количество блокеров, но и их “тяжесть” — суммарное время простоя. Иногда одна-две долгие блокировки наносят больше вреда, чем десяток коротких. Такой подход позволяет быстро находить настоящие узкие места и принимать решения на основе фактов, а не ощущений.

Как построить отчет по блокерам в JIra Metrics Plugin

А если хочется получить срез “здесь и сейчас” — отлично выручает бесплатный плагин Jira Metrics Plugin. Его главный плюс — не требует установки на сервер и работает прямо поверх любой Jira-доски. Достаточно открыть нужную доску, включить плагин и выбрать отчет Throughput. 

Как это использовать для анализа блокеров: 

  • Настроить доску, чтобы вывести только интересующие нас блокеры. Можно использовать пример JQL указанный выше для Custom Charts 

  • Добавить фильтры по типу блокера

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

  • Можно быстро сравнить разные команды, типы задач или периоды — и понять, где процесс буксует чаще всего. 

Как построить отчет по блокерам в Jira Metrics Plugin (JMP)

Установите расширение

  • Это можно сделать по ссылке Google Chrome Store. После установки иконка JMP появится на панели расширений браузера

Откройте нужную Jira-доску по которой будете анализировать блокеры

  • JMP работает “поверх” любой доски — не требуется никаких дополнительных прав или настроек на стороне Jira

Настройте фильтрацию задач

  • В JMP подтягиваются все задачи и фильтры отображаемые на доске. Чтобы анализировать только блокеры, используйте Quick Filters на доске. Например, фильтр по типу блокировки `` "Тип блокировки" = "Заблокирован на другую команду" ``

Запустите JMP. Есть два способа открыть аналитику

  • Способ 1: На доске появится голубая кнопка Analyze Metrics — кликните по ней

  • Способ 2 (рекомендуется): Кликните на иконку расширения JMP на панели браузера. Это удобно — впоследствии можно запускать аналитику по иконке, не переходя на доску в Jira

Перейдите в Throughput отчет

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

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

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

Послесловие

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

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

Забавный факт, я рассказывал про этот подход к работе с блокерами на Delivery Meetup СПБ в мае 2024 года. По итогам доклада многие члены сообщества попросили написать гайд, как это всё реализовать. И вот ирония, статья про управление зависимостями и ожиданиями, сама была заблокирована другими важными проектами и накопила целый год ожидания. 

Авторы и благодарности

Статью писали:

Илья Айден (ex Смирнов) — Head of Internal Automation в hh.ru
Компиляция всей статьи и автор блоков теория и практика.
Веду канал Археология Смыслов, если вам понравилось — подписывайтесь.

Радик Пивоваров — Ведущий Администратор Jira в hh.ru
Автор блока “Под капотом Jira”, реализовывал инструмент в Jira.

Владимир Сухоцкий — Ведущий инженер автоматизации в hh.ru
Автор блока “Под капотом n8n”, реализовывал автоматизацию в n8n.

Анвар Хакимов — Lead Delivery Manager в hh.ru
Автор блока “Визуализация метрик по блокерам”.
Создатель плагина Jira Metrics Plugin. Читайте Анвара в ТГ

Благодарности:

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

Олег Рассолов — Администратор Jira в hh.ru
Спасибо за техническую проработку инструмента и участие в его реализации.

Максим Фролов — Head of Product Operations в hh.ru
Спасибо за review статьи и доведение Continuous Improvement процесса до ума.
Макс читает много умных книг и пишет о них на своём канале ShitBooks 

Юлия Сметанина — Lead Delivery Manager в hh.ru
Спасибо за идею и ТЗ на автоматизацию, активное участие в тестировании.

Анастасия Айден (ex Арсеньева) — Project Manager в Т-Банк
Главный редактор статьи

Елизавета Абугова — стилистический редактор статьи

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