Development, Integration

[ART-015] Callout Framework Part 2


In the previous article, you have learned how to use the Framework to set up Callout Resources with configurable and reusable parameters, and also to route every callout to an alternative endpoint which can be another resources server or Salesforce itself. You have got a glimp of what we call Mocking! We will deep dive into the concept of Mocking and its purposes and we will also speak about cool features around logging and tracking.


Prerequisites

Before starting, I will recommend you to read my previous article that has introduced the Callout Framework:

https://ssdevl.com/2021/03/26/art-014-callout-framework-part-1/

Core Framework Settings

The Core Setting CustomMetadataType is the one place to set all the Framework behaviors, you’ll find one for default and one dedicated to Test Context only. It’s important to keep both unchanged as it will be used by the Framework in default mode. If you wish to have dedicated setting, I will show you how to do it later on.

The Default Core Settings is the default base setting used by the Framework when there isn’t any other setting configured for a dedicated Sandbox or Production org, it’s a kind of a fallback setting to ensure the Framework will work in any case.

SettingValueDescription
LabelDefault Core SettingsName of the setting
Core Setting NameDefaultCoreSettingsThe developername name used in your APEX code
User Info Session Cachelocal.UserInfoCacheName of the platform cache used to store User Infos
SObject Info Session Cache local.UserInfoCache Name of the platform cache used to store SObject Infos
is Active?CheckedIf true then the setting will used in current org
is Default?CheckedThis setting will be used as fallback if not found for current org
Org IdDefaultThis is set to ‘Default’ as it is not org dependent, set the orgId to make it org dependent for any other setting
With Schema DescriberCheckedEnforce Object and field level security with Schema methods
With Security EnforcedCheckedEnforce Object and field level security with SOQL keyword
With StripInaccessibleCheckedEnforce Object and field level security with SObjectAccessDecision methods
Core Trigger Manager ClassAPT001_TriggerEventManagerName of the core trigger event handler class
Recursion LevelNumber of recursion allowed in trigger execution
Logging EnabledCheckedEnable System debug to display info when using Logging Framework
Enable Logging PermissionEN_LOGGINGName of the custom permission to enable logging per user
Logging Event EnabledUncheckedEnable sending Custom Platform Event to track logging
Enable Logging Event Permission EN_LOGGING_EVENTName of the custom permission to enable logging event per user
Callout Tracking EnabledUncheckedEnable sending Custom Platform Event to track callout
Enable Callout Tracking PermissionEN_CALLOUT_TRACKINGName of the custom permission to enable callout tracking per user

Each sandbox or production org can have it own setting by cloning the default one and by setting the “Org Id” properly (must be unique). The “is Default?” must be unchecked also. In this way, you can deploy the setting between orgs without struggling with overwrite issues. In the default setting, all platform event features are deactivated by default to avoid any consumption of resources, it can be activated globally or per user for a period of time to investigate on issues for example or to maintain a log of activities.

With this version of the Framework, you also have now the possibility to control object and field level security with the pattern that suits best your implementation.

I will introduce a last setting MockCoreSettings which will the base settings for every test classes so that we don’t rely on any org dependent setting. It’s important to keep it unchanged.

You can notice that everything is filled or checked in order to allow the Framework to be fully tested. Even thought the Event Logging and Callout tracking are activated, it won’t be activated by default in test context to avoid resources consumption and any unexpected behavior, you have to set a static variable in order to use it when it really matters. We will see that later with some Test examples.

Callout Framework Settings

The Http Request Services CustomMetadataType will hold all resource settings used in any Callout. Here’s an example of how to configure one resource (from previous article):

