{clockify} Time Tracking from R

[This article was first published on R | datawookie, and kindly contributed to R-bloggers]. (You can report issue about the content on this page here)
Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.

At Fathom Data we use Clockify to keep detailed records of the time that we spend working on our clients’ projects. Up until fairly recently we manually generated timesheets at the end of each month that were sent through to the clients along with their invoices. Our experience has been that providing detailed timesheets helps foster trust and transparency. However, with a growing team and an expanding clientele, generating these timesheets has become progressively more laborious. Time to automate!

Luckily Clockify exposes an extensive API. I built a small R package, {clockify} which is a wrapper around the Clockify API, making it easy to tap into our timesheet data. We’re then using R Markdown to generate some pretty sweet automated timesheets and reports. This reporting all happens automatically courtesy of GitLab CI. But that’s a story for another day. Let’s take a look at {clockify}.

This post documents {clockify}-0.0.9.

Installation

You can install the latest development version straight from GitHub.

remotes::install_github("datawookie/clockify")

Or you can install a stable version from CRAN.

install.packages("clockify")

The package documentation is available here.

Once you’ve installed the package, load it into R and check the version.

library(clockify)

packageVersion("clockify")
[1] '0.0.9'

API Key

You’re going to need to have an API key from your Clockify account. If you don’t yet have an account, create one. Then retrieve the API key from the account settings.

Get Started

Set the API key to be used for the session. I store my API key in an environment variable called CLOCKIFY_API_KEY.

CLOCKIFY_API_KEY = Sys.getenv("CLOCKIFY_API_KEY")

set_api_key(CLOCKIFY_API_KEY)

Let’s turn on logging so we can see what’s happening behind the scenes.

library(logger)

log_threshold(DEBUG)

Workspaces

Clockify uses workspaces to group people and projects. Every Clockify account is automatically associated with a default workspace, but may be linked to other workspaces too.

Use workspaces() to retrieve a list of available workspaces.

workspaces()
2021-09-09 04:53:01 — GET /workspaces
# A tibble: 2 × 2
  workspace_id             name       
  <chr>                    <chr>      
1 5ef46294df73063139f60bfc Fathom Data
2 61343c45ab05e02be2c8c1fd Personal   

I have two workspaces associated with my account, Personal and Work.

Select the Personal workspace. All subsequent operations will apply within this workspace.

workspace("61343c45ab05e02be2c8c1fd")
2021-09-09 04:53:02 — Set active workspace -> 61343c45ab05e02be2c8c1fd.
[1] "61343c45ab05e02be2c8c1fd"

Users

The users() function generates a table of all users linked to the workspace.

users()
2021-09-09 04:53:02 — GET /workspaces/61343c45ab05e02be2c8c1fd/users
# A tibble: 2 × 3
  user_id                  user_name status
  <chr>                    <chr>     <chr> 
1 5f227e0cd7176a0e6e754409 Andrew    ACTIVE
2 5ef46293df73063139f60bf5 Emma      ACTIVE

Retrieve information from your user profile.

user()
2021-09-09 04:53:02 — GET /user
# A tibble: 1 × 3
  user_id                  user_name status
  <chr>                    <chr>     <chr> 
1 5f227e0cd7176a0e6e754409 Andrew    ACTIVE

Clients

So, who do you work for? Let’s get a list of clients associated with the workspace.

clients()
2021-09-09 04:53:02 — GET /workspaces/61343c45ab05e02be2c8c1fd/clients
# A tibble: 6 × 3
  client_id                client_name workspace_id            
  <chr>                    <chr>       <chr>                   
1 61343c6c00dc8f48962b9be9 Community   61343c45ab05e02be2c8c1fd
2 61343c5d00dc8f48962b9be3 Fathom Data 61343c45ab05e02be2c8c1fd
3 6136159d26616c7c680bff06 RStudio     61343c45ab05e02be2c8c1fd
4 61362f4d26616c7c680c11a4 RStudio2    61343c45ab05e02be2c8c1fd
5 613630656217e45fb49c25e1 RStudio3    61343c45ab05e02be2c8c1fd
6 613630d826616c7c680c1262 RStudio4    61343c45ab05e02be2c8c1fd

