Hello Trailblazer! There is a limit on how many fields you can track using the standard Salesforce Field History Tracking which is why we often need to write our own logic for custom field history tracking!
In this post, I will share with you how can reuse the simple and reusable custom field history tracking for any Salesforce Object.
Use Case
Need to create a custom field history tracking system so that we can track changes for more than 20 fields in Salesforce. Need to build a declarative tool so that admins can easily add/remove fields and objects in the field tracking system.
The Approach
-
We will create a generic object to store the field history data like
Field Name
,Old Value
,New Value
,Record Id
, and aUser
who changed the data. Here is the complete schema for Field history.
-
Also, we need two additional Long Text fields so that we can track Long text
fields as well.
- We need to create two custom metadata to store the Object and field information so that we can track data based on that.
-
Field Tracker Object
- Define the object name to track fields from.
-
Field Track Field
- Define fields to be tracked.
- Lastly, we need to write a generic class (FieldTrackerService) with a function that will take the name of the Object, and records data. Based on the provided information it will create the history records.
-
Also, each configuration can be turned on/off from the Object as well as the
Field level so that you can turn off field tracking by just
checking/unchecking the
Is Active
flag. -
Lastly, we need to call the generic class function from the Object
triggers.
Implementation
Download the source code from this GitHub repo: custom-field-tracker-salesforce.
Or
Install the unmanaged package from here: Custom Field Tracker Unmanaged Package.
Example Trigger Code
Account
object and BillingCountry
field.
trigger AccountTrigger on Account(after insert, after update) { AccountTriggerHandler.saveFieldHistories(Trigger.new, Trigger.oldMap); }
public with sharing class AccountTriggerHandler { public static void saveFieldHistories( List<Account> newAccounts, Map<Id, Account> oldMap ) { FieldTrackerService fts = FieldTrackerService.getInstance('Account'); fts.saveFieldHistories(newAccounts, oldMap); } }
@isTest class AccountTriggerTest { @TestSetup static void makeData() { List<Account> accounts = new List<Account>(); for (Integer i = 0; i < 200; i++) { Account acc = new Account( Name = 'Account FTS ' + i, BillingCountry = 'United States' ); accounts.add(acc); } insert accounts; } @IsTest static void testAfterInsertHistory() { Set<Id> accountIds = new Map<Id, Account>( [SELECT Id FROM Account WHERE Name LIKE 'Account FTS %'] ) .keySet(); Test.startTest(); System.assertEquals( [ SELECT Id FROM Field_History__c WHERE Tracked_Field_API__c = 'BillingCountry' AND Tracked_Record_Id__c IN :accountIds AND Old_Value__c = NULL ] ?.size(), 200, 'Field history records not found' ); Test.stopTest(); } @IsTest static void testAfterUpdateHistory() { Account[] accounts = [ SELECT Id FROM Account WHERE Name LIKE 'Account FTS %' ]; Set<Id> accountIds = new Map<Id, Account>(accounts).keySet(); for (Account acc : accounts) { acc.BillingCountry = 'India'; } update accounts; Test.startTest(); System.assertEquals( [ SELECT Id FROM Field_History__c WHERE Tracked_Field_API__c = 'BillingCountry' AND Tracked_Record_Id__c IN :accountIds AND Old_Value__c = 'United States' ] ?.size(), 200, 'Field history records not found' ); Test.stopTest(); } }
Further Customizations
You can add additional features as you need.
I have planned to add a Lightning web component to show the related history records on standard record pages.
Please let me know if you wish to add new features. You can also contribute to
this code by submitting a pull request to this repository - custom-field-tracker-salesforce.
I hope that was helpful! Thanks for reading!
Hi Rahul, will it work for custom object? I have tried for custom object but i'm getting null pointer exception in apex around this line i.e, ftObject = Field_Tracker_Object__mdt.getInstance(objectName). Its works only for account not for other objects. Also i called handler etc for custom object as well.
ReplyDeleteHello TechGang it will work for custom objects too. Check if you have correct data in custom mdt.
ReplyDeleteHi, is there a way to use @future methods here? I am a new developer, and I have this legacy code that is not very optimised. So, using a @future method pretty much ensures that I don't run into Salesforce government limits.
ReplyDeleteYes you can use future method but you will have to pass the data in primitive format like map or lists, but it isn't worth doing that, I would rather suggest to optimise your old code if possible.
DeleteYup, that's what I fear, it would take time for either solutions to be developed.
DeleteHow would I attach this object as related list to the record page layout (ex. Case)?
DeleteCreate a simple custom lwc component to show the list with some pagination. Put a condition to match the record id
DeleteYour picture for Field Tracker Object is the same as Field Tracker Field. Is that supposed to be like that?
ReplyDeleteThank you Raop for spotting that. i think that is by mistake I will correct it.
DeleteJust want to point out that if you have an enabled workflow rule on your object, you'll need to add a static boolean to prevent this from running more than once. public static boolean firstRun = true; in your AccountTriggerHandler and inside your trigger, do the following: if (AccountTriggerHandler.firstRun) {
ReplyDeleteAccountTriggerHandler.firstRun = false;
AccountTriggerHandler.saveFieldHistories(Trigger.new, triggerOldMap);
}
Hope this helps someone.
Agree, this is surely helpful, Mike
DeleteHi Rahul,
ReplyDeletei am trying this for a custom object would you mind putting a screen for the custom metadata entries.. I am getting null pointer exception...
Hi Ram, sure I will put in some time.
DeleteThis comment has been removed by the author.
ReplyDeleteHi Luiz, yes I have made lwc to show the history.
DeleteThis comment has been removed by the author.
DeleteHi Luiz, I will share it by tomorrow, thanks
Delete