Many Issue Links in Issue Sync generates "Issue Tracker Error" on Destination

Hi
There we use this incoming:

def projKey = "XXXXX"

// =====================
// UNEXALATE "INTERNES"
// =====================
if (replica.type?.name == "Internes") {
// stop sync if the issue type has changed to "Internes"
if (!firstSync) {
syncHelper.unExalateAfterProcessing()
}
return
}

// =====================
// PROJECT & ISSUE TYPE (First Sync Only)
// =====================
if (firstSync) {
issue.projectKey = projKey
}
issue.type = nodeHelper.getIssueType(replica.type?.name)

// =====================
// STANDARD FIELDS
// =====================
issue.summary    			= replica.summary
issue.resolution 			= replica.resolution
issue.labels     			= replica.labels
issue.description 			= replica.description
issue.parentId         		= replica.parentId
issue.project          		= replica.project
issue.resolution       		= replica.resolution
issue.components 			= replica.components
issue.changedComments 		= replica.changedComments
issue.removedComments 		= replica.removedComments
issue.changeHistory  		= replica.changeHistory
issue.watches          		= replica.watches
issue.created          		= replica.created
issue.updated          		= replica.updated
issue.due              		= replica.due
//issue.entityProperties 	= replica.entityProperties
issue.securityLevel    		= replica.securityLevel
issue.issueLinks 			= replica.issueLinks

// =====================
// NVX AUTOR
// =====================
def autor = replica.reporter
if (autor) {
def displayName = autor.displayName ?: autor.name ?: autor.key ?: "Unbekannt"
def email       = autor.email

// If email is present, put it on the second line in parentheses
def authorText = email
    ? "${displayName}\n(${email})"
    : displayName

issue.customFields."Ticket-Autor".value = authorText as String

}

// =====================
//  TICKETNUMMER
// =====================
if (replica.key) {
issue.customFields."Ticketnummer".value = replica.key.toString()
//syncHelper.syncBackAfterProcessing()
}

// =====================
// BEARBEITER
// =====================
def b = replica.assignee
if (b) {
def displayName = b.displayName ?: b.name ?: b.key ?: "Unbekannt"
def email       = b.email

// If email is present, put it on the second line in parentheses
def assigneeText = email
    ? "${displayName}\n(${email})"
    : displayName

issue.customFields."Ticket-Bearbeiter".value = assigneeText as String

}

// =====================
// KEY USER
// =====================

if (replica.customFields."Key User"?.value) {
def rawValue = replica.customFields."Key User".value

// Value often comes as a list with one entry for single-user pickers
def userRef = (rawValue instanceof List) ? rawValue[0] : rawValue

// userRef may be a Map with an "@key" entry, or already a resolved user-like object
def userKey = userRef instanceof Map ? userRef."@key" : userRef?.key

def resolvedUser = userKey ? nodeHelper.getUser(userKey) : null

def displayText = resolvedUser?.displayName ?: resolvedUser?.name ?: userKey ?: "Unbekannt"

issue.customFields."Key User".value = displayText as String

}

/*def keyUser = replica.customFields."Key User"?.value
def fallbackUser = nodeHelper.getUserByEmail("disp_kunde@domain.com")

if (firstSync) {
if (keyUser) {
def destinationUser = nodeHelper.getUserByEmail(keyUser.email)
issue.customFields."Key User".value = destinationUser ?: fallbackUser
} else {
issue.customFields."Key User".value = fallbackUser
}}
*/

// =====================
// SUB-TASKS
// =====================
/*if(firstSync && replica.parentId && replica.issueType.name == "Sub-task"){
issue.typeName     = "Sub-task" //Make sure to use the right subtask type here.
def localParent = nodeHelper.getLocalIssueFromRemoteId(replica.parentId.toLong())
if(localParent){
issue.parentId = localParent.id
} else {
throw new com.exalate.api.exception.IssueTrackerException("Subtask cannot be created: parent issue with remote id " + replica.parentId + " was not found. Please make sure the parent issue is synchronized before resolving this error" )
}
}
*/
// =====================
// STATUS
// =====================

