We’ve been using Service Desk for some time, and have found a few irritations and annoyances that get in the way of us firing on all cylinders.

Here we share how we’ve overcome them, to maximise productivity.

Finding the reporter’s company

We like to see who the reporter works for, sometimes it gives us a good feeling. We also want to identify when multiple different users from the same company are asking for help - perhaps it’s part of a theme.

The way to get this info is to hover over the reporter, then quickly click on the display name before the dialog closes, which is easier said than done. However, I’ve just noticed that this has been improved in the latest version of Service Desk where it shows the reporter email address. So this is not as useful as previously, but still worthwhile.

Our solution is to create a text script field, with the following code:

issue.reporter?.emailAddress?.replaceAll(/.*@/, "")

We can improve this a little by creating a link to a JQL function which will show us all tickets reported by this domain. We want to keep the returned value the same as that’s what we want indexed, but we’ll tweak the displayed value a little by choosing a Custom template, with the following code:

<a target="_blank" href="/jira/issues/?jql='Reporter Domain' ~ '$value'">$value</a>

We can go a bit further by creating a bunch of links to google them for company info, and take a look at their home page. This is mostly for curiousity’s sake.

$value - <a target="_blank"
    href="$applicationProperties.getString("jira.baseurl")/issues/?jql='Reporter Domain' ~ '$value'">
        Find similar
    </a>
|
<a target="_blank" href="http://$value">Company home</a>
|
<a target="_blank" href="http://google.com/#q=$value">Google them</a>

On the ticket you should see:

view company
Atlassian is not necessarily a customer, they are just being used as an example.

Canned Comments

We often find ourselves asking people to provide logs, or version information and screenshots etc. After doing this five or six times in a day you can find yourself getting a little terse.

In order to preserve the illusion of courtesy, we can pick from a template comment. These are processed on the server using groovy templates, so you can include substitutions like the user and agent’s first name.

This has been expanded into the template comments plugin, which should be used in preference to setting this up yourself. The code examples are now outdated, but could be useful to see the process you might follow.

SEN Integration

If you are a marketplace vendor you may be interested in validating the user’s Support Entitlement Number (SEN). Even if not, perhaps users of your product have some sort of token that they need to provide to show they are entitled to support. You might want to look this up in your CRM database and verify that the customer is within their maintenance agreement.

The following post-function, which we put on the Create action, validates the SEN and gets information from the marketplace API, which is used to populate the fields.

import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.config.properties.JiraProperties
import com.atlassian.jira.issue.CustomFieldManager
import com.atlassian.jira.issue.MutableIssue
import com.onresolve.scriptrunner.runner.customisers.ContextBaseScript
import groovy.sql.Sql
import groovy.transform.BaseScript
import groovyx.net.http.ContentType
import groovyx.net.http.HTTPBuilder
import groovyx.net.http.Method
import org.apache.http.HttpRequest
import org.apache.http.HttpRequestInterceptor
import org.apache.http.protocol.HttpContext

import java.sql.Driver
import java.sql.Timestamp
import java.text.SimpleDateFormat

def http = new HTTPBuilder("https://marketplace.atlassian.com")


def jiraProperties = ComponentAccessor.getComponent(JiraProperties)

/**
 * system properties defined like -Dplugin.marketplace.basic.auth.credential=user@example.com:password
 */
def cred = jiraProperties.getProperty("plugin.marketplace.basic.auth.credential")
def dbPassword = jiraProperties.getProperty("plugin.marketplace.database.password")

if (! cred) {
    log.warn("Set plugin.marketplace.database.password as system prop")
}

http.client.addRequestInterceptor(new HttpRequestInterceptor() {
    void process(HttpRequest httpRequest, HttpContext httpContext) {
        httpRequest.addHeader("Authorization", "Basic " + cred.bytes.encodeBase64().toString())
    }
})

@BaseScript ContextBaseScript baseScript

def issue = getIssueOrDefault("SD-12") as MutableIssue

def customFieldManager = ComponentAccessor.getComponent(CustomFieldManager)
def senCf = customFieldManager.getCustomFieldObjectByName("SEN")
def sen = issue.getCustomFieldValue(senCf) as String

// note: all the following fields shown here are required - see comment for type (1)
def licenceTypeCf = customFieldManager.getCustomFieldObjectByName("Licence Type")               // short text
def licenceSizeCf = customFieldManager.getCustomFieldObjectByName("Licence Size")               // short text
def licensedProductCf = customFieldManager.getCustomFieldObjectByName("Licensed Product")       // short text
def licensee = customFieldManager.getCustomFieldObjectByName("Licensee")                        // short text
def mainStartDateCf = customFieldManager.getCustomFieldObjectByName("Maintenance Start Date")   // date
def mainEndDateCf = customFieldManager.getCustomFieldObjectByName("Maintenance End Date")       // date

