Improving Sprint Metrics

Brendan Byers February 27, 2020

Hello,

In Jira, at the end of every Sprint you get the handy Sprint Report that divides up your issues into logical categories such as "Completed", "Removed" and "Not Completed" as will as indicate which issues were injected after the sprint has started.

This is great but I've always found myself wanting to do more with this information, be able to look at trends across multiple sprints and/or multiple teams.

Script runner adds some JQL queries that can find this information but they tend to be very sprint specific:

issueFunction in addedAfterSprintStart("Scrum Board", "Sprint")

I had been thinking that if I could just automatically tag an issue at the time it was injected or removed or sprint start or end, then I could run further queries or set up dashboards for that information.
To do this I've written a series of custom script listeners in Scriptrunner that trigger on various events that add a set value to a labels based custom field.

Here are the scripts:

Injection and Removal scripts trigger on "Issue Created, Issue Updated, Issue Closed, Issue Moved" events:

import com.atlassian.greenhopper.service.sprint.Sprint
import com.atlassian.greenhopper.service.sprint.SprintIssueService
import com.atlassian.greenhopper.service.sprint.SprintManager
import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.event.type.EventDispatchOption
import com.onresolve.scriptrunner.runner.customisers.PluginModuleCompilationCustomiser
import com.atlassian.jira.issue.Issue
import com.atlassian.jira.issue.MutableIssue
import com.atlassian.jira.issue.fields.CustomField
import com.atlassian.jira.issue.label.Label
import com.atlassian.jira.issue.label.LabelManager
import com.atlassian.jira.event.type.EventType

import com.onresolve.scriptrunner.runner.customisers.JiraAgileBean
import com.onresolve.scriptrunner.runner.customisers.WithPlugin

@WithPlugin("com.pyxis.greenhopper.jira")

@JiraAgileBean
SprintManager sprintManager

List changeItems = event?.getChangeLog()?.getRelated("ChildChangeItem")
MutableIssue issue = (MutableIssue) event.getIssue()

if(issue.isSubTask())
{
return
}

def sprintIssueService = PluginModuleCompilationCustomiser.getGreenHopperBean(SprintIssueService)
def user = ComponentAccessor.jiraAuthenticationContext.getLoggedInUser()

// get new sprint values
def serviceOutcome = sprintIssueService.getSprintsForIssue(user, issue)
def sprints = serviceOutcome.get()
def isActiveSprint = sprints*.state.any{ it == Sprint.State.ACTIVE}

def isSprintFieldModified = changeItems.any {
it.field == 'Sprint'
}

// get old sprint values
def wasActiveSprint = false

if (isSprintFieldModified) {
def sprintChange = changeItems.find{ it.field == 'Sprint' }
def sprintOldValue = sprintChange.oldvalue as String

if(sprintOldValue != null) {
def oldSprintList = sprintOldValue.split(',') as List

for(eachOldSprintId in oldSprintList) {

// if issue not in backlog
if(eachOldSprintId != null) {
def oldSprint = sprintManager.getSprint(eachOldSprintId as Long).getValue()
wasActiveSprint = (oldSprint.state == Sprint.State.ACTIVE)
if (wasActiveSprint) {
break
}}}}}

// check for new issue
def isIssueCreated = (event.eventTypeId == EventType.ISSUE_CREATED_ID)

// get sprint tags custom field
def cfSprintTags = ComponentAccessor.getCustomFieldManager().getCustomFieldObject("customfield_13702") // Sprint Tags

// check for injection into active sprint - ignore move to backlog
if(isActiveSprint && (isSprintFieldModified || isIssueCreated) && sprints.size() != 0) {

def injTag = "INJ" as String

// Get the Theme labels from the issue
def labelManager = ComponentAccessor.getComponent(LabelManager)
def valSprintTags = labelManager.getLabels(issue.id, cfSprintTags.getIdAsLong()).collect{it.getLabel()} as Set

def addTag = true

// check through list and do no add the same Tag twice
for(eachTag in valSprintTags) {
if(eachTag.toString().contains(injTag)) {
addTag = false
break
}}

if (addTag) {

// need to copy story points to original story points
def customFieldManager = ComponentAccessor.getCustomFieldManager()
def storyPointsCf = customFieldManager.getCustomFieldObject("customfield_10002") //Story points custom field
def origStoryPointsCf = customFieldManager.getCustomFieldObject("customfield_11718") //Original story points custom field
def storyPointsVal = issue.getCustomFieldValue(storyPointsCf)
def origStoryPointsVal = issue.getCustomFieldValue(origStoryPointsCf)

if(storyPointsVal != null && origStoryPointsVal == null){
issue.setCustomFieldValue(origStoryPointsCf, storyPointsVal)
ComponentAccessor.getIssueManager().updateIssue(user, issue, EventDispatchOption.DO_NOT_DISPATCH ,false)
}

//set tags
valSprintTags.add(injTag)
labelManager.setLabels(user, issue.id as Long, cfSprintTags.getIdAsLong(), valSprintTags, false, true)
}}

