3
2
1

I'd like to see how the sync of Insight Cloud fields works? The documentation only says Jira on-prem.


Cheers,


    CommentAdd your comment...

    1 answer

    1.  
      2
      1
      0

      Hi George,


      The basic issuedata contains all you need to get this to work with some additional scripting.


      You'll need to extract the workspaceId and the id of the object from the customfield:


      workspaceId = issue.customfield."Insight Object".workspaceId
      objectId = issue.customfield."Insight Object".objectId


      After that, you can call the Insight API to get the full data for this object:

      object = httpClient.get("https://api.atlassian.com/jsm/insight/workspace/" + workspaceId + "/v1/object/"+objectId)

      After which you can add all attributes you need to the sync via something like

      replica.insightObject = object.attributes[]


      Let me know if this helps,

      1. Charlie Misonne

        Hi Joachim,

        Thanks! George created this question on my behalf.


        I tried what you did but it doesn't work for me yet.

        We have an Insight custom field called Mobile OS. The Insight object has the same name.

        Here is my code:

        workspaceId = issue.customFields.'Mobile OS'?.value[0]?.workspaceId
        objectId = issue.customFields.'Mobile OS'?.value[0]?.objectId

        object = httpClient.get("https://api.atlassian.com/jsm/insight/workspace/" + workspaceId + "/v1/object/"+objectId)
        replica.insight."Mobile OS" = object.attributes[]


        When syncing Exalate says Name or service not known. So I assume something is wrong when fetching those URLs.

        First thing I noticed is you are using customfield but I think it should be customFields, or am I mistaken?

        Without s gives me Cannot get property 'Mobile OS' on null object.


        This in the local replica in Jira (I replaced the ID's)

        {
          "version": {
            "major": 1,
            "minor": 14,
            "patch": 0
          },
          "hubIssue": {
            "components": [],
            "attachments": [],
            "voters": [],
              "Mobile OS": {
                "id": 10063,
                "name": "Mobile OS",
                "uid": "10063",
                "type": "UNHANDLED",
                "value": [
                  {
                    "workspaceId": "1234567890",
                    "id": "1234567890:33",
                    "objectId": "33"
                  }
                ]
              },


      2. Francis Martens (Exalate)

        Hi Charlie Misonne 

        Nice to meet you on the bitstream

        replica.insight is not an object and when you do replica.insight."Mobile OS", groovy will not know what to do.

        If you do what Joachim proposes, then exalate will create the object
        So 

        // works
        replica.city = "Brussels"
        // doesn't work because country doesn't exists
        replica.country.city = "Brussels"





        FYI - you can check what the attributes are (returned by the httpClient)

        debug.error("Object attributes are ${object.attributes}")

        if you trigger an incoming sync, an error will be raised with the attributes

      3. Charlie Misonne

        Hi Francis Martens (Exalate) !

        Thank for pointing out my mistake about tha variable.


        However, fetching the values from Insight is still not working. I keep getting name or service not known when calling https://api.atlassian.com/...

        I have an ongoing support ticket about this but so far I'm out of luck: EASE-12717


        I wonder if this can be a network restriction in the plugin?


        cheers!

        Charlie

      4. Charlie Misonne

        Because I'm always getting Name or service not known when trying to fetch the Insight values from the API I implemented a dumb workaround:


        In the incoming script on the target application (Azure Devops) I create some If/Else statements checking for the Insight value ID. Each IF sets the string value of the field this way.

      5. Francis Martens (Exalate)

        I will have a look into the ticket and revert here.

      6. Joachim Bollen

        Hi Charlie Misonne ,


        My previous answer was a bit short in mentioning the httpClient. By default in Exalate for Jira Cloud, the httpClient will prepend all requests with the base Url of Jira, which is not what you need in this case.


        The code I used to build a customized http client:

        def http = { String method, String path, String workspaceId, String queryParams, String body, Map<String, List<String>> headers ->
            final def injector = play.api.Play$.MODULE$.current().injector()
        
            def orNull = { scala.Option<?> opt -> opt.isDefined() ? opt.get() : null }
            def pair = { l, r -> scala.Tuple2$.MODULE$.<?, ?>apply(l, r) }
            def none = { scala.Option$.MODULE$.<?> empty() }
            def getGeneralSettings = {
                def gsp = injector.instanceOf(com.exalate.api.persistence.issuetracker.jcloud.IJCloudGeneralSettingsPersistence.class)
                def gsOpt = await(gsp.get())
                def gs = orNull(gsOpt)
                gs
            }
            final def gs = getGeneralSettings()
        
            def removeTailingSlash = { String str -> str.trim().replace("/+\$", "") }
            final def insightRestApiUrl = "https://api.atlassian.com/jsm/insight/workspace/" + workspaceId
        
            def parseQueryString = { String string ->
                string.split('&').collectEntries{ param ->
                    param.split('=', 2).collect{ URLDecoder.decode(it, 'UTF-8') }
                }
            }
        
            //Usage examples: https://gist.github.com/treyturner/4c0f609677cbab7cef9f
            def parseUri
            parseUri = { String uri ->
                def parsedUri
                try {
                    parsedUri = new URI(uri)
                    if (parsedUri.scheme == 'mailto') {
                        def schemeSpecificPartList = parsedUri.schemeSpecificPart.split('\\?', 2)
                        def tempMailMap = parseQueryString(schemeSpecificPartList[1])
                        //noinspection GrUnresolvedAccess
                        parsedUri.metaClass.mailMap = [
                                recipient: schemeSpecificPartList[0],
                                cc       : tempMailMap.find { //noinspection GrUnresolvedAccess
                                    it.key.toLowerCase() == 'cc' }.value,
                                bcc      : tempMailMap.find { //noinspection GrUnresolvedAccess
                                    it.key.toLowerCase() == 'bcc' }.value,
                                subject  : tempMailMap.find { //noinspection GrUnresolvedAccess
                                    it.key.toLowerCase() == 'subject' }.value,
                                body     : tempMailMap.find { //noinspection GrUnresolvedAccess
                                    it.key.toLowerCase() == 'body' }.value
                        ]
                    }
                    if (parsedUri.fragment?.contains('?')) { // handle both fragment and query string
                        //noinspection GrUnresolvedAccess
                        parsedUri.metaClass.rawQuery = parsedUri.rawFragment.split('\\?')[1]
                        //noinspection GrUnresolvedAccess
                        parsedUri.metaClass.query = parsedUri.fragment.split('\\?')[1]
                        //noinspection GrUnresolvedAccess
                        parsedUri.metaClass.rawFragment = parsedUri.rawFragment.split('\\?')[0]
                        //noinspection GrUnresolvedAccess
                        parsedUri.metaClass.fragment = parsedUri.fragment.split('\\?')[0]
                    }
                    if (parsedUri.rawQuery) {
                        //noinspection GrUnresolvedAccess
                        parsedUri.metaClass.queryMap = parseQueryString(parsedUri.rawQuery)
                    } else {
                        //noinspection GrUnresolvedAccess
                        parsedUri.metaClass.queryMap = null
                    }
        
                    //noinspection GrUnresolvedAccess
                    if (parsedUri.queryMap) {
                        //noinspection GrUnresolvedAccess
                        parsedUri.queryMap.keySet().each { key ->
                            def value = parsedUri.queryMap[key]
                            //noinspection GrUnresolvedAccess
                            if (value.startsWith('http') || value.startsWith('/')) {
                                parsedUri.queryMap[key] = parseUri(value)
                            }
                        }
                    }
                } catch (e) {
                    throw new com.exalate.api.exception.IssueTrackerException("Parsing of URI failed: $uri\n$e", e)
                }
                parsedUri
            }
        
            def unsanitizedUrl = insightRestApiUrl + path
            def parsedUri = parseUri(unsanitizedUrl)
        
            //noinspection GrUnresolvedAccess
            def embeddedQueryParams = parsedUri.queryMap
        
        
            def urlWithoutQueryParams = { String url ->
                URI uri = new URI(url)
                new URI(uri.getScheme(),
                        uri.getUserInfo(), uri.getHost(), uri.getPort(),
                        uri.getPath(),
                        null, // Ignore the query part of the input url
                        uri.getFragment()).toString()
            }
            def sanitizedUrl = urlWithoutQueryParams(unsanitizedUrl)
            if (queryParams) {
                sanitizedUrl = sanitizedUrl + "?" + queryParams
            }
            def response
            try {
                def request = httpClient
                        .ws()
                        .url(sanitizedUrl)
                        .withMethod(method)
        
                if (headers != null && !headers.isEmpty()) {
                    def scalaHeaders = scala.collection.JavaConversions.asScalaBuffer(
                            headers.entrySet().inject([] as List<scala.Tuple2<String, String>>) { List<scala.Tuple2<String, String>> result, kv ->
                                kv.value.each { v -> result.add(pair(kv.key, v) as scala.Tuple2<String, String>) }
                                result
                            }
                    )
                    request = request.withHeaders(scalaHeaders)
                }
        
                if (body != null) {
                    def writable = play.api.http.Writeable$.MODULE$.wString(play.api.mvc.Codec.utf_8())
                    request = request.withBody(body, writable)
                }
        
                response = await(request.execute())
        
        
            } catch (Exception e) {
                throw new com.exalate.api.exception.IssueTrackerException("Unable to perform the request $method $path, please contact Exalate Support: ".toString() + e.message, e)
            }
            if (response.status() >= 300 && response.status() != 400) {
            //if (response.status() >= 300) {
                throw new com.exalate.api.exception.IssueTrackerException("Failed to perform the request $method $path ${body ? "with body `$body`".toString() : ""}(status ${response.status()}), please contact Exalate Support: ".toString() + response.body())
            }
            response.body() as String
        }
        
        

        And the request to Insight itself:

        def js = new groovy.json.JsonSlurper()
        
        def response = js.parseText(http(
                "GET",
                "/v1/object/" + objectId,
                workspaceId,
                null,
                null,
                [
                        "Authorization":["Bearer ${token}".toString()],
                        "Content-Type":["application/json"],
                ]
        ))

        Hope this clarifies it a bit.

      7. Charlie Misonne

        Hi Joachim,


        I finally got the time to take a look at this because it was planned in our current sprint.

        First I tried to put code with the customized http client in the incoming Azure Devops script but  I noticed the injector is not available.

        So I'm doing this in the Jira outgoing script now.


        1. This does not work
          final def injector = play.api.Play$.MODULE$.current().injector()

          I think injector is already available in the script so I removed that line

        2. def gsp = injector.instanceOf(com.exalate.api.persistence.issuetracker.jcloud.IJCloudGeneralSettingsPersistence.class)
          def gsOpt = await(gsp.get())

        I'm getting this on await:
        No signature of method: org.codehaus.groovy.jsr223.GroovyScriptEngineImpl.await() is applicable for argument types: (scala.concurrent.impl.Promise$DefaultPromise) values: [Future(Success(Some(JCloudGeneralSettings(Some(1),Some(https://jcloudnode-fogo-loft-blan-pear.exalate.cloud),https://dcsecosystem.atlassian.net,Some(ATCOzLwOqmOWHlr1p8E6Qh0HBbHHxjdhhMxnmp34MuqqO2cRDBbR6k2IV2WKRiFu13CqamY7u2KFKPBEAj3_nY_BtA3D001E05),Some(eyJob3N0S2V5IjoiYjVmMmRmNDEtN2I4NC0zNDljLTk5MjgtY2MwNThmYWYxZTJhIiwiYWRkb25LZXkiOiJjb20uZXhhbGF0ZS5qaXJhbm9kZSJ9),Some(b5f2df41-7b84-349c-9928-cc058faf1e2a),Some(MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDKQUn5qivi6JzLEzKLbhcuHYpSSjg08Xe5Apd9F2EZPsXWjxgLqfh56Sow6KokVRSMVDQqf+Dby5SBfpbkttZnZczOI92nMYAAhAHb9mTVngj9SEj6rhLD+4MINgjH5EA9kvFJPSbBVFQqWkVmJ/aB6J1dP9BpFBY/ZvVyo18MrwIDAQAB),true,true,false,true,true,false))))] Possible solutions: wait(), wait(long), wait(long, int), split(groovy.lang.Closure), with(groovy.lang.Closure), any()

      8. Charlie Misonne

        Hi Joachim Bollen 

        Perhaps you missed my previous comment.


        The script you provided does not seem to work. Does it work for you seemless on Jira Cloud?

        Perhaps we are on different Exalate versions...

      CommentAdd your comment...