Введение


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

— Отслеживание состояния задач
— Группировка задач в трекере
— Внутрипроектное обсуждение при необходимости
— Ведение документации (хоть и возможности весьма ограничены)
— Учет времени сотрудников и видов их деятельности

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

Задача


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

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

Подбор решения


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

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

Redmine предоставляет возможность получить данные с помощью своего API.

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

С чего начать?


Для начала необходимо подключиться из таблицы к API Redmine. Для этого проделываем ряд нехитрых действий:
1. Создаем новый скрипт внутри нашей таблицы (Инструменты -> Редактор скриптов…)
image

2. В открывшемся окне со сккриптом нужно убрать созданную функцию — мы будем писать свои.

Для начала нам потребуется следующая информация: ссылка, по которой доступен Redmine и API-key. С первым проблем не должно возникнуть — если вы пользуетесь баг-трекером, то точно знаете по какому адресу доступна стартовая страница (у нас, например, это redmine.greensight.ru). С ключом чуть сложнее. Достать его можно, отправив какой-нибудь запрос к API. Открываем FireFox, и пытаемся, например, получить список проектов. Для этого в адресной строке вводим <ссылка на редмайн>/projects.xml:
image

Вводим логин и пароль пользователя, зарегистрированного в системе. Далее в панели разработчика переходим во вкладку “Сеть” подраздел “Заголовки запроса”. То, что написано в поле “Authorization” после слова “Basic” и будет нужным нам ключом.
image

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

var baseUrl = "<ссылка в Redmine>";
var key = "<API-key>";

function APIRequest (reqUrl) {
  var url = encodeURI(baseUrl + reqUrl);
  var payload = { 'Authorization' : 'Basic ' + key};
  
  var opt = {
    'method' : 'GET',
    'headers' : payload,
    'muteHttpExceptions' : true
  };
  
  var response = UrlFetchApp.fetch(url, opt);
  return XmlService.parse(response.getContentText()).getRootElement();
}

function getRedmineProjects () {
  var response = APIRequest (‘projects.xml’);
  Logger.log(response);
}

Здесь необходимо заменить <ссылка в Redmine> и <API-key> на то, что вы получили выше. Теперь немного поподробнее о том, что мы сделали.

Переменная baseURL хранит в себе ссылку на главную страницу Redmine. Она нужна, чтобы в дальнейшем лишний раз не прописывать ее в функциях вызова методов API. Соответственно, переменная key нужна для того, чтобы Redmine предоставлял данные в ответ на запрос (подробнее можно почитать любую статью по Basic Authorization). Функция APIRequest осуществляет запрос к Redmine и возвращает ответ от него. opt — это объект, который хранит в себе параметры обращения к API — метод GET, заголовки, содержащие в себе данные о пользователе, который осуществляет запрос, игнорирование исключений.

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

Redmine умеет возвращать ответы на запросы в двух различных формах — xml и JSON. в данном варианте я пользовался xml-представлением ответа, поэтому в функции возврата ответа на запрос находится XmlService.parse(). Функция getRedmineProjects получает список проектов и выводит их в лог исполнения скрипта (чтобы выполнить скрипт нужно выбрать в меню Выполнить -> getRedmineProjects, а чтобы посмотреть результаты нажать Ctrl+Enter).

Собираем данные


Для сбора данных осталось только обратиться к результатам выполнения функции APIRequest и правильно взять из них данные. Я покажу на примере одного xml-файла как снимать данные. Механизм сбора для остальных аналогичный, а список доступных файлов описан по ссылке выше. Пусть это будет одна из метрик, которая будет полезна всем — списанное время (эта метрика взята для примера, чтобы показать как доставать данные, у нас, конечно же, более сложные метрики на проектах). Для этого будем брать данные из файла time_entries.xml. Их можно отфильтровать средствами Redmine, например, параметр spent_on отвечает за время, списанное в определенную дату или промежуток, а project_id — за время, списанное в определенный проект (более подробное описание есть в документации к API). Посмотрим что из себя представляет этот файл:
<time_entries type="array" total_count="11" limit="25" offset="0">
	<time_entry>
		<id>11510</id>
		<project name="Тестовый проект" id="150"/>
		<issue id="7666"/>
		<user name="Чудесный Разработчик" id="62"/>
		<activity name="Разработка" id="9"/>
		<hours>1.5</hours>
		<comments/>
		<spent_on>2014-06-10</spent_on>
		<created_on>2014-06-09T20:23:34Z</created_on>
		<updated_on>2014-06-09T20:23:34Z</updated_on>
	</time_entry>
	<time_entry>
		<id>11520</id>
		<project name="Боевой проект" id="87"/>
		<issue id="7484"/>
		<user name="Отличный Верстальщик" id="23"/>
		<activity name="Верстка" id="9"/>
		<hours>7.5</hours>
		<comments/>
		<spent_on>2014-06-10</spent_on>
		<created_on>2014-06-10T07:57:09Z</created_on>
		<updated_on>2014-06-10T07:57:09Z</updated_on>
	</time_entry>

