Когда я устраивалась в Skyeng, солнце светило чуть ярче, трава зеленее не была (шла такая же ранняя весна), а тимлид попросил записывать в Jira, сколько времени ушло на кодинг, а сколько на разговоры и ревью. Хотя бы раз в две недели.


«По этим данным мы пробуем понять, надо ли корректировать эстимейты и нет ли проблем в коммуникации в команде», — говорили они. А вот кто такой «бабайка», так и не рассказали..

Поскольку мы все удалёнщики, идея звучала разумно. Да и мне стало интересно, куда девались эти восемь часов: вот прошли, но за чем именно? Однако логировать было непривычно. И вообще лень. Тогда я решила поискать что-нибудь, что будет вести ворклоги за меня. А в процессе исследования немного увлеклась и написала свой плагин для IntelliJ IDEA.

Ниже вы найдете субъективный обзор готовых инструментов и мой велосипед (с исходниками).

Изучаю решения, которые использует команда


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

1. Clockify — одна кнопка, чтоб править всем


Затрекать время на кодревью или написание доки? Плагин добавит кнопку (довольно ненавязчивого серого пакмена) почти куда угодно.


Так смотрится в Google Docs


И на GitHub

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


Интеграция в Jira поставляется отдельно, тоже браузерным расширением

Впрочем, есть возможность загрузить в свой рабочий календарь таблицу с ворклогами, полученную из Clockify. А для учёта времени в оффлайне есть десктопные приложения под windows, linux и mac. Мобильные приложения — тоже. Основной функционал бесплатен, но есть и несколько тарифных планов с плюшками вроде приватности, напоминалок и темплейтов для проектов.

2. Toggl, просто Toggl


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


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

3. Toggl + скрипт на Python: когда просто кнопки уже недостаточно


Изобретение моего коллеги. Как-то он решил сократить телодвижения по экспорту таблиц в Jira и переложил выгрузку налогированного в Toggl на скрипт. Можно положить в cron и радоваться жизни.

4. Wakatime — там, куда мы отправляемся, кнопки не нужны


Логирует всё сам. Поэтому первое, о чём следует вспомнить, подключая его браузерное расширение — блэклист.

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


Плагины для IDE трекают время, проведённое как в определённой git-ветке, так и в определённом файле. На скрине список доступных — весьма впечатляет. Серенькие — “в планах”, за их скорейшую реализацию можно проголосовать. Некоторые плагины в платной подписке за 9$ в месяц.

Кроме того, на дашборде можно увидеть, сколько времени в процентном соотношении потрачено на написание кода на разных языках: если вспомнить, что обычный android-проект предполагает использование Kotlin, Java, Groovy и xml — это вполне себе обретает смысл). А если поработать с установленным плагином некоторое время, можно заметить, что он довольно безжалостен: в дашборд попадает время активного чтения и написания кода. И не попадает залипание в монитор со стеклянным взглядом. Что и говорить, на любителя.

Хмм, исходный код WakaTime открыт: можно попытаться понять, как он работает...
Это уже интереснее, правда? Вот и я не могла пройти мимо и выкачала себе исходники плагина для IDE от JetBrains.

Попробуем разобраться. Основная логика находится в WakaTime.java. Там мы можем увидеть, как плагин навешивает листенеры на редактирование и скролл документов, заполняет очередь из Heartbeat (), и через WakaTime CLI отправляет её на сервер. Логика расчёта времени остаётся за кадром, но, кажется, мы и сами в состоянии написать подобное.

Пишем свой велосипед по аналогии


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

А чтобы не засыпать сервер запросами во время работы над кодом, логи мы будем отправлять при закрытии Android Studio, а до этого момента хранить локально.

1. Какие сущности мы можем встретить в исходном коде плагина (и использовать в своём)?


