Jira Metrics
Mix.install([
{:req, "~> 0.3.1"},
{:kino, "~> 0.6.2"},
{:timex, "~> 3.7"}
])
Authenticate
Jira uses basic authentication for requests over the REST API. Therefore it requires an user and an API key. The user is the same one used to login in Jira, so your e-mail. The API key must be generated individually.
Please check the Jira documentation to understand how to create your API key.
To setup your credentials, please create new Livebook Secrets with the following patterns:
- JIRA_API_KEY
- JIRA_URL
- JIRA_USER
user = System.fetch_env!("LB_JIRA_USER")
api_key = System.fetch_env!("LB_JIRA_API_KEY")
auth = {user, api_key}
jira_req = Req.new(base_url: System.fetch_env!("LB_JIRA_URL"), auth: auth)
auth_req = Req.get!(jira_req, url: "/rest/api/3/serverInfo")
case auth_req.status do
200 ->
IO.puts("Successfully authenticated")
_status ->
IO.inspect(jira_req)
IO.inspect(auth_req)
IO.puts("Authentication failed with status #{auth_req.status}")
end
Setup project
board_id_input = Kino.Input.text("Board ID: ")
board_id = Kino.Input.read(board_id_input)
board =
Req.get!(jira_req, url: "rest/greenhopper/1.0/xboard/work/allData.json?rapidViewId=#{board_id}")
|> then(& &1.body["columnsData"]["columns"])
IO.puts("Getting issues...")
board_columns =
board
|> Enum.map(&{&1["name"], &1["statusIds"]})
|> Enum.into(%{})
status_to_column =
board_columns
|> Enum.flat_map(&Enum.map(elem(&1, 1), fn status_ids -> {status_ids, elem(&1, 0)} end))
|> Enum.into(%{})
map_column_fn = fn issue ->
transitions =
for history <- issue["changelog"]["histories"],
item <- history["items"],
item["field"] == "status",
from_id = item["from"],
to_id = item["to"],
from_column =
if(Map.has_key?(status_to_column, from_id), do: status_to_column[from_id], else: nil),
to_column =
if(Map.has_key?(status_to_column, to_id), do: status_to_column[to_id], else: nil),
from_column != to_column do
%{
from: from_column,
to: to_column,
timestamp: Timex.parse!(history["created"], "{ISO:Extended}")
}
end
for name <- Map.keys(board_columns),
all_entered =
transitions
|> Enum.filter(&(&1.to == name))
|> Enum.sort_by(& &1.timestamp)
|> Enum.map(& &1.timestamp),
all_exited =
transitions
|> Enum.filter(&(&1.from == name))
|> Enum.sort_by(& &1.timestamp)
|> Enum.map(& &1.timestamp),
entered =
transitions
|> Enum.filter(&(&1.to == name))
|> Enum.sort_by(& &1.timestamp, :desc)
|> Enum.map(& &1.timestamp)
|> List.first(),
exited =
transitions
|> Enum.filter(&(&1.from == name))
|> Enum.sort_by(& &1.timestamp, :desc)
|> Enum.map(& &1.timestamp)
|> List.first() do
transitions =
all_entered
|> Enum.map(
&%{
entered: &1,
exited:
all_exited
|> Enum.filter(fn date -> date >= &1 end)
}
)
%{
name: name,
entered: entered,
exited: exited,
transitions: transitions
}
end
end
map_issue_fn = fn issue ->
%{
key: issue["key"],
type: issue["fields"]["issuetype"]["name"],
created: Timex.parse!(issue["fields"]["created"], "{ISO:Extended}"),
columns: map_column_fn.(issue)
}
end
extract_next_url_from_response = fn response ->
current = response.body["startAt"] + 1
total = response.body["total"]
next = current + 50 - 1
left = total - next
cond do
left < 1 -> nil
true -> "/rest/agile/1.0/board/#{board_id}/issue?startAt=#{next}"
end
end
initial_url = "/rest/agile/1.0/board/#{board_id}/issue?startAt=0"
issues =
Stream.unfold(initial_url, fn
nil ->
nil
url ->
case Req.get(jira_req, url: url) do
{:ok, response} ->
next_url = extract_next_url_from_response.(response)
issues_from_page = response.body["issues"]
{issues_from_page, next_url}
{:error, response} ->
IO.puts(inspect(response))
{[], nil}
end
end)
|> Stream.flat_map(&Enum.map(&1, fn issue -> issue["key"] end))
|> Stream.chunk_every(100)
|> Stream.map(&Enum.join(&1, ","))
|> Stream.map(
&Req.get!(jira_req,
url:
"rest/api/2/search?jql=key%20in%20(#{&1})&expand=changelog&fields=issuetype,created&maxResults=100"
)
)
|> Stream.flat_map(& &1.body["issues"])
|> Stream.map(&map_issue_fn.(&1))
|> Enum.to_list()
"Fetched #{Enum.count(issues)} issues."