Родительский узел показывает информацию, которая нам так же потребуется при обработке информации: total_count — сколько найдено записей по данному запросу, limit — количество записей на странице, offset — отступ от начала списка. Параметры limit и offset можно так же регулировать в самом запросе. Например, стоит сделать limit побольше, чтобы было меньше запросов при сборе данных.

Как мы видим, файл обладает очень четкой структурой. Приступим к его обработке. Допустим, нам нужно написать функцию, которая собирает все время, списанное за сегодня на определенном проекте и посчитать сколько было потрачено на управление и разработку:
function getTimes(id, params) {
  var time = new Array();
  var offset = 0;
  
  do {
    var response = APIRequest ("time_entries.xml?project_id=" + id + "&spent_on=><" + params + "&offset="+offset + "&limit=100");
    
    for (i=0; i<response.getChildren('time_entry').length; i++) {
      var obj = new Array();
    obj.push(response.getChildren('time_entry')[i].getChild('activity').getAttribute('name').getValue());
      obj.push(response.getChildren('time_entry')[i].getChild('hours').getText());
      time.push(obj);
    }
    offset += 100;
  } while (response.getChildren('time_entry').length != 0);
  
  var manage = 0;
  var dev = 0;
    
  for (i=0; i<time.length; i++) {
    if (time[i][0] == 'Управление') {
      manage += +time[i][1];
    }
    else {
      dev += +time[i][1];
    }
  }
  return (manage/(manage+dev));
}

Внутри цикла мы каждый раз достаем информацию о времени, списанном в проекте с id = id (посмотреть id проекта можно так же через API запросом к projects.xml), потраченным во время params. Каждый запрос к Redmine получает 100 записей и, если этого недостаточно, двигаемся на offset.

Далее, в цикле, мы выполняем response.getChildren('time_entry')[i].getChild('activity').getAttribute('name').getValue(). Эта команда получает название активности из каждой записи списанного времени:


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

Для получения самих значений используем response.getChildren('time_entry')[i].getChild('hours').getText(). По аналогии с командой выше мы получаем списанное время:


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

Теперь нам осталось только организовать вывод посчитанных показателей. Для этого необходимо написать небольшую функцию, которая вставляет полученное значение в нужные ячейки нашей таблицы:
function getAllTimes() {
  var value;  
  SpreadsheetApp.getActive().setActiveSheet(SpreadsheetApp.getActive().getSheetByName('Project'));
  var data = SpreadsheetApp.getActive().getDataRange().getValues();
  
  for (k=0; k<data.length; k++) {
    value = 0;
    
    if ((data[k][2].toLowerCase()=='активный')&&(data[k][1].toString()!='')) {
      value = getTimes(data[k][1], data[3][9]);
      SpreadsheetApp.getActive().setActiveSelection("K" + (k+1)).setValue(value);
    }
  }
}

Чтобы понять это необходимо немного пояснений. Список проектов у нас хранится на листе с названием “Project”. Там же в разных колонках записываются метрики. Временной промежуток, по которому фильтруется время записан так же в отдельной ячейке. В переменную data считывается вся информация из таблицы. Цикл далее находит все проекты в статусе “Активный” с присутствующим id проекта из Redmine, вызывает функцию подсчета определенной метрики для этого проета и записывает результат в столбец K (в данном случае).

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

Вот такой вот способ автоматизировать снятие метрик для проекта. ��ользуясь возможностями API Redmine, можно получать в автоматическом режиме сколь угодно сложные данные и не тратить время на ручной расчет.

Грабли


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

1. Ограничение на вывод количества записей по запросу — не более 100 штук. Даже если вы прописываете в запросе limit=500, выведется все равно не более 100.

2. Для того, чтобы вывести задачи во всех статусах, нужно добавлять в запрос “status_id=*”, иначе в выборку не будут попадать задачи в статусах “Закрыта”, “Отменена” и т.п.

3. Можно попытаться сделать пользовательскую функцию и потом вставлять ее в ячейку таблицы так же, как мы пользуемся любыми другими формулами в таблице (например, “=gettimes(id, params)”). Этот способ будет давать сбой при большом количестве данных и вместо того, чтобы увидеть данные по проектам, вы
будете видеть в ячейках ERROR.

4. Не рекомендуется выводить данные через Logger в консоль. Возникает похожая проблема с п.3 — не до конца выполняется скрипт и в итоге мы имеем не полную картину.

5. Очень внимательно следите за данными, которые могут всплыть (в основном, это в пользовательских полях) — если оно будет отсутствовать, то попытка чтения такого поля или свойства в записи через getChild() приведет к выдачи объекта null и дальнейшая работа с ним будет невозможна.