From 6554537186bc8b73b0ce19d1ce646a6176249e3c Mon Sep 17 00:00:00 2001 From: Jonathan Gillespie Date: Sun, 15 Sep 2024 17:12:43 -0400 Subject: [PATCH] Added support for populating origin source metadata fields on LogEntry__c for OmniStudio entries --- README.md | 479 ++++++++++++++++-- docs/apex/Configuration/LoggerParameter.md | 4 + .../LogManagementDataSelector.md | 20 + docs/apex/Logger-Engine/LoggerSObjectProxy.md | 34 ++ docs/apex/index.md | 4 + .../configuration/classes/LoggerParameter.cls | 15 + ...Parameter.QueryOmniProcessData.md-meta.xml | 21 + .../classes/LogEntryEventHandler.cls | 1 + .../classes/LogEntryHandler.cls | 44 ++ .../classes/LogManagementDataSelector.cls | 25 +- ...OriginSourceMetadataType__c.field-meta.xml | 10 + .../fields/OriginType__c.field-meta.xml | 6 + .../AllOmniStudioLogEntries.listView-meta.xml | 18 + .../main/logger-engine/classes/Logger.cls | 7 +- .../classes/LoggerSObjectProxy.cls | 34 ++ .../classes/LoggerStackTrace.cls | 2 +- .../lwc/logger/__tests__/logger.test.js | 3 +- .../lwc/logger/logEntryBuilder.js | 2 +- .../classes/LoggerParameter_Tests.cls | 11 + .../classes/LogEntryEventHandler_Tests.cls | 2 + .../classes/LogEntryHandler_Tests.cls | 127 +++++ .../LogManagementDataSelector_Tests.cls | 54 ++ .../classes/LoggerSObjectProxy_Tests.cls | 45 +- .../classes/LoggerStackTrace_Tests.cls | 12 + .../name-shadowing/Schema/OmniProcess.cls | 10 + .../Schema/OmniProcess.cls-meta.xml | 5 + .../managed-package/sfdx-project.json | 2 +- package.json | 3 +- sfdx-project.json | 8 +- 29 files changed, 946 insertions(+), 62 deletions(-) create mode 100644 nebula-logger/core/main/configuration/customMetadata/LoggerParameter.QueryOmniProcessData.md-meta.xml create mode 100644 nebula-logger/core/main/log-management/objects/LogEntry__c/listViews/AllOmniStudioLogEntries.listView-meta.xml create mode 100644 nebula-logger/extra-tests/classes/name-shadowing/Schema/OmniProcess.cls create mode 100644 nebula-logger/extra-tests/classes/name-shadowing/Schema/OmniProcess.cls-meta.xml diff --git a/README.md b/README.md index 5f9170d0d..7da792e07 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ The most robust observability solution for Salesforce experts. Built 100% natively on the platform, and designed to work seamlessly with Apex, Lightning Components, Flow, Process Builder & integrations. -## Unlocked Package - v4.14.9 +## Unlocked Package - v4.14.10 [![Install Unlocked Package in a Sandbox](./images/btn-install-unlocked-package-sandbox.png)](https://test.salesforce.com/packaging/installPackage.apexp?p0=04t5Y0000015oSQQAY) [![Install Unlocked Package in Production](./images/btn-install-unlocked-package-production.png)](https://login.salesforce.com/packaging/installPackage.apexp?p0=04t5Y0000015oSQQAY) @@ -31,16 +31,17 @@ The most robust observability solution for Salesforce experts. Built 100% native ## Features -1. Easily add log entries via Apex, Lightning Components (lightning web components (LWCs) & aura components), Flow & Process Builder to generate 1 consolidated, unified log -2. Manage & report on logging data using the `Log__c` and `LogEntry__c` objects -3. Leverage `LogEntryEvent__e` platform events for real-time monitoring & integrations -4. Enable logging and set the logging level for different users & profiles using `LoggerSettings__c` custom hierarchy setting +1. A unified logging tool that supports easily adding log entries via Apex, Lightning Components (lightning web components (LWCs) & aura components), Flow & Process Builder, and OmniStudio's OmniScripts +2. For ISVs: optionally leverage Nebula Logger in your own packages - when it's available in a subscriber's org - using [Apex's `Callable` interface](https://developer.salesforce.com/docs/atlas.en-us.apexref.meta/apexref/apex_interface_System_Callable.htm) and Nebula Logger's included implementation `CallableLogger` +3. Manage & report on logging data using the 5 included custom objects `Log__c`, `LogEntry__c`, `LogEntryTag__c`, `LoggerTag__c`, and `LoggerScenario__c` +4. Leverage `LogEntryEvent__e` platform events for real-time monitoring & integrations +5. Enable logging and set the logging level for different users & profiles using `LoggerSettings__c` custom hierarchy setting - In addition to the required fields on this Custom Setting record, `LoggerSettings__c` ships with `SystemLogMessageFormat__c`, which uses Handlebars-esque syntax to refer to fields on the `LogEntryEvent__e` Platform Event. You can use curly braces to denote merge field logic, eg: `{OriginLocation__c}\n{Message__c}` - this will output the contents of `LogEntryEvent__e.OriginLocation__c`, a line break, and then the contents of `LogEntryEvent__e.Message__c` -5. Automatically mask sensitive data by configuring `LogEntryDataMaskRule__mdt` custom metadata rules -6. View related log entries on any Lightning SObject flexipage by adding the 'Related Log Entries' component in App Builder -7. Dynamically assign tags to `Log__c` and `LogEntry__c` records for tagging/labeling your logs -8. Plugin framework: easily build or install plugins that enhance the `Log__c` and `LogEntry__c` objects, using Apex or Flow (not currently available in the managed package) -9. Event-Driven Integrations with [Platform Events](https://developer.salesforce.com/docs/atlas.en-us.platform_events.meta/platform_events/platform_events_intro.htm), an event-driven messaging architecture. External integrations can subscribe to log events using the `LogEntryEvent__e` object - see more details at [the Platform Events Developer Guide site](https://developer.salesforce.com/docs/atlas.en-us.platform_events.meta/platform_events/platform_events_subscribe_cometd.htm) +6. Automatically mask sensitive data by configuring `LogEntryDataMaskRule__mdt` custom metadata rules +7. View related log entries on any Lightning SObject flexipage by adding the 'Related Log Entries' component in App Builder +8. Dynamically assign tags to `Log__c` and `LogEntry__c` records for tagging/labeling your logs +9. Plugin framework: easily build or install plugins that enhance the `Log__c` and `LogEntry__c` objects, using Apex or Flow (not currently available in the managed package) +10. Event-Driven Integrations with [Platform Events](https://developer.salesforce.com/docs/atlas.en-us.platform_events.meta/platform_events/platform_events_intro.htm), an event-driven messaging architecture. External integrations can subscribe to log events using the `LogEntryEvent__e` object - see more details at [the Platform Events Developer Guide site](https://developer.salesforce.com/docs/atlas.en-us.platform_events.meta/platform_events/platform_events_subscribe_cometd.htm) Learn more about the design and history of the project on [Joys Of Apex blog post](https://www.joysofapex.com/advanced-logging-using-nebula-logger/) @@ -111,8 +112,7 @@ After installing Nebula Logger in your org, there are a few additional configura For Apex developers, the `Logger` class has several methods that can be used to add entries with different logging levels. Each logging level's method has several overloads to support multiple parameters. -```java -// This will generate a debug statement within developer console +```apex// This will generate a debug statement within developer console System.debug('Debug statement using native Apex'); // This will create a new `Log__c` record with multiple related `LogEntry__c` records @@ -183,25 +183,9 @@ This results in a `Log__c` record with related `LogEntry__c` records. --- -### All Together: Apex, Lightning Components & Flow in One Log +### Logger for OmniStudio: Quick Start -After incorporating Logger into your Flows & Apex code (including controllers, trigger framework, etc.), you'll have a unified transaction log of all your declarative & custom code automations. - -```java -Case currentCase = [SELECT Id, CaseNumber, Type, Status, IsClosed FROM Case LIMIT 1]; - -Logger.info('First, log the case through Apex', currentCase); - -Logger.debug('Now, we update the case in Apex to cause our record-triggered Flow to run'); -update currentCase; - -Logger.info('Last, save our log'); -Logger.saveLog(); -``` - -This generates 1 consolidated `Log__c`, containing `LogEntry__c` records from both Apex and Flow - -![Flow Log Results](./images/combined-apex-flow-log.png) +For OmniStudio builders, the included Apex class `CallableLogger` provides access to Nebula Logger's core features, directly in omniscripts and omni integration procedures. Simply use the `CallableLogger` class as a remote action within OmniStudio, and provide any inputs needed for the logging action. For more details (including what actions are available, and their required inputs), see [the section on the `CallableLogger` Apex class](https://github.com/jongpie/NebulaLogger/wiki/Dynamically-Call-Logger). For more details on logging in OmniStudio, [see the OmniStudio wiki page](https://github.com/jongpie/NebulaLogger/wiki/Nebula-Logger-for-OmniStudio) --- @@ -232,8 +216,7 @@ This example batchable class shows how you can leverage this feature to relate a > :information_source: If you deploy this example class to your org,you can run it using `Database.executeBatch(new BatchableLoggerExample());` -```java -public with sharing class BatchableLoggerExample implements Database.Batchable, Database.Stateful { +```apexpublic with sharing class BatchableLoggerExample implements Database.Batchable, Database.Stateful { private String originalTransactionId; public Database.QueryLocator start(Database.BatchableContext batchableContext) { @@ -276,8 +259,7 @@ Queueable jobs can also leverage the parent transaction ID to relate logs togeth > :information_source: If you deploy this example class to your org,you can run it using `System.enqueueJob(new QueueableLoggerExample(3));` -```java -public with sharing class QueueableLoggerExample implements Queueable { +```apexpublic with sharing class QueueableLoggerExample implements Queueable { private Integer numberOfJobsToChain; private String parentLogTransactionId; @@ -332,8 +314,7 @@ To see the full list of overloads, check out the `Logger` class [documentation]( Each of the logging methods in `Logger` returns an instance of the class `LogEntryEventBuilder`. This class provides several additional methods together to further customize each log entry - each of the builder methods can be chained together. In this example Apex, 3 log entries are created using different approaches for calling `Logger` - all 3 approaches result in identical log entries. -```java -// Get the current user so we can log it (just as an example of logging an SObject) +```apex// Get the current user so we can log it (just as an example of logging an SObject) User currentUser = [SELECT Id, Name, Username, Email FROM User WHERE Id = :UserInfo.getUserId()]; // Using static Logger method overloads @@ -356,8 +337,7 @@ The class `LogMessage` provides the ability to generate string messages on deman 1. Improved CPU usage by skipping unnecessary calls to `String.format()` - ```java - // Without using LogMessage, String.format() is always called, even if the FINE logging level is not enabled for a user + ```apex // Without using LogMessage, String.format() is always called, even if the FINE logging level is not enabled for a user String formattedString = String.format('my example with input: {0}', List{'myString'}); Logger.fine(formattedString); @@ -367,8 +347,7 @@ The class `LogMessage` provides the ability to generate string messages on deman ``` 2. Easily build complex strings - ```java - // There are several constructors for LogMessage to support different numbers of parameters for the formatted string + ```apex // There are several constructors for LogMessage to support different numbers of parameters for the formatted string String unformattedMessage = 'my string with 3 inputs: {0} and then {1} and finally {2}'; String formattedMessage = new LogMessage(unformattedMessage, 'something', 'something else', 'one more').getMessage(); String expectedMessage = 'my string with 3 inputs: something and then something else and finally one more'; @@ -377,6 +356,417 @@ The class `LogMessage` provides the ability to generate string messages on deman For more details, check out the `LogMessage` class [documentation](https://jongpie.github.io/NebulaLogger/apex/Logger-Engine/LogMessage). +### ISVs & Package Developers: Dynamically Call Nebula Logger in Your Packages with `CallableLogger` + +As of `v4.14.10`, Nebula Logger includes the Apex class `CallableLogger`, which implements [Apex's `Callable` interface](https://developer.salesforce.com/docs/atlas.en-us.apexref.meta/apexref/apex_interface_System_Callable.htm). + +- The `Callable` interface only has 1 method: `Object call(String action, Map args)`. It leverages string values and generic `Object` values as a mechanism to provide loose coupling on Apex classes that may or may not exist in a Salesforce org. +- This can be used by ISVs & package developers to optionally leverage Nebula Logger for logging, when it's available in a customer's org. And when it's not available, your package can still be installed, and still be used. + +Using the provided `CallableLogger` class, a subset of Nebula Logger's features can be called dynamically in Apex. For example, this sample Apex class + +```apex// Check for both the managed package (Nebula namespace) and the unlocked package to see if either is available +Type nebulaLoggerType = Type.forName('Nebula', 'CallableLogger') ?? Type.forName('CallableLogger'); +Callable nebulaLoggerInstance = (Callable) nebulaLoggerType?.newInstance(); +if (nebulaLoggerInstance == null) { + // If it's null, then neither of Nebula Logger's packages is available in the org 🥲 + return; +} + +// Example: Add a basic "hello, world!" INFO extry +Map newEntryInput = new Map{ + 'loggingLevel' => System.LoggingLevel.INFO, + 'message' => 'hello, world!' +}; +nebulaLoggerInstance.call('newEntry', newEntryInput); + +// Example: Add an ERROR extry with an Apex exception +Exception someException = new DmlException('oops'); +Map newEntryInput = new Map{ + 'exception' => someException, + 'loggingLevel' => LoggingLevel.ERROR, + 'message' => 'An unexpected exception was thrown' +}; +nebulaLoggerInstance.call('newEntry', newEntryInput); + +// Example: Save any pending log entries +nebulaLoggerInstance.call('saveLog', null); +``` + +#### Available Actions + +There are currently 8 actions supported by `CallablerLogger` - each action (discussed below) essentially corresponds to a similar method in the core `Logger` Apex class. + +All actions return an instance of `Map` as the output. The `call()` method always returns an `Object`, so the returned value will have to be cast to `Map` if you wish to inspect the returned information. + +- When the `call()` method finishes successfully, the map contains the key `isSuccess`, with a value of `true` +- When the `call()` method fails due to some catchable exception, the map contains the key `isSuccess`, with a value of `false`. It also includes 3 `String` values for the exception that was thrown: + 1. `exceptionMessage` - the value of `thrownException.getMessage()` + 1. `exceptionStackTrace` - the value of `thrownException.getStackTraceString()` + 1. `exceptionType` - the value of `thrownException.getTypeName()` + +##### Example + +```apex// An example of verifying that the action call was successful +Callable nebulaLoggerInstance = (Callable) System.Type.forName('CallableLogger')?.newInstance(); +if (nebulaLoggerInstance == null) { + // If it's null, then Nebula Logger isn't available in the org 🥲 + return; +} +Map output = (Map) nebulaLoggerInstance.call('saveLog', null); +Boolean savedSuccessfully = ouput.get('isSuccess') == true; +System.debug('Log entries were successfully saved in Nebula Logger: ' + savedSuccessfully); +``` + +#### `newEntry` Action + +This action is used to add new log entries in Nebula Logger. It is the equivalent of using these methods (and their overloads) available in `Logger`: + +- `error()` overloads, like `Logger.error('hello, world');` +- `warn()` overloads, like `Logger.warn('hello, world');` +- `info()` overloads, like `Logger.info('hello, world');` +- `debug()` overloads, like `Logger.debug('hello, world');` +- `fine()` overloads, like `Logger.fine('hello, world');` +- `finer()` overloads, like `Logger.fine('rhello, world');` +- `finest()` overloads, like `Logger.finest('hello, world');` +- `newEntry()` overloads, like `Logger.newEntry(LoggingLevel.INFO, 'hello, world');` + +##### Input Values + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Input Map KeyRequiredExpected DatatypeNotes
loggingLevelRequiredString or System.LoggingLevel
messageRequiredString
exceptionOptionalSystem.ExceptionWhen provided, Nebula Logger automatically stores details about the provided exception the log entry to the specified SObject record ID.