issue.status = replica.status

// =====================
// PRIORITÄT
// =====================

//def defaultPriority = nodeHelper.getPriority("Trivial")
issue.priority = nodeHelper.getPriority(replica.priority?.name) //?: defaultPriority

// =====================
// RESOLUTION
// =====================
issue.resolution = replica.resolution

// =====================
// REPORTER
// =====================
if (firstSync) {
def destReporter = null

// 1. Try by email
try {
destReporter = nodeHelper.getUserByEmail(replica.reporter?.email)
} catch (Exception e) {
// email hidden or not found
}

// 2. Try by key
if (destReporter == null) {
try {
destReporter = nodeHelper.getUser(replica.reporter?.key)
} catch (Exception e) {
// key not found in destination
}
}

// 3. Try by full name
if (destReporter == null) {
try {
destReporter = nodeHelper.getUserByFullName(replica.reporter?.displayName)
} catch (Exception e) {
// full name not found
}
}

// 4. Fallback to disp_kunde@domain.com if still not found
if (destReporter == null) {
try {
destReporter = nodeHelper.getUserByEmail("disp_kunde@domain.com")
} catch (Exception e) {
// fallback account not found — Exalate proxy user will be used as last resort
}
}

if (destReporter != null) {
issue.reporter = destReporter
}
}
// =====================
// ASSIGNEE
// =====================
if (firstSync) {
if (replica.assignee != null) {
def destUser = null
// 1. Try by email
try {
destUser = nodeHelper.getUserByEmail(replica.assignee?.email)
} catch (Exception e) {}
// 2. Try by key
if (destUser == null) {
try {
destUser = nodeHelper.getUser(replica.assignee?.key)
} catch (Exception e) {}
}
// 3. Try by full name
if (destUser == null) {
try {
destUser = nodeHelper.getUserByFullName(replica.assignee?.displayName)
} catch (Exception e) {}
}
// 4. Try to assign — if user has assignable permission, assign; else leave as is (unassigned or current assignee)
if (destUser != null) {
try {
if (nodeHelper.isUserAssignable(issue.projectKey, destUser)) {
issue.assignee = destUser
} // else: leave as is (unassigned or current assignee)
} catch (Exception e) {
// Suppress error, leave as is
}
} // else: leave as is (unassigned or current assignee)
}
}

// =====================
// CUSTOM FIELDS
// =====================

issue.customFields."Gewünschtes Lieferdatum".value   	= replica.customFields."Gewünschtes Lieferdatum"?.value
issue.customFields."Externe Vorgangsnummer".value 		= replica.customFields."Externe Vorgangsnummer"?.value
issue.customFields."Auftragsklarheit".value 		= replica.customFields."Auftragsklarheit"?.value?.value?.toString()
issue.customFields."Releasemanagement".value      = replica.customFields."Releasemanagement".value

// =====================
// ERHÖHT SUPPORTGRUNDLAGE
// =====================
if (replica.customFields."Erhöht Supportgrundlage"?.value?.value) {
issue.customFields."Erhöht Supportgrundlage".value =
replica.customFields."Erhöht Supportgrundlage".value.value.toString()
}

// =====================
// ERHÖHT SUPPORTGRUNDLAGE AB
// =====================
if(replica.customFields."Erhöht Supportgrundlage ab"){
issue.customFields."Erhöht Supportgrundlage ab".value = replica.customFields."Erhöht Supportgrundlage ab".value
}

// =====================
// AUFWAND
// =====================
def formatAufwand = { rawValue ->
if (rawValue == null) return null
def d = rawValue.toDouble()
def germanFormat = new java.text.DecimalFormat("#.##", new java.text.DecimalFormatSymbols(Locale.GERMANY))
germanFormat.setGroupingUsed(false) // avoid thousands separator dots, e.g. "1.234,25"
return germanFormat.format(d)
}

