From f6b0afc5b59aff065ae9102a912df3799b93c6a2 Mon Sep 17 00:00:00 2001 From: Jonathan Gillespie Date: Wed, 13 Mar 2024 14:37:06 -0400 Subject: [PATCH] Optionally enforce scenario-based logging usage (#648) * Added the ability to optionally enforce using scenario-based logging. This is controlled by a new LoggerParameter__mdt record, RequireScenarioUsage, and is disabled by default. * Upgraded LoggerScenarioRecordPage flexipage to use dynamic related lists, added missing field permission to LoggerAdmin * Scope creep: added a shell script to handle generating log files for 'sf apex tail log' --- README.md | 10 +- config/linters/lint-staged.config.js | 11 +- docs/apex/Configuration/LoggerParameter.md | 4 + docs/apex/Logger-Engine/Logger.md | 4 + .../configuration/classes/LoggerParameter.cls | 20 +++ ...Parameter.RequireScenarioUsage.md-meta.xml | 19 +++ ...oggerScenarioRecordPage.flexipage-meta.xml | 135 ++++++++++++++++-- .../LoggerAdmin.permissionset-meta.xml | 5 + .../main/logger-engine/classes/Logger.cls | 12 +- .../lwc/logger/logEntryBuilder.js | 2 +- .../classes/LoggerParameter_Tests.cls | 27 ++++ .../logger-engine/classes/Logger_Tests.cls | 19 +++ package.json | 4 +- scripts/build/tail-log-to-file.sh | 6 + sfdx-project.json | 7 +- 15 files changed, 256 insertions(+), 29 deletions(-) create mode 100644 nebula-logger/core/main/configuration/customMetadata/LoggerParameter.RequireScenarioUsage.md-meta.xml create mode 100644 scripts/build/tail-log-to-file.sh diff --git a/README.md b/README.md index eacf02963..cd07bddd4 100644 --- a/README.md +++ b/README.md @@ -5,15 +5,15 @@ The most robust logger for Salesforce. Works with Apex, Lightning Components, Flow, Process Builder & Integrations. Designed for Salesforce admins, developers & architects. -## Unlocked Package - v4.13.2 +## Unlocked Package - v4.13.3 -[![Install Unlocked Package in a Sandbox](./images/btn-install-unlocked-package-sandbox.png)](https://test.salesforce.com/packaging/installPackage.apexp?p0=04t5Y000001MkEmQAK) -[![Install Unlocked Package in Production](./images/btn-install-unlocked-package-production.png)](https://login.salesforce.com/packaging/installPackage.apexp?p0=04t5Y000001MkEmQAK) +[![Install Unlocked Package in a Sandbox](./images/btn-install-unlocked-package-sandbox.png)](https://test.salesforce.com/packaging/installPackage.apexp?p0=04t5Y000001MkEwQAK) +[![Install Unlocked Package in Production](./images/btn-install-unlocked-package-production.png)](https://login.salesforce.com/packaging/installPackage.apexp?p0=04t5Y000001MkEwQAK) [![View Documentation](./images/btn-view-documentation.png)](https://jongpie.github.io/NebulaLogger/) -`sf package install --wait 20 --security-type AdminsOnly --package 04t5Y000001MkEmQAK` +`sf package install --wait 20 --security-type AdminsOnly --package 04t5Y000001MkEwQAK` -`sfdx force:package:install --wait 20 --securitytype AdminsOnly --package 04t5Y000001MkEmQAK` +`sfdx force:package:install --wait 20 --securitytype AdminsOnly --package 04t5Y000001MkEwQAK` --- diff --git a/config/linters/lint-staged.config.js b/config/linters/lint-staged.config.js index c48f715ac..1c05918bc 100644 --- a/config/linters/lint-staged.config.js +++ b/config/linters/lint-staged.config.js @@ -7,10 +7,11 @@ module.exports = { return [`eslint --config ./config/linters/.eslintrc.json ${filenames.join(' ')} --fix`]; // FIXME this command should only run tests for the changed LWCs (instead of running tests for all LWCs) // return [`eslint --config ./config/linters/.eslintrc.json ${filenames.join(' ')} --fix`, `npm run test:lwc`]; + }, + '*.{cls,trigger}': filenames => { + return filenames.map( + filename => `sf scanner run --pmdconfig ./config/linters/pmd-ruleset.xml --engine pmd --severity-threshold 3 --target '${filename}'` + ); + // return [`npm run scan:apex`, `npm run docs:fix && git add ./docs/ && git commit --amend --no-edit`]; } - // FIXME this command should only scan the changed Apex files (instead of scanning all Apex files) - // '*.{cls,trigger}': () => { - // return [`npm run scan:apex`]; - // // return [`npm run scan:apex`, `npm run docs:fix && git add ./docs/ && git commit --amend --no-edit`]; - // } }; diff --git a/docs/apex/Configuration/LoggerParameter.md b/docs/apex/Configuration/LoggerParameter.md index cc1f25232..8791a8ce5 100644 --- a/docs/apex/Configuration/LoggerParameter.md +++ b/docs/apex/Configuration/LoggerParameter.md @@ -94,6 +94,10 @@ Controls if Nebula Logger queries `Schema.User` data. When set to `false`, any ` Indicates if Nebula Logger queries `Schema.User` data is queried synchronously & populated on `LogEntryEvent__e` records. When set to `false`, any `Schema.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 +#### `REQUIRE_SCENARIO_USAGE` → `Boolean` + +Indicates if Nebula Logger will enforce scenario-based logging to be used. When set to `false`, specifying a scenario is completely optional. When set to `true`, a scenario is required to be set before any logging can occur. If a logging method is called & the current scenario is null/blank, then Nebula Logger will throw a runtime exception. Controlled by the custom metadata record `LoggerParameter.RequireScenarioUsage`, or `false` as the default + #### `SEND_ERROR_EMAIL_NOTIFICATIONS` → `Boolean` Indicates if Nebula 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 diff --git a/docs/apex/Logger-Engine/Logger.md b/docs/apex/Logger-Engine/Logger.md index 7a1c59cd4..8f0f15b44 100644 --- a/docs/apex/Logger-Engine/Logger.md +++ b/docs/apex/Logger-Engine/Logger.md @@ -5730,6 +5730,10 @@ The new entry's instance of `LogEntryEventBuilder`, useful for chaining met ### Inner Classes +#### Logger.LoggerException class + +--- + #### Logger.QueueableSaver class Inner class for publishing log entries via the Queueable interface. diff --git a/nebula-logger/core/main/configuration/classes/LoggerParameter.cls b/nebula-logger/core/main/configuration/classes/LoggerParameter.cls index 6b5b26e00..80831de63 100644 --- a/nebula-logger/core/main/configuration/classes/LoggerParameter.cls +++ b/nebula-logger/core/main/configuration/classes/LoggerParameter.cls @@ -335,6 +335,26 @@ public class LoggerParameter { private set; } + /** + * @description Indicates if Nebula Logger will enforce scenario-based logging to be used. + * When set to `false`, specifying a scenario is completely optional. + * When set to `true`, a scenario is required to be set before any logging can occur. + * If a logging method is called & the current scenario is null/blank, then Nebula Logger will throw a runtime exception. + * Controlled by the custom metadata record `LoggerParameter.RequireScenarioUsage`, or `false` as the default + */ + public static final Boolean REQUIRE_SCENARIO_USAGE { + get { + if (REQUIRE_SCENARIO_USAGE == null) { + // Most features in Nebula Logger are enabled (true) by default, + // but this one is intentionally set to false by default - not + // all orgs want or need to use scenario-based logging + REQUIRE_SCENARIO_USAGE = getBoolean('RequireScenarioUsage', false); + } + return REQUIRE_SCENARIO_USAGE; + } + private set; + } + /** * @description Indicates if Nebula 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 diff --git a/nebula-logger/core/main/configuration/customMetadata/LoggerParameter.RequireScenarioUsage.md-meta.xml b/nebula-logger/core/main/configuration/customMetadata/LoggerParameter.RequireScenarioUsage.md-meta.xml new file mode 100644 index 000000000..20f5c61fa --- /dev/null +++ b/nebula-logger/core/main/configuration/customMetadata/LoggerParameter.RequireScenarioUsage.md-meta.xml @@ -0,0 +1,19 @@ + + + + false + + Description__c + When set to 'false' (default), specifying a scenario is completely optional. + +When set to 'true', a scenario is required to be set before any logging can occur. If a logging method is called & the current scenario is null/blank, then Nebula Logger will throw a runtime exception. + + + Value__c + false + + diff --git a/nebula-logger/core/main/log-management/flexipages/LoggerScenarioRecordPage.flexipage-meta.xml b/nebula-logger/core/main/log-management/flexipages/LoggerScenarioRecordPage.flexipage-meta.xml index bb645707e..ee1001a16 100644 --- a/nebula-logger/core/main/log-management/flexipages/LoggerScenarioRecordPage.flexipage-meta.xml +++ b/nebula-logger/core/main/log-management/flexipages/LoggerScenarioRecordPage.flexipage-meta.xml @@ -33,6 +33,21 @@ + + actionNames + + + Log__c.Manage + + + + + adminFilters + + + maxRecordsToDisplay + 30 + parentFieldApiName LoggerScenario__c.Id @@ -42,19 +57,62 @@ Logs__r - relatedListComponentOverride + relatedListDisplayType ADVGRID - rowsToDisplay - 30 + relatedListFieldAliases + + + NAME + + + StartTime__c + + + LoggedByUsernameLink__c + + + TransactionId__c + + + TotalLogEntries__c + + + TotalERRORLogEntries__c + + + TotalWARNLogEntries__c + + + OWNER.ALIAS + + + Priority__c + + + Status__c + + + + + relatedListLabel + Logs showActionBar true - force:relatedListSingleContainer - force_relatedListSingleContainer + + sortFieldAlias + StartTime__c + + + sortFieldOrder + Descending + + lst:dynamicRelatedList + lst_dynamicRelatedList relatedTabContent @@ -63,6 +121,18 @@ + + actionNames + + + MassChangeOwner + + + + + maxRecordsToDisplay + 30 + parentFieldApiName LoggerScenario__c.Id @@ -72,19 +142,56 @@ LogEntries__r - relatedListComponentOverride + relatedListDisplayType ADVGRID - rowsToDisplay - 30 + relatedListFieldAliases + + + NAME + + + Timestamp__c + + + LoggedByUsernameLink__c + + + Origin__c + + + Message__c + + + LoggingLevelWithImage__c + + + Log__c + + + RecordLink__c + + + + + relatedListLabel + Log Entries showActionBar - true + false - force:relatedListSingleContainer - force_relatedListSingleContainer3 + + sortFieldAlias + Timestamp__c + + + sortFieldOrder + Descending + + lst:dynamicRelatedList + lst_dynamicRelatedList2 Facet-bf77ace2-6cee-4ca4-88b4-990633a74278 @@ -125,7 +232,7 @@ active - true + false body @@ -141,6 +248,10 @@ + + active + false + body Facet-bf77ace2-6cee-4ca4-88b4-990633a74278 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 68f3f3ad8..cb8131fac 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 @@ -1558,6 +1558,11 @@ Log__c.TransactionScenarioText__c true + + false + Log__c.TransactionScenario__c + true + false Log__c.UserLicenseDefinitionKey__c diff --git a/nebula-logger/core/main/logger-engine/classes/Logger.cls b/nebula-logger/core/main/logger-engine/classes/Logger.cls index da6f24f64..806ca1aa5 100644 --- a/nebula-logger/core/main/logger-engine/classes/Logger.cls +++ b/nebula-logger/core/main/logger-engine/classes/Logger.cls @@ -15,9 +15,10 @@ global with sharing class Logger { // There's no reliable way to get the version number dynamically in Apex @TestVisible - private static final String CURRENT_VERSION_NUMBER = 'v4.13.2'; + private static final String CURRENT_VERSION_NUMBER = 'v4.13.3'; private static final System.LoggingLevel FALLBACK_LOGGING_LEVEL = System.LoggingLevel.DEBUG; private static final List LOG_ENTRIES_BUFFER = new List(); + private static final String MISSING_SCENARIO_ERROR_MESSAGE = 'No logger scenario specified. A scenario is required for logging in this org.'; private static final String ORGANIZATION_DOMAIN_URL = System.URL.getOrgDomainUrl()?.toExternalForm(); private static final String REQUEST_ID = System.Request.getCurrent().getRequestId(); private static final Map SAVE_METHOD_NAME_TO_SAVE_METHOD = new Map(); @@ -3351,6 +3352,10 @@ global with sharing class Logger { private static LogEntryEventBuilder newEntry(System.LoggingLevel loggingLevel, Boolean shouldSave) { LogEntryEventBuilder logEntryEventBuilder = new LogEntryEventBuilder(getUserSettings(), loggingLevel, shouldSave); if (logEntryEventBuilder.shouldSave()) { + if (LoggerParameter.REQUIRE_SCENARIO_USAGE && String.isBlank(currentEntryScenario)) { + throw new LoggerException(MISSING_SCENARIO_ERROR_MESSAGE); + } + LogEntryEvent__e logEntryEvent = logEntryEventBuilder.getLogEntryEvent(); logEntryEvent.ApiVersion__c = getOrganizationApiVersion(); logEntryEvent.EntryScenario__c = currentEntryScenario; @@ -3493,6 +3498,11 @@ global with sharing class Logger { } } + // Inner class used for any exceptions specific to Nebula Logger functionality + @SuppressWarnings('PMD.ApexDoc') + public class LoggerException extends Exception { + } + // Inner class used for deserializing data from the status API @SuppressWarnings('PMD.ApexDoc, PMD.VariableNamingConventions') public class StatusApiResponse { diff --git a/nebula-logger/core/main/logger-engine/lwc/logger/logEntryBuilder.js b/nebula-logger/core/main/logger-engine/lwc/logger/logEntryBuilder.js index 6212e8c4d..9236b1756 100644 --- a/nebula-logger/core/main/logger-engine/lwc/logger/logEntryBuilder.js +++ b/nebula-logger/core/main/logger-engine/lwc/logger/logEntryBuilder.js @@ -4,7 +4,7 @@ //------------------------------------------------------------------------------------------------// import FORM_FACTOR from '@salesforce/client/formFactor'; -const CURRENT_VERSION_NUMBER = 'v4.13.2'; +const CURRENT_VERSION_NUMBER = 'v4.13.3'; // JavaScript equivalent to the Apex class ComponentLogger.ComponentLogEntry const ComponentLogEntry = class { diff --git a/nebula-logger/core/tests/configuration/classes/LoggerParameter_Tests.cls b/nebula-logger/core/tests/configuration/classes/LoggerParameter_Tests.cls index c65031596..fef755e5e 100644 --- a/nebula-logger/core/tests/configuration/classes/LoggerParameter_Tests.cls +++ b/nebula-logger/core/tests/configuration/classes/LoggerParameter_Tests.cls @@ -272,6 +272,33 @@ private class LoggerParameter_Tests { System.Assert.areEqual(mockValue, returnedValue); } + @IsTest + static void it_should_use_false_as_default_constant_value_for_require_scenario_usage_when_null() { + LoggerParameter.setMock(new LoggerParameter__mdt(DeveloperName = 'RequireScenarioUsage', Value__c = null)); + + Boolean returnedValue = LoggerParameter.REQUIRE_SCENARIO_USAGE; + + System.Assert.isFalse(returnedValue); + } + + @IsTest + static void it_should_return_constant_value_for_require_scenario_usage_when_true() { + LoggerParameter.setMock(new LoggerParameter__mdt(DeveloperName = 'RequireScenarioUsage', Value__c = JSON.serialize(true))); + + Boolean returnedValue = LoggerParameter.REQUIRE_SCENARIO_USAGE; + + System.Assert.isTrue(returnedValue); + } + + @IsTest + static void it_should_return_constant_value_for_require_scenario_usage_when_false() { + LoggerParameter.setMock(new LoggerParameter__mdt(DeveloperName = 'RequireScenarioUsage', Value__c = JSON.serialize(false))); + + Boolean returnedValue = LoggerParameter.REQUIRE_SCENARIO_USAGE; + + System.Assert.isFalse(returnedValue); + } + @IsTest static void it_should_return_constant_value_for_send_error_email_notifications() { Boolean mockValue = false; 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 f75d9f5e5..ee0ca5a90 100644 --- a/nebula-logger/core/tests/logger-engine/classes/Logger_Tests.cls +++ b/nebula-logger/core/tests/logger-engine/classes/Logger_Tests.cls @@ -325,6 +325,25 @@ private class Logger_Tests { System.Assert.areEqual(mockScenario, Logger.getScenario(), 'The mock scenario should have been used as the current transaction scenario'); } + @IsTest + static void it_should_require_scenario_when_parameter_enabled() { + LoggerParameter__mdt mockParameter = new LoggerParameter__mdt(DeveloperName = 'RequireScenarioUsage', Value__c = String.valueOf(true)); + LoggerParameter.setMock(mockParameter); + System.Assert.isTrue(LoggerParameter.REQUIRE_SCENARIO_USAGE); + String nullScenario = null; + + Logger.setScenario(nullScenario); + Exception thrownException; + try { + Logger.info('Some message'); + } catch (Exception ex) { + thrownException = ex; + } + + System.Assert.isNotNull(thrownException, 'An exception should have been thrown due to a null scenario'); + System.Assert.isInstanceOfType(thrownException, Logger.LoggerException.class); + } + @IsTest static void it_should_use_the_first_specified_scenario_as_transaction_scenario_when_parameter_is_true() { LoggerDataStore.setMock(LoggerMockDataStore.getEventBus()); diff --git a/package.json b/package.json index 15ee1f6ca..0bb6e5a49 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "nebula-logger", - "version": "4.13.2", + "version": "4.13.3", "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", @@ -33,7 +33,7 @@ }, "scripts": { "apex:tail:log": "sf apex tail log --color", - "apex:tail:log:file": "sf apex tail log | tee sf_tail.log", + "apex:tail:log:file": "sh scripts/build/tail-log-to-file.sh", "devhub:details": "pwsh ./scripts/build/get-devhub-org-details.ps1", "devhub:limits": "pwsh ./scripts/build/get-devhub-org-limits.ps1", "devhub:open": "pwsh ./scripts/build/open-devhub-org.ps1", diff --git a/scripts/build/tail-log-to-file.sh b/scripts/build/tail-log-to-file.sh new file mode 100644 index 000000000..8f68dca82 --- /dev/null +++ b/scripts/build/tail-log-to-file.sh @@ -0,0 +1,6 @@ +now=$(date +"%Y-%m-%dT%H_%M_%S%z") +logDirectory="logs" + +echo "Starting sf apex tail log at ${now}" +[[ -d ${logDirectory} ]] || mkdir ${logDirectory} +sf apex tail log --color | tee ./${logDirectory}/sf_tail_${now}.log diff --git a/sfdx-project.json b/sfdx-project.json index d5b1e9c53..7287749f7 100644 --- a/sfdx-project.json +++ b/sfdx-project.json @@ -14,9 +14,9 @@ "path": "./nebula-logger/core", "definitionFile": "./config/scratch-orgs/base-scratch-def.json", "scopeProfiles": true, - "versionNumber": "4.13.2.NEXT", - "versionName": "Capture Organization Limits Usage", - "versionDescription": "Added the ability to capture & view organization limits on Log__c with new LWC logOrganizationLimits, as well as the ability to toggle capturing of transaction limits with a new LoggerParameter__mdt record", + "versionNumber": "4.13.3.NEXT", + "versionName": "Optionally Enforce Scenario Usage", + "versionDescription": "Added the ability to require scenario-based logging, using a new LoggerParameter__mdt record 'RequireScenarioUsage'", "releaseNotesUrl": "https://github.com/jongpie/NebulaLogger/releases", "unpackagedMetadata": { "path": "./nebula-logger/extra-tests" @@ -170,6 +170,7 @@ "Nebula Logger - Core@4.13.0-spring-'24-release": "04t5Y000001Mk8dQAC", "Nebula Logger - Core@4.13.1-apex-observability": "04t5Y000001MkE3QAK", "Nebula Logger - Core@4.13.2-capture-organization-limits-usage": "04t5Y000001MkEmQAK", + "Nebula Logger - Core@4.13.3-optionally-enforce-scenario-usage": "04t5Y000001MkEwQAK", "Nebula Logger - Core Plugin - Async Failure Additions": "0Ho5Y000000blO4SAI", "Nebula Logger - Core Plugin - Async Failure Additions@1.0.0": "04t5Y0000015lhiQAA", "Nebula Logger - Core Plugin - Async Failure Additions@1.0.1": "04t5Y0000015lhsQAA",