Cannot be used at the same time as record, recordList, or recordMap
recordIdOptionalIdWhen provided, Nebula Logger automatically ties the log entry to the specified SObject record ID.

Cannot be used at the same time as record, recordList, or recordMap
recordOptionalSObjectWhen provided, Nebula Logger automatically ties the log entry to the specified SObject record, and stores a JSON copy of the provided SObject record.

Cannot be used at the same time as recordId, recordList, or recordMap
recordListOptionalListWhen provided, Nebula Logger automatically stores a JSON copy of the provided list of SObject records.

Cannot be used at the same time as recordId, record, or recordMap
recordMapOptionalMapWhen provided, Nebula Logger automatically stores a JSON copy of the provided map of SObject records.

Cannot be used at the same time as recordId, record, or recordList
saveLogOptionalBooleanWhen set to true, Nebula Logger automatically saves any pending log entries. By default, log entries are not automatically saved.
tagsOptionalListWhen provided, Nebula Logger stores the provided strings as tags associated with the log entry.
+ +##### Output Values + +No additional output values are returned for this action. + +##### Example Usage + +```apexAccount someAccount = [SELECT Id, Name ] +Exception someException = new DmlException('oops'); + +// Add a new entry with an account & an exception as supporting context/data +Map newEntryInput = new Map{ + 'exception' => someException, + 'loggingLevel' => LoggingLevel.ERROR, + 'message' => 'An unexpected exception was thrown' + 'record' => someAccount +}; +Callable nebulaLoggerInstance = (Callable) System.Type.forName('CallableLogger')?.newInstance(); +nebulaLoggerInstance?.call('newEntry', newEntryInput); +``` + +#### `saveLog` Action + +This action is used to save any pending new log entries in Nebula Logger. It is the equivalent to using `Logger.saveLog()` + +##### Input Values + + + + + + + + + + + + + + + + + + +
Input Map KeyRequiredExpected DatatypeNotes
saveMethodNameOptionalStringWhen provided, the specified save method will be used by Nebula Logger to save any pending log entries. By default, log entries are saved using the save method configured in LoggerSettings__c.DefaultSaveMethod__c.
+ +##### Output Values + +No additional output values are returned for this action. + +##### Example Usage + +```apexSystem.Callable nebulaLoggerInstance = (System.Callable) System.Type.forName('CallableLogger')?.newInstance(); + +// Save using the default save method +nebulaLoggerInstance?.call('saveLog', null); + +// Save using a specific save method +nebulaLoggerInstance?.call('saveLog', new Map{ 'saveMethodName' => 'SYNCHRONOUS_DML' }); +``` + +#### `getTransactionId` Action + +This action is used to return Nebula Logger's unique identifier for the current transaction. It is the equivalent to using `Logger.getTransactionId()` + +##### Input Values + +No input values are used for this action. + +##### Output Values + + + + + + + + + + + + + + +
Output Map KeyDatatype
`transactionId`String
+ +##### Example Usage + +```apexCallable nebulaLoggerInstance = (Callable) Type.forName('CallableLogger')?.newInstance(); + +Map output = (Map) nebulaLoggerInstance?.call('getTransactionId', null); +System.debug('Current transaction ID: ' + (String) output.get('transactionId')); +``` + +#### `getParentLogTransactionId` Action + +This action is used to return Nebula Logger's unique identifier for the parent log of the current transaction (if one has been set). It is the equivalent to using `Logger.getParentLogTransactionId()` + +##### Input Values + +No input values are used for this action. + +##### Output Values + + + + + + + + + + + + + + +
Output Map KeyDatatype
parentLogTransactionIdString
+ +##### Example Usage + +```apexCallable nebulaLoggerInstance = (Callable) Type.forName('CallableLogger')?.newInstance(); + +Map output = (Map) nebulaLoggerInstance?.call('getParentLogTransactionId', null); +System.debug('Current parent log transaction ID: ' + (String) output.get('parentLogTransactionId')); +``` + +#### `setParentLogTransactionId` Action + +This action is used to set Nebula Logger's the unique identifier for the parent log of the current transaction. It is the equivalent to using `Logger.setParentLogTransactionId(String)` + +##### Input Values + + + + + + + + + + + + + + + + + +
Input Map KeyRequiredExpected DatatypeNotes
parentLogTransactionIdRequiredString
+ +##### Output Values + +No output values are used for this action. + +##### Example Usage + +```apexCallable nebulaLoggerInstance = (Callable) Type.forName('CallableLogger')?.newInstance(); + +Map output = (Map) nebulaLoggerInstance?.call('getParentLogTransactionId', null); +System.debug('Current parent log transaction ID: ' + (String) output.get('parentLogTransactionId')); +``` + +#### `getScenario` Action + +This action is used to return Nebula Logger's current scenario for scenario-based-logging (if one has been set). It is the equivalent to using `Logger.getScenario()`. + +##### Input Values + +No input values are used for this action. + +##### Output Values + + + + + + + + + + + + + + +
Output Map KeyDatatype
scenarioString
+ +##### Example Usage + +```apexCallable nebulaLoggerInstance = (Callable) Type.forName('CallableLogger')?.newInstance(); + +Map output = (Map) nebulaLoggerInstance?.call('getScenario', null); +System.debug('Current scenario: ' + (String) output.get('scenario')); +``` + +#### `setScenario` Action + +This action is used to set Nebula Logger's current scenario for scenario-based-logging. It is the equivalent to using `Logger.setScenario(String)` + +##### Input Values + + + + + + + + + + + + + + + + + +
Input Map KeyRequiredExpected DatatypeNotes
scenarioRequiredString
+ +##### Output Values + +No additional output values are used for this action. + +##### Example Usage + +```apexCallable nebulaLoggerInstance = (Callable) Type.forName('CallableLogger')?.newInstance(); + +Map input = new Map{ 'scenario' => 'some scenario' }; +nebulaLoggerInstance?.call('setScenario', input); +``` + +#### `endScenario` Action + +This action is used to set Nebula Logger's current scenario for scenario-based-logging. It is the equivalent to using `Logger.endScenario(String)` + +##### Input Values + + + + + + + + + + + + + + + + + +
Input Map KeyRequiredExpected DatatypeNotes
scenarioRequiredString
+ +##### Output Values + +No additional output values are used for this action. + +##### Example Usage + +```apexCallable nebulaLoggerInstance = (Callable) Type.forName('CallableLogger')?.newInstance(); + +Map input = new Map{ 'scenario' => 'some scenario' }; +nebulaLoggerInstance?.call('endScenario', input); +``` + --- ## Features for Lightning Component Developers @@ -491,8 +881,7 @@ Nebula Logger supports dynamically tagging/labeling your `LogEntry__c` records v Apex developers can use 2 new methods in `LogEntryBuilder` to add tags - `LogEntryEventBuilder.addTag(String)` and `LogEntryEventBuilder.addTags(List)`. -```java -// Use addTag(String tagName) for adding 1 tag at a time +```apex// Use addTag(String tagName) for adding 1 tag at a time Logger.debug('my log message').addTag('some tag').addTag('another tag'); // Use addTags(List tagNames) for adding a list of tags in 1 method call @@ -609,8 +998,7 @@ The first step is to add a field to the platform event `LogEntryEvent__e` - In Apex, populate your field(s) by calling the instance method overloads `LogEntryEventBuilder.setField(Schema.SObjectField field, Object fieldValue)` or `LogEntryEventBuilder.setField(Map fieldToValue)` - ```apex - Logger.info('hello, world') + ```apex Logger.info('hello, world') // Set a single field .setField(LogEntryEvent__e.SomeCustomTextField__c, 'some text value') // Set multiple fields @@ -752,8 +1140,7 @@ If you want to add your own automation to the `Log__c` or `LogEntry__c` objects, - Apex plugins: your Apex class should extend the abstract class `LoggerSObjectHandlerPlugin`. For example: - ```java - public class ExamplePlugin extends LoggerSObjectHandlerPlugin { + ```apex public class ExamplePlugin extends LoggerSObjectHandlerPlugin { public override void execute( TriggerOperation triggerOperationType, List triggerNew, diff --git a/docs/apex/Configuration/LoggerParameter.md b/docs/apex/Configuration/LoggerParameter.md index 23b25fc4f..5d98b1bcc 100644 --- a/docs/apex/Configuration/LoggerParameter.md +++ b/docs/apex/Configuration/LoggerParameter.md @@ -74,6 +74,10 @@ Controls if Nebula Logger queries `Schema.Network` data. When set to `false`, an Controls if Nebula Logger queries `Schema.Network` data is queried synchronously & populated on `LogEntryEvent__e` records. When set to `false`, any `Schema.Network` fields on `LogEntryEvent__e` will not be populated - the data will instead be queried asynchronously and populated on any resulting `Log__c` records. Controlled by the custom metadata record `LoggerParameter.QueryNetworkDataSynchronously`, or `true` as the default +#### `QUERY_OMNI_PROCESS_DATA` → `Boolean` + +Controls if Nebula Logger queries `Schema.OmniProcess` data. When set to `false`, any `Schema.OmniProcess` fields on `LogEntry__c` will not be populated Controlled by the custom metadata record `LoggerParameter.QueryOmniProcessData`, or `true` as the default + #### `QUERY_ORGANIZATION_DATA` → `Boolean` Controls if Nebula Logger queries `Schema.Organization` data. When set to `false`, any `Schema.Organization` fields on `LogEntryEvent__e` and `Log__c` will not be populated Controlled by the custom metadata record `LoggerParameter.QueryOrganizationData`, or `true` as the default diff --git a/docs/apex/Log-Management/LogManagementDataSelector.md b/docs/apex/Log-Management/LogManagementDataSelector.md index eb40e0e23..3d642491c 100644 --- a/docs/apex/Log-Management/LogManagementDataSelector.md +++ b/docs/apex/Log-Management/LogManagementDataSelector.md @@ -377,6 +377,26 @@ List<Log\_\_c> The list of matching `Log__c` records +#### `getOmniProcessProxies(List omniProcessIds)` → `Map` + +Returns a list of matching `Schema.OmniProcess` records based on the provided list of OmniProcess IDs + +##### Parameters + +| Param | Description | +| ---------------- | --------------------------------------------- | +| `omniProcessIds` | The list of `Schema.OmniProcess` IDs to query | + +##### Return + +**Type** + +Map<Id, LoggerSObjectProxy.OmniProcess> + +**Description** + +The instance of `Map<Id, SObject>` containing any matching `Schema.OmniProcess` records + #### `getProfilesById(List profileIds)` → `List` Returns a `List<Schema.Profile>` of records with the specified profile IDs diff --git a/docs/apex/Logger-Engine/LoggerSObjectProxy.md b/docs/apex/Logger-Engine/LoggerSObjectProxy.md index 5cc6eb48f..58152caaf 100644 --- a/docs/apex/Logger-Engine/LoggerSObjectProxy.md +++ b/docs/apex/Logger-Engine/LoggerSObjectProxy.md @@ -91,3 +91,37 @@ Not all orgs have the SObject `Schema.Network` - it is only present in orgs that ###### `UrlPathPrefix` → `String` --- + +#### LoggerSObjectProxy.OmniProcess class + +Not all orgs have the SObject `Schema.OmniProcess` - it is only present in orgs that have enabled OmniStudio, so `Schema.OmniProcess` has to be referenced dynamically, including using hardcoded `String` values for field API names. The `LoggerSObjectProxy.OmniProcess` class acts as a substitute for a `Schema.OmniProcess` record so that the rest of the codebase can rely on strongly-typed references to fields (properties). + +--- + +##### Constructors + +###### `OmniProcess(SObject omniProcess)` + +--- + +##### Properties + +###### `CreatedBy` → `Schema.User` + +###### `CreatedById` → `Id` + +###### `CreatedDate` → `Datetime` + +###### `Id` → `String` + +###### `LastModifiedBy` → `Schema.User` + +###### `LastModifiedById` → `Id` + +###### `LastModifiedDate` → `Datetime` + +###### `OmniProcessType` → `String` + +###### `UniqueName` → `String` + +--- diff --git a/docs/apex/index.md b/docs/apex/index.md index 59c568ced..93b76c167 100644 --- a/docs/apex/index.md +++ b/docs/apex/index.md @@ -6,6 +6,10 @@ layout: default ## Logger Engine +### [CallableLogger](Logger-Engine/CallableLogger) + +A class that implements the standard interface `System.Callable`. This provides 2 benefits: 1. A loosely-coupled way to optionally integrate with Nebula Logger (useful for ISVs/package developers). 2. The ability to log in OmniStudio's OmniScripts & Integration Procedures. + ### [ComponentLogger](Logger-Engine/ComponentLogger) Controller class used by the lightning web component `logger` diff --git a/nebula-logger/core/main/configuration/classes/LoggerParameter.cls b/nebula-logger/core/main/configuration/classes/LoggerParameter.cls index 71ba009da..9eb9c5c49 100644 --- a/nebula-logger/core/main/configuration/classes/LoggerParameter.cls +++ b/nebula-logger/core/main/configuration/classes/LoggerParameter.cls @@ -227,6 +227,21 @@ public class LoggerParameter { private set; } + /** + * @description Controls if Nebula Logger queries `Schema.OmniProcess` data. + * When set to `false`, any `Schema.OmniProcess` fields on `LogEntry__c` will not be populated + * Controlled by the custom metadata record `LoggerParameter.QueryOmniProcessData`, or `true` as the default + */ + public static final Boolean QUERY_OMNI_PROCESS_DATA { + get { + if (QUERY_OMNI_PROCESS_DATA == null) { + QUERY_OMNI_PROCESS_DATA = getBoolean('QueryOmniProcessData', true); + } + return QUERY_OMNI_PROCESS_DATA; + } + private set; + } + /** * @description Controls if Nebula Logger queries `Schema.Network` data. * When set to `false`, any `Schema.Network` fields on `LogEntryEvent__e` and `Log__c` will not be populated diff --git a/nebula-logger/core/main/configuration/customMetadata/LoggerParameter.QueryOmniProcessData.md-meta.xml b/nebula-logger/core/main/configuration/customMetadata/LoggerParameter.QueryOmniProcessData.md-meta.xml new file mode 100644 index 000000000..79630f8d2 --- /dev/null +++ b/nebula-logger/core/main/configuration/customMetadata/LoggerParameter.QueryOmniProcessData.md-meta.xml @@ -0,0 +1,21 @@ + + + + false + + Comments__c + + + + Description__c + Note: this parameter only applies to orgs that are using OmniStudio. + + When set to 'true' (default), the OmniProcess object will be queried to track additional details about the OmniScript or OmniIntegrationProcedure that generated a log entry - the queried data is stored in fields on LogEntry__c. + +When set to 'false', the OmniProcess object will not be queried, and the related fields will be null. + + + Value__c + true + + diff --git a/nebula-logger/core/main/log-management/classes/LogEntryEventHandler.cls b/nebula-logger/core/main/log-management/classes/LogEntryEventHandler.cls index b5f9bd4ce..676b8c962 100644 --- a/nebula-logger/core/main/log-management/classes/LogEntryEventHandler.cls +++ b/nebula-logger/core/main/log-management/classes/LogEntryEventHandler.cls @@ -336,6 +336,7 @@ public without sharing class LogEntryEventHandler extends LoggerSObjectHandler { OriginLocation__c = logEntryEvent.OriginLocation__c, OriginSourceActionName__c = logEntryEvent.OriginSourceActionName__c, OriginSourceApiName__c = logEntryEvent.OriginSourceApiName__c, + OriginSourceId__c = logEntryEvent.OriginSourceId__c, OriginSourceMetadataType__c = logEntryEvent.OriginSourceMetadataType__c, OriginType__c = logEntryEvent.OriginType__c, RecordCollectionSize__c = logEntryEvent.RecordCollectionSize__c, diff --git a/nebula-logger/core/main/log-management/classes/LogEntryHandler.cls b/nebula-logger/core/main/log-management/classes/LogEntryHandler.cls index 36b432d47..412bc1c68 100644 --- a/nebula-logger/core/main/log-management/classes/LogEntryHandler.cls +++ b/nebula-logger/core/main/log-management/classes/LogEntryHandler.cls @@ -30,6 +30,7 @@ public without sharing class LogEntryHandler extends LoggerSObjectHandler { this.setComponentFields(); this.setFlowDefinitionFields(); this.setFlowVersionFields(); + this.setOmniProcessFields(); this.setRecordNames(); this.setCheckboxFields(); } @@ -219,6 +220,49 @@ public without sharing class LogEntryHandler extends LoggerSObjectHandler { } } + private void setOmniProcessFields() { + List omniProcessIds = new List(); + List omniProcessLogEntries = new List(); + for (LogEntry__c logEntry : this.logEntries) { + if (logEntry.OriginType__c == 'OmniStudio' && String.isNotBlank(logEntry.OriginSourceId__c)) { + omniProcessIds.add(logEntry.OriginSourceId__c); + omniProcessLogEntries.add(logEntry); + } + } + + if (omniProcessIds.isEmpty()) { + return; + } + + Map omniProcessIdToProxy = LogManagementDataSelector.getInstance().getOmniProcessProxies(omniProcessIds); + for (LogEntry__c logEntry : omniProcessLogEntries) { + LoggerSObjectProxy.OmniProcess omniProcessProxy = omniProcessIdToProxy.get(logEntry.OriginSourceId__c); + + if (omniProcessProxy == null) { + continue; + } + + String originSourceMetadataType; + switch on omniProcessProxy.OmniProcessType { + when 'Integration Procedure' { + originSourceMetadataType = 'OmniIntegrationProcedure'; + } + when 'OmniScript' { + originSourceMetadataType = 'OmniScript'; + } + } + + logEntry.OriginSourceApiName__c = omniProcessProxy.UniqueName; + logEntry.OriginSourceCreatedById__c = omniProcessProxy.CreatedById; + logEntry.OriginSourceCreatedByUsername__c = omniProcessProxy.CreatedBy?.Username; + logEntry.OriginSourceCreatedDate__c = omniProcessProxy.CreatedDate; + logEntry.OriginSourceLastModifiedById__c = omniProcessProxy.LastModifiedById; + logEntry.OriginSourceLastModifiedByUsername__c = omniProcessProxy.LastModifiedBy?.Username; + logEntry.OriginSourceLastModifiedDate__c = omniProcessProxy.LastModifiedDate; + logEntry.OriginSourceMetadataType__c = originSourceMetadataType; + } + } + @SuppressWarnings('PMD.OperationWithLimitsInLoop') private void setRecordNames() { if (LoggerParameter.QUERY_RELATED_RECORD_DATA == false) { diff --git a/nebula-logger/core/main/log-management/classes/LogManagementDataSelector.cls b/nebula-logger/core/main/log-management/classes/LogManagementDataSelector.cls index 13fa730ee..f21da0bba 100644 --- a/nebula-logger/core/main/log-management/classes/LogManagementDataSelector.cls +++ b/nebula-logger/core/main/log-management/classes/LogManagementDataSelector.cls @@ -7,7 +7,7 @@ * @group Log Management * @description Selector class used for all queries that are specific to the log management layer */ -@SuppressWarnings('PMD.ApexCrudViolation, PMD.ExcessivePublicCount') +@SuppressWarnings('PMD.ApexCrudViolation, PMD.CyclomaticComplexity, PMD.ExcessivePublicCount') public without sharing virtual class LogManagementDataSelector { private static LogManagementDataSelector instance = new LogManagementDataSelector(); @@ -295,6 +295,29 @@ public without sharing virtual class LogManagementDataSelector { return [SELECT Id, OwnerId, UniqueId__c FROM LoggerScenario__c WHERE Id IN :logScenarioIds]; } + /** + * @description Returns a list of matching `Schema.OmniProcess` records based on the provided list of OmniProcess IDs + * @param omniProcessIds The list of `Schema.OmniProcess` IDs to query + * @return The instance of `Map` containing any matching `Schema.OmniProcess` records + */ + public virtual Map getOmniProcessProxies(List omniProcessIds) { + if (LoggerParameter.QUERY_OMNI_PROCESS_DATA == false) { + return new Map(); + } + + // OmniStudio may not be enabled in the org, and the Schema.OmniProcess object may not exist, + // so run everything dynamically + Map omniProcessIdToOmniProcessProxy = new Map(); + String query = + 'SELECT CreatedBy.Username, CreatedById, CreatedDate, Id, IsIntegrationProcedure, LastModifiedBy.Username, LastModifiedById, LastModifiedDate, OmniProcessType, UniqueName' + + ' FROM OmniProcess WHERE Id IN :omniProcessIds'; + for (SObject omniProcessRecord : System.Database.query(String.escapeSingleQuotes(query))) { + LoggerSObjectProxy.OmniProcess omniProcessProxy = new LoggerSObjectProxy.OmniProcess(omniProcessRecord); + omniProcessIdToOmniProcessProxy.put(omniProcessProxy.Id, omniProcessProxy); + } + return omniProcessIdToOmniProcessProxy; + } + /** * @description Returns a `List` of records with the specified profile IDs * @param profileIds The list of `ID` of the `Schema.Profile` records to query diff --git a/nebula-logger/core/main/log-management/objects/LogEntry__c/fields/OriginSourceMetadataType__c.field-meta.xml b/nebula-logger/core/main/log-management/objects/LogEntry__c/fields/OriginSourceMetadataType__c.field-meta.xml index 291dbefb5..f63d813e5 100644 --- a/nebula-logger/core/main/log-management/objects/LogEntry__c/fields/OriginSourceMetadataType__c.field-meta.xml +++ b/nebula-logger/core/main/log-management/objects/LogEntry__c/fields/OriginSourceMetadataType__c.field-meta.xml @@ -41,6 +41,16 @@ false + + OmniIntegrationProcedure + false + + + + OmniScript + false + + diff --git a/nebula-logger/core/main/log-management/objects/LogEntry__c/fields/OriginType__c.field-meta.xml b/nebula-logger/core/main/log-management/objects/LogEntry__c/fields/OriginType__c.field-meta.xml index 2395cabe6..94c54452f 100644 --- a/nebula-logger/core/main/log-management/objects/LogEntry__c/fields/OriginType__c.field-meta.xml +++ b/nebula-logger/core/main/log-management/objects/LogEntry__c/fields/OriginType__c.field-meta.xml @@ -29,6 +29,12 @@ false + + OmniStudio + #FFCC33 + false + + diff --git a/nebula-logger/core/main/log-management/objects/LogEntry__c/listViews/AllOmniStudioLogEntries.listView-meta.xml b/nebula-logger/core/main/log-management/objects/LogEntry__c/listViews/AllOmniStudioLogEntries.listView-meta.xml new file mode 100644 index 000000000..1a77843f6 --- /dev/null +++ b/nebula-logger/core/main/log-management/objects/LogEntry__c/listViews/AllOmniStudioLogEntries.listView-meta.xml @@ -0,0 +1,18 @@ + + + AllOmniStudioLogEntries + NAME + Log__c + LoggingLevel__c + LoggedByUsernameLink__c + Message__c + OriginSourceMetadataType__c + Timestamp__c + Everything + + OriginType__c + equals + OmniStudio + + + diff --git a/nebula-logger/core/main/logger-engine/classes/Logger.cls b/nebula-logger/core/main/logger-engine/classes/Logger.cls index 733e1a5c4..9fa54941a 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.9'; + private static final String CURRENT_VERSION_NUMBER = 'v4.14.10'; 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.'; @@ -23,7 +23,6 @@ global with sharing class Logger { private static final String REQUEST_ID = System.Request.getCurrent().getRequestId(); private static final Map SAVE_METHOD_NAME_TO_SAVE_METHOD = new Map(); private static final String TRANSACTION_ID = System.UUID.randomUUID().toString(); - private static final System.Quiddity TRANSACTION_QUIDDITY = loadTransactionQuiddity(); private static AsyncContext currentAsyncContext; private static String currentEntryScenario; @@ -35,6 +34,8 @@ global with sharing class Logger { @TestVisible private static Integer saveLogCallCount = 0; private static Boolean suspendSaving = false; + @TestVisible + private static System.Quiddity transactionQuiddity = loadTransactionQuiddity(); private static String transactionScenario; private static final List CLASSES_TO_IGNORE { @@ -187,7 +188,7 @@ global with sharing class Logger { * @return System.Quiddity - The value of System.Request.getCurrent().getQuiddity() */ global static System.Quiddity getCurrentQuiddity() { - return TRANSACTION_QUIDDITY; + return transactionQuiddity; } /** diff --git a/nebula-logger/core/main/logger-engine/classes/LoggerSObjectProxy.cls b/nebula-logger/core/main/logger-engine/classes/LoggerSObjectProxy.cls index 08b9d66de..991816d1b 100644 --- a/nebula-logger/core/main/logger-engine/classes/LoggerSObjectProxy.cls +++ b/nebula-logger/core/main/logger-engine/classes/LoggerSObjectProxy.cls @@ -90,4 +90,38 @@ public without sharing class LoggerSObjectProxy { } } } + + /** + * @description Not all orgs have the SObject `Schema.OmniProcess` - it is only present in orgs that have enabled OmniStudio, + * so `Schema.OmniProcess` has to be referenced dynamically, including using hardcoded `String` values for field API names. The + * `LoggerSObjectProxy.OmniProcess` class acts as a substitute for a `Schema.OmniProcess` record so that the rest of the codebase can rely on + * strongly-typed references to fields (properties). + */ + @SuppressWarnings('PMD.FieldNamingConventions, PMD.VariableNamingConventions') + public class OmniProcess { + public Id CreatedById; + public Schema.User CreatedBy; + public Datetime CreatedDate; + public String Id; + public Id LastModifiedById; + public Schema.User LastModifiedBy; + public Datetime LastModifiedDate; + public String OmniProcessType; + public String UniqueName; + + @SuppressWarnings('PMD.ApexDoc') + public OmniProcess(SObject omniProcess) { + if (omniProcess != null) { + this.CreatedById = (String) omniProcess.get('CreatedById'); + this.CreatedBy = (Schema.User) omniProcess.getSObject('CreatedBy'); + this.CreatedDate = (Datetime) omniProcess.get('CreatedDate'); + this.Id = (String) omniProcess.get('Id'); + this.LastModifiedById = (String) omniProcess.get('LastModifiedById'); + this.LastModifiedBy = (Schema.User) omniProcess.getSObject('LastModifiedBy'); + this.LastModifiedDate = (Datetime) omniProcess.get('LastModifiedDate'); + this.OmniProcessType = (String) omniProcess.get('OmniProcessType'); + this.UniqueName = (String) omniProcess.get('UniqueName'); + } + } + } } diff --git a/nebula-logger/core/main/logger-engine/classes/LoggerStackTrace.cls b/nebula-logger/core/main/logger-engine/classes/LoggerStackTrace.cls index 5d6d86e46..0ac631744 100644 --- a/nebula-logger/core/main/logger-engine/classes/LoggerStackTrace.cls +++ b/nebula-logger/core/main/logger-engine/classes/LoggerStackTrace.cls @@ -20,7 +20,7 @@ public without sharing class LoggerStackTrace { private static final System.Pattern INVALID_NAMESPACED_STACK_TRACE_PATTERN { get { if (INVALID_NAMESPACED_STACK_TRACE_PATTERN == null) { - INVALID_NAMESPACED_STACK_TRACE_PATTERN = System.Pattern.compile('^\\([0-9A-Za-z_]+\\)$'); + INVALID_NAMESPACED_STACK_TRACE_PATTERN = System.Pattern.compile('^\\([0-9A-Za-z_ ]+\\)$'); } return INVALID_NAMESPACED_STACK_TRACE_PATTERN; } diff --git a/nebula-logger/core/main/logger-engine/lwc/logger/__tests__/logger.test.js b/nebula-logger/core/main/logger-engine/lwc/logger/__tests__/logger.test.js index 35a275ec9..1fd88ca04 100644 --- a/nebula-logger/core/main/logger-engine/lwc/logger/__tests__/logger.test.js +++ b/nebula-logger/core/main/logger-engine/lwc/logger/__tests__/logger.test.js @@ -1,6 +1,5 @@ import { createElement } from 'lwc'; import FORM_FACTOR from '@salesforce/client/formFactor'; -import { LoggerStackTrace } from '../loggerStackTrace'; // Recommended approach import { createLogger } from 'c/logger'; // Legacy approach @@ -13,7 +12,7 @@ const flushPromises = async () => { await new Promise(process.nextTick); }; -jest.mock('lightning/logger', () => ({ loadScript: jest.fn() }), { +jest.mock('lightning/logger', () => ({ log: jest.fn() }), { virtual: true }); diff --git a/nebula-logger/core/main/logger-engine/lwc/logger/logEntryBuilder.js b/nebula-logger/core/main/logger-engine/lwc/logger/logEntryBuilder.js index b1f63fab2..1150bda94 100644 --- a/nebula-logger/core/main/logger-engine/lwc/logger/logEntryBuilder.js +++ b/nebula-logger/core/main/logger-engine/lwc/logger/logEntryBuilder.js @@ -6,7 +6,7 @@ import FORM_FACTOR from '@salesforce/client/formFactor'; import { log as lightningLog } from 'lightning/logger'; import { LoggerStackTrace } from './loggerStackTrace'; -const CURRENT_VERSION_NUMBER = 'v4.14.9'; +const CURRENT_VERSION_NUMBER = 'v4.14.10'; const LOGGING_LEVEL_EMOJIS = { ERROR: '⛔', diff --git a/nebula-logger/core/tests/configuration/classes/LoggerParameter_Tests.cls b/nebula-logger/core/tests/configuration/classes/LoggerParameter_Tests.cls index d415eb634..f9ce345e4 100644 --- a/nebula-logger/core/tests/configuration/classes/LoggerParameter_Tests.cls +++ b/nebula-logger/core/tests/configuration/classes/LoggerParameter_Tests.cls @@ -192,6 +192,17 @@ private class LoggerParameter_Tests { System.Assert.areEqual(mockValue, returnedValue); } + @IsTest + static void it_should_return_constant_value_for_query_omni_process_data() { + Boolean mockValue = false; + LoggerParameter__mdt mockParameter = new LoggerParameter__mdt(DeveloperName = 'QueryOmniProcessData', Value__c = System.JSON.serialize(mockValue)); + LoggerParameter.setMock(mockParameter); + + Boolean returnedValue = LoggerParameter.QUERY_OMNI_PROCESS_DATA; + + System.Assert.areEqual(mockValue, returnedValue); + } + @IsTest static void it_should_return_constant_value_for_query_network_data() { Boolean mockValue = false; diff --git a/nebula-logger/core/tests/log-management/classes/LogEntryEventHandler_Tests.cls b/nebula-logger/core/tests/log-management/classes/LogEntryEventHandler_Tests.cls index 9c0f5c52d..fa4e48474 100644 --- a/nebula-logger/core/tests/log-management/classes/LogEntryEventHandler_Tests.cls +++ b/nebula-logger/core/tests/log-management/classes/LogEntryEventHandler_Tests.cls @@ -1437,6 +1437,7 @@ private class LogEntryEventHandler_Tests { OriginLocation__c, OriginSourceActionName__c, OriginSourceApiName__c, + OriginSourceId__c, OriginSourceMetadataType__c, OriginType__c, RecordCollectionSize__c, @@ -1748,6 +1749,7 @@ private class LogEntryEventHandler_Tests { 'logEntry.OriginSourceActionName__c was not properly set' ); System.Assert.areEqual(logEntryEvent.OriginSourceApiName__c, logEntry.OriginSourceApiName__c, 'logEntry.OriginSourceApiName__c was not properly set'); + System.Assert.areEqual(logEntryEvent.OriginSourceId__c, logEntry.OriginSourceId__c, 'logEntry.OriginSourceId__c was not properly set'); System.Assert.areEqual( logEntryEvent.OriginSourceMetadataType__c, logEntry.OriginSourceMetadataType__c, diff --git a/nebula-logger/core/tests/log-management/classes/LogEntryHandler_Tests.cls b/nebula-logger/core/tests/log-management/classes/LogEntryHandler_Tests.cls index 4e2ec342c..de58d8e0c 100644 --- a/nebula-logger/core/tests/log-management/classes/LogEntryHandler_Tests.cls +++ b/nebula-logger/core/tests/log-management/classes/LogEntryHandler_Tests.cls @@ -6,6 +6,8 @@ @SuppressWarnings('PMD.ApexDoc, PMD.CyclomaticComplexity, PMD.ExcessiveParameterList, PMD.MethodNamingConventions, PMD.NcssMethodCount') @IsTest(IsParallel=true) private class LogEntryHandler_Tests { + private static final Boolean IS_OMNISTUDIO_ENABLED = System.Type.forName('Schema.OmniProcess') != null; + @TestSetup static void setupData() { LoggerSObjectHandler.shouldExecute(false); @@ -1258,6 +1260,119 @@ private class LogEntryHandler_Tests { System.Assert.isNull(logEntry.OriginSourceSnippet__c); } + @IsTest + static void it_should_set_origin_omni_process_details() { + // No need to fail the test if it's running in an org that does not have OmniStudio enabled + if (IS_OMNISTUDIO_ENABLED == false) { + return; + } + Schema.User currentUser = new Schema.User(Id = System.UserInfo.getUserId(), Username = System.UserInfo.getUsername()); + SObject mockOmniProcessRecord = (SObject) (System.Type.forName('Schema.OmniProcess').newInstance()); + LoggerSObjectProxy.OmniProcess mockOmniProcessProxy = new LoggerSObjectProxy.OmniProcess(mockOmniProcessRecord); + mockOmniProcessProxy.CreatedById = System.UserInfo.getUserId(); + mockOmniProcessProxy.CreatedBy = currentUser; + mockOmniProcessProxy.CreatedDate = System.now().addDays(-7); + mockOmniProcessProxy.Id = LoggerMockDataCreator.createId(mockOmniProcessRecord.getSObjectType()); + mockOmniProcessProxy.LastModifiedById = currentUser.Id; + mockOmniProcessProxy.LastModifiedBy = currentUser; + mockOmniProcessProxy.LastModifiedDate = System.now().addDays(-1); + mockOmniProcessProxy.OmniProcessType = 'Integration Procedure'; + mockOmniProcessProxy.UniqueName = 'Mock_OmniScript_English_1'; + MockLogManagementDataSelector mockSelector = new MockLogManagementDataSelector(); + mockSelector.setMockOmniProcess(mockOmniProcessProxy); + LogManagementDataSelector.setMock(mockSelector); + Log__c log = [SELECT Id FROM Log__c LIMIT 1]; + LogEntry__c logEntry = new LogEntry__c(Log__c = log.Id, OriginSourceId__c = mockOmniProcessProxy.Id, OriginType__c = 'OmniStudio'); + LoggerMockDataCreator.createDataBuilder(logEntry).populateRequiredFields().getRecord(); + + LoggerDataStore.getDatabase().insertRecord(logEntry); + + System.Assert.areEqual( + 2, + LoggerSObjectHandler.getExecutedHandlers().get(Schema.LogEntry__c.SObjectType).size(), + 'Handler class should have executed two times - once for BEFORE_INSERT and once for AFTER_INSERT' + ); + logEntry = [ + SELECT + Id, + OriginLocation__c, + OriginSourceActionName__c, + OriginSourceApiName__c, + OriginSourceApiVersion__c, + OriginSourceCreatedById__c, + OriginSourceCreatedByUsername__c, + OriginSourceCreatedDate__c, + OriginSourceId__c, + OriginSourceLastModifiedById__c, + OriginSourceLastModifiedByUsername__c, + OriginSourceLastModifiedDate__c, + OriginSourceMetadataType__c, + OriginType__c + FROM LogEntry__c + WHERE Id = :logEntry.Id + ]; + System.Assert.areEqual('OmniStudio', logEntry.OriginType__c); + System.Assert.isNull(logEntry.OriginLocation__c); + System.Assert.isNull(logEntry.OriginSourceActionName__c); + System.Assert.areEqual(mockOmniProcessProxy.UniqueName, logEntry.OriginSourceApiName__c); + System.Assert.isNull(logEntry.OriginSourceApiVersion__c); + System.Assert.areEqual(mockOmniProcessProxy.CreatedById, logEntry.OriginSourceCreatedById__c); + System.Assert.areEqual(mockOmniProcessProxy.CreatedBy.Username, logEntry.OriginSourceCreatedByUsername__c); + System.Assert.areEqual(mockOmniProcessProxy.CreatedDate, logEntry.OriginSourceCreatedDate__c); + System.Assert.areEqual(mockOmniProcessProxy.Id, logEntry.OriginSourceId__c); + System.Assert.areEqual(mockOmniProcessProxy.LastModifiedById, logEntry.OriginSourceLastModifiedById__c); + System.Assert.areEqual(mockOmniProcessProxy.LastModifiedBy.Username, logEntry.OriginSourceLastModifiedByUsername__c); + System.Assert.areEqual(mockOmniProcessProxy.LastModifiedDate, logEntry.OriginSourceLastModifiedDate__c); + System.Assert.areEqual('OmniIntegrationProcedure', logEntry.OriginSourceMetadataType__c); + } + + @IsTest + static void it_should_set_skip_setting_origin_omni_process_details_when_origin_source_id_is_null() { + Log__c log = [SELECT Id FROM Log__c LIMIT 1]; + LogEntry__c logEntry = new LogEntry__c(Log__c = log.Id, OriginSourceId__c = null, OriginType__c = 'OmniStudio'); + LoggerMockDataCreator.createDataBuilder(logEntry).populateRequiredFields().getRecord(); + + LoggerDataStore.getDatabase().insertRecord(logEntry); + + System.Assert.areEqual( + 2, + LoggerSObjectHandler.getExecutedHandlers().get(Schema.LogEntry__c.SObjectType).size(), + 'Handler class should have executed two times - once for BEFORE_INSERT and once for AFTER_INSERT' + ); + logEntry = [ + SELECT + Id, + OriginLocation__c, + OriginSourceActionName__c, + OriginSourceApiName__c, + OriginSourceApiVersion__c, + OriginSourceCreatedById__c, + OriginSourceCreatedByUsername__c, + OriginSourceCreatedDate__c, + OriginSourceId__c, + OriginSourceLastModifiedById__c, + OriginSourceLastModifiedByUsername__c, + OriginSourceLastModifiedDate__c, + OriginSourceMetadataType__c, + OriginType__c + FROM LogEntry__c + WHERE Id = :logEntry.Id + ]; + System.Assert.areEqual('OmniStudio', logEntry.OriginType__c); + System.Assert.isNull(logEntry.OriginLocation__c); + System.Assert.isNull(logEntry.OriginSourceActionName__c); + System.Assert.isNull(logEntry.OriginSourceApiName__c); + System.Assert.isNull(logEntry.OriginSourceApiVersion__c); + System.Assert.isNull(logEntry.OriginSourceCreatedById__c); + System.Assert.isNull(logEntry.OriginSourceCreatedByUsername__c); + System.Assert.isNull(logEntry.OriginSourceCreatedDate__c); + System.Assert.isNull(logEntry.OriginSourceId__c); + System.Assert.isNull(logEntry.OriginSourceLastModifiedById__c); + System.Assert.isNull(logEntry.OriginSourceLastModifiedByUsername__c); + System.Assert.isNull(logEntry.OriginSourceLastModifiedDate__c); + System.Assert.isNull(logEntry.OriginSourceMetadataType__c); + } + private static String getNamespacePrefix() { String className = LogEntryHandler_Tests.class.getName(); String namespacePrefix = className.contains('.') ? className.substringBefore('.') : ''; @@ -1265,6 +1380,18 @@ private class LogEntryHandler_Tests { return namespacePrefix; } + private class MockLogManagementDataSelector extends LogManagementDataSelector { + private LoggerSObjectProxy.OmniProcess mockOmniProcessProxy; + + public override Map getOmniProcessProxies(List omniProcessIds) { + return new Map{ mockOmniProcessProxy.Id => mockOmniProcessProxy }; + } + + public void setMockOmniProcess(LoggerSObjectProxy.OmniProcess omniProcessProxy) { + this.mockOmniProcessProxy = omniProcessProxy; + } + } + // Helper class for testing stack trace parsing & Schema.ApexClass querying for an inner class private class SomeInnerClass { public LoggerStackTrace getLoggerStackTrace() { diff --git a/nebula-logger/core/tests/log-management/classes/LogManagementDataSelector_Tests.cls b/nebula-logger/core/tests/log-management/classes/LogManagementDataSelector_Tests.cls index 4556953b8..b0252745a 100644 --- a/nebula-logger/core/tests/log-management/classes/LogManagementDataSelector_Tests.cls +++ b/nebula-logger/core/tests/log-management/classes/LogManagementDataSelector_Tests.cls @@ -6,6 +6,8 @@ @SuppressWarnings('PMD.ApexDoc, PMD.CyclomaticComplexity, PMD.MethodNamingConventions') @IsTest(IsParallel=false) private class LogManagementDataSelector_Tests { + private static final Boolean IS_OMNISTUDIO_ENABLED = System.Type.forName('Schema.OmniProcess') != null; + @IsTest static void it_dynamically_queries_all_records_for_specified_sobject_type_and_fields() { Schema.SObjectType targetSObjectType = Schema.Organization.SObjectType; @@ -357,6 +359,58 @@ private class LogManagementDataSelector_Tests { System.Assert.areEqual(logs.size(), returnedResults.size()); } + @IsTest + static void it_returns_logger_scenarios_for_specified_ids() { + LoggerSObjectHandler.shouldExecute(false); + List loggerScenarios = new List(); + for (Integer i = 0; i < 5; i++) { + LoggerScenario__c loggerScenario = (LoggerScenario__c) LoggerMockDataCreator.createDataBuilder(Schema.LoggerScenario__c.SObjectType) + .populateRequiredFields() + .getRecord(); + loggerScenario.Name = 'some fake scenario ' + i; + loggerScenario.UniqueId__c = 'some fake scenario ' + i; + loggerScenarios.add(loggerScenario); + } + insert loggerScenarios; + List loggerScenarioIds = new List(new Map(loggerScenarios).keySet()); + + List returnedResults = LogManagementDataSelector.getInstance().getLoggerScenariosById(loggerScenarioIds); + + System.Assert.areEqual(loggerScenarios.size(), returnedResults.size()); + } + + @IsTest + static void it_returns_omni_processes_for_specified_ids() { + // No need to fail the test if it's running in an org that does not have OmniStudio enabled + if (IS_OMNISTUDIO_ENABLED == false) { + return; + } + // Fun fact: in an anonymous Apex script, you can easily create an OmniProcess record... + // ...but when you create one in a test class, you get a gack error 🥲 + // Because of this platform limitation, this test is not great - it doesn't create records (because it can't), + // and thus, it can't validate that only the correct records are returned. But, it does validate that the the + // query string is valid & can successfully be executed. + // TODO revisit to see if there is any other way to create OmniProcess records to improve this test. + List omniProcessIds = new List(); + + List returnedResults = LogManagementDataSelector.getInstance().getLoggerScenariosById(omniProcessIds); + + System.Assert.isNotNull(returnedResults); + } + + @IsTest + static void it_does_not_query_omni_processes_when_disabled_via_logger_parameter() { + // The IDs used in the query don't particularly matter here - the main concern is checking that the query does not execute at all + List targetOmniProcessIds = new List{ System.UserInfo.getUserId() }; + Integer originalQueryCount = System.Limits.getQueries(); + LoggerParameter.setMock(new LoggerParameter__mdt(DeveloperName = 'QueryOmniProcessData', Value__c = String.valueOf(false))); + + Map returnedResults = LogManagementDataSelector.getInstance().getOmniProcessProxies(targetOmniProcessIds); + + System.Assert.areEqual(originalQueryCount, System.Limits.getQueries()); + System.Assert.areEqual(0, returnedResults.size()); + } + @IsTest static void it_returns_profiles_for_specified_profile_ids() { List expectedResults = [SELECT Id, Name FROM Profile LIMIT 10]; diff --git a/nebula-logger/core/tests/logger-engine/classes/LoggerSObjectProxy_Tests.cls b/nebula-logger/core/tests/logger-engine/classes/LoggerSObjectProxy_Tests.cls index 993ffe825..680bc019c 100644 --- a/nebula-logger/core/tests/logger-engine/classes/LoggerSObjectProxy_Tests.cls +++ b/nebula-logger/core/tests/logger-engine/classes/LoggerSObjectProxy_Tests.cls @@ -6,7 +6,8 @@ @SuppressWarnings('PMD.ApexDoc, PMD.MethodNamingConventions') @IsTest(IsParallel=true) private class LoggerSObjectProxy_Tests { - private static final Boolean IS_EXPERIENCE_CLOUD_ENABLED = System.Type.forName('NetworkMember') != null; + private static final Boolean IS_EXPERIENCE_CLOUD_ENABLED = System.Type.forName('Schema.Network') != null; + private static final Boolean IS_OMNISTUDIO_ENABLED = System.Type.forName('Schema.OmniProcess') != null; @IsTest static void it_converts_auth_session_record_to_proxy() { @@ -94,4 +95,46 @@ private class LoggerSObjectProxy_Tests { System.Assert.areEqual((String) mockNetworkRecord.get(nameFieldName), networkProxy.Name); System.Assert.areEqual((String) mockNetworkRecord.get(urlPathPrefixFieldName), networkProxy.UrlPathPrefix); } + + @IsTest + static void it_converts_omni_process_record_to_proxy() { + // No need to fail the test if it's running in an org that does not have OmniStudio enabled + if (IS_OMNISTUDIO_ENABLED == false) { + return; + } + Schema.User currentUser = new Schema.User(Id = System.UserInfo.getUserId(), Username = System.UserInfo.getUsername()); + // String createdByFieldName = 'CreatedBy'; + String createdByIdFieldName = 'CreatedById'; + String createdDateFieldName = 'CreatedDate'; + String idFieldName = 'Id'; + // String lastModifiedByFieldName = 'LastModifiedBy'; + String lastModifiedByIdFieldName = 'LastModifiedById'; + String lastModifiedDateFieldName = 'LastModifiedDate'; + String omniProcessTypeFieldName = 'OmniProcessType'; + String uniqueFieldName = 'UniqueName'; + // Some audit fields, like CreatedById, can't be set on an SObject, so start with a Map + Map mockOmniProcessUntyped = new Map{ 'attributes' => new Map{ 'type' => 'OmniProcess' } }; + mockOmniProcessUntyped.put(createdByIdFieldName, currentUser.Id); + // mockOmniProcessUntyped.put(createdByFieldName, currentUser); + mockOmniProcessUntyped.put(createdDateFieldName, System.now().addDays(-7)); + mockOmniProcessUntyped.put(lastModifiedByIdFieldName, currentUser.Id); + // mockOmniProcessUntyped.put(lastModifiedByFieldName, currentUser); + mockOmniProcessUntyped.put(lastModifiedDateFieldName, System.now().addDays(-1)); + mockOmniProcessUntyped.put(omniProcessTypeFieldName, 'Integration Procedure'); + mockOmniProcessUntyped.put(uniqueFieldName, 'Mock_OmniScript_English_1'); + SObject mockOmniProcessRecord = (SObject) System.JSON.deserialize(System.JSON.serialize(mockOmniProcessUntyped), SObject.class); + mockOmniProcessRecord.put(idFieldName, LoggerMockDataCreator.createId(mockOmniProcessRecord.getSObjectType())); + + LoggerSObjectProxy.OmniProcess omniProcessProxy = new LoggerSObjectProxy.OmniProcess(mockOmniProcessRecord); + + // System.Assert.areEqual((Schema.User) mockOmniProcessRecord.get(createdByFieldName), omniProcessProxy.CreatedBy); + System.Assert.areEqual((String) mockOmniProcessRecord.get(createdByIdFieldName), omniProcessProxy.CreatedById); + System.Assert.areEqual((Datetime) mockOmniProcessRecord.get(createdDateFieldName), omniProcessProxy.CreatedDate); + System.Assert.areEqual((Id) mockOmniProcessRecord.get(idFieldName), omniProcessProxy.Id); + // System.Assert.areEqual((Schema.User) mockOmniProcessRecord.get(lastModifiedByFieldName), omniProcessProxy.LastModifiedBy); + System.Assert.areEqual((String) mockOmniProcessRecord.get(lastModifiedByIdFieldName), omniProcessProxy.LastModifiedById); + System.Assert.areEqual((Datetime) mockOmniProcessRecord.get(lastModifiedDateFieldName), omniProcessProxy.LastModifiedDate); + System.Assert.areEqual((String) mockOmniProcessRecord.get(omniProcessTypeFieldName), omniProcessProxy.OmniProcessType); + System.Assert.areEqual((String) mockOmniProcessRecord.get(uniqueFieldName), omniProcessProxy.UniqueName); + } } diff --git a/nebula-logger/core/tests/logger-engine/classes/LoggerStackTrace_Tests.cls b/nebula-logger/core/tests/logger-engine/classes/LoggerStackTrace_Tests.cls index 5fe6c7147..54404ed98 100644 --- a/nebula-logger/core/tests/logger-engine/classes/LoggerStackTrace_Tests.cls +++ b/nebula-logger/core/tests/logger-engine/classes/LoggerStackTrace_Tests.cls @@ -225,6 +225,18 @@ private class LoggerStackTrace_Tests { System.Assert.isNull(stackTrace.Source); } + @IsTest + static void it_should_not_set_details_for_system_code_stack_trace_string() { + String namespacedPparenthesisStackTrace = '(System Code)'; + + LoggerStackTrace stackTrace = new LoggerStackTrace(namespacedPparenthesisStackTrace); + + System.Assert.areEqual(LoggerStackTrace.SourceLanguage.Apex, stackTrace.Language); + System.Assert.isNull(stackTrace.Location); + System.Assert.isNull(stackTrace.ParsedStackTraceString); + System.Assert.isNull(stackTrace.Source); + } + private class DebugStringExample { private System.Exception constructorStackTraceGenerator; private System.Exception methodStackTraceGenerator; diff --git a/nebula-logger/extra-tests/classes/name-shadowing/Schema/OmniProcess.cls b/nebula-logger/extra-tests/classes/name-shadowing/Schema/OmniProcess.cls new file mode 100644 index 000000000..26afa224d --- /dev/null +++ b/nebula-logger/extra-tests/classes/name-shadowing/Schema/OmniProcess.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 `Schema.OmniProcess` instead of just `OmniProcess` +@SuppressWarnings('PMD.ApexDoc, PMD.EmptyStatementBlock') +public without sharing class OmniProcess { +} diff --git a/nebula-logger/extra-tests/classes/name-shadowing/Schema/OmniProcess.cls-meta.xml b/nebula-logger/extra-tests/classes/name-shadowing/Schema/OmniProcess.cls-meta.xml new file mode 100644 index 000000000..651b17293 --- /dev/null +++ b/nebula-logger/extra-tests/classes/name-shadowing/Schema/OmniProcess.cls-meta.xml @@ -0,0 +1,5 @@ + + + 61.0 + Active + diff --git a/nebula-logger/managed-package/sfdx-project.json b/nebula-logger/managed-package/sfdx-project.json index 8af3023f8..14fb313ae 100644 --- a/nebula-logger/managed-package/sfdx-project.json +++ b/nebula-logger/managed-package/sfdx-project.json @@ -7,7 +7,7 @@ "package": "Nebula Logger - Managed Package", "path": "./nebula-logger/managed-package/core", "default": true, - "definitionFile": "./config/scratch-orgs/build-base-scratch-def.json", + "definitionFile": "./config/scratch-orgs/base-scratch-def.json", "postInstallScript": "LoggerInstallHandler", "scopeProfiles": true, "ancestorVersion": "HIGHEST", diff --git a/package.json b/package.json index afdbf0d95..4aaf72cdf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "nebula-logger", - "version": "4.14.9", + "version": "4.14.10", "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", @@ -38,7 +38,6 @@ "docs:fix:apex": "pwsh ./scripts/build/generate-apex-docs.ps1 && git add ./docs/apex", "docs:fix:lwc": "pwsh ./scripts/build/generate-lwc-docs.ps1 && git add ./docs/lightning-components", "docs:verify": "pwsh ./scripts/build/verify-docs-up-to-date.ps1", - "experience:deploy": "sf project deploy start --source-dir ./config/experience-cloud --wait 30", "husky:pre-commit": "lint-staged --config ./config/linters/lint-staged.config.js", "package:version:create:managed": "pwsh ./scripts/build/create-managed-package-beta-version.ps1", "package:version:create:unlocked": "sf package version create --json --package \"Nebula Logger - Core\" --skip-ancestor-check --code-coverage --installation-key-bypass --wait 30", diff --git a/sfdx-project.json b/sfdx-project.json index b1a926582..bc49d86b1 100644 --- a/sfdx-project.json +++ b/sfdx-project.json @@ -7,11 +7,11 @@ { "package": "Nebula Logger - Core", "path": "./nebula-logger/core", - "definitionFile": "./config/scratch-orgs/build-base-scratch-def.json", + "definitionFile": "./config/scratch-orgs/base-scratch-def.json", "scopeProfiles": true, - "versionNumber": "4.14.9.NEXT", - "versionName": "Bugfix: Apex Code Snippets Auto-Truncated", - "versionDescription": "Updated LogEntryHandler to automatically truncate the code snippets stored in OriginSourceSnippet__c and ExceptionSourceSnippet__c", + "versionNumber": "4.14.10.NEXT", + "versionName": "New CallableLogger Apex class", + "versionDescription": "Added a new CallableLogger class that provides support for both OmniStudio logging, as well as the ability to dynamically call Nebula Logger in Apex when it's available", "releaseNotesUrl": "https://github.com/jongpie/NebulaLogger/releases", "unpackagedMetadata": { "path": "./nebula-logger/extra-tests"