Я Саша Хрущев, технический директор IT-компании WINFOX. Рассказываю о своем опыте освоения скриптинга в YouTrack и о том, как при помощи этого можно делать крутые отчеты.

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

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

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

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

Сначала создаем рабочий процесс.

Рабочий процесс для автоматического определения просрочки задач
Рабочий процесс для автоматического определения просрочки задач

А после создаем в этом процессе модуль, который срабатывает по таймеру. Для этого нам нужен вот такой код:

var entities = require('@jetbrains/youtrack-scripting-api/entities');
var workflow = require('@jetbrains/youtrack-scripting-api/workflow');
var dateTime = require('@jetbrains/youtrack-scripting-api/date-time');
var http = require('@jetbrains/youtrack-scripting-api/http');

exports.rule = entities.Issue.onSchedule({
  title: workflow.i18n('Notify assignee about overdue issues'),
  search: '#Unresolved has: {Due Date}',
  cron: '* 0/1 * ? * * *',
  guard: function(ctx) {
    return ctx.issue.fields.DueDate < Date.now();
  },
  action: function(ctx) {
    var issue = ctx.issue;
    
    if(!issue.hasTag('Просрочена')){
      
        issue.addTag('Просрочена');

        var formattedDate = dateTime.format(issue.fields.DueDate);
        var notificationText = workflow.i18n('Issue became overdue on <i>{0}</i>:', formattedDate) +
          ' <a href="' + issue.url + '">' + issue.summary + '</a><p style="color: gray;font-size: 12px;margin-top: 1em;border-top: 1px solid #D4D5D6">' +
          workflow.i18n('Sincerely yours, Angry Fox') + '</p>';
      
        // если вдруг решили добавить информирование в дискорде через вебхук, мы побаловались и отключили
        var connection = new http.Connection('https://discord.com/api/webhooks/********/***************');
        connection.addHeader('Content-Type', 'application/json');
        var content = 
        {
            "username": "AngryFox",
            "avatar_url": "https://i.imgur.com/4M34hi2.png",
            "content": notificationText
        };
        var response = connection.postSync('', [], JSON.stringify(content));
        if (response && response.code === 200) {
          issue.addComment('О просрочке задачи доложено в соответствующие инстанции');
        }
        else
        {
          issue.addComment('Ошибка информирования о просрочке: '+response.code);
        }
    }
  },
  requirements: {
    DueDate: {
      type: entities.Field.dateType,
      name: "Срок"
    },
    Assignee: {
      type: entities.User.fieldType
    }
  }
});

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

Мы попробовали добавить информирование в Discord. Жутко неудобно и надоедает, но для примера в коде этот кусок оставили.

Размяли лапки на простейшем скрипте, а теперь что-то посерьезней )

Аналитика

Представим, что нам нужно настроить сложную аналитику, например, посчитать рентабельность проектов или отдельных разработчиков, оценить результативность команды. Что нам мешает сделать это стандартными средствами? 

Дело в том, что самое слабое место встроенной аналитики в YouTrack — переходы по состояниям. Вот для примера стандартный флоу работы с задачей, принятый у нас в команде:

  1. Регистрируем задачу в статусе «Зарегистрирована».

  2. Оцениваем задачу (Estimate) и ставим дату исполнения согласно проектному плану.

  3. Задача переходит на исполнителя, мы меняем статус на «Открыта», а после корректируем его исходя из рабочего процесса. Например, задача падает на джуна и Estimate увеличивается. Или план поехал, тогда меняется DueDate.

  4. Исполнитель переводит задачу в статус «В работе».

  5. Исполнитель трекает время в задаче.

  6. Исполнитель решает задачу и переводит ее в статус «Решена».

  7. После сборки исполнителем назначается тестировщик и задача переходит в статус «Передано в тестирование».

  8. Тестировщик проводит тесты и трекает затраченное время в задаче.

  9. Если всплывают проблемы по части функциональности, задача переходит в статус «Открыта повторно».

  10. Переходим к пункту 4 и повторяем все до победного.

Стандартные средства, то есть отчеты YouTrack, не показывают нам всю картину ни по затраченному времени, ни по качеству, ни по производительности команды разработки и тестирования. Ну, по крайней мере этого не могут версии YouTrack, которые стоят у нас — 2020-й год с каким-то апдейтом в 2021-м. Поговаривают, что в версии 2023 года все точно так же. 

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

