*Избушка, Olga Kolopetko. https://illustrators.ru/illustrations/1474142)*
Избушка, Olga Kolopetko. https://illustrators.ru/illustrations/1474142


Повсеместная цифровизация не только в телевизоре. Она теперь повсюду нас окружает, на работе и не только. Типичным представителем являются трекеры действий (системы Сервис Деск, проектные системы, документообороты и пр.). Общей болевой точкой всех этих систем являются сложная объектная и процессная модель и фокус на поддержку операционного обслуживания. Шаг влево или вправо в попытках понять всю картину целиком повергает аналитиков в уныние и порождает безуспешные проекты на многие месяцы. А вопрос этот висит в воздухе, в том или ином виде, почти ежедневно.


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


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


Все предыдущие публикации.


Постановка задачи


Возьмем простейшую задачку.


Есть проект, в нем несколько потоков (бэк, фронт, аналитика, ...). Каждый спринт необходимо формировать закрывающую сводку по потраченному времени в разрезе разработчиков, задач и потоков.


Как будем решать?


Важно понимать, что все трекинговые системы построены примерно идентично. База с кучей перелинкованых табличек. Есть отдельные аккумулирующие счетчики, актуальные на «здесь и сейчас». Нет никаких полноценных сущностей, запросов на дату и пр. Надо все реконструировать самим. Примерно по такой последовательности (ключевые слова для поиска) «Project — Issue — Worklog»


При реконструкции будем использовать два принципа:


  1. Решаем поставленную задачу НИКАК не вовлекаясь в процессную философию, онтологию и пр. Наша задача — получить совпадение по показателям, все остальное — епархия консалтеров.
  2. Решаем поставленную задачу максимально простыми способами МИНИМАЛЬНО погружаясь в техническую специфику самого продукта. Только в объеме, необходимом и достаточном для исходной задачи.

Набор инструментов и материалов


Что достаем из чемоданчика на стол:



Пишем скрипт


Положим, что у нас в Jira включена basic авторизация. Дальше будем использовать ее. Для работы с REST API будем использовать новый пакет httr2, который кроссплатформенный и многопоточный.
Просто решаем задачу, не фиксируясь на диалектах и объеме кода. Tidyverse неплох.


Достаем проекты


Тут все достаточно просто. можно получить список проектов «в лоб», есть специальное API Get all projects . Получили все проекты, выбрали интересующие.


resp <- base_req %>%
  req_url_path_append("project") %>%
  req_perform() %>%
  resp_body_json() %>%
  bind_rows() %>%
  filter(stri_detect_fixed(name, "МОИ ПРОЕКТЫ")) %>%
  distinct(self, id, key, name)


Достаем нужные Issues


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


Ну и ладно. В Jira есть API для работы с поиском api/2/search. Тренируемся в web интерфейсе по проектной выборке, потом перетаскиваем в API запрос.



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


Оперировать в R или python вложенными списками нет ни малейшего желания. Для исследования и подбора магического jq преобразования пользуемся итеративно онлайн-песочницей и онлайн json редактором.


В ходе анализа находим в этом шлаке идентификатор спринта, на сей раз он оказался в поле customfield_10101. Не вдаемся в детали почему так, просто используем.



Получаем примерно такой код.


resp <- base_req %>%
  req_url_path_append("search") %>%
  req_url_query(jql = glue("project='{project_tag}'")) %>%
  req_url_query(maxResults = 1000) %>%
  req_perform()
resp_body <- resp_body_string(resp)

# убедимся, что количество записей меньше окна выдачи
jqr::jq(resp_body, '.total') %T>%
  {flog.info(glue("Project '{project_tag}' has {.} issues"))} %>%
  {assertInt(as.integer(.), upper = 1000)}

resp_body %>%
  jqr::jq('[.issues[] | . + .fields | del(.fields) | {id, key, 
   issuetype:(.issuetype.name), is_subtask:(.issuetype.subtask), created, 
   status:.status.name, summary, sprint_raw:(.customfield_10101[]? // ""), 
   progress, project_name:(.project.name)}]') %>%
  fromJSON()

Достаем списания часов


Имеем список интересующих нас задач (Issues). Теперь надо по каждой вытащить worklog и определить списания часов в интересующем интервале. В текущей постановке интересовала полная трудоемкость, поэтому постфильтрация по дате не делается.


