Attachment Syncing errors

Originally asked by Elyashiv on 27 October 2021 (original question)


Hi,

In our Exalate - Jira configuration we have a syncing rule we have internal syncing data.

We have in our company R&D developers and agents that support customers.

The workflow is: a customer opens a ticket, the agent gets it and he decides if R&D is needed.

If yes, he opens a new ticket to R&D that is synced with the original customer ticket.

The sync allows R&D to see all the history of the customer request.

Although the sync includes attachments and comments, we have a problem with it. To prevent infinite syncing, we were advised to add a prefix “CON_” to the attachment file names. This addition makes a mess in the attachment list of the issue, and the users don’t like it too much. Beyond that, renaming the file disrupts the preview of thumbnails in the issue comments.

Is there another way to prevent infinite syncing without renaming the files? Maye be changing a different property of the attachments?

Thanks


Comments:

Serhiy Onyshchenko commented on 28 October 2021

Hello, Elyashiv , as far as I understand, your sync set up was following this guide: https://docs.idalko.com/exalate/x/xoCKAg
And according ti this picture,

the addition of the file prefix happens in the Internal Service Desk issue, right?
Later on on the same instance you use these prefixes to avoid syncing attachments from Dev A to Dev B and visa-versa, correct?

If all of my suggestions were right so far, might I ask you to share the outgoing sync scripts that you have on the Internal Service Desk side, please?
I’ll suggest a script that doesn’t rely on file prefixes but instead uses Exalate’s own sync metadata to make sure it doesn’t sync the wrong attachments over.

Regards, Serhiy.

Elyashiv commented on 31 October 2021

Hi Serhiy Onyshchenko ,

Your suggestions are correct, this is our case.

This is the relevant code from the relevant rule of the outgoing synchronization of attachments:

And this is the relevant code from the incoming sync rule:

We have a few internal syncing rules and in each one, we have a different prefix: “CON1_” “CON2_” etc.

Serhiy Onyshchenko commented on 04 November 2021

Hey, Elyashiv ,
instead of filtering by filename, we could use internal sync link metadata which describes, how attachments and comments relate to one another…
Please, try replacing (in Outgoing sync) your current :

replica.attachments = issue.attachments.findAll { attachment ->
    attachment.filename.contains("CON1") ||
	!attachment.filename.contains("CON")
}

with

if (issue.key == "TEST-1") {
    def syncedAttachmentIds = nodeHelper
            .twinTraceRepository
            .findByLocalIssueKey(issueKey)
            .inject([]) { result, tt ->
                if (tt.connection.ID == connection.ID) { return result }
                def localAttachmentIds = tt
                        .fieldTraces
                        .get(com.exalate.api.domain.twintrace.TraceType.ATTACHMENT)
                        .localId
                result.addAll(localAttachmentIds)
                result
            }

    replica.attachments = issue.attachments.findAll { a ->
        !syncedAttachmentIds.contains(a.id as String)
    }
} else {
	replica.attachments = issue.attachments.findAll { attachment ->
    	attachment.filename.contains("CON1") ||
		!attachment.filename.contains("CON")
	}
}

And for Incoming sync, replace:

if(issue.key == "TEST-1") {
 	issue.attachments += replica.addedAttachments
} else {
	issue.attachments += replica.addedAttachments.collect { attachment ->
    	attachment.filename = "CON1"+attachment.filename
		attachment
	}
}

Note that I used TEST-1 as an example, replace it with an issue key which you’d like to test this out on

Regards, Serhiy.

Elyashiv commented on 07 November 2021

Hi Serhiy Onyshchenko,

Thanks for your answer!

Unfortunately, it didn’t work as expected.

R&D ticket (Dev A) didn’t get the attachments at all - from the customer ticket (Internal SD).

Elyashiv commented on 22 November 2021

Hi Serhiy Onyshchenko ,

Do you have any other solution?

Thanks

Serhiy Onyshchenko commented on 30 November 2021

Hello, Elyashiv , sorry for being so slow on turn around. Feel free to ping Saskia so that she chases me (old community)
Let’s troubleshoot if the suggested approach can work

if (issue.key == "TEST-1") {
    def syncedAttachmentIds = nodeHelper
            .twinTraceRepository
            .findByLocalIssueKey(issueKey)
            .inject([]) { result, tt ->
                if (tt.connection.ID == connection.ID) { return result }
                def localAttachmentIds = tt
                        .fieldTraces
                        .get(com.exalate.api.domain.twintrace.TraceType.ATTACHMENT)
                        .localId
                result.addAll(localAttachmentIds)
                result
            }
    debug.error("issue.attachments.id=${issue.attachments.id} syncedAttachmentIds=$syncedAttachmentIds")

    replica.attachments = issue.attachments.findAll { a ->
        !syncedAttachmentIds.contains(a.id as String)
    }
} else {
	replica.attachments = issue.attachments.findAll { attachment ->
    	attachment.filename.contains("CON1") ||
		!attachment.filename.contains("CON")
	}
}
Elyashiv commented on 01 December 2021

Hi Serhiy Onyshchenko ,

The script didn’t run until the debug line.