try {
    if (!sen.startsWith("SEN-L")) {
        def response = http.request(Method.GET, ContentType.JSON) {
            uri.path = "/rest/1.0/vendors/81/sales"
            uri.query = [limit: 1, "sort-by": "date", order: "desc", q: sen]
        }

        def dateFormat = new SimpleDateFormat("yyyy-MM-dd")

        def licence = response.sales.find { it.licenseId == sen } as Map
        if (licence) {
            issue.setCustomFieldValue(licenceTypeCf, licence.licenseType)
            issue.setCustomFieldValue(licenceSizeCf, licence.licenseSize)
            issue.setCustomFieldValue(licensee, licence.organisationName)
            issue.setCustomFieldValue(licensedProductCf, licence.pluginKey)
            issue.setCustomFieldValue(mainStartDateCf, new Timestamp(dateFormat.parse(licence.maintenanceStartDate as String).time))
            issue.setCustomFieldValue(mainEndDateCf, new Timestamp(dateFormat.parse(licence.maintenanceEndDate as String).time))
        }
        else {
            // couldn't find licence
        }
    }
}
catch (any) {
    log.warn ("Failed to get SEN details", any)
}
1 All of these fields must exist with the type as shown
Evaluation licenses are not available via the marketplate API, as far as we can see. We have a database that we can query for these…​ the code for this is not shown.

Updating Tickets when bugs are Fixed

When a user reports a bug, either already known or not yet known, we link to the public bug (creating it if necessary). Then we close the support ticket as a Known Issue, asking the user to watch the linked bug report. Sometimes they do, sometimes they don’t.

To ensure they are aware when the bug is fixed and released, we add a post-function on the Release transition for the bug workflow which adds a comment to all support tickets that link to it:

update blocking config

When the bug is released, Service Desk tickets are updated with the following comment (and a mail is sent, etc):

update blocking comment

Adding organizations when a Service Desk issue gets created

One of the new features in JIRA Service Desk Server 3.3.0 is the Organizations, which are groups of customers that can be used in multiple projects. When you add an organization to a project, its members can raise requests in the project and share them with the organization.

The example is a script listener, in order to overcome the limitation where if an agent creates a ticket (not through the customer portal) an organization is not automatically added.

The following listener, which listens for an Issue Created event, adds to the organization custom field all the organizations configured for the specific project.

import com.atlassian.fugue.Option
import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.issue.ModifiedValue
import com.atlassian.jira.issue.MutableIssue
import com.atlassian.jira.issue.util.DefaultIssueChangeHolder
import com.atlassian.servicedesk.api.ServiceDeskManager
import com.atlassian.servicedesk.api.organization.OrganizationService
import com.atlassian.servicedesk.api.organization.OrganizationsQuery
import com.atlassian.servicedesk.api.util.paging.LimitedPagedRequest
import com.atlassian.servicedesk.api.util.paging.LimitedPagedRequestImpl
import com.onresolve.scriptrunner.runner.customisers.PluginModule
import com.onresolve.scriptrunner.runner.customisers.WithPlugin

@WithPlugin("com.atlassian.servicedesk")

@PluginModule
ServiceDeskManager serviceDeskManager

@PluginModule
OrganizationService organizationService

MutableIssue issue = issue

def currentUser = ComponentAccessor.getJiraAuthenticationContext().getLoggedInUser()
def serviceDeskProject = serviceDeskManager.getServiceDeskForProject(issue.projectObject)

// if the project is not a Service Desk one then do nothing
if (serviceDeskProject.isLeft()) {
    log.error "${serviceDeskProject?.left()?.get()}"
    return
}

def serviceDeskId = serviceDeskProject?.right()?.get()?.id as Integer

// get the available organizations for that project
def organizationsQuery = new OrganizationsQuery() {
    @Override
    Option<Integer> serviceDeskId() {

        return new Option.Some<Integer>(serviceDeskId)
    }

    @Override
    LimitedPagedRequest pagedRequest() {
        return new LimitedPagedRequestImpl(0, 50, 100)
    }
}

// get all the organizations configured for that project
def organizationsToAdd = organizationService.getOrganizations(currentUser, organizationsQuery)?.right()?.get()?.results

// get the Organizations custom field
def cf = ComponentAccessor.getCustomFieldManager().getCustomFieldObjectByName("Organizations")

// finally update the organizations custom field
cf.updateValue(null, issue, new ModifiedValue(issue.getCustomFieldValue(cf), organizationsToAdd), new DefaultIssueChangeHolder())

For how-to questions please ask on Atlassian Answers where there is a very active community. Adaptavist staff are also likely to respond there.

Ask a question about ScriptRunner for JIRA, for for Bitbucket Server, or for Confluence.