Опять для исследований итеративно используем сэмпл ответа + JSON Editor online + jq play, получаем магические формулы преобразований.


resp <- base_req %>%
  req_url_path_append("issue") %>%
  req_url_path_append(issue_tag) %>%
  req_url_path_append("worklog") %>%
  req_url_query(maxResults = 1000) %>%
  req_perform()
resp_body <- resp_body_string(resp)

# убедимся, что количество записей меньше окна выдачи
crc_lst <- resp_body %>%
  jqr::jq('{maxResults, total, get_all:(.maxResults == .total)}') %>%
  fromJSON() %T>%
  {flog.info(glue("Issue '{issue_tag}' has {.$total} records"))}
assertTRUE(crc_lst$get_all)

resp_body %>%
  jqr::jq('[.worklogs[] | . + {author:.author.displayName} + 
  {updateAuthor:.updateAuthor.displayName}]') %>%
  fromJSON()

Постпроцессинг


Остались пустяки. Определиться с нужным концептом агрегации и требуемыми полями в выдаче, перевести в часы, провести агрегацию и сбросить в excel.



Пример полного кода


Дальнейшие модификации зависят от бизнес-требований и вашей фантазии. Единственно, не стоит превращать простой скрипт на один экран в мега-коннектор или продукт. Не стоит оно того, не стоит.


