From 977993e1afa1f1748bdc3517511ff7098ec703b5 Mon Sep 17 00:00:00 2001 From: Jonathan Gillespie Date: Sun, 30 Oct 2022 14:55:06 -0400 Subject: [PATCH] Query Optimizations & More Configurable Controls (#394) * Added new LoggerParameter__mdt records to control stacktrace parsing & synchronously querying/setting some fields, centralized the usage of UserInfo.getSessionId(), fixed milestone link in README to use v4.7.0 (instead of the old v4.6.0 milestone link) * Updated LogEntryEventBuilder to only run queries/set relevant fields based on the corresponding LoggerParameter__mdt "synchronous" records, implemented enabling/disabling stack trace parsing via LoggerParameter__mdt record * Updated LogEntryEventHandler to handle setting queried fields when corresponding LoggerParameter__mdt records are set to asynchronously query * Updated LogEntryEventBuilder and LogEntryEventHandler to fully skip some queries, based on new CMDT records to complete enable/disable different queries * Added all included parameters as constants in LoggerParameter, renamed some constant names and LoggerParameter__mdt records * Updated LogEntryEventBuilder & LoggerDataStore to use more lazy loading on some static variables/constants, * Updated LogEntryEventBuilder to consistently use of shouldShould() method internally instead of the shouldSave property * Updated LoggerCache to leverage a new platform cache partition (cleverly also named LoggerCache). Also added a new LoggerParameter__mdt record 'UsePlatformCache' to enable/disable the usage of platform cache * Updated the 2 data selector classes LoggerEngineDataSelector & LogManagementDataSelector to now use either org or session cache - the original transaction cache is now used internally as a fallback/secondary cache * Added new fields LogEntryEvent__e.RequestId__c and Log__c.RequestId__c to store the value of System.Request.getCurrent().getRequestId(). Previously, this value was used for Log__c.TransactionId__c, and I reverted to using a more reliable UUID in v4.7.3, but some orgs would still like to report on the request ID * Fixed an unreported bug where the value for the workaround field LogEntryEvent__e.TimestampString__c was out of sync with LogEntryEvent__e.TimestampString__c, resulting in inaccurate data on LogEntry__c.Timestamp__c * Fixed the approach used to dynamically determine if Experience Cloud is enabled in the current org * Fixed #393 by removing the ORDER BY statements in several queries because sorting didn't serve a functional purpose. Instead, the results are now sorted just in the tests to help with asserting on the returned values. * Updated build.yml to run Apex test twice - synchronously & synchronously. This helps with automatically testing functionality that relies on the AuthSession object by ensuring the code works with & without an active session * Added some new integration tests in the extra-tests folder * Added some additional Experience Site metadata to better test Network fields being queried & set * Updated pwsh script to not include the build number in sfdx-project.json, removed existing build numbers in sfdx-project.json * Created new version of async failure additions plugin based on changes made by @jamessimone in #392 * Create new version v1.5.1 of Slack plugin that fixes the formatting of multi-line text fields in SlackLoggerPlugin --- .forceignore | 1 + .github/workflows/build.yml | 30 +- .gitignore | 3 +- README.md | 6 +- .../brandingSets/buildYourOwnLWR.json | 2 +- .../routes/tooManyRequests.json | 2 +- .../Logger Test Site.network-meta.xml | 1 + ... Test Site Guest Profile.profile-meta.xml} | 0 ...er Test Site User Profile.profile-meta.xml | 4 + docs/apex/Configuration/LoggerCache.md | 160 +++++++ docs/apex/Configuration/LoggerParameter.md | 72 +++- .../LogManagementDataSelector.md | 14 + .../Logger-Engine/LoggerEngineDataSelector.md | 68 ++- .../Test-Utilities/LoggerMockDataCreator.md | 14 - docs/apex/index.md | 34 +- .../LoggerCache.cachePartition-meta.xml | 20 + .../configuration/classes/LoggerCache.cls | 232 ++++++++++ .../classes/LoggerCache.cls-meta.xml | 0 .../configuration/classes/LoggerParameter.cls | 248 ++++++++++- ...ameter.EnableStackTraceParsing.md-meta.xml | 21 + ...erParameter.QueryApexClassData.md-meta.xml | 17 + ...Parameter.QueryAuthSessionData.md-meta.xml | 17 + ...ryAuthSessionDataSynchronously.md-meta.xml | 17 + ...er.QueryFlowDefinitionViewData.md-meta.xml | 17 + ...ggerParameter.QueryNetworkData.md-meta.xml | 17 + ....QueryNetworkDataSynchronously.md-meta.xml | 17 + ...arameter.QueryOrganizationData.md-meta.xml | 17 + ...yOrganizationDataSynchronously.md-meta.xml | 17 + ...rameter.QueryRelatedRecordData.md-meta.xml | 17 + .../LoggerParameter.QueryUserData.md-meta.xml | 17 + ...ter.QueryUserDataSynchronously.md-meta.xml | 17 + ...ggerParameter.UsePlatformCache.md-meta.xml | 20 + .../classes/LogEntryEventHandler.cls | 196 ++++++--- .../classes/LogEntryHandler.cls | 22 +- .../classes/LogManagementDataSelector.cls | 108 +++-- .../LogRecordPage.flexipage-meta.xml | 16 + .../layouts/Log__c-Log Layout.layout-meta.xml | 4 + .../Log__c/fields/RequestId__c.field-meta.xml | 18 + .../LoggerAdmin.permissionset-meta.xml | 5 + .../LoggerEndUser.permissionset-meta.xml | 5 + .../LoggerLogViewer.permissionset-meta.xml | 5 + .../classes/LogEntryEventBuilder.cls | 366 ++++++++++------ .../main/logger-engine/classes/Logger.cls | 41 +- .../logger-engine/classes/LoggerCache.cls | 67 --- .../logger-engine/classes/LoggerDataStore.cls | 32 +- .../classes/LoggerEngineDataSelector.cls | 186 +++++--- .../fields/RequestId__c.field-meta.xml | 16 + .../classes/LoggerCache_Tests.cls | 402 ++++++++++++++++++ .../classes/LoggerCache_Tests.cls-meta.xml | 0 .../classes/LoggerParameter_Tests.cls | 152 ++++++- .../utilities/LoggerMockDataCreator.cls | 13 - .../classes/LogEntryEventHandler_Tests.cls | 95 ++++- .../classes/LogEntryHandler_Tests.cls | 82 +++- .../LogManagementDataSelector_Tests.cls | 102 ++++- .../classes/FlowCollectionLogEntry_Tests.cls | 2 + .../classes/FlowLogEntry_Tests.cls | 2 + .../classes/FlowRecordLogEntry_Tests.cls | 2 + .../classes/LogEntryEventBuilder_Tests.cls | 281 +++++++++++- .../classes/LoggerCache_Tests.cls | 35 -- .../LoggerEngineDataSelector_Tests.cls | 93 +++- .../logger-engine/classes/Logger_Tests.cls | 12 + .../LoggerExtraTests.testSuite-meta.xml | 18 + ....cls => LogBatchPurger_Tests_Database.cls} | 2 +- ...ogBatchPurger_Tests_Database.cls-meta.xml} | 0 .../LogEntryEventBuilder_Tests_Network.cls | 92 ++++ ...ryEventBuilder_Tests_Network.cls-meta.xml} | 0 ...> LogEntryEventBuilder_Tests_Security.cls} | 16 +- ...yEventBuilder_Tests_Security.cls-meta.xml} | 0 ...=> LogEntryHandler_Tests_EmailMessage.cls} | 2 +- ...ryHandler_Tests_EmailMessage.cls-meta.xml} | 0 .../tests/LogEntryHandler_Tests_Flow.cls | 29 +- .../LogManagementDataSelector_Tests_Flow.cls | 37 ++ ...ementDataSelector_Tests_Flow.cls-meta.xml} | 0 ...LogMassDeleteExtension_Tests_Security.cls} | 2 +- ...eleteExtension_Tests_Security.cls-meta.xml | 5 + .../tests/LoggerCache_Tests_PlatformCache.cls | 183 ++++++++ ...ggerCache_Tests_PlatformCache.cls-meta.xml | 5 + ...LoggerEngineDataSelector_Tests_Network.cls | 35 ++ ...ineDataSelector_Tests_Network.cls-meta.xml | 5 + ...oggerSettingsController_Tests_Security.cls | 6 - .../Logger_Tests_InboundEmailHandler.cls | 4 - .../tests/Logger_Tests_MergeResult.cls | 6 - ...ienceSite.cls => Logger_Tests_Network.cls} | 22 +- .../tests/Logger_Tests_Network.cls-meta.xml | 5 + .../managed-package/sfdx-project.json | 2 +- .../plugins/async-failure-additions/README.md | 4 +- .../LogFlowExecutionErrorEventHandler.cls | 8 +- ...ogFlowExecutionErrorEventHandler_Tests.cls | 8 +- ...cutionErrorEventHandler_Tests.cls-meta.xml | 2 +- ...FlowExecutionErrorEventHandled.md-meta.xml | 12 +- .../classes/LogEntryArchiveController.cls | 2 +- nebula-logger/plugins/slack/README.md | 4 +- .../slack/classes/SlackLoggerPlugin.cls | 2 +- .../LoggerRecipesAdmin.permissionset-meta.xml | 38 ++ .../default}/profiles/Admin.profile-meta.xml | 0 package.json | 10 +- scripts/build/enable-debug-mode.apex | 1 + scripts/build/generate-apex-docs.ps1 | 1 + sfdx-project.json | 27 +- 99 files changed, 3479 insertions(+), 615 deletions(-) rename config/experience-cloud/profiles/{Logger Test Site Profile.profile-meta.xml => Logger Test Site Guest Profile.profile-meta.xml} (100%) create mode 100644 config/experience-cloud/profiles/Logger Test Site User Profile.profile-meta.xml create mode 100644 docs/apex/Configuration/LoggerCache.md create mode 100644 nebula-logger/core/main/configuration/cachePartitions/LoggerCache.cachePartition-meta.xml create mode 100644 nebula-logger/core/main/configuration/classes/LoggerCache.cls rename nebula-logger/core/main/{logger-engine => configuration}/classes/LoggerCache.cls-meta.xml (100%) create mode 100644 nebula-logger/core/main/configuration/customMetadata/LoggerParameter.EnableStackTraceParsing.md-meta.xml create mode 100644 nebula-logger/core/main/configuration/customMetadata/LoggerParameter.QueryApexClassData.md-meta.xml create mode 100644 nebula-logger/core/main/configuration/customMetadata/LoggerParameter.QueryAuthSessionData.md-meta.xml create mode 100644 nebula-logger/core/main/configuration/customMetadata/LoggerParameter.QueryAuthSessionDataSynchronously.md-meta.xml create mode 100644 nebula-logger/core/main/configuration/customMetadata/LoggerParameter.QueryFlowDefinitionViewData.md-meta.xml create mode 100644 nebula-logger/core/main/configuration/customMetadata/LoggerParameter.QueryNetworkData.md-meta.xml create mode 100644 nebula-logger/core/main/configuration/customMetadata/LoggerParameter.QueryNetworkDataSynchronously.md-meta.xml create mode 100644 nebula-logger/core/main/configuration/customMetadata/LoggerParameter.QueryOrganizationData.md-meta.xml create mode 100644 nebula-logger/core/main/configuration/customMetadata/LoggerParameter.QueryOrganizationDataSynchronously.md-meta.xml create mode 100644 nebula-logger/core/main/configuration/customMetadata/LoggerParameter.QueryRelatedRecordData.md-meta.xml create mode 100644 nebula-logger/core/main/configuration/customMetadata/LoggerParameter.QueryUserData.md-meta.xml create mode 100644 nebula-logger/core/main/configuration/customMetadata/LoggerParameter.QueryUserDataSynchronously.md-meta.xml create mode 100644 nebula-logger/core/main/configuration/customMetadata/LoggerParameter.UsePlatformCache.md-meta.xml create mode 100644 nebula-logger/core/main/log-management/objects/Log__c/fields/RequestId__c.field-meta.xml delete mode 100644 nebula-logger/core/main/logger-engine/classes/LoggerCache.cls create mode 100644 nebula-logger/core/main/logger-engine/objects/LogEntryEvent__e/fields/RequestId__c.field-meta.xml create mode 100644 nebula-logger/core/tests/configuration/classes/LoggerCache_Tests.cls rename nebula-logger/core/tests/{logger-engine => configuration}/classes/LoggerCache_Tests.cls-meta.xml (100%) delete mode 100644 nebula-logger/core/tests/logger-engine/classes/LoggerCache_Tests.cls create mode 100644 nebula-logger/extra-tests/testSuites/LoggerExtraTests.testSuite-meta.xml rename nebula-logger/extra-tests/tests/{LogBatchPurger_Tests_Integration.cls => LogBatchPurger_Tests_Database.cls} (98%) rename nebula-logger/extra-tests/tests/{LogBatchPurger_Tests_Integration.cls-meta.xml => LogBatchPurger_Tests_Database.cls-meta.xml} (100%) create mode 100644 nebula-logger/extra-tests/tests/LogEntryEventBuilder_Tests_Network.cls rename nebula-logger/extra-tests/tests/{LogEntryEventBuilder_Tests_Integration.cls-meta.xml => LogEntryEventBuilder_Tests_Network.cls-meta.xml} (100%) rename nebula-logger/extra-tests/tests/{LogEntryEventBuilder_Tests_Integration.cls => LogEntryEventBuilder_Tests_Security.cls} (87%) rename nebula-logger/extra-tests/tests/{LogEntryHandler_Tests_Integration.cls-meta.xml => LogEntryEventBuilder_Tests_Security.cls-meta.xml} (100%) rename nebula-logger/extra-tests/tests/{LogEntryHandler_Tests_Integration.cls => LogEntryHandler_Tests_EmailMessage.cls} (97%) rename nebula-logger/extra-tests/tests/{LogMassDeleteExtension_Tests_Integration.cls-meta.xml => LogEntryHandler_Tests_EmailMessage.cls-meta.xml} (100%) create mode 100644 nebula-logger/extra-tests/tests/LogManagementDataSelector_Tests_Flow.cls rename nebula-logger/extra-tests/tests/{Logger_Tests_ExperienceSite.cls-meta.xml => LogManagementDataSelector_Tests_Flow.cls-meta.xml} (100%) rename nebula-logger/extra-tests/tests/{LogMassDeleteExtension_Tests_Integration.cls => LogMassDeleteExtension_Tests_Security.cls} (97%) create mode 100644 nebula-logger/extra-tests/tests/LogMassDeleteExtension_Tests_Security.cls-meta.xml create mode 100644 nebula-logger/extra-tests/tests/LoggerCache_Tests_PlatformCache.cls create mode 100644 nebula-logger/extra-tests/tests/LoggerCache_Tests_PlatformCache.cls-meta.xml create mode 100644 nebula-logger/extra-tests/tests/LoggerEngineDataSelector_Tests_Network.cls create mode 100644 nebula-logger/extra-tests/tests/LoggerEngineDataSelector_Tests_Network.cls-meta.xml rename nebula-logger/extra-tests/tests/{Logger_Tests_ExperienceSite.cls => Logger_Tests_Network.cls} (85%) create mode 100644 nebula-logger/extra-tests/tests/Logger_Tests_Network.cls-meta.xml create mode 100644 nebula-logger/recipes/permissionsets/LoggerRecipesAdmin.permissionset-meta.xml rename nebula-logger/{recipes => unsorted/default}/profiles/Admin.profile-meta.xml (100%) create mode 100644 scripts/build/enable-debug-mode.apex diff --git a/.forceignore b/.forceignore index 1e5b5a5eb..f2236fd4b 100644 --- a/.forceignore +++ b/.forceignore @@ -5,6 +5,7 @@ nebula-logger/**/main/default/ nebula-logger/managed-package/sfdx-project.json nebula-logger/managed-package/**/*.testSuite-meta.xml +/**/Admin.profile-meta.xml # Directory/package-specific README files **/README.md diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 655b3f426..971fde2e6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -166,9 +166,17 @@ jobs: - name: 'Assign Logger Admin Permission Set' run: npm run permset:assign:admin - - name: 'Run Apex Tests' + # Nebula Logger has functionality that queries the AuthSession object when the current user has an active session. + # The code should work with or without an active session, so the pipeline runs the tests twice - asynchronously and synchronously. + # This is done because, based on how you execute Apex tests, the running user may have an active session (synchrously) or not (asynchronously). + # Utlimately, this could/should probably be better mocked during tests, but the AuthSession is read-only in Apex, so it's a bit difficult to work with. + # Running the Apex tests sync & async serves as an extra level of integration testing to ensure that everything works with or without an active session. + - name: 'Run Apex Tests Asynchronously' run: npm run test:apex + - name: 'Run Apex Tests Synchronously' + run: npm run test:apex -- --synchronous + - name: 'Delete Base Scratch Org' run: npm run org:delete:noprompt if: ${{ always() }} @@ -219,9 +227,17 @@ jobs: - name: 'Assign Logger Admin Permission Set' run: npm run permset:assign:admin - - name: 'Run Apex Tests' + # Nebula Logger has functionality that queries the AuthSession object when the current user has an active session. + # The code should work with or without an active session, so the pipeline runs the tests twice - asynchronously and synchronously. + # This is done because, based on how you execute Apex tests, the running user may have an active session (synchrously) or not (asynchronously). + # Utlimately, this could/should probably be better mocked during tests, but the AuthSession is read-only in Apex, so it's a bit difficult to work with. + # Running the Apex tests sync & async serves as an extra level of integration testing to ensure that everything works with or without an active session. + - name: 'Run Apex Tests Asynchronously' run: npm run test:apex + - name: 'Run Apex Tests Synchronously' + run: npm run test:apex -- --synchronous + - name: 'Delete unsupported code coverage files' run: rm ./test-coverage/apex/test-result-707*-codecoverage.json @@ -323,9 +339,17 @@ jobs: - name: 'Create & Install Package Version' run: npx pwsh ./scripts/build/create-and-install-package-version.ps1 -targetpackagealias '"Nebula Logger - Core"' -targetreadme ./README.md -targetusername nebula-logger-package-demo - - name: 'Run Apex Tests' + # Nebula Logger has functionality that queries the AuthSession object when the current user has an active session. + # The code should work with or without an active session, so the pipeline runs the tests twice - asynchronously and synchronously. + # This is done because, based on how you execute Apex tests, the running user may have an active session (synchrously) or not (asynchronously). + # Utlimately, this could/should probably be better mocked during tests, but the AuthSession is read-only in Apex, so it's a bit difficult to work with. + # Running the Apex tests sync & async serves as an extra level of integration testing in the meantime to ensure that everything works with or without an active session. + - name: 'Run Apex Tests Asynchronously' run: npm run test:apex:nocoverage -- --targetusername nebula-logger-package-demo + - name: 'Run Apex Tests Synchronously' + run: npm run test:apex:nocoverage -- --targetusername nebula-logger-package-demo --synchronous + - name: 'Commit New Package Version' run: | git config --local user.email "action@github.com" diff --git a/.gitignore b/.gitignore index bc0e953d8..c987fdfb7 100644 --- a/.gitignore +++ b/.gitignore @@ -4,7 +4,8 @@ .sf/ .sfdx/ .vscode/ -nebula-logger/**/main/default/ +# nebula-logger/**/main/default/ +tests/ test-coverage/ temp/ diff --git a/README.md b/README.md index c13c513b4..8437808bb 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.8.3 +## Unlocked Package - v4.8.4 -[![Install Unlocked Package in a Sandbox](./images/btn-install-unlocked-package-sandbox.png)](https://test.salesforce.com/packaging/installPackage.apexp?p0=04t5Y0000015lw9QAA) -[![Install Unlocked Package in Production](./images/btn-install-unlocked-package-production.png)](https://login.salesforce.com/packaging/installPackage.apexp?p0=04t5Y0000015lw9QAA) +[![Install Unlocked Package in a Sandbox](./images/btn-install-unlocked-package-sandbox.png)](https://test.salesforce.com/packaging/installPackage.apexp?p0=04t5Y0000023R02QAE) +[![Install Unlocked Package in Production](./images/btn-install-unlocked-package-production.png)](https://login.salesforce.com/packaging/installPackage.apexp?p0=04t5Y0000023R02QAE) [![View Documentation](./images/btn-view-documentation.png)](https://jongpie.github.io/NebulaLogger/) ## Managed Package - v4.8.0 diff --git a/config/experience-cloud/experiences/Logger_Test_Site1/brandingSets/buildYourOwnLWR.json b/config/experience-cloud/experiences/Logger_Test_Site1/brandingSets/buildYourOwnLWR.json index f21b9727d..4e8823353 100644 --- a/config/experience-cloud/experiences/Logger_Test_Site1/brandingSets/buildYourOwnLWR.json +++ b/config/experience-cloud/experiences/Logger_Test_Site1/brandingSets/buildYourOwnLWR.json @@ -102,7 +102,7 @@ "LinkTextDecorationHover": "underline", "MaxContentWidthDesktop": "1800px", "MaxContentWidthMobile": "none", - "MobileBaseFontSize": "1rem", + "MobileBaseFontSize": "16px", "PrimaryAccentColor": "#005fb2", "PrimaryAccentForegroundColor": "#ffffff", "SiteLogo": "", diff --git a/config/experience-cloud/experiences/Logger_Test_Site1/routes/tooManyRequests.json b/config/experience-cloud/experiences/Logger_Test_Site1/routes/tooManyRequests.json index e4dc7e1be..2fb527914 100644 --- a/config/experience-cloud/experiences/Logger_Test_Site1/routes/tooManyRequests.json +++ b/config/experience-cloud/experiences/Logger_Test_Site1/routes/tooManyRequests.json @@ -1,7 +1,7 @@ { "activeViewId": "e02c0193-8cb2-41b6-8298-f31c4f3a3f3f", "appPageId": "3772c2ec-1566-4ffa-9231-d5db8d42b47d", - "configurationTags": ["allow-in-static-site", "too-many-requests"], + "configurationTags": ["too-many-requests", "allow-in-static-site"], "id": "58bd1d16-3a54-4cf5-84a9-16a870bce9d2", "label": "Too Many Requests", "routeType": "too-many-requests", diff --git a/config/experience-cloud/networks/Logger Test Site.network-meta.xml b/config/experience-cloud/networks/Logger Test Site.network-meta.xml index e6dbae8f7..547de4042 100644 --- a/config/experience-cloud/networks/Logger Test Site.network-meta.xml +++ b/config/experience-cloud/networks/Logger Test Site.network-meta.xml @@ -29,6 +29,7 @@ false admin + logger test site user profile Standard diff --git a/config/experience-cloud/profiles/Logger Test Site Profile.profile-meta.xml b/config/experience-cloud/profiles/Logger Test Site Guest Profile.profile-meta.xml similarity index 100% rename from config/experience-cloud/profiles/Logger Test Site Profile.profile-meta.xml rename to config/experience-cloud/profiles/Logger Test Site Guest Profile.profile-meta.xml diff --git a/config/experience-cloud/profiles/Logger Test Site User Profile.profile-meta.xml b/config/experience-cloud/profiles/Logger Test Site User Profile.profile-meta.xml new file mode 100644 index 000000000..e43244d1a --- /dev/null +++ b/config/experience-cloud/profiles/Logger Test Site User Profile.profile-meta.xml @@ -0,0 +1,4 @@ + + + High Volume Customer Portal + diff --git a/docs/apex/Configuration/LoggerCache.md b/docs/apex/Configuration/LoggerCache.md new file mode 100644 index 000000000..ee8f6718d --- /dev/null +++ b/docs/apex/Configuration/LoggerCache.md @@ -0,0 +1,160 @@ +--- +layout: default +--- + +## LoggerCache class + +Class used to cache query results returned by the selector classes + +--- + +### Methods + +#### `contains(String key)` → `Boolean` + +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. + +#### `contains(String key)` → `Boolean` + +Manages interacting with platform cache. The provided transaction cache instance is used internally as the primary caching method, and is further augmented by using Platform Cache to provide caching that spans multiple transactions. + +#### `contains(String key)` → `Boolean` + +Manages any transaction-specific caching, using `Map<String, Object>` + +#### `get(String key)` → `Object` + +#### `get(String key)` → `Object` + +#### `get(String key)` → `Object` + +#### `getOrganizationCache()` → `Cacheable` + +The instance of `Cacheable` used for any organization-specific caching via Platform Cache. When Platform Cache is disabled or not available, the transaction cache is instead used. + +##### Return + +**Type** + +Cacheable + +**Description** + +The singleton instance of `Cacheable` + +#### `getSessionCache()` → `Cacheable` + +The instance of `Cacheable` used for any session-specific caching via Platform Cache. When Platform Cache is disabled or not available, the transaction cache is instead used. + +##### Return + +**Type** + +Cacheable + +**Description** + +The singleton instance of `Cacheable` + +#### `getTransactionCache()` → `Cacheable` + +The instance of `Cacheable` used for any transaction-specific caching. Cached data is stored internally in-memory for the duration of the transaction. + +##### Return + +**Type** + +Cacheable + +**Description** + +The singleton instance of `Cacheable` + +#### `isAvailable()` → `Boolean` + +#### `put(String key, Object value, Integer cacheTtlSeconds, Cache.Visibility cacheVisiblity, Boolean isCacheImmutable)` → `void` + +#### `put(String key, Object value)` → `void` + +#### `put(String key, Object value)` → `void` + +#### `remove(String key)` → `void` + +#### `remove(String key)` → `void` + +#### `remove(String key)` → `void` + +--- + +### Inner Classes + +#### LoggerCache.Cacheable interface + +Interface used to define caches that can be used to store values via different mechanisms + +--- + +##### Methods + +###### `contains(String key)` → `Boolean` + +Indicates if the specified key has already been added to the cache + +####### Parameters + +| Param | Description | +| ----- | ---------------------------------------------- | +| `key` | The `String` key to check for within the cache | + +####### Return + +**Type** + +Boolean + +**Description** + +The `Boolean` result that indicates if the specified key is contained in the cache + +###### `get(String key)` → `Object` + +Returns the cached value for the specified key, or `null` if the specified key does not exist in the cache + +####### Parameters + +| Param | Description | +| ----- | ---------------------------------------------- | +| `key` | The `String` key to check for within the cache | + +####### Return + +**Type** + +Object + +**Description** + +The cached value, or null if no cached value is found for the specified key + +###### `put(String key, Object value)` → `void` + +Adds the provided `Object` value to the cache, using the specified `String` key + +####### Parameters + +| Param | Description | +| ------- | ------------------------------------------------- | +| `key` | The `String` key to add to the cache | +| `value` | The `Object` value to cache for the specified key | + +###### `remove(String key)` → `void` + +Removes the specified `String` key from the cache + +####### Parameters + +| Param | Description | +| ----- | ----------------------------------------- | +| `key` | The `String` key to remove from the cache | + +--- diff --git a/docs/apex/Configuration/LoggerParameter.md b/docs/apex/Configuration/LoggerParameter.md index f758d5de1..b2ca7f7c4 100644 --- a/docs/apex/Configuration/LoggerParameter.md +++ b/docs/apex/Configuration/LoggerParameter.md @@ -12,27 +12,83 @@ Provides a centralized way to load parameters for SObject handlers & plugins #### `CALL_STATUS_API` → `Boolean` -Indicates if Logger will make an async callout to https://api.status.salesforce.com to get additional details about the current org, which is then stored on the Log\_\_c record. Controlled by the custom metadata record LoggerParamer.CallStatusApi, or `false` as the default +Indicates if Nebula Logger will make an async callout to `https://api.status.salesforce.com` to get additional details about the current org, which is then stored on the Log\_\_c record. Controlled by the custom metadata record `LoggerParameter.CallStatusApi`, or `false` as the default + +#### `ENABLE_STACK_TRACE_PARSING` → `Boolean` + +Indicates if Nebula Logger will parse a stack trace for each log entry, which is then used to populate fields like `LogEntry__c.StackTrace__c` and `LogEntry__c.OriginLocation__c`. Controlled by the custom metadata record `LoggerParameter.EnableStackTraceParsing`, or `true` as the default #### `ENABLE_SYSTEM_MESSAGES` → `Boolean` -Indicates if Logger will append its own log entries about the logging system. Controlled by the custom metadata record LoggerParamer.EnableLoggerSystemMessages, or `false` as the default +Indicates if Nebula Logger will append its own log entries about the logging system. Controlled by the custom metadata record `LoggerParameter.EnableLoggerSystemMessages`, or `false` as the default + +#### `ENABLE_TAGGING` → `Boolean` + +Indicates if Nebula Logger's tagging system is enabled. Controlled by the custom metadata record `LoggerParameter.EnableTagging`, or `true` 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 + +#### `QUERY_AUTH_SESSION_DATA` → `Boolean` + +Controls if Nebula Logger queries `AuthSession` data. When set to `false`, any `AuthSession` fields on `LogEntryEvent__e` and `Log__c` will not be populated Controlled by the custom metadata record `LoggerParameter.QueryAuthSessionData`, or `true` as the default + +#### `QUERY_AUTH_SESSION_DATA_SYNCHRONOUSLY` → `Boolean` + +Controls if Nebula Logger queries `AuthSession` data synchronously & populated on `LogEntryEvent__e` records. When set to `false`, any `AuthSession` 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.QueryAuthSessionDataSynchronously`, or `true` as the default + +#### `QUERY_FLOW_DEFINITION_VIEW_DATA` → `Boolean` + +Controls if Nebula Logger queries `FlowDefinitionView` data. When set to `false`, any `FlowDefinitionView` fields on `LogEntryEvent__e` and `Log__c` will not be populated Controlled by the custom metadata record `LoggerParameter.QueryFlowDefinitionViewData`, or `true` as the default + +#### `QUERY_NETWORK_DATA` → `Boolean` + +Controls if Nebula Logger queries `Network` data. When set to `false`, any `Network` fields on `LogEntryEvent__e` and `Log__c` will not be populated Controlled by the custom metadata record `LoggerParameter.QueryNetworkData`, or `true` as the default + +#### `QUERY_NETWORK_DATA_SYNCHRONOUSLY` → `Boolean` + +Controls if Nebula Logger queries `Network` data is queried synchronously & populated on `LogEntryEvent__e` records. When set to `false`, any `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_ORGANIZATION_DATA` → `Boolean` + +Controls if Nebula Logger queries `Organization` data. When set to `false`, any `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 + +#### `QUERY_ORGANIZATION_DATA_SYNCHRONOUSLY` → `Boolean` + +Indicates if Nebula Logger queries `Organization` data is queried synchronously & populated on `LogEntryEvent__e` records. When set to `false`, any `Organization` 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.QueryOrganizationDataSynchronously`, or `true` as the default + +#### `QUERY_RELATED_RECORD_DATA` → `Boolean` + +Controls if Nebula Logger queries data for records synthetically related to a `LogEntry__c` via `LogEntry__c.RecordId__c`. When set to `false`, any fields on `LogEntry__c` related to `LogEntry__c.RecordId__c` not be populated Controlled by the custom metadata record `LoggerParameter.QueryRelatedRecordData`, or `true` as the default + +#### `QUERY_USER_DATA` → `Boolean` + +Controls if Nebula Logger queries `User` data. When set to `false`, any `User` fields on `LogEntryEvent__e` and `Log__c` will not be populated Controlled by the custom metadata record `LoggerParameter.QueryUserData`, or `true` as the default + +#### `QUERY_USER_DATA_SYNCHRONOUSLY` → `Boolean` + +Indicates if Nebula Logger queries `User` data is queried synchronously & populated on `LogEntryEvent__e` records. When set to `false`, any `User` fields on `LogEntryEvent__e` that rely on querying 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.QueryUserDataSynchronously`, or `true` as the default #### `SEND_ERROR_EMAIL_NOTIFICATIONS` → `Boolean` -Indicates if Logger will send an error email notification if any internal exceptions occur. Controlled by the custom metadata record LoggerParamer.SendErrorEmailNotifications, or `true` as the default +Indicates if Logger will send an error email notification if any internal exceptions occur. Controlled by the custom metadata record `LoggerParameter.SendErrorEmailNotifications`, or `true` as the default #### `SYSTEM_DEBUG_MESSAGE_FORMAT` → `String` -The merge-field syntax to use when calling System.debug(). Controlled by the custom metadata record LoggerParamer.SystebugMessageFormat, or `{OriginLocation__c}\n{Message__c}` as the default +The merge-field syntax to use when calling System.debug(). Controlled by the custom metadata record `LoggerParameter.SystebugMessageFormat`, or `{OriginLocation__c}\n{Message__c}` as the default + +#### `USE_FIRST_SCENARIO_FOR_TRANSACTION` → `Boolean` + +Indicates if `Logger.setScenario(String)` uses the first specified value (when `true`), or the last specified value (when `false`) Controlled by the custom metadata record `LoggerParameter.UseFirstSpecifiedScenario`, or `true` as the default -#### `TAGGING_IS_ENABLED` → `Boolean` +#### `USE_PLATFORM_CACHE` → `Boolean` -Indicates if Logger's tagging system is enabled. Controlled by the custom metadata record LoggerParamer.EnableTagging, or `true` as the default +Indicates if Platform Cache is used to cache organization & session data in the cache partition `LoggerCache` Controlled by the custom metadata record `LoggerParameter.UsePlatformCache`, or `true` as the default -#### `TAG_USING_TOPICS` → `Boolean` +#### `USE_TOPICS_FOR_TAGS` → `Boolean` -Indicates if Logger's tagging will use Topic and TopicAssignment for storing tags, or `false` as the default +Indicates if Logger's tagging will use `Topic` and `TopicAssignment` for storing tags (when `true`), or uses Nebula Logger's custom objects `LoggerTag__c` and `LogEntryTag__c` (when `false`) Controlled by the custom metadata record `LoggerParameter.UseTopicsForTags`, or `false` as the default --- diff --git a/docs/apex/Log-Management/LogManagementDataSelector.md b/docs/apex/Log-Management/LogManagementDataSelector.md index e6b1d72f8..f16afa6da 100644 --- a/docs/apex/Log-Management/LogManagementDataSelector.md +++ b/docs/apex/Log-Management/LogManagementDataSelector.md @@ -87,6 +87,20 @@ List<ApexEmailNotification> The cached `List<ApexEmailNotification>` records +#### `getCachedRecentLogWithApiReleaseDetails()` → `Log__c` + +Returns a cached `Log__c` record that has been created within the last 4 hours that has API details populated from calling https://api.status.salesforce.com + +##### Return + +**Type** + +Log\_\_c + +**Description** + +The cached `Log__c` record, or `null` if no match is found + #### `getCountOfAsyncApexJobs(String apexClassName, String apexMethodName, List jobStatuses)` → `Integer` Returns the count of `AsyncApexJob` records with the specified Apex class name, method name & job status diff --git a/docs/apex/Logger-Engine/LoggerEngineDataSelector.md b/docs/apex/Logger-Engine/LoggerEngineDataSelector.md index fa4393120..219393756 100644 --- a/docs/apex/Logger-Engine/LoggerEngineDataSelector.md +++ b/docs/apex/Logger-Engine/LoggerEngineDataSelector.md @@ -10,6 +10,26 @@ Selector class used for all queries that are specific to the logger engine layer ### Methods +#### `getAuthSessions(List userIds)` → `Map` + +Returns a `Map<Id, AuthSession>` for the specified user IDs & their matching active sessions, or `null` if there is not a current session + +##### Parameters + +| Param | Description | +| --------- | ------------------- | +| `userIds` | userIds description | + +##### Return + +**Type** + +Map<Id, AuthSession> + +**Description** + +The instance of `Map<Id, AuthSession>` containing any matching `AuthSession` records + #### `getCachedAuthSession()` → `AuthSession` Returns a cached copy of `AuthSession` for the current user's current session, or `null` if there is not a current session @@ -38,10 +58,16 @@ List<LoggerSObjectHandler_t> The cached `List<LoggerSObjectHandler_t>` records -#### `getCachedNetwork()` → `SObject` +#### `getCachedNetwork(Id networkId)` → `SObject` Returns a cached copy of the current user's `Network` site, or `null` if the current user is not associated with a `Network` site +##### Parameters + +| Param | Description | +| ----------- | --------------------------------------- | +| `networkId` | The record ID of the `Network` to query | + ##### Return **Type** @@ -108,4 +134,44 @@ LoggerEngineDataSelector The singleton instance of `LoggerEngineDataSelector` +#### `getNetworks(List networkIds)` → `Map` + +Returns a list of matching `Network` records based on the provided list of network IDs + +##### Parameters + +| Param | Description | +| ------------ | ---------------------------------- | +| `networkIds` | The list of `Network` IDs to query | + +##### Return + +**Type** + +Map<Id, SObject> + +**Description** + +The instance of `Map<Id, SObject>` containing any matching `Network` records + +#### `getUsers(List userIds)` → `Map` + +Returns a list of matching `User` records based on the provided list of user IDs + +##### Parameters + +| Param | Description | +| --------- | ------------------------------- | +| `userIds` | The list of `User` IDs to query | + +##### Return + +**Type** + +Map<Id, User> + +**Description** + +The instance of `Map<Id, User>` containing any matching `User` records + --- diff --git a/docs/apex/Test-Utilities/LoggerMockDataCreator.md b/docs/apex/Test-Utilities/LoggerMockDataCreator.md index 292d962a2..cdf7e4c18 100644 --- a/docs/apex/Test-Utilities/LoggerMockDataCreator.md +++ b/docs/apex/Test-Utilities/LoggerMockDataCreator.md @@ -393,20 +393,6 @@ User The generated `User` record - it is not automatically inserted into the database. -#### `getNetwork()` → `SObject` - -Returns the current user's `Network` (Experience Cloud site) - -##### Return - -**Type** - -SObject - -**Description** - -The matching `Network` record - #### `getOrganization()` → `Organization` Queries for the `Organization` record for the current environment. diff --git a/docs/apex/index.md b/docs/apex/index.md index 23138145a..12265539d 100644 --- a/docs/apex/index.md +++ b/docs/apex/index.md @@ -38,10 +38,6 @@ Provides the ability to generate string messages on demand, using String.format( The core class for logging -### [LoggerCache](Logger-Engine/LoggerCache) - -Class used to cache query results returned by the selector classes - ### [LoggerDataStore](Logger-Engine/LoggerDataStore) Class used to manage any data-related operations, including database DML statements, publishing platform events via the event bus, and enqueueing queueable jobs @@ -132,21 +128,11 @@ Handles trigger events for the `LoggerTag__c` object Controller class for the lightning web component `related-log-entries` -## Test Utilities - -### [LoggerMockDataCreator](/Test-Utilities/LoggerMockDataCreator) - -Utility class used to help with generating mock data when writing Apex tests for Nebula Logger. These methods are generic, and should work in any Salesforce org. These methods can be used when writing Apex tests for plugins. - -### [LoggerMockDataStore](/Test-Utilities/LoggerMockDataStore) - -Utility class used to mock any data-related operations for the database, event bus, and queueable jobs. These methods are generic, and should work in any Salesforce org. These methods can be used when writing Apex tests for plugins. - -### [LoggerTestConfigurator](/Test-Utilities/LoggerTestConfigurator) +## Configuration -Utility class used to help with setting up Nebula Logger's configurations within a test context. These methods are specific to metadata implemented within Nebula Logger. These methods can be used when writing Apex tests for plugins. +### [LoggerCache](Configuration/LoggerCache) -## Configuration +Class used to cache query results returned by the selector classes ### [LoggerParameter](Configuration/LoggerParameter) @@ -159,3 +145,17 @@ The core of the plugin framework, used to create custom Apex & Flow plugins ### [LoggerScenarioRule](Configuration/LoggerScenarioRule) Provides a centralized way to load scenario rules that override behavior within Nebula Logger + +## Test Utilities + +### [LoggerMockDataCreator](/Test-Utilities/LoggerMockDataCreator) + +Utility class used to help with generating mock data when writing Apex tests for Nebula Logger. These methods are generic, and should work in any Salesforce org. These methods can be used when writing Apex tests for plugins. + +### [LoggerMockDataStore](/Test-Utilities/LoggerMockDataStore) + +Utility class used to mock any data-related operations for the database, event bus, and queueable jobs. These methods are generic, and should work in any Salesforce org. These methods can be used when writing Apex tests for plugins. + +### [LoggerTestConfigurator](/Test-Utilities/LoggerTestConfigurator) + +Utility class used to help with setting up Nebula Logger's configurations within a test context. These methods are specific to metadata implemented within Nebula Logger. These methods can be used when writing Apex tests for plugins. diff --git a/nebula-logger/core/main/configuration/cachePartitions/LoggerCache.cachePartition-meta.xml b/nebula-logger/core/main/configuration/cachePartitions/LoggerCache.cachePartition-meta.xml new file mode 100644 index 000000000..7440a0ee6 --- /dev/null +++ b/nebula-logger/core/main/configuration/cachePartitions/LoggerCache.cachePartition-meta.xml @@ -0,0 +1,20 @@ + + + Platform cache partition used by Nebula Logger + false + LoggerCache + + 1 + 0 + 0 + 0 + Organization + + + 2 + 0 + 0 + 0 + Session + + diff --git a/nebula-logger/core/main/configuration/classes/LoggerCache.cls b/nebula-logger/core/main/configuration/classes/LoggerCache.cls new file mode 100644 index 000000000..26d8e0483 --- /dev/null +++ b/nebula-logger/core/main/configuration/classes/LoggerCache.cls @@ -0,0 +1,232 @@ +//------------------------------------------------------------------------------------------------// +// 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 Class used to cache query results returned by the selector classes + */ +@SuppressWarnings('PMD.ExcessivePublicCount') +public without sharing class 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 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) + ); + private static PlatformCachePartitionDelegate sessionPartitionDelegate = new PlatformCachePartitionDelegate( + Cache.Session.getPartition(PLATFORM_CACHE_PARTITION_NAME) + ); + private static PlatformCache organizationCacheInstance; + private static PlatformCache sessionCacheInstance; + + /** + * @description Interface used to define caches that can be used to store values via different mechanisms + */ + public interface Cacheable { + /** + * @description Indicates if the specified key has already been added to the cache + * @param key The `String` key to check for within the cache + * @return The `Boolean` result that indicates if the specified key is contained in the cache + */ + Boolean contains(String key); + + /** + * @description Returns the cached value for the specified key, or `null` if + * the specified key does not exist in the cache + * @param key The `String` key to check for within the cache + * @return The cached value, or null if no cached value is found for the specified key + */ + Object get(String key); + + /** + * @description Adds the provided `Object` value to the cache, + * using the specified `String` key + * @param key The `String` key to add to the cache + * @param value The `Object` value to cache for the specified key + */ + void put(String key, Object value); + + /** + * @description Removes the specified `String` key from the cache + * @param key The `String` key to remove from the cache + */ + void remove(String key); + } + + /** + * @description The instance of `Cacheable` used for any organization-specific caching via Platform Cache. + * When Platform Cache is disabled or not available, the transaction cache is instead used. + * @return The singleton instance of `Cacheable` + */ + public static Cacheable getOrganizationCache() { + if (organizationCacheInstance == null) { + Integer organizationCacheTtlSeconds = 86400; // 86,400 seconds == 24 hours, the max time-to-live (TTL) allowed for org cache + organizationCacheInstance = new PlatformCache(getTransactionCache(), organizationPartitionDelegate, organizationCacheTtlSeconds); + } + return organizationCacheInstance; + } + + /** + * @description The instance of `Cacheable` used for any session-specific caching via Platform Cache. + * When Platform Cache is disabled or not available, the transaction cache is instead used. + * @return The singleton instance of `Cacheable` + */ + public static Cacheable getSessionCache() { + if (sessionCacheInstance == null) { + Integer sessionCacheTtlSeconds = 28800; // 28,800 seconds == 8 hours, the max time-to-live (TTL) allowed for session cache + sessionCacheInstance = new PlatformCache(getTransactionCache(), sessionPartitionDelegate, sessionCacheTtlSeconds); + } + return sessionCacheInstance; + } + + /** + * @description The instance of `Cacheable` used for any transaction-specific caching. + * Cached data is stored internally in-memory for the duration of the transaction. + * @return The singleton instance of `Cacheable` + */ + public static Cacheable getTransactionCache() { + return TRANSACTION_CACHE_INSTANCE; + } + + private static String getQualifiedParitionName(String unqualifiedPartitionName) { + String className = LoggerCache.class.getName(); + String namespacePrefix = className.contains('.') ? className.substringBefore('.') + '.' : ''; + return namespacePrefix + unqualifiedPartitionName; + } + + @TestVisible + private static void setMockOrganizationPartitionDelegate(PlatformCachePartitionDelegate mockOrganizationPartitionDelegate) { + organizationPartitionDelegate = mockOrganizationPartitionDelegate; + } + + @TestVisible + private static void setMockSessionPartitionDelegate(PlatformCachePartitionDelegate mockSessionPartitionDelegate) { + sessionPartitionDelegate = mockSessionPartitionDelegate; + } + + /** + * @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') + @TestVisible + private virtual class PlatformCachePartitionDelegate { + private final Cache.Partition platformCachePartition; + + protected PlatformCachePartitionDelegate(Cache.Partition platformCachePartition) { + this.platformCachePartition = platformCachePartition; + } + + public virtual Boolean contains(String key) { + return this.platformCachePartition.contains(key); + } + + public virtual Object get(String key) { + return this.platformCachePartition.get(key); + } + + public virtual Boolean isAvailable() { + return 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); + } + + public virtual void remove(String key) { + this.platformCachePartition.remove(key); + } + } + + /** + * @description Manages interacting with platform cache. The provided transaction cache instance is used internally as the primary + * caching method, and is further augmented by using Platform Cache to provide caching that spans multiple transactions. + */ + @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; + + private PlatformCache(Cacheable transactionCache, PlatformCachePartitionDelegate cachePartitionDelegate, Integer cacheTtlSeconds) { + this.transactionCache = transactionCache; + this.cachePartitionDelegate = cachePartitionDelegate; + this.cacheTtlSeconds = cacheTtlSeconds; + } + + public Boolean contains(String key) { + if (LoggerParameter.USE_PLATFORM_CACHE == false || this.transactionCache.contains(key) || this.cachePartitionDelegate.isAvailable() == false) { + return this.transactionCache.contains(key); + } else { + return this.cachePartitionDelegate.contains(key); + } + } + + public Object get(String key) { + if (LoggerParameter.USE_PLATFORM_CACHE == false || this.transactionCache.contains(key) || this.cachePartitionDelegate.isAvailable() == false) { + return this.transactionCache.get(key); + } else { + Object value = this.cachePartitionDelegate.get(key); + // Platform cache does not support storing null values, so a predefined value is used as a substitute + if (value == PLATFORM_CACHE_NULL_VALUE) { + value = null; + } + this.transactionCache.put(key, value); + return value; + } + } + + public void put(String key, Object value) { + this.transactionCache.put(key, value); + + if (LoggerParameter.USE_PLATFORM_CACHE == true && this.cachePartitionDelegate.isAvailable() == true) { + // Platform cache does not support storing null values, so a predefined value is used as a substitute + if (value == null) { + value = PLATFORM_CACHE_NULL_VALUE; + } + this.cachePartitionDelegate.put(key, value, this.cacheTtlSeconds, PLATFORM_CACHE_VISIBILITY, PLATFORM_CACHE_IS_IMMUTABLE); + } + } + + public void remove(String key) { + this.transactionCache.remove(key); + + if (LoggerParameter.USE_PLATFORM_CACHE == true && this.cachePartitionDelegate.isAvailable() == true) { + this.cachePartitionDelegate.remove(key); + } + } + } + + /** + * @description Manages any transaction-specific caching, using `Map` + */ + @SuppressWarnings('PMD.ApexDoc') + private class TransactionCache implements Cacheable { + private final Map keyToValue = new Map(); + + public Boolean contains(String key) { + return this.keyToValue.containsKey(key); + } + + public Object get(String key) { + return this.keyToValue.get(key); + } + + public void put(String key, Object value) { + this.keyToValue.put(key, value); + } + + public void remove(String key) { + this.keyToValue.remove(key); + } + } +} diff --git a/nebula-logger/core/main/logger-engine/classes/LoggerCache.cls-meta.xml b/nebula-logger/core/main/configuration/classes/LoggerCache.cls-meta.xml similarity index 100% rename from nebula-logger/core/main/logger-engine/classes/LoggerCache.cls-meta.xml rename to nebula-logger/core/main/configuration/classes/LoggerCache.cls-meta.xml diff --git a/nebula-logger/core/main/configuration/classes/LoggerParameter.cls b/nebula-logger/core/main/configuration/classes/LoggerParameter.cls index d6c88f932..dc802ddeb 100644 --- a/nebula-logger/core/main/configuration/classes/LoggerParameter.cls +++ b/nebula-logger/core/main/configuration/classes/LoggerParameter.cls @@ -16,9 +16,9 @@ public class LoggerParameter { private static final Set PARAMETERS_TO_LOAD_DURING_TESTS = new Set{ SYSTEM_DEBUG_MESSAGE_FORMAT_PARAMETER }; /** - * @description Indicates if Logger will make an async callout to https://api.status.salesforce.com + * @description Indicates if Nebula Logger will make an async callout to `https://api.status.salesforce.com` * to get additional details about the current org, which is then stored on the Log__c record. - * Controlled by the custom metadata record LoggerParamer.CallStatusApi, or `false` as the default + * Controlled by the custom metadata record `LoggerParameter.CallStatusApi`, or `false` as the default */ public static final Boolean CALL_STATUS_API { get { @@ -31,8 +31,23 @@ public class LoggerParameter { } /** - * @description Indicates if Logger will append its own log entries about the logging system. - * Controlled by the custom metadata record LoggerParamer.EnableLoggerSystemMessages, or `false` as the default + * @description Indicates if Nebula Logger will parse a stack trace for each log entry, which is then used to populate + * fields like `LogEntry__c.StackTrace__c` and `LogEntry__c.OriginLocation__c`. + * Controlled by the custom metadata record `LoggerParameter.EnableStackTraceParsing`, or `true` as the default + */ + public static final Boolean ENABLE_STACK_TRACE_PARSING { + get { + if (ENABLE_STACK_TRACE_PARSING == null) { + ENABLE_STACK_TRACE_PARSING = getBoolean('EnableStackTraceParsing', true); + } + return ENABLE_STACK_TRACE_PARSING; + } + private set; + } + + /** + * @description Indicates if Nebula Logger will append its own log entries about the logging system. + * Controlled by the custom metadata record `LoggerParameter.EnableLoggerSystemMessages`, or `false` as the default */ public static final Boolean ENABLE_SYSTEM_MESSAGES { get { @@ -44,9 +59,192 @@ public class LoggerParameter { private set; } + /** + * @description Indicates if Nebula Logger's tagging system is enabled. + * Controlled by the custom metadata record `LoggerParameter.EnableTagging`, or `true` as the default + */ + public static final Boolean ENABLE_TAGGING { + get { + if (ENABLE_TAGGING == null) { + ENABLE_TAGGING = getBoolean('EnableTagging', true); + } + return ENABLE_TAGGING; + } + 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 + * Controlled by the custom metadata record `LoggerParameter.QueryApexClassData`, or `true` as the default + */ + public static final Boolean QUERY_APEX_CLASS_DATA { + get { + if (QUERY_APEX_CLASS_DATA == null) { + QUERY_APEX_CLASS_DATA = getBoolean('QueryApexClassData', true); + } + return QUERY_APEX_CLASS_DATA; + } + private set; + } + + /** + * @description Controls if Nebula Logger queries `AuthSession` data. + * When set to `false`, any `AuthSession` fields on `LogEntryEvent__e` and `Log__c` will not be populated + * Controlled by the custom metadata record `LoggerParameter.QueryAuthSessionData`, or `true` as the default + */ + public static final Boolean QUERY_AUTH_SESSION_DATA { + get { + if (QUERY_AUTH_SESSION_DATA == null) { + QUERY_AUTH_SESSION_DATA = getBoolean('QueryAuthSessionData', true); + } + return QUERY_AUTH_SESSION_DATA; + } + private set; + } + + /** + * @description Controls if Nebula Logger queries `AuthSession` data synchronously & populated on `LogEntryEvent__e` records. + * When set to `false`, any `AuthSession` 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.QueryAuthSessionDataSynchronously`, or `true` as the default + */ + public static final Boolean QUERY_AUTH_SESSION_DATA_SYNCHRONOUSLY { + get { + if (QUERY_AUTH_SESSION_DATA_SYNCHRONOUSLY == null) { + QUERY_AUTH_SESSION_DATA_SYNCHRONOUSLY = getBoolean('QueryAuthSessionDataSynchronously', true); + } + return QUERY_AUTH_SESSION_DATA_SYNCHRONOUSLY; + } + private set; + } + + /** + * @description Controls if Nebula Logger queries `FlowDefinitionView` data. + * When set to `false`, any `FlowDefinitionView` fields on `LogEntryEvent__e` and `Log__c` will not be populated + * Controlled by the custom metadata record `LoggerParameter.QueryFlowDefinitionViewData`, or `true` as the default + */ + public static final Boolean QUERY_FLOW_DEFINITION_VIEW_DATA { + get { + if (QUERY_FLOW_DEFINITION_VIEW_DATA == null) { + QUERY_FLOW_DEFINITION_VIEW_DATA = getBoolean('QueryFlowDefinitionViewData', true); + } + return QUERY_FLOW_DEFINITION_VIEW_DATA; + } + private set; + } + + /** + * @description Controls if Nebula Logger queries `Network` data. + * When set to `false`, any `Network` fields on `LogEntryEvent__e` and `Log__c` will not be populated + * Controlled by the custom metadata record `LoggerParameter.QueryNetworkData`, or `true` as the default + */ + public static final Boolean QUERY_NETWORK_DATA { + get { + if (QUERY_NETWORK_DATA == null) { + QUERY_NETWORK_DATA = getBoolean('QueryNetworkData', true); + } + return QUERY_NETWORK_DATA; + } + private set; + } + + /** + * @description Controls if Nebula Logger queries `Network` data is queried synchronously & populated on `LogEntryEvent__e` records. + * When set to `false`, any `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 + */ + public static final Boolean QUERY_NETWORK_DATA_SYNCHRONOUSLY { + get { + if (QUERY_NETWORK_DATA_SYNCHRONOUSLY == null) { + QUERY_NETWORK_DATA_SYNCHRONOUSLY = getBoolean('QueryNetworkDataSynchronously', true); + } + return QUERY_NETWORK_DATA_SYNCHRONOUSLY; + } + private set; + } + + /** + * @description Controls if Nebula Logger queries `Organization` data. + * When set to `false`, any `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 + */ + public static final Boolean QUERY_ORGANIZATION_DATA { + get { + if (QUERY_ORGANIZATION_DATA == null) { + QUERY_ORGANIZATION_DATA = getBoolean('QueryOrganizationData', true); + } + return QUERY_ORGANIZATION_DATA; + } + private set; + } + + /** + * @description Indicates if Nebula Logger queries `Organization` data is queried synchronously & populated on `LogEntryEvent__e` records. + * When set to `false`, any `Organization` 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.QueryOrganizationDataSynchronously`, or `true` as the default + */ + public static final Boolean QUERY_ORGANIZATION_DATA_SYNCHRONOUSLY { + get { + if (QUERY_ORGANIZATION_DATA_SYNCHRONOUSLY == null) { + QUERY_ORGANIZATION_DATA_SYNCHRONOUSLY = getBoolean('QueryOrganizationDataSynchronously', true); + } + return QUERY_ORGANIZATION_DATA_SYNCHRONOUSLY; + } + private set; + } + + /** + * @description Controls if Nebula Logger queries data for records synthetically related to a `LogEntry__c` via `LogEntry__c.RecordId__c`. + * When set to `false`, any fields on `LogEntry__c` related to `LogEntry__c.RecordId__c` not be populated + * Controlled by the custom metadata record `LoggerParameter.QueryRelatedRecordData`, or `true` as the default + */ + public static final Boolean QUERY_RELATED_RECORD_DATA { + get { + if (QUERY_RELATED_RECORD_DATA == null) { + QUERY_RELATED_RECORD_DATA = getBoolean('QueryRelatedRecordData', true); + } + return QUERY_RELATED_RECORD_DATA; + } + private set; + } + + /** + * @description Controls if Nebula Logger queries `User` data. + * When set to `false`, any `User` fields on `LogEntryEvent__e` and `Log__c` will not be populated + * Controlled by the custom metadata record `LoggerParameter.QueryUserData`, or `true` as the default + */ + public static final Boolean QUERY_USER_DATA { + get { + if (QUERY_USER_DATA == null) { + QUERY_USER_DATA = getBoolean('QueryUserData', true); + } + return QUERY_USER_DATA; + } + private set; + } + + /** + * @description Indicates if Nebula Logger queries `User` data is queried synchronously & populated on `LogEntryEvent__e` records. + * When set to `false`, any `User` fields on `LogEntryEvent__e` that rely on querying 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.QueryUserDataSynchronously`, or `true` as the default + */ + public static final Boolean QUERY_USER_DATA_SYNCHRONOUSLY { + get { + if (QUERY_USER_DATA_SYNCHRONOUSLY == null) { + QUERY_USER_DATA_SYNCHRONOUSLY = getBoolean('QueryUserDataSynchronously', true); + } + return QUERY_USER_DATA_SYNCHRONOUSLY; + } + private set; + } + /** * @description Indicates if Logger will send an error email notification if any internal exceptions occur. - * Controlled by the custom metadata record LoggerParamer.SendErrorEmailNotifications, or `true` as the default + * Controlled by the custom metadata record `LoggerParameter.SendErrorEmailNotifications`, or `true` as the default */ public static final Boolean SEND_ERROR_EMAIL_NOTIFICATIONS { get { @@ -60,7 +258,7 @@ public class LoggerParameter { /** * @description The merge-field syntax to use when calling System.debug(). - * Controlled by the custom metadata record LoggerParamer.SystemDebugMessageFormat, or `{OriginLocation__c}\n{Message__c}` as the default + * Controlled by the custom metadata record `LoggerParameter.SystemDebugMessageFormat`, or `{OriginLocation__c}\n{Message__c}` as the default */ public static final String SYSTEM_DEBUG_MESSAGE_FORMAT { get { @@ -73,28 +271,44 @@ public class LoggerParameter { } /** - * @description Indicates if Logger's tagging system is enabled. - * Controlled by the custom metadata record LoggerParamer.EnableTagging, or `true` as the default + * @description Indicates if `Logger.setScenario(String)` uses the first specified value (when `true`), or the last specified value (when `false`) + * Controlled by the custom metadata record `LoggerParameter.UseFirstSpecifiedScenario`, or `true` as the default + */ + public static final Boolean USE_FIRST_SCENARIO_FOR_TRANSACTION { + get { + if (USE_FIRST_SCENARIO_FOR_TRANSACTION == null) { + USE_FIRST_SCENARIO_FOR_TRANSACTION = getBoolean('UseFirstSpecifiedScenario', true); + } + return USE_FIRST_SCENARIO_FOR_TRANSACTION; + } + private set; + } + + /** + * @description Indicates if Platform Cache is used to cache organization & session data in the cache partition `LoggerCache` + * Controlled by the custom metadata record `LoggerParameter.UsePlatformCache`, or `true` as the default */ - public static final Boolean TAGGING_IS_ENABLED { + public static final Boolean USE_PLATFORM_CACHE { get { - if (TAGGING_IS_ENABLED == null) { - TAGGING_IS_ENABLED = getBoolean('EnableTagging', true); + if (USE_PLATFORM_CACHE == null) { + USE_PLATFORM_CACHE = getBoolean('UsePlatformCache', true); } - return TAGGING_IS_ENABLED; + return USE_PLATFORM_CACHE; } private set; } /** - * @description Indicates if Logger's tagging will use Topic and TopicAssignment for storing tags, or `false` as the default + * @description Indicates if Logger's tagging will use `Topic` and `TopicAssignment` for storing tags (when `true`), + * or uses Nebula Logger's custom objects `LoggerTag__c` and `LogEntryTag__c` (when `false`) + * Controlled by the custom metadata record `LoggerParameter.UseTopicsForTags`, or `false` as the default */ - public static final Boolean TAG_USING_TOPICS { + public static final Boolean USE_TOPICS_FOR_TAGS { get { - if (TAG_USING_TOPICS == null) { - TAG_USING_TOPICS = getBoolean('UseTopicsForTags', false); + if (USE_TOPICS_FOR_TAGS == null) { + USE_TOPICS_FOR_TAGS = getBoolean('UseTopicsForTags', false); } - return TAG_USING_TOPICS; + return USE_TOPICS_FOR_TAGS; } private set; } diff --git a/nebula-logger/core/main/configuration/customMetadata/LoggerParameter.EnableStackTraceParsing.md-meta.xml b/nebula-logger/core/main/configuration/customMetadata/LoggerParameter.EnableStackTraceParsing.md-meta.xml new file mode 100644 index 000000000..06842417e --- /dev/null +++ b/nebula-logger/core/main/configuration/customMetadata/LoggerParameter.EnableStackTraceParsing.md-meta.xml @@ -0,0 +1,21 @@ + + + + false + + Description__c + When set to 'true' (default), the Apex or JavaScript stack trace will be parsed on each log entry & used to set the fields OriginLocation__c tags will be saved in your org. Depending on how your org's configuration, tags are either stored in LoggerTag__c and LogEntryTag__c objects (default), or using the standard objects Topic and TopicAssignment. + +When set to 'false', stack traces will not be parsed - this can conserve CPU usage, at the expense of losing some data about log entries. + + + Value__c + true + + diff --git a/nebula-logger/core/main/configuration/customMetadata/LoggerParameter.QueryApexClassData.md-meta.xml b/nebula-logger/core/main/configuration/customMetadata/LoggerParameter.QueryApexClassData.md-meta.xml new file mode 100644 index 000000000..96d74f94a --- /dev/null +++ b/nebula-logger/core/main/configuration/customMetadata/LoggerParameter.QueryApexClassData.md-meta.xml @@ -0,0 +1,17 @@ + + + + false + + Description__c + + + + Value__c + true + + diff --git a/nebula-logger/core/main/configuration/customMetadata/LoggerParameter.QueryAuthSessionData.md-meta.xml b/nebula-logger/core/main/configuration/customMetadata/LoggerParameter.QueryAuthSessionData.md-meta.xml new file mode 100644 index 000000000..3722e7cda --- /dev/null +++ b/nebula-logger/core/main/configuration/customMetadata/LoggerParameter.QueryAuthSessionData.md-meta.xml @@ -0,0 +1,17 @@ + + + + false + + Description__c + + + + Value__c + true + + diff --git a/nebula-logger/core/main/configuration/customMetadata/LoggerParameter.QueryAuthSessionDataSynchronously.md-meta.xml b/nebula-logger/core/main/configuration/customMetadata/LoggerParameter.QueryAuthSessionDataSynchronously.md-meta.xml new file mode 100644 index 000000000..29421f563 --- /dev/null +++ b/nebula-logger/core/main/configuration/customMetadata/LoggerParameter.QueryAuthSessionDataSynchronously.md-meta.xml @@ -0,0 +1,17 @@ + + + + false + + Description__c + + + + Value__c + true + + diff --git a/nebula-logger/core/main/configuration/customMetadata/LoggerParameter.QueryFlowDefinitionViewData.md-meta.xml b/nebula-logger/core/main/configuration/customMetadata/LoggerParameter.QueryFlowDefinitionViewData.md-meta.xml new file mode 100644 index 000000000..f966c813e --- /dev/null +++ b/nebula-logger/core/main/configuration/customMetadata/LoggerParameter.QueryFlowDefinitionViewData.md-meta.xml @@ -0,0 +1,17 @@ + + + + false + + Description__c + + + + Value__c + true + + diff --git a/nebula-logger/core/main/configuration/customMetadata/LoggerParameter.QueryNetworkData.md-meta.xml b/nebula-logger/core/main/configuration/customMetadata/LoggerParameter.QueryNetworkData.md-meta.xml new file mode 100644 index 000000000..3ea2f8934 --- /dev/null +++ b/nebula-logger/core/main/configuration/customMetadata/LoggerParameter.QueryNetworkData.md-meta.xml @@ -0,0 +1,17 @@ + + + + false + + Description__c + + + + Value__c + true + + diff --git a/nebula-logger/core/main/configuration/customMetadata/LoggerParameter.QueryNetworkDataSynchronously.md-meta.xml b/nebula-logger/core/main/configuration/customMetadata/LoggerParameter.QueryNetworkDataSynchronously.md-meta.xml new file mode 100644 index 000000000..ac5aa9956 --- /dev/null +++ b/nebula-logger/core/main/configuration/customMetadata/LoggerParameter.QueryNetworkDataSynchronously.md-meta.xml @@ -0,0 +1,17 @@ + + + + false + + Description__c + + + + Value__c + true + + diff --git a/nebula-logger/core/main/configuration/customMetadata/LoggerParameter.QueryOrganizationData.md-meta.xml b/nebula-logger/core/main/configuration/customMetadata/LoggerParameter.QueryOrganizationData.md-meta.xml new file mode 100644 index 000000000..ecfc5cd04 --- /dev/null +++ b/nebula-logger/core/main/configuration/customMetadata/LoggerParameter.QueryOrganizationData.md-meta.xml @@ -0,0 +1,17 @@ + + + + false + + Description__c + + + + Value__c + true + + diff --git a/nebula-logger/core/main/configuration/customMetadata/LoggerParameter.QueryOrganizationDataSynchronously.md-meta.xml b/nebula-logger/core/main/configuration/customMetadata/LoggerParameter.QueryOrganizationDataSynchronously.md-meta.xml new file mode 100644 index 000000000..98a46cc6a --- /dev/null +++ b/nebula-logger/core/main/configuration/customMetadata/LoggerParameter.QueryOrganizationDataSynchronously.md-meta.xml @@ -0,0 +1,17 @@ + + + + false + + Description__c + + + + Value__c + true + + diff --git a/nebula-logger/core/main/configuration/customMetadata/LoggerParameter.QueryRelatedRecordData.md-meta.xml b/nebula-logger/core/main/configuration/customMetadata/LoggerParameter.QueryRelatedRecordData.md-meta.xml new file mode 100644 index 000000000..56dd6f6ff --- /dev/null +++ b/nebula-logger/core/main/configuration/customMetadata/LoggerParameter.QueryRelatedRecordData.md-meta.xml @@ -0,0 +1,17 @@ + + + + false + + Description__c + + + + Value__c + true + + diff --git a/nebula-logger/core/main/configuration/customMetadata/LoggerParameter.QueryUserData.md-meta.xml b/nebula-logger/core/main/configuration/customMetadata/LoggerParameter.QueryUserData.md-meta.xml new file mode 100644 index 000000000..a34f5ee62 --- /dev/null +++ b/nebula-logger/core/main/configuration/customMetadata/LoggerParameter.QueryUserData.md-meta.xml @@ -0,0 +1,17 @@ + + + + false + + Description__c + + + + Value__c + true + + diff --git a/nebula-logger/core/main/configuration/customMetadata/LoggerParameter.QueryUserDataSynchronously.md-meta.xml b/nebula-logger/core/main/configuration/customMetadata/LoggerParameter.QueryUserDataSynchronously.md-meta.xml new file mode 100644 index 000000000..d001c5e31 --- /dev/null +++ b/nebula-logger/core/main/configuration/customMetadata/LoggerParameter.QueryUserDataSynchronously.md-meta.xml @@ -0,0 +1,17 @@ + + + + false + + Description__c + + + + Value__c + true + + diff --git a/nebula-logger/core/main/configuration/customMetadata/LoggerParameter.UsePlatformCache.md-meta.xml b/nebula-logger/core/main/configuration/customMetadata/LoggerParameter.UsePlatformCache.md-meta.xml new file mode 100644 index 000000000..4faf1f1f0 --- /dev/null +++ b/nebula-logger/core/main/configuration/customMetadata/LoggerParameter.UsePlatformCache.md-meta.xml @@ -0,0 +1,20 @@ + + + + false + + Description__c + When set to 'true' (default), Nebula Logger will used Platform Cache to cache organization and session data in the cache partition LoggerCache. +When set to 'false', any cached data is only cached within a single transaction. + + + 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 2f14b36e4..9fcf71b88 100644 --- a/nebula-logger/core/main/log-management/classes/LogEntryEventHandler.cls +++ b/nebula-logger/core/main/log-management/classes/LogEntryEventHandler.cls @@ -15,6 +15,7 @@ public without sharing class LogEntryEventHandler extends LoggerSObjectHandler { private static final String DEFAULT_STORAGE_LOCATION_NAME = 'CUSTOM_OBJECTS'; private static final Database.DmlOptions DML_OPTIONS = createDmlOptions(); private static final String GUEST_USER_TYPE = 'Guest'; + private static final String NEW_LINE_DELIMITER = '\n'; private static final Map SCENARIO_UNIQUE_ID_TO_SCENARIO = new Map(); private static final Map TRANSACTION_ID_TO_LOG = new Map(); @@ -117,7 +118,7 @@ public without sharing class LogEntryEventHandler extends LoggerSObjectHandler { private void upsertLogs() { // To avoid making a callout for every log for details retrieved from api.status.salesforce.com, // try to query recent logs first to see if there is a recent log with the details already populated - Log__c recentLogWithApiReleaseDetails = getRecentLogWithApiReleaseDetails(); + Log__c recentLogWithApiReleaseDetails = LogManagementDataSelector.getInstance().getCachedRecentLogWithApiReleaseDetails(); for (LogEntryEvent__e logEntryEvent : this.logEntryEvents) { // The LogEntryEvent__e object stores a denormalized version of Log__c & LogEntry__c data @@ -126,8 +127,6 @@ public without sharing class LogEntryEventHandler extends LoggerSObjectHandler { continue; } - Id logOwnerId = this.determineLogOwnerId(logEntryEvent); - Log__c log = new Log__c( ApiReleaseNumber__c = recentLogWithApiReleaseDetails?.ApiReleaseNumber__c, ApiReleaseVersion__c = recentLogWithApiReleaseDetails?.ApiReleaseVersion__c, @@ -156,10 +155,11 @@ public without sharing class LogEntryEventHandler extends LoggerSObjectHandler { OrganizationName__c = logEntryEvent.OrganizationName__c, OrganizationNamespacePrefix__c = logEntryEvent.OrganizationNamespacePrefix__c, OrganizationType__c = logEntryEvent.OrganizationType__c, - OwnerId = logOwnerId, + OwnerId = this.determineLogOwnerId(logEntryEvent), ParentLogTransactionId__c = logEntryEvent.ParentLogTransactionId__c, ProfileId__c = logEntryEvent.ProfileId__c, ProfileName__c = logEntryEvent.ProfileName__c, + RequestId__c = logEntryEvent.RequestId__c, Scenario__c = logEntryEvent.TransactionScenario__c, SessionId__c = logEntryEvent.SessionId__c, SessionSecurityLevel__c = logEntryEvent.SessionSecurityLevel__c, @@ -187,9 +187,15 @@ public without sharing class LogEntryEventHandler extends LoggerSObjectHandler { log.TransactionScenario__c = SCENARIO_UNIQUE_ID_TO_SCENARIO.get(logEntryEvent.TransactionScenario__c).Id; } + log.setOptions(DML_OPTIONS); TRANSACTION_ID_TO_LOG.put(log.TransactionId__c, log); } + setQueriedAuthSessionDetails(TRANSACTION_ID_TO_LOG.values()); + setQueriedNetworkDetails(TRANSACTION_ID_TO_LOG.values()); + setQueriedOrganizationDetails(TRANSACTION_ID_TO_LOG.values()); + setQueriedUserDetails(TRANSACTION_ID_TO_LOG.values()); + List upsertResults = LoggerDataStore.getDatabase() .upsertRecords(TRANSACTION_ID_TO_LOG.values(), Schema.Log__c.TransactionId__c, System.Test.isRunningTest()); LoggerEmailSender.sendErrorEmail(Schema.Log__c.SObjectType, upsertResults); @@ -214,7 +220,9 @@ public without sharing class LogEntryEventHandler extends LoggerSObjectHandler { List logEntriesWithUuid = new List(); List logEntriesWithoutUuid = new List(); for (LogEntryEvent__e logEntryEvent : this.logEntryEvents) { - // Workaround field for platform issue w/ accurate datetimes + // Salesforce does not provide precise datetimes in Apex triggers for platform events + // Use the string value of timestamp to set the actual datetime field as a workaround + // See https://developer.salesforce.com/docs/atlas.en-us.platform_events.meta/platform_events/platform_events_api_considerations.htm Datetime timestamp = String.isNotBlank(logEntryEvent.TimestampString__c) ? Datetime.valueOf(Long.valueOf(logEntryEvent.TimestampString__c)) : logEntryEvent.Timestamp__c; @@ -305,7 +313,6 @@ public without sharing class LogEntryEventHandler extends LoggerSObjectHandler { } logEntry.setOptions(DML_OPTIONS); - this.logEntries.add(logEntry); if (logEntry.EventUuid__c == null) { logEntriesWithoutUuid.add(logEntry); @@ -326,15 +333,13 @@ public without sharing class LogEntryEventHandler extends LoggerSObjectHandler { } private void appendRuleBasedTags() { - if (LoggerParameter.TAGGING_IS_ENABLED == false || TAG_ASSIGNMENT_RULES.isEmpty() == true) { + if (LoggerParameter.ENABLE_TAGGING == false || TAG_ASSIGNMENT_RULES.isEmpty() == true) { return; } for (LogEntry__c logEntry : this.logEntries) { for (LogEntryTagRule__mdt rule : TAG_ASSIGNMENT_RULES) { - Boolean ruleCriteriaMet = ruleCriteriaMet(logEntry, rule); - - if (ruleCriteriaMet == true) { + if (isRuleCriteriaMet(logEntry, rule) == true) { List configuredTagNames = getTagNames(rule.Tags__c); this.tagNames.addAll(configuredTagNames); List logEntryTags = logEntryEventUuidToTagNames.get(logEntry.EventUuid__c); @@ -351,12 +356,12 @@ public without sharing class LogEntryEventHandler extends LoggerSObjectHandler { } private void upsertLogEntryTags() { - if (LoggerParameter.TAGGING_IS_ENABLED == false || this.tagNames.isEmpty()) { + if (LoggerParameter.ENABLE_TAGGING == false || this.tagNames.isEmpty() == true) { return; } // Orgs can be configured to either use LoggerTag__c & LogEntryTag__c (default), or use Topic & TopicAssignment - Schema.SObjectType tagSObjectType = LoggerParameter.TAG_USING_TOPICS == true ? Topic.SObjectType : LoggerTag__c.SObjectType; + Schema.SObjectType tagSObjectType = LoggerParameter.USE_TOPICS_FOR_TAGS == true ? Topic.SObjectType : LoggerTag__c.SObjectType; Map tagNameToId = getTagNameToId(tagSObjectType); this.tagNames.addAll(tagNameToId.keySet()); @@ -374,7 +379,7 @@ public without sharing class LogEntryEventHandler extends LoggerSObjectHandler { } for (String tagName : logEntryTagNames) { - if (LoggerParameter.TAG_USING_TOPICS == true) { + if (LoggerParameter.USE_TOPICS_FOR_TAGS == true) { // Add TopicAssignment records for both the LogEntry__c & the parent Log__c tagAssignmentSObjectType = Schema.TopicAssignment.SObjectType; tagAssignments.add(new TopicAssignment(EntityId = logEntry.Id, TopicId = tagNameToId.get(tagName))); @@ -456,12 +461,13 @@ public without sharing class LogEntryEventHandler extends LoggerSObjectHandler { if (existingTagNameToId.containsKey(tagName) == false) { SObject tag = tagSObjectType.newSObject(); tag.put('Name', tagName); + tag.setOptions(DML_OPTIONS); missingTagsToCreate.add(tag); } } - if (!missingTagsToCreate.isEmpty()) { - List saveResults = LoggerDataStore.getDatabase().insertRecords(missingTagsToCreate, DML_OPTIONS); + if (missingTagsToCreate.isEmpty() == false) { + List saveResults = LoggerDataStore.getDatabase().insertRecords(missingTagsToCreate); LoggerEmailSender.sendErrorEmail(tagSObjectType, saveResults); for (SObject tag : missingTagsToCreate) { missingTagNameToId.put((String) tag.get('Name'), (Id) tag.get('Id')); @@ -479,10 +485,33 @@ public without sharing class LogEntryEventHandler extends LoggerSObjectHandler { return dmlOptions; } + private static Boolean isRuleCriteriaMet(LogEntry__c logEntry, LogEntryTagRule__mdt rule) { + Boolean ruleCriteriaMet = false; + String logEntryFieldValue = String.valueOf(logEntry.get(rule.SObjectField__c)); + String ruleComparisonValue = rule.ComparisonValue__c; + + switch on rule?.ComparisonType__c.toUpperCase() { + when 'CONTAINS' { + ruleCriteriaMet = logEntryFieldValue.containsIgnoreCase(ruleComparisonValue); + } + when 'EQUALS' { + ruleCriteriaMet = logEntryFieldValue == ruleComparisonValue; + } + when 'MATCHES_REGEX' { + ruleCriteriaMet = Pattern.compile(ruleComparisonValue).matcher(logEntryFieldValue).matches(); + } + when 'STARTS_WITH' { + ruleCriteriaMet = logEntryFieldValue.startsWith(ruleComparisonValue); + } + } + + return ruleCriteriaMet; + } + private static List getTagNames(String tagsString) { List cleanedTagNames = new List(); - for (String tagName : tagsString.split('\n')) { + for (String tagName : tagsString.split(NEW_LINE_DELIMITER)) { if (String.isNotBlank(tagName) == true) { cleanedTagNames.add(tagName.trim()); } @@ -492,14 +521,7 @@ public without sharing class LogEntryEventHandler extends LoggerSObjectHandler { } private static List getTagAssignmentRules() { - if (LogEntryTagRule__mdt.getAll().isEmpty() == true) { - return new List(); - } - List tagAssignmentRules = LoggerEngineDataSelector.getInstance().getCachedTagAssignmentRules(); - for (LogEntryTagRule__mdt rule : tagAssignmentRules) { - rule.SObjectField__c = rule.SObjectField__r.QualifiedApiName; - } if (System.Test.isRunningTest() == true) { // During tests, only use mock records - tests can add mock records using LogEntryEventHandler.TAG_ASSIGNMENT_RULES.add() tagAssignmentRules.clear(); @@ -507,44 +529,118 @@ public without sharing class LogEntryEventHandler extends LoggerSObjectHandler { return tagAssignmentRules; } - private static Boolean ruleCriteriaMet(LogEntry__c logEntry, LogEntryTagRule__mdt rule) { - Boolean ruleCriteriaMet = false; - String logEntryFieldValue = String.valueOf(logEntry.get(rule.SObjectField__c)); - String ruleComparisonValue = rule.ComparisonValue__c; + private static void setQueriedAuthSessionDetails(List logs) { + if (LoggerParameter.QUERY_AUTH_SESSION_DATA_SYNCHRONOUSLY == true) { + return; + } - switch on rule?.ComparisonType__c.toUpperCase() { - when 'CONTAINS' { - ruleCriteriaMet = logEntryFieldValue.containsIgnoreCase(ruleComparisonValue); + List userIds = new List(); + for (Log__c log : logs) { + if (log.LoggedBy__c != null) { + userIds.add(log.LoggedBy__c); } - when 'EQUALS' { - ruleCriteriaMet = logEntryFieldValue == ruleComparisonValue; + } + Map userIdToAuthSession = LoggerEngineDataSelector.getInstance().getAuthSessions(userIds); + for (Log__c log : logs) { + if (log.LoggedBy__c == null) { + continue; } - when 'MATCHES_REGEX' { - ruleCriteriaMet = Pattern.compile(ruleComparisonValue).matcher(logEntryFieldValue).matches(); + + AuthSession matchingSession = userIdToAuthSession.get(log.LoggedBy__c); + log.LoginApplication__c = matchingSession?.LoginHistory.Application; + log.LoginBrowser__c = matchingSession?.LoginHistory.Browser; + log.LoginHistoryId__c = matchingSession?.LoginHistoryId; + log.LoginPlatform__c = matchingSession?.LoginHistory.Platform; + log.LoginType__c = matchingSession?.LoginType; + log.LogoutUrl__c = matchingSession?.LogoutUrl; + log.SessionId__c = matchingSession?.Id; + log.SessionSecurityLevel__c = matchingSession?.SessionSecurityLevel; + log.SessionType__c = matchingSession?.SessionType; + log.SourceIp__c = matchingSession?.SourceIp; + } + } + + private static void setQueriedNetworkDetails(List logs) { + if (LoggerParameter.QUERY_NETWORK_DATA_SYNCHRONOUSLY == true) { + return; + } + + List networkIds = new List(); + for (Log__c log : logs) { + if (log.NetworkId__c != null && log.NetworkId__c instanceof Id) { + networkIds.add(log.NetworkId__c); } - when 'STARTS_WITH' { - ruleCriteriaMet = logEntryFieldValue.startsWith(ruleComparisonValue); + } + + if (networkIds.isEmpty() == true) { + return; + } + + Map networkIdToNetwork = LoggerEngineDataSelector.getInstance().getNetworks(networkIds); + for (Log__c log : logs) { + if (log.NetworkId__c == null) { + continue; + } + + SObject matchingNetwork = networkIdToNetwork.get(log.NetworkId__c); + if (matchingNetwork == null) { + continue; } + + log.NetworkName__c = (String) matchingNetwork.get('Name'); + log.NetworkLoginUrl__c = Network.getLoginUrl(matchingNetwork.Id); + log.NetworkLogoutUrl__c = Network.getLogoutUrl(matchingNetwork.Id); + log.NetworkSelfRegistrationUrl__c = Network.getSelfRegUrl(matchingNetwork.Id); + log.NetworkUrlPathPrefix__c = (String) matchingNetwork.get('UrlPathPrefix'); } + } - return ruleCriteriaMet; + private static void setQueriedOrganizationDetails(List logs) { + if (LoggerParameter.QUERY_ORGANIZATION_DATA_SYNCHRONOUSLY == true) { + return; + } + + Organization cachedOrganization = LoggerEngineDataSelector.getInstance().getCachedOrganization(); + + if (cachedOrganization == null) { + return; + } + + for (Log__c log : logs) { + log.OrganizationId__c = cachedOrganization.Id; + log.OrganizationInstanceName__c = cachedOrganization.InstanceName; + log.OrganizationName__c = cachedOrganization.Name; + log.OrganizationNamespacePrefix__c = cachedOrganization.NamespacePrefix; + log.OrganizationType__c = cachedOrganization.OrganizationType; + } } - private static Log__c getRecentLogWithApiReleaseDetails() { - // Query for recent logs created only today - the status API should be called - // at least once per day to make sure that status details are still accurate. - // This query should make a callout approximately every 4 hours. - Datetime fourHoursAgo = System.now().addMinutes(-4 * 60); + private static void setQueriedUserDetails(List logs) { + if (LoggerParameter.QUERY_USER_DATA_SYNCHRONOUSLY == true) { + return; + } - List logs = [ - SELECT Id, ApiReleaseNumber__c, ApiReleaseVersion__c - FROM Log__c - WHERE CreatedDate >= :fourHoursAgo AND CreatedDate = TODAY AND ApiReleaseNumber__c != NULL - ORDER BY StartTime__c DESC - LIMIT 1 - ]; + List userIds = new List(); + for (Log__c log : logs) { + if (log.LoggedBy__c != null) { + userIds.add(log.LoggedBy__c); + } + } - return logs.isEmpty() == true ? null : logs.get(0); + Map userIdToUser = LoggerEngineDataSelector.getInstance().getUsers(userIds); + for (Log__c log : logs) { + if (log.LoggedBy__c == null) { + continue; + } + + User matchingUser = userIdToUser.get(log.LoggedBy__c); + log.LoggedByUsername__c = matchingUser.Username; + log.ProfileName__c = matchingUser.Profile.Name; + log.UserLicenseDefinitionKey__c = matchingUser.Profile.UserLicense.LicenseDefinitionKey; + log.UserLicenseId__c = matchingUser.Profile.UserLicenseId; + log.UserLicenseName__c = matchingUser.Profile.UserLicense.Name; + log.UserRoleName__c = matchingUser.UserRole?.Name; + } } @future(callout=true) diff --git a/nebula-logger/core/main/log-management/classes/LogEntryHandler.cls b/nebula-logger/core/main/log-management/classes/LogEntryHandler.cls index 88f1dd1e1..2c50aebd4 100644 --- a/nebula-logger/core/main/log-management/classes/LogEntryHandler.cls +++ b/nebula-logger/core/main/log-management/classes/LogEntryHandler.cls @@ -76,6 +76,10 @@ public without sharing class LogEntryHandler extends LoggerSObjectHandler { classNameToApexClass.put(apexClass.Name, apexClass); } + if (classNameToApexClass.isEmpty() == true) { + return; + } + for (LogEntry__c logEntry : apexLogEntries) { String topLevelApexClassName = logEntry.OriginLocation__c?.substringBefore('.'); ApexClass topLevelApexClass = classNameToApexClass.get(topLevelApexClassName); @@ -129,6 +133,10 @@ public without sharing class LogEntryHandler extends LoggerSObjectHandler { flowApiNameToDefinition.put(flowDefinition.ApiName, flowDefinition); } + if (flowApiNameToDefinition.isEmpty() == true) { + return; + } + for (LogEntry__c logEntry : flowLogEntries) { FlowDefinitionView flowDefinition = flowApiNameToDefinition.get(logEntry.OriginLocation__c); if (flowDefinition == null) { @@ -160,16 +168,20 @@ public without sharing class LogEntryHandler extends LoggerSObjectHandler { return; } - Map flowDurableId = new Map(); + Map flowDurableIdToFlowVersionView = new Map(); for (FlowVersionView flowVersionView : LogManagementDataSelector.getInstance().getFlowVersionViewsByDurableId(flowActiveVersionIds)) { // Filtering on Status in SOQL seems to always return 0 results, so filter in code instead if (flowVersionView.Status == 'Active') { - flowDurableId.put(flowVersionView.FlowDefinitionViewId, flowVersionView); + flowDurableIdToFlowVersionView.put(flowVersionView.FlowDefinitionViewId, flowVersionView); } } + if (flowDurableIdToFlowVersionView.isEmpty() == true) { + return; + } + for (LogEntry__c logEntry : flowLogEntries) { - FlowVersionView flowVersionView = flowDurableId.get(logEntry.FlowDurableId__c); + FlowVersionView flowVersionView = flowDurableIdToFlowVersionView.get(logEntry.FlowDurableId__c); if (flowVersionView == null) { continue; } @@ -182,6 +194,10 @@ public without sharing class LogEntryHandler extends LoggerSObjectHandler { @SuppressWarnings('PMD.OperationWithLimitsInLoop') private void setRecordNames() { + if (LoggerParameter.QUERY_RELATED_RECORD_DATA == false) { + return; + } + // Assumption - only valid record IDs will be populated in LogEntry__c.RecordId__c // If that changes, then extra checks may be needed before casting to Id, using getSObjectType(), etc. // TODO: This method is pretty long & it's doing multiple things - consider breaking it up into separate methods or an inner class diff --git a/nebula-logger/core/main/log-management/classes/LogManagementDataSelector.cls b/nebula-logger/core/main/log-management/classes/LogManagementDataSelector.cls index 6eb94a8b4..70b957837 100644 --- a/nebula-logger/core/main/log-management/classes/LogManagementDataSelector.cls +++ b/nebula-logger/core/main/log-management/classes/LogManagementDataSelector.cls @@ -8,10 +8,11 @@ * @description Selector class used for all queries that are specific to the log management layer */ @SuppressWarnings('PMD.ApexCrudViolation, PMD.ExcessivePublicCount') -public without sharing class LogManagementDataSelector { - private static final LogManagementDataSelector INSTANCE = new LogManagementDataSelector(); +public without sharing virtual class LogManagementDataSelector { + private static LogManagementDataSelector instance = new LogManagementDataSelector(); @SuppressWarnings('PMD.EmptyStatementBlock') + @TestVisible private LogManagementDataSelector() { } @@ -20,7 +21,7 @@ public without sharing class LogManagementDataSelector { * @return The singleton instance of `LogManagementDataSelector` */ public static LogManagementDataSelector getInstance() { - return INSTANCE; + return instance; } /** @@ -29,7 +30,7 @@ public without sharing class LogManagementDataSelector { * @param fieldNames `Set` API names of any fields to include in the query * @return `List` containing any records in the specified `SObjectType` */ - public List getAll(Schema.SObjectType sobjectType, Set fieldNames) { + public virtual List getAll(Schema.SObjectType sobjectType, Set fieldNames) { String query = String.format('SELECT {0} FROM {1}', new List{ String.join(new List(fieldNames), ', '), sobjectType }); return Database.query(String.escapeSingleQuotes(query)); } @@ -42,7 +43,7 @@ public without sharing class LogManagementDataSelector { * @param recordIds `List` of record IDs to include in the query results * @return `List` containing any matching records in the specified `SObjectType` */ - public List getById(Schema.SObjectType sobjectType, Set fieldNames, List recordIds) { + public virtual List getById(Schema.SObjectType sobjectType, Set fieldNames, List recordIds) { String query = String.format( 'SELECT {0} FROM {1} WHERE Id IN :recordIds', new List{ String.join(new List(fieldNames), ', '), sobjectType } @@ -55,7 +56,11 @@ public without sharing class LogManagementDataSelector { * @param apexClassNames The names of the Apex classes to query * @return `List` containing any matching records */ - public List getApexClasses(List apexClassNames) { + public virtual List getApexClasses(List apexClassNames) { + if (LoggerParameter.QUERY_APEX_CLASS_DATA == false) { + return new List(); + } + return [ SELECT ApiVersion, CreatedById, CreatedDate, Id, LastModifiedById, LastModifiedDate, Name FROM ApexClass @@ -68,17 +73,45 @@ public without sharing class LogManagementDataSelector { * @description Returns a cached copy of the `ApexEmailNotification` records in the org * @return The cached `List` records */ - public List getCachedApexEmailNotifications() { - String cacheKeyName = 'ApexEmailNotifications'; - if (LoggerCache.getTransactionCache().contains(cacheKeyName) == true) { - return (List) LoggerCache.getTransactionCache().get(cacheKeyName); + public virtual List getCachedApexEmailNotifications() { + String cacheKey = 'ApexEmailNotifications'; + if (LoggerCache.getOrganizationCache().contains(cacheKey) == true) { + return (List) LoggerCache.getOrganizationCache().get(cacheKey); } List apexEmailNotifications = [SELECT Email, UserId FROM ApexEmailNotification WHERE Email != NULL OR User.IsActive = TRUE]; - LoggerCache.getTransactionCache().put(cacheKeyName, apexEmailNotifications); + LoggerCache.getOrganizationCache().put(cacheKey, apexEmailNotifications); return apexEmailNotifications; } + /** + * @description Returns a cached `Log__c` record that has been created within the last 4 hours + * that has API details populated from calling https://api.status.salesforce.com + * @return The cached `Log__c` record, or `null` if no match is found + */ + public Log__c getCachedRecentLogWithApiReleaseDetails() { + String cacheKey = 'RecentLogWithApiReleaseDetails'; + if (LoggerCache.getOrganizationCache().contains(cacheKey) == true) { + return (Log__c) LoggerCache.getOrganizationCache().get(cacheKey); + } + + // Query for recent logs created only today - the status API should be called + // at least once per day to make sure that status details are still accurate. + // This query should make a callout approximately every 4 hours. + Datetime fourHoursAgo = System.now().addMinutes(-4 * 60); + + List logs = [ + SELECT Id, ApiReleaseNumber__c, ApiReleaseVersion__c + FROM Log__c + WHERE CreatedDate >= :fourHoursAgo AND CreatedDate = TODAY AND ApiReleaseNumber__c != NULL + ORDER BY CreatedDate DESC + LIMIT 1 + ]; + Log__c log = logs.isEmpty() == true ? null : logs.get(0); + LoggerCache.getOrganizationCache().put(cacheKey, log); + return log; + } + /** * @description Returns the count of `AsyncApexJob` records with the specified Apex class name, method name & job status * @param apexClassName The fully-qualified name of the Apex class associated with `AsyncApexJob` @@ -86,7 +119,7 @@ public without sharing class LogManagementDataSelector { * @param jobStatuses The list of job statuses that should be used to filter `AsynxApexJob` records * @return The `Integer` count of matching `AsynxApexJob` records */ - public Integer getCountOfAsyncApexJobs(String apexClassName, String apexMethodName, List jobStatuses) { + public virtual Integer getCountOfAsyncApexJobs(String apexClassName, String apexMethodName, List jobStatuses) { return [SELECT COUNT() FROM AsyncApexJob WHERE ApexClass.Name = :apexClassName AND MethodName = :apexMethodName AND Status IN :jobStatuses]; } @@ -95,7 +128,7 @@ public without sharing class LogManagementDataSelector { * @param recordId The `ID` to use for filtering `LogEntry__c` records * @return The `Integer` count of matching `LogEntry__c` records */ - public Integer getCountOfRelatedRecordLogEntries(Id recordId) { + public virtual Integer getCountOfRelatedRecordLogEntries(Id recordId) { return [SELECT COUNT() FROM LogEntry__c WHERE RecordId__c = :recordId]; } @@ -104,7 +137,7 @@ public without sharing class LogManagementDataSelector { * @param recordIds The list of `ID` for records to be deleted * @return The matching `List` records */ - public List getDeleteableUserRecordAccess(List recordIds) { + public virtual List getDeleteableUserRecordAccess(List recordIds) { return [SELECT RecordId FROM UserRecordAccess WHERE UserId = :UserInfo.getUserId() AND RecordId IN :recordIds AND HasDeleteAccess = TRUE]; } @@ -113,7 +146,11 @@ public without sharing class LogManagementDataSelector { * @param flowApiNames The names of the Apex classes to query * @return `List` containing any matching records */ - public List getFlowDefinitionViewsByFlowApiName(List flowApiNames) { + public virtual List getFlowDefinitionViewsByFlowApiName(List flowApiNames) { + if (LoggerParameter.QUERY_FLOW_DEFINITION_VIEW_DATA == false) { + return new List(); + } + return [ SELECT ActiveVersionId, @@ -136,7 +173,7 @@ public without sharing class LogManagementDataSelector { * @param durableIds The durable IDs of the Flows to query * @return `List` containing any matching records */ - public List getFlowVersionViewsByDurableId(List durableIds) { + public virtual List getFlowVersionViewsByDurableId(List durableIds) { return [SELECT ApiVersionRuntime, FlowDefinitionViewId, RunInMode, Status, VersionNumber FROM FlowVersionView WHERE DurableId IN :durableIds]; } @@ -145,7 +182,7 @@ public without sharing class LogManagementDataSelector { * @param logId The `ID` of the `Log__c` record to query * @return The matching `Log__c` record */ - public Log__c getLogById(Id logId) { + public virtual Log__c getLogById(Id logId) { List logFieldNames = new List(Schema.Log__c.SObjectType.getDescribe().fields.getMap().keySet()); logFieldNames.addAll(new List{ 'Owner.Name', 'Owner.Type' }); List logEntryFieldNames = new List(Schema.LogEntry__c.SObjectType.getDescribe().fields.getMap().keySet()); @@ -166,7 +203,7 @@ public without sharing class LogManagementDataSelector { * @param logIds The list of `ID` of the `Log__c` records to query * @return The list of matching `Log__c` records */ - public List getLogsById(List logIds) { + public virtual List getLogsById(List logIds) { return [SELECT Id, Name, LoggedBy__c, LoggedBy__r.Name, StartTime__c, TotalLogEntries__c, TransactionId__c FROM Log__c WHERE Id IN :logIds]; } @@ -175,7 +212,7 @@ public without sharing class LogManagementDataSelector { * @param transactionIds The list of `String` transaction IDs of the `Log__c` records to query * @return The list of matching `Log__c` records */ - public List getLogsByTransactionId(List transactionIds) { + public virtual List getLogsByTransactionId(List transactionIds) { return [SELECT Id, TransactionId__c FROM Log__c WHERE TransactionId__c IN :transactionIds]; } @@ -184,7 +221,7 @@ public without sharing class LogManagementDataSelector { * @param logScenarioIds The list of `ID` of the `Log__c` records to query * @return The list of matching `LoggerScenario__c` records */ - public List getLoggerScenariosById(List logScenarioIds) { + public virtual List getLoggerScenariosById(List logScenarioIds) { return [SELECT Id, OwnerId, UniqueId__c FROM LoggerScenario__c WHERE Id IN :logScenarioIds]; } @@ -193,8 +230,8 @@ public without sharing class LogManagementDataSelector { * @param profileIds The list of `ID` of the `Profile` records to query * @return The list of matching `Profile` records */ - public List getProfilesById(List profileIds) { - return [SELECT Id, Name FROM Profile WHERE Id IN :profileIds ORDER BY Name]; + public virtual List getProfilesById(List profileIds) { + return [SELECT Id, Name FROM Profile WHERE Id IN :profileIds]; } /** @@ -202,8 +239,8 @@ public without sharing class LogManagementDataSelector { * @param searchTerm The `String` search term to use for searching `Profile` records * @return The list of matching `Profile` records */ - public List getProfilesByNameSearch(String searchTerm) { - return [SELECT Id, Name, UserLicense.Name FROM Profile WHERE Name LIKE :searchTerm ORDER BY Name]; + public virtual List getProfilesByNameSearch(String searchTerm) { + return [SELECT Id, Name, UserLicense.Name FROM Profile WHERE Name LIKE :searchTerm]; } /** @@ -211,7 +248,7 @@ public without sharing class LogManagementDataSelector { * @param queueDeveloperNames The list of `String` queue developer names to query * @return The list of matching `Group` records */ - public List getQueuesByDeveloperName(List queueDeveloperNames) { + public virtual List getQueuesByDeveloperName(List queueDeveloperNames) { return [SELECT Id, DeveloperName FROM Group WHERE Type = 'Queue' AND DeveloperName IN :queueDeveloperNames]; } @@ -243,7 +280,7 @@ public without sharing class LogManagementDataSelector { * @param tagNames The set of `String` tag names to query * @return The list of matching `LoggerTag__c` records */ - public List getTagsByName(Set tagNames) { + public virtual List getTagsByName(Set tagNames) { return [SELECT Id, Name FROM LoggerTag__c WHERE Name IN :tagNames]; } @@ -252,7 +289,7 @@ public without sharing class LogManagementDataSelector { * @param topicNames The set of `String` topic names to query * @return The list of matching `Topic` records */ - public List getTopicsByName(Set topicNames) { + public virtual List getTopicsByName(Set topicNames) { return [SELECT Id, Name FROM Topic WHERE Name IN :topicNames]; } @@ -261,8 +298,8 @@ public without sharing class LogManagementDataSelector { * @param userIds The list of `ID` of the `User` records to query * @return The list of matching `User` records */ - public List getUsersById(List userIds) { - return [SELECT Id, Username FROM User WHERE Id IN :userIds ORDER BY Username]; + public virtual List getUsersById(List userIds) { + return [SELECT Id, Username FROM User WHERE Id IN :userIds]; } /** @@ -270,8 +307,8 @@ public without sharing class LogManagementDataSelector { * @param searchTerm The `String` search term to use for searching `User` records * @return The list of matching `User` records */ - public List getUsersByNameSearch(String searchTerm) { - return [SELECT Id, Name, Username, SmallPhotoUrl FROM User WHERE Name LIKE :searchTerm OR Username LIKE :searchTerm ORDER BY Username]; + public virtual List getUsersByNameSearch(String searchTerm) { + return [SELECT Id, Name, Username, SmallPhotoUrl FROM User WHERE Name LIKE :searchTerm OR Username LIKE :searchTerm]; } /** @@ -279,7 +316,12 @@ public without sharing class LogManagementDataSelector { * @param usernames The list of `String` user usernames to query * @return Tje list of matching `User` records */ - public List getUsersByUsername(List usernames) { - return [SELECT Id, Username FROM User WHERE Username IN :userNames ORDER BY Username]; + public virtual List getUsersByUsername(List usernames) { + return [SELECT Id, Username FROM User WHERE Username IN :userNames]; + } + + @TestVisible + private static void setMock(LogManagementDataSelector mockSelectorInstance) { + instance = mockSelectorInstance; } } diff --git a/nebula-logger/core/main/log-management/flexipages/LogRecordPage.flexipage-meta.xml b/nebula-logger/core/main/log-management/flexipages/LogRecordPage.flexipage-meta.xml index d3e16ba8a..8fad3349a 100644 --- a/nebula-logger/core/main/log-management/flexipages/LogRecordPage.flexipage-meta.xml +++ b/nebula-logger/core/main/log-management/flexipages/LogRecordPage.flexipage-meta.xml @@ -108,6 +108,22 @@ RecordTransactionId__cField + + + + uiBehavior + readonly + + Record.RequestId__c + RecordRequestId_cField + + + {!Record.RequestId__c} + NE + + + + diff --git a/nebula-logger/core/main/log-management/layouts/Log__c-Log Layout.layout-meta.xml b/nebula-logger/core/main/log-management/layouts/Log__c-Log Layout.layout-meta.xml index ee61a07f3..4fd75a39e 100644 --- a/nebula-logger/core/main/log-management/layouts/Log__c-Log Layout.layout-meta.xml +++ b/nebula-logger/core/main/log-management/layouts/Log__c-Log Layout.layout-meta.xml @@ -15,6 +15,10 @@ Readonly TransactionId__c + + Readonly + RequestId__c + Readonly StartTime__c diff --git a/nebula-logger/core/main/log-management/objects/Log__c/fields/RequestId__c.field-meta.xml b/nebula-logger/core/main/log-management/objects/Log__c/fields/RequestId__c.field-meta.xml new file mode 100644 index 000000000..4b752b33a --- /dev/null +++ b/nebula-logger/core/main/log-management/objects/Log__c/fields/RequestId__c.field-meta.xml @@ -0,0 +1,18 @@ + + + RequestId__c + Active + false + None + true + The value returned when calling System.Request.getCurrent().getRequestId() + + 36 + false + Confidential + false + false + false + Text + false + diff --git a/nebula-logger/core/main/log-management/permissionsets/LoggerAdmin.permissionset-meta.xml b/nebula-logger/core/main/log-management/permissionsets/LoggerAdmin.permissionset-meta.xml index b47fec9bb..54bd85524 100644 --- a/nebula-logger/core/main/log-management/permissionsets/LoggerAdmin.permissionset-meta.xml +++ b/nebula-logger/core/main/log-management/permissionsets/LoggerAdmin.permissionset-meta.xml @@ -980,6 +980,11 @@ Log__c.ProfileName__c true + + false + Log__c.RequestId__c + true + true Log__c.Scenario__c diff --git a/nebula-logger/core/main/log-management/permissionsets/LoggerEndUser.permissionset-meta.xml b/nebula-logger/core/main/log-management/permissionsets/LoggerEndUser.permissionset-meta.xml index 2bc396df0..9bf223e4b 100644 --- a/nebula-logger/core/main/log-management/permissionsets/LoggerEndUser.permissionset-meta.xml +++ b/nebula-logger/core/main/log-management/permissionsets/LoggerEndUser.permissionset-meta.xml @@ -788,6 +788,11 @@ Log__c.ProfileName__c true + + false + Log__c.RequestId__c + true + false Log__c.Scenario__c diff --git a/nebula-logger/core/main/log-management/permissionsets/LoggerLogViewer.permissionset-meta.xml b/nebula-logger/core/main/log-management/permissionsets/LoggerLogViewer.permissionset-meta.xml index 41d514060..5a46d05ab 100644 --- a/nebula-logger/core/main/log-management/permissionsets/LoggerLogViewer.permissionset-meta.xml +++ b/nebula-logger/core/main/log-management/permissionsets/LoggerLogViewer.permissionset-meta.xml @@ -904,6 +904,11 @@ Log__c.ProfileName__c true + + false + Log__c.RequestId__c + true + false Log__c.Scenario__c diff --git a/nebula-logger/core/main/logger-engine/classes/LogEntryEventBuilder.cls b/nebula-logger/core/main/logger-engine/classes/LogEntryEventBuilder.cls index 49ac77ded..18aeacf7b 100644 --- a/nebula-logger/core/main/logger-engine/classes/LogEntryEventBuilder.cls +++ b/nebula-logger/core/main/logger-engine/classes/LogEntryEventBuilder.cls @@ -12,10 +12,12 @@ 'PMD.AvoidGlobalModifier, PMD.CognitiveComplexity, PMD.CyclomaticComplexity, PMD.ExcessiveClassLength, PMD.NcssTypeCount, PMD.PropertyNamingConventions, PMD.StdCyclomaticComplexity' ) global with sharing class LogEntryEventBuilder { - private static final String API_VERSION = getApiVersion(); - private static final String NAMESPACE_PREFIX = getNamespacePrefix(); - private static final Map SOBJECT_NAME_TO_CLASSIFICATION = new Map(); - private static final Map SOBJECT_SUFFIX_TO_CLASSIFICATION = getSObjectSuffixToClassification(); + private static final Map CACHED_SOBJECT_NAME_TO_CLASSIFICATION = new Map(); + private static final User CURRENT_USER = new User(Id = UserInfo.getUserId(), ProfileId = UserInfo.getProfileId()); + private static final String NEW_LINE_DELIMITER = '\n'; + + @TestVisible + private static Id networkId = System.Network.getNetworkId(); private final LogEntryEvent__e logEntryEvent; private final LoggingLevel entryLoggingLevel; @@ -49,7 +51,46 @@ global with sharing class LogEntryEventBuilder { set; } - @SuppressWarnings('PMD.ExcessiveParameterList, PMD.NcssConstructorCount') + private static final LogEntryEvent__e LOG_ENTRY_EVENT_TEMPLATE { + get { + if (LOG_ENTRY_EVENT_TEMPLATE == null) { + LOG_ENTRY_EVENT_TEMPLATE = getLogEntryEventTemplate(); + } + return LOG_ENTRY_EVENT_TEMPLATE.clone(); + } + set; + } + + private static final String NAMESPACE_PREFIX { + get { + if (NAMESPACE_PREFIX == null) { + NAMESPACE_PREFIX = getNamespacePrefix(); + } + return NAMESPACE_PREFIX; + } + set; + } + + private static final String ORGANIZATION_API_VERSION { + get { + if (ORGANIZATION_API_VERSION == null) { + ORGANIZATION_API_VERSION = getOrganizationApiVersion(); + } + return ORGANIZATION_API_VERSION; + } + set; + } + + private static final Map SOBJECT_SUFFIX_TO_CLASSIFICATION { + get { + if (SOBJECT_SUFFIX_TO_CLASSIFICATION == null) { + SOBJECT_SUFFIX_TO_CLASSIFICATION = getSObjectSuffixToClassification(); + } + return SOBJECT_SUFFIX_TO_CLASSIFICATION; + } + set; + } + /** * @description Used by `Logger` to instantiate a new instance of `LogEntryEventBuilder` * @param userSettings The instance of `LoggerSettings__c` for the current to use to control any feature flags @@ -57,18 +98,19 @@ global with sharing class LogEntryEventBuilder { * @param shouldSave Indicates if the builder's instance of `LogEntryEvent__e` should be saved * @param ignoredOrigins A `Set` of the names of any Apex classes that should be ignored when parsing the entry's origin */ + @SuppressWarnings('PMD.ExcessiveParameterList, PMD.NcssConstructorCount') public LogEntryEventBuilder(LoggerSettings__c userSettings, LoggingLevel entryLoggingLevel, Boolean shouldSave, Set ignoredOrigins) { + this.shouldSave = shouldSave; + if (this.shouldSave() == false) { + return; + } + this.userSettings = userSettings; this.entryLoggingLevel = entryLoggingLevel; - this.shouldSave = shouldSave; if (ignoredOrigins != null) { this.ignoredApexClasses.addAll(ignoredOrigins); } - if (this.shouldSave == false) { - return; - } - Datetime timestamp = System.now(); this.logEntryEvent = new LogEntryEvent__e( EpochTimestamp__c = timestamp.getTime(), @@ -97,8 +139,7 @@ global with sharing class LogEntryEventBuilder { TriggerSObjectType__c = Trigger.new?.getSObjectType().getDescribe().getName() ); - DmlException stackTraceException = new DmlException(); - this.parseStackTrace(stackTraceException.getStackTraceString()); + this.parseStackTrace(); } /** @@ -107,7 +148,7 @@ global with sharing class LogEntryEventBuilder { * @return The same instance of `LogEntryEventBuilder`, useful for chaining methods */ global LogEntryEventBuilder setMessage(LogMessage logMessage) { - if (this.shouldSave == false && System.Test.isRunningTest() == false || logMessage == null) { + if (this.shouldSave() == false && System.Test.isRunningTest() == false || logMessage == null) { return this; } @@ -121,7 +162,7 @@ global with sharing class LogEntryEventBuilder { */ global LogEntryEventBuilder setMessage(String message) { // To help with debugging unit tests, always run System.debug statement in a test context - if ((this.shouldSave == true || System.Test.isRunningTest() == true) && this.logEntryEvent != null) { + if ((this.shouldSave() == true || System.Test.isRunningTest() == true) && this.logEntryEvent != null) { String cleanedMessage = applyDataMaskRules(message); Boolean messageMasked = cleanedMessage != message; String truncatedMessage = truncateFieldValue(Schema.LogEntryEvent__e.Message__c, cleanedMessage); @@ -142,7 +183,7 @@ global with sharing class LogEntryEventBuilder { * @return The same instance of `LogEntryEventBuilder`, useful for chaining methods */ global LogEntryEventBuilder setExceptionDetails(Exception apexException) { - if (this.shouldSave == false) { + if (this.shouldSave() == false) { return this; } @@ -150,7 +191,7 @@ global with sharing class LogEntryEventBuilder { this.logEntryEvent.ExceptionType__c = apexException.getTypeName(); // Stack traces are not returned for managed packages - if (isValidStackTrace(apexException.getStackTraceString())) { + if (isValidStackTrace(apexException.getStackTraceString()) == true) { this.logEntryEvent.ExceptionStackTrace__c = apexException.getStackTraceString(); } @@ -165,7 +206,7 @@ global with sharing class LogEntryEventBuilder { * @return The same instance of `LogEntryEventBuilder`, useful for chaining methods */ global LogEntryEventBuilder setDatabaseResult(Database.DeleteResult deleteResult) { - if (this.shouldSave == false) { + if (this.shouldSave() == false) { return this; } @@ -183,7 +224,7 @@ global with sharing class LogEntryEventBuilder { * @return The same instance of `LogEntryEventBuilder`, useful for chaining methods */ global LogEntryEventBuilder setDatabaseResult(Database.MergeResult mergeResult) { - if (this.shouldSave == false) { + if (this.shouldSave() == false) { return this; } @@ -201,7 +242,7 @@ global with sharing class LogEntryEventBuilder { * @return The same instance of `LogEntryEventBuilder`, useful for chaining methods */ global LogEntryEventBuilder setDatabaseResult(Database.SaveResult saveResult) { - if (this.shouldSave == false) { + if (this.shouldSave() == false) { return this; } @@ -219,7 +260,7 @@ global with sharing class LogEntryEventBuilder { * @return The same instance of `LogEntryEventBuilder`, useful for chaining methods */ global LogEntryEventBuilder setDatabaseResult(Database.UpsertResult upsertResult) { - if (this.shouldSave == false) { + if (this.shouldSave() == false) { return this; } @@ -240,7 +281,7 @@ global with sharing class LogEntryEventBuilder { * @return The same instance of `LogEntryEventBuilder`, useful for chaining methods */ global LogEntryEventBuilder setDatabaseResult(Database.UndeleteResult undeleteResult) { - if (this.shouldSave == false) { + if (this.shouldSave() == false) { return this; } @@ -258,7 +299,7 @@ global with sharing class LogEntryEventBuilder { * @return The same instance of `LogEntryEventBuilder`, useful for chaining methods */ global LogEntryEventBuilder setDatabaseResult(List deleteResults) { - if (this.shouldSave == false) { + if (this.shouldSave() == false) { return this; } @@ -276,7 +317,7 @@ global with sharing class LogEntryEventBuilder { * @return The same instance of `LogEntryEventBuilder`, useful for chaining methods */ global LogEntryEventBuilder setDatabaseResult(List mergeResults) { - if (this.shouldSave == false) { + if (this.shouldSave() == false) { return this; } @@ -294,7 +335,7 @@ global with sharing class LogEntryEventBuilder { * @return The same instance of `LogEntryEventBuilder`, useful for chaining methods */ global LogEntryEventBuilder setDatabaseResult(List saveResults) { - if (this.shouldSave == false) { + if (this.shouldSave() == false) { return this; } @@ -312,7 +353,7 @@ global with sharing class LogEntryEventBuilder { * @return The same instance of `LogEntryEventBuilder`, useful for chaining methods */ global LogEntryEventBuilder setDatabaseResult(List upsertResults) { - if (this.shouldSave == false) { + if (this.shouldSave() == false) { return this; } @@ -330,7 +371,7 @@ global with sharing class LogEntryEventBuilder { * @return The same instance of `LogEntryEventBuilder`, useful for chaining methods */ global LogEntryEventBuilder setDatabaseResult(List undeleteResults) { - if (this.shouldSave == false) { + if (this.shouldSave() == false) { return this; } @@ -366,7 +407,7 @@ global with sharing class LogEntryEventBuilder { * @return The same instance of `LogEntryEventBuilder`, useful for chaining methods */ global LogEntryEventBuilder setRecord(Id recordId) { - if (this.shouldSave == false || String.isBlank(recordId)) { + if (this.shouldSave() == false || String.isBlank(recordId)) { return this; } @@ -384,14 +425,14 @@ global with sharing class LogEntryEventBuilder { * @return The same instance of `LogEntryEventBuilder`, useful for chaining methods */ global LogEntryEventBuilder setRecord(SObject record) { - if (this.shouldSave == false) { + if (this.shouldSave() == false) { return this; } this.logEntryEvent.RecordCollectionSize__c = 1; this.logEntryEvent.RecordCollectionType__c = 'Single'; - String recordJson = getJson(record); + String recordJson = getJson(record, userSettings.IsRecordFieldStrippingEnabled__c); if (record == null) { this.logEntryEvent.RecordJson__c = recordJson; this.logEntryEvent.RecordSObjectClassification__c = 'Unknown'; @@ -417,7 +458,7 @@ global with sharing class LogEntryEventBuilder { * @return The same instance of `LogEntryEventBuilder`, useful for chaining methods */ global LogEntryEventBuilder setRecord(List records) { - if (this.shouldSave == false) { + if (this.shouldSave() == false) { return this; } @@ -425,7 +466,7 @@ global with sharing class LogEntryEventBuilder { this.logEntryEvent.RecordCollectionType__c = 'List'; Schema.SObjectType sobjectType = records?.getSObjectType(); - String recordJson = getJson(records); + String recordJson = getJson(records, userSettings.IsRecordFieldStrippingEnabled__c); if (sobjectType == null) { this.logEntryEvent.RecordJson__c = recordJson; this.logEntryEvent.RecordSObjectClassification__c = 'Unknown'; @@ -450,7 +491,7 @@ global with sharing class LogEntryEventBuilder { * @return The same instance of `LogEntryEventBuilder`, useful for chaining methods */ global LogEntryEventBuilder setHttpRequestDetails(HttpRequest request) { - if (this.shouldSave == false || request == null) { + if (this.shouldSave() == false || request == null) { return this; } @@ -471,7 +512,7 @@ global with sharing class LogEntryEventBuilder { * @return The same instance of `LogEntryEventBuilder`, useful for chaining methods */ global LogEntryEventBuilder setHttpResponseDetails(HttpResponse response) { - if (this.shouldSave == false || response == null) { + if (this.shouldSave() == false || response == null) { return this; } @@ -480,7 +521,7 @@ global with sharing class LogEntryEventBuilder { this.logEntryEvent.HttpResponseBody__c = cleanedResponseBody; this.logEntryEvent.HttpResponseBodyMasked__c = responseBodyMasked; - this.logEntryEvent.HttpResponseHeaderKeys__c = String.join(response.getHeaderKeys(), '\n'); + this.logEntryEvent.HttpResponseHeaderKeys__c = String.join(response.getHeaderKeys(), NEW_LINE_DELIMITER); this.logEntryEvent.HttpResponseStatus__c = response.getStatus(); this.logEntryEvent.HttpResponseStatusCode__c = response.getStatusCode(); return this; @@ -492,7 +533,7 @@ global with sharing class LogEntryEventBuilder { * @return The same instance of `LogEntryEventBuilder`, useful for chaining methods */ global LogEntryEventBuilder addTag(String tag) { - if (this.shouldSave == false || String.isBlank(tag)) { + if (this.shouldSave() == false || String.isBlank(tag)) { return this; } @@ -507,7 +548,7 @@ global with sharing class LogEntryEventBuilder { * @return The same instance of `LogEntryEventBuilder`, useful for chaining methods */ global LogEntryEventBuilder addTags(List tags) { - if (tags == null || tags.isEmpty()) { + if (tags == null || tags.isEmpty() == true) { return this; } @@ -534,18 +575,13 @@ global with sharing class LogEntryEventBuilder { */ @SuppressWarnings('PMD.NcssMethodCount') global LogEntryEventBuilder parseStackTrace(String stackTraceString) { - if (this.shouldSave == false) { - return this; - } - - // Stack traces are not returned for managed packages - if (!isValidStackTrace(stackTraceString)) { + if (LoggerParameter.ENABLE_STACK_TRACE_PARSING == false || this.shouldSave() == false || isValidStackTrace(stackTraceString) == false) { return this; } List stackTraceLines = new List(); String previousStackTraceLine; - for (String currentStackTraceLine : stackTraceString.split('\n')) { + for (String currentStackTraceLine : stackTraceString.split(NEW_LINE_DELIMITER)) { // Duplicate lines are sometimes introduced, so skip the current line if it's the same as the previous line if (currentStackTraceLine == previousStackTraceLine) { continue; @@ -571,7 +607,7 @@ global with sharing class LogEntryEventBuilder { } // In a managed package, we can end up with an invalid (unhelpful) stack trace, so only store when valid - stackTraceString = String.join(stackTraceLines, '\n'); + stackTraceString = String.join(stackTraceLines, NEW_LINE_DELIMITER); if (isValidStackTrace(stackTraceString)) { String originLocation = stackTraceLines.get(0); if (originLocation.contains(':')) { @@ -601,18 +637,15 @@ global with sharing class LogEntryEventBuilder { * @return The `LogEntryEvent__e` record */ public LogEntryEvent__e getLogEntryEvent() { - if (this.shouldSave == false) { + if (this.shouldSave() == false) { return null; } // Lazy-loading of some details to help minimize Apex heap size usage until needed if (this.detailsAreSet == false) { - if (Logger.getUserSettings().IsAnonymousModeEnabled__c == false) { - this.setUserDetails(); - this.setUserSessionDetails(); + for (String fieldName : LOG_ENTRY_EVENT_TEMPLATE.getPopulatedFieldsAsMap().keySet()) { + this.logEntryEvent.put(fieldName, LOG_ENTRY_EVENT_TEMPLATE.get(fieldName)); } - this.setNetworkDetails(); - this.setTransactionDetails(); this.detailsAreSet = true; } @@ -637,6 +670,10 @@ global with sharing class LogEntryEventBuilder { @SuppressWarnings('PMD.AvoidDebugStatements') private void logToApexDebug(String message) { + if (Logger.getUserSettings().IsApexSystemDebugLoggingEnabled__c == false) { + return; + } + String template = LoggerParameter.SYSTEM_DEBUG_MESSAGE_FORMAT?.unescapeJava(); if (String.isNotBlank(template)) { List possibleReplacements = template.split('\\{'); @@ -656,24 +693,18 @@ global with sharing class LogEntryEventBuilder { this.debugMessage = message; } - if (Logger.getUserSettings().IsApexSystemDebugLoggingEnabled__c == true) { - // 1 of 2 places in the codebase (except tests) that should use System.debug() - // The rest of the codebase should use a method in Logger.cls - System.debug(this.entryLoggingLevel, this.debugMessage); - } + // 1 of 2 places in the codebase (except tests) that should use System.debug() + // The rest of the codebase should use a method in Logger.cls + System.debug(this.entryLoggingLevel, this.debugMessage); } - private void setNetworkDetails() { - SObject networkRecord = LoggerEngineDataSelector.getInstance().getCachedNetwork(); - if (networkRecord == null) { + private void parseStackTrace() { + if (LoggerParameter.ENABLE_STACK_TRACE_PARSING == false) { return; } - this.logEntryEvent.NetworkName__c = (String) networkRecord.get('Name'); - this.logEntryEvent.NetworkLoginUrl__c = Network.getLoginUrl(networkRecord.Id); - this.logEntryEvent.NetworkLogoutUrl__c = Network.getLogoutUrl(networkRecord.Id); - this.logEntryEvent.NetworkSelfRegistrationUrl__c = Network.getSelfRegUrl(networkRecord.Id); - this.logEntryEvent.NetworkUrlPathPrefix__c = (String) networkRecord.get('UrlPathPrefix'); + DmlException stackTraceException = new DmlException(); + this.parseStackTrace(stackTraceException.getStackTraceString()); } private void setTagsDetails() { @@ -683,13 +714,11 @@ global with sharing class LogEntryEventBuilder { List sortedTags = new List(this.tags); sortedTags.sort(); - this.logEntryEvent.Tags__c = String.escapeSingleQuotes(String.join(sortedTags, '\n')); + this.logEntryEvent.Tags__c = String.escapeSingleQuotes(String.join(sortedTags, NEW_LINE_DELIMITER)); } private void setTransactionDetails() { - Organization cachedOrganization = LoggerEngineDataSelector.getInstance().getCachedOrganization(); - - this.logEntryEvent.ApiVersion__c = API_VERSION; + this.logEntryEvent.ApiVersion__c = ORGANIZATION_API_VERSION; this.logEntryEvent.LimitsAggregateQueriesMax__c = Limits.getLimitAggregateQueries(); this.logEntryEvent.LimitsAsyncCallsMax__c = Limits.getLimitAsyncCalls(); this.logEntryEvent.LimitsCalloutsMax__c = Limits.getLimitCallouts(); @@ -707,50 +736,127 @@ global with sharing class LogEntryEventBuilder { this.logEntryEvent.LimitsSoqlQueryRowsMax__c = Limits.getLimitQueryRows(); this.logEntryEvent.LimitsSoslSearchesMax__c = Limits.getLimitSoslQueries(); this.logEntryEvent.OrganizationDomainUrl__c = Url.getOrgDomainUrl()?.toExternalForm(); - this.logEntryEvent.OrganizationEnvironmentType__c = CACHED_ORGANIZATION_ENVIRONMENT_TYPE; - this.logEntryEvent.OrganizationId__c = cachedOrganization.Id; - this.logEntryEvent.OrganizationInstanceName__c = cachedOrganization.InstanceName; - this.logEntryEvent.OrganizationName__c = cachedOrganization.Name; - this.logEntryEvent.OrganizationNamespacePrefix__c = cachedOrganization.NamespacePrefix; - this.logEntryEvent.OrganizationType__c = cachedOrganization.OrganizationType; } - private void setUserDetails() { - User cachedUser = LoggerEngineDataSelector.getInstance().getCachedUser(); + private static LogEntryEvent__e getLogEntryEventTemplate() { + LogEntryEvent__e templateLogEntryEvent = new LogEntryEvent__e( + ApiVersion__c = ORGANIZATION_API_VERSION, + LimitsAggregateQueriesMax__c = Limits.getLimitAggregateQueries(), + LimitsAsyncCallsMax__c = Limits.getLimitAsyncCalls(), + LimitsCalloutsMax__c = Limits.getLimitCallouts(), + LimitsCpuTimeMax__c = Limits.getLimitCpuTime(), + LimitsDmlRowsMax__c = Limits.getLimitDmlRows(), + LimitsDmlStatementsMax__c = Limits.getLimitDmlStatements(), + LimitsEmailInvocationsMax__c = Limits.getLimitEmailInvocations(), + LimitsFutureCallsMax__c = Limits.getLimitFutureCalls(), + LimitsHeapSizeMax__c = Limits.getLimitHeapSize(), + LimitsMobilePushApexCallsMax__c = Limits.getLimitMobilePushApexCalls(), + LimitsPublishImmediateDmlStatementsMax__c = Limits.getLimitPublishImmediateDML(), + LimitsQueueableJobsMax__c = Limits.getLimitQueueableJobs(), + LimitsSoqlQueriesMax__c = Limits.getLimitQueries(), + LimitsSoqlQueryLocatorRowsMax__c = Limits.getLimitQueryLocatorRows(), + LimitsSoqlQueryRowsMax__c = Limits.getLimitQueryRows(), + LimitsSoslSearchesMax__c = Limits.getLimitSoslQueries(), + OrganizationDomainUrl__c = Url.getOrgDomainUrl()?.toExternalForm() + ); - this.logEntryEvent.LoggedById__c = cachedUser.Id; - this.logEntryEvent.LoggedByUsername__c = cachedUser.Username; - this.logEntryEvent.ProfileName__c = cachedUser.Profile.Name; - this.logEntryEvent.UserLicenseDefinitionKey__c = cachedUser.Profile.UserLicense.LicenseDefinitionKey; - this.logEntryEvent.UserLicenseId__c = cachedUser.Profile.UserLicenseId; - this.logEntryEvent.UserLicenseName__c = cachedUser.Profile.UserLicense.Name; - this.logEntryEvent.UserRoleName__c = cachedUser.UserRole?.Name; + if (Logger.getUserSettings().IsAnonymousModeEnabled__c == false) { + setUserInfoDetails(templateLogEntryEvent); + setQueriedAuthSessionDetails(templateLogEntryEvent); + setQueriedUserDetails(templateLogEntryEvent); + } + setQueriedNetworkDetails(templateLogEntryEvent); + setQueriedOrganizationDetails(templateLogEntryEvent); + + return templateLogEntryEvent; } - private void setUserSessionDetails() { + private static void setQueriedAuthSessionDetails(LogEntryEvent__e logEntryEvent) { + if (LoggerParameter.QUERY_AUTH_SESSION_DATA_SYNCHRONOUSLY == false) { + return; + } + AuthSession cachedAuthSession = LoggerEngineDataSelector.getInstance().getCachedAuthSession(); + if (cachedAuthSession == null) { + return; + } + + logEntryEvent.LoginApplication__c = cachedAuthSession.LoginHistory.Application; + logEntryEvent.LoginBrowser__c = cachedAuthSession.LoginHistory.Browser; + logEntryEvent.LoginHistoryId__c = cachedAuthSession.LoginHistoryId; + logEntryEvent.LoginPlatform__c = cachedAuthSession.LoginHistory.Platform; + logEntryEvent.LoginType__c = cachedAuthSession.LoginType; + logEntryEvent.LogoutUrl__c = cachedAuthSession.LogoutUrl; + logEntryEvent.SessionId__c = cachedAuthSession.Id; + logEntryEvent.SessionSecurityLevel__c = cachedAuthSession.SessionSecurityLevel; + logEntryEvent.SessionType__c = cachedAuthSession.SessionType; + logEntryEvent.SourceIp__c = cachedAuthSession.SourceIp; + } - this.logEntryEvent.Locale__c = UserInfo.getLocale(); - this.logEntryEvent.LoginApplication__c = cachedAuthSession?.LoginHistory.Application; - this.logEntryEvent.LoginBrowser__c = cachedAuthSession?.LoginHistory.Browser; - this.logEntryEvent.LoginHistoryId__c = cachedAuthSession?.LoginHistoryId; - this.logEntryEvent.LoginPlatform__c = cachedAuthSession?.LoginHistory.Platform; - this.logEntryEvent.LoginType__c = cachedAuthSession?.LoginType; - this.logEntryEvent.LogoutUrl__c = cachedAuthSession?.LogoutUrl; - this.logEntryEvent.NetworkId__c = Network.getNetworkId(); - this.logEntryEvent.ProfileId__c = UserInfo.getProfileId(); - this.logEntryEvent.SessionId__c = cachedAuthSession?.Id; - this.logEntryEvent.SessionSecurityLevel__c = cachedAuthSession?.SessionSecurityLevel; - this.logEntryEvent.SessionType__c = cachedAuthSession?.SessionType; - this.logEntryEvent.SourceIp__c = cachedAuthSession?.SourceIp; - this.logEntryEvent.TimeZoneId__c = UserInfo.getTimeZone().getId(); - this.logEntryEvent.TimeZoneName__c = UserInfo.getTimeZone().getDisplayName(); - this.logEntryEvent.UserRoleId__c = UserInfo.getUserRoleId(); - this.logEntryEvent.UserType__c = UserInfo.getUserType(); - - if (this.logEntryEvent.SessionType__c != 'Oauth2') { - this.logEntryEvent.ThemeDisplayed__c = UserInfo.getUiThemeDisplayed(); + private static void setQueriedNetworkDetails(LogEntryEvent__e logEntryEvent) { + if (LoggerParameter.QUERY_NETWORK_DATA_SYNCHRONOUSLY == false || String.isBlank(networkId) == true) { + return; } + + logEntryEvent.NetworkId__c = networkId; + SObject networkRecord = LoggerEngineDataSelector.getInstance().getCachedNetwork(networkId); + if (networkRecord == null) { + return; + } + + logEntryEvent.NetworkLoginUrl__c = System.Network.getLoginUrl(networkRecord.Id); + logEntryEvent.NetworkLogoutUrl__c = System.Network.getLogoutUrl(networkRecord.Id); + logEntryEvent.NetworkName__c = (String) networkRecord.get('Name'); + logEntryEvent.NetworkSelfRegistrationUrl__c = System.Network.getSelfRegUrl(networkRecord.Id); + logEntryEvent.NetworkUrlPathPrefix__c = (String) networkRecord.get('UrlPathPrefix'); + } + + private static void setQueriedOrganizationDetails(LogEntryEvent__e logEntryEvent) { + if (LoggerParameter.QUERY_ORGANIZATION_DATA_SYNCHRONOUSLY == false) { + return; + } + + Organization cachedOrganization = LoggerEngineDataSelector.getInstance().getCachedOrganization(); + if (cachedOrganization == null) { + return; + } + + logEntryEvent.OrganizationEnvironmentType__c = CACHED_ORGANIZATION_ENVIRONMENT_TYPE; + logEntryEvent.OrganizationId__c = cachedOrganization?.Id; + logEntryEvent.OrganizationInstanceName__c = cachedOrganization?.InstanceName; + logEntryEvent.OrganizationName__c = cachedOrganization?.Name; + logEntryEvent.OrganizationNamespacePrefix__c = cachedOrganization?.NamespacePrefix; + logEntryEvent.OrganizationType__c = cachedOrganization?.OrganizationType; + } + + private static void setQueriedUserDetails(LogEntryEvent__e logEntryEvent) { + if (LoggerParameter.QUERY_USER_DATA_SYNCHRONOUSLY == false) { + return; + } + + User cachedUser = LoggerEngineDataSelector.getInstance().getCachedUser(); + + if (cachedUser == null) { + return; + } + + logEntryEvent.LoggedByUsername__c = cachedUser.Username; + logEntryEvent.ProfileName__c = cachedUser.Profile.Name; + logEntryEvent.UserLicenseDefinitionKey__c = cachedUser.Profile.UserLicense.LicenseDefinitionKey; + logEntryEvent.UserLicenseId__c = cachedUser.Profile.UserLicenseId; + logEntryEvent.UserLicenseName__c = cachedUser.Profile.UserLicense.Name; + logEntryEvent.UserRoleName__c = cachedUser.UserRole?.Name; + } + + private static void setUserInfoDetails(LogEntryEvent__e templateLogEntryEvent) { + templateLogEntryEvent.Locale__c = UserInfo.getLocale(); + templateLogEntryEvent.LoggedById__c = UserInfo.getUserId(); + templateLogEntryEvent.ProfileId__c = UserInfo.getProfileId(); + templateLogEntryEvent.ThemeDisplayed__c = UserInfo.getUiThemeDisplayed(); + templateLogEntryEvent.TimeZoneId__c = UserInfo.getTimeZone().getId(); + templateLogEntryEvent.TimeZoneName__c = UserInfo.getTimeZone().getDisplayName(); + templateLogEntryEvent.UserRoleId__c = UserInfo.getUserRoleId(); + templateLogEntryEvent.UserType__c = UserInfo.getUserType(); } // Private static helper methods @@ -772,36 +878,39 @@ global with sharing class LogEntryEventBuilder { return dataInput; } - private static String getApiVersion() { - // Small hack to determine the org's current API version (since Apex doesn't natively provide it) - // Serializing any SObject w/ an ID will include the API version - // So, use UserInfo.getUserId() to create the current user's record without querying - // Then parse the JSON to get the API version - // Expected JSON: {"attributes":{"type":"User","url":"/services/data/v53.0/sobjects/User/005J000000AugnYIAR"} - String userJson = JSON.serialize(new User(Id = UserInfo.getUserId())); - return userJson.substringAfter('/data/').substringBefore('/sobjects/User'); - } - - private static String getJson(SObject record) { - List strippedRecords = stripInaccessible(new List{ record }); + private static String getJson(SObject record, Boolean isRecordFieldStrippingEnabled) { + List records = new List{ record }; + records = isRecordFieldStrippingEnabled == false ? records : stripInaccessible(records); - if (strippedRecords == null) { + if (records == null) { return null; } - SObject strippedRecord = strippedRecords.get(0); + SObject strippedRecord = records.get(0); return truncateFieldValue(Schema.LogEntryEvent__e.RecordJson__c, JSON.serializePretty(strippedRecord)); } - private static String getJson(List records) { - List strippedRecords = stripInaccessible(records); - return truncateFieldValue(Schema.LogEntryEvent__e.RecordJson__c, JSON.serializePretty(strippedRecords)); + private static String getJson(List records, Boolean isRecordFieldStrippingEnabled) { + records = isRecordFieldStrippingEnabled == false ? records : stripInaccessible(records); + return truncateFieldValue(Schema.LogEntryEvent__e.RecordJson__c, JSON.serializePretty(records)); + } + + private static String getOrganizationApiVersion() { + // Small hack to determine the org's current API version (since Apex doesn't natively provide it) + // Serializing any SObject w/ an ID will include the API version + // So, use UserInfo.getUserId() to create the current user's record without querying + // Then parse the JSON to get the API version + // Expected JSON: {"attributes":{"type":"User","url":"/services/data/v53.0/sobjects/User/005J000000AugnYIAR"} + String userJson = JSON.serialize(new User(Id = UserInfo.getUserId())); + return userJson.substringAfter('/data/').substringBefore('/sobjects/User'); } private static String getOrganizationEnvironmentType() { Organization cachedOrganization = LoggerEngineDataSelector.getInstance().getCachedOrganization(); String orgEnvironmentType; - if (cachedOrganization.IsSandbox == true && cachedOrganization.TrialExpirationDate != null) { + if (cachedOrganization == null) { + return ''; + } else if (cachedOrganization.IsSandbox == true && cachedOrganization.TrialExpirationDate != null) { orgEnvironmentType = 'Scratch Org'; } else if (cachedOrganization.IsSandbox == true) { orgEnvironmentType = 'Sandbox'; @@ -822,8 +931,8 @@ global with sharing class LogEntryEventBuilder { String sobjectName = sobjectType.getDescribe().getName(); // Check the map to see if we've already determined the classification for this SObject type - if (SOBJECT_NAME_TO_CLASSIFICATION.containsKey(sobjectName)) { - return SOBJECT_NAME_TO_CLASSIFICATION.get(sobjectName); + if (CACHED_SOBJECT_NAME_TO_CLASSIFICATION.containsKey(sobjectName)) { + return CACHED_SOBJECT_NAME_TO_CLASSIFICATION.get(sobjectName); } String sobjectClassification; @@ -846,7 +955,7 @@ global with sharing class LogEntryEventBuilder { } // Cache the results in case there are other entries related to the same SObject Type - SOBJECT_NAME_TO_CLASSIFICATION.put(sobjectName, sobjectClassification); + CACHED_SOBJECT_NAME_TO_CLASSIFICATION.put(sobjectName, sobjectClassification); return sobjectClassification; } @@ -901,7 +1010,7 @@ global with sharing class LogEntryEventBuilder { } */ private static Boolean isValidStackTrace(String stackTraceString) { - if (String.isBlank(stackTraceString) || stackTraceString == '()' || stackTraceString == '(' + NAMESPACE_PREFIX + ')') { + if (String.isBlank(stackTraceString) == true || stackTraceString == '()' || stackTraceString == '(' + NAMESPACE_PREFIX + ')') { return false; } @@ -924,12 +1033,7 @@ global with sharing class LogEntryEventBuilder { } private static List stripInaccessible(List records) { - if ( - Logger.getUserSettings().IsRecordFieldStrippingEnabled__c == false || - records == null || - records.isEmpty() == true || - records.get(0).getSObjectType() == Schema.AggregateResult.SObjectType - ) { + if (records == null || records.isEmpty() == true || records.get(0).getSObjectType() == Schema.AggregateResult.SObjectType) { return records; } diff --git a/nebula-logger/core/main/logger-engine/classes/Logger.cls b/nebula-logger/core/main/logger-engine/classes/Logger.cls index 8d614c609..8e8df4e27 100644 --- a/nebula-logger/core/main/logger-engine/classes/Logger.cls +++ b/nebula-logger/core/main/logger-engine/classes/Logger.cls @@ -10,19 +10,18 @@ * @see LogMessage */ @SuppressWarnings( - 'PMD.AvoidDebugStatements, PMD.AvoidGlobalModifier, PMD.CyclomaticComplexity, PMD.CognitiveComplexity, PMD.ExcessiveClassLength, PMD.StdCyclomaticComplexity' + 'PMD.AvoidDebugStatements, PMD.AvoidGlobalModifier, PMD.CyclomaticComplexity, PMD.CognitiveComplexity, PMD.ExcessiveClassLength, PMD.PropertyNamingConventions, PMD.StdCyclomaticComplexity' ) 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.8.3'; + private static final String CURRENT_VERSION_NUMBER = 'v4.8.4'; private static final LoggingLevel DEFAULT_LOGGING_LEVEL = LoggingLevel.DEBUG; private static final Set IGNORED_APEX_CLASSES = getIgnoredApexClasses(); private static final List LOG_ENTRIES_BUFFER = new List(); + 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 = new Uuid().getValue(); - private static final Quiddity TRANSACTION_QUIDDITY = setTransactionQuiddity(); - private static final Boolean USE_FIRST_SPECIFIED_SCENARIO_AS_TRANSACTION_SCENARIO = LoggerParameter.getBoolean('UseFirstSpecifiedScenario', true); private static String currentEntryScenario; private static Integer currentTransactionEntryNumber = 1; @@ -36,6 +35,29 @@ global with sharing class Logger { private static String transactionScenario; private static LoggerSettings__c userSettings; + private static final Quiddity TRANSACTION_QUIDDITY { + get { + if (TRANSACTION_QUIDDITY == null) { + TRANSACTION_QUIDDITY = getTransactionQuiddity(); + } + return TRANSACTION_QUIDDITY; + } + set; + } + + private static final String USER_SESSION_ID { + get { + if (USER_SESSION_ID == null) { + USER_SESSION_ID = UserInfo.getSessionId(); + // If UserInfo.getSessionId() returns null, set to an empty string to + // avoid calling UserInfo.getSessionId() again + USER_SESSION_ID = USER_SESSION_ID == null ? '' : USER_SESSION_ID; + } + return USER_SESSION_ID; + } + set; + } + private static String transactionSaveMethodName { get { if (transactionSaveMethodName == null) { @@ -2645,7 +2667,7 @@ global with sharing class Logger { */ @SuppressWarnings('PMD.NcssMethodCount') global static void setScenario(String scenario) { - if (USE_FIRST_SPECIFIED_SCENARIO_AS_TRANSACTION_SCENARIO == false || String.isBlank(transactionScenario) == true) { + if (LoggerParameter.USE_FIRST_SCENARIO_FOR_TRANSACTION == false || String.isBlank(transactionScenario) == true) { transactionScenario = scenario; } @@ -2821,7 +2843,7 @@ global with sharing class Logger { when REST { // If the user doesn't have a session ID (e.g., site guest user), the REST API call will fail // To avoid that, use the EventBus instead (even though REST was specified) - if (String.isBlank(UserInfo.getSessionId())) { + if (String.isBlank(USER_SESSION_ID) == true) { saveLog(Logger.SaveMethod.EVENT_BUS); } else { new RestApiSaver().insertRecords(logEntryEvents); @@ -2902,6 +2924,7 @@ global with sharing class Logger { LogEntryEvent__e logEntryEvent = logEntryEventBuilder.getLogEntryEvent(); logEntryEvent.EntryScenario__c = currentEntryScenario; logEntryEvent.LoggerVersionNumber__c = CURRENT_VERSION_NUMBER; + logEntryEvent.RequestId__c = REQUEST_ID; logEntryEvent.SystemMode__c = getCurrentQuiddity()?.name(); logEntryEvent.TransactionEntryNumber__c = currentTransactionEntryNumber++; logEntryEvent.TransactionId__c = getTransactionId(); @@ -2917,12 +2940,12 @@ global with sharing class Logger { return new Set{ Logger.class.getName(), LogEntryEventBuilder.class.getName() }; } - private static Quiddity setTransactionQuiddity() { + private static Quiddity getTransactionQuiddity() { // An error can sometimes occur when calling System.Request.getCurrent(), such as when logging // from an Auth Provider class (that implements Auth.RegistrationHandler). As a workaround, // skip calling System.Request.getCurrent() if there is no user session. // TODO: see if there is a better approach for this long term (or hopefully Salesforce fixes the gack error) - if (String.isNotBlank(UserInfo.getSessionId())) { + if (String.isNotBlank(USER_SESSION_ID) == true) { return System.Request.getCurrent().getQuiddity(); } else { return null; @@ -3024,7 +3047,7 @@ global with sharing class Logger { public void insertRecords(List records) { HttpRequest request = new HttpRequest(); request.setEndpoint(baseURL + compositeEndpoint); - request.setHeader('Authorization', 'Bearer ' + UserInfo.getSessionId()); + request.setHeader('Authorization', 'Bearer ' + USER_SESSION_ID); request.setHeader('Content-Type', 'application/json; charset=utf-8'); request.setMethod('POST'); diff --git a/nebula-logger/core/main/logger-engine/classes/LoggerCache.cls b/nebula-logger/core/main/logger-engine/classes/LoggerCache.cls deleted file mode 100644 index 38763ae4d..000000000 --- a/nebula-logger/core/main/logger-engine/classes/LoggerCache.cls +++ /dev/null @@ -1,67 +0,0 @@ -//------------------------------------------------------------------------------------------------// -// 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 Logger Engine - * @description Class used to cache query results returned by the selector classes - */ -public without sharing class LoggerCache { - private static final TransactionCache TRANSACTION_CACHE_INSTANCE = new TransactionCache(); - - @SuppressWarnings('PMD.ApexDoc') - private interface Cacheable { - Boolean contains(String keyName); - Object get(String keyName); - void put(String keyName, Object valueToCache); - } - - /** - * @description The instance `TransactionCache` used for any transaction-specific caching - * @return The singleton instance of `TransactionCache` - */ - public static TransactionCache getTransactionCache() { - return TRANSACTION_CACHE_INSTANCE; - } - - /** - * @description Manages any transaction-specific caching - */ - public class TransactionCache implements Cacheable { - private final Map cachedValues = new Map(); - - @SuppressWarnings('PMD.EmptyStatementBlock') - private TransactionCache() { - } - - /** - * @description Indicates if the specified key has already been added to the cache - * @param keyName The `String` key name to check for within the transaction cache - * @return The `Boolean` result - */ - public Boolean contains(String keyName) { - return this.cachedValues.containsKey(keyName); - } - - /** - * @description Returns the cached value for the specified key, or `null` if - * the specified key does not exist in the transaction cache - * @param keyName The `String` key name to check for within the transaction cache - * @return return description - */ - public Object get(String keyName) { - return this.cachedValues.get(keyName); - } - - /** - * @description Adds the provided `Object` value to the current transaction's cache, - * using the specified `String` key name - * @param keyName The `String` key name to add to the transaction cache - * @param valueToCache The `Object` value to cache for the specified key name - */ - public void put(String keyName, Object valueToCache) { - this.cachedValues.put(keyName, valueToCache); - } - } -} diff --git a/nebula-logger/core/main/logger-engine/classes/LoggerDataStore.cls b/nebula-logger/core/main/logger-engine/classes/LoggerDataStore.cls index f3af1070f..137941b6c 100644 --- a/nebula-logger/core/main/logger-engine/classes/LoggerDataStore.cls +++ b/nebula-logger/core/main/logger-engine/classes/LoggerDataStore.cls @@ -10,9 +10,35 @@ */ @SuppressWarnings('PMD.ExcessivePublicCount') public without sharing class LoggerDataStore { - private static Database databaseInstance = new Database(); - private static EventBus eventBusInstance = new EventBus(); - private static JobQueue jobQueueInstance = new JobQueue(); + private static Database databaseInstance { + get { + if (databaseInstance == null) { + databaseInstance = new Database(); + } + return databaseInstance; + } + set; + } + + private static EventBus eventBusInstance { + get { + if (eventBusInstance == null) { + eventBusInstance = new EventBus(); + } + return eventBusInstance; + } + set; + } + + private static JobQueue jobQueueInstance { + get { + if (jobQueueInstance == null) { + jobQueueInstance = new JobQueue(); + } + return jobQueueInstance; + } + set; + } /** * @description The instance `LoggerDataStore.Database` used for any DML diff --git a/nebula-logger/core/main/logger-engine/classes/LoggerEngineDataSelector.cls b/nebula-logger/core/main/logger-engine/classes/LoggerEngineDataSelector.cls index e3984756b..79ecc2fb0 100644 --- a/nebula-logger/core/main/logger-engine/classes/LoggerEngineDataSelector.cls +++ b/nebula-logger/core/main/logger-engine/classes/LoggerEngineDataSelector.cls @@ -8,10 +8,14 @@ * @description Selector class used for all queries that are specific to the logger engine layer */ @SuppressWarnings('PMD.ApexCrudViolation, PMD.ExcessivePublicCount') -public without sharing class LoggerEngineDataSelector { - private static final LoggerEngineDataSelector INSTANCE = new LoggerEngineDataSelector(); +public without sharing virtual class LoggerEngineDataSelector { + @TestVisible + private static final Boolean IS_EXPERIENCE_CLOUD_ENABLED = Type.forName('NetworkMember') != null; + + private static LoggerEngineDataSelector instance = new LoggerEngineDataSelector(); @SuppressWarnings('PMD.EmptyStatementBlock') + @TestVisible private LoggerEngineDataSelector() { } @@ -20,22 +24,23 @@ public without sharing class LoggerEngineDataSelector { * @return The singleton instance of `LoggerEngineDataSelector` */ public static LoggerEngineDataSelector getInstance() { - return INSTANCE; + return instance; } /** - * @description Returns a cached copy of `AuthSession` for the current user's current session, + * @description Returns a `Map` for the specified user IDs & their matching active sessions, * or `null` if there is not a current session - * @return The cached `AuthSession` record + * @param userIds userIds description + * @return The instance of `Map` containing any matching `AuthSession` records */ - public AuthSession getCachedAuthSession() { - Id userId = UserInfo.getUserId(); - String cacheKeyName = 'AuthSession' + userId; - if (LoggerCache.getTransactionCache().contains(cacheKeyName) == true) { - return (AuthSession) LoggerCache.getTransactionCache().get(cacheKeyName); + public virtual Map getAuthSessions(List userIds) { + Map userIdToAuthSession = new Map(); + + if (LoggerParameter.QUERY_AUTH_SESSION_DATA == false) { + return userIdToAuthSession; } - List sessions = [ + for (AuthSession session : [ SELECT Id, LoginType, @@ -46,13 +51,34 @@ public without sharing class LoggerEngineDataSelector { LogoutUrl, SessionSecurityLevel, SessionType, - SourceIp + SourceIp, + UsersId FROM AuthSession - WHERE UsersId = :UserInfo.getUserId() AND IsCurrent = TRUE AND ParentId = NULL // TODO this won't work in an async context (when running as Automated Process) - ]; + WHERE UsersId IN :userIds AND IsCurrent = TRUE AND ParentId = NULL + ]) { + userIdToAuthSession.put(session.UsersId, session); + } + return userIdToAuthSession; + } - AuthSession session = sessions.isEmpty() ? null : sessions.get(0); - LoggerCache.getTransactionCache().put(cacheKeyName, session); + /** + * @description Returns a cached copy of `AuthSession` for the current user's current session, + * or `null` if there is not a current session + * @return The cached `AuthSession` record + */ + public virtual AuthSession getCachedAuthSession() { + if (LoggerParameter.QUERY_AUTH_SESSION_DATA == false) { + return null; + } + + Id userId = UserInfo.getUserId(); + String cacheKey = 'AuthSession' + userId; + if (LoggerCache.getSessionCache().contains(cacheKey) == true) { + return (AuthSession) LoggerCache.getSessionCache().get(cacheKey); + } + + AuthSession session = getAuthSessions(new List{ userId }).get(userId); + LoggerCache.getSessionCache().put(cacheKey, session); return session; } @@ -61,10 +87,10 @@ public without sharing class LoggerEngineDataSelector { * including the field `SObjectType__r.QualifiedApiName` that cannot be accessed via `LoggerSObjectHandler__mdt.getAll()` * @return The cached `List` records */ - public List getCachedLoggerSObjectHandlers() { - String cacheKeyName = 'EnabledLoggerSObjectHandlers'; - if (LoggerCache.getTransactionCache().contains(cacheKeyName) == true) { - return (List) LoggerCache.getTransactionCache().get(cacheKeyName); + public virtual List getCachedLoggerSObjectHandlers() { + String cacheKey = 'EnabledLoggerSObjectHandlers'; + if (LoggerCache.getOrganizationCache().contains(cacheKey) == true) { + return (List) LoggerCache.getOrganizationCache().get(cacheKey); } List enabledSObjectHandlers = [ @@ -72,31 +98,32 @@ public without sharing class LoggerEngineDataSelector { FROM LoggerSObjectHandler__mdt WHERE IsEnabled__c = TRUE ]; - LoggerCache.getTransactionCache().put(cacheKeyName, enabledSObjectHandlers); + LoggerCache.getOrganizationCache().put(cacheKey, enabledSObjectHandlers); return enabledSObjectHandlers; } /** * @description Returns a cached copy of the current user's `Network` site, or `null` if the current user is not associated * with a `Network` site - * @return The cached `Network` record + * @param networkId The record ID of the `Network` to query + * @return The cached `Network` record */ - public SObject getCachedNetwork() { - Id networkId = System.Network.getNetworkId(); - if (networkId == null || Type.forName('System.Network') == null) { + public virtual SObject getCachedNetwork(Id networkId) { + if (LoggerParameter.QUERY_NETWORK_DATA == false) { return null; } - String cacheKeyName = 'Network' + networkId; - if (LoggerCache.getTransactionCache().contains(cacheKeyName) == true) { - return (SObject) LoggerCache.getTransactionCache().get(cacheKeyName); + if (networkId == null || IS_EXPERIENCE_CLOUD_ENABLED == false) { + return null; } - // Networks (aka experience sites aka community sites aka portal sites ò_ô) - // may not be enabled in the org (no Network object), so run everything dynamically - String query = 'SELECT Id, Name, UrlPathPrefix FROM Network WHERE Id = :networkId'; - SObject networkSite = Database.query(String.escapeSingleQuotes(query)); - LoggerCache.getTransactionCache().put(cacheKeyName, networkSite); + String cacheKey = 'Network' + networkId; + if (LoggerCache.getOrganizationCache().contains(cacheKey) == true) { + return (SObject) LoggerCache.getOrganizationCache().get(cacheKey); + } + + SObject networkSite = getNetworks(new List{ networkId }).get(networkId); + LoggerCache.getOrganizationCache().put(cacheKey, networkSite); return networkSite; } @@ -104,14 +131,18 @@ public without sharing class LoggerEngineDataSelector { * @description Returns a cached copy of the `Organization` record in the org, including some fields that cannot be accessed via `UserInfo` * @return The cached `Organization` record */ - public Organization getCachedOrganization() { - String cacheKeyName = 'Organization'; - if (LoggerCache.getTransactionCache().contains(cacheKeyName) == true) { - return (Organization) LoggerCache.getTransactionCache().get(cacheKeyName); + public virtual Organization getCachedOrganization() { + if (LoggerParameter.QUERY_ORGANIZATION_DATA == false) { + return null; + } + + String cacheKey = 'Organization'; + if (LoggerCache.getOrganizationCache().contains(cacheKey) == true) { + return (Organization) LoggerCache.getOrganizationCache().get(cacheKey); } Organization organization = [SELECT Id, InstanceName, IsSandbox, Name, NamespacePrefix, OrganizationType, TrialExpirationDate FROM Organization]; - LoggerCache.getTransactionCache().put(cacheKeyName, organization); + LoggerCache.getOrganizationCache().put(cacheKey, organization); return organization; } @@ -120,21 +151,24 @@ public without sharing class LoggerEngineDataSelector { * including the field `SObjectField__r.QualifiedApiName` that cannot be accessed via `LogEntryTagRule__mdt.getAll()` * @return The cached `List` records */ - public List getCachedTagAssignmentRules() { - String cacheKeyName = 'TagAssignmentRules'; - if (LoggerCache.getTransactionCache().contains(cacheKeyName) == true) { - return (List) LoggerCache.getTransactionCache().get(cacheKeyName); + public virtual List getCachedTagAssignmentRules() { + String cacheKey = 'TagAssignmentRules'; + if (LoggerCache.getOrganizationCache().contains(cacheKey) == true) { + return (List) LoggerCache.getOrganizationCache().get(cacheKey); } List tagAssignmentRules = new List(); if (LogEntryTagRule__mdt.getAll().isEmpty() == false || System.Test.isRunningTest() == true) { - tagAssignmentRules = [ + for (LogEntryTagRule__mdt rule : [ SELECT Id, SObjectField__r.QualifiedApiName, ComparisonType__c, ComparisonValue__c, Tags__c FROM LogEntryTagRule__mdt WHERE IsEnabled__c = TRUE AND SObjectType__r.DeveloperName = 'LogEntry' - ]; + ]) { + rule.SObjectField__c = rule.SObjectField__r.QualifiedApiName; + tagAssignmentRules.add(rule); + } } - LoggerCache.getTransactionCache().put(cacheKeyName, tagAssignmentRules); + LoggerCache.getOrganizationCache().put(cacheKey, tagAssignmentRules); return tagAssignmentRules; } @@ -142,20 +176,60 @@ public without sharing class LoggerEngineDataSelector { * @description Returns a cached copy of the current user, including some profile fields that cannot be accessed via `UserInfo` * @return The cached `User` record for the current user */ - public User getCachedUser() { - Id userId = UserInfo.getUserId(); + public virtual User getCachedUser() { + if (LoggerParameter.QUERY_USER_DATA == false) { + return null; + } - String cacheKeyName = 'User' + userId; - if (LoggerCache.getTransactionCache().contains(cacheKeyName) == true) { - return (User) LoggerCache.getTransactionCache().get(cacheKeyName); + Id userId = UserInfo.getUserId(); + String cacheKey = 'User' + userId; + if (LoggerCache.getSessionCache().contains(cacheKey) == true) { + return (User) LoggerCache.getSessionCache().get(cacheKey); } - User user = [ - SELECT Id, Profile.Name, Profile.UserLicenseId, Profile.UserLicense.LicenseDefinitionKey, Profile.UserLicense.Name, Username, UserRole.Name - FROM User - WHERE Id = :userId - ]; - LoggerCache.getTransactionCache().put(cacheKeyName, user); + User user = getUsers(new List{ userId }).get(userId); + LoggerCache.getSessionCache().put(cacheKey, user); return user; } + + /** + * @description Returns a list of matching `Network` records based on the provided list of network IDs + * @param networkIds The list of `Network` IDs to query + * @return The instance of `Map` containing any matching `Network` records + */ + public Map getNetworks(List networkIds) { + // TODO add caching in a future release + if (LoggerParameter.QUERY_NETWORK_DATA == false) { + return null; + } + + // Networks (aka experience sites aka community sites aka portal sites ò_ô) + // may not be enabled in the org (no Network object), so run everything dynamically + String query = 'SELECT Id, Name, UrlPathPrefix FROM Network WHERE Id IN :networkIds'; + return new Map(Database.query(String.escapeSingleQuotes(query))); + } + + /** + * @description Returns a list of matching `User` records based on the provided list of user IDs + * @param userIds The list of `User` IDs to query + * @return The instance of `Map` containing any matching `User` records + */ + public Map getUsers(List userIds) { + if (LoggerParameter.QUERY_USER_DATA == false) { + return new Map(); + } + + return new Map( + [ + SELECT Id, Profile.Name, Profile.UserLicenseId, Profile.UserLicense.LicenseDefinitionKey, Profile.UserLicense.Name, Username, UserRole.Name + FROM User + WHERE Id IN :userIds + ] + ); + } + + @TestVisible + private static void setMock(LoggerEngineDataSelector mockSelectorInstance) { + instance = mockSelectorInstance; + } } diff --git a/nebula-logger/core/main/logger-engine/objects/LogEntryEvent__e/fields/RequestId__c.field-meta.xml b/nebula-logger/core/main/logger-engine/objects/LogEntryEvent__e/fields/RequestId__c.field-meta.xml new file mode 100644 index 000000000..63591e6b3 --- /dev/null +++ b/nebula-logger/core/main/logger-engine/objects/LogEntryEvent__e/fields/RequestId__c.field-meta.xml @@ -0,0 +1,16 @@ + + + RequestId__c + Active + None + false + false + false + false + + 36 + true + Confidential + Text + false + diff --git a/nebula-logger/core/tests/configuration/classes/LoggerCache_Tests.cls b/nebula-logger/core/tests/configuration/classes/LoggerCache_Tests.cls new file mode 100644 index 000000000..a40f92371 --- /dev/null +++ b/nebula-logger/core/tests/configuration/classes/LoggerCache_Tests.cls @@ -0,0 +1,402 @@ +//------------------------------------------------------------------------------------------------// +// 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. // +//------------------------------------------------------------------------------------------------// + +/** + * @description When testing Platform Cache partitions, there is no way to directly mock the partitions. Furthermore, the partitions + * configured in the org are actually used in test contexts, so if a partition exists but does not have storage space + * allocated in the org, then any tests that try to assert that data is cached in the partitions will fail. + * To help overcome this platform limitation, a mock class - `MockPlatformCachePartitionDelegate` - is used + * to simulate how the code would behave with different partition configurations. + * Additional integration tests (that actually test real platform cache partitions) are used in Nebula Logger's pipeline + * but are not included in core package since those tests may fail in some orgs. + */ +@SuppressWarnings('PMD.ApexDoc, PMD.ApexAssertionsShouldIncludeMessage, PMD.CyclomaticComplexity, PMD.MethodNamingConventions') +@IsTest(IsParallel=true) +private class LoggerCache_Tests { + @IsTest + static void it_adds_new_key_to_transaction_cache() { + String mockKey = 'SomeKey'; + User mockValue = new User(Id = UserInfo.getUserId(), ProfileId = UserInfo.getProfileId()); + System.assertEquals(false, LoggerCache.getTransactionCache().contains(mockKey)); + + LoggerCache.getTransactionCache().put(mockKey, mockValue); + + System.assertEquals(true, LoggerCache.getTransactionCache().contains(mockKey)); + System.assertEquals(mockValue, LoggerCache.getTransactionCache().get(mockKey)); + } + + @IsTest + static void it_adds_new_key_with_null_value_to_transaction_cache() { + String mockKey = 'SomeKey'; + User mockValue = null; + System.assertEquals(false, LoggerCache.getTransactionCache().contains(mockKey)); + + LoggerCache.getTransactionCache().put(mockKey, mockValue); + + System.assertEquals(true, LoggerCache.getTransactionCache().contains(mockKey)); + System.assertEquals(mockValue, LoggerCache.getTransactionCache().get(mockKey)); + } + + @IsTest + static void it_updates_value_for_existing_key_in_transaction_cache() { + String mockKey = 'SomeKey'; + User oldMockValue = new User(Id = UserInfo.getUserId(), ProfileId = UserInfo.getProfileId()); + LoggerCache.getTransactionCache().put(mockKey, oldMockValue); + System.assertEquals(true, LoggerCache.getTransactionCache().contains(mockKey)); + System.assertEquals(oldMockValue, LoggerCache.getTransactionCache().get(mockKey)); + Account newMockValue = new Account(Name = 'Some fake account'); + + LoggerCache.getTransactionCache().put(mockKey, newMockValue); + + System.assertEquals(true, LoggerCache.getTransactionCache().contains(mockKey)); + System.assertEquals(newMockValue, LoggerCache.getTransactionCache().get(mockKey)); + } + + @IsTest + static void it_removes_value_for_existing_key_in_transaction_cache() { + String mockKey = 'SomeKey'; + User mockValue = new User(Id = UserInfo.getUserId(), ProfileId = UserInfo.getProfileId()); + System.assertEquals(false, LoggerCache.getTransactionCache().contains(mockKey)); + LoggerCache.getTransactionCache().put(mockKey, mockValue); + System.assertEquals(true, LoggerCache.getTransactionCache().contains(mockKey)); + System.assertEquals(mockValue, LoggerCache.getTransactionCache().get(mockKey)); + + LoggerCache.getTransactionCache().remove(mockKey); + + System.assertEquals(false, LoggerCache.getTransactionCache().contains(mockKey)); + } + + @IsTest + static void it_adds_new_key_to_organization_and_transaction_cache_when_platform_cache_is_available() { + String mockKey = 'SomeKey'; + User mockValue = new User(Id = UserInfo.getUserId(), ProfileId = UserInfo.getProfileId()); + MockPlatformCachePartitionDelegate mockOrganizationPartitionDelegate = new MockPlatformCachePartitionDelegate(true); + System.assertEquals(true, mockOrganizationPartitionDelegate.isAvailable()); + LoggerCache.setMockOrganizationPartitionDelegate(mockOrganizationPartitionDelegate); + System.assertEquals(0, mockOrganizationPartitionDelegate.containsMethodCallCount); + System.assertEquals(false, LoggerCache.getOrganizationCache().contains(mockKey)); + System.assertEquals(1, mockOrganizationPartitionDelegate.containsMethodCallCount); + System.assertEquals(false, LoggerCache.getTransactionCache().contains(mockKey)); + System.assertEquals(0, mockOrganizationPartitionDelegate.putMethodCallCount); + + LoggerCache.getOrganizationCache().put(mockKey, mockValue); + + System.assertEquals(1, mockOrganizationPartitionDelegate.putMethodCallCount); + System.assertEquals(true, mockOrganizationPartitionDelegate.contains(mockKey)); + System.assertEquals(mockValue, mockOrganizationPartitionDelegate.get(mockKey)); + System.assertEquals(true, LoggerCache.getOrganizationCache().contains(mockKey)); + System.assertEquals(mockValue, LoggerCache.getOrganizationCache().get(mockKey)); + System.assertEquals(true, LoggerCache.getTransactionCache().contains(mockKey)); + System.assertEquals(mockValue, LoggerCache.getTransactionCache().get(mockKey)); + } + + @IsTest + static void it_adds_new_key_with_null_value_to_organization_and_transaction_cache_when_platform_cache_is_available() { + String mockKey = 'SomeKey'; + User mockValue = null; + MockPlatformCachePartitionDelegate mockOrganizationPartitionDelegate = new MockPlatformCachePartitionDelegate(true); + System.assertEquals(true, mockOrganizationPartitionDelegate.isAvailable()); + LoggerCache.setMockOrganizationPartitionDelegate(mockOrganizationPartitionDelegate); + System.assertEquals(0, mockOrganizationPartitionDelegate.containsMethodCallCount); + System.assertEquals(false, LoggerCache.getOrganizationCache().contains(mockKey)); + System.assertEquals(1, mockOrganizationPartitionDelegate.containsMethodCallCount); + System.assertEquals(false, LoggerCache.getTransactionCache().contains(mockKey)); + System.assertEquals(0, mockOrganizationPartitionDelegate.putMethodCallCount); + + LoggerCache.getOrganizationCache().put(mockKey, mockValue); + + System.assertEquals(1, mockOrganizationPartitionDelegate.putMethodCallCount); + System.assertEquals(true, mockOrganizationPartitionDelegate.contains(mockKey)); + System.assertEquals(LoggerCache.PLATFORM_CACHE_NULL_VALUE, mockOrganizationPartitionDelegate.get(mockKey)); + System.assertEquals(true, LoggerCache.getOrganizationCache().contains(mockKey)); + System.assertEquals(mockValue, LoggerCache.getOrganizationCache().get(mockKey)); + System.assertEquals(true, LoggerCache.getTransactionCache().contains(mockKey)); + System.assertEquals(mockValue, LoggerCache.getTransactionCache().get(mockKey)); + } + + @IsTest + static void it_adds_new_key_to_only_transaction_cache_when_organization_platform_cache_is_not_available() { + String mockKey = 'SomeKey'; + User mockValue = new User(Id = UserInfo.getUserId(), ProfileId = UserInfo.getProfileId()); + MockPlatformCachePartitionDelegate mockOrganizationPartitionDelegate = new MockPlatformCachePartitionDelegate(false); + System.assertEquals(false, mockOrganizationPartitionDelegate.isAvailable()); + LoggerCache.setMockOrganizationPartitionDelegate(mockOrganizationPartitionDelegate); + System.assertEquals(0, mockOrganizationPartitionDelegate.containsMethodCallCount); + System.assertEquals(false, LoggerCache.getOrganizationCache().contains(mockKey)); + System.assertEquals(0, mockOrganizationPartitionDelegate.containsMethodCallCount); + System.assertEquals(false, LoggerCache.getTransactionCache().contains(mockKey)); + System.assertEquals(0, mockOrganizationPartitionDelegate.putMethodCallCount); + + LoggerCache.getOrganizationCache().put(mockKey, mockValue); + + System.assertEquals(0, mockOrganizationPartitionDelegate.putMethodCallCount); + System.assertEquals(false, mockOrganizationPartitionDelegate.contains(mockKey)); + System.assertEquals(null, mockOrganizationPartitionDelegate.get(mockKey)); + System.assertEquals(true, LoggerCache.getOrganizationCache().contains(mockKey)); + System.assertEquals(mockValue, LoggerCache.getOrganizationCache().get(mockKey)); + System.assertEquals(true, LoggerCache.getTransactionCache().contains(mockKey)); + System.assertEquals(mockValue, LoggerCache.getTransactionCache().get(mockKey)); + } + + @IsTest + static void it_updates_value_for_existing_key_in_organization_and_transaction_cache_when_platform_cache_is_available() { + String mockKey = 'SomeKey'; + User oldMockValue = new User(Id = UserInfo.getUserId(), ProfileId = UserInfo.getProfileId()); + MockPlatformCachePartitionDelegate mockOrganizationPartitionDelegate = new MockPlatformCachePartitionDelegate(true); + System.assertEquals(true, mockOrganizationPartitionDelegate.isAvailable()); + LoggerCache.setMockOrganizationPartitionDelegate(mockOrganizationPartitionDelegate); + System.assertEquals(0, mockOrganizationPartitionDelegate.putMethodCallCount); + LoggerCache.getOrganizationCache().put(mockKey, oldMockValue); + System.assertEquals(true, LoggerCache.getOrganizationCache().contains(mockKey)); + System.assertEquals(oldMockValue, LoggerCache.getOrganizationCache().get(mockKey)); + System.assertEquals(true, LoggerCache.getTransactionCache().contains(mockKey)); + System.assertEquals(oldMockValue, LoggerCache.getTransactionCache().get(mockKey)); + Account newMockValue = new Account(Name = 'Some fake account'); + System.assertEquals(1, mockOrganizationPartitionDelegate.putMethodCallCount); + + LoggerCache.getOrganizationCache().put(mockKey, newMockValue); + + System.assertEquals(2, mockOrganizationPartitionDelegate.putMethodCallCount); + System.assertEquals(true, mockOrganizationPartitionDelegate.contains(mockKey)); + System.assertEquals(newMockValue, mockOrganizationPartitionDelegate.get(mockKey)); + System.assertEquals(true, LoggerCache.getOrganizationCache().contains(mockKey)); + System.assertEquals(newMockValue, LoggerCache.getOrganizationCache().get(mockKey)); + System.assertEquals(true, LoggerCache.getTransactionCache().contains(mockKey)); + System.assertEquals(newMockValue, LoggerCache.getTransactionCache().get(mockKey)); + } + + @IsTest + static void it_returns_value_in_transaction_cache_when_organization_and_transaction_cache_both_contain_key() { + String mockKey = 'SomeKey'; + User transactionCacheValue = new User(Id = UserInfo.getUserId(), ProfileId = UserInfo.getProfileId()); + Account organizationCacheValue = new Account(Name = 'Some fake account'); + MockPlatformCachePartitionDelegate mockOrganizationPartitionDelegate = new MockPlatformCachePartitionDelegate(true); + System.assertEquals(true, mockOrganizationPartitionDelegate.isAvailable()); + LoggerCache.setMockOrganizationPartitionDelegate(mockOrganizationPartitionDelegate); + mockOrganizationPartitionDelegate.put(mockKey, organizationCacheValue, 5, Cache.Visibility.NAMESPACE, false); + System.assertEquals(true, mockOrganizationPartitionDelegate.contains(mockKey)); + LoggerCache.getTransactionCache().put(mockKey, transactionCacheValue); + System.assertEquals(true, LoggerCache.getTransactionCache().contains(mockKey)); + + Object returnedValue = LoggerCache.getOrganizationCache().get(mockKey); + + System.assertEquals(transactionCacheValue, returnedValue); + } + + @IsTest + static void it_removes_value_for_existing_key_in_organization_and_transaction_cache_when_platform_cache_is_available() { + String mockKey = 'SomeKey'; + User mockValue = new User(Id = UserInfo.getUserId(), ProfileId = UserInfo.getProfileId()); + MockPlatformCachePartitionDelegate mockOrganizationPartitionDelegate = new MockPlatformCachePartitionDelegate(true); + System.assertEquals(true, mockOrganizationPartitionDelegate.isAvailable()); + LoggerCache.setMockOrganizationPartitionDelegate(mockOrganizationPartitionDelegate); + System.assertEquals(false, LoggerCache.getOrganizationCache().contains(mockKey)); + System.assertEquals(false, LoggerCache.getTransactionCache().contains(mockKey)); + LoggerCache.getOrganizationCache().put(mockKey, mockValue); + System.assertEquals(true, mockOrganizationPartitionDelegate.contains(mockKey)); + System.assertEquals(mockValue, mockOrganizationPartitionDelegate.get(mockKey)); + System.assertEquals(true, LoggerCache.getOrganizationCache().contains(mockKey)); + System.assertEquals(mockValue, LoggerCache.getOrganizationCache().get(mockKey)); + System.assertEquals(true, LoggerCache.getTransactionCache().contains(mockKey)); + System.assertEquals(mockValue, LoggerCache.getTransactionCache().get(mockKey)); + System.assertEquals(0, mockOrganizationPartitionDelegate.removeMethodCallCount); + + LoggerCache.getOrganizationCache().remove(mockKey); + + System.assertEquals(1, mockOrganizationPartitionDelegate.removeMethodCallCount); + System.assertEquals(false, mockOrganizationPartitionDelegate.contains(mockKey)); + System.assertEquals(false, LoggerCache.getOrganizationCache().contains(mockKey)); + System.assertEquals(false, LoggerCache.getTransactionCache().contains(mockKey)); + } + + @IsTest + static void it_adds_new_key_to_session_and_transaction_cache_when_platform_cache_is_available() { + String mockKey = 'SomeKey'; + User mockValue = new User(Id = UserInfo.getUserId(), ProfileId = UserInfo.getProfileId()); + MockPlatformCachePartitionDelegate mockSessionPartitionDelegate = new MockPlatformCachePartitionDelegate(true); + System.assertEquals(true, mockSessionPartitionDelegate.isAvailable()); + LoggerCache.setMockSessionPartitionDelegate(mockSessionPartitionDelegate); + System.assertEquals(0, mockSessionPartitionDelegate.containsMethodCallCount); + System.assertEquals(false, LoggerCache.getSessionCache().contains(mockKey)); + System.assertEquals(1, mockSessionPartitionDelegate.containsMethodCallCount); + System.assertEquals(false, LoggerCache.getTransactionCache().contains(mockKey)); + System.assertEquals(0, mockSessionPartitionDelegate.putMethodCallCount); + + LoggerCache.getSessionCache().put(mockKey, mockValue); + + System.assertEquals(1, mockSessionPartitionDelegate.putMethodCallCount); + System.assertEquals(true, mockSessionPartitionDelegate.contains(mockKey)); + System.assertEquals(mockValue, mockSessionPartitionDelegate.get(mockKey)); + System.assertEquals(true, LoggerCache.getSessionCache().contains(mockKey)); + System.assertEquals(mockValue, LoggerCache.getSessionCache().get(mockKey)); + System.assertEquals(true, LoggerCache.getTransactionCache().contains(mockKey)); + System.assertEquals(mockValue, LoggerCache.getTransactionCache().get(mockKey)); + } + + @IsTest + static void it_adds_new_key_with_null_value_to_session_and_transaction_cache_when_platform_cache_is_available() { + String mockKey = 'SomeKey'; + User mockValue = null; + MockPlatformCachePartitionDelegate mockSessionPartitionDelegate = new MockPlatformCachePartitionDelegate(true); + System.assertEquals(true, mockSessionPartitionDelegate.isAvailable()); + LoggerCache.setMockSessionPartitionDelegate(mockSessionPartitionDelegate); + System.assertEquals(0, mockSessionPartitionDelegate.containsMethodCallCount); + System.assertEquals(false, LoggerCache.getSessionCache().contains(mockKey)); + System.assertEquals(1, mockSessionPartitionDelegate.containsMethodCallCount); + System.assertEquals(false, LoggerCache.getTransactionCache().contains(mockKey)); + System.assertEquals(0, mockSessionPartitionDelegate.putMethodCallCount); + + LoggerCache.getSessionCache().put(mockKey, mockValue); + + System.assertEquals(1, mockSessionPartitionDelegate.putMethodCallCount); + System.assertEquals(true, mockSessionPartitionDelegate.contains(mockKey)); + System.assertEquals(LoggerCache.PLATFORM_CACHE_NULL_VALUE, mockSessionPartitionDelegate.get(mockKey)); + System.assertEquals(true, LoggerCache.getSessionCache().contains(mockKey)); + System.assertEquals(mockValue, LoggerCache.getSessionCache().get(mockKey)); + System.assertEquals(true, LoggerCache.getTransactionCache().contains(mockKey)); + System.assertEquals(mockValue, LoggerCache.getTransactionCache().get(mockKey)); + } + + @IsTest + static void it_adds_new_key_to_only_transaction_cache_when_session_platform_cache_is_not_available() { + String mockKey = 'SomeKey'; + User mockValue = new User(Id = UserInfo.getUserId(), ProfileId = UserInfo.getProfileId()); + MockPlatformCachePartitionDelegate mockSessionPartitionDelegate = new MockPlatformCachePartitionDelegate(false); + System.assertEquals(false, mockSessionPartitionDelegate.isAvailable()); + LoggerCache.setMockSessionPartitionDelegate(mockSessionPartitionDelegate); + System.assertEquals(0, mockSessionPartitionDelegate.containsMethodCallCount); + System.assertEquals(false, LoggerCache.getSessionCache().contains(mockKey)); + System.assertEquals(0, mockSessionPartitionDelegate.containsMethodCallCount); + System.assertEquals(false, LoggerCache.getTransactionCache().contains(mockKey)); + System.assertEquals(0, mockSessionPartitionDelegate.putMethodCallCount); + + LoggerCache.getSessionCache().put(mockKey, mockValue); + + System.assertEquals(0, mockSessionPartitionDelegate.putMethodCallCount); + System.assertEquals(false, mockSessionPartitionDelegate.contains(mockKey)); + System.assertEquals(null, mockSessionPartitionDelegate.get(mockKey)); + System.assertEquals(true, LoggerCache.getSessionCache().contains(mockKey)); + System.assertEquals(mockValue, LoggerCache.getSessionCache().get(mockKey)); + System.assertEquals(true, LoggerCache.getTransactionCache().contains(mockKey)); + System.assertEquals(mockValue, LoggerCache.getTransactionCache().get(mockKey)); + } + + @IsTest + static void it_updates_value_for_existing_key_in_session_and_transaction_cache_when_platform_cache_is_available() { + String mockKey = 'SomeKey'; + User oldMockValue = new User(Id = UserInfo.getUserId(), ProfileId = UserInfo.getProfileId()); + MockPlatformCachePartitionDelegate mockSessionPartitionDelegate = new MockPlatformCachePartitionDelegate(true); + System.assertEquals(true, mockSessionPartitionDelegate.isAvailable()); + LoggerCache.setMockSessionPartitionDelegate(mockSessionPartitionDelegate); + System.assertEquals(0, mockSessionPartitionDelegate.putMethodCallCount); + LoggerCache.getSessionCache().put(mockKey, oldMockValue); + System.assertEquals(true, LoggerCache.getSessionCache().contains(mockKey)); + System.assertEquals(oldMockValue, LoggerCache.getSessionCache().get(mockKey)); + System.assertEquals(true, LoggerCache.getTransactionCache().contains(mockKey)); + System.assertEquals(oldMockValue, LoggerCache.getTransactionCache().get(mockKey)); + Account newMockValue = new Account(Name = 'Some fake account'); + System.assertEquals(1, mockSessionPartitionDelegate.putMethodCallCount); + + LoggerCache.getSessionCache().put(mockKey, newMockValue); + + System.assertEquals(2, mockSessionPartitionDelegate.putMethodCallCount); + System.assertEquals(true, mockSessionPartitionDelegate.contains(mockKey)); + System.assertEquals(newMockValue, mockSessionPartitionDelegate.get(mockKey)); + System.assertEquals(true, LoggerCache.getSessionCache().contains(mockKey)); + System.assertEquals(newMockValue, LoggerCache.getSessionCache().get(mockKey)); + System.assertEquals(true, LoggerCache.getTransactionCache().contains(mockKey)); + System.assertEquals(newMockValue, LoggerCache.getTransactionCache().get(mockKey)); + } + + @IsTest + static void it_returns_value_in_transaction_cache_when_session_and_transaction_cache_both_contain_key() { + String mockKey = 'SomeKey'; + User transactionCacheValue = new User(Id = UserInfo.getUserId(), ProfileId = UserInfo.getProfileId()); + Account sessionCacheValue = new Account(Name = 'Some fake account'); + MockPlatformCachePartitionDelegate mockSessionPartitionDelegate = new MockPlatformCachePartitionDelegate(true); + System.assertEquals(true, mockSessionPartitionDelegate.isAvailable()); + LoggerCache.setMockSessionPartitionDelegate(mockSessionPartitionDelegate); + mockSessionPartitionDelegate.put(mockKey, sessionCacheValue, 5, Cache.Visibility.NAMESPACE, false); + System.assertEquals(true, mockSessionPartitionDelegate.contains(mockKey)); + LoggerCache.getTransactionCache().put(mockKey, transactionCacheValue); + System.assertEquals(true, LoggerCache.getTransactionCache().contains(mockKey)); + + Object returnedValue = LoggerCache.getSessionCache().get(mockKey); + + System.assertEquals(transactionCacheValue, returnedValue); + } + + @IsTest + static void it_removes_value_for_existing_key_in_session_and_transaction_cache_when_platform_cache_is_available() { + String mockKey = 'SomeKey'; + User mockValue = new User(Id = UserInfo.getUserId(), ProfileId = UserInfo.getProfileId()); + MockPlatformCachePartitionDelegate mockSessionPartitionDelegate = new MockPlatformCachePartitionDelegate(true); + System.assertEquals(true, mockSessionPartitionDelegate.isAvailable()); + LoggerCache.setMockSessionPartitionDelegate(mockSessionPartitionDelegate); + System.assertEquals(false, LoggerCache.getSessionCache().contains(mockKey)); + System.assertEquals(false, LoggerCache.getTransactionCache().contains(mockKey)); + LoggerCache.getSessionCache().put(mockKey, mockValue); + System.assertEquals(true, mockSessionPartitionDelegate.contains(mockKey)); + System.assertEquals(mockValue, mockSessionPartitionDelegate.get(mockKey)); + System.assertEquals(true, LoggerCache.getSessionCache().contains(mockKey)); + System.assertEquals(mockValue, LoggerCache.getSessionCache().get(mockKey)); + System.assertEquals(true, LoggerCache.getTransactionCache().contains(mockKey)); + System.assertEquals(mockValue, LoggerCache.getTransactionCache().get(mockKey)); + System.assertEquals(0, mockSessionPartitionDelegate.removeMethodCallCount); + + LoggerCache.getSessionCache().remove(mockKey); + + System.assertEquals(1, mockSessionPartitionDelegate.removeMethodCallCount); + System.assertEquals(false, mockSessionPartitionDelegate.contains(mockKey)); + System.assertEquals(false, LoggerCache.getSessionCache().contains(mockKey)); + System.assertEquals(false, LoggerCache.getTransactionCache().contains(mockKey)); + } + + // Since the class `Cache.Partition` can't have be mocked & can't have its methods overridden, + // the `LoggerCache` class internally uses a delegate to help abstract out the usage of the partition, + // which lets us mock the delegate within (true) unit tests. + private class MockPlatformCachePartitionDelegate extends LoggerCache.PlatformCachePartitionDelegate { + private final Boolean isAvailable; + // Since `Cache.Partition` can't be mocked, this mock delegate uses a map as a substitute + private final Map keyToValue = new Map(); + + public Integer isAvailableMethodCallCount = 0; + public Integer containsMethodCallCount = 0; + public Integer getMethodCallCount = 0; + public Integer putMethodCallCount = 0; + public Integer removeMethodCallCount = 0; + + private MockPlatformCachePartitionDelegate(Boolean isAvailable) { + super(null); + this.isAvailable = isAvailable; + } + + public override Boolean isAvailable() { + this.isAvailableMethodCallCount++; + return this.isAvailable; + } + + public override Boolean contains(String key) { + this.containsMethodCallCount++; + return this.keyToValue.containsKey(key); + } + + public override Object get(String key) { + this.getMethodCallCount++; + return this.keyToValue.get(key); + } + + @SuppressWarnings('PMD.ExcessiveParameterList') + public override void put(String key, Object value, Integer cacheTtlSeconds, Cache.Visibility cacheVisiblity, Boolean isCacheImmutable) { + this.putMethodCallCount++; + this.keyToValue.put(key, value); + } + + public override void remove(String key) { + this.removeMethodCallCount++; + this.keyToValue.remove(key); + } + } +} diff --git a/nebula-logger/core/tests/logger-engine/classes/LoggerCache_Tests.cls-meta.xml b/nebula-logger/core/tests/configuration/classes/LoggerCache_Tests.cls-meta.xml similarity index 100% rename from nebula-logger/core/tests/logger-engine/classes/LoggerCache_Tests.cls-meta.xml rename to nebula-logger/core/tests/configuration/classes/LoggerCache_Tests.cls-meta.xml diff --git a/nebula-logger/core/tests/configuration/classes/LoggerParameter_Tests.cls b/nebula-logger/core/tests/configuration/classes/LoggerParameter_Tests.cls index 265c32cf8..197c21c36 100644 --- a/nebula-logger/core/tests/configuration/classes/LoggerParameter_Tests.cls +++ b/nebula-logger/core/tests/configuration/classes/LoggerParameter_Tests.cls @@ -37,7 +37,7 @@ private class LoggerParameter_Tests { @IsTest static void it_should_return_constant_value_for_call_status_api() { - Boolean mockValue = true; + Boolean mockValue = false; LoggerParameter__mdt mockParameter = new LoggerParameter__mdt(DeveloperName = 'CallStatusApi', Value__c = JSON.serialize(mockValue)); LoggerParameter.setMock(mockParameter); @@ -48,7 +48,7 @@ private class LoggerParameter_Tests { @IsTest static void it_should_return_constant_value_for_enable_system_messages() { - Boolean mockValue = true; + Boolean mockValue = false; LoggerParameter__mdt mockParameter = new LoggerParameter__mdt(DeveloperName = 'EnableLoggerSystemMessages', Value__c = JSON.serialize(mockValue)); LoggerParameter.setMock(mockParameter); @@ -57,6 +57,144 @@ private class LoggerParameter_Tests { System.assertEquals(mockValue, returnedValue); } + @IsTest + static void it_should_return_constant_value_for_enable_tagging() { + Boolean mockValue = false; + LoggerParameter__mdt mockParameter = new LoggerParameter__mdt(DeveloperName = 'EnableTagging', Value__c = JSON.serialize(mockValue)); + LoggerParameter.setMock(mockParameter); + + Boolean returnedValue = LoggerParameter.ENABLE_TAGGING; + + System.assertEquals(mockValue, returnedValue); + } + + @IsTest + static void it_should_return_constant_value_for_query_apex_class_data() { + Boolean mockValue = false; + LoggerParameter__mdt mockParameter = new LoggerParameter__mdt(DeveloperName = 'QueryApexClassData', Value__c = JSON.serialize(mockValue)); + LoggerParameter.setMock(mockParameter); + + Boolean returnedValue = LoggerParameter.QUERY_APEX_CLASS_DATA; + + System.assertEquals(mockValue, returnedValue); + } + + @IsTest + static void it_should_return_constant_value_for_query_auth_session_data() { + Boolean mockValue = false; + LoggerParameter__mdt mockParameter = new LoggerParameter__mdt(DeveloperName = 'QueryAuthSessionData', Value__c = JSON.serialize(mockValue)); + LoggerParameter.setMock(mockParameter); + + Boolean returnedValue = LoggerParameter.QUERY_AUTH_SESSION_DATA; + + System.assertEquals(mockValue, returnedValue); + } + + @IsTest + static void it_should_return_constant_value_for_query_auth_session_data_synchronously() { + Boolean mockValue = false; + LoggerParameter__mdt mockParameter = new LoggerParameter__mdt( + DeveloperName = 'QueryAuthSessionDataSynchronously', + Value__c = JSON.serialize(mockValue) + ); + LoggerParameter.setMock(mockParameter); + + Boolean returnedValue = LoggerParameter.QUERY_AUTH_SESSION_DATA_SYNCHRONOUSLY; + + System.assertEquals(mockValue, returnedValue); + } + + @IsTest + static void it_should_return_constant_value_for_query_flow_definition_view_data() { + Boolean mockValue = false; + LoggerParameter__mdt mockParameter = new LoggerParameter__mdt(DeveloperName = 'QueryFlowDefinitionViewData', Value__c = JSON.serialize(mockValue)); + LoggerParameter.setMock(mockParameter); + + Boolean returnedValue = LoggerParameter.QUERY_FLOW_DEFINITION_VIEW_DATA; + + System.assertEquals(mockValue, returnedValue); + } + + @IsTest + static void it_should_return_constant_value_for_query_network_data() { + Boolean mockValue = false; + LoggerParameter__mdt mockParameter = new LoggerParameter__mdt(DeveloperName = 'QueryNetworkData', Value__c = JSON.serialize(mockValue)); + LoggerParameter.setMock(mockParameter); + + Boolean returnedValue = LoggerParameter.QUERY_NETWORK_DATA; + + System.assertEquals(mockValue, returnedValue); + } + + @IsTest + static void it_should_return_constant_value_for_query_network_data_synchronously() { + Boolean mockValue = false; + LoggerParameter__mdt mockParameter = new LoggerParameter__mdt(DeveloperName = 'QueryNetworkDataSynchronously', Value__c = JSON.serialize(mockValue)); + LoggerParameter.setMock(mockParameter); + + Boolean returnedValue = LoggerParameter.QUERY_NETWORK_DATA_SYNCHRONOUSLY; + + System.assertEquals(mockValue, returnedValue); + } + + @IsTest + static void it_should_return_constant_value_for_query_organization_data() { + Boolean mockValue = false; + LoggerParameter__mdt mockParameter = new LoggerParameter__mdt(DeveloperName = 'QueryOrganizationData', Value__c = JSON.serialize(mockValue)); + LoggerParameter.setMock(mockParameter); + + Boolean returnedValue = LoggerParameter.QUERY_ORGANIZATION_DATA; + + System.assertEquals(mockValue, returnedValue); + } + + @IsTest + static void it_should_return_constant_value_for_query_organization_data_synchronously() { + Boolean mockValue = false; + LoggerParameter__mdt mockParameter = new LoggerParameter__mdt( + DeveloperName = 'QueryOrganizationDataSynchronously', + Value__c = JSON.serialize(mockValue) + ); + LoggerParameter.setMock(mockParameter); + + Boolean returnedValue = LoggerParameter.QUERY_ORGANIZATION_DATA_SYNCHRONOUSLY; + + System.assertEquals(mockValue, returnedValue); + } + + @IsTest + static void it_should_return_constant_value_for_query_related_record_data() { + Boolean mockValue = false; + LoggerParameter__mdt mockParameter = new LoggerParameter__mdt(DeveloperName = 'QueryRelatedRecordData', Value__c = JSON.serialize(mockValue)); + LoggerParameter.setMock(mockParameter); + + Boolean returnedValue = LoggerParameter.QUERY_RELATED_RECORD_DATA; + + System.assertEquals(mockValue, returnedValue); + } + + @IsTest + static void it_should_return_constant_value_for_query_user_data() { + Boolean mockValue = false; + LoggerParameter__mdt mockParameter = new LoggerParameter__mdt(DeveloperName = 'QueryUserData', Value__c = JSON.serialize(mockValue)); + LoggerParameter.setMock(mockParameter); + + Boolean returnedValue = LoggerParameter.QUERY_USER_DATA; + + System.assertEquals(mockValue, returnedValue); + } + + @IsTest + static void it_should_return_constant_value_for_query_user_data_synchronously() { + Boolean mockValue = false; + LoggerParameter__mdt mockParameter = new LoggerParameter__mdt(DeveloperName = 'QueryUserDataSynchronously', Value__c = JSON.serialize(mockValue)); + LoggerParameter.setMock(mockParameter); + + Boolean returnedValue = LoggerParameter.QUERY_USER_DATA_SYNCHRONOUSLY; + + System.assertEquals(mockValue, returnedValue); + } + @IsTest static void it_should_return_constant_value_for_send_error_email_notifications() { Boolean mockValue = false; @@ -81,23 +219,23 @@ private class LoggerParameter_Tests { } @IsTest - static void it_should_return_constant_value_for_tagging_is_enabled() { + static void it_should_return_constant_value_for_use_platform_cache() { Boolean mockValue = false; - LoggerParameter__mdt mockParameter = new LoggerParameter__mdt(DeveloperName = 'EnableTagging', Value__c = JSON.serialize(mockValue)); + LoggerParameter__mdt mockParameter = new LoggerParameter__mdt(DeveloperName = 'UsePlatformCache', Value__c = JSON.serialize(mockValue)); LoggerParameter.setMock(mockParameter); - Boolean returnedValue = LoggerParameter.TAGGING_IS_ENABLED; + Boolean returnedValue = LoggerParameter.USE_PLATFORM_CACHE; System.assertEquals(mockValue, returnedValue); } @IsTest - static void it_should_return_constant_value_for_tag_using_topics() { + static void it_should_return_constant_value_for_use_topics_for_tags() { Boolean mockValue = true; LoggerParameter__mdt mockParameter = new LoggerParameter__mdt(DeveloperName = 'UseTopicsForTags', Value__c = JSON.serialize(mockValue)); LoggerParameter.setMock(mockParameter); - Boolean returnedValue = LoggerParameter.TAG_USING_TOPICS; + Boolean returnedValue = LoggerParameter.USE_TOPICS_FOR_TAGS; System.assertEquals(mockValue, returnedValue); } diff --git a/nebula-logger/core/tests/configuration/utilities/LoggerMockDataCreator.cls b/nebula-logger/core/tests/configuration/utilities/LoggerMockDataCreator.cls index 7fd5d200e..59571fa62 100644 --- a/nebula-logger/core/tests/configuration/utilities/LoggerMockDataCreator.cls +++ b/nebula-logger/core/tests/configuration/utilities/LoggerMockDataCreator.cls @@ -333,19 +333,6 @@ public class LoggerMockDataCreator { return orgEnvironmentType; } - /** - * @description Returns the current user's `Network` (Experience Cloud site) - * @return The matching `Network` record - */ - public static SObject getNetwork() { - if (Network.getNetworkId() == null || Type.forName('Network') == null) { - return null; - } - - String query = 'SELECT Id, Name, UrlPathPrefix FROM Network WHERE Id = :Network.getNetworkId()'; - return Database.query(String.escapeSingleQuotes(query)); - } - /** * @description Returns the current user * @return The matching `User` record 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 0d48e7d9a..36db7c292 100644 --- a/nebula-logger/core/tests/log-management/classes/LogEntryEventHandler_Tests.cls +++ b/nebula-logger/core/tests/log-management/classes/LogEntryEventHandler_Tests.cls @@ -452,6 +452,80 @@ private class LogEntryEventHandler_Tests { System.assertEquals(loggerScenario.OwnerId, log.OwnerId); } + @IsTest + static void it_should_query_and_set_auth_session_data_when_synchronous_querying_is_disabled() { + LoggerDataStore.setMock(LoggerMockDataStore.getEventBus()); + LoggerTestConfigurator.setupMockSObjectHandlerConfigurations(); + LoggerTestConfigurator.getSObjectHandlerConfiguration(Schema.LoggerScenario__c.SObjectType).IsEnabled__c = false; + LoggerTestConfigurator.getSObjectHandlerConfiguration(Schema.Log__c.SObjectType).IsEnabled__c = false; + LoggerTestConfigurator.getSObjectHandlerConfiguration(Schema.LogEntry__c.SObjectType).IsEnabled__c = false; + LogEntryEvent__e logEntryEvent = createLogEntryEvent(); + LoggerTestConfigurator.setMock(new LoggerParameter__mdt(DeveloperName = 'QueryAuthSessionDataSynchronously', Value__c = String.valueOf(false))); + + LoggerMockDataStore.getEventBus().publishRecord(logEntryEvent); + LoggerMockDataStore.getEventBus().deliver(new LogEntryEventHandler()); + + List userIds = new List{ logEntryEvent.LoggedById__c }; + AuthSession expectedAuthSession = LoggerEngineDataSelector.getInstance().getAuthSessions(userIds).get(logEntryEvent.LoggedById__c); + Log__c log = getLog(); + System.assertEquals(expectedAuthSession?.LoginHistory.Application, log.LoginApplication__c); + System.assertEquals(expectedAuthSession?.LoginHistory.Browser, log.LoginBrowser__c); + System.assertEquals(expectedAuthSession?.LoginHistoryId, log.LoginHistoryId__c); + System.assertEquals(expectedAuthSession?.LoginHistory.Platform, log.LoginPlatform__c); + System.assertEquals(expectedAuthSession?.LoginType, log.LoginType__c); + System.assertEquals(expectedAuthSession?.LogoutUrl, log.LogoutUrl__c); + System.assertEquals(expectedAuthSession?.Id, log.SessionId__c); + System.assertEquals(expectedAuthSession?.SessionSecurityLevel, log.SessionSecurityLevel__c); + System.assertEquals(expectedAuthSession?.SessionType, log.SessionType__c); + System.assertEquals(expectedAuthSession?.SourceIp, log.SourceIp__c); + } + + @IsTest + static void it_should_query_and_set_organization_data_when_synchronous_querying_is_disabled() { + LoggerDataStore.setMock(LoggerMockDataStore.getEventBus()); + LoggerTestConfigurator.setupMockSObjectHandlerConfigurations(); + LoggerTestConfigurator.getSObjectHandlerConfiguration(Schema.LoggerScenario__c.SObjectType).IsEnabled__c = false; + LoggerTestConfigurator.getSObjectHandlerConfiguration(Schema.Log__c.SObjectType).IsEnabled__c = false; + LoggerTestConfigurator.getSObjectHandlerConfiguration(Schema.LogEntry__c.SObjectType).IsEnabled__c = false; + LogEntryEvent__e logEntryEvent = createLogEntryEvent(); + LoggerTestConfigurator.setMock(new LoggerParameter__mdt(DeveloperName = 'QueryOrganizationDataSynchronously', Value__c = String.valueOf(false))); + + LoggerMockDataStore.getEventBus().publishRecord(logEntryEvent); + LoggerMockDataStore.getEventBus().deliver(new LogEntryEventHandler()); + + Organization expectedOrganization = LoggerEngineDataSelector.getInstance().getCachedOrganization(); + Log__c log = getLog(); + System.assertEquals(String.valueOf(expectedOrganization.Id), log.OrganizationId__c); + System.assertEquals(expectedOrganization.InstanceName, log.OrganizationInstanceName__c); + System.assertEquals(expectedOrganization.Name, log.OrganizationName__c); + System.assertEquals(expectedOrganization.NamespacePrefix, log.OrganizationNamespacePrefix__c); + System.assertEquals(expectedOrganization.OrganizationType, log.OrganizationType__c); + } + + @IsTest + static void it_should_query_and_set_user_data_when_synchronous_querying_is_disabled() { + LoggerDataStore.setMock(LoggerMockDataStore.getEventBus()); + LoggerTestConfigurator.setupMockSObjectHandlerConfigurations(); + LoggerTestConfigurator.getSObjectHandlerConfiguration(Schema.LoggerScenario__c.SObjectType).IsEnabled__c = false; + LoggerTestConfigurator.getSObjectHandlerConfiguration(Schema.Log__c.SObjectType).IsEnabled__c = false; + LoggerTestConfigurator.getSObjectHandlerConfiguration(Schema.LogEntry__c.SObjectType).IsEnabled__c = false; + LogEntryEvent__e logEntryEvent = createLogEntryEvent(); + LoggerTestConfigurator.setMock(new LoggerParameter__mdt(DeveloperName = 'QueryUserDataSynchronously', Value__c = String.valueOf(false))); + + LoggerMockDataStore.getEventBus().publishRecord(logEntryEvent); + LoggerMockDataStore.getEventBus().deliver(new LogEntryEventHandler()); + + List userIds = new List{ logEntryEvent.LoggedById__c }; + User expectedUser = LoggerEngineDataSelector.getInstance().getUsers(userIds).get(logEntryEvent.LoggedById__c); + Log__c log = getLog(); + System.assertEquals(expectedUser.Username, log.LoggedByUsername__c); + System.assertEquals(expectedUser.Profile.Name, log.ProfileName__c); + System.assertEquals(expectedUser.Profile.UserLicense.LicenseDefinitionKey, log.UserLicenseDefinitionKey__c); + System.assertEquals(expectedUser.Profile.UserLicenseId, log.UserLicenseId__c); + System.assertEquals(expectedUser.Profile.UserLicense.Name, log.UserLicenseName__c); + System.assertEquals(expectedUser.UserRole?.Name, log.UserRoleName__c); + } + // TODO - testing topics is tricky. Within the unlocked package, we can test it, // but for people that prefer using the unpackaged metadata + don't use topics, // the tests would fail. Need to further investigate ways to handle this @@ -470,8 +544,8 @@ private class LogEntryEventHandler_Tests { // ); // System.Test.startTest(); - // LogEntryEventHandler.TAGGING_IS_ENABLED = true; - // LogEntryEventHandler.TAG_USING_TOPICS = true; + // LogEntryEventHandler.ENABLE_TAGGING = true; + // LogEntryEventHandler.USE_TOPICS_FOR_TAGS = true; // Database.SaveResult saveResult = EventBus.publish(logEntryEvent); // System.Test.stopTest(); @@ -498,7 +572,7 @@ private class LogEntryEventHandler_Tests { LoggerTestConfigurator.setupMockSObjectHandlerConfigurations(); LoggerTestConfigurator.getSObjectHandlerConfiguration(Schema.Log__c.SObjectType).IsEnabled__c = false; LoggerTestConfigurator.getSObjectHandlerConfiguration(Schema.LogEntry__c.SObjectType).IsEnabled__c = false; - System.assertEquals(true, LoggerParameter.TAGGING_IS_ENABLED, 'Tagging is not enabled within test context, cannot execute tagging test'); + System.assertEquals(true, LoggerParameter.ENABLE_TAGGING, 'Tagging is not enabled within test context, cannot execute tagging test'); List tags = new List{ 'test-tag-1', 'test-tag-2' }; LogEntryEvent__e logEntryEvent = createLogEntryEvent(); System.assertNotEquals(null, logEntryEvent.EventUuid); @@ -533,7 +607,7 @@ private class LogEntryEventHandler_Tests { LoggerTestConfigurator.setupMockSObjectHandlerConfigurations(); LoggerTestConfigurator.getSObjectHandlerConfiguration(Schema.Log__c.SObjectType).IsEnabled__c = false; LoggerTestConfigurator.getSObjectHandlerConfiguration(Schema.LogEntry__c.SObjectType).IsEnabled__c = false; - System.assertEquals(true, LoggerParameter.TAGGING_IS_ENABLED, 'Tagging is not enabled within test context, cannot execute tagging test'); + System.assertEquals(true, LoggerParameter.ENABLE_TAGGING, 'Tagging is not enabled within test context, cannot execute tagging test'); String testTagName = 'Some tag!'; LoggerTag__c tag = new LoggerTag__c(Name = testTagName); insert tag; @@ -569,7 +643,7 @@ private class LogEntryEventHandler_Tests { LoggerTestConfigurator.setupMockSObjectHandlerConfigurations(); LoggerTestConfigurator.getSObjectHandlerConfiguration(Schema.Log__c.SObjectType).IsEnabled__c = false; LoggerTestConfigurator.getSObjectHandlerConfiguration(Schema.LogEntry__c.SObjectType).IsEnabled__c = false; - System.assertEquals(true, LoggerParameter.TAGGING_IS_ENABLED, 'Tagging is not enabled within test context, cannot execute tagging test'); + System.assertEquals(true, LoggerParameter.ENABLE_TAGGING, 'Tagging is not enabled within test context, cannot execute tagging test'); LogEntryEvent__e logEntryEvent = createLogEntryEvent(); logEntryEvent.Message__c = 'my message'; logEntryEvent.Tags__c = null; @@ -610,7 +684,7 @@ private class LogEntryEventHandler_Tests { LoggerTestConfigurator.setupMockSObjectHandlerConfigurations(); LoggerTestConfigurator.getSObjectHandlerConfiguration(Schema.Log__c.SObjectType).IsEnabled__c = false; LoggerTestConfigurator.getSObjectHandlerConfiguration(Schema.LogEntry__c.SObjectType).IsEnabled__c = false; - System.assertEquals(true, LoggerParameter.TAGGING_IS_ENABLED, 'Tagging is not enabled within test context, cannot execute tagging test'); + System.assertEquals(true, LoggerParameter.ENABLE_TAGGING, 'Tagging is not enabled within test context, cannot execute tagging test'); LogEntryEvent__e logEntryEvent = createLogEntryEvent(); logEntryEvent.Tags__c = null; String configuredTagName = 'CMDT Tag'; @@ -650,7 +724,7 @@ private class LogEntryEventHandler_Tests { LoggerTestConfigurator.setupMockSObjectHandlerConfigurations(); LoggerTestConfigurator.getSObjectHandlerConfiguration(Schema.Log__c.SObjectType).IsEnabled__c = false; LoggerTestConfigurator.getSObjectHandlerConfiguration(Schema.LogEntry__c.SObjectType).IsEnabled__c = false; - System.assertEquals(true, LoggerParameter.TAGGING_IS_ENABLED, 'Tagging is not enabled within test context, cannot execute tagging test'); + System.assertEquals(true, LoggerParameter.ENABLE_TAGGING, 'Tagging is not enabled within test context, cannot execute tagging test'); String zipCodeRegEx = '(^[0-9]{4}?[0-9]$|^[0-9]{4}?[0-9]-[0-9]{4}$)'; LogEntryEvent__e logEntryEvent = createLogEntryEvent(); logEntryEvent.Message__c = '94541'; @@ -692,7 +766,7 @@ private class LogEntryEventHandler_Tests { LoggerTestConfigurator.setupMockSObjectHandlerConfigurations(); LoggerTestConfigurator.getSObjectHandlerConfiguration(Schema.Log__c.SObjectType).IsEnabled__c = false; LoggerTestConfigurator.getSObjectHandlerConfiguration(Schema.LogEntry__c.SObjectType).IsEnabled__c = false; - System.assertEquals(true, LoggerParameter.TAGGING_IS_ENABLED, 'Tagging is not enabled within test context, cannot execute tagging test'); + System.assertEquals(true, LoggerParameter.ENABLE_TAGGING, 'Tagging is not enabled within test context, cannot execute tagging test'); LogEntryEvent__e logEntryEvent = createLogEntryEvent(); logEntryEvent.Message__c = 'my message'; logEntryEvent.Tags__c = null; @@ -919,8 +993,11 @@ private class LogEntryEventHandler_Tests { LoggedBy__c, LoggedByUsername__c, LoggerVersionNumber__c, + LoginApplication__c, + LoginBrowser__c, LoginDomain__c, LoginHistoryId__c, + LoginPlatform__c, LoginType__c, LogoutUrl__c, NetworkId__c, @@ -935,6 +1012,7 @@ private class LogEntryEventHandler_Tests { ParentLogTransactionId__c, ProfileId__c, ProfileName__c, + RequestId__c, Scenario__c, SessionId__c, SessionSecurityLevel__c, @@ -1070,6 +1148,7 @@ private class LogEntryEventHandler_Tests { System.assertEquals(logEntryEvent.ParentLogTransactionId__c, log.ParentLogTransactionId__c, 'log.ParentLogTransactionId__c was not properly set'); System.assertEquals(logEntryEvent.ProfileId__c, log.ProfileId__c, 'log.ProfileId__c was not properly set'); System.assertEquals(logEntryEvent.ProfileName__c, log.ProfileName__c, 'log.ProfileName__c was not properly set'); + System.assertEquals(logEntryEvent.RequestId__c, log.RequestId__c, 'log.RequestId__c was not properly set'); System.assertEquals(logEntryEvent.SessionId__c, log.SessionId__c, 'log.SessionId__c was not properly set'); System.assertEquals(logEntryEvent.SessionId__c, log.SessionId__c, 'log.SessionId__c was not properly set'); System.assertEquals(logEntryEvent.SessionSecurityLevel__c, log.SessionSecurityLevel__c, 'log.SessionSecurityLevel__c was not properly set'); 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 bed02834b..c7e5743b4 100644 --- a/nebula-logger/core/tests/log-management/classes/LogEntryHandler_Tests.cls +++ b/nebula-logger/core/tests/log-management/classes/LogEntryHandler_Tests.cls @@ -85,7 +85,26 @@ private class LogEntryHandler_Tests { } @IsTest - static void it_should_set_hasRecordJson_to_true_when_populated() { + static void it_should_not_populate_related_record_fields_on_log_entry_when_disabled_via_logger_parameter() { + Log__c log = [SELECT Id FROM Log__c LIMIT 1]; + User currentUser = [SELECT Id, Username FROM User WHERE Id = :UserInfo.getUserId()]; + LogEntry__c logEntry = new LogEntry__c(Log__c = log.Id, RecordId__c = currentUser.Id); + LoggerParameter.setMock(new LoggerParameter__mdt(DeveloperName = 'QueryRelatedRecordData', Value__c = String.valueOf(false))); + + LoggerDataStore.getDatabase().insertRecord(logEntry); + + System.assertEquals( + 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, RecordId__c, RecordName__c FROM LogEntry__c WHERE Id = :logEntry.Id]; + System.assertEquals(currentUser.Id, logEntry.RecordId__c); + System.assertEquals(null, logEntry.RecordName__c); + } + + @IsTest + static void it_should_set_hasRecordJson_to_true_when_populated_on_insert() { Log__c log = [SELECT Id FROM Log__c LIMIT 1]; String recordJson = '{}'; LogEntry__c logEntry = new LogEntry__c(Log__c = log.Id, RecordJson__c = recordJson); @@ -99,12 +118,31 @@ private class LogEntryHandler_Tests { 'Handler class should have executed two times - once for BEFORE_INSERT and once for AFTER_INSERT' ); logEntry = [SELECT Id, HasRecordJson__c, RecordJson__c FROM LogEntry__c WHERE Id = :logEntry.Id]; - System.assert(logEntry.HasRecordJson__c); + System.assertEquals(true, logEntry.HasRecordJson__c); System.assertEquals(recordJson, logEntry.RecordJson__c); } @IsTest - static void it_should_set_hasRecordJson_to_true_when_updated() { + static void it_should_set_hasRecordJson_to_false_when_not_populated_on_insert() { + Log__c log = [SELECT Id FROM Log__c LIMIT 1]; + String recordJson = null; + LogEntry__c logEntry = new LogEntry__c(Log__c = log.Id, RecordJson__c = recordJson); + LoggerMockDataCreator.createDataBuilder(logEntry).populateRequiredFields().getRecord(); + + LoggerDataStore.getDatabase().insertRecord(logEntry); + + System.assertEquals( + 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, HasRecordJson__c, RecordJson__c FROM LogEntry__c WHERE Id = :logEntry.Id]; + System.assertEquals(false, logEntry.HasRecordJson__c); + System.assertEquals(null, logEntry.RecordJson__c); + } + + @IsTest + static void it_should_set_hasRecordJson_to_true_when_populated_on_updated() { Log__c log = [SELECT Id FROM Log__c LIMIT 1]; LogEntry__c logEntry = new LogEntry__c(Log__c = log.Id, RecordJson__c = null); LoggerMockDataCreator.createDataBuilder(logEntry).populateRequiredFields().getRecord(); @@ -122,10 +160,33 @@ private class LogEntryHandler_Tests { 'Handler class should have executed four times - two times for BEFORE_INSERT/AFTER_INSERT' + ' and two more times for BEFORE_UPDATE/AFTER_UPDATE' ); logEntry = [SELECT Id, HasRecordJson__c, RecordJson__c FROM LogEntry__c WHERE Id = :logEntry.Id]; - System.assert(logEntry.HasRecordJson__c); + System.assertEquals(true, logEntry.HasRecordJson__c); System.assertEquals(recordJson, logEntry.RecordJson__c); } + @IsTest + static void it_should_set_hasRecordJson_to_false_when_not_populated_on_updated() { + Log__c log = [SELECT Id FROM Log__c LIMIT 1]; + String recordJson = '{}'; + LogEntry__c logEntry = new LogEntry__c(Log__c = log.Id, RecordJson__c = recordJson); + LoggerMockDataCreator.createDataBuilder(logEntry).populateRequiredFields().getRecord(); + LoggerDataStore.getDatabase().insertRecord(logEntry); + logEntry = [SELECT Id, RecordJson__c FROM LogEntry__c WHERE Id = :logEntry.Id]; + System.assertNotEquals(null, logEntry.RecordJson__c); + logEntry.RecordJson__c = null; + + update logEntry; + + System.assertEquals( + 4, + LoggerSObjectHandler.getExecutedHandlers().get(Schema.LogEntry__c.SObjectType).size(), + 'Handler class should have executed four times - two times for BEFORE_INSERT/AFTER_INSERT' + ' and two more times for BEFORE_UPDATE/AFTER_UPDATE' + ); + logEntry = [SELECT Id, HasRecordJson__c, RecordJson__c FROM LogEntry__c WHERE Id = :logEntry.Id]; + System.assertEquals(false, logEntry.HasRecordJson__c); + System.assertEquals(null, logEntry.RecordJson__c); + } + @IsTest static void it_should_set_hasExceptionStackTrace_to_false_when_null() { Log__c log = [SELECT Id FROM Log__c LIMIT 1]; @@ -396,4 +457,17 @@ private class LogEntryHandler_Tests { return namespacePrefix; } + + private class MockLogManagementDataSelector extends LogManagementDataSelector { + private Integer apexClassesQueryCount = 0; + + public override List getApexClasses(List apexClassNames) { + this.apexClassesQueryCount++; + return super.getApexClasses(apexClassNames); + } + + public Integer getApexClassesQueryCount() { + return apexClassesQueryCount; + } + } } 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 48f1fad2a..e1dfc3335 100644 --- a/nebula-logger/core/tests/log-management/classes/LogManagementDataSelector_Tests.cls +++ b/nebula-logger/core/tests/log-management/classes/LogManagementDataSelector_Tests.cls @@ -3,7 +3,7 @@ // See LICENSE file or go to https://github.com/jongpie/NebulaLogger for full license details. // //------------------------------------------------------------------------------------------------// -@SuppressWarnings('PMD.ApexDoc, PMD.CyclomaticComplexity, PMD.ExcessiveParameterList, PMD.MethodNamingConventions') +@SuppressWarnings('PMD.ApexDoc, PMD.CyclomaticComplexity, PMD.MethodNamingConventions') @IsTest(IsParallel=false) private class LogManagementDataSelector_Tests { @IsTest @@ -50,6 +50,19 @@ private class LogManagementDataSelector_Tests { System.assertEquals(expectedResults, returnedResults); } + @IsTest + static void it_does_not_query_apex_classes_when_disabled_via_logger_parameter() { + // The class names used in the query don't particularly matter here - the main concern is checking that the query does not execute at all + List targetApexClassNames = new List{ 'SomeClass', 'AnotherClass' }; + Integer originalQueryCount = Limits.getQueries(); + LoggerParameter.setMock(new LoggerParameter__mdt(DeveloperName = 'QueryApexClassData', Value__c = String.valueOf(false))); + + List returnedResults = LogManagementDataSelector.getInstance().getApexClasses(targetApexClassNames); + + System.assertEquals(originalQueryCount, Limits.getQueries()); + System.assertEquals(0, returnedResults.size()); + } + @IsTest static void it_returns_cached_apex_email_notifications() { List expectedResults = [SELECT Email, UserId FROM ApexEmailNotification WHERE Email != NULL OR User.IsActive = TRUE]; @@ -83,6 +96,39 @@ private class LogManagementDataSelector_Tests { // System.assertEquals(1, returnedCount); // } + @IsTest + static void it_returns_cached_recent_log_with_api_release_details() { + LoggerTestConfigurator.setupMockSObjectHandlerConfigurations(); + LoggerTestConfigurator.getSObjectHandlerConfiguration(Schema.Log__c.SObjectType).IsEnabled__c = false; + Log__c olderLog = new Log__c(ApiReleaseNumber__c = 'QWERTY', ApiReleaseVersion__c = 'ASDF', TransactionId__c = 'ABC'); + Log__c expectedLog = new Log__c(ApiReleaseNumber__c = 'QWERTY', ApiReleaseVersion__c = 'ASDF', TransactionId__c = 'XYZ'); + insert new List{ olderLog, expectedLog }; + System.Test.setCreatedDate(olderLog.Id, System.now().addMinutes(-5)); + System.assertEquals(1, Limits.getQueries()); + + for (Integer i = 0; i < 5; i++) { + Log__c returnedLog = LogManagementDataSelector.getInstance().getCachedRecentLogWithApiReleaseDetails(); + + System.assertEquals(expectedLog.Id, returnedLog.Id); + } + + System.assertEquals(2, Limits.getQueries()); + } + + @IsTest + static void it_returns_null_when_no_recent_log_with_api_release_details_is_found() { + System.assertEquals(0, [SELECT COUNT() FROM Log__c]); + System.assertEquals(1, Limits.getQueries()); + + for (Integer i = 0; i < 5; i++) { + Log__c returnedLog = LogManagementDataSelector.getInstance().getCachedRecentLogWithApiReleaseDetails(); + + System.assertEquals(null, returnedLog); + } + + System.assertEquals(2, Limits.getQueries()); + } + @IsTest static void it_returns_count_of_related_record_log_entries() { Id targetRecordId = UserInfo.getUserId(); @@ -104,8 +150,19 @@ private class LogManagementDataSelector_Tests { System.assertEquals(1, returnedCount); } - // TODO add integration test for LogManagementDataSelector.getInstance().getDeleteableUserRecordAccess(List recordIds); - // TODO add integration test for LogManagementDataSelector.getInstance().getFlowVersionViewsByDurableId(List durableIds); + @IsTest + static void it_returns_deleteable_user_record_access() { + LoggerSObjectHandler.shouldExecute(false); + Log__c deleteableRecord = (Log__c) LoggerMockDataCreator.createDataBuilder(Schema.Log__c.SObjectType).populateRequiredFields().getRecord(); + insert deleteableRecord; + User undeleteableRecord = new User(Id = UserInfo.getUserId()); + List recordIds = new List{ deleteableRecord.Id, undeleteableRecord.Id }; + + List returnedResults = LogManagementDataSelector.getInstance().getDeleteableUserRecordAccess(recordIds); + + System.assertEquals(1, returnedResults.size()); + System.assertEquals(deleteableRecord.Id, returnedResults.get(0).RecordId); + } @IsTest static void it_returns_log_for_specified_log_id() { @@ -155,22 +212,25 @@ private class LogManagementDataSelector_Tests { @IsTest static void it_returns_profiles_for_specified_profile_ids() { - List expectedResults = [SELECT Id, Name FROM Profile ORDER BY Name LIMIT 3]; - expectedResults.sort(); + List expectedResults = [SELECT Id, Name FROM Profile LIMIT 10]; List targetProfileIds = new List(new Map(expectedResults).keySet()); List returnedResults = LogManagementDataSelector.getInstance().getProfilesById(targetProfileIds); + expectedResults.sort(); + returnedResults.sort(); System.assertEquals(expectedResults, returnedResults); } @IsTest static void it_returns_profiles_for_specified_search_term() { String searchTerm = 'Admin'; - List expectedResults = [SELECT Id, Name, UserLicense.Name FROM Profile WHERE Name LIKE :searchTerm ORDER BY Name]; + List expectedResults = [SELECT Id, Name, UserLicense.Name FROM Profile WHERE Name LIKE :searchTerm]; List returnedResults = LogManagementDataSelector.getInstance().getProfilesByNameSearch(searchTerm); + expectedResults.sort(); + returnedResults.sort(); System.assertEquals(expectedResults, returnedResults); } @@ -257,32 +317,31 @@ private class LogManagementDataSelector_Tests { @IsTest static void it_returns_users_for_user_ids() { - List expectedResults = [SELECT Id, Username FROM User ORDER BY Username LIMIT 3]; + List expectedResults = [SELECT Id, Username FROM User LIMIT 3]; List targetUserIds = new List(new Map(expectedResults).keySet()); List returnedResults = LogManagementDataSelector.getInstance().getUsersById(targetUserIds); + expectedResults.sort(); + returnedResults.sort(); System.assertEquals(expectedResults, returnedResults); } @IsTest static void it_returns_user_for_specified_search_term() { String searchTerm = UserInfo.getLastName(); - List expectedResults = [ - SELECT Id, Name, Username, SmallPhotoUrl - FROM User - WHERE Name LIKE :searchTerm OR Username LIKE :searchTerm - ORDER BY Username - ]; + List expectedResults = [SELECT Id, Name, Username, SmallPhotoUrl FROM User WHERE Name LIKE :searchTerm OR Username LIKE :searchTerm]; List returnedResults = LogManagementDataSelector.getInstance().getUsersByNameSearch(searchTerm); + expectedResults.sort(); + returnedResults.sort(); System.assertEquals(expectedResults, returnedResults); } @IsTest static void it_returns_users_for_user_usernames() { - List expectedResults = [SELECT Id, Username FROM User ORDER BY Username LIMIT 3]; + List expectedResults = [SELECT Id, Username FROM User LIMIT 3]; List targetUserUsernames = new List(); for (User user : expectedResults) { targetUserUsernames.add(user.Username); @@ -290,12 +349,27 @@ private class LogManagementDataSelector_Tests { List returnedResults = LogManagementDataSelector.getInstance().getUsersByUsername(targetUserUsernames); + expectedResults.sort(); + returnedResults.sort(); System.assertEquals(expectedResults, returnedResults); } + @IsTest + static void it_loads_mock_instance() { + MockLogManagementDataSelector mockSelector = new MockLogManagementDataSelector(); + System.assertNotEquals(mockSelector, LogManagementDataSelector.getInstance()); + + LogManagementDataSelector.setMock(mockSelector); + + System.assertEquals(mockSelector, LogManagementDataSelector.getInstance()); + } + @SuppressWarnings('PMD.EmptyStatementBlock') @future private static void executeSomeFutureMethod() { // This method intentionally does nothing - it\'s only used to help test queries on AsynxApexJob } + + private class MockLogManagementDataSelector extends LogManagementDataSelector { + } } diff --git a/nebula-logger/core/tests/logger-engine/classes/FlowCollectionLogEntry_Tests.cls b/nebula-logger/core/tests/logger-engine/classes/FlowCollectionLogEntry_Tests.cls index a33546aea..41b1eb453 100644 --- a/nebula-logger/core/tests/logger-engine/classes/FlowCollectionLogEntry_Tests.cls +++ b/nebula-logger/core/tests/logger-engine/classes/FlowCollectionLogEntry_Tests.cls @@ -32,6 +32,7 @@ private class FlowCollectionLogEntry_Tests { FlowCollectionLogEntry flowCollectionEntry = createFlowCollectionLogEntry(); flowCollectionEntry.loggingLevelName = flowCollectionEntryLoggingLevel.name(); flowCollectionEntry.records = new List{ currentUser }; + flowCollectionEntry.timestamp = System.now().addSeconds(-20); System.assertEquals(0, Logger.saveLogCallCount); System.assertEquals(0, LoggerMockDataStore.getEventBus().getPublishCallCount()); System.assertEquals(0, LoggerMockDataStore.getEventBus().getPublishedPlatformEvents().size()); @@ -52,6 +53,7 @@ private class FlowCollectionLogEntry_Tests { System.assertEquals('List', publishedLogEntryEvent.RecordCollectionType__c); System.assertEquals('User', publishedLogEntryEvent.RecordSObjectType__c); System.assertEquals(expectedUserJson, publishedLogEntryEvent.RecordJson__c); + System.assertEquals(flowCollectionEntry.timestamp, publishedLogEntryEvent.Timestamp__c); } @IsTest diff --git a/nebula-logger/core/tests/logger-engine/classes/FlowLogEntry_Tests.cls b/nebula-logger/core/tests/logger-engine/classes/FlowLogEntry_Tests.cls index b179fb092..494d0965c 100644 --- a/nebula-logger/core/tests/logger-engine/classes/FlowLogEntry_Tests.cls +++ b/nebula-logger/core/tests/logger-engine/classes/FlowLogEntry_Tests.cls @@ -25,6 +25,7 @@ private class FlowLogEntry_Tests { LoggerTestConfigurator.setupMockSObjectHandlerConfigurations(); FlowLogEntry flowEntry = createFlowLogEntry(); flowEntry.loggingLevelName = flowEntryLoggingLevel.name(); + flowEntry.timestamp = System.now().addSeconds(-20); System.assertEquals(0, Logger.saveLogCallCount); System.assertEquals(0, LoggerMockDataStore.getEventBus().getPublishCallCount()); System.assertEquals(0, LoggerMockDataStore.getEventBus().getPublishedPlatformEvents().size()); @@ -40,6 +41,7 @@ private class FlowLogEntry_Tests { LogEntryEvent__e publishedLogEntryEvent = (LogEntryEvent__e) LoggerMockDataStore.getEventBus().getPublishedPlatformEvents().get(0); System.assertEquals(flowEntry.loggingLevelName, publishedLogEntryEvent.LoggingLevel__c); System.assertEquals(flowEntry.message, publishedLogEntryEvent.Message__c); + System.assertEquals(flowEntry.timestamp, publishedLogEntryEvent.Timestamp__c); System.assertEquals('Flow', publishedLogEntryEvent.OriginType__c); } diff --git a/nebula-logger/core/tests/logger-engine/classes/FlowRecordLogEntry_Tests.cls b/nebula-logger/core/tests/logger-engine/classes/FlowRecordLogEntry_Tests.cls index f4e16709f..26bc358f3 100644 --- a/nebula-logger/core/tests/logger-engine/classes/FlowRecordLogEntry_Tests.cls +++ b/nebula-logger/core/tests/logger-engine/classes/FlowRecordLogEntry_Tests.cls @@ -33,6 +33,7 @@ private class FlowRecordLogEntry_Tests { FlowRecordLogEntry flowRecordEntry = createFlowRecordLogEntry(); flowRecordEntry.loggingLevelName = flowRecordEntryLoggingLevel.name(); flowRecordEntry.record = currentUser; + flowRecordEntry.timestamp = System.now().addSeconds(-20); System.assertEquals(0, Logger.saveLogCallCount); System.assertEquals(0, LoggerMockDataStore.getEventBus().getPublishCallCount()); System.assertEquals(0, LoggerMockDataStore.getEventBus().getPublishedPlatformEvents().size()); @@ -52,6 +53,7 @@ private class FlowRecordLogEntry_Tests { System.assertEquals('Flow', publishedLogEntryEvent.OriginType__c); System.assertEquals(currentUser.Id, publishedLogEntryEvent.RecordId__c); System.assertEquals(expectedUserJson, publishedLogEntryEvent.RecordJson__c); + System.assertEquals(flowRecordEntry.timestamp, publishedLogEntryEvent.Timestamp__c); } @IsTest 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 89135a041..64e796091 100644 --- a/nebula-logger/core/tests/logger-engine/classes/LogEntryEventBuilder_Tests.cls +++ b/nebula-logger/core/tests/logger-engine/classes/LogEntryEventBuilder_Tests.cls @@ -35,26 +35,182 @@ private class LogEntryEventBuilder_Tests { System.assertEquals(null, builder.parseStackTrace(new DmlException().getStackTraceString()).getLogEntryEvent()); } + @IsTest + static void it_should_short_circuit_when_enabled_logging_level_above_called_level() { + LogEntryEventBuilder builder = new LogEntryEventBuilder(getUserSettings(), LoggingLevel.FINE, false, new Set()); + System.assertEquals(null, builder.getLogEntryEvent()); + } + + @IsTest + static void it_should_not_short_circuit_when_enabled() { + LogEntryEventBuilder builder = new LogEntryEventBuilder(getUserSettings(), LoggingLevel.FINE, true, new Set()); + System.assertNotEquals(null, builder.getLogEntryEvent()); + } + @IsTest static void it_should_not_run_queries_when_logging_disabled() { + LoggerParameter.setMock(new LoggerParameter__mdt(DeveloperName = 'QueryAuthSessionDataSynchronously', Value__c = String.valueOf(true))); + LoggerParameter.setMock(new LoggerParameter__mdt(DeveloperName = 'QueryNetworkDataSynchronously', Value__c = String.valueOf(true))); + LoggerParameter.setMock(new LoggerParameter__mdt(DeveloperName = 'QueryOrganizationDataSynchronously', Value__c = String.valueOf(true))); + LoggerParameter.setMock(new LoggerParameter__mdt(DeveloperName = 'QueryUserDataSynchronously', Value__c = String.valueOf(true))); Logger.getUserSettings().IsEnabled__c = false; System.assertEquals(false, Logger.isEnabled()); System.assertEquals(0, Limits.getQueries()); - new LogEntryEventBuilder(getUserSettings(), LoggingLevel.FINE, false, new Set()).setMessage('some message').getLogEntryEvent(); + new LogEntryEventBuilder(getUserSettings(), LoggingLevel.INFO, Logger.getUserSettings().IsEnabled__c, new Set()) + .setMessage('some message') + .getLogEntryEvent(); System.assertEquals(0, Limits.getQueries()); } @IsTest - static void it_should_short_circuit_when_enabled_logging_level_above_called_level() { - LogEntryEventBuilder builder = new LogEntryEventBuilder(getUserSettings(), LoggingLevel.FINE, false, new Set()); - System.assertEquals(null, builder.getLogEntryEvent()); + static void it_should_not_run_authSession_query_when_disabled_via_logger_parameter() { + LoggerParameter.setMock(new LoggerParameter__mdt(DeveloperName = 'QueryAuthSessionDataSynchronously', Value__c = String.valueOf(false))); + System.assertEquals(false, LoggerParameter.QUERY_AUTH_SESSION_DATA_SYNCHRONOUSLY); + MockLoggerEngineDataSelector mockSelector = new MockLoggerEngineDataSelector(); + LoggerEngineDataSelector.setMock(mockSelector); + System.assertEquals(0, mockSelector.getCachedAuthSessionQueryCount()); + + LogEntryEvent__e logEntryEvent = new LogEntryEventBuilder(getUserSettings(), LoggingLevel.INFO, true, new Set()) + .setMessage('some message') + .getLogEntryEvent(); + + System.assertEquals(0, mockSelector.getCachedAuthSessionQueryCount()); + System.assertEquals(null, logEntryEvent.LoginApplication__c); + System.assertEquals(null, logEntryEvent.LoginBrowser__c); + System.assertEquals(null, logEntryEvent.LoginHistoryId__c); + System.assertEquals(null, logEntryEvent.LoginPlatform__c); + System.assertEquals(null, logEntryEvent.LoginType__c); + System.assertEquals(null, logEntryEvent.LogoutUrl__c); + System.assertEquals(null, logEntryEvent.SessionId__c); + System.assertEquals(null, logEntryEvent.SessionSecurityLevel__c); + System.assertEquals(null, logEntryEvent.SessionType__c); + System.assertEquals(null, logEntryEvent.SourceIp__c); } @IsTest - static void it_should_not_short_circuit_when_enabledl() { - LogEntryEventBuilder builder = new LogEntryEventBuilder(getUserSettings(), LoggingLevel.FINE, true, new Set()); - System.assertNotEquals(null, builder.getLogEntryEvent()); + static void it_should_run_authSession_query_when_enabled_via_logger_parameter() { + LoggerParameter.setMock(new LoggerParameter__mdt(DeveloperName = 'QueryAuthSessionDataSynchronously', Value__c = String.valueOf(true))); + System.assertEquals(true, LoggerParameter.QUERY_AUTH_SESSION_DATA_SYNCHRONOUSLY); + MockLoggerEngineDataSelector mockSelector = new MockLoggerEngineDataSelector(); + LoggerEngineDataSelector.setMock(mockSelector); + System.assertEquals(0, mockSelector.getCachedAuthSessionQueryCount()); + + LogEntryEvent__e logEntryEvent = new LogEntryEventBuilder(getUserSettings(), LoggingLevel.INFO, true, new Set()) + .setMessage('some message') + .getLogEntryEvent(); + + System.assertEquals(1, mockSelector.getCachedAuthSessionQueryCount()); + AuthSession cachedAuthSession = LoggerEngineDataSelector.getInstance().getCachedAuthSession(); + System.assertEquals(cachedAuthSession?.LoginHistory.Application, logEntryEvent.LoginApplication__c); + System.assertEquals(cachedAuthSession?.LoginHistory.Browser, logEntryEvent.LoginBrowser__c); + System.assertEquals(cachedAuthSession?.LoginHistoryId, logEntryEvent.LoginHistoryId__c); + System.assertEquals(cachedAuthSession?.LoginHistory.Platform, logEntryEvent.LoginPlatform__c); + System.assertEquals(cachedAuthSession?.LoginType, logEntryEvent.LoginType__c); + System.assertEquals(cachedAuthSession?.LogoutUrl, logEntryEvent.LogoutUrl__c); + System.assertEquals(cachedAuthSession?.Id, logEntryEvent.SessionId__c); + System.assertEquals(cachedAuthSession?.SessionSecurityLevel, logEntryEvent.SessionSecurityLevel__c); + System.assertEquals(cachedAuthSession?.SessionType, logEntryEvent.SessionType__c); + System.assertEquals(cachedAuthSession?.SourceIp, logEntryEvent.SourceIp__c); + } + + @IsTest + static void it_should_not_run_organization_query_when_disabled_via_logger_parameter() { + LoggerParameter.setMock(new LoggerParameter__mdt(DeveloperName = 'QueryOrganizationDataSynchronously', Value__c = String.valueOf(false))); + System.assertEquals(false, LoggerParameter.QUERY_ORGANIZATION_DATA_SYNCHRONOUSLY); + MockLoggerEngineDataSelector mockSelector = new MockLoggerEngineDataSelector(); + LoggerEngineDataSelector.setMock(mockSelector); + System.assertEquals(0, mockSelector.getCachedOrganizationQueryCount()); + + LogEntryEvent__e logEntryEvent = new LogEntryEventBuilder(getUserSettings(), LoggingLevel.INFO, true, new Set()) + .setMessage('some message') + .getLogEntryEvent(); + + System.assertEquals(0, mockSelector.getCachedOrganizationQueryCount()); + System.assertEquals(null, logEntryEvent.OrganizationId__c); + System.assertEquals(null, logEntryEvent.OrganizationInstanceName__c); + System.assertEquals(null, logEntryEvent.OrganizationName__c); + System.assertEquals(null, logEntryEvent.OrganizationNamespacePrefix__c); + System.assertEquals(null, logEntryEvent.OrganizationType__c); + } + + @IsTest + static void it_should_run_organization_query_when_enabled_via_logger_parameter() { + LoggerParameter.setMock(new LoggerParameter__mdt(DeveloperName = 'QueryOrganizationDataSynchronously', Value__c = String.valueOf(true))); + System.assertEquals(true, LoggerParameter.QUERY_ORGANIZATION_DATA_SYNCHRONOUSLY); + MockLoggerEngineDataSelector mockSelector = new MockLoggerEngineDataSelector(); + LoggerEngineDataSelector.setMock(mockSelector); + System.assertEquals(0, mockSelector.getCachedOrganizationQueryCount()); + + LogEntryEvent__e logEntryEvent = new LogEntryEventBuilder(getUserSettings(), LoggingLevel.INFO, true, new Set()) + .setMessage('some message') + .getLogEntryEvent(); + + System.assertNotEquals(0, mockSelector.getCachedOrganizationQueryCount()); + Organization cachedOrganization = LoggerEngineDataSelector.getInstance().getCachedOrganization(); + System.assertEquals(cachedOrganization.Id, logEntryEvent.OrganizationId__c); + System.assertEquals(cachedOrganization.InstanceName, logEntryEvent.OrganizationInstanceName__c); + System.assertEquals(cachedOrganization.Name, logEntryEvent.OrganizationName__c); + System.assertEquals(cachedOrganization.NamespacePrefix, logEntryEvent.OrganizationNamespacePrefix__c); + System.assertEquals(cachedOrganization.OrganizationType, logEntryEvent.OrganizationType__c); + } + + @IsTest + static void it_should_not_run_user_query_when_disabled_via_logger_parameter() { + LoggerParameter.setMock(new LoggerParameter__mdt(DeveloperName = 'QueryUserDataSynchronously', Value__c = String.valueOf(false))); + System.assertEquals(false, LoggerParameter.QUERY_USER_DATA_SYNCHRONOUSLY); + MockLoggerEngineDataSelector mockSelector = new MockLoggerEngineDataSelector(); + LoggerEngineDataSelector.setMock(mockSelector); + System.assertEquals(0, mockSelector.getCachedUserQueryCount()); + + LogEntryEvent__e logEntryEvent = new LogEntryEventBuilder(getUserSettings(), LoggingLevel.INFO, true, new Set()) + .setMessage('some message') + .getLogEntryEvent(); + + System.assertEquals(0, mockSelector.getCachedUserQueryCount()); + System.assertEquals(null, logEntryEvent.LoggedByUsername__c); + System.assertEquals(null, logEntryEvent.ProfileName__c); + System.assertEquals(null, logEntryEvent.UserLicenseDefinitionKey__c); + System.assertEquals(null, logEntryEvent.UserLicenseId__c); + System.assertEquals(null, logEntryEvent.UserLicenseName__c); + System.assertEquals(null, logEntryEvent.UserRoleName__c); + } + + @IsTest + static void it_should_run_user_query_when_enabled_via_logger_parameter() { + LoggerParameter.setMock(new LoggerParameter__mdt(DeveloperName = 'QueryUserDataSynchronously', Value__c = String.valueOf(true))); + System.assertEquals(true, LoggerParameter.QUERY_USER_DATA_SYNCHRONOUSLY); + MockLoggerEngineDataSelector mockSelector = new MockLoggerEngineDataSelector(); + LoggerEngineDataSelector.setMock(mockSelector); + System.assertEquals(0, mockSelector.getCachedUserQueryCount()); + + LogEntryEvent__e logEntryEvent = new LogEntryEventBuilder(getUserSettings(), LoggingLevel.INFO, true, new Set()) + .setMessage('some message') + .getLogEntryEvent(); + + System.assertNotEquals(0, mockSelector.getCachedUserQueryCount()); + User cachedUser = LoggerEngineDataSelector.getInstance().getCachedUser(); + System.assertEquals(cachedUser.Username, logEntryEvent.LoggedByUsername__c); + System.assertEquals(cachedUser.Profile.Name, logEntryEvent.ProfileName__c); + System.assertEquals(cachedUser.Profile.UserLicense.LicenseDefinitionKey, logEntryEvent.UserLicenseDefinitionKey__c); + System.assertEquals(cachedUser.Profile.UserLicenseId, logEntryEvent.UserLicenseId__c); + System.assertEquals(cachedUser.Profile.UserLicense.Name, logEntryEvent.UserLicenseName__c); + System.assertEquals(cachedUser.UserRole?.Name, logEntryEvent.UserRoleName__c); + } + + @IsTest + static void it_should_keep_timestamp_string_in_sync_with_timestamp() { + LogEntryEventBuilder builder = new LogEntryEventBuilder(getUserSettings(), LoggingLevel.INFO, true, new Set()).setMessage('some message'); + + for (Integer i = 0; i < 5; i++) { + builder.getLogEntryEvent().Timestamp__c = builder.getLogEntryEvent().Timestamp__c.addSeconds(i); + + System.assertEquals( + String.valueOf(builder.getLogEntryEvent().Timestamp__c.getTime()), + builder.getLogEntryEvent().TimestampString__c, + 'Timestamp string was inaccurate when i ==' + i + ); + } } @IsTest @@ -607,6 +763,27 @@ private class LogEntryEventBuilder_Tests { System.assertEquals(LogStatus__mdt.SObjectType.getDescribe().getName(), builder.getLogEntryEvent().RecordSObjectType__c); } + @IsTest + static void it_should_set_record_fields_for_record_when_platform_event() { + LogEntryEventBuilder builder = new LogEntryEventBuilder(getUserSettings(), LoggingLevel.INFO, true, new Set()); + System.assertEquals(null, builder.getLogEntryEvent().RecordId__c); + System.assertEquals(null, builder.getLogEntryEvent().RecordJson__c); + System.assertEquals(null, builder.getLogEntryEvent().RecordSObjectClassification__c); + System.assertEquals(null, builder.getLogEntryEvent().RecordSObjectType__c); + System.assertEquals(null, builder.getLogEntryEvent().RecordSObjectTypeNamespace__c); + + // To avoid creating a new platform event just for testing purposes, Nebula Logger's LogEntryEvent__e is reused + LogEntryEvent__e platformEvent = new LogEntryEvent__e(); + builder.setRecordId(platformEvent); + + System.assertEquals(1, builder.getLogEntryEvent().RecordCollectionSize__c); + System.assertEquals('Single', builder.getLogEntryEvent().RecordCollectionType__c); + System.assertEquals(null, builder.getLogEntryEvent().RecordId__c); + System.assertEquals(JSON.serializePretty(platformEvent), builder.getLogEntryEvent().RecordJson__c); + System.assertEquals('Platform Event Object', builder.getLogEntryEvent().RecordSObjectClassification__c); + System.assertEquals(LogEntryEvent__e.SObjectType.getDescribe().getName(), builder.getLogEntryEvent().RecordSObjectType__c); + } + @IsTest static void it_should_skip_stripping_inaccessible_fields_for_aggregate_result() { User standardUser = LoggerMockDataCreator.createUser(); @@ -816,7 +993,44 @@ private class LogEntryEventBuilder_Tests { } @IsTest - static void it_should_set_stack_trace_and_origin_location_for_method_constructor_trace_string() { + static void it_should_not_set_stack_trace_for_new_builder_instance_when_disabled_via_logger_parameter() { + // Don't bother testing stack trace logic when using a namespace prefix - there are + // some platform limitations that prevent these tests from behaving as expected + if (String.isNotBlank(Logger.getNamespacePrefix()) == true) { + return; + } + + LoggerParameter__mdt mockParameter = new LoggerParameter__mdt(DeveloperName = 'EnableStackTraceParsing', Value__c = String.valueOf(false)); + LoggerParameter.setMock(mockParameter); + System.assertEquals(false, LoggerParameter.ENABLE_STACK_TRACE_PARSING); + + LogEntryEventBuilder builder = new LogEntryEventBuilder(getUserSettings(), LoggingLevel.INFO, true, new Set()); + + System.assertEquals(null, builder.getLogEntryEvent().OriginLocation__c); + System.assertEquals(null, builder.getLogEntryEvent().StackTrace__c); + } + + @IsTest + static void it_should_not_set_stack_trace_for_parseStackTrace_method_when_disabled_via_logger_parameter() { + // Don't bother testing stack trace logic when using a namespace prefix - there are + // some platform limitations that prevent these tests from behaving as expected + if (String.isNotBlank(Logger.getNamespacePrefix()) == true) { + return; + } + + LoggerParameter__mdt mockParameter = new LoggerParameter__mdt(DeveloperName = 'EnableStackTraceParsing', Value__c = String.valueOf(false)); + LoggerParameter.setMock(mockParameter); + System.assertEquals(false, LoggerParameter.ENABLE_STACK_TRACE_PARSING); + LogEntryEventBuilder builder = new LogEntryEventBuilder(getUserSettings(), LoggingLevel.INFO, true, new Set()); + + builder.parseStackTrace('AnonymousBlock: line 1, column 1'); + + System.assertEquals(null, builder.getLogEntryEvent().OriginLocation__c); + System.assertEquals(null, builder.getLogEntryEvent().StackTrace__c); + } + + @IsTest + static void it_should_set_stack_trace_and_origin_location_for_class_constructor_trace_string() { // Don't bother testing stack trace logic when using a namespace prefix - there are // some platform limitations that prevent these tests from behaving as expected if (String.isNotBlank(Logger.getNamespacePrefix()) == true) { @@ -953,7 +1167,8 @@ private class LogEntryEventBuilder_Tests { @IsTest static void it_should_set_user_fields_when_anonymous_mode_disabled() { - // Get expected data + LoggerParameter.setMock(new LoggerParameter__mdt(DeveloperName = 'QueryOrganizationDataSynchronously', Value__c = String.valueOf(true))); + LoggerParameter.setMock(new LoggerParameter__mdt(DeveloperName = 'QueryUserDataSynchronously', Value__c = String.valueOf(true))); Organization organization = LoggerMockDataCreator.getOrganization(); String organizationEnvironmentType = LoggerMockDataCreator.getOrganizationEnvironmentType(); User user = LoggerMockDataCreator.getUser(); @@ -962,6 +1177,7 @@ private class LogEntryEventBuilder_Tests { // Verify organization fields System.assertEquals(Url.getOrgDomainUrl().toExternalForm(), builder.getLogEntryEvent().OrganizationDomainUrl__c); + // TODO switch to using mock instances of Organization & re-enable these asserts System.assertEquals(organizationEnvironmentType, builder.getLogEntryEvent().OrganizationEnvironmentType__c); System.assertEquals(organization.Id, builder.getLogEntryEvent().OrganizationId__c); System.assertEquals(organization.InstanceName, builder.getLogEntryEvent().OrganizationInstanceName__c); @@ -970,13 +1186,13 @@ private class LogEntryEventBuilder_Tests { System.assertEquals(organization.OrganizationType, builder.getLogEntryEvent().OrganizationType__c); // Verify user fields - System.assertEquals(USER.Id, builder.getLogEntryEvent().LoggedById__c); - System.assertEquals(USER.Username, builder.getLogEntryEvent().LoggedByUsername__c); - System.assertEquals(USER.Profile.Name, builder.getLogEntryEvent().ProfileName__c); - System.assertEquals(USER.Profile.UserLicense.LicenseDefinitionKey, builder.getLogEntryEvent().UserLicenseDefinitionKey__c); - System.assertEquals(USER.Profile.UserLicenseId, builder.getLogEntryEvent().UserLicenseId__c); - System.assertEquals(USER.Profile.UserLicense.Name, builder.getLogEntryEvent().UserLicenseName__c); - System.assertEquals(USER.UserRole?.Name, builder.getLogEntryEvent().UserRoleName__c); + System.assertEquals(user.Id, builder.getLogEntryEvent().LoggedById__c); + System.assertEquals(user.Username, builder.getLogEntryEvent().LoggedByUsername__c); + System.assertEquals(user.Profile.Name, builder.getLogEntryEvent().ProfileName__c); + System.assertEquals(user.Profile.UserLicense.LicenseDefinitionKey, builder.getLogEntryEvent().UserLicenseDefinitionKey__c); + System.assertEquals(user.Profile.UserLicenseId, builder.getLogEntryEvent().UserLicenseId__c); + System.assertEquals(user.Profile.UserLicense.Name, builder.getLogEntryEvent().UserLicenseName__c); + System.assertEquals(user.UserRole?.Name, builder.getLogEntryEvent().UserRoleName__c); } @IsTest @@ -1084,4 +1300,37 @@ private class LogEntryEventBuilder_Tests { return Logger.debug(loggingString); } } + + private class MockLoggerEngineDataSelector extends LoggerEngineDataSelector { + private Integer cachedAuthSessionQueryCount = 0; + private Integer cachedOrganizationQueryCount = 0; + private Integer cachedUserQueryCount = 0; + + public override AuthSession getCachedAuthSession() { + this.cachedAuthSessionQueryCount++; + return super.getCachedAuthSession(); + } + + public Integer getCachedAuthSessionQueryCount() { + return cachedAuthSessionQueryCount; + } + + public override Organization getCachedOrganization() { + this.cachedOrganizationQueryCount++; + return super.getCachedOrganization(); + } + + public Integer getCachedOrganizationQueryCount() { + return cachedOrganizationQueryCount; + } + + public override User getCachedUser() { + this.cachedUserQueryCount++; + return super.getCachedUser(); + } + + public Integer getCachedUserQueryCount() { + return cachedUserQueryCount; + } + } } diff --git a/nebula-logger/core/tests/logger-engine/classes/LoggerCache_Tests.cls b/nebula-logger/core/tests/logger-engine/classes/LoggerCache_Tests.cls deleted file mode 100644 index c3873055c..000000000 --- a/nebula-logger/core/tests/logger-engine/classes/LoggerCache_Tests.cls +++ /dev/null @@ -1,35 +0,0 @@ -//------------------------------------------------------------------------------------------------// -// 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.CyclomaticComplexity, PMD.MethodNamingConventions') -@IsTest(IsParallel=true) -private class LoggerCache_Tests { - @IsTest - static void it_adds_new_key_to_cache() { - String mockKeyName = 'SomeKey'; - User mockValue = new User(Id = UserInfo.getUserId(), ProfileId = UserInfo.getProfileId()); - System.assertEquals(false, LoggerCache.getTransactionCache().contains(mockKeyName)); - - LoggerCache.getTransactionCache().put(mockKeyName, mockValue); - - System.assertEquals(true, LoggerCache.getTransactionCache().contains(mockKeyName)); - System.assertEquals(mockValue, LoggerCache.getTransactionCache().get(mockKeyName)); - } - - @IsTest - static void it_updates_value_for_existing_key_in_cache() { - String mockKeyName = 'SomeKey'; - User oldMockValue = new User(Id = UserInfo.getUserId(), ProfileId = UserInfo.getProfileId()); - LoggerCache.getTransactionCache().put(mockKeyName, oldMockValue); - System.assertEquals(true, LoggerCache.getTransactionCache().contains(mockKeyName)); - System.assertEquals(oldMockValue, LoggerCache.getTransactionCache().get(mockKeyName)); - Account newMockValue = new Account(Name = 'Some fake account'); - - LoggerCache.getTransactionCache().put(mockKeyName, newMockValue); - - System.assertEquals(true, LoggerCache.getTransactionCache().contains(mockKeyName)); - System.assertEquals(newMockValue, LoggerCache.getTransactionCache().get(mockKeyName)); - } -} diff --git a/nebula-logger/core/tests/logger-engine/classes/LoggerEngineDataSelector_Tests.cls b/nebula-logger/core/tests/logger-engine/classes/LoggerEngineDataSelector_Tests.cls index 4e3ae27d3..3249dba78 100644 --- a/nebula-logger/core/tests/logger-engine/classes/LoggerEngineDataSelector_Tests.cls +++ b/nebula-logger/core/tests/logger-engine/classes/LoggerEngineDataSelector_Tests.cls @@ -6,6 +6,16 @@ @SuppressWarnings('PMD.ApexDoc, PMD.CyclomaticComplexity, PMD.ExcessiveParameterList, PMD.MethodNamingConventions') @IsTest(IsParallel=true) private class LoggerEngineDataSelector_Tests { + @IsTest + static void it_loads_mock_instance() { + MockLoggerEngineDataSelector mockSelector = new MockLoggerEngineDataSelector(); + System.assertNotEquals(mockSelector, LoggerEngineDataSelector.getInstance()); + + LoggerEngineDataSelector.setMock(mockSelector); + + System.assertEquals(mockSelector, LoggerEngineDataSelector.getInstance()); + } + @IsTest static void it_returns_cached_auth_session() { List sessions = [ @@ -34,6 +44,20 @@ private class LoggerEngineDataSelector_Tests { System.assertEquals(expectedAuthSession, returnedAuthSession); } + @IsTest + static void it_does_not_query_auth_session_when_disabled_via_logger_parameter() { + MockLoggerEngineDataSelector mockSelector = new MockLoggerEngineDataSelector(); + LoggerEngineDataSelector.setMock(mockSelector); + System.assertEquals(mockSelector, LoggerEngineDataSelector.getInstance()); + System.assertEquals(0, mockSelector.getCachedAuthSessionQueryCount()); + LoggerParameter.setMock(new LoggerParameter__mdt(DeveloperName = 'QueryAuthSessionData', Value__c = String.valueOf(false))); + + AuthSession returnedAuthSession = LoggerEngineDataSelector.getInstance().getCachedAuthSession(); + + System.assertEquals(0, mockSelector.getCachedAuthSessionQueryCount()); + System.assertEquals(null, returnedAuthSession); + } + @IsTest static void it_returns_cached_sobject_handlers() { List expectedSObjectHandlers = [ @@ -53,38 +77,38 @@ private class LoggerEngineDataSelector_Tests { System.assertEquals(expectedSObjectHandlers, returnedSObjectHandlers); } - @IsTest - static void it_returns_cached_network() { - // By default, the current user (in the pipeline's scratch org) will not have a Network ID - // TODO create an integration test class that verifies the actual Network is returned when the user is associated with the site - Id expectedNetworkId = Network.getNetworkId(); - System.assertEquals(0, Limits.getQueries()); - Integer expectedQueryCount = expectedNetworkId == null ? 0 : 1; - - SObject returnedNetworkSite = LoggerEngineDataSelector.getInstance().getCachedNetwork(); - - System.assertEquals(expectedQueryCount, Limits.getQueries()); - LoggerEngineDataSelector.getInstance().getCachedNetwork(); - System.assertEquals(expectedQueryCount, Limits.getQueries(), 'Query results should have been cached'); - System.assertEquals(expectedNetworkId, returnedNetworkSite?.Id); - } - @IsTest static void it_returns_cached_organization() { Organization expectedOrganization = [ SELECT Id, InstanceName, IsSandbox, Name, NamespacePrefix, OrganizationType, TrialExpirationDate FROM Organization ]; - System.assertEquals(1, Limits.getQueries()); + MockLoggerEngineDataSelector mockSelector = new MockLoggerEngineDataSelector(); + LoggerEngineDataSelector.setMock(mockSelector); + System.assertEquals(mockSelector, LoggerEngineDataSelector.getInstance()); + System.assertEquals(0, mockSelector.getCachedOrganizationQueryCount()); Organization returnedOrganization = LoggerEngineDataSelector.getInstance().getCachedOrganization(); - System.assertEquals(2, Limits.getQueries()); + System.assertEquals(1, mockSelector.getCachedOrganizationQueryCount()); LoggerEngineDataSelector.getInstance().getCachedOrganization(); - System.assertEquals(2, Limits.getQueries(), 'Query results should have been cached'); + System.assertEquals(1, mockSelector.getCachedOrganizationQueryCount(), 'Query results should have been cached'); System.assertEquals(expectedOrganization, returnedOrganization); } + @IsTest + static void it_does_not_query_organization_when_disabled_via_logger_parameter() { + MockLoggerEngineDataSelector mockSelector = new MockLoggerEngineDataSelector(); + System.assertNotEquals(mockSelector, LoggerEngineDataSelector.getInstance()); + System.assertEquals(0, mockSelector.getCachedOrganizationQueryCount()); + LoggerParameter.setMock(new LoggerParameter__mdt(DeveloperName = 'QueryOrganizationData', Value__c = String.valueOf(false))); + + Organization returnedOrganization = LoggerEngineDataSelector.getInstance().getCachedOrganization(); + + System.assertEquals(0, mockSelector.getCachedOrganizationQueryCount()); + System.assertEquals(null, returnedOrganization); + } + @IsTest static void it_returns_cached_tag_assignment_rules() { List expectedTagAssignmentRules = [ @@ -118,4 +142,35 @@ private class LoggerEngineDataSelector_Tests { System.assertEquals(2, Limits.getQueries(), 'Query results should have been cached'); System.assertEquals(expectedUser, returnedUser); } + + private class MockLoggerEngineDataSelector extends LoggerEngineDataSelector { + private Integer authSessionQueryCount = 0; + private Integer organizationQueryCount = 0; + + public override AuthSession getCachedAuthSession() { + Integer originalQueryCount = Limits.getQueries(); + AuthSession result = super.getCachedAuthSession(); + if (Limits.getQueries() != originalQueryCount) { + authSessionQueryCount = Limits.getQueries() - originalQueryCount; + } + return result; + } + + public Integer getCachedAuthSessionQueryCount() { + return authSessionQueryCount; + } + + public override Organization getCachedOrganization() { + Integer originalQueryCount = Limits.getQueries(); + Organization result = super.getCachedOrganization(); + if (Limits.getQueries() != originalQueryCount) { + organizationQueryCount = Limits.getQueries() - originalQueryCount; + } + return result; + } + + public Integer getCachedOrganizationQueryCount() { + return organizationQueryCount; + } + } } diff --git a/nebula-logger/core/tests/logger-engine/classes/Logger_Tests.cls b/nebula-logger/core/tests/logger-engine/classes/Logger_Tests.cls index d66f42889..84afd4ef1 100644 --- a/nebula-logger/core/tests/logger-engine/classes/Logger_Tests.cls +++ b/nebula-logger/core/tests/logger-engine/classes/Logger_Tests.cls @@ -140,6 +140,16 @@ private class Logger_Tests { } } + @IsTest + static void it_should_set_request_id() { + String expectedRequestId = System.Request.getCurrent().getRequestId(); + for (Integer i = 0; i < 5; i++) { + LogEntryEventBuilder builder = Logger.info('my log entry'); + + System.assertEquals(expectedRequestId, builder.getLogEntryEvent().RequestId__c); + } + } + @IsTest static void it_should_set_scenario_when_default_scenario_configured() { String mockScenario = 'some test scenario for this transaction'; @@ -169,6 +179,7 @@ private class Logger_Tests { static void it_should_use_the_first_specified_scenario_as_transaction_scenario_when_parameter_is_true() { LoggerDataStore.setMock(LoggerMockDataStore.getEventBus()); LoggerParameter.setMock(new LoggerParameter__mdt(DeveloperName = 'UseFirstSpecifiedScenario', Value__c = String.valueOf(true))); + System.assertEquals(true, LoggerParameter.USE_FIRST_SCENARIO_FOR_TRANSACTION); String firstMockScenario = 'the first test scenario specified for this transaction'; String secondMockScenario = 'the second test scenario specified for this transaction'; @@ -188,6 +199,7 @@ private class Logger_Tests { static void it_should_use_the_last_specified_scenario_as_transaction_scenario_when_parameter_is_false() { LoggerDataStore.setMock(LoggerMockDataStore.getEventBus()); LoggerParameter.setMock(new LoggerParameter__mdt(DeveloperName = 'UseFirstSpecifiedScenario', Value__c = String.valueOf(false))); + System.assertEquals(false, LoggerParameter.USE_FIRST_SCENARIO_FOR_TRANSACTION); String firstMockScenario = 'the first test scenario specified for this transaction'; String secondMockScenario = 'the second test scenario specified for this transaction'; diff --git a/nebula-logger/extra-tests/testSuites/LoggerExtraTests.testSuite-meta.xml b/nebula-logger/extra-tests/testSuites/LoggerExtraTests.testSuite-meta.xml new file mode 100644 index 000000000..0f70b6427 --- /dev/null +++ b/nebula-logger/extra-tests/testSuites/LoggerExtraTests.testSuite-meta.xml @@ -0,0 +1,18 @@ + + + LogBatchPurger_Tests_Database + LogBatchPurger_Tests_Flow + LogEntryEventBuilder_Tests_Network + LogEntryEventBuilder_Tests_Security + LogEntryHandler_Tests_EmailMessage + LogEntryHandler_Tests_Flow + LoggerCache_Tests_PlatformCache + LoggerEngineDataSelector_Tests_Network + LoggerSettingsController_Tests_Security + LoggerSObjectHandler_Tests_Flow + Logger_Tests_InboundEmailHandler + Logger_Tests_MergeResult + Logger_Tests_Network + LogManagementDataSelector_Tests_Flow + LogMassDeleteExtension_Tests_Security + diff --git a/nebula-logger/extra-tests/tests/LogBatchPurger_Tests_Integration.cls b/nebula-logger/extra-tests/tests/LogBatchPurger_Tests_Database.cls similarity index 98% rename from nebula-logger/extra-tests/tests/LogBatchPurger_Tests_Integration.cls rename to nebula-logger/extra-tests/tests/LogBatchPurger_Tests_Database.cls index 3cd419bac..71c56af21 100644 --- a/nebula-logger/extra-tests/tests/LogBatchPurger_Tests_Integration.cls +++ b/nebula-logger/extra-tests/tests/LogBatchPurger_Tests_Database.cls @@ -5,7 +5,7 @@ @SuppressWarnings('PMD.ApexDoc, PMD.CyclomaticComplexity, PMD.ExcessiveParameterList, PMD.MethodNamingConventions, PMD.NcssMethodCount') @IsTest(IsParallel=false) -private class LogBatchPurger_Tests_Integration { +private class LogBatchPurger_Tests_Database { private static final Integer NUMBER_OF_LOG_ENTRIES = 10; private static final Profile STANDARD_USER_PROFILE = [SELECT Id FROM Profile WHERE Name IN ('Standard User', 'Usuario estándar')]; diff --git a/nebula-logger/extra-tests/tests/LogBatchPurger_Tests_Integration.cls-meta.xml b/nebula-logger/extra-tests/tests/LogBatchPurger_Tests_Database.cls-meta.xml similarity index 100% rename from nebula-logger/extra-tests/tests/LogBatchPurger_Tests_Integration.cls-meta.xml rename to nebula-logger/extra-tests/tests/LogBatchPurger_Tests_Database.cls-meta.xml diff --git a/nebula-logger/extra-tests/tests/LogEntryEventBuilder_Tests_Network.cls b/nebula-logger/extra-tests/tests/LogEntryEventBuilder_Tests_Network.cls new file mode 100644 index 000000000..efcc02915 --- /dev/null +++ b/nebula-logger/extra-tests/tests/LogEntryEventBuilder_Tests_Network.cls @@ -0,0 +1,92 @@ +//------------------------------------------------------------------------------------------------// +// 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.CyclomaticComplexity, PMD.ExcessiveParameterList, PMD.MethodNamingConventions, PMD.NcssMethodCount') +@IsTest(IsParallel=false) +private class LogEntryEventBuilder_Tests_Network { + private static final String EXPERIENCE_CLOUD_NETWORK_NAME = 'Logger Test Site'; + private static final String EXPERIENCE_CLOUD_USER_PROFILE_NAME = 'Logger Test Site User Profile'; + + @IsTest + static void it_should_set_network_fields_for_experience_site_user() { + // No need to fail the test if it's running in an org that does not have Experience Cloud enabled + if (LoggerEngineDataSelector.IS_EXPERIENCE_CLOUD_ENABLED == false) { + return; + } + + System.assertEquals(true, LoggerParameter.QUERY_NETWORK_DATA); + SObject networkRecord = getExperienceCloudNetwork(); + System.assertNotEquals(null, networkRecord.Id); + LogEntryEventBuilder.networkId = networkRecord.Id; + User experienceSiteUser = setupExperienceSiteUser(); + + LogEntryEvent__e logEntryEvent; + System.runAs(experienceSiteUser) { + logEntryEvent = new LogEntryEventBuilder(getUserSettings(), LoggingLevel.INFO, true, new Set()).getLogEntryEvent(); + } + + System.assertEquals(networkRecord.get('Id'), logEntryEvent.NetworkId__c); + System.assertEquals((String) networkRecord.get('Name'), logEntryEvent.NetworkName__c); + System.assertEquals(System.Network.getLoginUrl(networkRecord.Id), logEntryEvent.NetworkLoginUrl__c); + System.assertEquals(System.Network.getLogoutUrl(networkRecord.Id), logEntryEvent.NetworkLogoutUrl__c); + System.assertEquals(System.Network.getSelfRegUrl(networkRecord.Id), logEntryEvent.NetworkSelfRegistrationUrl__c); + System.assertEquals((String) networkRecord.get('UrlPathPrefix'), logEntryEvent.NetworkUrlPathPrefix__c); + } + + @IsTest + static void it_should_not_set_network_fields_for_experience_site_user_when_disabled_via_logger_parameter() { + // No need to fail the test if it's running in an org that does not have Experience Cloud enabled + if (LoggerEngineDataSelector.IS_EXPERIENCE_CLOUD_ENABLED == false) { + return; + } + + LoggerParameter.setMock(new LoggerParameter__mdt(DeveloperName = 'QueryNetworkData', Value__c = String.valueOf(false))); + System.assertEquals(false, LoggerParameter.QUERY_NETWORK_DATA); + SObject networkRecord = getExperienceCloudNetwork(); + System.assertNotEquals(null, networkRecord.Id); + LogEntryEventBuilder.networkId = networkRecord.Id; + User experienceSiteUser = setupExperienceSiteUser(); + + LogEntryEvent__e logEntryEvent; + System.runAs(experienceSiteUser) { + logEntryEvent = new LogEntryEventBuilder(getUserSettings(), LoggingLevel.INFO, true, new Set()).getLogEntryEvent(); + } + + System.assertEquals(null, logEntryEvent.NetworkName__c); + System.assertEquals(null, logEntryEvent.NetworkLoginUrl__c); + System.assertEquals(null, logEntryEvent.NetworkLogoutUrl__c); + System.assertEquals(null, logEntryEvent.NetworkSelfRegistrationUrl__c); + System.assertEquals(null, logEntryEvent.NetworkUrlPathPrefix__c); + } + + static SObject getExperienceCloudNetwork() { + return Database.query('SELECT Id, Name, UrlPathPrefix FROM Network WHERE Name = :EXPERIENCE_CLOUD_NETWORK_NAME'); + } + + static LoggerSettings__c getUserSettings() { + LoggerSettings__c userSettings = (LoggerSettings__c) Schema.LoggerSettings__c.SObjectType.newSObject(null, true); + userSettings.SetupOwnerId = UserInfo.getUserId(); + return userSettings; + } + + static User setupExperienceSiteUser() { + UserRole userRole = new UserRole(DeveloperName = 'LoggerTestRole', Name = 'Logger Test Role'); + insert userRole; + User currentUser = new User(Id = UserInfo.getUserId(), UserRoleId = userRole.Id); + update currentUser; + User experienceSiteUser; + System.runAs(currentUser) { + Account account = new Account(Name = 'Test Account', OwnerId = currentUser.Id); + insert account; + Contact contact = new Contact(AccountId = account.Id, LastName = 'testcontact'); + insert contact; + Id experienceSiteUserProfileId = [SELECT Id FROM Profile WHERE Name = :EXPERIENCE_CLOUD_USER_PROFILE_NAME LIMIT 1].Id; + experienceSiteUser = LoggerMockDataCreator.createUser(experienceSiteUserProfileId); + experienceSiteUser.ContactId = contact.Id; + insert experienceSiteUser; + } + return [SELECT Id, AccountId, ContactId FROM User WHERE Id = :experienceSiteUser.Id]; + } +} diff --git a/nebula-logger/extra-tests/tests/LogEntryEventBuilder_Tests_Integration.cls-meta.xml b/nebula-logger/extra-tests/tests/LogEntryEventBuilder_Tests_Network.cls-meta.xml similarity index 100% rename from nebula-logger/extra-tests/tests/LogEntryEventBuilder_Tests_Integration.cls-meta.xml rename to nebula-logger/extra-tests/tests/LogEntryEventBuilder_Tests_Network.cls-meta.xml diff --git a/nebula-logger/extra-tests/tests/LogEntryEventBuilder_Tests_Integration.cls b/nebula-logger/extra-tests/tests/LogEntryEventBuilder_Tests_Security.cls similarity index 87% rename from nebula-logger/extra-tests/tests/LogEntryEventBuilder_Tests_Integration.cls rename to nebula-logger/extra-tests/tests/LogEntryEventBuilder_Tests_Security.cls index 2c06d9c5f..b2a546734 100644 --- a/nebula-logger/extra-tests/tests/LogEntryEventBuilder_Tests_Integration.cls +++ b/nebula-logger/extra-tests/tests/LogEntryEventBuilder_Tests_Security.cls @@ -5,11 +5,11 @@ @SuppressWarnings('PMD.ApexDoc, PMD.CyclomaticComplexity, PMD.ExcessiveParameterList, PMD.MethodNamingConventions, PMD.NcssMethodCount') @IsTest(IsParallel=false) -private class LogEntryEventBuilder_Tests_Integration { +private class LogEntryEventBuilder_Tests_Security { private static final Profile STANDARD_USER_PROFILE = [SELECT Id FROM Profile WHERE Name IN ('Standard User', 'Usuario estándar')]; @IsTest - static void stripInaccessibleFieldsForRecordWhenEnabled() { + static void it_should_strip_inaccessible_fields_for_record_when_enabled() { User standardUser = LoggerMockDataCreator.createUser(STANDARD_USER_PROFILE.Id); AccountBrand mockAccountBrand = (AccountBrand) LoggerMockDataCreator.createDataBuilder(Schema.AccountBrand.SObjectType) .populateMockId() @@ -31,8 +31,9 @@ private class LogEntryEventBuilder_Tests_Integration { System.assertEquals(false, Schema.AccountBrand.Name.getDescribe().isAccessible(), 'AccountBrand Name was accessible, and should not have been.'); System.assertEquals(false, Schema.AccountBrand.Phone.getDescribe().isAccessible(), 'AccountBrand Phone was accessible, and should not have been.'); - Logger.getUserSettings().IsRecordFieldStrippingEnabled__c = true; - builder = new LogEntryEventBuilder(getUserSettings(), LoggingLevel.INFO, true, new Set()).setRecord(mockAccountBrand); + LoggerSettings__c userSettings = getUserSettings(); + userSettings.IsRecordFieldStrippingEnabled__c = true; + builder = new LogEntryEventBuilder(userSettings, LoggingLevel.INFO, true, new Set()).setRecord(mockAccountBrand); } System.assertNotEquals(JSON.serializePretty(mockAccountBrand), builder.getLogEntryEvent().RecordJson__c, 'Log entry event record JSON was incorrect.'); @@ -40,7 +41,7 @@ private class LogEntryEventBuilder_Tests_Integration { } @IsTest - static void stripInaccessibleFieldsForRecordsWhenEnabled() { + static void it_should_strip_inaccessible_fields_for_records_when_enabled() { User standardUser = LoggerMockDataCreator.createUser(STANDARD_USER_PROFILE.Id); List mockAccountBrands = new List(); List strippedAccountBrands = new List(); @@ -69,8 +70,9 @@ private class LogEntryEventBuilder_Tests_Integration { System.assertEquals(false, Schema.AccountBrand.Name.getDescribe().isAccessible(), 'AccountBrand Name was accessible, and should not have been.'); System.assertEquals(false, Schema.AccountBrand.Phone.getDescribe().isAccessible(), 'AccountBrand Phone was accessible, and should not have been.'); - Logger.getUserSettings().IsRecordFieldStrippingEnabled__c = true; - builder = new LogEntryEventBuilder(getUserSettings(), LoggingLevel.INFO, true, new Set()).setRecord(mockAccountBrands); + LoggerSettings__c userSettings = getUserSettings(); + userSettings.IsRecordFieldStrippingEnabled__c = true; + builder = new LogEntryEventBuilder(userSettings, LoggingLevel.INFO, true, new Set()).setRecord(mockAccountBrands); } System.assertNotEquals(JSON.serializePretty(mockAccountBrands), builder.getLogEntryEvent().RecordJson__c, 'Record JSON is incorrect.'); diff --git a/nebula-logger/extra-tests/tests/LogEntryHandler_Tests_Integration.cls-meta.xml b/nebula-logger/extra-tests/tests/LogEntryEventBuilder_Tests_Security.cls-meta.xml similarity index 100% rename from nebula-logger/extra-tests/tests/LogEntryHandler_Tests_Integration.cls-meta.xml rename to nebula-logger/extra-tests/tests/LogEntryEventBuilder_Tests_Security.cls-meta.xml diff --git a/nebula-logger/extra-tests/tests/LogEntryHandler_Tests_Integration.cls b/nebula-logger/extra-tests/tests/LogEntryHandler_Tests_EmailMessage.cls similarity index 97% rename from nebula-logger/extra-tests/tests/LogEntryHandler_Tests_Integration.cls rename to nebula-logger/extra-tests/tests/LogEntryHandler_Tests_EmailMessage.cls index a817f03f9..0e6b52a4d 100644 --- a/nebula-logger/extra-tests/tests/LogEntryHandler_Tests_Integration.cls +++ b/nebula-logger/extra-tests/tests/LogEntryHandler_Tests_EmailMessage.cls @@ -5,7 +5,7 @@ @SuppressWarnings('PMD.ApexDoc, PMD.CyclomaticComplexity, PMD.ExcessiveParameterList, PMD.MethodNamingConventions, PMD.NcssMethodCount') @IsTest(IsParallel=true) -private class LogEntryHandler_Tests_Integration { +private class LogEntryHandler_Tests_EmailMessage { @IsTest static void it_should_populate_related_record_name_field_on_log_entry_with_email_message_subject() { System.assertEquals( diff --git a/nebula-logger/extra-tests/tests/LogMassDeleteExtension_Tests_Integration.cls-meta.xml b/nebula-logger/extra-tests/tests/LogEntryHandler_Tests_EmailMessage.cls-meta.xml similarity index 100% rename from nebula-logger/extra-tests/tests/LogMassDeleteExtension_Tests_Integration.cls-meta.xml rename to nebula-logger/extra-tests/tests/LogEntryHandler_Tests_EmailMessage.cls-meta.xml diff --git a/nebula-logger/extra-tests/tests/LogEntryHandler_Tests_Flow.cls b/nebula-logger/extra-tests/tests/LogEntryHandler_Tests_Flow.cls index bfa888bdb..f485989db 100644 --- a/nebula-logger/extra-tests/tests/LogEntryHandler_Tests_Flow.cls +++ b/nebula-logger/extra-tests/tests/LogEntryHandler_Tests_Flow.cls @@ -9,7 +9,7 @@ private class LogEntryHandler_Tests_Flow { private static final String EXAMPLE_FLOW_API_NAME = 'LogEntryHandler_Tests_Flow'; @IsTest - static void setSkipSettingFlowDetailsWhenOriginLocationIsNull() { + static void it_should_set_skip_setting_flow_details_when_origin_location_is_null() { Log__c log = new Log__c(TransactionId__c = '1234'); insert log; LoggerTestConfigurator.setupMockSObjectHandlerConfigurations(); @@ -34,26 +34,27 @@ private class LogEntryHandler_Tests_Flow { } @IsTest - static void setFlowDetails() { - FlowDefinitionView flowDefinition = getFlowDefinition(); - FlowVersionView flowVersion = getFlowVersion(flowDefinition.ActiveVersionId); + static void it_should_set_flow_details_when_origin_location_is_a_flow() { + FlowDefinitionView flowDefinitionView = getFlowDefinitionView(); + FlowVersionView flowVersion = getFlowVersion(flowDefinitionView.ActiveVersionId); Log__c log = new Log__c(TransactionId__c = '1234'); insert log; LoggerTestConfigurator.setupMockSObjectHandlerConfigurations(); - LogEntry__c logEntry = new LogEntry__c(Log__c = log.Id, OriginLocation__c = flowDefinition.ApiName, OriginType__c = 'Flow'); + LogEntry__c logEntry = new LogEntry__c(Log__c = log.Id, OriginLocation__c = flowDefinitionView.ApiName, OriginType__c = 'Flow'); insert logEntry; logEntry = getLogEntry(); + System.assertEquals(EXAMPLE_FLOW_API_NAME, logEntry.OriginLocation__c, 'Origin Location was not null.'); System.assertEquals('Flow', logEntry.OriginType__c, 'OriginType was not flow.'); - System.assertEquals(flowDefinition.ActiveVersionId, logEntry.FlowActiveVersionId__c, 'FlowActiveVersionId was incorrect.'); - System.assertEquals(flowDefinition.Description, logEntry.FlowDescription__c, 'FlowDescription was incorrect.'); - System.assertEquals(flowDefinition.DurableId, logEntry.FlowDurableId__c, 'FlowDurableId was incorrect.'); - System.assertEquals(flowDefinition.Label, logEntry.FlowLabel__c, 'FlowLabel was incorrect'); - System.assertEquals(flowDefinition.LastModifiedBy, logEntry.FlowLastModifiedByName__c, 'FlowLastModifiedByName was incorrect.'); - System.assertEquals(flowDefinition.LastModifiedDate, logEntry.FlowLastModifiedDate__c, 'FlowLastModifiedDate was incorrect.'); - System.assertEquals(flowDefinition.ProcessType, logEntry.FlowProcessType__c, 'FlowProcessType was incorrect.'); - System.assertEquals(flowDefinition.TriggerType, logEntry.FlowTriggerType__c, 'FlowTriggerType was incorrect.'); + System.assertEquals(flowDefinitionView.ActiveVersionId, logEntry.FlowActiveVersionId__c, 'FlowActiveVersionId was incorrect.'); + System.assertEquals(flowDefinitionView.Description, logEntry.FlowDescription__c, 'FlowDescription was incorrect.'); + System.assertEquals(flowDefinitionView.DurableId, logEntry.FlowDurableId__c, 'FlowDurableId was incorrect.'); + System.assertEquals(flowDefinitionView.Label, logEntry.FlowLabel__c, 'FlowLabel was incorrect'); + System.assertEquals(flowDefinitionView.LastModifiedBy, logEntry.FlowLastModifiedByName__c, 'FlowLastModifiedByName was incorrect.'); + System.assertEquals(flowDefinitionView.LastModifiedDate, logEntry.FlowLastModifiedDate__c, 'FlowLastModifiedDate was incorrect.'); + System.assertEquals(flowDefinitionView.ProcessType, logEntry.FlowProcessType__c, 'FlowProcessType was incorrect.'); + System.assertEquals(flowDefinitionView.TriggerType, logEntry.FlowTriggerType__c, 'FlowTriggerType was incorrect.'); System.assertEquals( 'v' + flowVersion.ApiVersionRuntime + @@ -88,7 +89,7 @@ private class LogEntryHandler_Tests_Flow { ]; } - private static FlowDefinitionView getFlowDefinition() { + private static FlowDefinitionView getFlowDefinitionView() { return [ SELECT ActiveVersionId, ApiName, Description, DurableId, Label, LastModifiedBy, LastModifiedDate, ManageableState, ProcessType, TriggerType FROM FlowDefinitionView diff --git a/nebula-logger/extra-tests/tests/LogManagementDataSelector_Tests_Flow.cls b/nebula-logger/extra-tests/tests/LogManagementDataSelector_Tests_Flow.cls new file mode 100644 index 000000000..a4093d6ce --- /dev/null +++ b/nebula-logger/extra-tests/tests/LogManagementDataSelector_Tests_Flow.cls @@ -0,0 +1,37 @@ +//------------------------------------------------------------------------------------------------// +// 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.CyclomaticComplexity, PMD.ExcessiveParameterList, PMD.MethodNamingConventions') +@IsTest(IsParallel=false) +private class LogManagementDataSelector_Tests_Flow { + private static final String TEST_FLOW_NAME = 'MockLogBatchPurgerPlugin'; + + @IsTest + static void it_returns_matching_flow_definition_view_for_specified_flow_api_name() { + List targetFlowApiNames = new List{ TEST_FLOW_NAME }; + List expectedResults = [ + SELECT ActiveVersionId, ApiName, Description, DurableId, Label, LastModifiedBy, LastModifiedDate, ManageableState, ProcessType, TriggerType + FROM FlowDefinitionView + WHERE ApiName IN :targetFlowApiNames AND IsActive = TRUE + ]; + + List returnedResults = LogManagementDataSelector.getInstance().getFlowDefinitionViewsByFlowApiName(targetFlowApiNames); + + System.assertEquals(expectedResults, returnedResults); + } + + @IsTest + static void it_does_not_query_flow_definition_views_when_disabled_via_logger_parameter() { + List targetFlowApiNames = new List{ TEST_FLOW_NAME }; + LoggerParameter.setMock(new LoggerParameter__mdt(DeveloperName = 'QueryFlowDefinitionViewData', Value__c = String.valueOf(false))); + System.assertEquals(false, LoggerParameter.QUERY_FLOW_DEFINITION_VIEW_DATA); + Integer originalQueryCount = Limits.getQueries(); + + List returnedResults = LogManagementDataSelector.getInstance().getFlowDefinitionViewsByFlowApiName(targetFlowApiNames); + + System.assertEquals(originalQueryCount, Limits.getQueries()); + System.assertEquals(0, returnedResults.size()); + } +} diff --git a/nebula-logger/extra-tests/tests/Logger_Tests_ExperienceSite.cls-meta.xml b/nebula-logger/extra-tests/tests/LogManagementDataSelector_Tests_Flow.cls-meta.xml similarity index 100% rename from nebula-logger/extra-tests/tests/Logger_Tests_ExperienceSite.cls-meta.xml rename to nebula-logger/extra-tests/tests/LogManagementDataSelector_Tests_Flow.cls-meta.xml diff --git a/nebula-logger/extra-tests/tests/LogMassDeleteExtension_Tests_Integration.cls b/nebula-logger/extra-tests/tests/LogMassDeleteExtension_Tests_Security.cls similarity index 97% rename from nebula-logger/extra-tests/tests/LogMassDeleteExtension_Tests_Integration.cls rename to nebula-logger/extra-tests/tests/LogMassDeleteExtension_Tests_Security.cls index deac95b57..ebc408881 100644 --- a/nebula-logger/extra-tests/tests/LogMassDeleteExtension_Tests_Integration.cls +++ b/nebula-logger/extra-tests/tests/LogMassDeleteExtension_Tests_Security.cls @@ -5,7 +5,7 @@ @SuppressWarnings('PMD.ApexDoc, PMD.CyclomaticComplexity, PMD.ExcessiveParameterList, PMD.MethodNamingConventions, PMD.NcssMethodCount') @IsTest(IsParallel=false) -private class LogMassDeleteExtension_Tests_Integration { +private class LogMassDeleteExtension_Tests_Security { private static final Profile STANDARD_USER_PROFILE = [SELECT Id FROM Profile WHERE Name IN ('Standard User', 'Usuario estándar')]; @TestSetup diff --git a/nebula-logger/extra-tests/tests/LogMassDeleteExtension_Tests_Security.cls-meta.xml b/nebula-logger/extra-tests/tests/LogMassDeleteExtension_Tests_Security.cls-meta.xml new file mode 100644 index 000000000..c47403d0b --- /dev/null +++ b/nebula-logger/extra-tests/tests/LogMassDeleteExtension_Tests_Security.cls-meta.xml @@ -0,0 +1,5 @@ + + + 55.0 + Active + diff --git a/nebula-logger/extra-tests/tests/LoggerCache_Tests_PlatformCache.cls b/nebula-logger/extra-tests/tests/LoggerCache_Tests_PlatformCache.cls new file mode 100644 index 000000000..1f162e40a --- /dev/null +++ b/nebula-logger/extra-tests/tests/LoggerCache_Tests_PlatformCache.cls @@ -0,0 +1,183 @@ +//------------------------------------------------------------------------------------------------// +// 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. // +//------------------------------------------------------------------------------------------------// + +/** + * @description When testing Platform Cache partitions, there is no way to directly mock the partitions. Furthermore, the partitions + * configured in the org are actually used in test contexts, so if a partition exists but does not have storage space + * allocated in the org, then any tests that try to assert that data is cached in the partitions will fail. + * To help overcome this platform limitation, this test class only runs in Nebula Logger's pipeline - this ensures that the tests + * are running in an org that has a platform cache partition with space allocated. + */ +@SuppressWarnings('PMD.ApexDoc, PMD.ApexAssertionsShouldIncludeMessage, PMD.CyclomaticComplexity, PMD.MethodNamingConventions') +@IsTest(IsParallel=true) +private class LoggerCache_Tests_PlatformCache { + @IsTest + static void it_adds_new_key_to_organization_and_transaction_cache() { + String mockKey = 'SomeKey'; + User mockValue = new User(Id = UserInfo.getUserId(), ProfileId = UserInfo.getProfileId()); + System.assertEquals(false, LoggerCache.getOrganizationCache().contains(mockKey)); + System.assertEquals(false, LoggerCache.getTransactionCache().contains(mockKey)); + + LoggerCache.getOrganizationCache().put(mockKey, mockValue); + + System.assertEquals(true, LoggerCache.getOrganizationCache().contains(mockKey)); + System.assertEquals(mockValue, LoggerCache.getOrganizationCache().get(mockKey)); + System.assertEquals(true, Cache.Org.getPartition(LoggerCache.PLATFORM_CACHE_PARTITION_NAME).contains(mockKey)); + System.assertEquals(mockValue, Cache.Org.getPartition(LoggerCache.PLATFORM_CACHE_PARTITION_NAME).get(mockKey)); + System.assertEquals(true, LoggerCache.getTransactionCache().contains(mockKey)); + System.assertEquals(mockValue, LoggerCache.getTransactionCache().get(mockKey)); + } + + @IsTest + static void it_adds_new_key_with_null_value_to_organization_and_transaction_cache() { + String mockKey = 'SomeKey'; + User mockValue = null; + System.assertEquals(false, LoggerCache.getOrganizationCache().contains(mockKey)); + System.assertEquals(false, LoggerCache.getTransactionCache().contains(mockKey)); + + LoggerCache.getOrganizationCache().put(mockKey, mockValue); + + System.assertEquals(true, LoggerCache.getOrganizationCache().contains(mockKey)); + System.assertEquals(mockValue, LoggerCache.getOrganizationCache().get(mockKey)); + System.assertEquals(true, Cache.Org.getPartition(LoggerCache.PLATFORM_CACHE_PARTITION_NAME).contains(mockKey)); + System.assertEquals(LoggerCache.PLATFORM_CACHE_NULL_VALUE, Cache.Org.getPartition(LoggerCache.PLATFORM_CACHE_PARTITION_NAME).get(mockKey)); + System.assertEquals(true, LoggerCache.getTransactionCache().contains(mockKey)); + System.assertEquals(mockValue, LoggerCache.getTransactionCache().get(mockKey)); + } + + @IsTest + static void it_updates_value_for_existing_key_in_organization_and_transaction_cache() { + String mockKey = 'SomeKey'; + User oldMockValue = new User(Id = UserInfo.getUserId(), ProfileId = UserInfo.getProfileId()); + LoggerCache.getOrganizationCache().put(mockKey, oldMockValue); + System.assertEquals(true, LoggerCache.getOrganizationCache().contains(mockKey)); + System.assertEquals(oldMockValue, LoggerCache.getOrganizationCache().get(mockKey)); + System.assertEquals(true, LoggerCache.getTransactionCache().contains(mockKey)); + System.assertEquals(oldMockValue, LoggerCache.getTransactionCache().get(mockKey)); + Account newMockValue = new Account(Name = 'Some fake account'); + + LoggerCache.getOrganizationCache().put(mockKey, newMockValue); + + System.assertEquals(true, LoggerCache.getOrganizationCache().contains(mockKey)); + System.assertEquals(newMockValue, LoggerCache.getOrganizationCache().get(mockKey)); + System.assertEquals(true, Cache.Org.getPartition(LoggerCache.PLATFORM_CACHE_PARTITION_NAME).contains(mockKey)); + System.assertEquals(newMockValue, Cache.Org.getPartition(LoggerCache.PLATFORM_CACHE_PARTITION_NAME).get(mockKey)); + System.assertEquals(true, LoggerCache.getTransactionCache().contains(mockKey)); + System.assertEquals(newMockValue, LoggerCache.getTransactionCache().get(mockKey)); + } + + @IsTest + static void it_removes_value_for_existing_key_in_organization_and_transaction_cache() { + String mockKey = 'SomeKey'; + User mockValue = new User(Id = UserInfo.getUserId(), ProfileId = UserInfo.getProfileId()); + System.assertEquals(false, LoggerCache.getOrganizationCache().contains(mockKey)); + System.assertEquals(false, LoggerCache.getTransactionCache().contains(mockKey)); + LoggerCache.getOrganizationCache().put(mockKey, mockValue); + System.assertEquals(true, LoggerCache.getOrganizationCache().contains(mockKey)); + System.assertEquals(mockValue, LoggerCache.getOrganizationCache().get(mockKey)); + System.assertEquals(true, Cache.Org.getPartition(LoggerCache.PLATFORM_CACHE_PARTITION_NAME).contains(mockKey)); + System.assertEquals(mockValue, Cache.Org.getPartition(LoggerCache.PLATFORM_CACHE_PARTITION_NAME).get(mockKey)); + System.assertEquals(true, LoggerCache.getTransactionCache().contains(mockKey)); + System.assertEquals(mockValue, LoggerCache.getTransactionCache().get(mockKey)); + + LoggerCache.getOrganizationCache().remove(mockKey); + + System.assertEquals(false, LoggerCache.getOrganizationCache().contains(mockKey)); + System.assertEquals(false, Cache.Org.getPartition(LoggerCache.PLATFORM_CACHE_PARTITION_NAME).contains(mockKey)); + System.assertEquals(false, LoggerCache.getTransactionCache().contains(mockKey)); + } + + @IsTest + static void it_adds_new_key_to_session_and_transaction_cache() { + String mockKey = 'SomeKey'; + User mockValue = new User(Id = UserInfo.getUserId(), ProfileId = UserInfo.getProfileId()); + System.assertEquals(false, LoggerCache.getSessionCache().contains(mockKey)); + System.assertEquals(false, LoggerCache.getTransactionCache().contains(mockKey)); + + LoggerCache.getSessionCache().put(mockKey, mockValue); + + System.assertEquals(true, LoggerCache.getSessionCache().contains(mockKey)); + System.assertEquals(mockValue, LoggerCache.getSessionCache().get(mockKey)); + // Depending on how you start Apex tests, you may or may not have an active session + // during the test execution, so session cache may or may not be available (╯°□°)╯︵ ┻━┻ + if (Cache.Session.getPartition(LoggerCache.PLATFORM_CACHE_PARTITION_NAME).isAvailable() == true) { + System.assertEquals(true, Cache.Session.getPartition(LoggerCache.PLATFORM_CACHE_PARTITION_NAME).contains(mockKey)); + System.assertEquals(mockValue, Cache.Session.getPartition(LoggerCache.PLATFORM_CACHE_PARTITION_NAME).get(mockKey)); + } + System.assertEquals(true, LoggerCache.getTransactionCache().contains(mockKey)); + System.assertEquals(mockValue, LoggerCache.getTransactionCache().get(mockKey)); + } + + @IsTest + static void it_adds_new_key_with_null_value_to_session_and_transaction_cache() { + String mockKey = 'SomeKey'; + User mockValue = null; + System.assertEquals(false, LoggerCache.getSessionCache().contains(mockKey)); + System.assertEquals(false, LoggerCache.getTransactionCache().contains(mockKey)); + + LoggerCache.getSessionCache().put(mockKey, mockValue); + + System.assertEquals(true, LoggerCache.getSessionCache().contains(mockKey)); + System.assertEquals(mockValue, LoggerCache.getSessionCache().get(mockKey)); + // Depending on how you start Apex tests, you may or may not have an active session + // during the test execution, so session cache may or may not be available (╯°□°)╯︵ ┻━┻ + if (Cache.Session.getPartition(LoggerCache.PLATFORM_CACHE_PARTITION_NAME).isAvailable() == true) { + System.assertEquals(true, Cache.Session.getPartition(LoggerCache.PLATFORM_CACHE_PARTITION_NAME).contains(mockKey)); + System.assertEquals(LoggerCache.PLATFORM_CACHE_NULL_VALUE, Cache.Session.getPartition(LoggerCache.PLATFORM_CACHE_PARTITION_NAME).get(mockKey)); + } + System.assertEquals(true, LoggerCache.getTransactionCache().contains(mockKey)); + System.assertEquals(mockValue, LoggerCache.getTransactionCache().get(mockKey)); + } + + @IsTest + static void it_updates_value_for_existing_key_in_session_and_transaction_cache() { + String mockKey = 'SomeKey'; + User oldMockValue = new User(Id = UserInfo.getUserId(), ProfileId = UserInfo.getProfileId()); + LoggerCache.getSessionCache().put(mockKey, oldMockValue); + System.assertEquals(true, LoggerCache.getSessionCache().contains(mockKey)); + System.assertEquals(oldMockValue, LoggerCache.getSessionCache().get(mockKey)); + System.assertEquals(true, LoggerCache.getTransactionCache().contains(mockKey)); + System.assertEquals(oldMockValue, LoggerCache.getTransactionCache().get(mockKey)); + Account newMockValue = new Account(Name = 'Some fake account'); + + LoggerCache.getSessionCache().put(mockKey, newMockValue); + + System.assertEquals(true, LoggerCache.getSessionCache().contains(mockKey)); + System.assertEquals(newMockValue, LoggerCache.getSessionCache().get(mockKey)); + // Depending on how you start Apex tests, you may or may not have an active session + // during the test execution, so session cache may or may not be available (╯°□°)╯︵ ┻━┻ + if (Cache.Session.getPartition(LoggerCache.PLATFORM_CACHE_PARTITION_NAME).isAvailable() == true) { + System.assertEquals(true, Cache.Session.getPartition(LoggerCache.PLATFORM_CACHE_PARTITION_NAME).contains(mockKey)); + System.assertEquals(newMockValue, Cache.Session.getPartition(LoggerCache.PLATFORM_CACHE_PARTITION_NAME).get(mockKey)); + } + System.assertEquals(true, LoggerCache.getTransactionCache().contains(mockKey)); + System.assertEquals(newMockValue, LoggerCache.getTransactionCache().get(mockKey)); + } + + @IsTest + static void it_removes_value_for_existing_key_in_session_and_transaction_cache() { + String mockKey = 'SomeKey'; + User mockValue = new User(Id = UserInfo.getUserId(), ProfileId = UserInfo.getProfileId()); + System.assertEquals(false, LoggerCache.getSessionCache().contains(mockKey)); + System.assertEquals(false, LoggerCache.getTransactionCache().contains(mockKey)); + LoggerCache.getSessionCache().put(mockKey, mockValue); + System.assertEquals(true, LoggerCache.getSessionCache().contains(mockKey)); + System.assertEquals(mockValue, LoggerCache.getSessionCache().get(mockKey)); + // Depending on how you start Apex tests, you may or may not have an active session + // during the test execution, so session cache may or may not be available (╯°□°)╯︵ ┻━┻ + if (Cache.Session.getPartition(LoggerCache.PLATFORM_CACHE_PARTITION_NAME).isAvailable() == true) { + System.assertEquals(true, Cache.Session.getPartition(LoggerCache.PLATFORM_CACHE_PARTITION_NAME).contains(mockKey)); + System.assertEquals(mockValue, Cache.Session.getPartition(LoggerCache.PLATFORM_CACHE_PARTITION_NAME).get(mockKey)); + } + System.assertEquals(true, LoggerCache.getTransactionCache().contains(mockKey)); + System.assertEquals(mockValue, LoggerCache.getTransactionCache().get(mockKey)); + + LoggerCache.getSessionCache().remove(mockKey); + + System.assertEquals(false, LoggerCache.getSessionCache().contains(mockKey)); + System.assertEquals(false, Cache.Session.getPartition(LoggerCache.PLATFORM_CACHE_PARTITION_NAME).contains(mockKey)); + System.assertEquals(false, LoggerCache.getTransactionCache().contains(mockKey)); + } +} diff --git a/nebula-logger/extra-tests/tests/LoggerCache_Tests_PlatformCache.cls-meta.xml b/nebula-logger/extra-tests/tests/LoggerCache_Tests_PlatformCache.cls-meta.xml new file mode 100644 index 000000000..c47403d0b --- /dev/null +++ b/nebula-logger/extra-tests/tests/LoggerCache_Tests_PlatformCache.cls-meta.xml @@ -0,0 +1,5 @@ + + + 55.0 + Active + diff --git a/nebula-logger/extra-tests/tests/LoggerEngineDataSelector_Tests_Network.cls b/nebula-logger/extra-tests/tests/LoggerEngineDataSelector_Tests_Network.cls new file mode 100644 index 000000000..d646b8d3b --- /dev/null +++ b/nebula-logger/extra-tests/tests/LoggerEngineDataSelector_Tests_Network.cls @@ -0,0 +1,35 @@ +//------------------------------------------------------------------------------------------------// +// 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.CyclomaticComplexity, PMD.ExcessiveParameterList, PMD.MethodNamingConventions') +@IsTest(IsParallel=true) +private class LoggerEngineDataSelector_Tests_Network { + private static final String EXPERIENCE_CLOUD_GUEST_PROFILE_NAME = 'Logger Test Site Guest Profile'; + private static final String EXPERIENCE_CLOUD_NETWORK_NAME = 'Logger Test Site'; + private static final String GUEST_USER_TYPE = 'Guest'; + private static final String LOG_CREATOR_PERMISSION_SET_NAME = 'LoggerLogCreator'; + + @IsTest + static void it_returns_cached_network() { + if (LoggerEngineDataSelector.IS_EXPERIENCE_CLOUD_ENABLED == false) { + return; + } + + Id expectedNetworkId = (Id) getExperienceCloudNetwork().get('Id'); + System.assertEquals(1, Limits.getQueries()); + Integer expectedQueryCount = Limits.getQueries() + 1; + + SObject returnedNetworkSite = LoggerEngineDataSelector.getInstance().getCachedNetwork(expectedNetworkId); + + System.assertEquals(expectedQueryCount, Limits.getQueries()); + LoggerEngineDataSelector.getInstance().getCachedNetwork(expectedNetworkId); + System.assertEquals(expectedQueryCount, Limits.getQueries(), 'Query results should have been cached'); + System.assertEquals(expectedNetworkId, returnedNetworkSite?.Id); + } + + static SObject getExperienceCloudNetwork() { + return Database.query('SELECT Id, Name, UrlPathPrefix FROM Network WHERE Name = :EXPERIENCE_CLOUD_NETWORK_NAME'); + } +} diff --git a/nebula-logger/extra-tests/tests/LoggerEngineDataSelector_Tests_Network.cls-meta.xml b/nebula-logger/extra-tests/tests/LoggerEngineDataSelector_Tests_Network.cls-meta.xml new file mode 100644 index 000000000..c47403d0b --- /dev/null +++ b/nebula-logger/extra-tests/tests/LoggerEngineDataSelector_Tests_Network.cls-meta.xml @@ -0,0 +1,5 @@ + + + 55.0 + Active + diff --git a/nebula-logger/extra-tests/tests/LoggerSettingsController_Tests_Security.cls b/nebula-logger/extra-tests/tests/LoggerSettingsController_Tests_Security.cls index 7396961a2..75d0b6c94 100644 --- a/nebula-logger/extra-tests/tests/LoggerSettingsController_Tests_Security.cls +++ b/nebula-logger/extra-tests/tests/LoggerSettingsController_Tests_Security.cls @@ -3,12 +3,6 @@ // See LICENSE file or go to https://github.com/jongpie/NebulaLogger for full license details. // //------------------------------------------------------------------------------------------------// -/** - * @group Extra Tests - * @description Additional integration tests for testing security checks in `LoggerSettingsController`. - * If an org installs Nebula Logger for all users, these tests will start failing since it relies on an existing Profile - * ...from within the normal tests, and each org might have additional required fields, VR, etc on the std objects - */ @SuppressWarnings('PMD.ApexDoc, PMD.MethodNamingConventions') @IsTest(IsParallel=false) private class LoggerSettingsController_Tests_Security { diff --git a/nebula-logger/extra-tests/tests/Logger_Tests_InboundEmailHandler.cls b/nebula-logger/extra-tests/tests/Logger_Tests_InboundEmailHandler.cls index 09e7c6547..402389ffc 100644 --- a/nebula-logger/extra-tests/tests/Logger_Tests_InboundEmailHandler.cls +++ b/nebula-logger/extra-tests/tests/Logger_Tests_InboundEmailHandler.cls @@ -3,10 +3,6 @@ // See LICENSE file or go to https://github.com/jongpie/NebulaLogger for full license details. // //------------------------------------------------------------------------------------------------// -/** - * @group Extra Tests - * @description Additional integration tests for logging from an instance of `Messaging.InboundEmailHandler` - */ @SuppressWarnings('PMD.ApexDoc, PMD.CyclomaticComplexity, PMD.ExcessiveParameterList, PMD.MethodNamingConventions, PMD.NcssMethodCount') @IsTest(IsParallel=true) private class Logger_Tests_InboundEmailHandler { diff --git a/nebula-logger/extra-tests/tests/Logger_Tests_MergeResult.cls b/nebula-logger/extra-tests/tests/Logger_Tests_MergeResult.cls index 2c634305b..71ddd4396 100644 --- a/nebula-logger/extra-tests/tests/Logger_Tests_MergeResult.cls +++ b/nebula-logger/extra-tests/tests/Logger_Tests_MergeResult.cls @@ -5,12 +5,6 @@ // TODO move these test methods back into Logger_Tests, now that mocking is possible via LoggerMockDataCreator.createDatabaseMergeResult() // (and then delete this class) -/** - * @group Extra Tests - * @description Additional integration tests for merging records. - * Since merging is only supported on some standard objects, there's not a good way to test merging - * from within the normal tests, and each org might have additional required fields, VR, etc on the std objects - */ @SuppressWarnings('PMD.ApexDoc, PMD.CyclomaticComplexity, PMD.ExcessiveParameterList, PMD.MethodNamingConventions, PMD.NcssMethodCount') @IsTest(IsParallel=true) private class Logger_Tests_MergeResult { diff --git a/nebula-logger/extra-tests/tests/Logger_Tests_ExperienceSite.cls b/nebula-logger/extra-tests/tests/Logger_Tests_Network.cls similarity index 85% rename from nebula-logger/extra-tests/tests/Logger_Tests_ExperienceSite.cls rename to nebula-logger/extra-tests/tests/Logger_Tests_Network.cls index 44cf90732..24d3528d3 100644 --- a/nebula-logger/extra-tests/tests/Logger_Tests_ExperienceSite.cls +++ b/nebula-logger/extra-tests/tests/Logger_Tests_Network.cls @@ -3,15 +3,11 @@ // See LICENSE file or go to https://github.com/jongpie/NebulaLogger for full license details. // //------------------------------------------------------------------------------------------------// -/** - * @group Extra Tests - * @description Additional integration tests for orgs with Experience Sites (Communities) enabled - */ @SuppressWarnings('PMD.ApexDoc, PMD.CyclomaticComplexity, PMD.ExcessiveParameterList, PMD.MethodNamingConventions, PMD.NcssMethodCount') @IsTest(IsParallel=false) -private class Logger_Tests_ExperienceSite { - private static final Boolean IS_EXPERIENCE_CLOUD_ENABLED = Schema.getGlobalDescribe().containsKey('Network'); - private static final String GUEST_USER_PROFILE_NAME = 'Logger Test Site Profile'; +private class Logger_Tests_Network { + private static final Boolean IS_EXPERIENCE_CLOUD_ENABLED = Type.forName('NetworkMember') != null; + private static final String EXPERIENCE_CLOUD_GUEST_PROFILE_NAME = 'Logger Test Site Guest Profile'; private static final String GUEST_USER_TYPE = 'Guest'; private static final String LOG_CREATOR_PERMISSION_SET_NAME = 'LoggerLogCreator'; @@ -21,7 +17,7 @@ private class Logger_Tests_ExperienceSite { return; } - List matchingProfiles = [SELECT Id, UserLicense.Name FROM Profile WHERE Name = :GUEST_USER_PROFILE_NAME]; + List matchingProfiles = [SELECT Id, UserLicense.Name FROM Profile WHERE Name = :EXPERIENCE_CLOUD_GUEST_PROFILE_NAME]; if (matchingProfiles.isEmpty() == true) { return; @@ -31,7 +27,7 @@ private class Logger_Tests_ExperienceSite { System.assertEquals('Guest User License', loggerSiteProfile.UserLicense.Name, 'User license did not match Guest User License.'); // Even if Experience Cloud is enabled, the expected test site might not exist, so exit early if the guest user cannot be found - List guestUsers = [SELECT Id FROM User WHERE Profile.Name = :GUEST_USER_PROFILE_NAME AND Profile.UserType = :GUEST_USER_TYPE]; + List guestUsers = [SELECT Id FROM User WHERE Profile.Name = :EXPERIENCE_CLOUD_GUEST_PROFILE_NAME AND Profile.UserType = :GUEST_USER_TYPE]; if (guestUsers.isEmpty() == true) { return; } @@ -53,7 +49,7 @@ private class Logger_Tests_ExperienceSite { System.Test.startTest(); // Even if Experience Cloud is enabled, the expected test site might not exist, so exit early if the guest user cannot be found - List guestUsers = [SELECT Id FROM User WHERE Profile.Name = :GUEST_USER_PROFILE_NAME AND Profile.UserType = :GUEST_USER_TYPE]; + List guestUsers = [SELECT Id FROM User WHERE Profile.Name = :EXPERIENCE_CLOUD_GUEST_PROFILE_NAME AND Profile.UserType = :GUEST_USER_TYPE]; if (guestUsers.isEmpty() == true) { return; } @@ -78,7 +74,11 @@ private class Logger_Tests_ExperienceSite { } // Even if Experience Cloud is enabled, the expected test site might not exist, so exit early if the guest user cannot be found - List guestUsers = [SELECT Id, Profile.UserType FROM User WHERE Profile.Name = :GUEST_USER_PROFILE_NAME AND Profile.UserType = :GUEST_USER_TYPE]; + List guestUsers = [ + SELECT Id, Profile.UserType + FROM User + WHERE Profile.Name = :EXPERIENCE_CLOUD_GUEST_PROFILE_NAME AND Profile.UserType = :GUEST_USER_TYPE + ]; if (guestUsers.isEmpty() == true) { return; } diff --git a/nebula-logger/extra-tests/tests/Logger_Tests_Network.cls-meta.xml b/nebula-logger/extra-tests/tests/Logger_Tests_Network.cls-meta.xml new file mode 100644 index 000000000..c47403d0b --- /dev/null +++ b/nebula-logger/extra-tests/tests/Logger_Tests_Network.cls-meta.xml @@ -0,0 +1,5 @@ + + + 55.0 + Active + diff --git a/nebula-logger/managed-package/sfdx-project.json b/nebula-logger/managed-package/sfdx-project.json index 30e798114..27994dbbc 100644 --- a/nebula-logger/managed-package/sfdx-project.json +++ b/nebula-logger/managed-package/sfdx-project.json @@ -1,7 +1,7 @@ { "namespace": "Nebula", "sfdcLoginUrl": "https://login.salesforce.com", - "sourceApiVersion": "54.0", + "sourceApiVersion": "55.0", "packageDirectories": [ { "package": "Nebula Logger - Managed Package", diff --git a/nebula-logger/plugins/async-failure-additions/README.md b/nebula-logger/plugins/async-failure-additions/README.md index bb79d74da..f9bf94c3f 100644 --- a/nebula-logger/plugins/async-failure-additions/README.md +++ b/nebula-logger/plugins/async-failure-additions/README.md @@ -2,8 +2,8 @@ > :information_source: This plugin requires `v4.7.2` or newer of Nebula Logger's unlocked package -[![Install Unlocked Package Plugin in a Sandbox](../.images/btn-install-unlocked-package-plugin-sandbox.png)](https://test.salesforce.com/packaging/installPackage.apexp?p0=04t5Y0000015lhsQAA) -[![Install Unlocked Package Plugin in Production](../.images/btn-install-unlocked-package-plugin-production.png)](https://login.salesforce.com/packaging/installPackage.apexp?p0=04t5Y0000015lhsQAA) +[![Install Unlocked Package Plugin in a Sandbox](../.images/btn-install-unlocked-package-plugin-sandbox.png)](https://test.salesforce.com/packaging/installPackage.apexp?p0=04t5Y0000023QttQAE) +[![Install Unlocked Package Plugin in Production](../.images/btn-install-unlocked-package-plugin-production.png)](https://login.salesforce.com/packaging/installPackage.apexp?p0=04t5Y0000023QttQAE) ## What's Included diff --git a/nebula-logger/plugins/async-failure-additions/plugin/classes/LogFlowExecutionErrorEventHandler.cls b/nebula-logger/plugins/async-failure-additions/plugin/classes/LogFlowExecutionErrorEventHandler.cls index 8ec15adb6..96871eb8f 100644 --- a/nebula-logger/plugins/async-failure-additions/plugin/classes/LogFlowExecutionErrorEventHandler.cls +++ b/nebula-logger/plugins/async-failure-additions/plugin/classes/LogFlowExecutionErrorEventHandler.cls @@ -33,7 +33,13 @@ public without sharing class LogFlowExecutionErrorEventHandler { ); flowLogEntry.timestamp = flowExecutionErrorEvent.EventDate; LogEntryEventBuilder builder = flowLogEntry.addToLoggerBuffer()?.setRecord(flowExecutionErrorEvent.FlowVersionId); - if (builder != null) { + if (builder.shouldSave() == true) { + // TODO remove this in a future release, this is a temporary fix until orgs have upgraded to v4.8.4. + // Prior to v4.8.4, there was a bug in core package where the TimestampString__c field would be out of + // sync with the Timestamp__c field, resulting in inaccurate data being used. + // builder.getLogEntryEvent().Timestamp__c = flowLogEntry.timestamp.getTime()); + builder.getLogEntryEvent().TimestampString__c = String.valueOf(flowLogEntry.timestamp.getTime()); + List builders = usernameToBuilders.containsKey(flowExecutionErrorEvent.Username) ? usernameToBuilders.get(flowExecutionErrorEvent.Username) : new List(); diff --git a/nebula-logger/plugins/async-failure-additions/plugin/classes/LogFlowExecutionErrorEventHandler_Tests.cls b/nebula-logger/plugins/async-failure-additions/plugin/classes/LogFlowExecutionErrorEventHandler_Tests.cls index e97553e20..e6777f4ad 100644 --- a/nebula-logger/plugins/async-failure-additions/plugin/classes/LogFlowExecutionErrorEventHandler_Tests.cls +++ b/nebula-logger/plugins/async-failure-additions/plugin/classes/LogFlowExecutionErrorEventHandler_Tests.cls @@ -9,9 +9,9 @@ private class LogFlowExecutionErrorEventHandler_Tests { static void it_does_not_log_when_plugin_parameter_disabled() { setLoggerParamEnabled(false); - Test.startTest(); + System.Test.startTest(); LogFlowExecutionErrorEventHandler.logErrors(new List{ new FlowExecutionErrorEvent() }); - Test.stopTest(); + System.Test.stopTest(); System.assertEquals(0, [SELECT COUNT() FROM Log__c]); } @@ -27,9 +27,9 @@ private class LogFlowExecutionErrorEventHandler_Tests { flowExecutionErrorEvent.Username = UserInfo.getUserName(); System.runAs(new User(Id = [SELECT Id FROM User WHERE Alias = 'autoproc'].Id)) { - Test.startTest(); + System.Test.startTest(); LogFlowExecutionErrorEventHandler.logErrors(new List{ flowExecutionErrorEvent }); - Test.stopTest(); + System.Test.stopTest(); } LogEntry__c entry = [ diff --git a/nebula-logger/plugins/async-failure-additions/plugin/classes/LogFlowExecutionErrorEventHandler_Tests.cls-meta.xml b/nebula-logger/plugins/async-failure-additions/plugin/classes/LogFlowExecutionErrorEventHandler_Tests.cls-meta.xml index 4b0bc9f38..c47403d0b 100644 --- a/nebula-logger/plugins/async-failure-additions/plugin/classes/LogFlowExecutionErrorEventHandler_Tests.cls-meta.xml +++ b/nebula-logger/plugins/async-failure-additions/plugin/classes/LogFlowExecutionErrorEventHandler_Tests.cls-meta.xml @@ -1,4 +1,4 @@ - + 55.0 Active diff --git a/nebula-logger/plugins/async-failure-additions/plugin/customMetadata/LoggerParameter.FlowExecutionErrorEventHandled.md-meta.xml b/nebula-logger/plugins/async-failure-additions/plugin/customMetadata/LoggerParameter.FlowExecutionErrorEventHandled.md-meta.xml index fc36226dd..0ceb7d870 100644 --- a/nebula-logger/plugins/async-failure-additions/plugin/customMetadata/LoggerParameter.FlowExecutionErrorEventHandled.md-meta.xml +++ b/nebula-logger/plugins/async-failure-additions/plugin/customMetadata/LoggerParameter.FlowExecutionErrorEventHandled.md-meta.xml @@ -1,10 +1,16 @@ - - + + true Description__c - "true" if enabled, "false" if disabled. True values create Nebula Logger entries for unhandled flow errors. + "true" if enabled, "false" if disabled. True values create Nebula Logger entries for unhandled flow errors. Value__c diff --git a/nebula-logger/plugins/big-object-archiving/plugin/classes/LogEntryArchiveController.cls b/nebula-logger/plugins/big-object-archiving/plugin/classes/LogEntryArchiveController.cls index 97857448e..a1e1c3c95 100644 --- a/nebula-logger/plugins/big-object-archiving/plugin/classes/LogEntryArchiveController.cls +++ b/nebula-logger/plugins/big-object-archiving/plugin/classes/LogEntryArchiveController.cls @@ -9,6 +9,7 @@ * @see LogEntryArchivePlugin * @see LogEntryArchiveBuilder */ +@SuppressWarnings('PMD.ApexCRUDViolation') public with sharing class LogEntryArchiveController { @TestVisible private static final List MOCK_RECORDS = new List(); @@ -51,7 +52,6 @@ public with sharing class LogEntryArchiveController { TransactionId__c FROM LogEntryArchive__b WHERE Timestamp__c >= :startDate AND Timestamp__c <= :endDate.addDays(1) - WITH SECURITY_ENFORCED ORDER BY Timestamp__c DESC, TransactionId__c ASC, TransactionEntryNumber__c DESC LIMIT :queryLimit ]) { diff --git a/nebula-logger/plugins/slack/README.md b/nebula-logger/plugins/slack/README.md index 156504705..bcab1a187 100644 --- a/nebula-logger/plugins/slack/README.md +++ b/nebula-logger/plugins/slack/README.md @@ -2,8 +2,8 @@ > :information_source: This plugin requires `v4.7.1` or newer of Nebula Logger's unlocked package -[![Install Unlocked Package Plugin in a Sandbox](../.images/btn-install-unlocked-package-plugin-sandbox.png)](https://test.salesforce.com/packaging/installPackage.apexp?p0=04t5Y0000015lvVQAQ) -[![Install Unlocked Package Plugin in Production](../.images/btn-install-unlocked-package-plugin-production.png)](https://login.salesforce.com/packaging/installPackage.apexp?p0=04t5Y0000015lvVQAQ) +[![Install Unlocked Package Plugin in a Sandbox](../.images/btn-install-unlocked-package-plugin-sandbox.png)](https://test.salesforce.com/packaging/installPackage.apexp?p0=04t5Y0000023Qu8QAE) +[![Install Unlocked Package Plugin in Production](../.images/btn-install-unlocked-package-plugin-production.png)](https://login.salesforce.com/packaging/installPackage.apexp?p0=04t5Y0000023Qu8QAE) Adds a Slack integration for the unlocked package edition of Nebula Logger. Any logs with log entries that meet a certain (configurable) logging level will automatically be posted to your Slack channel via an asynchronous `Queueable` job. diff --git a/nebula-logger/plugins/slack/plugin/slack/classes/SlackLoggerPlugin.cls b/nebula-logger/plugins/slack/plugin/slack/classes/SlackLoggerPlugin.cls index d944c73ea..9d186f873 100644 --- a/nebula-logger/plugins/slack/plugin/slack/classes/SlackLoggerPlugin.cls +++ b/nebula-logger/plugins/slack/plugin/slack/classes/SlackLoggerPlugin.cls @@ -187,7 +187,7 @@ public without sharing class SlackLoggerPlugin implements LoggerPlugin.Triggerab @SuppressWarnings('PMD.NcssMethodCount') private LogDto convertLog(Log__c log) { LogEntry__c lastLogEntry = log.LogEntries__r.get(0); - String messageText = 'Last Log Entry Message' + '\n`' + lastLogEntry.LoggingLevel__c + ': ' + lastLogEntry.Message__c + '`'; + String messageText = 'Last Log Entry Message' + '\n```' + lastLogEntry.LoggingLevel__c + ': ' + lastLogEntry.Message__c + '```'; LogDto notification = new LogDto(); notification.author_link = Url.getSalesforceBaseUrl().toExternalForm() + '/' + log.LoggedBy__c; diff --git a/nebula-logger/recipes/permissionsets/LoggerRecipesAdmin.permissionset-meta.xml b/nebula-logger/recipes/permissionsets/LoggerRecipesAdmin.permissionset-meta.xml new file mode 100644 index 000000000..ec75996b6 --- /dev/null +++ b/nebula-logger/recipes/permissionsets/LoggerRecipesAdmin.permissionset-meta.xml @@ -0,0 +1,38 @@ + + + + LoggerRecipes + true + + + Account_Batch_Logger_Example + true + + + Account_Queueable_Logger_Example + true + + + ExampleBigObjectDataGenerator + true + + + ExampleClassWithLogging + true + + + LoggerLWCDemoController + true + + false + + Salesforce + + LoggerAuraDemo + Visible + + + Logger_lwc_demo + Visible + + diff --git a/nebula-logger/recipes/profiles/Admin.profile-meta.xml b/nebula-logger/unsorted/default/profiles/Admin.profile-meta.xml similarity index 100% rename from nebula-logger/recipes/profiles/Admin.profile-meta.xml rename to nebula-logger/unsorted/default/profiles/Admin.profile-meta.xml diff --git a/package.json b/package.json index 4d1d1900d..c5472ea7c 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "nebula-logger", - "version": "4.8.3", - "description": "Designed for Salesforce admins, developers & architects. A robust logger for Apex, Flow, Process Builder & Integrations.", + "version": "4.8.4", + "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", "repository": { @@ -44,8 +44,8 @@ "lint:verify": "npm run lint:verify:apex && npm run lint:verify:lwc", "lint:verify:apex": "sfdx scanner:run --pmdconfig ./config/linters/pmd-ruleset.xml --target ./nebula-logger/ --engine pmd --severity-threshold 3", "lint:verify:lwc": "eslint --config ./config/linters/.eslintrc.json **/lwc/** && eslint --config ./config/linters/.eslintrc.json **/aura/**", - "org:create:base": "pwsh ./scripts/build/create-scratch-org.ps1 -definitionfile ./config/scratch-orgs/base-scratch-def.json", - "org:create:experience-cloud": "pwsh ./scripts/build/create-scratch-org.ps1 -definitionfile ./config/scratch-orgs/experience-cloud-scratch-def.json", + "org:create:base": "pwsh ./scripts/build/create-scratch-org.ps1 -definitionfile ./config/scratch-orgs/base-scratch-def.json && sfdx force:apex:execute --apexcodefile ./scripts/build/enable-debug-mode.apex", + "org:create:experience-cloud": "pwsh ./scripts/build/create-scratch-org.ps1 -definitionfile ./config/scratch-orgs/experience-cloud-scratch-def.json && sfdx force:apex:execute --apexcodefile ./scripts/build/enable-debug-mode.apex", "org:delete": "sfdx force:org:delete --json", "org:delete:noprompt": "sfdx force:org:delete --json --noprompt", "org:details": "sfdx force:org:display --json --verbose", @@ -78,7 +78,7 @@ "test": "npm run test:lwc && npm run test:apex", "test:apex": "sfdx force:apex:test:run --verbose --testlevel RunLocalTests --wait 30 --resultformat human --codecoverage --detailedcoverage --outputdir ./test-coverage/apex", "test:apex:nocoverage": "sfdx force:apex:test:run --testlevel RunLocalTests --wait 30 --resultformat human --outputdir ./test-coverage/apex", - "test:apex:suites": "sfdx force:apex:test:run --verbose --suitenames LoggerConfiguration,LoggerEngine,LoggerLogManagement --wait 30 --resultformat human --codecoverage --detailedcoverage --outputdir ./tests/apex", + "test:apex:suites": "sfdx force:apex:test:run --verbose --suitenames LoggerCore,LoggerExtraTests --wait 30 --resultformat human --codecoverage --detailedcoverage --outputdir ./tests/apex", "test:lwc": "sfdx-lwc-jest --coverage --skipApiVersionCheck --verbose", "test:lwc:nocoverage": "sfdx-lwc-jest --skipApiVersionCheck --verbose", "test:nocoverage": "npm run test:lwc && npm run test:apex" diff --git a/scripts/build/enable-debug-mode.apex b/scripts/build/enable-debug-mode.apex new file mode 100644 index 000000000..aa27d0bdb --- /dev/null +++ b/scripts/build/enable-debug-mode.apex @@ -0,0 +1 @@ +update new User(Id = UserInfo.getUserId(), UserPreferencesUserDebugModePref = true); \ No newline at end of file diff --git a/scripts/build/generate-apex-docs.ps1 b/scripts/build/generate-apex-docs.ps1 index 766fbe7f7..ccf1bb4a6 100644 --- a/scripts/build/generate-apex-docs.ps1 +++ b/scripts/build/generate-apex-docs.ps1 @@ -2,6 +2,7 @@ find ./docs/apex/ -maxdepth 2 -type f -name "*.md" -delete npx apexdocs-generate --configPath ./config/docs/apexdocs.json --scope global public --sourceDir ./nebula-logger/core/ ./nebula-logger/plugins/ --targetDir ./docs/apex --targetGenerator jekyll +# npx apexdocs-generate --configPath ./config/docs/apexdocs.json --group false --scope global public --sourceDir ./nebula-logger/core/ ./nebula-logger/plugins/ --targetDir ./wiki2 --targetGenerator jekyll # Make a few adjustments to the generated markdown files so that they work correctly in Github Pages $indexPageFile = "docs/apex/index.md" diff --git a/sfdx-project.json b/sfdx-project.json index 77ab38037..c24769b39 100644 --- a/sfdx-project.json +++ b/sfdx-project.json @@ -1,22 +1,24 @@ { + "name": "Nebula Logger", "namespace": "", - "sfdcLoginUrl": "https://login.salesforce.com", "sourceApiVersion": "55.0", + "sfdcLoginUrl": "https://login.salesforce.com", "plugins": { "sfdx-plugin-prettier": { "enabled": true } }, + "pushPackageDirectoriesSequentially": true, "packageDirectories": [ { "package": "Nebula Logger - Core", "path": "./nebula-logger/core", "definitionFile": "./config/scratch-orgs/base-scratch-def.json", - "versionNumber": "4.8.3.NEXT", - "versionName": "Improved LogEntry__c Formula Fields for Limits", - "versionDescription": "Updated formula fields on LogEntry__c for limits to include the percentage of each limit used & to include a flag indicator (>= 90% displays a red flag, < 90 && >= 80: displays a yellow flag, otherwise a green flag is displayed)", + "versionNumber": "4.8.4.NEXT", + "versionName": "Optimized Synchronous Context", + "versionDescription": "Added new LoggerParameter__mdt records to control synchronous queries & stack trace parsing, incorporated Platform Cache into LoggerCache class", "releaseNotesUrl": "https://github.com/jongpie/NebulaLogger/releases", - "default": true + "default": false }, { "default": false, @@ -78,9 +80,9 @@ "package": "Nebula Logger - Core@4.7.1-plugin-framework-overhaul" } ], - "versionName": "Improved formatting for stack traces", - "versionNumber": "1.5.0.NEXT", - "versionDescription": "Cleaned up formatting of stack trace fields to handle line breaks", + "versionName": "Improved formatting for log entry message", + "versionNumber": "1.5.1.NEXT", + "versionDescription": "Updated formatting of the log entry message to handle line breaks", "default": false }, { @@ -90,6 +92,10 @@ { "path": "./nebula-logger/recipes", "default": false + }, + { + "path": "./nebula-logger/unsorted", + "default": true } ], "packageAliases": { @@ -133,9 +139,11 @@ "Nebula Logger - Core@4.8.1-new-logger-scenario-custom-object": "04t5Y0000015luIQAQ", "Nebula Logger - Core@4.8.2-more-controls-for-scenario-based-logging": "04t5Y0000015lvuQAA", "Nebula Logger - Core@4.8.3-improved-logentry__c-formula-fields-for-limits": "04t5Y0000015lw9QAA", + "Nebula Logger - Core@4.8.4-optimized-synchronous-context": "04t5Y0000023R02QAE", "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", + "Nebula Logger - Plugin - Async Failure Additions@1.0.2": "04t5Y0000023QttQAE", "Nebula Logger - Plugin - Big Object Archiving": "0Ho5Y000000blMSSAY", "Nebula Logger - Plugin - Big Object Archiving@0.9.0": "04t5Y0000015lgLQAQ", "Nebula Logger - Plugin - Log Retention Rules": "0Ho5Y000000blNfSAI", @@ -147,6 +155,7 @@ "Nebula Logger - Plugin - Slack@0.9.1-beta-release-round-2": "04t5e00000065xiAAA", "Nebula Logger - Plugin - Slack@0.9.2-beta-release-round-3": "04t5Y0000015l2WQAQ", "Nebula Logger - Plugin - Slack@0.10.0": "04t5Y0000015lgQQAQ", - "Nebula Logger - Plugin - Slack@1.5.0": "04t5Y0000015lvVQAQ" + "Nebula Logger - Plugin - Slack@1.5.0": "04t5Y0000015lvVQAQ", + "Nebula Logger - Plugin - Slack@1.5.1": "04t5Y0000023Qu8QAE" } }