diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 000000000..019e4ba35 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +github: [jongpie] diff --git a/README.md b/README.md index a03f23c67..91a0dcf51 100644 --- a/README.md +++ b/README.md @@ -5,15 +5,15 @@ The most robust logger for Salesforce. Works with Apex, Lightning Components, Flow, Process Builder & Integrations. Designed for Salesforce admins, developers & architects. -## Unlocked Package - v4.13.13 +## Unlocked Package - v4.13.14 -[![Install Unlocked Package in a Sandbox](./images/btn-install-unlocked-package-sandbox.png)](https://test.salesforce.com/packaging/installPackage.apexp?p0=04t5Y0000015oDsQAI) -[![Install Unlocked Package in Production](./images/btn-install-unlocked-package-production.png)](https://login.salesforce.com/packaging/installPackage.apexp?p0=04t5Y0000015oDsQAI) +[![Install Unlocked Package in a Sandbox](./images/btn-install-unlocked-package-sandbox.png)](https://test.salesforce.com/packaging/installPackage.apexp?p0=04t5Y0000015oE2QAI) +[![Install Unlocked Package in Production](./images/btn-install-unlocked-package-production.png)](https://login.salesforce.com/packaging/installPackage.apexp?p0=04t5Y0000015oE2QAI) [![View Documentation](./images/btn-view-documentation.png)](https://jongpie.github.io/NebulaLogger/) -`sf package install --wait 20 --security-type AdminsOnly --package 04t5Y0000015oDsQAI` +`sf package install --wait 20 --security-type AdminsOnly --package 04t5Y0000015oE2QAI` -`sfdx force:package:install --wait 20 --securitytype AdminsOnly --package 04t5Y0000015oDsQAI` +`sfdx force:package:install --wait 20 --securitytype AdminsOnly --package 04t5Y0000015oE2QAI` --- @@ -31,7 +31,7 @@ The most robust logger for Salesforce. Works with Apex, Lightning Components, Fl ## Features -1. Easily add log entries via Apex, Lightning Components (lwc & aura), Flow & Process Builder to generate 1 consolidated, unified log +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 @@ -86,11 +86,6 @@ Nebula Logger is available as both an unlocked package and a managed package. Th System.debug() is automatically called - the output can be configured with LoggerSettings__c.SystemLogMessageFormat__c to use any field on LogEntryEvent__e Requires adding your own calls for System.debug() due to Salesforce limitations with managed packages - - Apex Stack Traces - Automatically stored in LogEntry__c.StackTrace__c when calling methods like Logger.debug('my message'); - Requires calling parseStackTrace() due to Salesforce limitations with managed packages. For example:
Logger.debug('my message').parseStackTrace(new DmlException().getStackTrace()); - Logger Plugin Framework Leverage Apex or Flow to build your own "plugins" for Logger - easily add your own automation to the any of the included objects: LogEntryEvent__e, Log__c, LogEntry__c, LogEntryTag__c and LoggerTag__c. The logger system will then automatically run your plugins for each trigger event (BEFORE_INSERT, BEFORE_UPDATE, AFTER_INSERT, AFTER_UPDATE, and so on). @@ -142,20 +137,21 @@ This results in 1 `Log__c` record with several related `LogEntry__c` records. ### Logger for Lightning Components: Quick Start -For lightning component developers, the `logger` lwc provides very similar functionality that is offered in Apex. Simply embed the `logger` lwc in your component, and call the desired logging methods within your code. +For lightning component developers, the `logger` LWC provides very similar functionality that is offered in Apex. Simply incorporate the `logger` LWC into your component, and call the desired logging methods within your code. ```javascript -// For lwc, retrieve logger from your component's template -const logger = this.template.querySelector('c-logger'); +// For LWC, import logger's createLogger() function into your component +import { createLogger } from 'c/logger'; -logger.error('Hello, world!').addTag('some important tag'); -logger.warn('Hello, world!'); -logger.info('Hello, world!'); -logger.debug('Hello, world!'); -logger.fine('Hello, world!'); -logger.finer('Hello, world!'); -logger.finest('Hello, world!'); -logger.saveLog(); +export default class LoggerLWCDemo extends LightningElement { + logger; + + async connectedCallback() { + this.logger = await createLogger(); + this.logger.info('Hello, world'); + this.logger.saveLog(); + } +} ``` ```javascript @@ -388,7 +384,7 @@ For more details, check out the `LogMessage` class [documentation](https://jongp ## Features for Lightning Component Developers -For lightning component developers, the included `logger` lwc can be used in other lwc & aura components for frontend logging. Similar to `Logger` and `LogEntryBuilder` Apex classes, the lwc has both `logger` and `logEntryBuilder` classes. This provides a fluent API for javascript developers so they can chain the method calls. +For lightning component developers, the included `logger` LWC can be used in other LWCs & aura components for frontend logging. Similar to `Logger` and `LogEntryBuilder` Apex classes, the LWC has both `logger` and `logEntryBuilder` classes. This provides a fluent API for JavaScript developers so they can chain the method calls. Once you've incorporated `logger` into your lightning components, you can see your `LogEntry__c` records using the included list view "All Component Log Entries'. @@ -400,34 +396,48 @@ Each `LogEntry__c` record automatically stores the component's type ('Aura' or ' #### Example LWC Usage -To use the logger component, it has to be added to your lwc's markup: +For lightning component developers, the `logger` LWC provides very similar functionality that is offered in Apex. Simply import the `logger` LWC in your component, and call the desired logging methods within your code. -```html - -``` +export default class LoggerLWCDemo extends LightningElement { + logger; -Once you've added logger to your markup, you can call it in your lwc's controller: + async connectedCallback() { + // Call createLogger() once per component + this.logger = await createLogger(); -```javascript -import { LightningElement } from 'lwc'; + this.logger.setScenario('some scenario'); + this.logger.finer('initialized demo LWC'); + } -export default class LoggerDemo extends LightningElement { logSomeStuff() { - const logger = this.template.querySelector('c-logger'); - - logger.error('Hello, world!').addTag('some important tag'); - logger.warn('Hello, world!'); - logger.info('Hello, world!'); - logger.debug('Hello, world!'); - logger.fine('Hello, world!'); - logger.finer('Hello, world!'); - logger.finest('Hello, world!'); + this.logger.error('Add log entry using Nebula Logger with logging level == ERROR').addTag('some important tag'); + this.logger.warn('Add log entry using Nebula Logger with logging level == WARN'); + this.logger.info('Add log entry using Nebula Logger with logging level == INFO'); + this.logger.debug('Add log entry using Nebula Logger with logging level == DEBUG'); + this.logger.fine('Add log entry using Nebula Logger with logging level == FINE'); + this.logger.finer('Add log entry using Nebula Logger with logging level == FINER'); + this.logger.finest('Add log entry using Nebula Logger with logging level == FINEST'); + + this.logger.saveLog(); + } - logger.saveLog(); + doSomething(event) { + this.logger.finest('Starting doSomething() with event: ' + JSON.stringify(event)); + try { + this.logger.debug('TODO - finishing implementation of doSomething()').addTag('another tag'); + // TODO add the function's implementation below + } catch (thrownError) { + this.logger + .error('An unexpected error log entry using Nebula Logger with logging level == ERROR') + .setError(thrownError) + .addTag('some important tag'); + } finally { + this.logger.saveLog(); + } } } ``` @@ -584,6 +594,53 @@ Once you've implementing log entry tagging within Apex or Flow, you can choose h --- +## Adding Custom Fields to Nebula Logger's Data Model + +As of `v4.13.14`, Nebula Logger supports defining, setting, and mapping custom fields within Nebula Logger's data model. This is helpful in orgs that want to extend Nebula Logger's included data model by creating their own org/project-specific fields. + +This feature requires that you populate your custom fields yourself, and is only available in Apex currently. The plan is to add in a future release the ability to also set custom fields via JavaScript & Flow. + +### Adding Custom Fields to the Platform Event `LogEntryEvent__e` + +The first step is to add a field to the platform event `LogEntryEvent__e` + +- Create a custom field on `LogEntryEvent__e`. Any data type supported by platform events can be used. + + - In this example, a custom text field called `SomeCustomField__c` has been added: + + ![Custom Field on LogEntryEvent__e](./images/custom-field-log-entry-event.png) + +- Populate your field(s) in Apex by calling the instance method overloads `LogEntryEventBuilder.setField(Schema.SObjectField field, Object fieldValue)` or `LogEntryEventBuilder.setField(Map fieldToValue)` + + ```apex + Logger.info('hello, world') + // Set a single field + .setField(LogEntryEvent__e.SomeCustomTextField__c, 'some text value') + // Set multiple fields + .setFields(new Map{ + LogEntryEvent__e.AnotherCustomTextField__c => 'another text value', + LogEntryEvent__e.SomeCustomDatetimeField__c => System.now() + }); + ``` + +### Adding Custom Fields to the Custom Objects `Log__c`, `LogEntry__c`, and `LoggerScenario__c` + +If you want to store the data in one of Nebula Logger's custom objects, you can follow the above steps, and also... + +- Create an equivalent custom field on one of Nebula Logger's custom objects - right now, only `Log__c`, `LogEntry__c`, and `LoggerScenario__c` are supported. + + - In this example, a custom text field _also_ called `SomeCustomField__c` has been added to `Log__c` object - this will be used to store the value of the field `LogEntryEvent__e.SomeCustomField__c`: + + ![Custom Field on LogEntryEvent__e](./images/custom-field-log.png) + +- Create a record in the new CMDT `LoggerFieldMapping__mdt` to map the `LogEntryEvent__e` custom field to the custom object's custom field, shown below. Nebula Logger will automatically populate the custom object's target field with the value of the source `LogEntryEvent__e` field. + + - In this example, a custom text field called `SomeCustomField__c` has been added to both `LogEntryEvent__e` and `Log__c`. + + ![Custom Field on LogEntryEvent__e](./images/custom-field-mapping.png) + +--- + ## Log Management ### Logger Console App diff --git a/docs/apex/Configuration/LoggerFieldMapper.md b/docs/apex/Configuration/LoggerFieldMapper.md new file mode 100644 index 000000000..2c5672f7b --- /dev/null +++ b/docs/apex/Configuration/LoggerFieldMapper.md @@ -0,0 +1,24 @@ +--- +layout: default +--- + +## LoggerFieldMapper class + +Maps fields values from custom fields on `LogEntryEvent__e` to equivalent fields on `Log__c`, `LogEntry__c`, and `LoggerScenario__c` + +--- + +### Methods + +#### `mapFieldValues(SObject sourceRecord, SObject targetRecord)` → `void` + +Copies field values from the `sourceRecord` to the `targetRecord`, based on rules configured in `LoggerFieldMapping_t` + +##### Parameters + +| Param | Description | +| -------------- | ------------------------------------------------------------------------------- | +| `sourceRecord` | The source `SObject` record containing the data to copy | +| `targetRecord` | The target `SObject` record that should have fields & field values appended | + +--- diff --git a/docs/apex/Logger-Engine/LogEntryEventBuilder.md b/docs/apex/Logger-Engine/LogEntryEventBuilder.md index e2444bb14..85e115430 100644 --- a/docs/apex/Logger-Engine/LogEntryEventBuilder.md +++ b/docs/apex/Logger-Engine/LogEntryEventBuilder.md @@ -400,6 +400,47 @@ LogEntryEventBuilder The same instance of `LogEntryEventBuilder`, useful for chaining methods +#### `setField(Schema.SObjectField field, Object fieldValue)` → `LogEntryEventBuilder` + +Sets a field values on the builder's `LogEntryEvent__e` record + +##### Parameters + +| Param | Description | +| ------------ | -------------------------------------------------------- | +| `field` | The `Schema.SObjectField` token of the field to populate | +| `fieldValue` | The `Object` value to populate in the provided field | + +##### Return + +**Type** + +LogEntryEventBuilder + +**Description** + +The same instance of `LogEntryEventBuilder`, useful for chaining methods + +#### `setField(Map fieldToValue)` → `LogEntryEventBuilder` + +Sets multiple field values on the builder's `LogEntryEvent__e` record + +##### Parameters + +| Param | Description | +| -------------- | ---------------------------------------------------------------------- | +| `fieldToValue` | An instance of `Map<Schema.SObjectField, Object>` containing the | + +##### Return + +**Type** + +LogEntryEventBuilder + +**Description** + +The same instance of `LogEntryEventBuilder`, useful for chaining methods + #### `setHttpRequestDetails(System.HttpRequest request)` → `LogEntryEventBuilder` Sets the log entry event's HTTP Request fields diff --git a/docs/apex/index.md b/docs/apex/index.md index e20382a88..59c568ced 100644 --- a/docs/apex/index.md +++ b/docs/apex/index.md @@ -150,6 +150,10 @@ Class used by the logging system for batch contextual details Class used to cache query results returned by the selector classes +### [LoggerFieldMapper](Configuration/LoggerFieldMapper) + +Maps fields values from custom fields on `LogEntryEvent__e` to equivalent fields on `Log__c`, `LogEntry__c`, and `LoggerScenario__c` + ### [LoggerParameter](Configuration/LoggerParameter) Provides a centralized way to load parameters for SObject handlers & plugins, and casts the parameters to common data types diff --git a/images/custom-field-log-entry-event.png b/images/custom-field-log-entry-event.png new file mode 100644 index 000000000..40e0ad2fe Binary files /dev/null and b/images/custom-field-log-entry-event.png differ diff --git a/images/custom-field-log.png b/images/custom-field-log.png new file mode 100644 index 000000000..724cde990 Binary files /dev/null and b/images/custom-field-log.png differ diff --git a/images/custom-field-mapping.png b/images/custom-field-mapping.png new file mode 100644 index 000000000..db2e64045 Binary files /dev/null and b/images/custom-field-mapping.png differ diff --git a/nebula-logger/core/main/configuration/classes/LoggerFieldMapper.cls b/nebula-logger/core/main/configuration/classes/LoggerFieldMapper.cls new file mode 100644 index 000000000..c0e79bf02 --- /dev/null +++ b/nebula-logger/core/main/configuration/classes/LoggerFieldMapper.cls @@ -0,0 +1,145 @@ +//------------------------------------------------------------------------------------------------// +// 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. // +//------------------------------------------------------------------------------------------------// + +/** + * @group Configuration + * @description Maps fields values from custom fields on `LogEntryEvent__e` + * to equivalent fields on `Log__c`, `LogEntry__c`, and `LoggerScenario__c` + */ +public without sharing class LoggerFieldMapper { + // TODO this is a goofy data structure, revisit how CMDT records are tracked/cached to see if there's a simpler & cleaner approach + @TestVisible + private static final Map>> SOURCE_SOBJECT_TYPE_TO_TARGET_FIELD_MAPPINGS = loadRecords(); + + /** + * @description Copies field values from the `sourceRecord` to the `targetRecord`, based on rules configured in `LoggerFieldMapping__mdt` + * @param sourceRecord The source `SObject` record containing the data to copy + * @param targetRecord The target `SObject` record that should have fields & field values appended + */ + @SuppressWarnings('PMD.CyclomaticComplexity') + public static void mapFieldValues(SObject sourceRecord, SObject targetRecord) { + if (sourceRecord == null || targetRecord == null) { + return; + } + + Map> targetSObjectTypeToFieldMappings = SOURCE_SOBJECT_TYPE_TO_TARGET_FIELD_MAPPINGS.get( + sourceRecord.getSObjectType() + ); + + if (targetSObjectTypeToFieldMappings == null) { + return; + } + + Schema.SObjectType targetSObjectType = targetRecord.getSObjectType(); + List fieldMappings = targetSObjectTypeToFieldMappings.get(targetSObjectType); + + if (fieldMappings == null) { + return; + } + + // TODO consider caching the SObjectField instances and/or DescribeFieldResult instances + Map targetFieldNameToField = targetRecord.getSObjectType().getDescribe().fields.getMap(); + + List incompatibleFieldMappings = new List(); + for (LoggerFieldMapping__mdt fieldMapping : fieldMappings) { + try { + Schema.SObjectField targetField = targetFieldNameToField.get(fieldMapping.TargetField__c); + Object sourceRecordFieldValue = sourceRecord.get(fieldMapping.SourceField__c); + // TODO consider caching the SObjectField instances and/or DescribeFieldResult instances + if (targetField.getDescribe().getSoapType() == Schema.SoapType.STRING) { + sourceRecordFieldValue = LoggerDataStore.truncateFieldValue(targetField, (String) sourceRecordFieldValue); + } + targetRecord.put(fieldMapping.TargetField__c, sourceRecordFieldValue); + } catch (System.Exception ex) { + incompatibleFieldMappings.add(fieldMapping); + } + } + if (LoggerParameter.ENABLE_SYSTEM_MESSAGES && incompatibleFieldMappings.isEmpty() == false) { + LogMessage logMessage = new LogMessage( + 'Unable to map {0} custom fields on {1} record. Target SObject record:\n\n{2}', + incompatibleFieldMappings.size(), + targetRecord.getSObjectType(), + System.JSON.serializePretty(targetRecord) + ); + Logger.finest(logMessage, incompatibleFieldMappings); + } + } + + @TestVisible + private static void setMock(LoggerFieldMapping__mdt fieldMapping) { + if (String.isBlank(fieldMapping.DeveloperName)) { + throw new System.IllegalArgumentException('DeveloperName is required on `LoggerFieldMapping__mdt: \n' + System.JSON.serializePretty(fieldMapping)); + } + + addFieldMapping(fieldMapping, SOURCE_SOBJECT_TYPE_TO_TARGET_FIELD_MAPPINGS); + } + + @SuppressWarnings('PMD.ApexCrudViolation') + private static Map>> loadRecords() { + // TODO decide if this query should be moved to LogManagementDataSelector + List fieldMappings = [ + SELECT + DeveloperName, + IsEnabled__c, + SourceSObjectType__r.QualifiedApiName, + SourceField__r.QualifiedApiName, + TargetSObjectType__r.QualifiedApiName, + TargetField__r.QualifiedApiName + FROM LoggerFieldMapping__mdt + WHERE IsEnabled__c = TRUE + ]; + Map>> sourceSObjectTypeToTargetFieldMappings = new Map>>(); + for (LoggerFieldMapping__mdt fieldMapping : fieldMappings) { + // This simplifies some of the CMDT fields by avoiding the use of parent fields directly, + // which makes downstream code & testing much easier + LoggerFieldMapping__mdt cleanedFieldMapping = new LoggerFieldMapping__mdt( + DeveloperName = fieldMapping.DeveloperName, + IsEnabled__c = fieldMapping.IsEnabled__c, + SourceSObjectType__c = fieldMapping.SourceSObjectType__r.QualifiedApiName, + SourceField__c = fieldMapping.SourceField__r.QualifiedApiName, + TargetSObjectType__c = fieldMapping.TargetSObjectType__r.QualifiedApiName, + TargetField__c = fieldMapping.TargetField__r.QualifiedApiName + ); + + addFieldMapping(cleanedFieldMapping, sourceSObjectTypeToTargetFieldMappings); + } + + return sourceSObjectTypeToTargetFieldMappings; + } + + private static void addFieldMapping( + LoggerFieldMapping__mdt fieldMapping, + Map>> sourceSObjectTypeToTargetFieldMappings + ) { + // TODO decide if it's worth the overhead to use SObjectType, or if String is enough. + // Since the data originates from Entity + Field Definition fields (that enforce valid values), + // using String might be ok. + Schema.SObjectType sourceSObjectType = getSObjectType(fieldMapping.SourceSObjectType__c); + Schema.SObjectType targetSObjectType = getSObjectType(fieldMapping.TargetSObjectType__c); + + if (sourceSObjectTypeToTargetFieldMappings.containsKey(sourceSObjectType) == false) { + Map> targetSObjectTypeToFieldMapping = new Map>{ + targetSObjectType => new List{ fieldMapping } + }; + sourceSObjectTypeToTargetFieldMappings.put(sourceSObjectType, targetSObjectTypeToFieldMapping); + } else { + Map> targetSObjectTypeToFieldMapping = sourceSObjectTypeToTargetFieldMappings.get( + sourceSObjectType + ); + if (targetSObjectTypeToFieldMapping.containsKey(targetSObjectType) == false) { + targetSObjectTypeToFieldMapping.put(targetSObjectType, new List()); + } + targetSObjectTypeToFieldMapping.get(targetSObjectType).add(fieldMapping); + } + } + + private static Schema.SObjectType getSObjectType(String sobjectApiName) { + return ((SObject) System.Type.forName(sobjectApiName).newInstance()).getSObjectType(); + } + + private static Schema.SObjectField getSObjectField(Schema.SObjectType sobjectType, String fieldApiName) { + return sobjectType.getDescribe().fields.getMap().get(fieldApiName); + } +} diff --git a/nebula-logger/core/main/configuration/classes/LoggerFieldMapper.cls-meta.xml b/nebula-logger/core/main/configuration/classes/LoggerFieldMapper.cls-meta.xml new file mode 100644 index 000000000..df13efa80 --- /dev/null +++ b/nebula-logger/core/main/configuration/classes/LoggerFieldMapper.cls-meta.xml @@ -0,0 +1,5 @@ + + + 60.0 + Active + diff --git a/nebula-logger/core/main/configuration/classes/LoggerParameter.cls b/nebula-logger/core/main/configuration/classes/LoggerParameter.cls index 96039da48..e0d0f13c5 100644 --- a/nebula-logger/core/main/configuration/classes/LoggerParameter.cls +++ b/nebula-logger/core/main/configuration/classes/LoggerParameter.cls @@ -781,6 +781,15 @@ public class LoggerParameter { return matchingParameters; } + @TestVisible + private static void setMock(LoggerParameter__mdt parameter) { + if (String.isBlank(parameter.DeveloperName)) { + throw new System.IllegalArgumentException('DeveloperName is required on `LoggerParameter__mdt: \n' + System.JSON.serializePretty(parameter)); + } + + DEVELOPER_NAME_TO_RECORD.put(parameter.DeveloperName, parameter); + } + private static Map loadRecords() { Map parameters = LoggerParameter__mdt.getAll().clone(); if (System.Test.isRunningTest()) { @@ -799,15 +808,6 @@ public class LoggerParameter { return parameters; } - @TestVisible - private static void setMock(LoggerParameter__mdt parameter) { - if (String.isBlank(parameter.DeveloperName)) { - throw new System.IllegalArgumentException('DeveloperName is required on `LoggerParameter__mdt: \n' + System.JSON.serializePretty(parameter)); - } - - DEVELOPER_NAME_TO_RECORD.put(parameter.DeveloperName, parameter); - } - private static Object castParameterValue(String parameterDeveloperName, System.Type dataType) { String parameterValue = loadParameterValue(parameterDeveloperName); if (parameterValue == null) { diff --git a/nebula-logger/core/main/configuration/classes/LoggerPlugin.cls b/nebula-logger/core/main/configuration/classes/LoggerPlugin.cls index dea4d508a..b9eed1ab2 100644 --- a/nebula-logger/core/main/configuration/classes/LoggerPlugin.cls +++ b/nebula-logger/core/main/configuration/classes/LoggerPlugin.cls @@ -98,6 +98,18 @@ public without sharing class LoggerPlugin { return (Triggerable) pluginInstance; } + @TestVisible + private static void setMock(LoggerPlugin__mdt pluginConfiguration) { + if (String.isBlank(pluginConfiguration.DeveloperName)) { + throw new System.IllegalArgumentException( + 'DeveloperName is required on mock LoggerPlugin__mdt: \n' + System.JSON.serializePretty(pluginConfiguration) + ); + } + if (pluginConfiguration.IsEnabled__c) { + DEVELOPER_NAME_TO_RECORD.put(pluginConfiguration.DeveloperName, pluginConfiguration); + } + } + private static Object newPluginInstance(String apexClassTypeName) { return System.Type.forName(apexClassTypeName)?.newInstance(); } @@ -115,18 +127,6 @@ public without sharing class LoggerPlugin { return pluginDeveloperNameToConfiguration; } - @TestVisible - private static void setMock(LoggerPlugin__mdt pluginConfiguration) { - if (String.isBlank(pluginConfiguration.DeveloperName)) { - throw new System.IllegalArgumentException( - 'DeveloperName is required on mock LoggerPlugin__mdt: \n' + System.JSON.serializePretty(pluginConfiguration) - ); - } - if (pluginConfiguration.IsEnabled__c) { - DEVELOPER_NAME_TO_RECORD.put(pluginConfiguration.DeveloperName, pluginConfiguration); - } - } - @SuppressWarnings('PMD.ApexDoc') private class PluginConfigurationSorter implements System.Comparable { public LoggerPlugin__mdt pluginConfiguration; diff --git a/nebula-logger/core/main/configuration/layouts/LoggerFieldMapping__mdt-Logger Field Mapping Layout.layout-meta.xml b/nebula-logger/core/main/configuration/layouts/LoggerFieldMapping__mdt-Logger Field Mapping Layout.layout-meta.xml new file mode 100644 index 000000000..0cd9e4ea8 --- /dev/null +++ b/nebula-logger/core/main/configuration/layouts/LoggerFieldMapping__mdt-Logger Field Mapping Layout.layout-meta.xml @@ -0,0 +1,101 @@ + + + + false + true + true + + + + Required + MasterLabel + + + Required + DeveloperName + + + + + Edit + IsEnabled__c + + + + + + true + true + true + + + + Required + SourceSObjectType__c + + + Required + SourceField__c + + + + + Required + TargetSObjectType__c + + + Required + TargetField__c + + + + + + false + true + true + + + + Edit + IsProtected + + + Readonly + CreatedById + + + + + Required + NamespacePrefix + + + Readonly + LastModifiedById + + + + + + true + true + false + + + + + + + false + false + false + false + false + + 00h8F000000KpfT + 4 + 0 + Default + + diff --git a/nebula-logger/core/main/configuration/objects/LoggerFieldMapping__mdt/LoggerFieldMapping__mdt.object-meta.xml b/nebula-logger/core/main/configuration/objects/LoggerFieldMapping__mdt/LoggerFieldMapping__mdt.object-meta.xml new file mode 100644 index 000000000..1f6c79c2e --- /dev/null +++ b/nebula-logger/core/main/configuration/objects/LoggerFieldMapping__mdt/LoggerFieldMapping__mdt.object-meta.xml @@ -0,0 +1,8 @@ + + + Used to configure custom field mappings in Nebula Logger to map data between LogEntryEvent__e and the custom objects Log__c, LogEntry__c, and LoggerScenario__c + + Logger Field Mappings + Public + diff --git a/nebula-logger/core/main/configuration/objects/LoggerFieldMapping__mdt/fields/IsEnabled__c.field-meta.xml b/nebula-logger/core/main/configuration/objects/LoggerFieldMapping__mdt/fields/IsEnabled__c.field-meta.xml new file mode 100644 index 000000000..7c3dca299 --- /dev/null +++ b/nebula-logger/core/main/configuration/objects/LoggerFieldMapping__mdt/fields/IsEnabled__c.field-meta.xml @@ -0,0 +1,9 @@ + + + IsEnabled__c + true + false + SubscriberControlled + + Checkbox + diff --git a/nebula-logger/core/main/configuration/objects/LoggerFieldMapping__mdt/fields/SourceField__c.field-meta.xml b/nebula-logger/core/main/configuration/objects/LoggerFieldMapping__mdt/fields/SourceField__c.field-meta.xml new file mode 100644 index 000000000..c9c7e48c6 --- /dev/null +++ b/nebula-logger/core/main/configuration/objects/LoggerFieldMapping__mdt/fields/SourceField__c.field-meta.xml @@ -0,0 +1,16 @@ + + + SourceField__c + false + SubscriberControlled + The source field containing the value to map to the target field. Any custom fields that you create on the source SObject type must by populated by your own code/customizations. + + LoggerFieldMapping__mdt.SourceSObjectType__c + FieldDefinition + Logger Source Field Mappings + LoggerSourceFieldMappings + true + MetadataRelationship + false + diff --git a/nebula-logger/core/main/configuration/objects/LoggerFieldMapping__mdt/fields/SourceSObjectType__c.field-meta.xml b/nebula-logger/core/main/configuration/objects/LoggerFieldMapping__mdt/fields/SourceSObjectType__c.field-meta.xml new file mode 100644 index 000000000..b454857b6 --- /dev/null +++ b/nebula-logger/core/main/configuration/objects/LoggerFieldMapping__mdt/fields/SourceSObjectType__c.field-meta.xml @@ -0,0 +1,14 @@ + + + SourceSObjectType__c + false + SubscriberControlled + Only LogEntryEvent__e is supported as the Source SObject Type + + EntityDefinition + Logger Source Field Mappings + LoggerSourceFieldMappings + true + MetadataRelationship + false + diff --git a/nebula-logger/core/main/configuration/objects/LoggerFieldMapping__mdt/fields/TargetField__c.field-meta.xml b/nebula-logger/core/main/configuration/objects/LoggerFieldMapping__mdt/fields/TargetField__c.field-meta.xml new file mode 100644 index 000000000..bbd83ef06 --- /dev/null +++ b/nebula-logger/core/main/configuration/objects/LoggerFieldMapping__mdt/fields/TargetField__c.field-meta.xml @@ -0,0 +1,16 @@ + + + TargetField__c + false + SubscriberControlled + The target field to populate with the value of the source field. The data type of this field must match the data type of the source field - mappings that use different/incompatible data types will be ignored, resulting in lost logging data. + + LoggerFieldMapping__mdt.TargetSObjectType__c + FieldDefinition + Logger Target Field Mappings + LoggerTargetFieldMappings + true + MetadataRelationship + true + diff --git a/nebula-logger/core/main/configuration/objects/LoggerFieldMapping__mdt/fields/TargetSObjectType__c.field-meta.xml b/nebula-logger/core/main/configuration/objects/LoggerFieldMapping__mdt/fields/TargetSObjectType__c.field-meta.xml new file mode 100644 index 000000000..403d18949 --- /dev/null +++ b/nebula-logger/core/main/configuration/objects/LoggerFieldMapping__mdt/fields/TargetSObjectType__c.field-meta.xml @@ -0,0 +1,14 @@ + + + TargetSObjectType__c + false + SubscriberControlled + Only Log__c, LogEntry__c, and LoggerScenario__c are supported as the Target SObject Type + + EntityDefinition + Logger Target Field Mappings + LoggerTargetFieldMappings + true + MetadataRelationship + false + diff --git a/nebula-logger/core/main/configuration/objects/LoggerFieldMapping__mdt/listViews/All.listView-meta.xml b/nebula-logger/core/main/configuration/objects/LoggerFieldMapping__mdt/listViews/All.listView-meta.xml new file mode 100644 index 000000000..5fe7b5979 --- /dev/null +++ b/nebula-logger/core/main/configuration/objects/LoggerFieldMapping__mdt/listViews/All.listView-meta.xml @@ -0,0 +1,13 @@ + + + All + MasterLabel + DeveloperName + IsEnabled__c + SourceSObjectType__c + SourceField__c + TargetSObjectType__c + TargetField__c + Everything + + diff --git a/nebula-logger/core/main/configuration/objects/LoggerFieldMapping__mdt/validationRules/UnsupportedSourceSObjectType.validationRule-meta.xml b/nebula-logger/core/main/configuration/objects/LoggerFieldMapping__mdt/validationRules/UnsupportedSourceSObjectType.validationRule-meta.xml new file mode 100644 index 000000000..7afb6a4f4 --- /dev/null +++ b/nebula-logger/core/main/configuration/objects/LoggerFieldMapping__mdt/validationRules/UnsupportedSourceSObjectType.validationRule-meta.xml @@ -0,0 +1,8 @@ + + + UnsupportedSourceSObjectType + true + SourceSObjectType__r.DeveloperName != 'LogEntryEvent' + SourceSObjectType__c + Only LogEntryEvent__e is supported as the Source SObject Type + diff --git a/nebula-logger/core/main/configuration/objects/LoggerFieldMapping__mdt/validationRules/UnsupportedTargetSObjectType.validationRule-meta.xml b/nebula-logger/core/main/configuration/objects/LoggerFieldMapping__mdt/validationRules/UnsupportedTargetSObjectType.validationRule-meta.xml new file mode 100644 index 000000000..9b5371da8 --- /dev/null +++ b/nebula-logger/core/main/configuration/objects/LoggerFieldMapping__mdt/validationRules/UnsupportedTargetSObjectType.validationRule-meta.xml @@ -0,0 +1,12 @@ + + + UnsupportedTargetSObjectType + true + AND( + TargetSObjectType__r.DeveloperName != 'Log', + TargetSObjectType__r.DeveloperName != 'LogEntry', + TargetSObjectType__r.DeveloperName != 'LoggerScenario' +) + TargetSObjectType__c + Only Log__c, LogEntry__c, and LoggerScenario__c are supported as the Target SObject Type + diff --git a/nebula-logger/core/main/log-management/classes/LogEntryEventHandler.cls b/nebula-logger/core/main/log-management/classes/LogEntryEventHandler.cls index f6e67753a..ca14befda 100644 --- a/nebula-logger/core/main/log-management/classes/LogEntryEventHandler.cls +++ b/nebula-logger/core/main/log-management/classes/LogEntryEventHandler.cls @@ -108,6 +108,8 @@ public without sharing class LogEntryEventHandler extends LoggerSObjectHandler { continue; } else if (LoggerParameter.NORMALIZE_SCENARIO_DATA) { LoggerScenario__c loggerScenario = new LoggerScenario__c(Name = scenario, UniqueId__c = scenario); + + LoggerFieldMapper.mapFieldValues(logEntryEvent, loggerScenario); loggerScenario.setOptions(DML_OPTIONS); SCENARIO_UNIQUE_ID_TO_SCENARIO.put(loggerScenario.UniqueId__c, loggerScenario); } @@ -215,6 +217,7 @@ public without sharing class LogEntryEventHandler extends LoggerSObjectHandler { log.TransactionScenario__c = SCENARIO_UNIQUE_ID_TO_SCENARIO.get(logEntryEvent.TransactionScenario__c).Id; } + LoggerFieldMapper.mapFieldValues(logEntryEvent, log); log.setOptions(DML_OPTIONS); TRANSACTION_ID_TO_LOG.put(log.TransactionId__c, log); } @@ -370,6 +373,7 @@ public without sharing class LogEntryEventHandler extends LoggerSObjectHandler { logEntry.Tags__c = logEntryEvent.Tags__c; } + LoggerFieldMapper.mapFieldValues(logEntryEvent, logEntry); logEntry.setOptions(DML_OPTIONS); this.logEntries.add(logEntry); diff --git a/nebula-logger/core/main/logger-engine/classes/LogEntryEventBuilder.cls b/nebula-logger/core/main/logger-engine/classes/LogEntryEventBuilder.cls index b80d4213b..31e697a41 100644 --- a/nebula-logger/core/main/logger-engine/classes/LogEntryEventBuilder.cls +++ b/nebula-logger/core/main/logger-engine/classes/LogEntryEventBuilder.cls @@ -603,6 +603,50 @@ global with sharing class LogEntryEventBuilder { return this; } + /** + * @description Sets a field values on the builder's `LogEntryEvent__e` record + * @param field The `Schema.SObjectField` token of the field to populate + * on the builder's `LogEntryEvent__e` record + * @param fieldValue The `Object` value to populate in the provided field + * @return The same instance of `LogEntryEventBuilder`, useful for chaining methods + */ + global LogEntryEventBuilder setField(Schema.SObjectField field, Object fieldValue) { + if (this.shouldSave() == false) { + return this; + } + + return this.setField(new Map{ field => fieldValue }); + } + + /** + * @description Sets multiple field values on the builder's `LogEntryEvent__e` record + * @param fieldToValue An instance of `Map` containing the + * the fields & values to populate the builder's `LogEntryEvent__e` record + * @return The same instance of `LogEntryEventBuilder`, useful for chaining methods + */ + @SuppressWarnings('PMD.AvoidDebugStatements') + global LogEntryEventBuilder setField(Map fieldToValue) { + if (this.shouldSave() == false || fieldToValue == null) { + return this; + } + + for (Schema.SObjectField field : fieldToValue.keySet()) { + Schema.DescribeFieldResult fieldDescribe = field.getDescribe(); + Object value = fieldToValue.get(field); + if (fieldDescribe.getSoapType() == Schema.SoapType.STRING) { + value = LoggerDataStore.truncateFieldValue(field, (String) value); + } + try { + this.logEntryEvent.put(field, value); + } catch (System.Exception ex) { + LogMessage logMessage = new LogMessage('Could not set field {0} with value {1}', field, value); + System.debug(System.LoggingLevel.WARN, logMessage.getMessage()); + } + } + + return this; + } + /** * @description Appends the tag to the existing list of tags * @param tag The string to use as a tag for the current entry diff --git a/nebula-logger/core/main/logger-engine/classes/Logger.cls b/nebula-logger/core/main/logger-engine/classes/Logger.cls index 23d8c3151..e7c970b3f 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.13.13'; + private static final String CURRENT_VERSION_NUMBER = 'v4.13.14'; 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.'; 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 05339428c..882cf86f9 100644 --- a/nebula-logger/core/main/logger-engine/lwc/logger/logEntryBuilder.js +++ b/nebula-logger/core/main/logger-engine/lwc/logger/logEntryBuilder.js @@ -4,7 +4,7 @@ //------------------------------------------------------------------------------------------------// import FORM_FACTOR from '@salesforce/client/formFactor'; -const CURRENT_VERSION_NUMBER = 'v4.13.13'; +const CURRENT_VERSION_NUMBER = 'v4.13.14'; // JavaScript equivalent to the Apex class ComponentLogger.ComponentLogEntry const ComponentLogEntry = class { diff --git a/nebula-logger/core/tests/LoggerCore.testSuite-meta.xml b/nebula-logger/core/tests/LoggerCore.testSuite-meta.xml index 4e0056c23..d9683d0ab 100644 --- a/nebula-logger/core/tests/LoggerCore.testSuite-meta.xml +++ b/nebula-logger/core/tests/LoggerCore.testSuite-meta.xml @@ -19,6 +19,7 @@ LoggerDataStore_Tests LoggerEmailSender_Tests LoggerEngineDataSelector_Tests + LoggerFieldMapper_Tests LoggerHomeHeaderController_Tests LoggerParameter_Tests LoggerPlugin_Tests diff --git a/nebula-logger/core/tests/configuration/classes/LoggerFieldMapper_Tests.cls b/nebula-logger/core/tests/configuration/classes/LoggerFieldMapper_Tests.cls new file mode 100644 index 000000000..8ef41fbc7 --- /dev/null +++ b/nebula-logger/core/tests/configuration/classes/LoggerFieldMapper_Tests.cls @@ -0,0 +1,142 @@ +//------------------------------------------------------------------------------------------------// +// 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. // +//------------------------------------------------------------------------------------------------// + +@SuppressWarnings('PMD.MethodNamingConventions, PMD.PropertyNamingConventions') +@IsTest(IsParallel=true) +private class LoggerFieldMapper_Tests { + static { + // Don't use the org's actual custom metadata records when running tests + LoggerFieldMapper.SOURCE_SOBJECT_TYPE_TO_TARGET_FIELD_MAPPINGS.clear(); + } + + @IsTest + static void it_safely_returns_when_no_field_mappings_are_configured() { + LogEntryEvent__e logEntryEvent = new LogEntryEvent__e(Message__c = 'Some value'); + LogEntry__c logEntry = new LogEntry__c(); + + LoggerFieldMapper.mapFieldValues(logEntryEvent, logEntry); + + System.Assert.isTrue(logEntry.getPopulatedFieldsAsMap().isEmpty(), System.JSON.serialize(logEntry)); + } + + @IsTest + static void it_maps_field_value_when_configured() { + LoggerFieldMapper.setMock( + new LoggerFieldMapping__mdt( + DeveloperName = 'SomeDevName', + IsEnabled__c = true, + SourceSObjectType__c = Schema.LogEntryEvent__e.SObjectType.getDescribe().getName(), + SourceField__c = Schema.LogEntryEvent__e.Message__c.getDescribe().getName(), + TargetSObjectType__c = Schema.LogEntry__c.SObjectType.getDescribe().getName(), + TargetField__c = Schema.LogEntry__c.Message__c.getDescribe().getName() + ) + ); + LogEntryEvent__e logEntryEvent = new LogEntryEvent__e(Message__c = 'Some value'); + LogEntry__c logEntry = new LogEntry__c(); + + LoggerFieldMapper.mapFieldValues(logEntryEvent, logEntry); + + System.Assert.isTrue( + logEntry.getPopulatedFieldsAsMap().containsKey(Schema.LogEntry__c.Message__c.getDescribe().getName()), + System.JSON.serialize(logEntry) + ); + System.Assert.areEqual(logEntryEvent.Message__c, logEntry.Message__c); + } + + @IsTest + static void it_truncates_string_value_when_mapping_string_field_value() { + LoggerFieldMapper.setMock( + new LoggerFieldMapping__mdt( + DeveloperName = 'SomeDevName', + IsEnabled__c = true, + SourceSObjectType__c = Schema.LogEntryEvent__e.SObjectType.getDescribe().getName(), + SourceField__c = Schema.LogEntryEvent__e.Message__c.getDescribe().getName(), + TargetSObjectType__c = Schema.LogEntry__c.SObjectType.getDescribe().getName(), + TargetField__c = Schema.LogEntry__c.Message__c.getDescribe().getName() + ) + ); + Integer messageMaxLength = Schema.LogEntryEvent__e.Message__c.getDescribe().getLength(); + LogEntryEvent__e logEntryEvent = new LogEntryEvent__e(Message__c = 'A'.repeat(messageMaxLength + 1)); + LogEntry__c logEntry = new LogEntry__c(); + + LoggerFieldMapper.mapFieldValues(logEntryEvent, logEntry); + + System.Assert.isTrue( + logEntry.getPopulatedFieldsAsMap().containsKey(Schema.LogEntry__c.Message__c.getDescribe().getName()), + System.JSON.serialize(logEntry) + ); + System.Assert.areEqual( + logEntryEvent.Message__c.left(messageMaxLength), + logEntry.Message__c, + 'Expected length of ' + logEntryEvent.Message__c.left(messageMaxLength).length() + ', received length ' + logEntry.Message__c.length() + ); + } + + @IsTest + static void it_handles_when_field_mappings_are_not_configured_for_current_target_sobject_type_but_do_exist_for_other_types() { + LoggerFieldMapper.setMock( + new LoggerFieldMapping__mdt( + DeveloperName = 'SomeDevName', + IsEnabled__c = true, + SourceSObjectType__c = Schema.LogEntryEvent__e.SObjectType.getDescribe().getName(), + SourceField__c = Schema.LogEntryEvent__e.TransactionId__c.getDescribe().getName(), + TargetSObjectType__c = Schema.Log__c.SObjectType.getDescribe().getName(), + TargetField__c = Schema.Log__c.TransactionId__c.getDescribe().getName() + ) + ); + LogEntryEvent__e logEntryEvent = new LogEntryEvent__e(Message__c = 'Some value'); + LogEntry__c logEntry = new LogEntry__c(); + + LoggerFieldMapper.mapFieldValues(logEntryEvent, logEntry); + + System.Assert.isTrue(logEntry.getPopulatedFieldsAsMap().isEmpty(), System.JSON.serialize(logEntry)); + } + + @IsTest + static void it_safely_skips_field_mappings_for_incompatible_field_mappings() { + LoggerFieldMapper.setMock( + new LoggerFieldMapping__mdt( + DeveloperName = '🥳 Some Valid Mapping', + IsEnabled__c = true, + SourceSObjectType__c = Schema.LogEntryEvent__e.SObjectType.getDescribe().getName(), + SourceField__c = Schema.LogEntryEvent__e.EntryScenario__c.getDescribe().getName(), + TargetSObjectType__c = Schema.LogEntry__c.SObjectType.getDescribe().getName(), + TargetField__c = Schema.LogEntry__c.EntryScenario__c.getDescribe().getName() + ) + ); + LoggerFieldMapper.setMock( + new LoggerFieldMapping__mdt( + DeveloperName = '😭 An Incompatible Mapping (mapping a String to a Datetime)', + IsEnabled__c = true, + SourceSObjectType__c = Schema.LogEntryEvent__e.SObjectType.getDescribe().getName(), + SourceField__c = Schema.LogEntryEvent__e.Message__c.getDescribe().getName(), + TargetSObjectType__c = Schema.LogEntry__c.SObjectType.getDescribe().getName(), + TargetField__c = Schema.LogEntry__c.Timestamp__c.getDescribe().getName() + ) + ); + LoggerFieldMapper.setMock( + new LoggerFieldMapping__mdt( + DeveloperName = '😀 Another Valid Mapping', + IsEnabled__c = true, + SourceSObjectType__c = Schema.LogEntryEvent__e.SObjectType.getDescribe().getName(), + SourceField__c = Schema.LogEntryEvent__e.TransactionEntryNumber__c.getDescribe().getName(), + TargetSObjectType__c = Schema.LogEntry__c.SObjectType.getDescribe().getName(), + TargetField__c = Schema.LogEntry__c.TransactionEntryNumber__c.getDescribe().getName() + ) + ); + LogEntryEvent__e logEntryEvent = new LogEntryEvent__e( + EntryScenario__c = 'Some entry scenario', + Message__c = 'Some value', + TransactionEntryNumber__c = 9999 + ); + LogEntry__c logEntry = new LogEntry__c(Message__c = 'Some string value that definitely will fail to map to LogEntry__c.Timestamp__c'); + + LoggerFieldMapper.mapFieldValues(logEntryEvent, logEntry); + + System.Assert.areEqual(logEntryEvent.EntryScenario__c, logEntry.EntryScenario__c, System.JSON.serialize(logEntry)); + System.Assert.isNull(logEntry.Timestamp__c, System.JSON.serialize(logEntry)); + System.Assert.areEqual(logEntryEvent.TransactionEntryNumber__c, logEntry.TransactionEntryNumber__c, System.JSON.serialize(logEntry)); + } +} diff --git a/nebula-logger/core/tests/configuration/classes/LoggerFieldMapper_Tests.cls-meta.xml b/nebula-logger/core/tests/configuration/classes/LoggerFieldMapper_Tests.cls-meta.xml new file mode 100644 index 000000000..df13efa80 --- /dev/null +++ b/nebula-logger/core/tests/configuration/classes/LoggerFieldMapper_Tests.cls-meta.xml @@ -0,0 +1,5 @@ + + + 60.0 + Active + diff --git a/nebula-logger/core/tests/logger-engine/classes/LogEntryEventBuilder_Tests.cls b/nebula-logger/core/tests/logger-engine/classes/LogEntryEventBuilder_Tests.cls index e1ec129b0..e29c362b5 100644 --- a/nebula-logger/core/tests/logger-engine/classes/LogEntryEventBuilder_Tests.cls +++ b/nebula-logger/core/tests/logger-engine/classes/LogEntryEventBuilder_Tests.cls @@ -1793,6 +1793,66 @@ private class LogEntryEventBuilder_Tests { System.Assert.isNull(builder.getLogEntryEvent().UserType__c); } + @IsTest + static void it_should_set_single_field_with_specified_value() { + Schema.SObjectField field = Schema.LogEntryEvent__e.OrganizationId__c; + String fieldValue = 'Some_value'; + LogEntryEventBuilder builder = new LogEntryEventBuilder(getUserSettings(), System.LoggingLevel.INFO, true); + // Wipe out any pre-populated field value to avoid a false-positive on the asserts below + builder.getLogEntryEvent().put(field, null); + + builder.setField(field, fieldValue); + + System.Assert.areEqual(fieldValue, builder.getLogEntryEvent().get(field)); + } + + @IsTest + static void it_should_set_map_of_fields_with_specified_values() { + Map fieldToValue = new Map{ + Schema.LogEntryEvent__e.OrganizationId__c => 'Some_value', + Schema.LogEntryEvent__e.ProfileId__c => 'Some Profile Name' + }; + LogEntryEventBuilder builder = new LogEntryEventBuilder(getUserSettings(), System.LoggingLevel.INFO, true); + // Wipe out any pre-populated fields values to avoid a false-positive on the asserts below + for (Schema.SObjectField field : fieldToValue.keySet()) { + builder.getLogEntryEvent().put(field, null); + } + + builder.setField(fieldToValue); + + for (Schema.SObjectField field : fieldToValue.keySet()) { + Object fieldValue = fieldToValue.get(field); + System.Assert.areEqual(fieldValue, builder.getLogEntryEvent().get(field)); + } + } + + @IsTest + static void it_should_set_single_field_with_truncated_string_value() { + Schema.SObjectField stringField = Schema.LogEntryEvent__e.OrganizationId__c; + Integer maxLength = stringField.getDescribe().getLength(); + String fieldValue = 'A'.repeat(maxLength + 1); + LogEntryEventBuilder builder = new LogEntryEventBuilder(getUserSettings(), System.LoggingLevel.INFO, true); + // Wipe out any pre-populated field value to avoid a false-positive on the asserts below + builder.getLogEntryEvent().put(stringField, null); + + builder.setField(stringField, fieldValue); + + System.Assert.areEqual(fieldValue.left(maxLength), builder.getLogEntryEvent().get(stringField)); + } + + @IsTest + static void it_gracefully_handles_invalid_value_for_field() { + Schema.SObjectField datetimeField = Schema.LogEntryEvent__e.Timestamp__c; + String thingThatIsNotADatetime = 'this is DEFINITELY not a valid datetime, surely 🤷'; + LogEntryEventBuilder builder = new LogEntryEventBuilder(getUserSettings(), System.LoggingLevel.INFO, true); + // Wipe out any pre-populated field value to avoid a false-positive on the asserts below + builder.getLogEntryEvent().put(datetimeField, null); + + builder.setField(datetimeField, thingThatIsNotADatetime); + + System.Assert.isNull(builder.getLogEntryEvent().get(datetimeField)); + } + @IsTest static void it_should_use_configured_log_entry_event_fields_for_debug_string() { // Don't bother testing stack trace logic when using a namespace prefix - there are diff --git a/nebula-logger/extra-tests/customMetadata/LoggerFieldMapping.SomeLogEntryField.md-meta.xml b/nebula-logger/extra-tests/customMetadata/LoggerFieldMapping.SomeLogEntryField.md-meta.xml new file mode 100644 index 000000000..f3378e4ce --- /dev/null +++ b/nebula-logger/extra-tests/customMetadata/LoggerFieldMapping.SomeLogEntryField.md-meta.xml @@ -0,0 +1,29 @@ + + + + false + + IsEnabled__c + true + + + SourceField__c + SomeLogEntryField__c + + + SourceSObjectType__c + LogEntryEvent__e + + + TargetField__c + SomeLogEntryField__c + + + TargetSObjectType__c + LogEntry__c + + diff --git a/nebula-logger/extra-tests/customMetadata/LoggerFieldMapping.SomeLogField.md-meta.xml b/nebula-logger/extra-tests/customMetadata/LoggerFieldMapping.SomeLogField.md-meta.xml new file mode 100644 index 000000000..bbd04768b --- /dev/null +++ b/nebula-logger/extra-tests/customMetadata/LoggerFieldMapping.SomeLogField.md-meta.xml @@ -0,0 +1,29 @@ + + + + false + + IsEnabled__c + true + + + SourceField__c + SomeLogField__c + + + SourceSObjectType__c + LogEntryEvent__e + + + TargetField__c + SomeLogField__c + + + TargetSObjectType__c + Log__c + + diff --git a/nebula-logger/extra-tests/customMetadata/LoggerFieldMapping.SomeLoggerScenarioField.md-meta.xml b/nebula-logger/extra-tests/customMetadata/LoggerFieldMapping.SomeLoggerScenarioField.md-meta.xml new file mode 100644 index 000000000..3fd5ee606 --- /dev/null +++ b/nebula-logger/extra-tests/customMetadata/LoggerFieldMapping.SomeLoggerScenarioField.md-meta.xml @@ -0,0 +1,29 @@ + + + + false + + IsEnabled__c + true + + + SourceField__c + SomeLogEntryField__c + + + SourceSObjectType__c + LogEntryEvent__e + + + TargetField__c + SomeLoggerScenarioField__c + + + TargetSObjectType__c + LoggerScenario__c + + diff --git a/nebula-logger/extra-tests/objects/LogEntryEvent__e/fields/SomeLogEntryField__c.field-meta.xml b/nebula-logger/extra-tests/objects/LogEntryEvent__e/fields/SomeLogEntryField__c.field-meta.xml new file mode 100644 index 000000000..d870b72a3 --- /dev/null +++ b/nebula-logger/extra-tests/objects/LogEntryEvent__e/fields/SomeLogEntryField__c.field-meta.xml @@ -0,0 +1,16 @@ + + + SomeLogEntryField__c + Active + PII;GDPR;CCPA + false + false + false + false + + 255 + false + Confidential + Text + false + diff --git a/nebula-logger/extra-tests/objects/LogEntryEvent__e/fields/SomeLogField__c.field-meta.xml b/nebula-logger/extra-tests/objects/LogEntryEvent__e/fields/SomeLogField__c.field-meta.xml new file mode 100644 index 000000000..7d8fd273f --- /dev/null +++ b/nebula-logger/extra-tests/objects/LogEntryEvent__e/fields/SomeLogField__c.field-meta.xml @@ -0,0 +1,16 @@ + + + SomeLogField__c + Active + PII;GDPR;CCPA + false + false + false + false + + 255 + false + Confidential + Text + false + diff --git a/nebula-logger/extra-tests/objects/LogEntryEvent__e/fields/SomeLoggerScenarioField__c.field-meta.xml b/nebula-logger/extra-tests/objects/LogEntryEvent__e/fields/SomeLoggerScenarioField__c.field-meta.xml new file mode 100644 index 000000000..2a74d781b --- /dev/null +++ b/nebula-logger/extra-tests/objects/LogEntryEvent__e/fields/SomeLoggerScenarioField__c.field-meta.xml @@ -0,0 +1,16 @@ + + + SomeLoggerScenarioField__c + Active + PII;GDPR;CCPA + false + false + false + false + + 255 + false + Confidential + Text + false + diff --git a/nebula-logger/extra-tests/objects/LogEntry__c/fields/SomeLogEntryField__c.field-meta.xml b/nebula-logger/extra-tests/objects/LogEntry__c/fields/SomeLogEntryField__c.field-meta.xml new file mode 100644 index 000000000..d328abc79 --- /dev/null +++ b/nebula-logger/extra-tests/objects/LogEntry__c/fields/SomeLogEntryField__c.field-meta.xml @@ -0,0 +1,14 @@ + + + SomeLogEntryField__c + Active + PII;GDPR;CCPA + false + + 255 + false + Confidential + false + Text + false + diff --git a/nebula-logger/extra-tests/objects/Log__c/fields/SomeLogField__c.field-meta.xml b/nebula-logger/extra-tests/objects/Log__c/fields/SomeLogField__c.field-meta.xml new file mode 100644 index 000000000..4326e3ea7 --- /dev/null +++ b/nebula-logger/extra-tests/objects/Log__c/fields/SomeLogField__c.field-meta.xml @@ -0,0 +1,15 @@ + + + SomeLogField__c + Active + PII;GDPR;CCPA + false + + 255 + false + Confidential + false + false + Text + false + diff --git a/nebula-logger/extra-tests/objects/LoggerScenario__c/fields/SomeLoggerScenarioField__c.field-meta.xml b/nebula-logger/extra-tests/objects/LoggerScenario__c/fields/SomeLoggerScenarioField__c.field-meta.xml new file mode 100644 index 000000000..b9ccd5af9 --- /dev/null +++ b/nebula-logger/extra-tests/objects/LoggerScenario__c/fields/SomeLoggerScenarioField__c.field-meta.xml @@ -0,0 +1,15 @@ + + + SomeLoggerScenarioField__c + Active + PII;GDPR;CCPA + false + + 255 + false + Confidential + false + false + Text + false + diff --git a/nebula-logger/extra-tests/tests/LogEntryEventHandler_Tests_FieldMappings.cls b/nebula-logger/extra-tests/tests/LogEntryEventHandler_Tests_FieldMappings.cls new file mode 100644 index 000000000..be0bcf1f9 --- /dev/null +++ b/nebula-logger/extra-tests/tests/LogEntryEventHandler_Tests_FieldMappings.cls @@ -0,0 +1,93 @@ +//------------------------------------------------------------------------------------------------// +// 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. // +//------------------------------------------------------------------------------------------------// + +@SuppressWarnings('PMD.ApexDoc, PMD.MethodNamingConventions') +@IsTest(IsParallel=false) +private class LogEntryEventHandler_Tests_FieldMappings { + @IsTest + static void it_should_use_field_mappings_on_logger_scenario_and_log_and_log_entry_when_mappings_have_been_configured() { + LoggerDataStore.setMock(LoggerMockDataStore.getEventBus()); + LoggerTestConfigurator.setupMockSObjectHandlerConfigurations(); + LoggerTestConfigurator.getSObjectHandlerConfiguration(Schema.Log__c.SObjectType).IsEnabled__c = false; + LoggerTestConfigurator.getSObjectHandlerConfiguration(Schema.LogEntry__c.SObjectType).IsEnabled__c = false; + LoggerFieldMapper.setMock( + new LoggerFieldMapping__mdt( + DeveloperName = 'LoggerScenarioFieldMapping', + IsEnabled__c = true, + SourceSObjectType__c = Schema.LogEntryEvent__e.SObjectType.getDescribe().getName(), + SourceField__c = Schema.LogEntryEvent__e.SomeLoggerScenarioField__c.getDescribe().getName(), + TargetSObjectType__c = Schema.LoggerScenario__c.SObjectType.getDescribe().getName(), + TargetField__c = Schema.LoggerScenario__c.SomeLoggerScenarioField__c.getDescribe().getName() + ) + ); + LoggerFieldMapper.setMock( + new LoggerFieldMapping__mdt( + DeveloperName = 'LogFieldMapping', + IsEnabled__c = true, + SourceSObjectType__c = Schema.LogEntryEvent__e.SObjectType.getDescribe().getName(), + SourceField__c = Schema.LogEntryEvent__e.SomeLogField__c.getDescribe().getName(), + TargetSObjectType__c = Schema.Log__c.SObjectType.getDescribe().getName(), + TargetField__c = Schema.Log__c.SomeLogField__c.getDescribe().getName() + ) + ); + LoggerFieldMapper.setMock( + new LoggerFieldMapping__mdt( + DeveloperName = 'LogEntryFieldMapping', + IsEnabled__c = true, + SourceSObjectType__c = Schema.LogEntryEvent__e.SObjectType.getDescribe().getName(), + SourceField__c = Schema.LogEntryEvent__e.SomeLogEntryField__c.getDescribe().getName(), + TargetSObjectType__c = Schema.LogEntry__c.SObjectType.getDescribe().getName(), + TargetField__c = Schema.LogEntry__c.SomeLogEntryField__c.getDescribe().getName() + ) + ); + LogEntryEvent__e logEntryEvent = createLogEntryEvent(); + logEntryEvent.SomeLoggerScenarioField__c = 'Some value to map to LoggerScenario__c.SomeLoggerScenarioField__c'; + logEntryEvent.SomeLogField__c = 'Some value to map to Log__c.SomeLogField__c'; + logEntryEvent.SomeLogEntryField__c = 'Some value to map to LogEntry__c.SomeLogEntryField__c'; + + Database.SaveResult saveResult = LoggerMockDataStore.getEventBus().publishRecord(logEntryEvent); + LoggerMockDataStore.getEventBus().deliver(new LogEntryEventHandler()); + + System.Assert.isTrue(saveResult.isSuccess(), saveResult.getErrors().toString()); + System.Assert.areEqual( + 1, + LoggerSObjectHandler.getExecutedHandlers().get(Schema.LogEntryEvent__e.SObjectType).size(), + 'Handler class should have executed one time for AFTER_INSERT' + ); + // 2 scenarios should be created: 1 for TransactionScenario__c, and 1 for EntryScenario__c + List loggerScenarios = [SELECT Id, SomeLoggerScenarioField__c FROM LoggerScenario__c]; + System.Assert.areEqual(2, loggerScenarios.size()); + for (LoggerScenario__c loggerScenario : loggerScenarios) { + System.Assert.areEqual(logEntryEvent.SomeLoggerScenarioField__c, loggerScenario.SomeLoggerScenarioField__c); + } + Log__c log = [SELECT Id, SomeLogField__c, (SELECT SomeLogEntryField__c FROM LogEntries__r) FROM Log__c]; + LogEntry__c logEntry = log.LogEntries__r.get(0); + System.Assert.areEqual(logEntryEvent.SomeLogField__c, log.SomeLogField__c); + System.Assert.areEqual(logEntryEvent.SomeLogEntryField__c, logEntry.SomeLogEntryField__c); + } + + private static LogEntryEvent__e createLogEntryEvent() { + // The data builder class handles populating field values, but for some fields, + // certain values are expected (e.g., LoggedById__c should have a valid user ID), + // so this method handles any additional manipulation to the field values + LogEntryEvent__e logEntryEvent = (LogEntryEvent__e) LoggerMockDataCreator.createDataBuilder(Schema.LogEntryEvent__e.SObjectType) + .populateAllFields() + .getRecord(); + logEntryEvent.ImpersonatedById__c = null; + logEntryEvent.LoggedById__c = System.UserInfo.getUserId(); + logEntryEvent.LoggingLevel__c = System.LoggingLevel.INFO.name(); + logEntryEvent.LoggingLevelOrdinal__c = System.LoggingLevel.INFO.ordinal(); + logEntryEvent.ProfileId__c = System.UserInfo.getProfileId(); + logEntryEvent.RecordCollectionSize__c = 1; + logEntryEvent.RecordCollectionType__c = 'Single'; + logEntryEvent.RecordId__c = System.UserInfo.getUserId(); + logEntryEvent.TimestampString__c = String.valueOf(logEntryEvent.Timestamp__c.getTime()); + logEntryEvent.UserLoggingLevel__c = System.LoggingLevel.INFO.name(); + logEntryEvent.UserLoggingLevelOrdinal__c = System.LoggingLevel.INFO.ordinal(); + logEntryEvent = (LogEntryEvent__e) LoggerMockDataCreator.setReadOnlyField(logEntryEvent, Schema.LogEntryEvent__e.EventUuid, System.UUID.randomUUID()); + + return logEntryEvent; + } +} diff --git a/nebula-logger/extra-tests/tests/LogEntryEventHandler_Tests_FieldMappings.cls-meta.xml b/nebula-logger/extra-tests/tests/LogEntryEventHandler_Tests_FieldMappings.cls-meta.xml new file mode 100644 index 000000000..df13efa80 --- /dev/null +++ b/nebula-logger/extra-tests/tests/LogEntryEventHandler_Tests_FieldMappings.cls-meta.xml @@ -0,0 +1,5 @@ + + + 60.0 + Active + diff --git a/package.json b/package.json index 68d32db56..d2d04e75a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "nebula-logger", - "version": "4.13.13", + "version": "4.13.14", "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 6c2251d3e..8fe340abf 100644 --- a/sfdx-project.json +++ b/sfdx-project.json @@ -3,20 +3,15 @@ "namespace": "", "sourceApiVersion": "60.0", "sfdcLoginUrl": "https://login.salesforce.com", - "plugins": { - "sfdx-plugin-prettier": { - "enabled": true - } - }, "packageDirectories": [ { "package": "Nebula Logger - Core", "path": "./nebula-logger/core", "definitionFile": "./config/scratch-orgs/base-scratch-def.json", "scopeProfiles": true, - "versionNumber": "4.13.13.NEXT", - "versionName": "Improved Fully-Qualified References", - "versionDescription": "Switched to using fully-qualified references to all standard classes in the Schema & System namespaces", + "versionNumber": "4.13.14.NEXT", + "versionName": "Custom Field Mappings Support", + "versionDescription": "Added the ability to set & map custom fields, using new instance method overloads LogEntryEventBuilder.setField(), and a new CMDT LoggerFieldMapping__mdt", "releaseNotesUrl": "https://github.com/jongpie/NebulaLogger/releases", "unpackagedMetadata": { "path": "./nebula-logger/extra-tests" @@ -181,6 +176,7 @@ "Nebula Logger - Core@4.13.11-track-user-federation-identifier": "04t5Y0000027L98QAE", "Nebula Logger - Core@4.13.12-added-refresh-button-on-relatedlogentries-lwc": "04t5Y0000015oCkQAI", "Nebula Logger - Core@4.13.13-improved-fully-qualified-references": "04t5Y0000015oDsQAI", + "Nebula Logger - Core@4.13.14-custom-field-mappings-support": "04t5Y0000015oE2QAI", "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",