If you want metrics out of Jira, the single most important thing you need to access is the issue history and it’s not obvious how to get that. Just about all the metrics you’ll want to collect will need data from the history - you might need to know when an item started or when it entered a certain priority or when the flag was set or cleared.

Searching the docs, you’ll likely find /rest/api/2/search which allows you to pass in a JQL statement (properly URL encoded) and get back data on the issues. The gotcha is that by default, this API does not return any of the history you want.

To get the history, you need to pass in the extra parameter expand=changelog and to get a bunch of extra fields that you’ll likely want, you also need fields=*all. Lastly, to handle pagination, you’ll need maxResults and startAt

So the command might look something like this. Note that you’ll still need to wrap this in the appropriate authentication as explained in this article.

/rest/api/2/search?jql=project%3DABC&maxResults=100&startAt=0&expand=changelog&fields=*all"

What gets returned is an enormous JSON file that’s documented here. What isn’t documented there is the issue history though so we’ll talk about that.

Inside each issue, there is a changelog element, which has all the history. Note that I’ve stripped out a massive amount of data that I don’t care about for this sample. The actual JSON you get back will be huge.

{
  "changelog": {
    "startAt": 0,
    "maxResults": 4,
    "total": 4,
    "histories": [
      {
        "created": "2024-03-29T17:11:04.249+0000",
        "items": [
          {
            "field": "Epic Child",
            "fieldtype": "custom",
            "from": null,
            "fromString": null,
            "to": "10048",
            "toString": "SP-41"
          },
          {
            "field": "status",
            "fieldtype": "jira",
            "fieldId": "status",
            "from": "10000",
            "fromString": "Backlog",
            "to": "10001",
            "toString": "Selected for Development"
          }
        ]
      }
    ]
  }
}

There can be multiple hashes within histories and multiple hashes within the items list. Each hash within items happened at the same time so instead of giving each one its own timestamp, Jira groups them.

You can NOT assume that the items in histories will be returned in any particular order. I’ve seen them returned in ascending order on one instance and in descending in another. Always sort them yourself.

The things you find in the items hashes will depend on what the field is, and I haven’t seen this documented anywhere so you’ll have a lot of trial and error. The field could be status to represent a status change or the name of an actual field like priority.

For dealing with metrics, I find that I’m most often dealing with the fields status, Flagged, priority, resolution, Sprint, Story Points, and Link. As you can see, there is no consistency in case sensitivity; some are all lower case while others are mixed.

The most interesting part has to do with items you would expect to find in the history that just aren’t there.

There is no created item in history so you have to look elsewhere in the JSON to find that. That also means that the first status change isn’t in history so you’ll have to look elsewhere to find what status it was created in.

It’s also possible for things like Flagged and priority to be be assigned at creation time, which means that the first time you’ll see an entry for either of them in history might be when they’re being turned off, and this can be confusing if you don’t know what to look for. You have to look at the regular fields for the issue (ie not history) to find out what their value was at creation and then you can assume that they entered that state at the creation timestamp.

Comments aren’t in the history either, although they are elsewhere in the JSON, if you need them.

For determining things like cycletime, we often want to know the status category that a status belongs to, and this is also missing from the history. To determine that status category, you need to make another API call to get status information, and that’s covered in this article.

Then we may want information about the board that an issue is moving across. Perhaps we want to start the clock when the issue enters a certain column. This information isn’t just missing from the history here, it’s missing entirely from Jira. Issues do not track what boards they were on, at any time. I’ve found ways to infer that information with reasonable accuracy, which I’ll likely cover later, but this is a real gap on Jira’s part.

As you can see, the history is a mess, although once you understand its limitations, it’s not that hard to work around them. The hard part is figuring out what it’s really doing and that’s been a lot of trial and error for me, across multiple Jira instances.

To make my own logic a lot simpler, I make a first pass through the issue and artificially create a bunch of history items to compensate for the things that are missing. If the issue was created with the Flag set then I artificially insert a history item to indicate it turning on the flag.

One more weird inconsistency is that although Jira will return so many fully qualified URL’s that you’ll get lost in them, the one thing they don’t provide a URL for is the issue itself. So if you need that, you’ll need to fabricate it based on other URLs that you find. Today, the URL to an issue looks like https://<server>/browse/<key> or https://improvingflow.atlassian.net/browse/ABC-1 but of course, this is subject to change without notice.

As you start to make these REST calls to the API, you’ll want to do some error handling. You’ll very quickly discover that sometimes Jira returns errors in the field errorMessage (singular) and sometimes in errorMessages (plural) so you have to check for both.

Next up will be retrieving status information.


Other articles about the Jira API: