Jira data in Power BI

Often, project or work related data exists in Jira and there is value in reporting/visualizing it in Power BI. Perhaps the reporting capabilities within Jira are not sufficient, there is value in extracting it to use in Power BI, or there is desire to combine the data from Jira with other data sources.

The are commercial Jira plugins available for Power BI, but I’m going to roll our own. For this technique, you’ll need basic understanding of JIRA, JQL, Power Query M, REST, and JSON.

Helpful tools

  • Postman

Power BI Managed Parameters

Parameter Description Example
JIRA_URL Base url for JIRA instance https://prod-1-jira.bogus-example.com
JIRA_PAT Personal Access Token (PAT) for JIRA API use HGA?Q`Z-DJ{H@4M2NZF2~&CS>A’2QQ;DLZ`){VWIGB’L

 

Jira Fields

Jira includes many predefined fields as well as an custom fields that have been created on a Jira instance. It is important to know the unique ID, field name, and schema type for the fields you which to use in reports. Here is how to get it.

We’re going to leverage Power Query M and REST APIs to retrieve data from JIRA.


Define a Power BI Transformation with the first step below to retrieve the data

= let
    Source = let 
  headers = [#"Authorization" = Text.Combine("Bearer",JIRA_PAT," ")]],
  Source = Json.Document(Web.Contents(JIRA_URL,[RelativePath="/rest/api/2/field", Headers=headers]))
in
  Table.FromList(Source, Splitter.SplitByNothing(), null, null, ExtractValues.Error),
    #"Expanded Column1" = Table.ExpandRecordColumn(Source, "Column1", {"id", "name", "custom", "order able", "navigable", "searchable", "clauses", "schema"}, {"id", "name", "custom", "orderable", "navigable", "searchable", "clauses", "schema"}),
    #"Filtered Rows" = Table.SelectRows("Expanded Column1", each true)
in
    #"Extract Values" = Table.TransformColumns(Source, {"clauseNames", each Text.Combine(List.Combine(List.Transform(_, Text.From), ","), type text})
    #"Expanded schema" = Table.ExpandRecordColumn(#"Extracted Values", "schema", {"type"}, {"schema.type})
    #"Renamed Columns" = Table.RenameColumns(#"Expanded schema", {{"schema.type", "schema_type"}})
in
    #"Renamed Columns"

Retrieving JIRA data of a particular issue type

It is often useful to write a Jira JQL query, first, to retrieve all issues of a particular type, that are of interest for the reporting needs. Issues retrieved should have the same issue type because different issue types have different fields. We’re going to need to map the data from JIRA into specific fields in Power BI.

JQL

Sample JQL query

project in (prj_a, prj_b) AND issuetype=Story AND status!=Canceled

Data Retrieval

The more data requested from Jira, the longer it will take to retrieve it. To reduce the data payload, it is helpful to explicitly request only the needed Jira fields.

Don’t ask for “*all” fields, changelog, or history. It makes the quantity of data explode resulting in slower transfers or a timeout.

Extraction Code

Ok, hold on tight for some extraction code and preliminary transformations.

= let
MAX RESULTS=200,

headers = [#"Authorization"= Text.Combine("Bearer",JIRA_PAT), " "],

QUERY = "project IN(prj_a, prj_b) AND issuetype=Story AND status!=Canceled"

FIELDS = "&fields=key,summary,status,assignee,priority,issuetype,resolutiondate,custom field_123456",

RenameColumnsFromMap = (data_table, fields) => 
  let 
    // Transform custom field numbers back to their textual names
    fields_rep = List.Transform(fields, each {_[id], _[name]})
    expand1 = Table.ExpandRecordColumn(data_table, "Column1", {"id", "key", "fields"}, {"id", "key", "fields"}),
    expand2 = Table.ExpandRecordColumn(expand1, "fields", Record.FieldNames(expand1{0}[fields]), null),
    renamed = Table.RenameColumns(expand2, fields_rep, MissingField,Ignore)
  in Value,

GetFields = () => 
  let 
    // extract field ids and names for transformation back to names
    response = Json.Document(Web.Contents(JIRA_URL,[RelativePath="/rest/api/2/field/", Headers=headers])),
    Value = List.Transform(response, each [id=_[id], name=_[name], schema_type=_[schema][type]])
  in Value,

TransformToIdKeyFields = (data_list) => 
  let 
    // custom field id to key map
    xformed = List.Transform(data_list, each [id=_[id], key=_[key], fields=_[fields]])
  in xformed,

GetTotal = () => 
  let 
    // Retrieve total number of issues that would be returned by JQL
    // total needed for retrieving pages of data from JIRA API, in pages of MAX_RESULTS size
    Skip = "&StartAt=0",
    Top = "&maxResults=1",
    RawData = Web.Contents(JIRA_URL, [Headers=headers, Timeout=#duration(0, 0, 2, 0), RelativePath="rest/api/2/search?jql=" & QUERY & Skip & Top & "&fields=key"]),
    Json = Json.Document(RawData),
    Value = Json["#issues"]
  in Value,
  
GetPage = (Index) => 
  let 
      // get 1 page of data, based on index and MAX_RESULTS
      Skip = "&startAt=" & Text.From(Index * MAX_RESULTS),
      Top = "&maxResults=" & Text.From(MAX_RESULTS),
      RawData = Web.Contents(JIRA_URL, [Headers=headers, RelativePath="rest/api/2/search?jql=" & QUERY & Skip & Top & FIELDS]),
      Json = Json.Document(RawData),
      Value = Json["#issues"]
  in Value,

GetPages = (Loop) => 
  let // Get all the pages of data 
      total_issues = Get_Total(),
      total_pages = Number.RoundUp(totaL_issues / MAX_RESULTS),
      page_indices = {0 .. total_pages -1},
      pages_data = List.Transform(page_indices, each GetPage(_)),
      pages = List.Union(pages_data)
  in pages,

fields = GetFields(),
pages_list = GetPages(0),
data_id_key_fields = TransformToIdKeyFields(pages_list),
data_table = Table.FromList(data_id_key_fields, Splitter.SplitByNothing(), null, null, ExtractValues.Error),
data_table_mapped_columns = RenameColumnsFromMap(data_table, fields)

Transformation code

After acquiring the data, there are multiple transformations which you’ll want to apply.

One category transforms is about extracting individual properties from complex JSON objects. Some of those are

  • Assignee
  • Priority
  • Status

The Assignee’s Full name can be extracted from the Assignee object, by extracting the “displayName” field. Here is what an Assignee object might look like:

{
  "self": "https://prod-1-jira.bogus-example.com/rest/api/2/user?username=ABC123",
  "name": "ABC123",
  "key": "JIRAUSER123456",
  "emailAddress": "REALLY_JUST_AN_EXAMPLE@bogus-example.com",
  "avatarUrls": {
    "not_going_into_more_detail": "sample_url_for_avatar_at_certain_size"
  },
  "displayName": "Zaphod Beeblebrox",
  "active": true,
  "timeZone": "America/Los Angeles"
}

A transformation step to get just the displayName would be like below.

= Table.ExpandRecordColumn(#"Prior Step Name", "Assignee", {"displayName"}, {"assignee"})

Leave a Reply

Your email address will not be published. Required fields are marked *