In my personal workspace I only work for one client, Fathom Data.

Projects

Get a table of projects. The endpoint used to retrieve projects is paginated, so there are multiple API calls to retrieve the complete list.

projects()
2021-09-09 04:53:02 — GET /workspaces/61343c45ab05e02be2c8c1fd/projects
2021-09-09 04:53:02 — Page contains 2 results.
2021-09-09 04:53:02 — GET /workspaces/61343c45ab05e02be2c8c1fd/projects
2021-09-09 04:53:02 — Page is empty.
2021-09-09 04:53:02 — API returned 2 results.
# A tibble: 2 × 4
  project_id               project_name client_id                billable
  <chr>                    <chr>        <chr>                    <lgl>   
1 6134506c777d5361dcdeb3b5 {cex}        61343c5d00dc8f48962b9be3 TRUE    
2 61343c9ba15c1d53ad33369f {clockify}   61343c5d00dc8f48962b9be3 FALSE   

I’m logging time on two projects, {clockify} and {emayili}.

Let’s change the logging threshold, because it’s going to get noisy.

log_threshold(INFO)

Time Entries

The really interesting data is captured in the time entries: how much time am I spending on each of the projects and what am I doing?

Retrieve Time Entries

Retrieve the time entries for the authenticated user.

time_entries()
# A tibble: 8 × 4
  id                       project_id               description         duration
  <chr>                    <chr>                    <chr>                  <dbl>
1 61343cc1777d5361dcdea70a 61343c9ba15c1d53ad33369f Setting up GitHub …     12  
2 61343d06777d5361dcdea729 61343c9ba15c1d53ad33369f Make coffee              5  
3 61343d27ab05e02be2c8c266 61343c9ba15c1d53ad33369f Populate README.Rmd     68  
4 613448d7777d5361dcdead37 61343c9ba15c1d53ad33369f Add GET /workspace…     12.0
5 61344bcad01d3b4a27a82310 61343c9ba15c1d53ad33369f Add GET /workspace…     31.8
6 6134548f00dc8f48962bace7 6134506c777d5361dcdeb3b5 Add GET https://ce…     16.0
7 6134585a777d5361dcdebc5c 6134506c777d5361dcdeb3b5 Add GET https://ce…     16.5
8 61345c45d01d3b4a27a833c7 6134506c777d5361dcdeb3b5 Add GET https://ce…     31.6

You can also get the start and end times (as well as a bunch of other fields) for each entry by setting concise = FALSE.

time_entries(concise = FALSE) %>% select(id, starts_with("time"))
# A tibble: 8 × 3
  id                       time_start          time_end           
  <chr>                    <dttm>              <dttm>             
1 61343cc1777d5361dcdea70a 2021-09-03 05:15:00 2021-09-03 05:27:00
2 61343d06777d5361dcdea729 2021-09-03 05:27:00 2021-09-03 05:32:00
3 61343d27ab05e02be2c8c266 2021-09-03 05:45:00 2021-09-03 06:53:00
4 613448d7777d5361dcdead37 2021-09-05 04:34:31 2021-09-05 04:46:30
5 61344bcad01d3b4a27a82310 2021-09-05 04:47:06 2021-09-05 05:18:56
6 6134548f00dc8f48962bace7 2021-09-05 05:24:30 2021-09-05 05:40:29
7 6134585a777d5361dcdebc5c 2021-09-05 05:40:42 2021-09-05 05:57:13
8 61345c45d01d3b4a27a833c7 2021-09-05 05:57:25 2021-09-05 06:29:01

Retrieve time entries for another user specified by their user ID.