Чтобы решить проблему, мы ввели новый рабочий процесс Register Youtrack Actions. По факту мы просто добавили логгер всех изменений задач через REST в дополнительную базу данных.

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

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

Рабочий для аналитики
Рабочий для аналитики

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

var entities = require('@jetbrains/youtrack-scripting-api/entities');
var workflow = require('@jetbrains/youtrack-scripting-api/workflow');
var dateTime = require('@jetbrains/youtrack-scripting-api/date-time');
var http = require('@jetbrains/youtrack-scripting-api/http');

exports.rule = entities.Issue.onChange({
  // TODO: give the rule a human-readable title
  title: 'Register_action',
  guard: function(ctx) {
    // TODO specify the conditions for executing the rule
    return true;
  },
  action: function(ctx) {
    var issue = ctx.issue;
    // TODO: specify what to do when a change is applied to an issue
    if(issue.id == 'Черновик' || issue.id == 'Issue.Draft'){//пока задача заводится - у нее миллион ревизий с ID "черновик"
      console.log('No need to be logged');
      return;
    }


    var content = {};
    content.created = issue.created;
    content.description = issue.description;
    content.project = {id: issue.project.key, name: issue.project.name};
    content.issue_id = issue.id;
    content.reporter = {login: issue.reporter.login, name: issue.reporter.fullName};
    if(issue.fields.Assignee) // исполнителя может и не быть
    	content.assignee = {login: issue.fields.Assignee.login, name: issue.fields.Assignee.fullName};
    content.resolved = issue.resolved;
    content.summary = issue.summary;
    content.updated = issue.updated;
    content.url = issue.url;
    if(issue.fields["Due Date"]) // даты может и не быть
    	content.due_date = issue.fields["Due Date"];
    if(issue.fields.Estimation){ //Estimation - объект Joda time, напрямую в JS нельзя использовать
        var period = issue.fields.Estimation;
        var minutes = !period ? 0 : (period.getMinutes() +
                             60 * (period.getHours() +
                                   8 * period.getDays() + 40* period.getWeeks()));
        content.estimation = minutes;
    }
    content.issue_type = {name:issue.fields.Type.name, description:issue.fields.Type.description};
    content.state = {name:issue.fields.State.name, description:issue.fields.State.description};
    content.workitems = [];
    issue.workItems.forEach(function(dep) {
  		content.workitems.push({author: {login: dep.author.login, name: dep.author.fullName}, created:dep.created, date: dep.date, description: dep.description, duration: dep.duration});
    });
    content.comments = [];
    issue.comments.forEach(function(dep) {
  		content.workitems.push({author: {login: dep.author.login, name: dep.author.fullName}, created:dep.created, text: dep.text});
    });

    // отправляем получившийся объект в REST
    const connection = new http.Connection('https://xx.xx.xx.xx/registerYoutrackAction');
    connection.addHeader('Content-Type', 'application/json');
    connection.addHeader('Authorization', 'Bearer: xxxxxxxxxxxx');
       
    const response = connection.postSync('', [], JSON.stringify(content));
    if (response && response.code === 200) {
      console.log('Change logged');
    }
    else
    {
      issue.addComment('Ошибка информирования об изменениях: '+response.code);
    }
  },
  requirements: {
    // TODO: add requirements
  }
});

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

Поле

Тип

Описание

ID

String

Идентификатор ревизии, генерится при регистрации

ISSUE_ID

String

Идентификатор задачи в YouTrack

PROJECT

String

Идентификатор проекта

TITLE

String

Заголовок (summary) задачи

DESCRIPTION

String

Описание задачи

REPORTER

String

Ссылка на автора задачи

ASSIGNEE

String

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

LINK

String

Ссылка на задачу в трекере

CREATED

Unixtime

Дата создания задачи

UPDATED

Unixtime

Дата ревизии задачи

DUE_DATE

Unixtime

Due date, плановая дата исполнения

ESTIMATION

Int

Плановая оценка в минутах

TYPE

String

Тип задачи (Bug/Task)

STATE

String

Статус задачи

WORKITEMS

Array

Записи о затреканном времени (автор, длительность, дата)

COMMENTS

Array

Записи о комментариях (автор, содержимое, дата)

Выглядит это примерно так:

Ревизии
Ревизии
Ревизии
Ревизии

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

