История проваленного спринта, или Кастомизация Jira за две недели и один день

Aleksandr Kostarev
Xsolla Tech Blog
Published in
8 min readMar 16, 2021

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

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

Планнинг. Начало

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

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

Пример кастомных полей для бизнес-девелоперов

Для привязки задачи Jira к соответствующим компаниям-партнерам и рекламным кампаниям нужны новые кастомные поля в виде списка. Значения должны подтягиваться из внешней базы MySQL. Штатные возможности Jira не позволяют сделать это из коробки, но может помочь плагин ScriptRunner. Он расширяет наши возможности, плюс уверенности добавляет найденный похожий кейс в документации от Adaptavist — компании-разработчика плагина. Кажется, что нужно только адаптировать все это под текущие задачи. Создание кастомной кнопки — несколько кликов в админке, неопределенность компенсируется двухнедельной длиной спринта. Берем цель в спринт и вроде как можем даже не заморачиваться дополнительными исследованиями. “Изи” (с)

Пример реализации списка в документации Adaptavist с помощью ScriptRunner

Первые звоночки

Непредвиденные сложности не заставили себя долго ждать. Настройка кастомных полей выполняется в админке Jira. Для получения доступа нужно создать тикет и согласовать это с командой админов — в итоге мы смогли продолжить работу только на следующий день, во вторник. Еще день ушел на обсуждение подключения к базе MySQL — изначально предполагалось, что мы сделаем прямое подключение при помощи стандартного коннектора в Jira, но в процессе обсуждения решили, что для наших целей лучше использовать развернутый для внутренних сервисов GraphQL-endpoint. Это позволит нам инкапсулировать данные и в перспективе не зависеть ни от версий сервера Jira, коннектора или драйвера, с которыми уже довелось изрядно повозиться ранее, ни от типа самой базы данных. Вдобавок исчезает лишняя зависимость от команды админов при апгрейдах версий, а это тоже большой плюс.

Используемая связка — локальный сервер Jira и данные через GraphQL-эндпоинт

Технически более правильный вариант подключения тут же потянул за собой вопрос авторизации на GraphQL-endpoint. Как решить его теоретически, мы определились почти сразу: будем формировать свой JWT-токен. Осталось понять, как это сделать технически в Jira. Уже вечер среды, а мы все еще занимаемся обсуждением и исследованием. Впервые кажется, что что-то идет не так, но впереди еще полторы недели спринта. Есть надежда наверстать, пусть даже придется немного поработать сверхурочно.

Закапываемся все глубже

Сложности продолжают нарастать. Оказалось, что из скрипта, который привязывается к кастомной кнопке в ScriptRunner, нельзя слать запросы на наш endpoint напрямую (anti-XSS measures). Нужен проксирующий REST-endpoint, который настраивается в плагине отдельно. Плюс скрипты тут пишутся на Groovy, который хоть и не является каким-то сложным языком, но никогда не использовался в нашем стеке технологий. Как объявить переменные? Как подключить нужные библиотеки и собрать JWT-токен? И можно ли вообще сделать это в Jira? Как сделать запросы к endpoint и передать параметры, которые могут зависеть от значений в других полях? Ответы на все эти вопросы также потребовали дополнительного времени. Уже вечер пятницы, а мы все еще пытаемся при помощи библиотеки java-jwt собрать в Jira этот злосчастный токен, который никак не хочет авторизовываться на нашем endpoint. К слову, вечер субботы также прошел за попыткой сделать это уже в качестве эксперимента при помощи другой библиотеки — node-jws. И тоже безрезультатно.

Формируем токен в ScriptRunner

Авторизация была побеждена только в понедельник утром на второй неделе спринта. Но почти сразу обозначилась новая проблема. В кастомных полях нам нужен SingleSelect — только одно значение, сохраняющееся в поле. При выборе любого другого новое значение должно заменять собой уже существующее. То есть к одной задаче Jira нужно привязывать только одного партнера или одну рекламную кампанию. В примере документации от Adaptavist, напротив, можно выбирать сразу несколько значений в кастомное поле (MultiSelect). Наиболее логичным показался выбор метода .convertToSingleSelect(), но после нескольких безуспешных попыток его применить выяснилось, что работает он каким-то совершенно другим способом и вообще не умеет делать запросы к endpoint, в отличие от похожего по названию .convertToMultiSelect(). Еще полдня потеряно в попытках найти другой метод, способ, тип кастомного поля или что-то еще. Единственным выходом оказалась проверка скриптом кастомного поля на наличие в нем какого-либо существующего значения и его предварительный сброс. Получилось не очень эстетично, но работоспособно. Уфф…. Но на дворе уже вторник второй недели спринта!

Призрачные надежды

Казалось, что к этому моменту решены основные технические проблемы и мы еще успеваем отдать все на тестирование, задокументировать и подготовиться к спринт-ревью. Описываем реализованное решение в Confluence, а тестировщики с готовностью берутся за дело… Среда! Выясняется, что сортировка полей работает неверно! Точнее, данные подтягиваются правильно, но только один раз, при открытии формы. В дальнейшем, если попытаться изменить выбранное значение в поле, то нового вызова endpoint и получения новой порции данных не происходит — отображаются те, что уже есть. Еще день уходит на исследования и выяснения неочевидных особенностей работы скриптов в ScriptRunner.

Настройка поведения кастомного поля в ScriptRunner

Четверг. Повторное получение данных при обновлении значения в полях реализовано, но появившаяся было радость оказывается преждевременной… Мы так увлеклись запросами к GraphQL и так обрадовались, когда с ним все получилось, что совсем не обратили внимания на сложную логику последнего этапа задачи. Здесь, в зависимости от выбора значений в одних полях, в других должен был изменяться список выбора! Например, список Interactions (взаимодействий с партнерами посредством электронных писем, звонков, встреч) должен фильтроваться в зависимости от значений в полях Closing reason, Date of Interaction, Interaction source. Или если в Closing reason выбрано via email, то в поле Interaction нужно подгрузить письма. А если выбрано Other — see comments, то поле Closing reason comment должно стать обязательным.

Звучит просто, но на деле все многократно усложняется тем, что во всех скриптах, привязанных к кастомным полям, нужно учесть эту логику, а также особенности поведения скриптов в ScriptRunner. Становится совсем грустно… Уже почти всем понятно, что к спринт-ревью, запланированному на 14:45 пятницы последней недели спринта, мы не успеваем — слишком много еще неопределенности и совершенно непонятно, как все разрулить. Из последних сил пытаемся найти решение втроем. Вечер четверга. Утро пятницы. День… Не работает. Спринт-ревью. Все. Не успели… Угрюмо расходимся на выходные.

Экран закрытия задачи со сложной логикой кастомных полей

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

Рефлексия и выводы

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

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

Что можно было сделать, чтобы этого избежать?

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

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

--

--