Saturday 26 June 2021

Extracting Limits Information from Test Debug Logs

(The follow up to No Limits by 2Unlimited)


In large enterprise Salesforce implementations there's often a particular piece of functionality that is limit-poor - it consumes most of one or more limits as a matter of course.  It might have to retrieve information from myriad standard and custom objects, update a whole load of other objects and do a bunch of processing in between that consumes a lot of CPU time, and when it is invoked with a large number of records it flies close to the maximum values like Icarus flying too close to the sun. Often these areas will have unit tests so that unlike Icarus they don't fall into the sea and drown if the code is changed to add a few more SOQL queries.

Often these tests will try to ensure the published maximum number of records can still be handled, but the problem there is that by the time the test fails you have nowhere to go. Another approach is to determine how much of each limit is currently consumed and fail the test if it goes over, which leads to brittle tests that fail for minor changes or, in the case of CPU time, flappy tests that sometimes pass and sometimes fail  even though the code hasn't changed, depending on what else is happening on the Salesforce instance. 

Ideally there would be some way to figure out when the consumed limits change, but without the nuclear option of failing the unit test. In Apex everything that tests do is rolled back when the test completes, regardless of success or failure, sending notifications isn't an option, nor is saving the limit information to an object. This is something I've been wrestling with for years and haven't been able to come up with a solution for. Until now. And in a turn of events that will surprise nobody, it involves a Salesforce CLI plug-in.

The Plug-in

The limitlog plug-in executes the unit test supplied by the user, then relies on the fact that unit tests always generate a debug log - from the Salesforce help on Debug Logs Order of Precedence :

If you don’t have active trace flags, synchronous and asynchronous Apex tests execute with the default logging levels.

The most recent log for the user is then retrieved, and the limits information parsed from it. There's a bit of a wrinkle around this if your org has a namespace, so the plug-in allows you to specify one if you need something other than the default.

The limits information is then returned in JSON format or written to a Salesforce custom object - you can find the metadata for this at the limitlogsalesforce Github repository. 

The metadata also includes a flow that checks the limit information from the previous run for the unit test and sends me an email if anything is different:

So now I can add the plug-in to my continuous integration operations and get notified if the limits consumption has gone up, without having to force unit test failures. 

Installing and Executing

If you want to capture the limit information in Salesforce, the first thing to do is spin up a scratch org or developer edition, clone the limitlogsalesforce Github repository and push/deploy to this org. Make sure to authenticate against it via the CLI.

Next, identify or create a unit test that has some limit impact - this can be in the same developer/scratch org or a different one.

The install the plug-in into your CLI :

    sfdx plugins:install limitlog

Then execute the test command from the plug-in as follows:

sfdx bblimlog:test -n LimitLogSample -r KABDEV -t LimitLogSampleTest.DoWorkTest -s KAB_TUTORIAL -u LIMITLOG -w


  • -n LimitLogSample is the unique name for this test - the flow uses this to find the previous record and figure out if anything has changed.
  • -r KABDEV is the username to run the tests as. If your tests are in the same org as you are writing the data, you can omit this.
  • -t LimitLogSampleTest.DoWorkTest is the unit test method to run
  • -s KAB_TUTORIAL is the namespace of my dev org - if you don't have a namespace, omit this to pick up the default
  • -u LIMITLOG is the username for the org where the test information will be written to
  • -w says to write the limits objects to Salesforce. If you don't supply this, make sure to add the --json switch to see the output.

You will then see output similar to the following:

Executing test LimitLogSampleTest.DoWorkTest
Retrieving test log file
Extracting limits information
Writing limits information to Salesforce
Limits information written to Salesforce

and the limits record will be available in the Salesforce UI:

and here's the list of records in my org that caused the notifications to be sent when the limits increased:

In future blogs I'll go through how this works under the hood, but this feels like enough for now. As this is the first version of the plug-in I'm sure there will be some scenarios it doesn't handle - if you come across one of these, please raise an issue in the plug-in source repo and I'll see what I can do.

Friday 18 June 2021

Permission Set Group Assignments with Expiration Dates


Permission set group assignments with expiration dates are in beta in Summer 21, and do pretty much what it says on the tin - when a permission set group is assigned to a user, an expiration date can be specified after which the assignment will be removed. This might seem like a small change, but it's a powerful one. The release notes mention the obvious use case - someone needing additional permissions for the duration of a project, but there are a fair few more that spring to my mind. 

Use Cases

Pilot Program 

If you are trialling some new functionality and you want everyone to try it out and give feedback by a certain date, provide access to the app/tab/custom objects through a permission set group and set the expiration date to the pilot end date. The artificial scarcity introduced by a hard deadline usually focuses effort.

Scheduled Elevated Permissions

Does your accounts team need access to a bunch of Salesforce data to carry out invoicing, but only on the last day of the month? You can create permission set assignments programatically, so just set up a scheduled apex job that supplies the expiration date, along the lines of :

PermissionSetAssignment psa=new PermissionSetAssignment(
                                  ExpirationDate=Date.newInstance(2021, 06, 20));
insert psa;

and there's no need to clean up the access later.

Elevated Permissions for an Event

When attending a trade show, you often want those attending to be able to capture leads even though that isn't their usual role. Assign them the permission and expire it at the end of the day of the event and it's one less thing to worry about afterwards.

Additional Help for New Joiners

When new staff are being onboarded to Salesforce, it can be useful to give them additional information or a way to get help on the pages that they regularly use, but once they have found their feet you can remove the stabilisers (training wheels for our friends across the pond). Create a group with a permission set containing a New Joiners custom permission and assign that to new joiners with an expiration date of two weeks time, and you can conditionally display information based on that. 

 Like an explanation of what a Lead is and how it is worked

Maybe Another

This one I haven't really thought through, but it relates to the sudo command, which in Unix like systems allows you to take on the security privileges of another user, typically the super user.  Using the programmatic assignment from above, users with the appropriate permissions could elect to give themselves additional permissions for the day, thus allowing them to carry out some destructive changes when they need to, but stopping them from accidentally deleting records on other occasions.

The One that Got Away

My favourite use case that I came up with turned out not to be possible. It was going to be punishing bad actors and the example was removing the ability for a user to change the amount or probability on opportunities if they were doing this too often. A muting permission set seemed like it would offer this capability, but sadly these can only mute other permissions granted from the same permission set group rather than remove a permission from a user regardless of where it came from. Shame.