# News feed for ongoing Exalate updates

**URL:** <https://community.exalate.com/t/news-feed-for-ongoing-exalate-updates/5489>\
**Category:** General Questions\
**Tags:** exalate, jira-cloud, worklog, changehistory, migrated\_question\
**Created:** [November 4, 2024, 8:07am UTC](https://community.exalate.com/t/news-feed-for-ongoing-exalate-updates/5489 "2024-11-04T08:07:44Z")\
**Posts on this page:** 3\
**Page:** 1

<div class="post-metadata">

**Author:** ![xl8bot](https://dub1.discourse-cdn.com/flex018/user_avatar/community.exalate.com/xl8bot/32/324_2.png) [@xl8bot](https://community.exalate.com/u/xl8bot)\
**Post date:** [November 4, 2024, 8:07am UTC](https://community.exalate.com/t/news-feed-for-ongoing-exalate-updates/5489/1 "2024-11-04T08:07:44Z")

</div>

_Originally asked by Guy Toyber on 11 May 2023 [(original question)](https://oldcommunity.exalate.com/questions/58501849)_

* * *

Hi there,

I would like to know if it will be possible to create some custom field to include a news-feed that will update all Exalate sync events.

The sync could include some metadata, like the user who trigger the sync; time stamp ; the custom field and more.

This feature will increase the clarity about the sync process for all end users who would like to know why a field was change and who did it.

Is it something that you can support?

Regards,

Guy

* * *

#### Comments:

> **Serhiy Onyshchenko commented on 12 May 2023**
>
> > Hey, Guy Toyber ,
> 
> Thanks for raising this on community,
> 
> While the audit log feature is being considered by our product team, I think we could work it around by either:
> 
> - adding a comment
> - modifying a custom field called “Exalate Change Reason”
> 
> each time that Exalate is processing an incoming sync with some text clarifying the reasons why it did so (taken from the change history of the remote side).
> 
> Let me come back to you with an answer next Tuesday May 16th 2023.
> 
> Regards, Serhiy.

---

<div class="post-metadata">

**Author:** ![xl8bot](https://dub1.discourse-cdn.com/flex018/user_avatar/community.exalate.com/xl8bot/32/324_2.png) [@xl8bot](https://community.exalate.com/u/xl8bot)\
**Post date:** [November 4, 2024, 8:07am UTC](https://community.exalate.com/t/news-feed-for-ongoing-exalate-updates/5489/2 "2024-11-04T08:07:46Z")

</div>

_Answer by Serhiy Onyshchenko on 16 May 2023_

Hey, Guy Toyber ,

Thanks for reaching out to the community for this one.

There’s a way to configure Exalate to track the reasoning behind updates in a text custom field.

For this example, I’m using Jira \<\> Jira synchronization.

Here’s a video showing the solution:

Basically, I needed to configure first the conflict handling (from this community article: [Avoid updating issue with older changes ADO \<\> Jira, Jira \<\> Jira, ServiceNow \<\> Jira](https://oldcommunity.exalate.com/questions/42839472/avoid-updating-issue-with-older-changes-ado-jira-jira-jira-servicenow-jira-zendesk-jira)_(old community)_),  
and then modify it quite a bit (Chapter 2) to have more useful info in the reasoning log.

# Chapter 1: main logic

## On the source side

I needed to add

```auto
replica.changeHistory = issue.changeHistory
replica.created = issue.created

```

to the outgoing sync script

## On destination side

I need to create some sort of a place where I’d aggregate all the reasons for changes:

```auto
def changeLogBuilder = new StringBuilder();

```

I need to describe the reasons, why the source triggered synchronization:

```auto
def latestChange = replica
    .changeHistory
    .sort { c -> c.created.time }
    .reverse()
    .find()
def latestChangeMsg = (latestChange == null) ? 
    "${replica.key} created by ${userStr(replica.reporter)} on ${replica.created}" : 
    "${latestChange.changeItems.field.join(", ")} updated by ${userStr(latestChange.author)} on ${latestChange.created}"
    
def syncMsg = firstSync ?
    "First sync" :
    "Sync"
changeLogBuilder
    .append("# $syncMsg triggered due to $latestChangeMsg")

```

On the incoming, for every line like

```auto
issue.AAA = ...

```

I needed to add some reasoning to the changeLogBuilder:

```auto
changeLogBuilder
  .append("\n# ")
  .append(
       "Overwriting AAA because Exalate is configured to do it on every sync"
  )

```

if the line is surrounded by an if:

```auto
if (XXX && YYY) {
	issue.ZZZ = ...
}

```

Then both XXX and YYY need to be mentioned in the reasoning message:

```auto
 changeLogBuilder
    .append("\n# ")
    .append("Setting ZZZ to ... because XXX and YYY")

```

if the right hand side of the assignment uses a ternary statement:

```auto
issue.AAA = BBB ? CCC : DDD

```

then the condition BBB has to be mentioned in the message:

```auto
def msg = BBB ? 
          "BBB is true and so AAA is set to ${CCC}" : 
          "BBB is false and so AAA is set to ${DDD}"

```

note, that elvis operator:

```auto
issue.XXX = YYY ?: ZZZ

```

is just a shorter ternary (same thing can be said this way):

```auto
issue.XXX = (YYY == null) ? YYY : ZZZ

```

For comments and attachments, since we’re using these special functions mergeComments and mergeAttachments, the reasoning also takes a special form:

```auto
issue.comments = commentHelper.mergeComments(issue, replica)
issue.attachments = attachmentHelper.mergeAttachments(issue, replica)

```

replaced with

```auto
issue.comments = commentHelper.mergeComments(issue, replica)
if (issue.comments != issueBeforeScript.comments) {
    if (!replica.removedComments.empty) {
        changeLogBuilder
         .append("\n# ")
         .append(
             "There were ${replica.removedComments.size()} comments removed on the remote side, so Exalate is going to remove them here too".toString()
         )
    }
    if (!replica.addedComments.empty) {
        changeLogBuilder
         .append("\n# ")
         .append(
             "There were ${replica.addedComments.size()} comments added on the remote side by ${replica.addedComments.collect { userStr(it?.author) }.join(", ")}, so Exalate is going to add them here too".toString()
         )
    }
}
issue.attachments = attachmentHelper.mergeAttachments(issue, replica)
if (issue.attachments != issueBeforeScript.attachments) {
    if (!replica.removedAttachments.empty) {
        changeLogBuilder
         .append("\n# ")
         .append(
             "There were ${replica.removedComments.size()} attachments removed on the remote side, so Exalate is going to remove them here too".toString()
         )
    }
    if (!replica.addedAttachments.empty) {
        changeLogBuilder
         .append("\n# ")
         .append(
             "There were ${replica.addedAttachments.size()} attachments added on the remote side by ${replica.addedAttachments.collect { userStr(it?.author) }.join(", ")}, so Exalate is going to add them here too".toString()
         )
    }
}

```

if you’re using conflict handling script ([Avoid updating issue with older changes ADO \<\> Jira, Jira \<\> Jira, ServiceNow \<\> Jira](https://oldcommunity.exalate.com/questions/42839472/avoid-updating-issue-with-older-changes-ado-jira-jira-jira-servicenow-jira-zendesk-jira)_(old community)_) for some fields, than your code looks like this:

```auto
if (firstSync) {
  ...
  issue."BBB" = replica."AAA"
  ...
} else {
  ...
  if(remoteFieldToLastUpdateDate."AAA" > localFieldToLastUpdateDate."BBB") {
	issue."BBB" = replica."AAA"
  }
  ...
}

```

I’d modified the conflict handling helper functions a bit (discussed in Chapter 2), so now these blocks would look like this instead:

```auto
if (firstSync || remoteFn()?."AAA"?.date > localFn()?."BBB"?.date) {
  issue."BBB" = replica."AAA"
  def reason = firstSync ? 
        "it's first sync, and there is no local value yet" :
        "it was changed by ${userStr(remoteFn()?."AAA"?.who)} "+
        "more recently (${remoteFn()?."AAA"?.date}) on remote side "+
        "than on local side (${localFn()?."BBB"?.date})"
  changeLogBuilder
     .append("\n# ")
     .append(
         "Overwriting \"BBB\" because $reason".toString()
     )
} else {
  changeLogBuilder
     .append("\n# ")
     .append(
         "Not touching \"BBB\" because "+
         "it is not the first sync and "+
         "it was changed by ${userStr(localFn()?."BBB"?.who)} "+
         "more recently (${localFn()?."BBB"?.date}) locally "+
         "than on the remote side (${remoteFn()?."AAA"?.date})".toString()
     )
}

```

Finally, we’re making sure the change log we’d been accumulating this whole time is put into the “Exalate Change Reason” custom field:

```auto
issue."Exalate Change Reason" = changeLogBuilder.toString()

```

# Chapter 2: helper methods and conflict handling update

So I had to change

```auto
// Community 42839472: Avoid updating issue with older changes ADO <> Jira
def fieldToLastUpdateDateFn = { exalateUserKey -> { history ->
    history
            .sort { c -> c.created.time }
            .reverse()
            .findAll { c ->
                c.author.key != exalateUserKey
            }
            .inject([:]) { result, c ->
                c.changeItems.inject(result) { r, i ->
                    String k = i.field
                    if (r[k] == null) {
                        r[k] = c.created
                    }
                    r
                }
            }
}}

```

into

```auto
def fieldToLastUpdateDateFn = { exalateUserKey -> { history ->
    history
        .sort { c -> c.created.time }
        .reverse()
        .findAll { c ->
            c.author.key != exalateUserKey
        }
        .inject([:]) { result, c ->
            c.changeItems.inject(result) { r, i ->
                String k = i.field
                if (r[k] == null) {
                    r[k] = ["date":c.created,"who":c.author]
                }
                r
            }
        }
}}
def changeDateFnFn = { exalateUser, timeDiffHours, changeHistory ->
    def cache = null
    def fn = { ->
        if(cache == null) {
            def HOUR = 1000 * 60 * 60
            def TIME_DIFF = timeDiffHours * HOUR
            cache = fieldToLastUpdateDateFn(exalateUser)(changeHistory)
                .inject([:]) { r, k, v ->
                    r[k] = v
                    v?.date = new Date((((v?.date instanceof Integer) ? v?.date : v?.date?.time) + TIME_DIFF) as Long)
                    r
                }
            cache
        } else {
            cache
        }
    }
    fn
}

```

and then it’s call later on:

```auto
def remoteFieldToLastUpdateDate = fieldToLastUpdateDateFn("exalate")(replica.changeHistory)
def localFieldToLastUpdateDate = fieldToLastUpdateDateFn("exalate")(issue.changeHistory)

```

became:

```auto
def remoteFn = changeDateFnFn("exalate", 0, replica.changeHistory)
def localFn = changeDateFnFn("exalate", 0, issue.changeHistory)

```

so then the conflict conditions changed from

```auto
if(firstSync) {
  issue."BBB" = replica."AAA"
} else { 
  if (remoteFieldToLastUpdateDate."AAA" > localFieldToLastUpdateDate."BBB") {
    issue."BBB" = replica."AAA"
  }}

```

to

```auto
if(firstSync || remoteFn()?."AAA"?.date > localFn()?."BBB"?.date) {
  issue."BBB" = replica."AAA"
}

```

I also introduced the userStr helper function that converts a user object into a read-able text:

```auto
def userStr = { usr ->
    (usr == null) ? 
    "Unknown user" :
    "${usr?.displayName} (${usr?.email})".toString()
}

```

# Chapter 3: all code together

Outgoing on source:

```auto
// community 58501849 send history information to help with the reasoning
replica.changeHistory = issue.changeHistory
replica.created = issue.created

```

Incoming on destination:

```auto
def changeLogBuilder = new StringBuilder();
def userStr = { usr ->
    (usr == null) ? 
    "Unknown user" :
    "${usr?.displayName} (${usr?.email})".toString()
}

// community 58501849 start accumulating the change log
def latestChange = replica
    .changeHistory
    .sort { c -> c.created.time }
    .reverse()
    .find()
def latestChangeMsg = (latestChange == null) ? 
    "${replica.key} created by ${userStr(replica.reporter)} on ${replica.created}" : 
    "${latestChange.changeItems.field.join(", ")} updated by ${userStr(latestChange.author)} on ${latestChange.created}"
    
def syncMsg = firstSync ?
    "First sync" :
    "Sync"
changeLogBuilder
    .append("# $syncMsg triggered due to $latestChangeMsg")
// end: community 58501849 start accumulating the change log

if(firstSync) {
  issue.projectKey = "BB"

// community 58501849 add to change log due to issue.projectKey
  changeLogBuilder
    .append("\n# ")
    .append("Setting project to ${issue.projectKey} because it's first sync coming from ${replica.project.key}") 
// end: community 58501849 add to change log due to issue.projectKey

  issue.typeName = nodeHelper.getIssueType(replica.type?.name, issue.projectKey)?.name ?: "Task"

// community 58501849 add to change log due to issue.typeName
  def isSameTypeFound = nodeHelper.getIssueType(replica.type?.name, issue.projectKey)?.name != null
  def issueTypeMsg = isSameTypeFound ? 
          "same issue type as ${replica.type?.name} found in ${issue.projectKey}" : 
          "issue type ${replica.type?.name} not found in ${issue.projectKey} "+
          "so Exalate assigns a default issue type \"Task\""
  changeLogBuilder
    .append("\n# ")
    .append(
        "Setting issue type to ${issue.typeName} because "+
        "it's first sync coming from ${replica.project.key} "+
        "and $issueTypeMsg".toString()
    ) 
// end: community 58501849 add to change log due to issue.typeName
}

// community 58501849 + 58500894 conflict handling + useful info for change reasons
def fieldToLastUpdateDateFn = { exalateUserKey -> { history ->
    history
        .sort { c -> c.created.time }
        .reverse()
        .findAll { c ->
            c.author.key != exalateUserKey
        }
        .inject([:]) { result, c ->
            c.changeItems.inject(result) { r, i ->
                String k = i.field
                if (r[k] == null) {
                    r[k] = ["date":c.created,"who":c.author]
                }
                r
            }
        }
}}
def changeDateFnFn = { exalateUser, timeDiffHours, changeHistory ->
    def cache = null
    def fn = { ->
        if(cache == null) {
            def HOUR = 1000 * 60 * 60
            def TIME_DIFF = timeDiffHours * HOUR
            cache = fieldToLastUpdateDateFn(exalateUser)(changeHistory)
                .inject([:]) { r, k, v ->
                    r[k] = v
                    v?.date = new Date((((v?.date instanceof Integer) ? v?.date : v?.date?.time) + TIME_DIFF) as Long)
                    r
                }
            cache
        } else {
            cache
        }
    }
    fn
}
def remoteFn = changeDateFnFn("557058:c020323a-70e4-4c07-9ccc-3ad89b1c02ec", 0, replica.changeHistory) // 557058:c020323a-70e4-4c07-9ccc-3ad89b1c02ec is the exalate's user id for Jira Cloud
def localFn = changeDateFnFn("557058:c020323a-70e4-4c07-9ccc-3ad89b1c02ec", 0, issue.changeHistory) // 557058:c020323a-70e4-4c07-9ccc-3ad89b1c02ec is the exalate's user id for Jira Cloud 
// end: community 58501849 + 58500894 conflict handling + useful info for change reasons

issue.comments = commentHelper.mergeComments(issue, replica)

// community 58501849 logging comment changes into the change log
if (issue.comments != issueBeforeScript.comments) {
    if (!replica.removedComments.empty) {
        changeLogBuilder
         .append("\n# ")
         .append(
             "There were ${replica.removedComments.size()} comments removed on the remote side, so Exalate is going to remove them here too".toString()
         )
    }
    if (!replica.addedComments.empty) {
        changeLogBuilder
         .append("\n# ")
         .append(
             "There were ${replica.addedComments.size()} comments added on the remote side by ${replica.addedComments.collect { userStr(it?.author) }.join(", ")}, so Exalate is going to add them here too".toString()
         )
    }
}
// end: community 58501849 logging comment changes into the change log

issue.attachments = attachmentHelper.mergeAttachments(issue, replica)
// community 58501849 logging attachment changes into the change log
if (issue.attachments != issueBeforeScript.attachments) {
    if (!replica.removedAttachments.empty) {
        changeLogBuilder
         .append("\n# ")
         .append(
             "There were ${replica.removedComments.size()} attachments removed on the remote side, so Exalate is going to remove them here too".toString()
         )
    }
    if (!replica.addedAttachments.empty) {
        changeLogBuilder
         .append("\n# ")
         .append(
             "There were ${replica.addedAttachments.size()} attachments added on the remote side by ${replica.addedAttachments.collect { userStr(it?.author) }.join(", ")}, so Exalate is going to add them here too".toString()
         )
    }
}
// end: community 58501849 logging attachment changes into the change log

issue.labels = replica.labels

// community 58501849 logging label changes into the change log
if (issue.labels != issueBeforeScript.labels) {
    changeLogBuilder
     .append("\n# ")
     .append(
         "Overwriting labels because Exalate is configured to do it on every sync"
     )
}
// end: community 58501849 logging label changes into the change log

// community 58501849 + 58500894 conflict handling = logging why was a field changed or left untouched
if (firstSync || remoteFn()?."description"?.date > localFn()?."description"?.date) {
    issue.description = replica.description
    def reason = firstSync ? 
        "it's first sync, and there is no local value yet" :
        "it was changed by ${userStr(remoteFn()?."description"?.who)} "+
        "more recently (${remoteFn()?."description"?.date}) on remote side "+
        "than on local side (${localFn()?."description"?.date})"
    changeLogBuilder
     .append("\n# ")
     .append(
         "Overwriting description because $reason".toString()
     )
} else {
    changeLogBuilder
     .append("\n# ")
     .append(
         "Not touching \"description\" because "+
         "it is not the first sync and "+
         "it was changed by ${userStr(localFn()?."description"?.who)} "+
         "more recently (${localFn()?."description"?.date}) locally "+
         "than on the remote side (${remoteFn()?."description"?.date})".toString()
     )
}

if (firstSync || remoteFn()."summary"?.date > localFn()."summary"?.date) {
    issue.summary = replica.summary
    def reason = firstSync ? 
        "it's first sync, and there is no local value yet" :
        "it was changed by ${userStr(remoteFn()?."summary"?.who)} "+
        "more recently (${remoteFn()?."summary"?.date}) on remote side "+
        "than on local side (${localFn()?."summary"?.date})"
    changeLogBuilder
     .append("\n# ")
     .append(
         "Overwriting \"summary\" because $reason".toString()
     )
} else {
    changeLogBuilder
     .append("\n# ")
     .append(
         "Not touching \"summary\" because "+
         "it is not the first sync and "+
         "it was changed by ${userStr(localFn()?."summary"?.who)} "+
         "more recently (${localFn()?."summary"?.date}) locally "+
         "than on the remote side (${remoteFn()?."summary"?.date})".toString()
     )
}
if (firstSync || remoteFn()?."Classification"?.date > remoteFn()?."Classification"?.date) {         
    issue.Classification = replica.Classification
    def reason = firstSync ? 
        "it's first sync, and there is no local value yet" :
        "it was changed by ${userStr(remoteFn()?."Classification"?.who)} "+
        "more recently (${remoteFn()?."Classification"?.date}) on remote side "+
        "than on local side (${localFn()?."Classification"?.date})"
    changeLogBuilder
     .append("\n# ")
     .append(
         "Overwriting \"Classification\" because $reason".toString()
     )
} else {
    changeLogBuilder
     .append("\n# ")
     .append(
         "Not touching \"Classification\" because "+
         "it is not the first sync and "+
         "it was changed by ${userStr(localFn()?."Classification"?.who)} "+
         "more recently (${localFn()?."Classification"?.date}) locally "+
         "than on the remote side (${remoteFn()?."Classification"?.date})".toString()
     )
}
// end: community 58501849 + 58500894 conflict handling = logging why was a field changed or left untouched

// community 58501849 adding the change log to a custom field
issue."Exalate Change Reason" = changeLogBuilder.toString()
// end: community 58501849 adding the change log to a custom field

```

Let me know how it goes,  
Happy Exalating!

* * *

---

<div class="post-metadata">

**Author:** ![xl8bot](https://dub1.discourse-cdn.com/flex018/user_avatar/community.exalate.com/xl8bot/32/324_2.png) [@xl8bot](https://community.exalate.com/u/xl8bot)\
**Post date:** [November 6, 2024, 8:08am UTC](https://community.exalate.com/t/news-feed-for-ongoing-exalate-updates/5489/3 "2024-11-06T08:08:24Z")

</div>

This topic was automatically closed 2 days after the last reply. New replies are no longer allowed.
