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.
canned responses

To set this up you need two REST endpoints, which just entails pointing to this file:

package com.onresolve.base.test.rest.jsd

import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.issue.link.IssueLinkManager
import com.onresolve.scriptrunner.runner.rest.common.CustomEndpointDelegate
import groovy.json.JsonOutput
import groovy.text.GStringTemplateEngine
import groovy.transform.BaseScript

import javax.ws.rs.core.MediaType
import javax.ws.rs.core.MultivaluedMap
import javax.ws.rs.core.Response

@BaseScript CustomEndpointDelegate delegate

def msgs = [ (1)
    "Provide logs..."             : '''\
            Hi ${customerFirstName},

            Please could you add the application server logs after you have reproduced this problem.

            You can find out how to get the server logs for [JIRA|https://confluence.atlassian.com/jira/where-are-the-application-server-logs-16121981.html], for [Confluence|https://confluence.atlassian.com/doc/working-with-confluence-logs-108364721.html], or for [Bitbucket|https://confluence.atlassian.com/bitbucketserver/bitbucket-server-home-directory-776640890.html].

            It may be helpful to add screenshots too.

            Regards, ${agentFirstname}''',

    "Bug created (known issue)...": '''\
            Hi ${customerFirstName},

            Thank you for reporting this problem. We have created a bug report and added it to our backlog (<% out << (causedByIssues*.key.join(", ") ?: "_if you had linked the issue first you wouldn't have to type it in after_") %>).

            Please _watch_ it to be informed of when it's fixed and released.

            Regards, ${agentFirstname}''',

    "So happy you fixed it..."    : '''\
            Hi ${customerFirstName},

            Great - we're happy you were able to fix it!

            Thanks for letting us know.

            Regards, ${agentFirstname}''',

     "Add us to Atlassian ticket..."    : '''\
            Hi ${customerFirstName},

            Sorry to hear you are having a problem. Please could you add us as _Request Participants_ to your ticket on _support.atlassian.com_.

            Thanks for letting us know.

            Regards, ${agentFirstname}''',

    "Ask on Answers..." : '''\
            Hi ${customerFirstName},

            Although we would like to help with all coding questions, unfortunately time does not allow us to. Custom coding questions, by their very nature, are limitless, so this is not covered by our EULA.

            We recommend you ask your question on [Atlassian Answers|https://answers.atlassian.com] where there is a very active community. Adaptavist staff are also likely to respond there.

            Please tag your question with the ScriptRunner plugin tag, which is:

            * for JIRA: {{com.onresolve.jira.groovy.groovyrunner}}
            * for Confluence: {{com.onresolve.jira.confluence.groovyrunner}}
            * for Bitbucket Server: {{com.onresolve.jira.stash.groovyrunner}}

            Alternatively you can ask your question from the Atlassian Answers link on the relevant plugin's marketplace listing page.

            If you post the link to your question in a comment here, we'll be happy to follow up.

            Regards, ${agentFirstname}'''

]

def issueManager = ComponentAccessor.getIssueManager()
def issueLinkManager = ComponentAccessor.getComponent(IssueLinkManager)

getCannedCommentKeys(httpMethod: "GET", groups: ["service-desk-agents", "jira-servicedesk-users"]) { MultivaluedMap queryParams ->
    def issueId = queryParams.getFirst("issueId") as Long

    // can filter on issue if required...
    return Response.ok(JsonOutput.toJson(msgs.keySet())).build()
}

getCannedComment(httpMethod: "GET", groups: ["service-desk-agents", "jira-servicedesk-users"]) { MultivaluedMap queryParams ->

    // expect an issue ID and the key of the comment to get.
    def issueId = queryParams.getFirst("issueId") as Long
    String msgKey = queryParams.getFirst("msgKey")

    def issue = issueManager.getIssueObject(issueId)

    // Comments will be run through gstring template engine
    def engine = new GStringTemplateEngine()

    def currentUser = ComponentAccessor.jiraAuthenticationContext.getLoggedInUser()
    def agentFirstname = currentUser.displayName.replaceAll(/\s.*/, '')
    def customerFirstName = issue.reporter?.displayName?.replaceAll(/\s.*/, '')

    def causedByIssues = issueLinkManager.getInwardLinks(issue.id).findAll {
        it.issueLinkType.name in ["Problem/Incident"]
    }*.sourceObject

    // this is like this because someone has set up the Cause link type back to front in PS
    if (! causedByIssues) {
        causedByIssues = issueLinkManager.getOutwardLinks(issue.id).findAll {
            it.issueLinkType.name in ["Cause"]
        }*.destinationObject
    }

    def binding = [ (2)
        currentUser      : currentUser,
        issue            : issue,
        agentFirstname   : agentFirstname,
        customerFirstName: customerFirstName,
        causedByIssues: causedByIssues,
    ]

    def text = msgs.get(msgKey) ?: "Template not found"
    def template = engine.createTemplate(text.stripIndent()).make(binding)
    return Response.ok(template.toString()).type(MediaType.TEXT_PLAIN).build()
}
1 to modify or add to the messages, just modify this msgs map
2 these are the objects available to the template, feel free to add more

Next thing is to inject some javascript into the jira.view.issue web context, which will wait for the comment container to appear, then add the select list.

Configuration:

canned responses config
Source of javascript file
Unresolved directive in <stdin> - include::../shared/src/main/resources/js/scriptrunner/jira/sd-canned-responses.js[]

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.