Simple framework for using feature flags in apex driven by Custom Permissions and Custom Metadata Types.
FeatureFlags
class- Custom Permissions and Metadata Types
- Info with
FeatureEvaluationResult
- In LWC
- Testing
The main entry point is the FeatureFlags
class, which is meant to be used as follows
//a lot of code here, then
FeatureFlags flags = new FeatureFlags();
if(flags.evaluate('enhancedQuoteEditor').isEnabled()){
//do something interesting
}
In the above example
if(flags.evaluate('enhancedQuoteEditor').isEnabled()){
//do something interesting
}
The code works by evaluating (in this order) if:
- The running user has a Custom Permission with a matching name of
enhancedQuoteEditor
. This is done using the standardFeatureManagement
class from Apex - A custom metadata type record of the
FeatureFlag__mdt
type matches the nameenhancedQuoteEditor
and hasIs_Active__c
set totrue
This allows you to configure user-based feature flags (as custom permissions) and global feature flags (as custom metadata types).
If neither a custom permission or custom metadata type record is found, the flag defaults to false
(see more info on the next section).
The call to flags.evaluate('flagName')
returns an instance of FeatureEvaluationResult
, which can be used to determine whether the flag is active and why. For example
FeatureFlags.FeatureEvaluationResult result = flags.evaluate('enhancedQuoteEditor');
System.debug(result);
//these are the possible permutations
[featureName=enhancedQuoteEditor, reason=HAS_CUSTOM_PERMISSION, result=true]
[featureName=enhancedQuoteEditor, reason=MISSING_CUSTOM_PERMISSION, result=false]
[featureName=enhancedQuoteEditor, reason=METADATA_TYPE_ENABLED, result=true]
[featureName=enhancedQuoteEditor, reason=METADATA_TYPE_DISABLED, result=false]
//if there are no matching custom permissions or metadata types
[featureName=enhancedQuoteEditor, reason=FLAG_NOT_FOUND, result=false]
This information can be useful for logging and troubleshooting.
To use a feature flag in LWC, use the static method lwcEvaluate
, which simply returns true
or false
. For example
import { LightningElement } from 'lwc';
import lwcEvaluate from '@salesforce/apex/FeatureFlags.lwcEvaluate';
export default class FlagsInLWC extends LightningElement {
async connectedCallback() {
let featureNewUIComponents = await lwcEvaluate({ featureName: 'featureNewUIComponents' });
if(featureNewUIComponents){
console.log('featureNewUIComponents is enabled. Show the new UI components');
}
else{
console.log('featureNewUIComponents is disabled. Show the old UI components');
}
}
}
If you simply want to set the value of a flag to true
or false
at test time, without relaying on existing custom permissions or metadata types, simply use the static FeatureFlags.setMockValue('mockedFeatureValue', true);
method, as follows
@IsTest
static void testMockValues(){
FeatureFlags.setMockValue('mockedFeatureValue', true);
Test.startTest();
insert new Account(Name='Name');
Test.stopTest();
In the above example, inserting the account fires the AccountTriggerHandler
. Somewhere in that code the FeatureFlags
class will be instantiated, and it will remember that mockedFeatureValue
was set to true
. The code will not try to find this flag in custom permissions or metadata types.
Another approach is when you have a class that takes an instance of FeatureFlags
as a constructor parameter, for example
FeatureFlags flags = new FeatureFlags();
AccountService service = new AccountService(flags)
In this scenario, you can pass an implementation of IFeatureFlagsProvider
, which will allow you to mock the status of the feature flags.
This works because the default constructor of FeatureFlags
using an instance of FeatureFlagsProvider
, which returns the custom permissions and metadata types from the database
public with sharing class FeatureFlagProvider implements IFeatureFlagProvider {
public Set<String> getCustomPermissionNames(){
Set<String> customPermissionNames = new Set<String>();
List<CustomPermission> perms = [SELECT Id, DeveloperName FROM CustomPermission];
for(CustomPermission perm : perms){
customPermissionNames.add(perm.DeveloperName);
}
return customPermissionNames;
}
public Map<String,FeatureFlag__mdt> getFeatureFlags(){
return FeatureFlag__mdt.getAll();
}
}
In a test class, you can create a Mock that implements IFeatureFlagProvider
, and pass this as a constructor for the FeatureFlags
class. For example:
public with sharing class FeatureFlagProviderMock implements IFeatureFlagProvider {
public Set<String> getCustomPermissionNames(){
Set<String> customPermissionNames = new Set<String>();
customPermissionNames.add('permission1');
customPermissionNames.add('permission2');
return customPermissionNames;
}
public Map<String,FeatureFlag__mdt> getFeatureFlags(){
Map<String,FeatureFlag__mdt> flags = new Map<String,FeatureFlag__mdt>();
FeatureFlag__mdt flag1 = new FeatureFlag__mdt(DeveloperName = 'flag1', Is_Active__c = true);
FeatureFlag__mdt flag2 = new FeatureFlag__mdt(DeveloperName = 'flag2', Is_Active__c = false);
flags.put('flag1',flag1);
flags.put('flag2',flag2);
return flags;
}
}
then in your test code
FeatureFlagProviderMock mockFlags = new FeatureFlagProviderMock();
FeatureFlags flags = new FeatureFlags(mockFlags);
Test.startTest();
FeatureFlags.FeatureEvaluationResult result = flags.evaluate('flag1');
Test.stopTest();
You can see a complete implementation of this pattern here