SettingValueDescription
LabelHeroku Service 01 To identify the service
Http request Service NameHEROKU_S01The name used in your APEX code
Service IdSID-0001Id used to identify resource in test context
Resource/securedThe resource path
Timeout120000Timeout to set in HttpRequest
Primary Named EndpointCALLOUT:MyHerokuCALLOUT: followed by the named credential
Secondary Named EndpointCALLOUT:MySalesforce/services/apexrestCALLOUT: followed by the named credential and path
Bypass Primary Endpoint PermissionLeave it blankName of the custom permission to use bypass feature
Force Secondary EndpointunCheckedSwitch to Secondary endpoint if checked
Custom Headers{“bearer”:”{!$Credential.OAuthToken}”, “instanceurl”:”[your domain]”}Special headers used in this callout in JSON

We will need to test it as well, that’s why we need to introduce a non mutable setting TestingMockService:

Here again, we will fill every setting in order to test fully the Framework behaviors. This one will not work in real condition as the named credentials haven’t been specified.

Logging Event and Callout Tracking

When you deal with Callout, there are some Governor Limits that can be annoying and prevent us to do whatever we want in the way we want. And it’s really annoying when you want to only write once a feature to be reused automatically everywhere without coding any new line or without reminding developers to call the method at the end of a transaction for example.

Here, we see that DML is not allowed after a Callout has been made, it will break the current transaction. Generally, we will have to do all callouts first and then make all DML operations at last. Which imply to know how to deal with each callout result, store it somewhere and make the DML at last and re do it again for new developments, with the risk of someone adding a new Callout or DML in the middle that will mess everything. In this situation, I can’t log anything into custom table and I can’t track callout activities also in a generic way. It will be great to have some Pre-Transaction or Post-Transaction features to implement these but it doesn’t exist actually.

The challenge will be to make our callout, logging and tracking (success or failure) each one immediately and in a generic way and then processing any other callout or DML statements that are normally part of the transaction:

We will use Platform Event capabilities to overcome this situation by publishing a platform event instead of doing a direct DML call to a target SObject. Publishing an event is also considered as a DML operation but has a separate Governor Limits count from the standard DML operation. A second important point to keep in mind is that an event can be published Immediately or At the end of the transaction, we will choose Publish Immediately.

Publish Immediately means that Salesforce will create a new transaction not dependent from the source transaction and publish the event even if the source transaction is on failure. This behavior is a perfect match to log and track everything happening without breaking or be dependent from the source transaction.

In reality, the flow will be more like this:

In this example, every time my transaction start, I will call the Logging Framework then I will fire 2 callouts which will generate one Event each through the Callout Tracking Framework, then I will make some updates in database through DML operations and finally I will call the Logging Framework to notify that my transaction has ended.

It can be represented by something like this in code level:

//Calling the logging Framework
APU000_Logger.log(LoggingLevel.DEBUG, 'MyClass', 'MyMethod', 'My transaction 1 is starting');

ITF004_HttpRequestManager dm = new DM003_HttpRequestService('SFDC');
WRP003_HttpRequest inf = new WRP003_HttpRequest();
inf.requestType = 'GET';
inf.queryParameters = '/'+UserInfo.getUserId();

//making callout and calling the callout tracking Framework
dm.sendRequest(inf);

//Making Some DML
update accountsInList;

//Calling the logging Framework
APU000_Logger.log(LoggingLevel.DEBUG, 'MyClass', 'MyMethod', 'My transaction 1 is ending');

As you can see, the Callout Tracking Framework is embedded into the Callout Framework itself while the Logging Framework has to be used like a simple System.debug(…) statement. The number of event is not unlimited, that’s why it’s important to activate the event publishing only if you need to debug something.

Logging Framework Implementation and Testing

The Logging Framework is based on Platform Event LoggingMessage__e event object with Publish Option set to Publish Immediately:

FieldDescription
ClassName__cName of the class from where the logger is called
EndTime__cTime of the logging event
LogLevel__cSystem.LoggingLevel (FINEST, ERROR, INFO, DEBUG, …)
Message__cContent to publish
MethodName__cName of the method from where the logger is called
StartTime__cTime of the logging event
TransactionId__cSystem generated GUID
UserId__cId of the connected User
Username__cUsername of the connected User