It stopped with error:

Script error for issue TMH1-23. Details: Cannot get property 'localId' on null object. 

referring to the line:

def localAttachmentIds = tt
   						.fieldTraces
                        .get(com.exalate.api.domain.twintrace.TraceType.ATTACHMENT)
                        .localId
Serhiy Onyshchenko commented on 01 December 2021

Huh, let’s try and fix that:

if (issue.key == "TEST-1") {
    def syncedAttachmentIds = nodeHelper
            .twinTraceRepository
            .findByLocalIssueKey(issueKey)
            .inject([]) { result, tt ->
                if (tt.connection.ID == connection.ID) { return result }
                def localAttachmentIds = tt
                        .fieldTraces
                        .get(com.exalate.api.domain.twintrace.TraceType.ATTACHMENT)
                        ?.localId ?: []
                result.addAll(localAttachmentIds)
                result
            }
    debug.error("issue.attachments.id=${issue.attachments.id} syncedAttachmentIds=$syncedAttachmentIds")

    replica.attachments = issue.attachments.findAll { a ->
        !syncedAttachmentIds.contains(a.id as String)
    }
} else {
	replica.attachments = issue.attachments.findAll { attachment ->
    	attachment.filename.contains("CON1") ||
		!attachment.filename.contains("CON")
	}
}

Please, let me know, how that goes.

Elyashiv commented on 01 December 2021

Hi Serhiy Onyshchenko

this is the error now:

Script error for issue TMH1-23. Details: No such property: debug for class: Script12954. Error line: Script12954.groovy:38
Elyashiv commented on 09 January 2022

Hi Serhiy Onyshchenko ,
We still have an issue.

referring to this scheme:

If we add an attachment to the “Dev A” issue (using a comment or regularly attaching an attachment to the issue) we will see it in all connected issues (Internal Service Desk + Dev A + Dev B).

We are supposed to see the attachment only on “Dev A” & “Internal Service Desk” issues. Any other *“Dev”*issues are not supposed to get the attachment.

I will add that if you comment "Dev A" you will see the comment only on the "Internal Service Desk" issue.

Can you assist?

Serhiy Onyshchenko commented on 14 January 2022

Hey, Elyashiv , seeing my snail-speed at helping you, let me take another approach and take my time to set it up in my env to give you a video and full description of the set up to get it working. Let me get back to you with an update on this Monday Jan 17th.

Elyashiv commented on 16 January 2022

OK Serhiy Onyshchenko

Thanks.

Serhiy Onyshchenko commented on 17 January 2022

Hey, Elyashiv , a small update: got the case setup, starting to run the debug.error I suggested earlier to see what I’ll find.
Let me give an update on my findings Tomorrow

Serhiy Onyshchenko commented on 18 January 2022

Hey, Elyashiv
So the suggestion with the traces doesn’t seem to work, as Exalate creates traces after attaching the attachments. I’ll be looking into other places - entity properties. Let me come back to you on Thursday with an update.

Serhiy Onyshchenko commented on 20 January 2022

Hey, Elyashiv , looking into options to store multiple records for the issue so that Exalate knows that a certain attachment comes from a certain connection.

Currently exploring this option:
https://docs.atlassian.com/software/jira/docs/api/8.13.16/com/atlassian/jira/entity/property/EntityPropertyService.html#setProperty-com.atlassian.jira.user.ApplicationUser-com.atlassian.jira.entity.property.EntityPropertyService.SetPropertyValidationResult-

Regards, Serhiy.

Serhiy Onyshchenko commented on 24 January 2022

Hello, Elyashiv , let me give you an update on Wed 26ths re the entity property option

Serhiy Onyshchenko commented on 28 January 2022

Hey, Elyashiv , apologies for delayed update, the property service approach bears fruit - already able to note stuff to the issue property before the attachments are actually attached to the issue. Next update on Tue Feb 1st.

Elyashiv commented on 30 January 2022

Hi Serhiy Onyshchenko ,

I will wait.

Thanks in meantime.

Answer by Serhiy Onyshchenko on 01 February 2022

Hello, Elyashiv
Here’s a solution :smile:

