Powered by AppSignal & Oban Pro
Would you like to see your link here? Contact us

Jira Metrics

jira-metrics.livemd

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(&amp;(&amp;1.to == name))
        |> Enum.sort_by(&amp; &amp;1.timestamp)
        |> Enum.map(&amp; &amp;1.timestamp),
      all_exited =
        transitions
        |> Enum.filter(&amp;(&amp;1.from == name))
        |> Enum.sort_by(&amp; &amp;1.timestamp)
        |> Enum.map(&amp; &amp;1.timestamp),
      entered =
        transitions
        |> Enum.filter(&amp;(&amp;1.to == name))
        |> Enum.sort_by(&amp; &amp;1.timestamp, :desc)
        |> Enum.map(&amp; &amp;1.timestamp)
        |> List.first(),
      exited =
        transitions
        |> Enum.filter(&amp;(&amp;1.from == name))
        |> Enum.sort_by(&amp; &amp;1.timestamp, :desc)
        |> Enum.map(&amp; &amp;1.timestamp)
        |> List.first() do
    transitions =
      all_entered
      |> Enum.map(
        &amp;%{
          entered: &amp;1,
          exited:
            all_exited
            |> Enum.filter(fn date -> date >= &amp;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(&amp;Enum.map(&amp;1, fn issue -> issue["key"] end))
  |> Stream.chunk_every(100)
  |> Stream.map(&amp;Enum.join(&amp;1, ","))
  |> Stream.map(
    &amp;Req.get!(jira_req,
      url:
        "rest/api/2/search?jql=key%20in%20(#{&amp;1})&expand=changelog&fields=issuetype,created&maxResults=100"
    )
  )
  |> Stream.flat_map(&amp; &amp;1.body["issues"])
  |> Stream.map(&amp;map_issue_fn.(&amp;1))
  |> Enum.to_list()

"Fetched #{Enum.count(issues)} issues."