Не используем Сomponents (это легаси), ранее представлявшие собой основные структурные единицы плагина. Могут быть уровня приложения, уровня проекта или уровня модуля. В исходниках WakaTime есть ApplicationComponent, но им можно, исходному коду уже лет пять и он должен сохранять обратную совместимость. Компоненты имеют привязку к жизненному циклу уровня, с которым они связаны. Так, например, ApplicationComponent загружается при старте IDE. При использовании будут блокировать перезапуск плагина без перезапуска IDE и вообще ведут себя неприятно. Поэтому вместо них лучше использовать services.

Используем Services — нынешние основные структурные единицы. Делятся на те же уровни приложения, проекта и модуля, помогут нам инкапсулировать логику на соответствующих уровнях и хранить состояние плагина.

В отличие от компонентов, Services нужно загружать самостоятельно, используя метод ServiceManager.getService(). Платформа гарантирует, что каждый сервис является синглтоном.

Добавляем Actions — могут быть шорткатом, дополнительным пунктом меню — словом, отвечают за всё, что как-то затрагивает пользовательские действия в IDE.

Используем Extensions — любое расширение функциональности, которое будет сложнее Actions: например, подвязаться к жизненному циклу IDE и выполнить что-то во время показа сплэшскрина или перед выходом.

Всё это должно быть объявлено в файлике /META_INF/plugin.xml.

2. Plugin.xml и build.gradle



Окно создания проекта. Мы будем писать наш плагин на Kotlin, а для сборки использовать Gradle

Это — первое, что мы видим после создания заготовки плагина в IDEA (File -> New -> Project… -> Gradle -> IntelliJ Platform Plugin). Он содержит информацию о зависимостях плагина и его краткое описание. В нём должны быть объявлены компоненты плагина — ранее упомянутые components, services, actions и extensions. Никакой определённой точки входа не будет — ею станет пользовательское действие или событие жизненного цикла IDE, в зависимости от наших потребностей.

Нам понадобятся вот эти зависимости — ведь мы хотим написать плагин под студию и чтобы он умел работать с Git.

<depends>Git4Idea</depends>
<depends>com.intellij.modules.androidstudio</depends>

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

Поскольку мы хотим ещё и того, чтобы плагин умел работать с Jira, подключим через Gradle любезно предоставленную библиотеку с rest-клиентом. Для этого добавим туда maven-репозиторий Atlassian:

repositories {
   mavenCentral()
   maven {
       url "https://packages.atlassian.com/maven/repository/public"
   }
}

И собственно библиотеки:

implementation "joda-time:joda-time:2.10.4"
implementation("com.atlassian.jira:jira-rest-java-client-core:4.0.0") {
   exclude group: 'org.slf4j'
   dependencies {
       implementation "com.atlassian.fugue:fugue:2.6.1"
   }
}

Здесь мы определяем интересующую нас версию IDE и путь к ней (о, если бы настоящая Android Studio запускалась и работала так же шустро, как её облегчённая версия для отладки плагинов):

intellij {
   version '2019.1'
   plugins 'git4idea'
   alternativeIdePath 'E:\\Android Studio'
}

А здесь, возможно, когда-нибудь напишем о новом, улучшенном функционале. Но не сейчас.

patchPluginXml {
   changeNotes """
     Add change notes here.<br>
     <em>most HTML tags may be used</em>"""
}

3. Создание UI


Чтобы что-то пушить в Jira, для начала нужно залогиниться. Логично сделать это прямо после открытия проекта. Похоже, нам нужен extension, и мы, не колеблясь, объявим его в plugin.xml:

<postStartupActivity implementation="Heartbeat"/>

и в нём покажем диалог.

UI-компоненты — это главным образом компоненты из Swing с некоторыми расширениями из platform sdk. Всё это с недавних пор обёрнуто в kotlin ui dsl. На момент написания в документации отмечено, что dsl может очень сильно меняться между мажорными версиями, поэтому используем его с лёгким опасением:


