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."