Apex Stem · Lifecycle Tracing
v1.4.0·Release notes

ApexTrace

The log you already write becomes what your test asserts on.Pin the path taken, on an axis separate from side effects.

A line written for the production debug log becomes the assertion that answers "did this Usecase really take that path?". No instrumenting twice, once for logs and once for tests.

CopyIndustryToOpp.cls — usecase
private Trace t = Trace.of('Copy the parent account industry'); public void invoke() {  this.t.start();   if (this.opportunityIds.isEmpty()) {    this.t.skip('No target opportunities, exiting.');    return;  }  // ... business logic ...  this.t.finish('Copied onto ' + entries.size() + ' records.');}// These 3 lines are the production log AND the test assertion.

Whether it left through skip or ran all the way tofinish is readable from the test side viaTraceFlow.

Three blind spots, one log

This is not a list of three features. It is three blind spots that one lifecycle log closes at once.

The blind spot

"It is not working in production. How far did it get?"

What changes

Scatter System.debug around and the format and granularity vary per author, leaving nothing you can follow afterwards. The label in Trace.of('...') is attached to every line automatically, so which Usecase stopped where is readable from the log alone.

The blind spot

"It skipped, correctly. But there is no way to confirm that."

What changes

With a void return, verifying this means either distorting the return type or adding a @TestVisible field, and test-only code bleeds into production. Place one t.skip() line and it is pinnable throughTraceFlow.isLastSkip().The log you leave for future debugging is itself the verification route.

The blind spot

"Ran it in bulk and hit 101 SOQL queries"

What changes

An implementation whose queries scale with record count sails through a one-record test. ApexTrace records governor usage per Usecase, so the insurance that "it does not explode in bulk" goes into an integration test as a single loose ceiling.

The test that proves nothing

Your tests are green.That is not the same as verified.

A Usecase returning void has an awkward property:it passes tests most easily when it did nothing at all. Early-return with zero DML and the assertion that "verifies the saved records" is justmatching zero against zero, going green without confirming anything. ApexTrace adds a second axis, the exit path, independent of side effects, so a test that was swinging at air fails the moment it swings at air.

How it reads

The same test — "it skips when there are no targets" — verified on the left with side effects only, on the right with the path too. The pink lines are an assertion that proves nothing; the green lines are the TraceFlow that makes it real.

Vacuously true

"It is zero" holds whether it skipped, processed and found zero, or aborted on an exception. It cannot tell three different outcomes apart.

Path-verified

isLastSkip() admits exactly one of the three. Pin the reason message too and you confirm it took the branch you meant.

CopyIndustryToOpp_T.clsSide effects only
MockEloquent mock = new MockEloquent(); (new CopyIndustryToOpp(  new Set<Id>(), mock)).invoke(); // Nothing was savedAssert.areEqual(  0,  mock.upsertedRecordsAt(LBL_UPDATE).size()); // Skipped, processed-zero, or aborted:// this assert passes either way
CopyIndustryToOpp_T.clsPath-verified
MockEloquent mock = new MockEloquent(); (new CopyIndustryToOpp(  new Set<Id>(), mock)).invoke(); // Nothing was savedAssert.areEqual(  0,  mock.upsertedRecordsAt(LBL_UPDATE).size()); // AND it took the skip pathAssert.isTrue(TraceFlow.isLastSkip());Assert.isTrue(  TraceFlow.contains('No target opportunities'));

There are three exit paths: finish (completed),skip (exited normally without doing anything) andabort (did not complete). Keep them distinct and your tests catch both "thought it completed but it skipped" and "should have skipped but it ran" (verifying execution paths with TraceFlow).

A ceiling, not a budget

This is not a tool for deciding how many SOQL queries a Usecase ought to use. It isinsurance: it pins one thing, that running in bulk has not let inan implementation firing a query per record (N+1), and it pins it with a loose ceiling.

Running production-sized bulk through ApexBlueprint and deliberately degrading the implementation to query per record made it firebefore it reached the platform ceiling of 100.

Usage assertion failed: SOQL queries is 32, exceeding the allowed maximum of 10. Actual usage: Invocations: 2, SOQL: 32 (rows: 92), DML: 1 (rows: 60), Callouts: 0

Invocations is 2 while SOQL is 32.That single line separates "extra wiring" from a loop in the body.usageOf(name) pulls a Usecase out by name, so you can pin the one you care about even when the handler calls three. Put discardArrange() on the Arrange / Act boundary and the measured window narrows to the Act alone.

Where Limits can't reach

Asynchronous work (batches / Queueables) cannot be measured by Limits in principle.It only starts running at Test.stopTest(), and the counters then revert to their startTest() values. TraceUsage diffs at context boundaries, so neither affects it.

How to build the governor IT, and how to pick the ceiling

One record. Three readers.

The API is four methods on Trace. Neither path verification nor governor measurement is something you add on top. Once you have writtenstart and an exit, three readers consume that same single record for three different purposes.

Reader 1
Debug log

The person reading in production. Labelled START / SKIP / FINISH lines, each with its reason.

Reader 2
TraceFlow

The mechanism reading in tests. It extracts which path the context closed on, from the same record.

Reader 3
TraceUsage

It reuses that same start-to-exit span as the measurement window for governor usage.

A by-product of this structure is that lifecycle inconsistencies themselves fail your tests. Return without calling an exit and an unbalanced "still open" trace is left behind; under test, Strict mode is on automatically and it raises immediately. In production, Relaxed mode absorbs it, so a logging slip never stops the business.Strictness in tests, tolerance in production(nested traces and the two modes).

The log stops lying

Ordinary logs lie because nobody reads them. They are written and forgotten, and next read during an incident. By then the code has been refactored many times, and a line saying "skipped" while the code actually processed is entirely normal.Nothing breaks, so nobody notices.The moment you put one assertion on it, that log line acquires a consumer. With a consumer, drift surfaces as a failure. It is guaranteed by structure, not by discipline.

Read how to use Trace / TraceFlow / TraceUsage

Position in Apex Stem

ApexTrace is the Lifecycle Logging member of the four OSS libraries that make upApex Stem. It lives in the Usecase layer of theHandler-Usecase Architecture, and across both unit and integration tests it takes on the "path" and "governor headroom in bulk" axes of thetest strategy.

ApexEloquent
Data Access: SOQL / DML and mocking
ApexBlueprint
Test Data Factory: integration-test data for real DML
ApexTrace
Lifecycle Logging: tracing Usecase paths and verifying them in tests
ApexTools
Foundation: the TriggerHandler base class and an HTTP DI wrapper
Related Documents

Start with the Developer Guide

Instrumenting a Usecase, verifying the path with TraceFlow, and measuring governors with TraceUsage, followed through in real code.