// check for removal
if(wasActiveSprint && isSprintFieldModified) {

def remTag = "REM" as String

// Get the Sprint Tags from the issue
def labelManager = ComponentAccessor.getComponent(LabelManager)
def valSprintTags = labelManager.getLabels(issue.id, cfSprintTags.getIdAsLong()).collect{ it.getLabel() } as Set

def addTag = true

for(eachTag in valSprintTags) {
if(eachTag.toString().contains(remTag)) {
addTag = false
break
}}

if (addTag) {
valSprintTags.add(remTag)
labelManager.setLabels(user, issue.id as Long, cfSprintTags.getIdAsLong(), valSprintTags, false, true)
}}

Commitment script triggers on "Sprint Started" events:

import com.atlassian.greenhopper.service.rapid.view.RapidViewService
import com.atlassian.greenhopper.service.sprint.Sprint
import com.atlassian.greenhopper.web.rapid.chart.HistoricSprintDataFactory
import com.atlassian.jira.component.ComponentAccessor
import com.onresolve.scriptrunner.runner.customisers.PluginModuleCompilationCustomiser
import com.onresolve.scriptrunner.runner.customisers.WithPlugin
import com.atlassian.jira.workflow.TransitionOptions
import com.atlassian.jira.issue.label.Label
import com.atlassian.jira.issue.label.LabelManager
import com.atlassian.jira.event.type.EventType
import com.atlassian.jira.issue.CustomFieldManager
import com.atlassian.jira.issue.fields.CustomField

@WithPlugin("com.pyxis.greenhopper.jira")
def historicSprintDataFactory = PluginModuleCompilationCustomiser.getGreenHopperBean(HistoricSprintDataFactory)
def rapidViewService = PluginModuleCompilationCustomiser.getGreenHopperBean(RapidViewService)

def user = ComponentAccessor.jiraAuthenticationContext.getLoggedInUser()
def sprint = event.sprint as Sprint
def sprintState = sprint.state
def sprintName = sprint.name
def cfSprintTags = ComponentAccessor.getCustomFieldManager().getCustomFieldObject("customfield_13702") // Sprint Tags


// if current sprint state is active
if (sprintState == Sprint.State.ACTIVE) {
def view = rapidViewService.getRapidView(user, sprint.rapidViewId).value
def sprintContents = historicSprintDataFactory.getSprintOriginalContents(user, view, sprint)
def sprintData = sprintContents.value
def issueService = ComponentAccessor.getIssueService()

if (sprintData) {
// get all issues in Sprint
def issues = sprintData.contents.issuesNotCompletedInCurrentSprint

def cmtTag = "CMT" as String

def labelManager = ComponentAccessor.getComponent(LabelManager)

issues.each { issue ->

def issueId = issue.id
def valSprintTags = labelManager.getLabels(issue.id, cfSprintTags.getIdAsLong()).collect{it.getLabel()} as Set

// check for valid project keys as Sprint Started is not a project specifc event
def project = ComponentAccessor.issueManager.getIssueObject(issueId)?.projectObject

Set<String> validProjectKeys = new HashSet<String>()
validProjectKeys.add("BTP")
validProjectKeys.add("UA")

if (validProjectKeys.contains(project.key)){
def addTag = true

for(eachlabel in valSprintTags) {
if(eachlabel.toString().contains(cmtTag)) {
addTag = false
break
}}

if (addTag) {
valSprintTags.add(cmtTag)

labelManager.setLabels(user, issue.id as Long, cfSprintTags.getIdAsLong(), valSprintTags, false, true)
}

// get custom fields for Story Points fields
def issueInputParameters = issueService.newIssueInputParameters()
def storyPoints = ComponentAccessor.issueManager.getIssueObject(issueId)?.getCustomFieldValue(ComponentAccessor.getCustomFieldManager().getCustomFieldObject("customfield_10002"))

// set original story points to
issueInputParameters.addCustomFieldValue("customfield_11718", storyPoints as String)
issueInputParameters.setSkipScreenCheck(true)

def updateValidationResult = issueService.validateUpdate(user, issue.id, issueInputParameters)

if (updateValidationResult.isValid()) {
issueService.update(user, updateValidationResult)
}}}}}

 Completed and Unfinished script triggers on "Sprint Ended" events:

import com.atlassian.greenhopper.service.rapid.view.RapidViewService
import com.atlassian.greenhopper.service.sprint.Sprint
import com.atlassian.greenhopper.web.rapid.chart.HistoricSprintDataFactory
import com.atlassian.jira.component.ComponentAccessor
import com.onresolve.scriptrunner.runner.customisers.PluginModuleCompilationCustomiser
import com.onresolve.scriptrunner.runner.customisers.WithPlugin
import com.atlassian.jira.workflow.TransitionOptions
import com.atlassian.jira.issue.label.Label
import com.atlassian.jira.issue.label.LabelManager

@WithPlugin("com.pyxis.greenhopper.jira")
def historicSprintDataFactory = PluginModuleCompilationCustomiser.getGreenHopperBean(HistoricSprintDataFactory)
def rapidViewService = PluginModuleCompilationCustomiser.getGreenHopperBean(RapidViewService)

def user = ComponentAccessor.jiraAuthenticationContext.getLoggedInUser()
def sprint = event.sprint as Sprint
def sprintState = sprint.state
def sprintName = sprint.name
def sprintTags = ComponentAccessor.getCustomFieldManager().getCustomFieldObject("customfield_13702") // Sprint Tags

if (sprintState == Sprint.State.CLOSED) {
def view = rapidViewService.getRapidView(user, sprint.rapidViewId).value
def sprintContents = historicSprintDataFactory.getSprintOriginalContents(user, view, sprint)
def sprintData = sprintContents.value
def issueService = ComponentAccessor.getIssueService()

if (sprintData) {

def cmpLabel = "CMP" as String
def unfLabel = "UNF" as String

// Get the Theme labels from the issue
def labelManager = ComponentAccessor.getComponent(LabelManager)

def cmpIssues = sprintData.contents.completedIssues

cmpIssues.each { issue ->

def issueId = issue.id

// check for valid project keys as Sprint Started is not a project specifc event
def project = ComponentAccessor.issueManager.getIssueObject(issueId)?.projectObject

Set<String> validProjectKeys = new HashSet<String>()
validProjectKeys.add("BTP")
validProjectKeys.add("UA")

if (validProjectKeys.contains(project.key)){

def labels = labelManager.getLabels(issueId, sprintTags.getIdAsLong()).collect{it.getLabel()} as Set
def addLabel = true

for(eachlabel in labels) {
if(eachlabel.toString().contains(cmpLabel)) {

addLabel = false
break
}}

if (addLabel) {

labels.add(cmpLabel)
labelManager.setLabels(user, issueId as Long, sprintTags.getIdAsLong(), labels, false, true)
}}}

def unfIssues = sprintData.contents.issuesNotCompletedInCurrentSprint

unfIssues.each { issue ->

def issueId = issue.id

// check for valid project keys as Sprint Started is not a project specifc event
def project = ComponentAccessor.issueManager.getIssueObject(issueId)?.projectObject

Set<String> validProjectKeys = new HashSet<String>()
validProjectKeys.add("BTP")
validProjectKeys.add("UA")

if (validProjectKeys.contains(project.key)){

def labels = labelManager.getLabels(issueId, sprintTags.getIdAsLong()).collect{it.getLabel()} as Set

def addLabel = true

for(eachlabel in labels) {
if(eachlabel.toString().contains(unfLabel)) {
addLabel = false
break
}}

if (addLabel) {

labels.add(unfLabel)
labelManager.setLabels(user, issueId as Long, sprintTags.getIdAsLong(), labels, false, true)
}}}}

The scripts also store Story Points into an "Original Story points" field to track any changes with regard to that value.

I've tested these script and they work as is but I would be interested to get any feedback on possible improvements or optimizations.

Thank you

0 comments

Comment

Log in or Sign up to comment
TAGS
AUG Leaders

Atlassian Community Events