if (replica.customFields."Aufwand (angefragt)") {
def rawValue = replica.customFields."Aufwand (angefragt)".value
issue.customFields."Aufwand (angefragt)".value = formatAufwand(rawValue)
}

if (replica.customFields."Aufwand (Std.)") {
def rawValue = replica.customFields."Aufwand (Std.)".value
issue.customFields."Aufwand (Std.)".value = formatAufwand(rawValue)
}

// =====================
// ANSPRECHPARTNER
// =====================
if (replica.customFields."Ansprechpartner"?.value) {
def rawValue = replica.customFields."Ansprechpartner".value

// Value often comes as a list with one entry for single-user pickers
def userRef = (rawValue instanceof List) ? rawValue[0] : rawValue

// userRef may be a Map with an "@key" entry, or already a resolved user-like object
def userKey = userRef instanceof Map ? userRef."@key" : userRef?.key

def resolvedUser = userKey ? nodeHelper.getUser(userKey) : null

def displayText = resolvedUser?.displayName ?: resolvedUser?.name ?: userKey ?: "Unbekannt"

issue.customFields."Ansprechpartner".value = displayText as String

}

// =====================
// ORIGINAL ERSTELLUNGSDATUM
// =====================
issue.customFields."Erstellungsdatum".value           = replica.created?.toString()  // Original Erstellungsdatum

// =====================
// ORIGINAL ÄNDERUNGSDATUM
// =====================
issue.customFields."Änderungsdatum".value = replica.updated?.toString()

// =====================
// KUNDE BEARBEITER
// =====================
// This will override the above if present
// Only assign if user is assignable, else leave as is
/*
def assigneeDisplayName = replica.customFields."Externer Bearbeiter (Kunde)"?.value?.asString
if (assigneeDisplayName) {
def assignee = nodeHelper.getUserByFullName(assigneeDisplayName)
if (assignee && nodeHelper.isUserAssignable(issue.projectKey, assignee)) {
issue.assignee = assignee
} // else: leave as is
}

*/

// =====================
// FIX VERSIONS
// =====================
def fixVersionNames = replica.fixVersions?.collect { it.name }?.join(", ") ?: ""

issue.customFields."Lösungsversion (Fix Version)".value = fixVersionNames

// =====================
// ATTACHMENTS + UPLOAD COMMENT
// =====================
def dateFormat = new java.text.SimpleDateFormat("dd.MM.yyyy HH:mm")
dateFormat.setTimeZone(java.util.TimeZone.getTimeZone("Europe/Vienna"))

// Snapshot existing filenames before merge
def existingNames = issue.attachments?.collect { it.filename }?.toSet() ?: 
 as Set
// Merge attachments
issue.attachments = attachmentHelper.mergeAttachments(issue, replica)
// Only add upload comments after the first sync
if (!firstSync) {
def newAttachments = issue.attachments?.findAll { a ->
!existingNames.contains(a.filename)
} ?: 

newAttachments.each { a ->
def uploaderName = a.author?.displayName ?: replica.reporter?.displayName ?: "Unknown User"
def uploadDate = a.created ? dateFormat.format(a.created) : "unbekanntes Datum"
def comment = new com.exalate.basic.domain.hubobject.v1.BasicHubComment()
comment.body = "${uploaderName} hat am ${uploadDate} ein Dokument hochgeladen: ${a.filename}"
comment.author = a.author
comment.internal = false
comment.restrictSync = true  // never sync this comment to the other side
issue.comments << comment
}
}

