Overcoming Custom Field History Tracking Limits in Salesforce: Tips to Track More Fields
As you know Salesforce field history tracking is the wonderful standard feature to store the historical data for an audit purpose. Historical data is always powerful when it comes to data analytics. But there is a limitation in this feature, we can track up to a maximum of 20 fields per object only. What if your organization needs to track more than that? Here is the solution.
Below are the steps to setup custom history tracking,
Field Label | API Name | Data Type |
Record Id | Record_Id | Text(18) |
Field Changed | Field_Changed | Text(100) |
Related To | Related_To | Text(100) |
Old Value | Old_Value | Text(255) |
New value | New_Value | Text(255) |
Create the below Apex class
public with sharing class CustomHistoryTracking {
public static void trackHistory(String strRelatedTo, map mapNewRecords, map mapOldRecords) {
try {
SObjectType objType = mapNewRecords.values()[0].getSObjectType();
String strObjectName = String.valueOf(objType);
if (strObjectName.contains('__c')) {
strObjectName = strObjectName.left(strObjectName.length() - 3);
}
// 1. Read Custom History Tracking custom meta data to get the list of fields to track
list lstFieldsToTrack = new List();
for (Custom_History_Tracking__mdt customHistoryTracking : [SELECT Fields__c FROM Custom_History_Tracking__mdt WHERE DeveloperName = :strObjectName]) {
lstFieldsToTrack.addAll(customHistoryTracking.Fields__c.split('; '));
System.debug('lstFieldsToTrack: ' + lstFieldsToTrack);
}
if (!lstFieldsToTrack.isEmpty()) {
// 2. Retrieve the field describe result to know the data type
Schema.DescribeFieldResult fieldDescribeResult;
DescribeSObjectResult objectDescribeResult = objType.getDescribe();
map mapFieldDescribes = new Map();
for (String strFieldAPIName : lstFieldsToTrack) {
// Field API Name for Lookup field is the format eg: Social_Reputation_Manager__c:ACC_Social_Reputation_Manager_r_Name
// The first part before : is the actual Lookup field being tracked.
if (strFieldAPIName.contains(':')) {
strFieldAPIName = strFieldAPIName.substringBefore(':');
}
mapFieldDescribes.put(strFieldAPIName, objectDescribeResult.fields.getMap().get(strFieldAPIName).getDescribe());
}
// 3. Track history if the field values are changed
list lstHistoryRecords = new List();
Map> mapRecIdRelNames = new Map>();
Map> mapRelNameLookupIds = new Map>();
for (sObject newRecord : mapNewRecords.values()) {
sObject oldRecord = mapOldRecords.get(newRecord.Id);
for (String strFieldAPIName : lstFieldsToTrack) {
// Extract the API Field Name in case of Lookup fields.
String strRefFieldAPIName;
if (strFieldAPIName.contains(':')) {
strRefFieldAPIName = strFieldAPIName;
strFieldAPIName = strFieldAPIName.substringBefore(':');
}
if (newRecord.get(strFieldAPIName) != oldRecord.get(strFieldAPIName)) {
fieldDescribeResult = mapFieldDescribes.get(strFieldAPIName);
// In case of Lookup fields collect the old and new Look up Field ID's.
if (String.isNotBlank(strRefFieldAPIName) && fieldDescribeResult.getType() == Schema.DisplayType.Reference) {
String strRelationshipName = strRefFieldAPIName.substringAfter(':');
if (oldRecord.get(strFieldAPIName) != null) {
if (!mapRelNameLookupIds.containsKey(strRelationshipName)) {
mapRelNameLookupIds.put(strRelationshipName, new List());
}
mapRelNameLookupIds.get(strRelationshipName).add((Id) oldRecord.get(strFieldAPIName));
}
if (newRecord.get(strFieldAPIName) != null) {
if (!mapRelNameLookupIds.containsKey(strRelationshipName)) {
mapRelNameLookupIds.put(strRelationshipName, new List());
}
mapRelNameLookupIds.get(strRelationshipName).add((Id) newRecord.get(strFieldAPIName));
}
if (!mapRecIdRelNames.containsKey(newRecord.Id)) {
mapRecIdRelNames.put(newRecord.Id, new List());
}
mapRecIdRelNames.get(newRecord.Id).add(strRefFieldAPIName);
} else {
Custom_History__c customHistory = new Custom_History__c();
customHistory.Record_Id__c = newRecord.Id;
customHistory.Field_Changed__c = fieldDescribeResult.getLabel();
customHistory.Related_To__c = strRelatedTo;
if (fieldDescribeResult.getType() == Schema.DisplayType.TextArea && fieldDescribeResult.getLength() > 255) {
// If it is long text area or rich text area, then do not track what exactly changed
customHistory.New_Value__c = 'This field value changed';
} else if (fieldDescribeResult.getType() == Schema.DisplayType.Date || fieldDescribeResult.getType() == Schema.DisplayType.DateTime) {
customHistory.Old_Value__c = (oldRecord.get(strFieldAPIName) != null ? String.valueOf(oldRecord.get(strFieldAPIName)) + ' GMT' : '');
customHistory.New_Value__c = (newRecord.get(strFieldAPIName) != null ? String.valueOf(newRecord.get(strFieldAPIName)) + ' GMT' : '');
} else {
customHistory.Old_Value__c = String.valueOf(oldRecord.get(strFieldAPIName));
customHistory.New_Value__c = String.valueOf(newRecord.get(strFieldAPIName));
}
lstHistoryRecords.add(customHistory);
}
}
}
}
// Insert Custom History for Lookup Fields here.
if (!mapRecIdRelNames.isEmpty()) {
// Retrieiving the list of Queries for the Lookup field
Map mapRelNameQuery = new Map();
for (Custom_History_Tracking__mdt customHistoryTracking : [
SELECT Fields__c, DeveloperName
FROM Custom_History_Tracking__mdt
WHERE DeveloperName IN :mapRelNameLookupIds.keyset()
]) {
mapRelNameQuery.put(customHistoryTracking.DeveloperName, customHistoryTracking.Fields__c);
}
if (!mapRelNameQuery.isEmpty()) {
Map mapRelNameWhereIds = new Map();
for (String strRelName : mapRelNameQuery.keyset()) {
String strRefIds = '(';
for (Id objRefId : mapRelNameLookupIds.get(strRelName)) {
strRefIds = strRefIds + '\'' + objRefId + '\',';
}
if (strRefIds.length() > 1) {
strRefIds = strRefIds.left(strRefIds.length() - 1);
strRefIds = strRefIds + ')';
}
mapRelNameWhereIds.put(strRelName, strRefIds);
}
// Query the Name field corresponding to the ID's
Map mapLookupIdName = new Map();
for (String strRelName : mapRelNameQuery.keyset()) {
String strQuery = mapRelNameQuery.get(strRelName) + ' WHERE Id IN ' + mapRelNameWhereIds.get(strRelName);
for (SObject objSObject : Database.query(strQuery)) {
mapLookupIdName.put(objSObject.Id, (String) objSObject.get('Name'));
}
}
for (Id recordId : mapRecIdRelNames.keyset()) {
for (String strRefFieldAPIName : mapRecIdRelNames.get(recordId)) {
sObject newRecord = mapNewRecords.get(recordId);
sObject oldRecord = mapOldRecords.get(recordId);
Custom_History__c customHistory = new Custom_History__c();
customHistory.Record_Id__c = recordId;
customHistory.Related_To__c = strRelatedTo;
String strFieldAPIName = strRefFieldAPIName.substringBefore(':');
fieldDescribeResult = mapFieldDescribes.get(strFieldAPIName);
customHistory.Field_Changed__c = fieldDescribeResult.getLabel();
if (oldRecord.get(strFieldAPIName) != null && mapLookupIdName.get((Id) oldRecord.get(strFieldAPIName)) != null) {
customHistory.Old_Value__c = mapLookupIdName.get((Id) oldRecord.get(strFieldAPIName));
} else {
customHistory.Old_Value__c = '';
}
if (newRecord.get(strFieldAPIName) != null && mapLookupIdName.get((Id) newRecord.get(strFieldAPIName)) != null) {
customHistory.New_Value__c = mapLookupIdName.get((Id) newRecord.get(strFieldAPIName));
} else {
customHistory.New_Value__c = '';
}
lstHistoryRecords.add(customHistory);
}
}
}
}
if (!lstHistoryRecords.isEmpty()) {
insert lstHistoryRecords;
}
}
} catch (Exception exp) {
System.debug('The following exception has occurred: ' + exp.getMessage());
}
}
} Add the following line into your trigger, in my case I am trying to track Account Name, Phone field means, add this line in after trigger logic,
trigger AccountTrigger on Account (after insert, after update) {
if(Trigger.isAfter && Trigger.isUpdate) {
CustomHistoryTracking.trackHistory('Account', Trigger.newMap, Trigger.oldMap);
}
} SELECT Record_Id__c, Related_To__c, Field_Changed__c, Old_Value__c, New_Value__c, CreatedDate FROM Custom_History__c