SD_to_DEV1 SD_to_DEV2
​Out ``` … replica.resolution = issue.resolution replica.status = issue.status replica.parentId = issue.parentId replica.priority = issue.priority if (issue.project.key == “SDT”) { def _IssuePropertyService = com.atlassian.jira.component.ComponentAccessor.getClassLoader().loadClass(“com.atlassian.jira.bc.issue.properties.IssuePropertyService”) // com.atlassian.jira.bc.issue.properties.IssuePropertyService ips = com.atlassian.jira.component.ComponentAccessor.getComponent(com.atlassian.jira.bc.issue.properties.IssuePropertyService.class) def ips = com.atlassian.jira.component.ComponentAccessor.getComponent(_IssuePropertyService) def nserv = com.atlassian.jira.component.ComponentAccessor.getOSGiComponentInstanceOfType(com.exalate.api.node.INodeService.class) def errorCollectionToStr = { ec → “${ec.errorMessages.join(”, “)} ; ${ec.errors.collect { k, v → “$k:$v” }.join(”, “)}” } def proxyAppUser = nserv.getProxyUser() def propsResult = ips.getProperties( proxyAppUser, issue.id as Long ) def js = new groovy.json.JsonSlurper() def addedAttachments = propsResult .collect { prop → if (prop.key.startsWith(“exalate-attachment-added”)) js.parseText(prop.value) else null } .findAll() .flatten() //debug.error(“addedAttachments=$addedAttachments”) replica.attachments = issue.attachments.findAll { a → !addedAttachments.any { addedAttachment → addedAttachment.filename.equals(a.filename) && addedAttachment.filesize == a.filesize } } replica.comments = issue.comments.findAll { comment → comment.author.username == “SyncCon1”
In ... if (replica.project.key == "AAA") { issue.comments = commentHelper.mergeComments(issue, replica, { comment -> comment.roleLevel = "Team" comment.executor = nodeHelper.getUserByUsername("SyncCon1") comment }) def _IssuePropertyService = com.atlassian.jira.component.ComponentAccessor.getClassLoader().loadClass("com.atlassian.jira.bc.issue.properties.IssuePropertyService") //com.atlassian.jira.bc.issue.properties.IssuePropertyService ips = com.atlassian.jira.component.ComponentAccessor.getComponent(com.atlassian.jira.bc.issue.properties.IssuePropertyService.class) def ips = com.atlassian.jira.component.ComponentAccessor.getComponent(_IssuePropertyService) def nserv = com.atlassian.jira.component.ComponentAccessor.getOSGiComponentInstanceOfType(com.exalate.api.node.INodeService.class) def errorCollectionToStr = { ec -> "${ec.errorMessages.join(", ")} ; ${ec.errors.collect { k, v -> "$k:$v" }.join(", ")}" } def proxyAppUser = nserv.getProxyUser() def nowMilli = java.time.Instant.now().toEpochMilli() def json = ({ def attachmentJsons = replica .addedAttachments .inject([]) { result, attachment -> result += ["filename":attachment.filename,"filesize":attachment.filesize] result } if (attachmentJsons.empty) return null else groovy.json.JsonOutput.toJson(attachmentJsons) })() if (json) { def setPropValResult = ips.validateSetProperty( proxyAppUser, issue.id as Long, new com.atlassian.jira.entity.property.EntityPropertyService.PropertyInput(json, "exalate-attachment-added-$nowMilli") ) if (setPropValResult.valid) { ips.setProperty(proxyAppUser, setPropValResult) } else { debug.error("Failed to set a property `${"exalate-attachment-added-${connection.name}-$nowMilli"}` to issue ${issue.key} (${issue.id}): ${errorCollectionToStr(setPropValResult.errorCollection)}") } } //debug.error("blocking sync to try reading / writing via properties API") } issue.attachments = attachmentHelper.mergeAttachments(issue, replica) ... ... if (replica.project.key == "BBB") { issue.comments = commentHelper.mergeComments(issue, replica, { comment -> comment.roleLevel = "Team" comment.executor = nodeHelper.getUserByUsername("SyncCon2") comment }) def _IssuePropertyService = com.atlassian.jira.component.ComponentAccessor.getClassLoader().loadClass("com.atlassian.jira.bc.issue.properties.IssuePropertyService") //com.atlassian.jira.bc.issue.properties.IssuePropertyService ips = com.atlassian.jira.component.ComponentAccessor.getComponent(com.atlassian.jira.bc.issue.properties.IssuePropertyService.class) def ips = com.atlassian.jira.component.ComponentAccessor.getComponent(_IssuePropertyService) def nserv = com.atlassian.jira.component.ComponentAccessor.getOSGiComponentInstanceOfType(com.exalate.api.node.INodeService.class) def errorCollectionToStr = { ec -> "${ec.errorMessages.join(", ")} ; ${ec.errors.collect { k, v -> "$k:$v" }.join(", ")}" } def proxyAppUser = nserv.getProxyUser() def nowMilli = java.time.Instant.now().toEpochMilli() def json = ({ def attachmentJsons = replica .addedAttachments .inject([]) { result, attachment -> result += ["filename":attachment.filename,"filesize":attachment.filesize] result } if (attachmentJsons.empty) return null else groovy.json.JsonOutput.toJson(attachmentJsons) })() if (json) { def setPropValResult = ips.validateSetProperty( proxyAppUser, issue.id as Long, new com.atlassian.jira.entity.property.EntityPropertyService.PropertyInput(json, "exalate-attachment-added-$nowMilli") ) if (setPropValResult.valid) { ips.setProperty(proxyAppUser, setPropValResult) } else { debug.error("Failed to set a property `${"exalate-attachment-added-${connection.name}-$nowMilli"}` to issue ${issue.key} (${issue.id}): ${errorCollectionToStr(setPropValResult.errorCollection)}") } } issue.attachments = attachmentHelper.mergeAttachments(issue, replica) //debug.error("blocking sync to try reading / writing via properties API") } else { issue.comments = commentHelper.mergeComments(issue, replica) issue.attachments = attachmentHelper.mergeAttachments(issue, replica) } ...

Happy Exalating!