// =====================================================
// COMMENTS (Merge + Format)
// =====================================================
if (replica.comments) {
def dateFormatter = new java.text.SimpleDateFormat("dd.MM.yyyy HH:mm")
dateFormatter.setTimeZone(java.util.TimeZone.getTimeZone("Europe/Vienna"))

def formatDate = { rawDate ->
    if (!rawDate) return ""
    try {
        if (rawDate instanceof java.util.Date) {
            return dateFormatter.format(rawDate)
        }
        return dateFormatter.format(new java.util.Date(rawDate.toString().isLong() ? rawDate.toString().toLong() : java.sql.Timestamp.valueOf(rawDate.toString()).time))
    } catch (Exception e) {
        return rawDate.toString()
    }
}

issue.comments = commentHelper.mergeComments(issue, replica, { comment ->
    if ((comment.body ?: "").contains("uploaded attachment:")) {
        return null
    }
    def email = comment.author?.email
    def localUser = email ? nodeHelper.getUserByEmail(email) : null
    def authorName = comment.author?.displayName ?: comment.author?.name ?: "Jemand"
    def commentDate = formatDate(comment.created)
    def prefix     = "${authorName} hat am ${commentDate} kommentiert:"

    if (!(comment.body ?: "").startsWith("${authorName} hat")) {
        comment.body = "${prefix}\n${comment.body ?: ""}\n"
    }
    return comment
})

}

When syncing from Source issues with lots (35+) Issue Links, the Destination throws “Issue Tracker Error”, but “Resolve all” for the error populates the Destination, until the error comes again, and then we need to ignore it.
When updating the same issue, the same scenario, but then we need to ignore the error => not Field updates happening.

In a earlier script we had at the end, for the comments this return statement which might have caused the problem.
if (!replica.comments) {
return
}

But its removed now - see end of attached script
// ===================================================== // COMMENTS (Merge + Format) // =====================================================
.............

Any Ideas anybody ? we hit a dead end…
Thanks & BR

It looks like you’re running into “Issue Tracker Error” on the destination side when syncing issues with a large number of issue links (35+), and the error can be temporarily resolved with “Resolve all,” but it recurs on updates. This is a common challenge when syncing issues with many links, especially in Jira environments.

Here are some key points and suggestions based on the documentation:

  1. Bulk Issue Link Updates Can Cause Errors
    When syncing a high number of issue links at once, Jira’s API or the destination instance may hit internal limits or timeouts, resulting in “Issue Tracker Error.” This is especially true if some of the linked issues are not yet synchronized or if there are permission issues.

  2. Handling Issue Links in the Sync Script
    The line issue.issueLinks = replica.issueLinks attempts to set all links in one go. If some of the linked issues do not exist yet on the destination, or if there are circular dependencies, this can cause errors.
    The recommended approach is to use the issueLinkHelper to add or remove links more granularly, and to check if the linked issues exist before attempting to link them.

  3. Possible Solutions

    • Defer Linking Until All Issues Are Synced:
      Only create links if the destination issues exist. You can check for the existence of each linked issue using nodeHelper.getLocalIssueFromRemoteKey(link.key) before adding the link.
    • Limit the Number of Links Per Sync:
      If possible, batch the linking process or limit the number of links processed in a single sync to avoid overwhelming the API.
    • Error Handling:
      Wrap your linking logic in try/catch blocks to handle failures gracefully and log problematic links for manual review.
  4. Script Example for Safe Linking
    Instead of assigning all links at once, you can use a loop to add links only if the destination issue exists:

    if (replica.issueLinks) {
        replica.issueLinks.each { link ->
            def localIssue = nodeHelper.getLocalIssueFromRemoteKey(link.key)
            if (localIssue) {
                issueLinkHelper.addIssueLink(issue, localIssue, link.type, link.direction)
            }
        }
    }
    

    This approach helps avoid errors caused by missing linked issues.

  5. Documentation Reference
    For more details and best practices on syncing issue links, see:
    How to synchronize issue links

Summary:
The “Issue Tracker Error” is likely due to trying to set too many issue links at once, especially when some linked issues don’t exist yet on the destination. Switch to a more granular approach using issueLinkHelper and check for the existence of each linked issue before linking. This should make your syncs more robust and reduce errors.

Let me know if you need a more detailed script example or further troubleshooting steps!