Scripted fields for time in statuses

- February 26, 2021

Hi all,

I want to create a few scripted fields that essentially act in the same way as the SLAs fields from Jira Service Desk, but for use in our Jira Software instance to calculate the time that an issue has been in a certain collection of statuses (within context of its pertaining workflow) from the time of its creation all the way to its resolution.

I want to create a few fields that calculate time in this manner:

  • TO DO statuses: Time in statuses ('Intake' + 'Selected for Development' + 'To Do',... > majority of To Do Category Statuses )
  • IN PROGRESS statuses: Time in statuses ('In Progress' + 'In Analysis' + 'In Review' + 'With Team', > Majority of In Progress Category Statuses)
  • PROGRESS HALTED statuses: Time in statuses ('On Hold' + 'Blocked' + 'Stalled',... > any named status that indicates progress on an issue is halted)
  • Total Time (Actual): (Time in all statuses of a workflow) - (TO DO statuses + PROGRESS HALTED statuses)
  • Total Time (Raw): (Time in all statuses of a workflow)

 

So at first, I figured that my best bet was to leverage the Count the time an issue was in a particular status code from Adaptavist Library, which you can use in a scripted field template to record the time an issue has been in a specific status. 

The big thing that I want to do, which will hopefully help me create the fields listed above, is change this code up so that it can calculate the time in not just one status, but rather a list of statuses...or maybe all statuses in a certain status category (that pertains to the context of the workflow), like the To Do statuses.

Here's how I modified the library code snippet so far in an attempt to create the TO DO statuses field:

 

import com.atlassian.core.util.DateUtils
import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.issue.history.ChangeItemBean
def changeHistoryManager = ComponentAccessor.changeHistoryManager


// Status to be counted

def toDo = 'To Do'
def totalStatusTime = [0L]


// Every status change is checked

def changeItems = changeHistoryManager.getChangeItemsForField(issue, "status")
changeItems.reverse().each { ChangeItemBean item ->
    def timeDiff = System.currentTimeMillis() - item.created.time

    // Subtract time if the "from" status is equal to the status to be checked and from and to statuses are different.

    // This allows to count the time the issue is in the state for the first time

    if (item.fromString == toDo && item.fromString != item.toString) {
        totalStatusTime << -timeDiff
    }

    // Add time if the "to" status is equal to the status to be checked

    if (item.toString == toDo) {
        totalStatusTime << timeDiff
    }
}

def intakeReview = "Intake"
def totalStatusTime2 = [0L]

changeHistoryManager.getChangeItemsForField (issue, "status")
changeItems.reverse().each {ChangeItemBean item ->
def timeDiff = System.currentTimeMillis() - item.created.time

    if (item.fromString == intakeReview && item.fromString != item.toString) {
        totalStatusTime << -timeDiff
    }

    if (item.toString == intakeReview) {
        totalStatusTime << timeDiff
    }
}

// Combining the time values in both arrays to get total time in both statuses
totalStatusTime.addAll(totalStatusTime2)

def total = totalStatusTime.sum() as Long

// Every time (added or subtracted) is summed and divided by 1000 to get seconds

(total / 1000) as long ?: 0L

As you can see, this was basically just me copying and pasting the changeItems code block and handling the Intake status in addition to To Do.

Previewing this code with a test issue in our instance (TEST-1518), I'm having two major problems with this approach:

 

1.) To Do is the first status in the workflow for TEST-1518. It was in the To Do status for 4 days, 18 hours, 53 minutes. Then it was transitioned to Intake and has been in that status for 35 weeks, 4 days, 6 hours, 57 minutes. I expected my code to add the two times together, where the calculated result would be: [4 days, 18 hours, and 53 minutes] + [35 weeks, 4 days, 6 hours, and 57 minutes] =  [36 weeks, 2 days, 1 hour, 50 minutes]. Unfortunately, I'm only getting null in the Result Log.

If I were to replace To Do with a status like In Progress, then it would add to the time in Intake and the time total would calculate as expected...but not for the first status in the workflow for some reason. Even if I used the base script from Adaptavist and tested it solely with time in the To Do status, it'll still return null! I'm assuming it was because there were no change history items to be found from anything past the issue being in To Do?

How do I fix this?

 

2.) Even if I had no problems with my changes to the script, I feel like I'm not doing this efficiently. I know that pasting a bunch of copies of the changeItems code block to account for different statuses wouldn't be efficient. To anyone out there...is there a way I could make the library code snippet work for more than one status? So instead of calculating the time in one status, I could do it for a string array of statuses like:

// Statuses to be counted
def statuses = ['New', 'To Do', 'Scope', 'Backlog', 'Pending Assignment', 'Intake', 'Selected for Development']
.
.
.

Or maybe reference all the To Do type statuses (and block out certain named statuses like On Hold, BlockedStalled)...whichever makes more sense. If you have an approach to the calculated fields that is better than what I have in mind, I'm all ears.

Thanks in advance to anyone that might have insight on my question post! Any tips/suggestions/corrections to the code would be invaluable to me. Any more info you need from me, let me know.

1 answer

0 votes
Benz Kek _Adaptavist_
Rising Star
Rising Star
Rising Stars are recognized for providing high-quality answers to other users. Rising Stars receive a certificate of achievement and are on the path to becoming Community Leaders.
February 28, 2021

Hi Ian,

Before you jump deeper into it, you have to understand how Jira's issue history works a.k.a. Change History.

