diff --git a/README.md b/README.md index 5cb7f9d76..8be3a8377 100644 --- a/README.md +++ b/README.md @@ -5,15 +5,15 @@ The most robust observability solution for Salesforce experts. Built 100% natively on the platform, and designed to work seamlessly with Apex, Lightning Components, Flow, OmniStudio, and integrations. -## Unlocked Package - v4.14.14 +## Unlocked Package - v4.14.15 -[![Install Unlocked Package in a Sandbox](./images/btn-install-unlocked-package-sandbox.png)](https://test.salesforce.com/packaging/installPackage.apexp?p0=04t5Y0000015oWIQAY) -[![Install Unlocked Package in Production](./images/btn-install-unlocked-package-production.png)](https://login.salesforce.com/packaging/installPackage.apexp?p0=04t5Y0000015oWIQAY) +[![Install Unlocked Package in a Sandbox](./images/btn-install-unlocked-package-sandbox.png)](https://test.salesforce.com/packaging/installPackage.apexp?p0=04t5Y0000015obxQAA) +[![Install Unlocked Package in Production](./images/btn-install-unlocked-package-production.png)](https://login.salesforce.com/packaging/installPackage.apexp?p0=04t5Y0000015obxQAA) [![View Documentation](./images/btn-view-documentation.png)](https://github.com/jongpie/NebulaLogger/wiki) -`sf package install --wait 20 --security-type AdminsOnly --package 04t5Y0000015oWIQAY` +`sf package install --wait 20 --security-type AdminsOnly --package 04t5Y0000015obxQAA` -`sfdx force:package:install --wait 20 --securitytype AdminsOnly --package 04t5Y0000015oWIQAY` +`sfdx force:package:install --wait 20 --securitytype AdminsOnly --package 04t5Y0000015obxQAA` --- @@ -238,41 +238,41 @@ This example batchable class shows how you can leverage this feature to relate a ```apex public with sharing class BatchableLoggerExample implements Database.Batchable, Database.Stateful { - private String originalTransactionId; + private String originalTransactionId; - public Database.QueryLocator start(Database.BatchableContext batchableContext) { - // Each batchable method runs in a separate transaction, - // so store the first transaction ID to later relate the other transactions - this.originalTransactionId = Logger.getTransactionId(); + public Database.QueryLocator start(Database.BatchableContext batchableContext) { + // Each batchable method runs in a separate transaction, + // so store the first transaction ID to later relate the other transactions + this.originalTransactionId = Logger.getTransactionId(); - Logger.info('Starting BatchableLoggerExample'); - Logger.saveLog(); + Logger.info('Starting BatchableLoggerExample'); + Logger.saveLog(); - // Just as an example, query all accounts - return Database.getQueryLocator([SELECT Id, Name, RecordTypeId FROM Account]); - } - - public void execute(Database.BatchableContext batchableContext, List scope) { - // One-time call (per transaction) to set the parent log - Logger.setParentLogTransactionId(this.originalTransactionId); + // Just as an example, query all accounts + return Database.getQueryLocator([SELECT Id, Name, RecordTypeId FROM Account]); + } - for (Account account : scope) { - // Add your batch job's logic here + public void execute(Database.BatchableContext batchableContext, List scope) { + // One-time call (per transaction) to set the parent log + Logger.setParentLogTransactionId(this.originalTransactionId); - // Then log the result - Logger.info('Processed an account record', account); - } + for (Account account : scope) { + // Add your batch job's logic here - Logger.saveLog(); + // Then log the result + Logger.info('Processed an account record', account); } - public void finish(Database.BatchableContext batchableContext) { - // The finish method runs in yet-another transaction, so set the parent log again - Logger.setParentLogTransactionId(this.originalTransactionId); + Logger.saveLog(); + } - Logger.info('Finishing running BatchableLoggerExample'); - Logger.saveLog(); - } + public void finish(Database.BatchableContext batchableContext) { + // The finish method runs in yet-another transaction, so set the parent log again + Logger.setParentLogTransactionId(this.originalTransactionId); + + Logger.info('Finishing running BatchableLoggerExample'); + Logger.saveLog(); + } } ``` @@ -282,42 +282,42 @@ Queueable jobs can also leverage the parent transaction ID to relate logs togeth ```apex public with sharing class QueueableLoggerExample implements Queueable { - private Integer numberOfJobsToChain; - private String parentLogTransactionId; + private Integer numberOfJobsToChain; + private String parentLogTransactionId; - private List logEntryEvents = new List(); + private List logEntryEvents = new List(); - // Main constructor - for demo purposes, it accepts an integer that controls how many times the job runs - public QueueableLoggerExample(Integer numberOfJobsToChain) { - this(numberOfJobsToChain, null); - } + // Main constructor - for demo purposes, it accepts an integer that controls how many times the job runs + public QueueableLoggerExample(Integer numberOfJobsToChain) { + this(numberOfJobsToChain, null); + } - // Second constructor, used to pass the original transaction's ID to each chained instance of the job - // You don't have to use a constructor - a public method or property would work too. - // There just needs to be a way to pass the value of parentLogTransactionId between instances - public QueueableLoggerExample(Integer numberOfJobsToChain, String parentLogTransactionId) { - this.numberOfJobsToChain = numberOfJobsToChain; - this.parentLogTransactionId = parentLogTransactionId; - } + // Second constructor, used to pass the original transaction's ID to each chained instance of the job + // You don't have to use a constructor - a public method or property would work too. + // There just needs to be a way to pass the value of parentLogTransactionId between instances + public QueueableLoggerExample(Integer numberOfJobsToChain, String parentLogTransactionId) { + this.numberOfJobsToChain = numberOfJobsToChain; + this.parentLogTransactionId = parentLogTransactionId; + } - // Creates some log entries and starts a new instance of the job when applicable (based on numberOfJobsToChain) - public void execute(System.QueueableContext queueableContext) { - Logger.setParentLogTransactionId(this.parentLogTransactionId); + // Creates some log entries and starts a new instance of the job when applicable (based on numberOfJobsToChain) + public void execute(System.QueueableContext queueableContext) { + Logger.setParentLogTransactionId(this.parentLogTransactionId); - Logger.fine('queueableContext==' + queueableContext); - Logger.info('this.numberOfJobsToChain==' + this.numberOfJobsToChain); - Logger.info('this.parentLogTransactionId==' + this.parentLogTransactionId); + Logger.fine('queueableContext==' + queueableContext); + Logger.info('this.numberOfJobsToChain==' + this.numberOfJobsToChain); + Logger.info('this.parentLogTransactionId==' + this.parentLogTransactionId); - // Add your queueable job's logic here + // Add your queueable job's logic here - Logger.saveLog(); + Logger.saveLog(); - --this.numberOfJobsToChain; - if (this.numberOfJobsToChain > 0) { - String parentLogTransactionId = this.parentLogTransactionId != null ? this.parentLogTransactionId : Logger.getTransactionId(); - System.enqueueJob(new QueueableLoggerExample(this.numberOfJobsToChain, parentLogTransactionId)); - } + --this.numberOfJobsToChain; + if (this.numberOfJobsToChain > 0) { + String parentLogTransactionId = this.parentLogTransactionId != null ? this.parentLogTransactionId : Logger.getTransactionId(); + System.enqueueJob(new QueueableLoggerExample(this.numberOfJobsToChain, parentLogTransactionId)); } + } } ``` diff --git a/nebula-logger/core/main/logger-engine/classes/Logger.cls b/nebula-logger/core/main/logger-engine/classes/Logger.cls index 73b278609..5d6bc9926 100644 --- a/nebula-logger/core/main/logger-engine/classes/Logger.cls +++ b/nebula-logger/core/main/logger-engine/classes/Logger.cls @@ -15,7 +15,7 @@ global with sharing class Logger { // There's no reliable way to get the version number dynamically in Apex @TestVisible - private static final String CURRENT_VERSION_NUMBER = 'v4.14.14'; + private static final String CURRENT_VERSION_NUMBER = 'v4.14.15'; private static final System.LoggingLevel FALLBACK_LOGGING_LEVEL = System.LoggingLevel.DEBUG; private static final List LOG_ENTRIES_BUFFER = new List(); private static final String MISSING_SCENARIO_ERROR_MESSAGE = 'No logger scenario specified. A scenario is required for logging in this org.'; @@ -3473,7 +3473,9 @@ global with sharing class Logger { // intended behavior if (currentAsyncContext == null) { currentAsyncContext = asyncContext; - System.debug(System.LoggingLevel.INFO, 'Nebula Logger - Async Context: ' + System.JSON.serializePretty(asyncContext)); + if (LoggerParameter.ENABLE_SYSTEM_MESSAGES) { + info('Nebula Logger - Async Context: ' + System.JSON.serializePretty(asyncContext, true)).setExceptionDetails(asyncContext.finalizerException); + } } } @@ -3584,11 +3586,31 @@ global with sharing class Logger { // Inner class for tracking details about the current transaction's async context @SuppressWarnings('PMD.ApexDoc') + @TestVisible private class AsyncContext { public final String type; public final String parentJobId; public final String childJobId; public final String triggerId; + public final String finalizerResult; + // Instances of Exception can't be serialized, but instances of AsyncContext + // are sometimes serialized - so, the AsyncContext's Exception is transient, + // and an extra getter for Map, containing the exception's data, + // is used to provide a quick & easy serializable version + public transient final Exception finalizerException; + public Map finalizerUnhandledException { + get { + return finalizerException == null + ? null + : new Map{ + 'cause' => this.finalizerException.getCause(), + 'lineNumber' => this.finalizerException.getLineNumber(), + 'message' => this.finalizerException.getMessage(), + 'stackTraceString' => this.finalizerException.getStackTraceString(), + 'typeName' => this.finalizerException.getTypeName() + }; + } + } public AsyncContext(Database.BatchableContext batchableContext) { this.childJobId = batchableContext?.getChildJobId(); @@ -3597,7 +3619,11 @@ global with sharing class Logger { } public AsyncContext(System.FinalizerContext finalizerContext) { - this.parentJobId = finalizerContext?.getAsyncApexJobId(); + if (finalizerContext != null) { + this.finalizerException = finalizerContext.getException(); + this.finalizerResult = finalizerContext.getResult()?.name(); + this.parentJobId = finalizerContext.getAsyncApexJobId(); + } this.type = System.FinalizerContext.class.getName(); } diff --git a/nebula-logger/core/main/logger-engine/lwc/logger/loggerService.js b/nebula-logger/core/main/logger-engine/lwc/logger/loggerService.js index 534cd90e1..d885b3cb8 100644 --- a/nebula-logger/core/main/logger-engine/lwc/logger/loggerService.js +++ b/nebula-logger/core/main/logger-engine/lwc/logger/loggerService.js @@ -10,7 +10,7 @@ import LoggerServiceTaskQueue from './loggerServiceTaskQueue'; import getSettings from '@salesforce/apex/ComponentLogger.getSettings'; import saveComponentLogEntries from '@salesforce/apex/ComponentLogger.saveComponentLogEntries'; -const CURRENT_VERSION_NUMBER = 'v4.14.14'; +const CURRENT_VERSION_NUMBER = 'v4.14.15'; const CONSOLE_OUTPUT_CONFIG = { messagePrefix: `%c Nebula Logger ${CURRENT_VERSION_NUMBER} `, diff --git a/nebula-logger/core/tests/configuration/utilities/LoggerMockDataCreator.cls b/nebula-logger/core/tests/configuration/utilities/LoggerMockDataCreator.cls index 6b95a0b52..72419c376 100644 --- a/nebula-logger/core/tests/configuration/utilities/LoggerMockDataCreator.cls +++ b/nebula-logger/core/tests/configuration/utilities/LoggerMockDataCreator.cls @@ -323,7 +323,7 @@ public class LoggerMockDataCreator { public static Schema.Organization getOrganization() { // TODO Switch to creating mock instance of Schema.Organization with sensible defaults that tests can then update as needed for different scenarios if (cachedOrganization == null) { - cachedOrganization = [SELECT Id, Name, InstanceName, IsSandbox, NamespacePrefix, OrganizationType, TrialExpirationDate FROM Organization]; + cachedOrganization = [SELECT Id, Name, InstanceName, IsSandbox, NamespacePrefix, OrganizationType, TrialExpirationDate FROM Organization LIMIT 1]; } return cachedOrganization; } @@ -457,6 +457,7 @@ public class LoggerMockDataCreator { @SuppressWarnings('PMD.ApexDoc') public class MockFinalizerContext implements System.FinalizerContext { + private Exception apexException; private Id asyncApexJobId; public MockFinalizerContext() { @@ -467,19 +468,24 @@ public class LoggerMockDataCreator { this.asyncApexJobId = asyncApexJobId; } + public MockFinalizerContext(Exception ex) { + this(); + this.apexException = ex; + } + public Id getAsyncApexJobId() { return this.asyncApexJobId; } public Exception getException() { - return null; + return this.apexException; } public System.ParentJobResult getResult() { - return System.ParentJobResult.SUCCESS; + return this.apexException == null ? System.ParentJobResult.SUCCESS : System.ParentJobResult.UNHANDLED_EXCEPTION; } - public Id getRequestId() { + public String getRequestId() { return System.Request.getCurrent().getRequestId(); } } diff --git a/nebula-logger/core/tests/logger-engine/classes/Logger_Tests.cls b/nebula-logger/core/tests/logger-engine/classes/Logger_Tests.cls index 2bd16ad8b..1c7c0bd2d 100644 --- a/nebula-logger/core/tests/logger-engine/classes/Logger_Tests.cls +++ b/nebula-logger/core/tests/logger-engine/classes/Logger_Tests.cls @@ -825,6 +825,41 @@ private class Logger_Tests { System.Assert.areEqual(Database.BatchableContext.class.getName(), logEntryEvent.AsyncContextType__c); } + @IsTest + static void it_should_auto_create_log_entry_event_for_batchable_async_context_details_when_system_messages_are_enabled() { + Database.BatchableContext mockContext = new LoggerMockDataCreator.MockBatchableContext(); + LoggerDataStore.setMock(LoggerMockDataStore.getEventBus()); + LoggerTestConfigurator.setMock(new LoggerParameter__mdt(DeveloperName = 'EnableLoggerSystemMessages', Value__c = 'true')); + System.Assert.isTrue(LoggerParameter.ENABLE_SYSTEM_MESSAGES, 'System messages should be enabled'); + System.Assert.areEqual(0, Logger.getBufferSize()); + System.Assert.areEqual(0, LoggerMockDataStore.getEventBus().getPublishedPlatformEvents().size()); + + Logger.setAsyncContext(mockContext); + Logger.saveLog(Logger.SaveMethod.EVENT_BUS); + + System.Assert.areEqual(0, Logger.getBufferSize()); + Integer publishedEventsCount = LoggerMockDataStore.getEventBus().getPublishedPlatformEvents().size(); + System.Assert.isTrue( + publishedEventsCount >= 1, + publishedEventsCount + + ' events published, but at least 1 should have been auto-created & published\n\n' + + System.JSON.serializePretty(LoggerMockDataStore.getEventBus().getPublishedPlatformEvents()) + ); + LogEntryEvent__e expectedLogEntryEvent = new LogEntryEvent__e( + LoggingLevel__c = System.LoggingLevel.INFO.name(), + Message__c = 'Nebula Logger - Async Context: ' + System.JSON.serializePretty(new Logger.AsyncContext(mockContext), true) + ); + List matchingPublishedLogEntryEvents = (List) LoggerMockDataStore.getEventBus() + .getMatchingPublishedPlatformEvents(expectedLogEntryEvent); + System.Assert.areEqual(1, matchingPublishedLogEntryEvents.size()); + LogEntryEvent__e matchingPublishedLogEntryEvent = matchingPublishedLogEntryEvents.get(0); + System.Assert.areEqual(Database.BatchableContext.class.getName(), matchingPublishedLogEntryEvent.AsyncContextType__c); + System.Assert.areEqual(expectedLogEntryEvent.LoggingLevel__c, matchingPublishedLogEntryEvent.LoggingLevel__c); + System.Assert.areEqual(expectedLogEntryEvent.Message__c, matchingPublishedLogEntryEvent.Message__c); + System.Assert.isNull(matchingPublishedLogEntryEvent.ExceptionMessage__c); + System.Assert.isNull(matchingPublishedLogEntryEvent.ExceptionType__c); + } + @IsTest static void it_should_set_async_context_details_for_finalizer_context_when_event_published() { Id mockParentAsyncApexJobId = LoggerMockDataCreator.createId(Schema.AsyncApexJob.SObjectType); @@ -884,6 +919,81 @@ private class Logger_Tests { System.Assert.areEqual(System.FinalizerContext.class.getName(), logEntryEvent.AsyncContextType__c); } + @IsTest + static void it_should_auto_create_log_entry_event_for_success_finalizer_async_context_details_when_system_messages_are_enabled() { + System.FinalizerContext mockContext = new LoggerMockDataCreator.MockFinalizerContext(); + System.Assert.isNull(mockContext.getException()); + System.Assert.areEqual(System.ParentJobResult.SUCCESS, mockContext.getResult()); + LoggerDataStore.setMock(LoggerMockDataStore.getEventBus()); + LoggerTestConfigurator.setMock(new LoggerParameter__mdt(DeveloperName = 'EnableLoggerSystemMessages', Value__c = 'true')); + System.Assert.isTrue(LoggerParameter.ENABLE_SYSTEM_MESSAGES, 'System messages should be enabled'); + System.Assert.areEqual(0, Logger.getBufferSize()); + System.Assert.areEqual(0, LoggerMockDataStore.getEventBus().getPublishedPlatformEvents().size()); + + Logger.setAsyncContext(mockContext); + Logger.saveLog(Logger.SaveMethod.EVENT_BUS); + + System.Assert.areEqual(0, Logger.getBufferSize()); + Integer publishedEventsCount = LoggerMockDataStore.getEventBus().getPublishedPlatformEvents().size(); + System.Assert.isTrue( + publishedEventsCount >= 1, + publishedEventsCount + + ' events published, but at least 1 should have been auto-created & published\n\n' + + System.JSON.serializePretty(LoggerMockDataStore.getEventBus().getPublishedPlatformEvents()) + ); + LogEntryEvent__e expectedLogEntryEvent = new LogEntryEvent__e( + LoggingLevel__c = System.LoggingLevel.INFO.name(), + Message__c = 'Nebula Logger - Async Context: ' + System.JSON.serializePretty(new Logger.AsyncContext(mockContext), true) + ); + List matchingPublishedLogEntryEvents = (List) LoggerMockDataStore.getEventBus() + .getMatchingPublishedPlatformEvents(expectedLogEntryEvent); + System.Assert.areEqual(1, matchingPublishedLogEntryEvents.size()); + LogEntryEvent__e matchingPublishedLogEntryEvent = matchingPublishedLogEntryEvents.get(0); + System.Assert.areEqual(System.FinalizerContext.class.getName(), matchingPublishedLogEntryEvent.AsyncContextType__c); + System.Assert.areEqual(expectedLogEntryEvent.LoggingLevel__c, matchingPublishedLogEntryEvent.LoggingLevel__c); + System.Assert.areEqual(expectedLogEntryEvent.Message__c, matchingPublishedLogEntryEvent.Message__c); + System.Assert.isNull(matchingPublishedLogEntryEvent.ExceptionMessage__c); + System.Assert.isNull(matchingPublishedLogEntryEvent.ExceptionType__c); + } + + @IsTest + static void it_should_auto_create_log_entry_event_for_unhandled_exception_finalizer_async_context_details_when_system_messages_are_enabled() { + System.DmlException mockFinalizerUnhandledException = new System.DmlException('Oops'); + System.FinalizerContext mockContext = new LoggerMockDataCreator.MockFinalizerContext(mockFinalizerUnhandledException); + System.Assert.isNotNull(mockContext.getException()); + System.Assert.areEqual(System.ParentJobResult.UNHANDLED_EXCEPTION, mockContext.getResult()); + LoggerDataStore.setMock(LoggerMockDataStore.getEventBus()); + LoggerTestConfigurator.setMock(new LoggerParameter__mdt(DeveloperName = 'EnableLoggerSystemMessages', Value__c = 'true')); + System.Assert.isTrue(LoggerParameter.ENABLE_SYSTEM_MESSAGES, 'System messages should be enabled'); + System.Assert.areEqual(0, Logger.getBufferSize()); + System.Assert.areEqual(0, LoggerMockDataStore.getEventBus().getPublishedPlatformEvents().size()); + + Logger.setAsyncContext(mockContext); + Logger.saveLog(Logger.SaveMethod.EVENT_BUS); + + System.Assert.areEqual(0, Logger.getBufferSize()); + Integer publishedEventsCount = LoggerMockDataStore.getEventBus().getPublishedPlatformEvents().size(); + System.Assert.isTrue( + publishedEventsCount >= 1, + publishedEventsCount + + ' events published, but at least 1 should have been auto-created & published\n\n' + + System.JSON.serializePretty(LoggerMockDataStore.getEventBus().getPublishedPlatformEvents()) + ); + LogEntryEvent__e expectedLogEntryEvent = new LogEntryEvent__e( + LoggingLevel__c = System.LoggingLevel.INFO.name(), + Message__c = 'Nebula Logger - Async Context: ' + System.JSON.serializePretty(new Logger.AsyncContext(mockContext), true) + ); + List matchingPublishedLogEntryEvents = (List) LoggerMockDataStore.getEventBus() + .getMatchingPublishedPlatformEvents(expectedLogEntryEvent); + System.Assert.areEqual(1, matchingPublishedLogEntryEvents.size()); + LogEntryEvent__e matchingPublishedLogEntryEvent = matchingPublishedLogEntryEvents.get(0); + System.Assert.areEqual(System.FinalizerContext.class.getName(), matchingPublishedLogEntryEvent.AsyncContextType__c); + System.Assert.areEqual(expectedLogEntryEvent.LoggingLevel__c, matchingPublishedLogEntryEvent.LoggingLevel__c); + System.Assert.areEqual(expectedLogEntryEvent.Message__c, matchingPublishedLogEntryEvent.Message__c); + System.Assert.areEqual(mockFinalizerUnhandledException.getMessage(), matchingPublishedLogEntryEvent.ExceptionMessage__c); + System.Assert.areEqual(mockFinalizerUnhandledException.getTypeName(), matchingPublishedLogEntryEvent.ExceptionType__c); + } + @IsTest static void it_should_set_async_context_details_for_queueable_context_when_event_published() { Id mockParentAsyncApexJobId = LoggerMockDataCreator.createId(Schema.AsyncApexJob.SObjectType); @@ -943,6 +1053,41 @@ private class Logger_Tests { System.Assert.areEqual(System.QueueableContext.class.getName(), logEntryEvent.AsyncContextType__c); } + @IsTest + static void it_should_auto_create_log_entry_event_for_queueable_async_context_details_when_system_messages_are_enabled() { + System.QueueableContext mockContext = new LoggerMockDataCreator.MockQueueableContext(); + LoggerDataStore.setMock(LoggerMockDataStore.getEventBus()); + LoggerTestConfigurator.setMock(new LoggerParameter__mdt(DeveloperName = 'EnableLoggerSystemMessages', Value__c = 'true')); + System.Assert.isTrue(LoggerParameter.ENABLE_SYSTEM_MESSAGES, 'System messages should be enabled'); + System.Assert.areEqual(0, Logger.getBufferSize()); + System.Assert.areEqual(0, LoggerMockDataStore.getEventBus().getPublishedPlatformEvents().size()); + + Logger.setAsyncContext(mockContext); + Logger.saveLog(Logger.SaveMethod.EVENT_BUS); + + System.Assert.areEqual(0, Logger.getBufferSize()); + Integer publishedEventsCount = LoggerMockDataStore.getEventBus().getPublishedPlatformEvents().size(); + System.Assert.isTrue( + publishedEventsCount >= 1, + publishedEventsCount + + ' events published, but at least 1 should have been auto-created & published\n\n' + + System.JSON.serializePretty(LoggerMockDataStore.getEventBus().getPublishedPlatformEvents()) + ); + LogEntryEvent__e expectedLogEntryEvent = new LogEntryEvent__e( + LoggingLevel__c = System.LoggingLevel.INFO.name(), + Message__c = 'Nebula Logger - Async Context: ' + System.JSON.serializePretty(new Logger.AsyncContext(mockContext), true) + ); + List matchingPublishedLogEntryEvents = (List) LoggerMockDataStore.getEventBus() + .getMatchingPublishedPlatformEvents(expectedLogEntryEvent); + System.Assert.areEqual(1, matchingPublishedLogEntryEvents.size()); + LogEntryEvent__e matchingPublishedLogEntryEvent = matchingPublishedLogEntryEvents.get(0); + System.Assert.areEqual(System.QueueableContext.class.getName(), matchingPublishedLogEntryEvent.AsyncContextType__c); + System.Assert.areEqual(expectedLogEntryEvent.LoggingLevel__c, matchingPublishedLogEntryEvent.LoggingLevel__c); + System.Assert.areEqual(expectedLogEntryEvent.Message__c, matchingPublishedLogEntryEvent.Message__c); + System.Assert.isNull(matchingPublishedLogEntryEvent.ExceptionMessage__c); + System.Assert.isNull(matchingPublishedLogEntryEvent.ExceptionType__c); + } + @IsTest static void it_should_set_async_context_details_for_schedulable_context_when_event_published() { Id mockCronTriggerId = LoggerMockDataCreator.createId(Schema.CronTrigger.SObjectType); @@ -1001,6 +1146,41 @@ private class Logger_Tests { System.Assert.areEqual(System.SchedulableContext.class.getName(), logEntryEvent.AsyncContextType__c); } + @IsTest + static void it_should_auto_create_log_entry_event_for_scheduleable_async_context_details_when_system_messages_are_enabled() { + System.SchedulableContext mockContext = new LoggerMockDataCreator.MockSchedulableContext(); + LoggerDataStore.setMock(LoggerMockDataStore.getEventBus()); + LoggerTestConfigurator.setMock(new LoggerParameter__mdt(DeveloperName = 'EnableLoggerSystemMessages', Value__c = 'true')); + System.Assert.isTrue(LoggerParameter.ENABLE_SYSTEM_MESSAGES, 'System messages should be enabled'); + System.Assert.areEqual(0, Logger.getBufferSize()); + System.Assert.areEqual(0, LoggerMockDataStore.getEventBus().getPublishedPlatformEvents().size()); + + Logger.setAsyncContext(mockContext); + Logger.saveLog(Logger.SaveMethod.EVENT_BUS); + + System.Assert.areEqual(0, Logger.getBufferSize()); + Integer publishedEventsCount = LoggerMockDataStore.getEventBus().getPublishedPlatformEvents().size(); + System.Assert.isTrue( + publishedEventsCount >= 1, + publishedEventsCount + + ' events published, but at least 1 should have been auto-created & published\n\n' + + System.JSON.serializePretty(LoggerMockDataStore.getEventBus().getPublishedPlatformEvents()) + ); + LogEntryEvent__e expectedLogEntryEvent = new LogEntryEvent__e( + LoggingLevel__c = System.LoggingLevel.INFO.name(), + Message__c = 'Nebula Logger - Async Context: ' + System.JSON.serializePretty(new Logger.AsyncContext(mockContext), true) + ); + List matchingPublishedLogEntryEvents = (List) LoggerMockDataStore.getEventBus() + .getMatchingPublishedPlatformEvents(expectedLogEntryEvent); + System.Assert.areEqual(1, matchingPublishedLogEntryEvents.size()); + LogEntryEvent__e matchingPublishedLogEntryEvent = matchingPublishedLogEntryEvents.get(0); + System.Assert.areEqual(System.SchedulableContext.class.getName(), matchingPublishedLogEntryEvent.AsyncContextType__c); + System.Assert.areEqual(expectedLogEntryEvent.LoggingLevel__c, matchingPublishedLogEntryEvent.LoggingLevel__c); + System.Assert.areEqual(expectedLogEntryEvent.Message__c, matchingPublishedLogEntryEvent.Message__c); + System.Assert.isNull(matchingPublishedLogEntryEvent.ExceptionMessage__c); + System.Assert.isNull(matchingPublishedLogEntryEvent.ExceptionType__c); + } + @IsTest static void it_should_set_parent_transaction_id() { String expectedParentTransactionId = 'imagineThisWereAGuid'; diff --git a/nebula-logger/core/tests/logger-engine/utilities/LoggerMockDataStore.cls b/nebula-logger/core/tests/logger-engine/utilities/LoggerMockDataStore.cls index 37311c772..70eb581f9 100644 --- a/nebula-logger/core/tests/logger-engine/utilities/LoggerMockDataStore.cls +++ b/nebula-logger/core/tests/logger-engine/utilities/LoggerMockDataStore.cls @@ -83,6 +83,39 @@ public without sharing class LoggerMockDataStore { return this.publishedPlatformEvents; } + /** + * @description Returns a list of published platform events that have the same field values + * as the provided platform event record `comparisonPlatformEvent`. This is useful for + * easily filtering to only the `LogEntryEvent__e` records relevant to a particular test method + * in a transaction/test scenario where multiple `LogEntryEvent__e` are being generated. + * Long-term, this helper method might be moved elsewhere, or replaced with something else, + * but for now, the mock event bus is a good-enough spot for it. + * @param comparisonPlatformEvent An instance of the platform event record to use for comparing + * against the list of platform event records that have been published + * @return A list containing any matches. When no matches are found, the list is empty. + */ + public List getMatchingPublishedPlatformEvents(SObject comparisonPlatformEvent) { + Map comparisonFieldToValue = comparisonPlatformEvent.getPopulatedFieldsAsMap(); + List matchingRecords = new List(); + for (SObject eventToCheck : this.getPublishedPlatformEvents()) { + Boolean isMatchingRecord = true; + Map targetFieldToValue = eventToCheck.getPopulatedFieldsAsMap(); + + for (String populatedField : comparisonFieldToValue.keySet()) { + Object expectedValue = comparisonFieldToValue.get(populatedField); + if (targetFieldToValue.containsKey(populatedField) == false || eventToCheck.get(populatedField) != expectedValue) { + isMatchingRecord = false; + break; + } + } + + if (isMatchingRecord) { + matchingRecords.add(eventToCheck); + } + } + return matchingRecords; + } + public override Database.SaveResult publishRecord(SObject platformEvent) { return this.publishRecords(new List{ platformEvent }).get(0); } diff --git a/nebula-logger/extra-tests/classes/name-shadowing/System/ParentJobResult.cls b/nebula-logger/extra-tests/classes/name-shadowing/System/ParentJobResult.cls new file mode 100644 index 000000000..4294d6b06 --- /dev/null +++ b/nebula-logger/extra-tests/classes/name-shadowing/System/ParentJobResult.cls @@ -0,0 +1,10 @@ +//------------------------------------------------------------------------------------------------// +// This file is part of the Nebula Logger project, released under the MIT License. // +// See LICENSE file or go to https://github.com/jongpie/NebulaLogger for full license details. // +//------------------------------------------------------------------------------------------------// + +// This class intentionally does nothing - it's here to ensure that any references +// in Nebula Logger's codebase use `System.ParentJobResult` instead of just `ParentJobResult` +@SuppressWarnings('PMD.ApexDoc, PMD.EmptyStatementBlock') +public without sharing class ParentJobResult { +} diff --git a/nebula-logger/extra-tests/classes/name-shadowing/System/ParentJobResult.cls-meta.xml b/nebula-logger/extra-tests/classes/name-shadowing/System/ParentJobResult.cls-meta.xml new file mode 100644 index 000000000..651b17293 --- /dev/null +++ b/nebula-logger/extra-tests/classes/name-shadowing/System/ParentJobResult.cls-meta.xml @@ -0,0 +1,5 @@ + + + 61.0 + Active + diff --git a/package.json b/package.json index c024195d6..979a6c713 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "nebula-logger", - "version": "4.14.14", + "version": "4.14.15", "description": "The most robust logger for Salesforce. Works with Apex, Lightning Components, Flow, Process Builder & Integrations. Designed for Salesforce admins, developers & architects.", "author": "Jonathan Gillespie", "license": "MIT", diff --git a/sfdx-project.json b/sfdx-project.json index c42f5d7a8..47a477463 100644 --- a/sfdx-project.json +++ b/sfdx-project.json @@ -9,9 +9,9 @@ "path": "./nebula-logger/core", "definitionFile": "./config/scratch-orgs/base-scratch-def.json", "scopeProfiles": true, - "versionNumber": "4.14.14.NEXT", - "versionName": "New Apex Static Method & JavaScript Function Logger.setField()", - "versionDescription": "Added a new Apex static method Logger.setField() and LWC function logger.setField() so custom fields can be set once --> auto-populated on all subsequent LogEntryEvent__e records", + "versionNumber": "4.14.15.NEXT", + "versionName": "Create log entry for AsyncApexContext", + "versionDescription": "Adds additional details to the AsyncApexContext object being logged to expose System.Finalizer-specific details", "releaseNotesUrl": "https://github.com/jongpie/NebulaLogger/releases", "unpackagedMetadata": { "path": "./nebula-logger/extra-tests" @@ -199,6 +199,7 @@ "Nebula Logger - Core@4.14.12-replaced-httprequestendpoint__c-with-httprequestendpointaddress__c": "04t5Y0000015oV0QAI", "Nebula Logger - Core@4.14.13-new-getlogger()-js-function": "04t5Y0000015oW3QAI", "Nebula Logger - Core@4.14.14-new-apex-static-method-&-javascript-function-logger.setfield()": "04t5Y0000015oWIQAY", + "Nebula Logger - Core@4.14.15-create-log-entry-for-asyncapexcontext": "04t5Y0000015obxQAA", "Nebula Logger - Core Plugin - Async Failure Additions": "0Ho5Y000000blO4SAI", "Nebula Logger - Core Plugin - Async Failure Additions@1.0.0": "04t5Y0000015lhiQAA", "Nebula Logger - Core Plugin - Async Failure Additions@1.0.1": "04t5Y0000015lhsQAA",