2 Custom Permission are also needed to control who can trigger the event:

Custom PermissionBehavior
EN_LOGGINGwill enable logging feature for assigned users
EN_LOGGING_EVENTwill enable logging event publishing for assigned users

If the global setting Logging Enabled is checked or the custom permission EN_LOGGING is assigned then the message will be published in the Debug Log.

If the global setting Logging Event Enabled is checked or the custom permission EN_LOGGING_EVENT is assigned then the message will be published in Platform Event.

The logger can be called by simply typing this line:

APU000_Logger.log(LoggingLevel.DEBUG, 'MyClass', 'MyMethod', 'My Message');

Every other parameters are calculated by the framework on the fly and if you want to unit test it you can type these lines:

APU002_Context.bypassEventPublishingInTestContext = false;

String results = APU000_Logger.log(LoggingLevel.DEBUG, 'APU000_Logger_TEST', 'testLog', 'This is a test');
System.assertEquals('## >> [APU000_Logger_TEST][testLog][MESSAGE]: This is a test [EVENT PUBLISHED=true]', results);

APU002_Context.bypassEventPublishingInTestContext = true;

Callout Tracking Framework Implementation and Testing

The Callout Tracking Framework is based on Platform Event CalloutTracker__e event object with Publish Option set to Publish Immediately:

FieldDescription
Endpoint__cEndpoint URL of the service retrieved from Http Request Service settings
EndTime__cRequest End Time calculated on the fly
Headers__cJSON Custom Headers of the service retrieved from Http Request Service settings
HttpMethod__cType of the Http Request (GET, PUT, POST …)
Message__cCallout errors message if any
QueryParameters__cParameters used in URL
Resource__cName of the resource that was called retrieved from Http Request Service settings
ServiceId__cId of the service retrieved from Http Request Service settings
StartTime__cRequest Start Time calculated on the fly
Status__cHttp Request Status after the request has received a reply
StatusCode__c Http Request Status Code after the request has received a reply
Timeout__cHttp Request timeout value retrieved from Http Request Service settings
TransactionId__c System generated GUID
UserId__c Id of the connected User
Username__c Username of the connected User

1 Custom Permission is also needed to control who can trigger the event:

Custom PermissionBehavior
EN_CALLOUT_TRACKINGwill enable callout tracking for assigned users

If the global setting Callout Tracking Enabled is checked or the custom permission EN_CALLOUT_TRACKING is assigned then the message will be published in the Platform Event.

Remember that Callout Tracking is embedded in the Callout Framework so you don’t need to call it explicitly. If you want to unit test, you can type these lines:

HttpRequestService__mdt service = HttpRequestService__mdt.getInstance('TestingMockService');

WRP003_HttpRequest wrpReq = new WRP003_HttpRequest();
wrpReq.endPoint = 'https://mock';
wrpReq.requestType = 'GET';
wrpReq.header.put('testH', 'testV');
wrpReq.queryParameters = '/Id/xxxx';
wrpReq.timeout = 120000;

APU002_Context.bypassEventPublishingInTestContext = false;

Boolean results = APU000_Logger.trackCallout(service, System.now(), System.now(), 'Sent', wrpReq);
System.assertEquals(true, results);

APU002_Context.bypassEventPublishingInTestContext = true;

Callout Framework Implementation and Testing

For each new resource to implement, you need to create 1 entry in Http Request Services with all the static and default configuration that is needed to call this resource.

Then you can use the Framework dynamic resolution classes or implement some extra customization by extending the Framework.

ITF004_HttpRequestManager requestManager = new DM003_HttpRequestService('MyNewResource');

Here, we will get a new instance to manage callout to this resource with all framework capabilities (Logging, Tracking …) and default behavior. The interface will give you access to 2 methods parseHttpResponse which can be overriden and sendRequest which will execute the callout. In case for any of your resource you need to implement special behavior to parse the response, you can write a new class extending DM003_HttpRequestService.