The ChangeItemBean object itself contains fromString and toString, which is equivalent to what you see from the UI here: 

Screenshot 2021-03-01 at 8.00.06 AM.png

With that said, let's get to your questions: 

  1. From the library code, the first status or To Do status should return you a negative number. You are getting null because you didn't return the object properly as you now have two of the following: 
    (total / 1000) as long ?: 0L
    Simply add return to specify which to return. Alternatively, check my next reply item for a more scalable return object using Map [:]. 

  2. To do this in a more efficient manner, you can hook it with a loop like below instead: 
    import com.atlassian.jira.component.ComponentAccessor
    import com.atlassian.jira.issue.history.ChangeItemBean

    def statuses = ['To Do', 'In Progress', 'Done']
    def time = [:]

    statuses.each{
    // Status to be counted
    def statusName = it
      
       def changeHistoryManager = ComponentAccessor.changeHistoryManager
       def totalStatusTime = [0L]
      
       // Every status change is checked
       def changeItems = changeHistoryManager.getChangeItemsForField(issue, "status")
       changeItems.reverse().each { ChangeItemBean item ->
         def timeDiff = System.currentTimeMillis() - item.created.time
         // Subtract time if the "from" status is equal to the status to be checked and from and to statuses are different.
         
         // This allows to count the time the issue is in the state for the first time
         if (item.fromString == statusName && item.fromString != item.toString) {
           totalStatusTime << -timeDiff
         }

         // Add time if the "to" status is equal to the status to be checked
         if (item.toString == statusName) {
           totalStatusTime << timeDiff
         }
       }

       def total = totalStatusTime.sum() as Long
       // Every time (added or subtracted) is summed and divided by 1000 to get seconds
       time[statusName] = (total / 1000) as long ?: 0L

    }

    return time

    You probably will end up with something like below as the result (in seconds): 
    [To Do:-814, In Progress:814, Done:0]

I hope this helps! 

 

Regards

- March 4, 2021

Hi @Benz Kek _Adaptavist_ ,

Thanks for the reply! And sorry for my late reply, was absent from my computer for the past few days.

So I tried incorporating the loop you suggested, and here's the updated script:

import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.issue.history.ChangeItemBean

def statuses = ['Open','New','Scope', 'To Do', 'Backlogged', 'Backlog', 'Intake Review', 'Intake', 'Selected for Development', 'Ready']
def time = [:]

statuses.each {
def statusName = it

def changeHistoryManager = ComponentAccessor.changeHistoryManager
def totalStatusTime = [0L]

def changeItems = changeHistoryManager.getChangeItemsForField(issue, "status")
changeItems.reverse().each {
ChangeItemBean item ->
def timeDiff = System.currentTimeMillis() - item.created.time

if (item.fromString == statusName && item.fromString != item.toString) {
totalStatusTime << -timeDiff
}

if (item.toString == statusName) {
totalStatusTime << timeDiff
}
}

def total = totalStatusTime.sum() as Long
time[statusName] = (total / 1000) as long ?: 0L
}

return time

 

I previewed over an issue that has TO DO Statuses called 'New', 'Intake Review', 'Backlogged', and 'Ready'.

When I set the return template as "Text Field (multi-line)", I get the following string:

[Open:0, New:-1104, Scope:0, To Do:0, Backlogged:495, Backlog:0, Intake Review:532, Intake:0, Selected for Development:0, Ready:75]

So I have a few followup questions with this new outcome in place:

1.) Why does the output for the first status come out as a negative number? Is there a way to have it normally display the time difference in between that and the next status it transitions to?

2.) Is there a way to take the number of seconds for each status, add them together, and display the result using Duration as the template for the result? Because the big thing I want to get out of these fields as result is the total time spent in all of the statuses mentioned in the statuses array. When I ran your version of the code using the Duration template rather than the Text Field (multi-line) template, unfortunately the result didn't come out as expected. I got this error message instead:

An error occurred whilst rendering this message. 
Please contact the administrators, and inform them of this bug.
Details: ------- org.apache.velocity.exception.MethodInvocationException:
Invocation of method 'formatDurationPretty' in class com.atlassian.core.util.DateUtils threw exception
java.lang.NumberFormatException: For input string: "[Open:0, New:-931, Scope:0, To Do:0, Backlogged:323,
Backlog:0, Intake Review:532, Intake:0, Selected for Development:0, Ready:75]" at templates/customfield/view-duration.vm[line 3, column 16]
at org.apache.velocity.runtime.parser.node.ASTMethod.handleInvocationException(ASTMethod.java:342) at
org.apache.velocity.runtime.parser.node.ASTMethod.execute(ASTMethod.java:284) at org.apache.velocity.runtime.parser.node.ASTReference.execute(ASTReference.java:262)
at org.apache.velocity.runtime.parser.node.ASTReference.render(ASTReference.java:342) at org.apache.velocity.runtime.parser.node.ASTBlock.render(ASTBlock.java:72)
at org.apache.velocity.runtime.parser.node.ASTIfStatement.render(ASTIfStatement.java:87) at org.apache.velocity.runtime.parser.node.SimpleNode.render(SimpleNode.java:336) at org.apache.velocity.Template.merge(Template.java:328) at org.apache.velocity.Template.merge(Template.java:235) at org.apache.velocity.app.VelocityEngine.mergeTemplate(VelocityEngine.java:381).....
 

Suggest an answer

Log in or Sign up to answer
TAGS
AUG Leaders

Atlassian Community Events