Написав нехитрый скрипт аналитики, мы получили краткую сводку по каждой задаче.

Поле

Тип

Описание

Идентификатор задачи

String

Идентификатор задачи в YouTrack

Дата обновления

Unixtime

Дата последней ревизии задачи

Первый исполнитель

String

Разработчик, на которого первый раз была назначена задача

Дата постановки

Unixtime

Дата первого назначения на разработчика

Первый трекер

String

Первый разработчик, затрекавший время на задачу

Дата/время первого трека

Unixtime

Дата первого трекинга разработчиком

Дата возврата задачи

Unixtime

Дата, когда задача была первый раз переоткрыта (Reopen) = точка с которой время становится бесполезным

Кому была возвращена задача

String

На кого задача была переоткрыта

Последний трекер

String

Разработчик, последним трекавший время на задачу

Дата последнего трека

Unixtime

Дата последнего трекинга разработчиком

Тип задачи

String

Bug/Task

Изначальная оценка

Float

Первая оценка (в задачу она приходит из сметы и первоначального плана)

Трекинг времени

Array

Последняя информация по трекингу времени

Дата решения задачи

Unixtime

Время, когда задача или баг перешли в статус Verified

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

Промежуточная таблица
Промежуточная таблица

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

Вариантов и флуктуаций — масса, но в целом такие данные позволяют строить более-менее адекватную аналитику.

На основе этих данных мы можем формировать два нужных отчета: отчет план/факт и отчет по разработчикам.

Отчет план-факт

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

Чтобы получать такие отчеты, мы разделили все переходы на «хорошие» и «плохие». Пока задача идет по «хорошим» переходам, все затраченное на нее время считается полезным. Если хотя бы один переход задачи идет по «плохому» пути, это означает, что в задаче что-то пошло не так в сравнении с идеальным вариантом, и мы должны учитывать все оставшееся время как бесполезное. Баги —  изначально бесполезное время.

Вот как выглядит отчет план-факт.

Отчет план-факт
Отчет план-факт

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

Боже, почему мы не сделали этого раньше?

Отчет по разработчикам

Этот детальный отчет с разбивкой по разработчикам.  

Нас интересуют следующие показатели:

  • сколько задач было в работе;

  • сколько задач закрыто;

  • полезные часы;

  • бесполезные часы;

  • сколько задач вернулось к разработчику;

  • сколько багов было заведено;

  • сколько багов было закрыто;

  • сколько полезных часов подтверждено тестировщиками;

  • сколько бесполезных часов подтверждено тестировщиками;

  • соотношение продуктивного и непродуктивного времени.

Отчет по разработчикам
Отчет по разработчикам

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

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

У внимательного читателя наверняка появились вопросы к таблице. Почему есть разработчики с 0% бесполезного времени? Почему у кого-то проверено больше часов, чем отработано? Дело вот в чем.

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

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

Что дальше

Мы написали и другие полезные скрипты для YouTrack. Вот несколько задач, которые они решают:

  • Невозможность трекать время в задачу без Estimation.

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

  • Напоминания о забытых задачах.

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

Пишите в комментариях, если вам интересно узнать про скриптинг в YouTrack что-то еще — я постараюсь поделиться опытом. Ну и рассказывайте, как вы допиливаете трекер, чтобы решать свои задачи.

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


  1. Karbofoss
    28.07.2023 10:13

    Судя по тому, как быстро джетбрейнс слили россиян, вкладывать время в YouTrack - рисковая затея


    1. winfox_tech Автор
      28.07.2023 10:13

      есть такое, но есть пожизненная standalone лицензия и перемещаться куда-то чревато космическими затратами


  1. serg-mizun
    28.07.2023 10:13

    Скажите пожалуйста, есть там возможность автоматически (по rest, например) сформировать задачи и автоматически же отмечать их выполнение? Например, каждый понедельник должна выполниться задача task1, скрипт (или программа), который ее выполняет, проставляет код завершения. Админу остается только посмотреть, что все запланированные задачи успешно выполнены.


    1. winfox_tech Автор
      28.07.2023 10:13

      Да, у Ютрека есть свой API, все манипуляции с задачами возможные через него. Документация по апишке тут https://www.jetbrains.com/help/youtrack/devportal/youtrack-rest-api.html


      1. serg-mizun
        28.07.2023 10:13

        Спасибо!