Предыдущая публикация — «Круглое катить, прямоугольное тащить. А шестигранник?»

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


  1. Arty_Fact
    25.08.2022 15:35

    А накрутили-то…
    Могли просто получить все тикеты с конкретного спринта прямо в эксель, потыкать несколько кнопочек в UI, и получить нужную табличку.


    1. i_shutov Автор
      25.08.2022 15:45
      +1

      Да, можно и с борда. Оттуда менеджеры и пытались сначала грузить в excel. И не устраивало по полной.

      Если доска настроена.

      Если спринты ведутся как надо.

      Если все задачи в спринте ровно по тем проектам, что надо.

      Если точно ясна методика разнесения часов.

      Если ясно, что делать с перетекающими из спринта в спринт задачами.

      Если...


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


      1. Arty_Fact
        25.08.2022 16:42

        Я что-то не понимаю ваши «если». Они все относятся и к вашей реализации. Просто вместо использования подходящего эндпоинта, вы берете все проекты, находите там конкретный, в конкретном выбираете все тикеты, у которых в конкретном кастомфилде указан конкретный спринт. Это все делается одним запросом, ссылку на который я прикрепил выше. Какая уж тут мега-задача?


        1. i_shutov Автор
          25.08.2022 17:25

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

          Да, Ваш ендпойнт хороший, но тут пошли сначала от проектов. В спринте много проектов, интересовали конкретные. Менеджеру удобно так думать -- прекрасно.

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

          Это АБСОЛЮТНО безразлично.
          Потянули с одной нитки -- ну ладно. Потянули с другой -- пожалуйста. Главное до конца дотянуть. Ворклоги все равно остаются. Цифровой шлак никуда не делся.

          Взята Jira, а мог быть наумен, гитлаб, битрикс, и прочее имя чему Легион.

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


          1. Arty_Fact
            25.08.2022 18:38

            Потому что эта статья — пример как работают многие люди, и мне такой подход не мил. Вместо того, чтобы открыть документацию к API, найти необходимый эндпоинт, посмотреть структуру ответа и написать требуемый запрос и код обработки, в статье предлагается ручками искать что там где находится и придумывать магические преобразования. Возможно, если бы статья была о реверс-инжиниринге приватного API, а не прекрасно задокументированного API Jira, у меня бы не возникло таких вопросов. А так мне непонятна цель этой конкретной статьи.


            1. i_shutov Автор
              25.08.2022 20:08

              Наверное, Вы архитектор и тогда это многое объясняет.
              Но непонятно, почему тогда Вы не читаете весь текст? Почти через абзац идет упоминание про документацию API. Про "магические преобразования", коими являются регулярки для json, похоже, Вы тоже не поняли. Почитайте про jq парсер, благо ссылки по тексту есть.

              Поясняю еще раз, задачи подобного класса являются проходным мусором и представляют собой определенный вид инженерной/аналитической деятельности. Подключиться к чему угодно, содрать то, что надо, чтобы на выходе получить то, что требуется.
              Выход = f(вход). И все части являются переменными, включая передаточную функцию. Просто посмотрите на множество различных групп. С подобными вопросами мучается каждый второй начинающий.

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

              Вам цель непонятна -- это не страшно. Вы пытаетесь аргументировать в терминах ценностей, которые многим ценны, как фантик от бумажки. Так не работает.
              Конкретно Jira после закрытия on-premise ветки и высказыванием всяких позиций вообще неинтересна. Разбираться до винтиков в ее API -- бездарно и бессмысленно тратить время.


            1. i_shutov Автор
              25.08.2022 20:49

              Дайте свой пример рабочего кода. Это будет хорошей аргументацией и демонстрацией. Просто комментарии "мне непонятно" писать -- дело нехитрое.


              1. Arty_Fact
                26.08.2022 16:24
                +1

                Конкретно Jira после закрытия on-premise ветки
                Ничего они не закрывали, просто смерджили DC и Server.
                Дайте свой пример рабочего кода.
                Сперва добейся, ага.
                Я использую Java API, поэтому у меня нет необходимости корячить авторизации и парсинг JSON. Быстрый и простой код, возвращающий мапу с тикетами и ворклогами:
                import com.atlassian.jira.bc.issue.search.SearchService
                import com.atlassian.jira.component.ComponentAccessor
                import com.atlassian.jira.issue.Issue
                import com.atlassian.jira.jql.parser.JqlQueryParser
                import com.atlassian.jira.web.bean.PagerFilter
                
                def projectKey = "Project Key"
                def sprintName = "\"Sprint Name\""
                
                def user = ComponentAccessor.jiraAuthenticationContext.loggedInUser
                def stringQuery = "project = ${projectKey} and sprint = ${sprintName}"
                def query = ComponentAccessor.getComponent(JqlQueryParser).parseQuery(stringQuery)
                def searchService = ComponentAccessor.getComponent(SearchService)
                
                def worklogManager = ComponentAccessor.worklogManager
                def data = searchService.searchOverrideSecurity(user, query, PagerFilter.getUnlimitedFilter()).results.collectEntries { issue ->
                    [issue,
                    worklogManager.getByIssue(issue).groupBy {it.authorObject.displayName}.collectEntries { k, v ->
                         [k, v.collect{it.timeSpent}.sum() / 60 / 60]
                     }]
                }.findAll{!it.value.isEmpty()}
                

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

                Кстати, обратите внимание, я не использую апи досок и спринтов, а просто все получаю через поиск. Это можно реализовать и в вашем коде, чтобы не получать все проекты и не парсить кастомфилды.
                PS Не знаю R, поэтому просто хочу спросить. Выглядит так, что ваш код не будет корректно обрабатывать, если тикет входит сразу в несколько спринтов.


                1. i_shutov Автор
                  26.08.2022 16:51

                  Спасибо, хороший ответ.

                  1. Про авторизацию не совсем понял, ну есть она и есть. Проблем не доставляет.

                  1. Да, в исходном коде тоже есть jql. Проекты ищутся просто для проверки. Но он не один. Их список известен, можно и вектором задать. Но всегда бывают нюансы, пусть уж будут в том виде, как они в системе хранятся.

                  2. А еще иногда консультанты так штатные модели данных раскорячат, что сразу и не поймешь что где лежит.

                  3. Как я сразу отметил, вопрос учета -- внешний вопрос. Куда и по каким правилам соотносить затраты. Ответ сошелся -- замечательно. Не сошелся -- смотрим косяки. Народ в джиру тоже не просто так пишет, а с подвыподвертом. Как только кто-то подумает, что так нельзя по процессу, так тут же найдется кейс с нарушителем.

                  4. Вы Jira, видимо, знаете неплохо, все под руками и в горячем доступе. Но джава -- редкий случай. Есть интерфейсы и все в контуре. Для других систем (особенно живущих в облаке) REST чаще всего будет выступать общим знаменателем. Ну вот джира подвернулась под руку. Это не специально.

                  5. Может будет а может не будет. Не знаю. Вопрос пользователям. Надо ассерты ставить и изучать нарушения. Потому что это потянет за собой двойной учет -- проблема более высокого порядка чем код. Для "задачи-пятиминутки" вопрос этот выходит за рамки.