Tracking working hours in ActivityWatch based on git activities - Flexiana
avatar

Krisztián Gulyás

Posted on 28th October 2024

Tracking working hours in ActivityWatch based on git activities

news-paper News | Software Development |

Efficient time reporting is the baseline of trust. Doing it manually is a frustrating and time consuming process. I’ll show you an opportunity to do it with less pain using ActivityWatch and some Babashka scripting.

Source of truth:

Who would know better what are you working on and when are you working like your version control system? When you calls git it’s always knows

  • what is your actual project
  • the actual branch name
  • changes you already made
  • Etc.

And we are only one step far to knowing it.

Introduction to ActivityWatch:

ActivityWatch is an open-source, automated time-tracking application. It’s designed to record how one’s time is spent across different digital platforms, providing an overview of how you spent your time with your different devices. ActivityWatch uses buckets and events to keep track of user activities, like AFK, windows and through some browser extensions the visited pages. When you visit the website you will find several plugins and watchers for different tools, editors.

Babashka Scripts – Empowering Clojure:

Babashka is a scripting environment for Clojure, providing a fast-starting platform for scripting in this robust language. Babashka scripts bring the full power of Clojure into shell-like scripts without compromising on speed, making it an ideal candidate for quick automation tasks.

ActivityWatch RestAPI:

REST API documentation of ActivityWatch is a good place to start, just like the served API documentation. From these two sources you will be able to learn about buckets, events and heartbeats.

To have some practical examples:

#!/usr/bin/env bb
(require
 '[babashka.http-client :as http] 
 '[cheshire.core :as cc]
(:import java.net.InetAddress)

(def watcher-api "http://localhost:5600/api")
(def hostname (.getHostName (java.net.InetAddress/getLocalHost)))
(def bucket-name (str "git-watcher_" hostname))

(def bucket-api (str watcher-api "/0/buckets" "/" bucket-name))

(def json-headers
  {"Content-type" "application/json"
   "Accept"       "application/json"})

(defn bucket-exist? []
  (= 200 (:status (http/get bucket-api {:throw false}))))

(defn create-bucket []
  (http/post bucket-api
             {:body    (cc/generate-string
                        {:client   bucket-name
                         :type     "git-tracker"
                         :hostname hostname})
              :headers json-headers
              :throw   false}))

(defn send-event [data]
  (http/post (str watcher-api "/0/buckets" "/" bucket-name "/events")
             {:body             (cc/generate-string {:data      data
                                                     :timestamp (java.util.Date.)
                                                     :duration  0})
              :headers json-headers
              :throw false}))


(defn send-heartbeat [data]
  (decode-body
   (http/post (str watcher-api "/0/buckets" "/" bucket-name "/heartbeat?pulsetime=" (* 5 60))
              {:body    (cc/generate-string {:data      data
                                             :timestamp (java.util.Date.)
                                             :duration  1})
               :headers json-headers
               :throw   false})))

(defn decode-body [response]
  (-> response
      :body
      (cc/decode true)))

(defn get-events []
  (decode-body
   (http/get (str watcher-api "/0/buckets" "/" bucket-name "/events")
             {:headers json-headers
              :throw   false})))

(defn get-last-event []
  (decode-body
   (http/get (str watcher-api "/0/buckets" "/" bucket-name "/events?limit=1")
             {:headers json-headers
              :throw   false})))

Interrogating Git:

When you execute a git command in any directory, git should know if it belongs to a git repository, where its root directory is. From your shell this command is 

git rev-parse –show-toplevel

And to know your actual brach you should execute

git branch –show-current

From babashka you can execute these commands with using 

(require  
 '[clojure.java.shell :refer [sh]]
 '[clojure.string :as str])

(def git-cmd "/usr/bin/git")

(def root-dir
  (->
   (sh git-cmd "rev-parse" "--show-toplevel")
   :out
   str/trim))

(def branch
  (->
   (sh git-cmd "branch" "--show-current")
   :out
   str/trim))

Bridging Git and ActivityWatch with Babashka:

As you can see above we already have the data (the actual branch / feature what we are working on, and the project in the form of the git root directory) all we have to do is storing it in ActivityWatch. When you already have your bucket it’s enough to send one event, then you can keep tracking your git activities with only using heartbeats.

(defn git-status []
  {:directory root-dir 
   :branch    branch})

(send-heartbeat (git-status))

Automating Time Tracking:

So far so good, we have everything for creating events in ActivityWatch, based on the actual directory. How to do it periodically, from the actual working directory? It’s easy. You shouldn’t do it by yourself. Leave it to your editor, which should execute your script when git should be used. In IntelliJ you can set up the git path to yourself:

I’m sure you can figure it out for your IDE too. You can set up an alias for your shell too. Like I did:

alias git=’/home/g-krisztian/git/git-watcher/git.bb’

I owe you one more thing, the missing pieces like the main function and how to execute it.

(defn -main [& args]
  #_(when-not (bucket-exist?) (create-bucket))
  #_(when-not (seq (get-last-event)) (send-event (git-status)))
  (apply shell git-cmd args)
  (send-heartbeat (git-status)))

(when (= *file* (System/getProperty "babashka.file"))
  (apply -main *command-line-args*))

Conclusion:

Babashka scripts offer an excellent approach to automate the process of logging hours into ActivityWatch by utilizing data from Git. This approach not only automatizes the time-tracking process but minimizes manual entry. It brings the time tracking to the developer’s world, and results in a well detailed base for the actual time reporting, which I will write about next time.