time_entries(user_id = "5ef46293df73063139f60bf5")
# A tibble: 2 × 4
  id                       project_id               description        duration
  <chr>                    <chr>                    <chr>                 <dbl>
1 613630ebba4b374e57155a72 61343c9ba15c1d53ad33369f Another test entry     87  
2 613630cb89516b1767a56a08 61343c9ba15c1d53ad33369f Creating hex logo      45.4

Insert Time Entry

What about inserting a new time entry? No problem, call time_entry_insert() and supply the details.

prepare_cran_id <- time_entry_insert(
 project_id = "61343c9ba15c1d53ad33369f",
 start = "2021-08-30 08:00:00",
 end   = "2021-08-30 10:30:00",
 description = "Prepare for CRAN submission"
)

The ID for the newly inserted time entry is returned.

prepare_cran_id
[1] "61399330dacb7878935d7ba6"

Confirm that it has been inserted.

time_entries(concise = FALSE) %>% select(id, description, duration)
# A tibble: 9 × 3
  id                       description                                  duration
  <chr>                    <chr>                                           <dbl>
1 61399330dacb7878935d7ba6 Prepare for CRAN submission                     150  
2 61343cc1777d5361dcdea70a Setting up GitHub Actions                        12  
3 61343d06777d5361dcdea729 Make coffee                                       5  
4 61343d27ab05e02be2c8c266 Populate README.Rmd                              68  
5 613448d7777d5361dcdead37 Add GET /workspaces/{workspaceId}/projects/…     12.0
6 61344bcad01d3b4a27a82310 Add GET /workspaces/{workspaceId}/projects/…     31.8
7 6134548f00dc8f48962bace7 Add GET https://cex.io/api/ticker/{symbol1}…     16.0
8 6134585a777d5361dcdebc5c Add GET https://cex.io/api/tickers/{symbol1…     16.5
9 61345c45d01d3b4a27a833c7 Add GET https://cex.io/api/last_price/{symb…     31.6

Delete Time Entry

What about deleting a time entry? Easy, just call time_entry_delete() and supply the time entry ID.

time_entry_delete(prepare_cran_id)
[1] TRUE

Confirm that it has been deleted.

time_entries(concise = FALSE) %>% select(id, description, duration)
# A tibble: 8 × 3
  id                       description                                  duration
  <chr>                    <chr>                                           <dbl>
1 61343cc1777d5361dcdea70a Setting up GitHub Actions                        12  
2 61343d06777d5361dcdea729 Make coffee                                       5  
3 61343d27ab05e02be2c8c266 Populate README.Rmd                              68  
4 613448d7777d5361dcdead37 Add GET /workspaces/{workspaceId}/projects/…     12.0
5 61344bcad01d3b4a27a82310 Add GET /workspaces/{workspaceId}/projects/…     31.8
6 6134548f00dc8f48962bace7 Add GET https://cex.io/api/ticker/{symbol1}…     16.0
7 6134585a777d5361dcdebc5c Add GET https://cex.io/api/tickers/{symbol1…     16.5
8 61345c45d01d3b4a27a833c7 Add GET https://cex.io/api/last_price/{symb…     31.6

Test Coverage

I’ve started writing unit tests. You can check on the current test coverage here. Evidently there’s still work to be done!

Conclusion

Exposing an API adds a lot of value to Clockify as a time tracking product. If you’re using Clockify, then you’ll find the {clockify} package convenient and useful for accessing those data. It plays particularly well in automated reporting scenarios.

To leave a comment for the author, please follow the link and comment on their blog: R | datawookie.

R-bloggers.com offers daily e-mail updates about R news and tutorials about learning R and many other topics. Click here if you're looking to post or find an R/data-science job.
Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.

Never miss an update!
Subscribe to R-bloggers to receive
e-mails with the latest R posts.
(You will not see this message again.)

Click here to close (This popup will not appear again)