This is a repost of an article I wrote on Medium.com back in 2016. At the time I was playing with Elixir which is a really interesting programming language which leverages the Erlang Virtual Machine. Much to my surprise this article even briefly made it on to Hacker News! If you haven’t tried Elixir I highly recommend it. Even my short forray into it made me a better coder.
I started playing with Elixir around a month ago. Having come from a non-functional background (PHP and Python) I found it engaging almost immediately. The syntax was familiar but once I dug a little deeper I realised that’s where the similarity ends. I read tutorials and books but knew I’d not really learn anything until I could put it to use in a real word problem. Luckily one presented itself.
The Problem
At work we use a SaaS product called Harvest (https://www.getharvest.com/). It’s used for tracking time spent on projects and it’s great. The problem is remembering to start and stop timers throughout the day. Quite often users would realise at the end of the week that they had forgotten and then have to backfill all their time.
The Idea
Using Elixir, write a nagging script that checks each users time-sheets throughout the day. If insufficient hours have been logged then email them to politely remind them. Harvest has a well documented API and I could see all the information I needed was available.
Strategy
Try as much as possible to use a functional style to achieve what I needed. I kept two main concepts in mind:
- Write small composable functions which can be chained together.
- Think in terms of transforming data from one form to the next.
API Access
First I had to find a client library in Elixir. I found HTTPotion. I wanted the script to be as maintenance free as possible so the first thing I needed was a function to fetch a list of active users from the API.
def get_users() do
response = HTTPotion.get("https://<our_account_name>.harvestapp.com/people",
headers: [
"Content-Type": "application/json",
"Accept": "application/json",
"Authorization": get_auth()
]
)
Poison.decode!(response.body)
end
The get_auth function here just takes my Harvest API username and password and Base64 encodes them to include in the Authorization header. The get_users function returned a list of users like this:
[
{
"user": {
"id": 439921,
"email": "[email protected]",
"created_at": "2012-12-20T18:29:48Z",
"is_admin": false,
"first_name": "John",
"last_name": "Smith",
"timezone": "Eastern Time (US & Canada)",
"is_contractor": false,
"telephone": "",
"is_active": true,
"has_access_to_all_future_projects": false,
"default_hourly_rate": 60,
"department": "",
"wants_newsletter": true,
"updated_at": "2016-06-08T13:12:00Z",
"cost_rate": 30,
"signup_redirection_cookie": null
}
}
]
I was only interested in active users so I wrote another function to filter the list.
def filter_to_active_users(full_list) do
Enum.filter(full_list, fn(x) -> x["user"]["is_active"] end )
end
Now I had a list of active users which I could loop through and fetch their current timesheet for that day.
def get_today_timesheet(user) do
url = "https://<our_account_name>.harvestapp.com/daily?of_user=#{to_string(user["id"])}&slim=1"
response = HTTPotion.get(url,
headers: [
"Content-Type": "application/json",
"Accept": "application/json",
"Authorization": get_auth()
]
)
[user["email"], Poison.decode!(response.body)]
end
def get_timesheets_for_users(users_list) do
users_list
|> Enum.map(fn(x) -> get_today_timesheet(x["user"]) end )
end
The function returned a List with two items:
- The users email address – I needed this in tact to send the reminder later.
- The full timesheet response for that user.
The timesheet response:
{
"for_day": "2016-07-04",
"day_entries": [
{
"id": 48323487,
"user_id": 439123,
"spent_at": "2016-07-04",
"created_at": "2016-07-04T10:19:09Z",
"updated_at": "2016-07-04T10:21:25Z",
"project_id": "3545264",
"task_id": "1764309",
"project": "Technology / Systems",
"task": "Admin",
"client": "My Client",
"notes": "My Notes",
"hours_without_timer": 0.83,
"hours": 0.83
},
{
"id": 483321340,
"user_id": 454200,
"spent_at": "2016-07-04",
"created_at": "2016-07-04T10:20:59Z",
"updated_at": "2016-07-04T10:21:15Z",
"project_id": "3576664",
"task_id": "1123609",
"project": "Technology / Systems",
"task": "Admin",
"client": "A Different Client",
"notes": "A New Note",
"hours_without_timer": 1.5,
"hours": 1.5
}
]
}
I needed to sum all the ‘hours’ values in the list of ‘day_entries’.
def sum_hours([]) do
0
end
def sum_hours([h|t]) do
h["hours"] + sum_hours(t)
end
def group_hours(timesheets) do
timesheets
|> Enum.map(
fn(x) ->
email = List.first(x)
timesheet = List.last(x)
total_hours = sum_hours(timesheet["day_entries"])
[email, total_hours]
end
)
end
sum_hours() was my first attempt at recursion. At first it seems odd but you soon realise the power vs. a for loop in PHP where you have to maintain a mutable variable.
group_hours() returns a list containing the users email and the total number of hours they’ve recorded that day.
The Pipe Operator
Now I had all these functions built correctly I could combine them using Elixir’s pipe operator into one easy to call function:
def get_hours_for_active_users() do
get_users()
|> filter_to_active_users()
|> get_timesheets_for_users()
|> group_hours()
end
I love how readable the code above is. It’s clear to anyone what is happening and that must make it more maintainable. This returned a simple list like this:
[
["[email protected]", 7],
["[email protected]", 3],
["[email protected]", 3]
]
All that was then left to do was to see if each user had logged the expected amount of hours and email them if they hadn’t.
Calculating ‘expected’ hours
Luckily all our users start at 9am so it was quite easy to work out how many hours they should have done. I fetched the current hour of the day and subtracted 9. I then further reduced this number by 1 to work out my expected hours figure. For example. By 1pm I expected them to have logged 3 Hours (13–9–1).
def hours_since_nine() do
{_, {hour,_,_}} = :calendar.local_time()
hour - 9
end
def expected_hours() do
hours_since_nine() - 1
end
The hours_since_nine function get’s the hour using pattern matching on the Erlang calendar local_time() function. Now I knew how many hours I expected I could further filter my list of hours for active users.
def get_users_with_low_hours() do
get_hours_for_active_users()
|> Enum.filter(
fn (x) ->
[_email, hours] = x
hours < expected_hours()
end
)
end
Email Alerts
Once I had this final list it was a simple case of emailing each person to ask them to check their timesheet.
def email_users() do
get_users_with_low_hours()
|> Enum.map(fn(x) ->
[email, hours] = x
actualSend(email, hours)
end
)
end
Here I used the Enum map function to apply the actualSend function to each person in the list. I’ve not included all the detail on how the actualSend function works here but I used the Mailman module which made it really simple.
Conclusion
This small script is now in production and nagging users on a regular basis. I used escript to create an executable shell script I could call via cron. It runs mid morning and late afternoon every work day. I’m yet to tell if it’s effective!
I’m really pleased with my first foray into Elixir and will definitely be looking for other projects I can use it on. I’m sure there are nicer/better ways to do what I’ve done here so I’d love to hear some feedback.