Auditing when a Salesforce record is viewed

Auditing when a Salesforce record is viewed

Using Visualforce Remote Objects, you can audit whenever a user views a Salesforce record. The audit will show the user’s name and the date they viewed the record. On the detail page of the record the user viewed, it looks like this:

In the example below, we’ll track views of records on the Contact object but this can be extended to any standard or custom object.

What you need
 
– A custom object to record the ‘views’
– A Visualforce page that appears on every page layout for the object
 
The custom object
 
– The API Name should be Audit_Log__c
– The singular and plural labels, etc. don’t really matter
– I’ve set up the Record Name field to be Audit Log Name with a Display Format of AL-{000000} but that’s just so it looks OK in related lists
– You need 3 custom fields:
  – A lookup to the Contact object. The API name should be Contact__c
  – A text field with a length of 255 characters. The API name should be Operation__c
  – A lookup to the User object. The API name should be User__c
  – Optionally, you can have a formula field called ‘Viewed date’, again just so it looks OK in related lists. The formula would just be this: CreatedDate
  – Your fields should look something like this:

The Visualforce page
 
– Name and label your page something like Audit_Log_Contact
– This is the code:

<apex:page standardController="Contact">
    <!-- variables to change for different objects -->
    <apex:variable value="{!Contact.Id}"         var="RecordId" rendered="true"/>
    <apex:variable value="Contact__c"            var="LookupField" rendered="true"/><!-- value = The lookup field from the audit log to the object being logged. -->
    <apex:variable value="Audit_Log__c"          var="LogObject" rendered="true"/><!-- value = The API Name of the audit log object. Only change this if you are using different audit log objects per custom object. -->
    <apex:remoteObjects >
        <apex:remoteObjectModel name="{!LogObject}" jsShorthand="cal" fields="{!LookupField},User__c" />
    </apex:remoteObjects>
    <script>
    function createUserLog() {
        // Create log record
        var contactAuditLog = new SObjectModel.{!LogObject}();
        contactAuditLog.set('<apex:outputText value="{!LookupField}" />','<apex:outputText value="{!RecordId}" />');
        contactAuditLog.set('User__c', '<apex:outputText value="{!$User.Id}" />');
        contactAuditLog.set('Operation__c', 'View');        
        contactAuditLog.create(function(error, result, event) {
                // Success? Exit the function.
                if(error == null) {
                    return;
                }
                // Error? Log it.
                console.log(error);
            } );
    }
    createUserLog();
    </script>
</apex:page>

Configuration
 
– For this to work, you absolutely, positively have to put the Visualforce page on every page layout for the Contact object. You can make it as small as you want – I find 18 pixels about right – and put it where a field would normally be. Like this:

– The users whose views are being tracked must have create access to the Audit Log object and edit access to the fields. This is because Remote Objects respects CRUD / FLS.

Good to know
 
– You can extend this to other objects. For example, to get this working on Opportunities:
   – On the Audit Log Object, add a lookup to the Opportunity object, ensuring the API Name is Opportunity__c
   – Clone the Visualforce page and re-name and re-label it to  Audit_Log_Opportunity
     – Change Contact at line 1 to Opportunity
     – Change Contact.Id at line 3 to Opportunity.Id
     – Change Contact__c at line 4 to Opportunity__c
– It also works on the Salesforce1 mobile interface but you will need to place the Visualforce near the top of the page layout because of the way the mobile UI is loaded

Some important limitations
 
– Really, the proper way to do this is via Event Monitoring which I’ve written about elsewhere on this site. But that costs extra so if you’re on a budget, this is one way to (partially) solve the problem
– This is only going to track record views on Detail pages. Views from reports, related lists, list views, etc. don’t get picked up
– It is based on putting a VisualForce page on every page layout for the Contact object. This means an Administrator could create or modify a page layout thereby causing untracked views. Of course, the fact that the Page Layout has been changed is tracked in the Setup Audit Trail…
– The JavaScript could use the module pattern / IIFE and, yes, there are no excuses but I’ve yet to see a Visualforce page in a managed package listed on the AppExchange that does. And I’ve been looking : )
– It records the “views” in a custom object so someone with delete permissions on the object could delete view records. This is easily prevented with a trigger on the Audit Log object like so:

Trigger

trigger AuditLog on Audit_Log__c (before delete) {
    for (Audit_Log__c a : trigger.old) {
        a.AddError('You cannot delete Audit Logs.');
    }
}

Test class

@isTest

public class AuditLogTest{

    //Test setup data
    @TestSetup
    public static void setupData(){
        Contact con = new Contact(Lastname='TestName', FirstName='TestFname');
        INSERT con;
    }

    // Test method to test the trigger that prevents Audit Logs being deleted
    static testmethod void TestAuditDeletion() {
        Contact c = [SELECT Id FROM Contact WHERE Lastname =: 'TestName' AND FirstName =: 'TestFname' LIMIT 1];
        AuditLog.logView(c.Id, 'Contact__c', UserInfo.GetUserId());
        Audit_Log__c a = [SELECT Contact__c FROM Audit_Log__c WHERE User__c =: UserInfo.GetUserId() LIMIT 1];
        try {
            delete a;
        } catch (Exception e) {
            System.AssertEquals(true, e.getMessage().contains('You cannot delete Audit Logs.'));
        }

    }
}
Salesforce password resets – when all else fails

Salesforce password resets – when all else fails

Dreamforce 2016: all the keynote videos

Dreamforce 2016: all the keynote videos