override fun createCenterPanel(): JComponent? {

   title = "Jira credentials"
   setOKButtonText("Save")

   return panel {
       row {
           JLabel("Jira hostname")()
           hostname = JTextField("https://")
           hostname()
       }
       row {
           JLabel("Username:")()
           username = JTextField()
           username()
       }
       row {
           JLabel("Password:")()
           password = JPasswordField()
           password()
       }
   }
}

Создали диалог, показали его, получили учётные данные от Jira. Теперь надо их сохранить, и желательно — безопасно, насколько это возможно. На помощь в этом придёт PersistingStateComponent.

4. Устройство PersistingStateComponent (не очень сложное)


PersistingStateComponent умеет делать две вещи — saveState и loadState: сериализовывать и сохранять переданный ему объект и извлекать его из хранилища.

Куда именно он должен сохранять данные, он узнаёт из аннотации State. В документации упомянуты два самых простых варианта — указать свой файл или хранить всё в настройках воркспейса:

@Storage("yourName.xml")
@Storage(StoragePathMacros.WORKSPACE_FILE) 
@State(
   name = "JiraSettings",
   storages = [
       Storage("trackerSettings.xml")
   ])

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

override fun getState(): Credentials? {
   if (credentials == null)  {
       val credentialAttributes = createCredentialAttributes()
       credentials = PasswordSafe.instance.get(credentialAttributes)
   }
   return credentials
}
override fun loadState(state: Credentials) {
   credentials = state
   val credentialAttributes = createCredentialAttributes()
   PasswordSafe.instance.set(credentialAttributes, credentials)
}

Ещё один PersistingStateComponent будет заниматься хранением времени, залогированного для разных веток.

С логином в Jira разобрались. С хранением пользовательских данных — тоже. Теперь нужно как-то отследить событие чекаута на ветку, чтобы начать отсчёт времени.

5. Подписываемся на ивенты


IntelliJ Platform предоставляет возможность повесить Observer на интересующие нас события, подписавшись на их топики в messageBus нужного уровня. Вот дока, она довольно интересная.

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

Например, мне нужно слушать чекаут в Git…

subscribeToProjectTopic(project, GitRepository.GIT_REPO_CHANGE) {
    GitRepositoryChangeListener {
        currentBranch = it.currentBranch
    }
}

… закрытие приложения (чтобы радостно залогировать время)…

subscribeToAppTopic(AppLifecycleListener.TOPIC) {
    object: AppLifecycleListener {
        override fun appWillBeClosed(isRestart: Boolean) {
            if (!isRestart) {
                JiraClient.logTime(entry.key, DateTime.now(), entry.value)
            }
        }
    }
}

… и сохранение документов (просто так, чтобы было).

subscribeToAppTopic(AppTopics.FILE_DOCUMENT_SYNC) {
    CustomSaveListener()
}

Этого, в принципе, уже достаточно (если нет, то всегда можно написать свой). Но что, если мы захотим слышать малейший скролл и движение мыши?

6. EditorEventMulticaster


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

Теперь у нас есть всё необходимое. Мы можем выделить из текущей ветки имя задачи в Jira (если оно там есть), посчитать время, проведённое в работе над ней, и при выходе из студии незамедлительно дописать его в задачу.

7. Код и подробности здесь


Для запуска нужна актуальная Android Studio (3.6.1).
Для проверки работоспособности — git и ветка с именем, хотя бы отдалённо похожим на таск в Jira.
Чтобы плагин не просто выводил время в консоль, а трекал его — раскомментить соответствующую строчку в Heartbeat.kt.

8. Полезные ссылки



P.S. А что выбрала в итоге?


— Блин, оно какое-то слишком злое. А если я не пишу код? А если я ковыряю в носу и напряжённо пытаюсь понять, что в нём происходит? В коде, в смысле. Ведь на это и уходит основное время.

— Тогда хотя бы скролль его.

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

— Определённо не надо. Тогда нужен другой повод, а то мне понравилось писать плагины. И с VCS я ещё толком не разбиралась.

— А ты сама им пользуешься?

— Не-а. Я как-то руками приучилась логировать, незаметно для себя.