Skip to main content

Field History Tracking in Replication Models

The following table describes how the field history tracking works in different replication models:

note

Please make your solution decisions based on generally available functionality only.

FunctionalityLegacy replicated model (pre-commit/post-commit)UI-based replicated model (custom Lightning Web Components)
Create/update a record from UIThe Field History saves a regulated data value before the pre-commit process:
  1. User saves a record.
  2. The Field History tracks a regulated data value.
  3. Pre-commit is executed.
  4. The Field History saves a hashed value.
  5. The record is synchronized with the InCountry platform.
  6. Post-commit is executed.
  7. The Field History tracks a regulated data value.

In this scenario, the Field History tracks a regulated data value BEFORE its synchronization with the InCountry platform.

The Field History saves a regulated data value after its synchronization with the InCountry platform:
  1. User saves a record.
  2. The record is synchronized with the InCountry platform.
  3. The record is saved to the Salesforce database.
  4. The Filed History tracks a regulated data value.

In this scenario, the Filed History tracks a regulated data value AFTER its synchronization with the InCountry platform.

Create/update a record via custom Salesforce APIDepends on the architecture of a custom Apex webservice.

The following option is available (with Apex SDK):

  1. A record is saved via an Apex webservice.
  2. The record is synchronized with the InCountry platform.
  3. The record is saved to the Salesforce database.
  4. The Filed History tracks a regulated data value.

In this scenario, the Field History is recorded after its synchronization with the InCountry platform.

Create/update a record via the native Salesforce APIIn this scenario the Field History always tracks a regulated data value BEFORE its synchronization with the InCountry platform.

As a workaround, you can use InbCountry Border. In this case, Border will capture regulated values and will store them in InCountry. The Field History will capture the regulated data value AFTER its synchronization with the InCountry platform.

Update a regulated field in BEFORE triggersThe Field History tracks a regulated data value before its synchronization with the InCountry platform.

Two potential workarounds are available:

  1. Regulated data fields should not be changed synchronously BEFORE execution of triggers.
  2. Regulated data fields can be updated asynchronously in a separate transaction:
    • Asynchronous transaction.
    • Change of the regulated field value.
    • Synchronization with the InCountry platform (Apex SDK).
    • The record is saved to the Salesforce database.
    • The Field History tracks a regulated data value AFTER its synchronization with the InCountry platform.
Change a regulated field in custom Apex controllersDepends on the architecture of Apex controllers.

The following option is available (with Apex SDK):

  1. A record is saved via an Apex controller.
  2. The record is synchronized with the InCountry platform.
  3. The record is saved to the Salesforce database.
  4. The Filed History tracks a regulated data value.

In this scenario, the Field History tracks a regulated data value after its synchronization with the InCountry platform.

Code examples

Cases are created or updated via a custom Apex RESTful web-service

@RestResource(urlMapping='/Cases/*')
global with sharing class CaseWebService {
@HttpPut
global static Case upsertCase(String Subject, String Status, String Origin, String Priority, String Id) {
// Prepare Case record in memory
Case thisCase = new Case(
Subject=subject,
Status=status,
Origin=origin,
Priority=priority);
if (String.isNotBlank(Id)) {
thisCase.Id = Id;
}
// First save the case to InCountry
InCountryRecordSynchronizer synchronizer = new InCountryRecordSynchronizer('Case');
String temporaryId = synchronizer.addSObject(thisCase);
System.debug('temporary id = ' + temporaryId);
Map<String, InCountryRecordSynchronizer.InCountrySaveStatus> statuses = synchronizer.sync();
if (statuses.get(temporaryId).isSuccess) {
InCountryReplicationTriggerHandler.disableTrigger = true;
// Then upsert the case in Salesforce
upsert thisCase;

// If necessary change the case Id in InCountry
String recordId = String.valueOf(thisCase.get('Id'));
if (recordId != temporaryId) {
InCountryRecordSynchronizer.migrate(JSON.serialize(synchronizer.mapSensitiveData), new Map<String, String> {
temporaryId => recordId
});
}
}
// Return the case.
return thisCase;
}
}

REST Explorer

A request is performed at this endpoint /services/apexrest/Cases/

{
"Status" : "Working",
"Subject" : "Bigfoot Sighting!",
"Priority" : "Medium"
}

The following result is returned:

{
"attributes" : {
"type" : "Case",
"url" : "/services/data/v53.0/sobjects/Case/5005w00001r5XvzAAE"
},
"Subject" : "Bigfoot Sighting!",
"Status" : "Working",
"Origin" : null,
"Priority" : "Medium",
"Id" : "5005w00001r5XvzAAE"
}

Find in InCountry

Regulated fields are updated in Case Trigger

Case - regulated object. Regulated fields are Type and Priority. A Customer wants to update Priority to high if Type is set to Structural.

Apex trigger:

trigger CaseTrigger on Case (after insert) {
if (Trigger.isAfter && Trigger.isInsert) {
List<Case> casesToUpdate = CaseTriggerHandler.getCasesToUpdate(Trigger.New);
if (!casesToUpdate.isEmpty()) {
CaseTriggerHandler.updateRegulatedFields((new Map<Id, Case>(casesToUpdate)).keySet());
}
}
}

Trigger Handler:

public class CaseTriggerHandler {

/*
* Asynchronous method with business logic and synchronization to InCountry PoP
*/
@future(callout=true)
public static void updateRegulatedFields(Set<Id> caseIds) {
List<Case> cases = [SELECT Id, Type, Priority FROM Case WHERE Id IN :caseIds];
List<Case> casesToUpdate = getCasesToUpdate(cases);
for (Case caseRecord : casesToUpdate) {
caseRecord.Priority = 'High';
}
if (!casesToUpdate.isEmpty()) {
InCountry service = InCountry.getInstance();
service.write(casesToUpdate);
}
update casesToUpdate;
}

public static List<Case> getCasesToUpdate(List<Case> cases) {
List<Case> casesToUpdate = new List<Case>();
for(Case caseRecord : cases) {
// Type and Priority are regulated fields
if (caseRecord.Type == 'Structural') {
casesToUpdate.add(caseRecord);
}
}
return casesToUpdate;
}
}

Details

Cases are created via Apex Lightning Controller

public with sharing class CasesConroller {
@AuraEnabled
public static Case upsertCase(String Subject, String Status, String Origin, String Priority, String Id) {
// Prepare Case record in memory
Case thisCase = new Case(
Subject=subject,
Status=status,
Origin=origin,
Priority=priority);
if (String.isNotBlank(Id)) {
thisCase.Id = Id;
}
// First save the case to InCountry
InCountryRecordSynchronizer synchronizer = new InCountryRecordSynchronizer('Case');
String temporaryId = synchronizer.addSObject(thisCase);
System.debug('temporary id = ' + temporaryId);
Map<String, InCountryRecordSynchronizer.InCountrySaveStatus> statuses = synchronizer.sync();
if (statuses.get(temporaryId).isSuccess) {
InCountryReplicationTriggerHandler.disableTrigger = true;
// Then upsert the case in Salesforce
upsert thisCase;

// If necessary change the case Id in InCountry
String recordId = String.valueOf(thisCase.get('Id'));
if (recordId != temporaryId) {
InCountryRecordSynchronizer.migrate(JSON.serialize(synchronizer.mapSensitiveData), new Map<String, String> {
temporaryId => recordId
});
}
}
// Return the case.
return thisCase;
}
}