public inherited sharing class MyCustomImpl extends DM003_HttpRequestService implements ITF004_HttpRequestManager{

    public MyCustomImpl(String serviceName){
        super(serviceName);
    }

    public override void parseHttpResponse(WRP003_HttpRequest httpRequestInfos){
        //WRITE YOUR OWN LOGIC FOR THESE SET OF RESOURCES
        system.debug('overriden');
    }
}

And then instantiate it like this to execute:

ITF004_HttpRequestManager requestManager = new MyCustomImpl('MyNewResource');

WRP003_HttpRequest wrpReq = new WRP003_HttpRequest();
wrpReq.requestType = 'GET';

requestManager.sendRequest(wrpReq);

It must display “overriden” in Debug Log.

In general, we will have to use 1 class per System to communicate with and not one class per resource, because each system will have their own implementation, constraints and behaviors that have to be handled specifically.

The WRP003_HttpRequest class is used as a container to send information through the Http Request, you can customize some information and most of them are calculated or retrieved from Http Request Services setting:

VariableDescription
bodyHttp Request Body if any (not used in GET)
endPointEndPoint URL of the service retrieved from Http Request Service settings
queryParametersURL parameters to append if any
requestTypeType of the Http Request (GET, PUT, POST …)
timeout Http Request timeout value retrieved from Http Request Service settings
userNameusername for Basic Authentication (just for testing purpose, use Named Credentials instead)
passwordpassword for Basic Authentication (just for testing purpose, use Named Credentials instead)
certificateSSL certificate name (just for testing purpose, use Named Credentials instead)
headerMap add more header key/value if any
httpRequestHttp Request that has been built
httpResponseHttp Response from callout
theExceptionAny exception that has been caught during the callout

All these information are valuable, used by the Callout Framework and the Callout Tracking Framework, and can be used in the parseHttpResponse to handle the result of the callout and the expected behavior depending on the external system you are integrating with (ex: technical/functional error codes, exception…).

So now, what remains is the testing part. As you already know, it’s not possible to test Callout in Test Context, it will break your test unit and you won’t be able to cover your process if there at least one callout in it. To overcome this situation, we will use mocking to simulate the callout behavior.

You will start to write the Mock class like this:

public inherited sharing class TestingMock implements HttpCalloutMock{
       public HttpResponse respond(HttpRequest req) {
           HttpResponse res = new HttpResponse();
           res.setStatus('OK');
           res.setStatusCode(200);
           res.setBody('This is a mock');

           return res;
       }
}

In my case it’s an inner class of my test class because I want to handle specific response depending on my test case. Now, I’m able to use to replace my real callout with this fake callout on the fly:

@isTest
    private static void testSuccess(){
        Test.setMock(HttpCalloutMock.class, new TestingMock());

        Test.startTest();

        ITF004_HttpRequestManager requestManager = new DM003_HttpRequestService('TestingMockService');
        WRP003_HttpRequest wrpReq = new WRP003_HttpRequest();
        wrpReq.requestType = 'GET';

        requestManager.sendRequest(wrpReq);

        System.assertNotEquals(null, wrpReq.httpResponse);
        System.assertEquals('This is a mock', wrpReq.httpResponse.getBody());
        System.assertEquals('OK', wrpReq.httpResponse.getStatus());
        System.assertEquals(200, wrpReq.httpResponse.getStatusCode());

        Test.stopTest();
    }

As you can see, I didn’t touch my real implementation of the callout, all I need to do is to declare which class will be used as a Mock. So, in order to test full process in test context, you will only need to write the mock class and declare it like this in your test method:

Test.setMock(HttpCalloutMock.class, new TestingMock());

Sounds great? But wait !!! There is a big issue behind this approach! Imagine your process is making more than 1 callout to different endpoints, what do you think will happen? Salesforce will apply the same mock instance to all callout! Meaning, you won’t be able to have different responses for different services or use cases.

