diff --git a/.github/codecov.yml b/.github/codecov.yml index a8d661d84..1e2ea9854 100644 --- a/.github/codecov.yml +++ b/.github/codecov.yml @@ -7,6 +7,7 @@ coverage: if_ci_failed: success patch: off ignore: + - 'config/experience-cloud/**/*' - 'nebula-logger/recipes/**/*' comment: behavior: new diff --git a/README.md b/README.md index 3240f8512..a6db30db9 100644 --- a/README.md +++ b/README.md @@ -5,10 +5,10 @@ The most robust logger for Salesforce. Works with Apex, Lightning Components, Flow, Process Builder & Integrations. Designed for Salesforce admins, developers & architects. -## Unlocked Package - v4.9.4 +## Unlocked Package - v4.9.5 -[![Install Unlocked Package in a Sandbox](./images/btn-install-unlocked-package-sandbox.png)](https://test.salesforce.com/packaging/installPackage.apexp?p0=04t5Y0000023R8WQAU) -[![Install Unlocked Package in Production](./images/btn-install-unlocked-package-production.png)](https://login.salesforce.com/packaging/installPackage.apexp?p0=04t5Y0000023R8WQAU) +[![Install Unlocked Package in a Sandbox](./images/btn-install-unlocked-package-sandbox.png)](https://test.salesforce.com/packaging/installPackage.apexp?p0=04t5Y0000023R9KQAU) +[![Install Unlocked Package in Production](./images/btn-install-unlocked-package-production.png)](https://login.salesforce.com/packaging/installPackage.apexp?p0=04t5Y0000023R9KQAU) [![View Documentation](./images/btn-view-documentation.png)](https://jongpie.github.io/NebulaLogger/) ## Managed Package - v4.9.0 diff --git a/docs/apex/Configuration/LoggerParameter.md b/docs/apex/Configuration/LoggerParameter.md index b2ca7f7c4..03e23fa3e 100644 --- a/docs/apex/Configuration/LoggerParameter.md +++ b/docs/apex/Configuration/LoggerParameter.md @@ -26,6 +26,10 @@ Indicates if Nebula Logger will append its own log entries about the logging sys Indicates if Nebula Logger's tagging system is enabled. Controlled by the custom metadata record `LoggerParameter.EnableTagging`, or `true` as the default +#### `PLATFORM_CACHE_PARTITION_NAME` → `String` + +The name of the Platform Cache partition to use for caching (when platform cache is enabled). Controlled by the custom metadata record `LoggerParameter.PlatformCachePartitionName`, or `LoggerCache` as the default + #### `QUERY_APEX_CLASS_DATA` → `Boolean` Controls if Nebula Logger queries `ApexClass` data. When set to `false`, any `ApexClass` fields on `LogEntryEvent__e` and `Log__c` will not be populated Controlled by the custom metadata record `LoggerParameter.QueryApexClassData`, or `true` as the default diff --git a/nebula-logger/core/main/configuration/classes/LoggerCache.cls b/nebula-logger/core/main/configuration/classes/LoggerCache.cls index 26d8e0483..ef8a83145 100644 --- a/nebula-logger/core/main/configuration/classes/LoggerCache.cls +++ b/nebula-logger/core/main/configuration/classes/LoggerCache.cls @@ -9,23 +9,32 @@ */ @SuppressWarnings('PMD.ExcessivePublicCount') public without sharing class LoggerCache { + private static final String DEFAULT_PARTITION_NAME = 'LoggerCache'; private static final Boolean PLATFORM_CACHE_IS_IMMUTABLE = false; @TestVisible private static final String PLATFORM_CACHE_NULL_VALUE = '<{(CACHE_VALUE_IS_NULL)}>'; // Presumably, no one will ever use this as an actual value @TestVisible - private static final String PLATFORM_CACHE_PARTITION_NAME = getQualifiedParitionName('LoggerCache'); + private static final String PLATFORM_CACHE_PARTITION_NAME = getQualifiedParitionName(LoggerParameter.PLATFORM_CACHE_PARTITION_NAME); private static final Cache.Visibility PLATFORM_CACHE_VISIBILITY = Cache.Visibility.NAMESPACE; private static final TransactionCache TRANSACTION_CACHE_INSTANCE = new TransactionCache(); private static PlatformCachePartitionDelegate organizationPartitionDelegate = new PlatformCachePartitionDelegate( - Cache.Org.getPartition(PLATFORM_CACHE_PARTITION_NAME) + PlatformCachePartitionType.ORGANIZATION, + PLATFORM_CACHE_PARTITION_NAME ); private static PlatformCachePartitionDelegate sessionPartitionDelegate = new PlatformCachePartitionDelegate( - Cache.Session.getPartition(PLATFORM_CACHE_PARTITION_NAME) + PlatformCachePartitionType.SESSION, + PLATFORM_CACHE_PARTITION_NAME ); private static PlatformCache organizationCacheInstance; private static PlatformCache sessionCacheInstance; + @TestVisible + private enum PlatformCachePartitionType { + ORGANIZATION, + SESSION + } + /** * @description Interface used to define caches that can be used to store values via different mechanisms */ @@ -96,8 +105,15 @@ public without sharing class LoggerCache { } private static String getQualifiedParitionName(String unqualifiedPartitionName) { - String className = LoggerCache.class.getName(); - String namespacePrefix = className.contains('.') ? className.substringBefore('.') + '.' : ''; + // Since Nebula Logger includes a cache partition (LoggerCache), the included partition's name + // needs to be qualified in order to work in the managed package. If a custom partition is being + // used instead, then just use the partition name as-is - no namespace is added. + if (unqualifiedPartitionName != DEFAULT_PARTITION_NAME) { + return unqualifiedPartitionName; + } + + String qualifiedClassName = LoggerCache.class.getName(); + String namespacePrefix = qualifiedClassName.contains('.') ? qualifiedClassName.substringBefore('.') + '.' : ''; return namespacePrefix + unqualifiedPartitionName; } @@ -115,34 +131,51 @@ public without sharing class LoggerCache { * @description Manages interacting with platform cache partitions, and can be mocked during unit tests * so that tests don't have to rely on the actual platform cache partitions configured in the org. */ - @SuppressWarnings('PMD.ApexDoc') + @SuppressWarnings('PMD.ApexDoc, PMD.EmptyCatchBlock') @TestVisible private virtual class PlatformCachePartitionDelegate { private final Cache.Partition platformCachePartition; - protected PlatformCachePartitionDelegate(Cache.Partition platformCachePartition) { - this.platformCachePartition = platformCachePartition; + protected PlatformCachePartitionDelegate(PlatformCachePartitionType partitionType, String partitionName) { + // Since orgs can customize the platform cache partition (via LoggerParameter__mdt.PlatformCachePartitionName), + // some orgs could have problematic configurations (or may have even deleted the included LoggerCache partition), + // and it seems better to eat the exceptions & fallback to the transaction cache (which doesn't rely on Platform Cache). + // The alternative is a runtime exception, which isn't ideal. + try { + switch on partitionType { + when ORGANIZATION { + this.platformCachePartition = Cache.Org.getPartition(partitionName); + } + when SESSION { + this.platformCachePartition = Cache.Session.getPartition(partitionName); + } + } + } catch (Cache.Org.OrgCacheException orgCacheException) { + // No-op if the partition can't be found - the rest of the code will fallback to using the transaction cache + } catch (Cache.Session.SessionCacheException sessionCacheException) { + // No-op if the partition can't be found - the rest of the code will fallback to using the transaction cache + } } public virtual Boolean contains(String key) { - return this.platformCachePartition.contains(key); + return this.platformCachePartition != null && this.platformCachePartition.contains(key); } public virtual Object get(String key) { - return this.platformCachePartition.get(key); + return this.platformCachePartition?.get(key); } public virtual Boolean isAvailable() { - return this.platformCachePartition.isAvailable(); + return this.platformCachePartition != null && this.platformCachePartition.isAvailable(); } @SuppressWarnings('PMD.ExcessiveParameterList') public virtual void put(String key, Object value, Integer cacheTtlSeconds, Cache.Visibility cacheVisiblity, Boolean isCacheImmutable) { - this.platformCachePartition.put(key, value, cacheTtlSeconds, cacheVisiblity, isCacheImmutable); + this.platformCachePartition?.put(key, value, cacheTtlSeconds, cacheVisiblity, isCacheImmutable); } public virtual void remove(String key) { - this.platformCachePartition.remove(key); + this.platformCachePartition?.remove(key); } } @@ -152,7 +185,6 @@ public without sharing class LoggerCache { */ @SuppressWarnings('PMD.ApexDoc') private class PlatformCache implements Cacheable { - private final Cache.Partition cachePartition; private final PlatformCachePartitionDelegate cachePartitionDelegate; private final Integer cacheTtlSeconds; private final Cacheable transactionCache; diff --git a/nebula-logger/core/main/configuration/classes/LoggerParameter.cls b/nebula-logger/core/main/configuration/classes/LoggerParameter.cls index 25e85a4f9..01d8ca507 100644 --- a/nebula-logger/core/main/configuration/classes/LoggerParameter.cls +++ b/nebula-logger/core/main/configuration/classes/LoggerParameter.cls @@ -73,6 +73,20 @@ public class LoggerParameter { private set; } + /** + * @description The name of the Platform Cache partition to use for caching (when platform cache is enabled). + * Controlled by the custom metadata record `LoggerParameter.PlatformCachePartitionName`, or `LoggerCache` as the default + */ + public static final String PLATFORM_CACHE_PARTITION_NAME { + get { + if (PLATFORM_CACHE_PARTITION_NAME == null) { + PLATFORM_CACHE_PARTITION_NAME = getString('PlatformCachePartitionName', 'LoggerCache'); + } + return PLATFORM_CACHE_PARTITION_NAME; + } + private set; + } + /** * @description Controls if Nebula Logger queries `ApexClass` data. * When set to `false`, any `ApexClass` fields on `LogEntryEvent__e` and `Log__c` will not be populated diff --git a/nebula-logger/core/main/configuration/customMetadata/LoggerParameter.PlatformCachePartitionName.md-meta.xml b/nebula-logger/core/main/configuration/customMetadata/LoggerParameter.PlatformCachePartitionName.md-meta.xml new file mode 100644 index 000000000..95581e200 --- /dev/null +++ b/nebula-logger/core/main/configuration/customMetadata/LoggerParameter.PlatformCachePartitionName.md-meta.xml @@ -0,0 +1,19 @@ + + + + false + + Description__c + Indicates the name of the Platform Cache partition to use for caching (when platform cache is enabled). + +By default, the included cache partition 'LoggerCache' is used. + + + Value__c + LoggerCache + + diff --git a/nebula-logger/core/main/logger-engine/classes/Logger.cls b/nebula-logger/core/main/logger-engine/classes/Logger.cls index 838d8803d..ff0cb326f 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.9.4'; + private static final String CURRENT_VERSION_NUMBER = 'v4.9.5'; private static final System.LoggingLevel DEFAULT_LOGGING_LEVEL = System.LoggingLevel.DEBUG; private static final Set IGNORED_APEX_CLASSES = initializeIgnoredApexClasses(); private static final List LOG_ENTRIES_BUFFER = new List(); diff --git a/nebula-logger/core/tests/configuration/classes/LoggerCache_Tests.cls b/nebula-logger/core/tests/configuration/classes/LoggerCache_Tests.cls index 8f599519f..86f75e9f5 100644 --- a/nebula-logger/core/tests/configuration/classes/LoggerCache_Tests.cls +++ b/nebula-logger/core/tests/configuration/classes/LoggerCache_Tests.cls @@ -369,7 +369,7 @@ private class LoggerCache_Tests { public Integer removeMethodCallCount = 0; private MockPlatformCachePartitionDelegate(Boolean isAvailable) { - super(null); + super(null, null); this.isAvailable = isAvailable; } diff --git a/nebula-logger/core/tests/configuration/classes/LoggerParameter_Tests.cls b/nebula-logger/core/tests/configuration/classes/LoggerParameter_Tests.cls index fd20986c8..ddc71c154 100644 --- a/nebula-logger/core/tests/configuration/classes/LoggerParameter_Tests.cls +++ b/nebula-logger/core/tests/configuration/classes/LoggerParameter_Tests.cls @@ -68,6 +68,17 @@ private class LoggerParameter_Tests { System.Assert.areEqual(mockValue, returnedValue); } + @IsTest + static void it_should_return_constant_value_for_platform_cache_partition_name() { + String mockValue = 'SomeValue'; + LoggerParameter__mdt mockParameter = new LoggerParameter__mdt(DeveloperName = 'PlatformCachePartitionName', Value__c = mockValue); + LoggerParameter.setMock(mockParameter); + + String returnedValue = LoggerParameter.PLATFORM_CACHE_PARTITION_NAME; + + System.Assert.areEqual(mockValue, returnedValue); + } + @IsTest static void it_should_return_constant_value_for_query_apex_class_data() { Boolean mockValue = false; diff --git a/nebula-logger/extra-tests/tests/LoggerCache_Tests_PlatformCache.cls b/nebula-logger/extra-tests/tests/LoggerCache_Tests_PlatformCache.cls index bd47e21dd..3b0f9aabe 100644 --- a/nebula-logger/extra-tests/tests/LoggerCache_Tests_PlatformCache.cls +++ b/nebula-logger/extra-tests/tests/LoggerCache_Tests_PlatformCache.cls @@ -13,6 +13,92 @@ @SuppressWarnings('PMD.ApexDoc, PMD.ApexAssertionsShouldIncludeMessage, PMD.CyclomaticComplexity, PMD.MethodNamingConventions') @IsTest(IsParallel=true) private class LoggerCache_Tests_PlatformCache { + @IsTest + static void it_gracefully_handles_nonexistent_platform_cache_partition_name_for_organization_cache() { + LoggerParameter__mdt platformCachePartitionNameParameter = new LoggerParameter__mdt( + DeveloperName = 'PlatformCachePartitionName', + Value__c = 'SomeValueThatWillHopefullyAndProbablyNeverExistForAPlatformCachePartitionName' + ); + LoggerParameter.setMock(platformCachePartitionNameParameter); + + LoggerCache.Cacheable organizationCache = LoggerCache.getOrganizationCache(); + + // Run the different Cacheable methods to ensure everything works + String testKey = 'someKey'; + Object testValue = System.now(); + System.Assert.isNotNull(organizationCache); + System.Assert.isFalse(organizationCache.contains(testKey)); + System.Assert.areEqual( + organizationCache.contains(testKey), + LoggerCache.getTransactionCache().contains(testKey), + 'Organization and transaction cache should be in sync' + ); + System.Assert.isNull(organizationCache.get(testKey)); + System.Assert.areEqual( + organizationCache.get(testKey), + LoggerCache.getTransactionCache().get(testKey), + 'Organization and transaction cache should be in sync' + ); + organizationCache.remove(testKey); + organizationCache.put(testKey, testValue); + System.Assert.isTrue(organizationCache.contains(testKey)); + System.Assert.areEqual( + organizationCache.contains(testKey), + LoggerCache.getTransactionCache().contains(testKey), + 'Organization and transaction cache should be in sync' + ); + System.Assert.areEqual(testValue, organizationCache.get(testKey)); + System.Assert.areEqual( + organizationCache.get(testKey), + LoggerCache.getTransactionCache().get(testKey), + 'Organization and transaction cache should be in sync' + ); + organizationCache.remove(testKey); + System.Assert.isNull(organizationCache.get(testKey)); + System.Assert.areEqual( + organizationCache.get(testKey), + LoggerCache.getTransactionCache().get(testKey), + 'Organization and transaction cache should be in sync' + ); + } + + @IsTest + static void it_gracefully_handles_nonexistent_platform_cache_partition_name_for_session_cache() { + LoggerParameter__mdt platformCachePartitionNameParameter = new LoggerParameter__mdt( + DeveloperName = 'PlatformCachePartitionName', + Value__c = 'SomeValueThatWillHopefullyAndProbablyNeverExistForAPlatformCachePartitionName' + ); + LoggerParameter.setMock(platformCachePartitionNameParameter); + + LoggerCache.Cacheable sessionCache = LoggerCache.getSessionCache(); + + // Run the different Cacheable methods to ensure everything works + String testKey = 'someKey'; + Object testValue = System.now(); + System.Assert.isNotNull(sessionCache); + System.Assert.isFalse(sessionCache.contains(testKey)); + System.Assert.areEqual( + sessionCache.contains(testKey), + LoggerCache.getTransactionCache().contains(testKey), + 'Session and transaction cache should be in sync' + ); + System.Assert.isNull(sessionCache.get(testKey)); + System.Assert.areEqual(sessionCache.get(testKey), LoggerCache.getTransactionCache().get(testKey), 'Session and transaction cache should be in sync'); + sessionCache.remove(testKey); + sessionCache.put(testKey, testValue); + System.Assert.isTrue(sessionCache.contains(testKey)); + System.Assert.areEqual( + sessionCache.contains(testKey), + LoggerCache.getTransactionCache().contains(testKey), + 'Session and transaction cache should be in sync' + ); + System.Assert.areEqual(testValue, sessionCache.get(testKey)); + System.Assert.areEqual(sessionCache.get(testKey), LoggerCache.getTransactionCache().get(testKey), 'Session and transaction cache should be in sync'); + sessionCache.remove(testKey); + System.Assert.isNull(sessionCache.get(testKey)); + System.Assert.areEqual(sessionCache.get(testKey), LoggerCache.getTransactionCache().get(testKey), 'Session and transaction cache should be in sync'); + } + @IsTest static void it_adds_new_key_to_organization_and_transaction_cache() { String mockKey = 'SomeKey'; diff --git a/package.json b/package.json index b346a1cd9..df37bc54a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "nebula-logger", - "version": "4.9.4", + "version": "4.9.5", "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 441a97302..0af76f2e3 100644 --- a/sfdx-project.json +++ b/sfdx-project.json @@ -14,9 +14,9 @@ "package": "Nebula Logger - Core", "path": "./nebula-logger/core", "definitionFile": "./config/scratch-orgs/base-scratch-def.json", - "versionNumber": "4.9.4.NEXT", - "versionName": "New Indicator Icons", - "versionDescription": "Updated formula fields on LogEntry__c to replace old flag icons with emojis that are more visually distinct from one another, updated format of LogEntry__c limits formula fields to display percentage first (after new indicators) with limit used & max fields displayed in parenthesis", + "versionNumber": "4.9.5.NEXT", + "versionName": "Configurable Platform Cache Partition Name", + "versionDescription": "Added new CMDT record LoggerParameter.PlatformCachePartitionName to control which platform cache partition is used for caching", "releaseNotesUrl": "https://github.com/jongpie/NebulaLogger/releases", "unpackagedMetadata": { "path": "./nebula-logger/extra-tests" @@ -148,6 +148,7 @@ "Nebula Logger - Core@4.9.2-enhancements-for-log-entry-event-stream-lwc": "04t5Y0000023R7iQAE", "Nebula Logger - Core@4.9.3-new-indicator-icons": "04t5Y0000023R7sQAE", "Nebula Logger - Core@4.9.4-new-indicator-icons": "04t5Y0000023R8WQAU", + "Nebula Logger - Core@4.9.5-configurable-platform-cache-partition-name": "04t5Y0000023R9KQAU", "Nebula Logger - Plugin - Async Failure Additions": "0Ho5Y000000blO4SAI", "Nebula Logger - Plugin - Async Failure Additions@1.0.0": "04t5Y0000015lhiQAA", "Nebula Logger - Plugin - Async Failure Additions@1.0.1": "04t5Y0000015lhsQAA",