To overcome this, I will introduce the concept of multimocking. Let’s take the same example assuming we have 2 callout to 2 different systems.

I will create 2 inner classes like this in my test class to simulate 2 different responses:

public inherited sharing class TestingMock1 implements HttpCalloutMock{
       public HttpResponse respond(HttpRequest req) {
           HttpResponse res = new HttpResponse();
           res.setStatus('OK');
           res.setStatusCode(200);
           res.setBody('This is a mock1');

           return res;
       }
}

public inherited sharing class TestingMock2 implements HttpCalloutMock{
       public HttpResponse respond(HttpRequest req) {
           HttpResponse res = new HttpResponse();
           res.setStatus('OK');
           res.setStatusCode(200);
           res.setBody('This is a mock2');

           return res;
       }
}

Test.setMock(HttpCalloutMock.class, new TestingMock1()); can only be called once per test method so I cannot declare my second mock class. But with the MCK000_MultiRequestMock class, it’s now possible:

@isTest
    private static void testSuccess(){
        MCK000_MultiRequestMock multiMock = new MCK000_MultiRequestMock();
        Test.setMock(HttpCalloutMock.class, multiMock);

        multiMock.addRequestMock('SID-0000', new TestingMock1());
        multiMock.addRequestMock('SID-0001', new TestingMock2());

        Test.startTest();

        ITF004_HttpRequestManager requestManager1 = new DM003_HttpRequestService('TestingMockService1');
        WRP003_HttpRequest wrpReq1 = new WRP003_HttpRequest();
        wrpReq1.requestType = 'GET';

        ITF004_HttpRequestManager requestManager2 = new DM003_HttpRequestService('TestingMockService2');
        WRP003_HttpRequest wrpReq2 = new WRP003_HttpRequest();
        wrpReq2.requestType = 'GET';

        requestManager1.sendRequest(wrpReq1);
        requestManager2.sendRequest(wrpReq2);

        System.assertNotEquals(null, wrpReq1.httpResponse);
        System.assertEquals('This is a mock1', wrpReq1.httpResponse.getBody());
        System.assertEquals('OK', wrpReq1.httpResponse.getStatus());
        System.assertEquals(200, wrpReq1.httpResponse.getStatusCode());

        System.assertNotEquals(null, wrpReq2.httpResponse);
        System.assertEquals('This is a mock2', wrpReq2.httpResponse.getBody());
        System.assertEquals('OK', wrpReq2.httpResponse.getStatus());
        System.assertEquals(200, wrpReq2.httpResponse.getStatusCode());

        Test.stopTest();
    }

The MCK000_MultiRequestMock class will play as a router to the right implementation of each resource identified by the serviceId SID-XXX. It’s important that each resource has it unique ServiceId in order to identify it and make the match with the mock implementation.

How to deal with Platform Events ?

So far, you have learned how to use the Framework, implement your callout and unit test your processes. But we haven’t seen yet how to retrieve all platform events information. There are many possibilities to do so.

Platform Event-Triggered Flow:

In this example, every time a platform event is received, a flow will be triggered to send chatter notification in my feed with details from the event message.

Process Builder:

In this example, every time a platform event is received, a Process Builder will be triggered to send chatter notification in my feed with details from the event message.

Trigger:

trigger CalloutTrackerTrigger on CalloutTracker__e (after insert) {
     //IMPLEMENT CUSTOM LOGIC HERE
}

Be aware that only after insert event is available. You can implement any custom logics to handle your event notification.

Lightning Component or Lightning Web Component using empAPI:

<aura:component implements="flexipage:availableForAllPageTypes" access="global" >
	<lightning:empApi aura:id="empApi" />
    <aura:handler name="init" value="{!this}" action="{!c.onInit}"/>
    <aura:attribute name="subscription" type="Object" />
    <aura:attribute name="results" type="List" />
    <aura:attribute name="columns" type="List"/>
    
    <div>
  		<div class="slds-card">
            <lightning:button label="Subscribe" onclick="{! c.subscribe }" />
            <lightning:button label="Unsubscribe" onclick="{! c.unsubscribe }" disabled="{!empty(v.subscription)}"/>
        </div>
    
        <div class="slds-card">
            <lightning:datatable
                        keyField="TransactionId__c"
                        data="{! v.results }"
                        columns="{! v.columns }"
                        hideCheckboxColumn="true"/>
        </div>
    </div>	
</aura:component>

({
    // Sets an empApi error handler on component initialization
    onInit : function(component, event, helper) {
        component.set('v.columns', [
            {label: 'Transaction Id', fieldName: 'TransactionId__c', type: 'text'},
            {label: 'Start Time', fieldName: 'StartTime__c', type: 'datetime-local'},
            {label: 'End Time', fieldName: 'EndTime__c', type: 'datetime-local'},
            {label: 'User Id', fieldName: 'UserId__c', type: 'text'},
            {label: 'User Name', fieldName: 'Username__c', type: 'text'},
            {label: 'Class Name', fieldName: 'ClassName__c', type: 'text'},
            {label: 'Method Name', fieldName: 'MethodName__c', type: 'text'},
            {label: 'Message', fieldName: 'Message__c', type: 'text'},
        	{label: 'Log Level', fieldName: 'LogLevel__c', type: 'text'}]);
        // Get the empApi component
        const empApi = component.find('empApi');
        //  below line to enable debug logging (optional)
           empApi.setDebugFlag(true);
    },
    // Invokes the subscribe method on the empApi component
    subscribe : function(component, event, helper) {
        // Get the empApi component
        const empApi = component.find('empApi');
        // Get the channel from the input box
        const channel = '/event/LoggingMessage__e';
        // Replay option to get new events
        const replayId = -1;
 
        // Subscribe to an event
        empApi.subscribe(channel, replayId, $A.getCallback(eventReceived => {
            // Process event (this is called each time we receive an event)
            var resultList = component.get('v.results');
            resultList.push(eventReceived.data.payload);
			component.set('v.results', resultList);
            console.log('Received event 1 ', eventReceived.data);
 
            console.log('Received event2  ', eventReceived.data.payload);
        }))
        .then(subscription => {
            // Confirm that we have subscribed to the event channel.
            // We haven't received an event yet.
            console.log('Subscribed to channel ', subscription.channel);
            // Save subscription to unsubscribe later
            component.set('v.subscription', subscription);
        });
    },
 
    // Invokes the unsubscribe method on the empApi component
    unsubscribe : function(component, event, helper) {
        // Get the empApi component
        const empApi = component.find('empApi');
        // Get the subscription that we saved when subscribing
        const subscription = component.get('v.subscription');
 
        // Unsubscribe from event
        empApi.unsubscribe(subscription, $A.getCallback(unsubscribed => {
          // Confirm that we have unsubscribed from the event channel
          console.log('Unsubscribed from channel '+ unsubscribed.subscription);
          component.set('v.subscription', null);
        }));
    }
})

Add this component to the a new Lightning App and see the event messages filling the data table when you click on subscribe. This component has been customized to display logging message only when calling this line:

APU000_Logger.log(LoggingLevel.INFO,'MyClass', 'MyMethod','MyMessage');

There are many examples on Salesforce website to do it with LWC and it’s up to you to customize as you need.

Others:

You can also implement any other external listener (ex: Java, Mulesoft, …) compatible with Bayeux/CometD protocol.

Conclusion

In this article, we have learned how to setup the Callout, Logging, Tracking Framework and how to use them. We have introduced the concept of multimocking and we have also learned the concept of Platform Events and the way we can leverage it to special use cases like tracking and monitoring.

Hope you enjoy reading this article, see you soon for the next one ...

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.