diff --git a/README.md b/README.md index 5cb7f9d76..3c82a9b4d 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ The most robust observability solution for Salesforce experts. Built 100% natively on the platform, and designed to work seamlessly with Apex, Lightning Components, Flow, OmniStudio, and integrations. -## Unlocked Package - v4.14.14 +## Unlocked Package - v4.14.16 [![Install Unlocked Package in a Sandbox](./images/btn-install-unlocked-package-sandbox.png)](https://test.salesforce.com/packaging/installPackage.apexp?p0=04t5Y0000015oWIQAY) [![Install Unlocked Package in Production](./images/btn-install-unlocked-package-production.png)](https://login.salesforce.com/packaging/installPackage.apexp?p0=04t5Y0000015oWIQAY) @@ -238,41 +238,41 @@ This example batchable class shows how you can leverage this feature to relate a ```apex public with sharing class BatchableLoggerExample implements Database.Batchable, Database.Stateful { - private String originalTransactionId; + private String originalTransactionId; - public Database.QueryLocator start(Database.BatchableContext batchableContext) { - // Each batchable method runs in a separate transaction, - // so store the first transaction ID to later relate the other transactions - this.originalTransactionId = Logger.getTransactionId(); + public Database.QueryLocator start(Database.BatchableContext batchableContext) { + // Each batchable method runs in a separate transaction, + // so store the first transaction ID to later relate the other transactions + this.originalTransactionId = Logger.getTransactionId(); - Logger.info('Starting BatchableLoggerExample'); - Logger.saveLog(); + Logger.info('Starting BatchableLoggerExample'); + Logger.saveLog(); - // Just as an example, query all accounts - return Database.getQueryLocator([SELECT Id, Name, RecordTypeId FROM Account]); - } - - public void execute(Database.BatchableContext batchableContext, List scope) { - // One-time call (per transaction) to set the parent log - Logger.setParentLogTransactionId(this.originalTransactionId); + // Just as an example, query all accounts + return Database.getQueryLocator([SELECT Id, Name, RecordTypeId FROM Account]); + } - for (Account account : scope) { - // Add your batch job's logic here + public void execute(Database.BatchableContext batchableContext, List scope) { + // One-time call (per transaction) to set the parent log + Logger.setParentLogTransactionId(this.originalTransactionId); - // Then log the result - Logger.info('Processed an account record', account); - } + for (Account account : scope) { + // Add your batch job's logic here - Logger.saveLog(); + // Then log the result + Logger.info('Processed an account record', account); } - public void finish(Database.BatchableContext batchableContext) { - // The finish method runs in yet-another transaction, so set the parent log again - Logger.setParentLogTransactionId(this.originalTransactionId); + Logger.saveLog(); + } - Logger.info('Finishing running BatchableLoggerExample'); - Logger.saveLog(); - } + public void finish(Database.BatchableContext batchableContext) { + // The finish method runs in yet-another transaction, so set the parent log again + Logger.setParentLogTransactionId(this.originalTransactionId); + + Logger.info('Finishing running BatchableLoggerExample'); + Logger.saveLog(); + } } ``` @@ -282,42 +282,42 @@ Queueable jobs can also leverage the parent transaction ID to relate logs togeth ```apex public with sharing class QueueableLoggerExample implements Queueable { - private Integer numberOfJobsToChain; - private String parentLogTransactionId; + private Integer numberOfJobsToChain; + private String parentLogTransactionId; - private List logEntryEvents = new List(); + private List logEntryEvents = new List(); - // Main constructor - for demo purposes, it accepts an integer that controls how many times the job runs - public QueueableLoggerExample(Integer numberOfJobsToChain) { - this(numberOfJobsToChain, null); - } + // Main constructor - for demo purposes, it accepts an integer that controls how many times the job runs + public QueueableLoggerExample(Integer numberOfJobsToChain) { + this(numberOfJobsToChain, null); + } - // Second constructor, used to pass the original transaction's ID to each chained instance of the job - // You don't have to use a constructor - a public method or property would work too. - // There just needs to be a way to pass the value of parentLogTransactionId between instances - public QueueableLoggerExample(Integer numberOfJobsToChain, String parentLogTransactionId) { - this.numberOfJobsToChain = numberOfJobsToChain; - this.parentLogTransactionId = parentLogTransactionId; - } + // Second constructor, used to pass the original transaction's ID to each chained instance of the job + // You don't have to use a constructor - a public method or property would work too. + // There just needs to be a way to pass the value of parentLogTransactionId between instances + public QueueableLoggerExample(Integer numberOfJobsToChain, String parentLogTransactionId) { + this.numberOfJobsToChain = numberOfJobsToChain; + this.parentLogTransactionId = parentLogTransactionId; + } - // Creates some log entries and starts a new instance of the job when applicable (based on numberOfJobsToChain) - public void execute(System.QueueableContext queueableContext) { - Logger.setParentLogTransactionId(this.parentLogTransactionId); + // Creates some log entries and starts a new instance of the job when applicable (based on numberOfJobsToChain) + public void execute(System.QueueableContext queueableContext) { + Logger.setParentLogTransactionId(this.parentLogTransactionId); - Logger.fine('queueableContext==' + queueableContext); - Logger.info('this.numberOfJobsToChain==' + this.numberOfJobsToChain); - Logger.info('this.parentLogTransactionId==' + this.parentLogTransactionId); + Logger.fine('queueableContext==' + queueableContext); + Logger.info('this.numberOfJobsToChain==' + this.numberOfJobsToChain); + Logger.info('this.parentLogTransactionId==' + this.parentLogTransactionId); - // Add your queueable job's logic here + // Add your queueable job's logic here - Logger.saveLog(); + Logger.saveLog(); - --this.numberOfJobsToChain; - if (this.numberOfJobsToChain > 0) { - String parentLogTransactionId = this.parentLogTransactionId != null ? this.parentLogTransactionId : Logger.getTransactionId(); - System.enqueueJob(new QueueableLoggerExample(this.numberOfJobsToChain, parentLogTransactionId)); - } + --this.numberOfJobsToChain; + if (this.numberOfJobsToChain > 0) { + String parentLogTransactionId = this.parentLogTransactionId != null ? this.parentLogTransactionId : Logger.getTransactionId(); + System.enqueueJob(new QueueableLoggerExample(this.numberOfJobsToChain, parentLogTransactionId)); } + } } ``` diff --git a/nebula-logger/core/main/logger-engine/classes/Logger.cls b/nebula-logger/core/main/logger-engine/classes/Logger.cls index 73b278609..370c55893 100644 --- a/nebula-logger/core/main/logger-engine/classes/Logger.cls +++ b/nebula-logger/core/main/logger-engine/classes/Logger.cls @@ -15,7 +15,7 @@ global with sharing class Logger { // There's no reliable way to get the version number dynamically in Apex @TestVisible - private static final String CURRENT_VERSION_NUMBER = 'v4.14.14'; + private static final String CURRENT_VERSION_NUMBER = 'v4.14.16'; private static final System.LoggingLevel FALLBACK_LOGGING_LEVEL = System.LoggingLevel.DEBUG; private static final List LOG_ENTRIES_BUFFER = new List(); private static final String MISSING_SCENARIO_ERROR_MESSAGE = 'No logger scenario specified. A scenario is required for logging in this org.'; diff --git a/nebula-logger/core/main/logger-engine/lwc/logger/logEntryBuilder.js b/nebula-logger/core/main/logger-engine/lwc/logger/logEntryBuilder.js index 563a35425..06f0cd55b 100644 --- a/nebula-logger/core/main/logger-engine/lwc/logger/logEntryBuilder.js +++ b/nebula-logger/core/main/logger-engine/lwc/logger/logEntryBuilder.js @@ -106,8 +106,29 @@ export default class LogEntryEventBuilder { this.#componentLogEntry.error = {}; if (exception.body) { this.#componentLogEntry.error.message = exception.body.message; - this.#componentLogEntry.error.stackTrace = exception.body.stackTrace; this.#componentLogEntry.error.type = exception.body.exceptionType; + + const transformedErrorStackTrace = { + className: undefined, + methodName: undefined, + metadataType: undefined, + triggerName: undefined, + stackTraceString: exception.body.stackTrace + }; + if (exception.body.stackTrace?.indexOf(':') > -1) { + const stackTracePieces = exception.body.stackTrace.split(':')[0].split('.'); + + if (stackTracePieces[0] === 'Class') { + transformedErrorStackTrace.className = stackTracePieces[1]; + transformedErrorStackTrace.methodName = stackTracePieces[stackTracePieces.length - 1]; + transformedErrorStackTrace.metadataType = 'ApexClass'; + } else if (stackTracePieces[0] === 'Trigger') { + transformedErrorStackTrace.triggerName = stackTracePieces[1]; + transformedErrorStackTrace.metadataType = 'ApexTrigger'; + } + } + + this.#componentLogEntry.error.stackTrace = transformedErrorStackTrace; } else { this.#componentLogEntry.error.message = exception.message; this.#componentLogEntry.error.stackTrace = new LoggerStackTrace().parse(exception); @@ -154,9 +175,11 @@ export default class LogEntryEventBuilder { * @return {LogEntryBuilder} The same instance of `LogEntryBuilder`, useful for chaining methods */ addTag(tag) { - this.#componentLogEntry.tags.push(tag); - // Deduplicate the list of tags - this.#componentLogEntry.tags = Array.from(new Set(this.#componentLogEntry.tags)); + if (tag?.trim()) { + this.#componentLogEntry.tags.push(tag?.trim()); + // Deduplicate the list of tags + this.#componentLogEntry.tags = Array.from(new Set(this.#componentLogEntry.tags)); + } return this; } diff --git a/nebula-logger/core/main/logger-engine/lwc/logger/loggerService.js b/nebula-logger/core/main/logger-engine/lwc/logger/loggerService.js index 534cd90e1..61e61f117 100644 --- a/nebula-logger/core/main/logger-engine/lwc/logger/loggerService.js +++ b/nebula-logger/core/main/logger-engine/lwc/logger/loggerService.js @@ -10,7 +10,7 @@ import LoggerServiceTaskQueue from './loggerServiceTaskQueue'; import getSettings from '@salesforce/apex/ComponentLogger.getSettings'; import saveComponentLogEntries from '@salesforce/apex/ComponentLogger.saveComponentLogEntries'; -const CURRENT_VERSION_NUMBER = 'v4.14.14'; +const CURRENT_VERSION_NUMBER = 'v4.14.16'; const CONSOLE_OUTPUT_CONFIG = { messagePrefix: `%c Nebula Logger ${CURRENT_VERSION_NUMBER} `, @@ -195,7 +195,11 @@ export default class LoggerService { this.#componentLogEntries.push(logEntry); if (this.#settings.isConsoleLoggingEnabled) { - this._logToConsole(logEntry.loggingLevel, logEntry.message, logEntry); + // Use setTimeout() so any extra fields included in the log entry are added first before printing to the console + // eslint-disable-next-line @lwc/lwc/no-async-operation + setTimeout(() => { + this._logToConsole(logEntry.loggingLevel, logEntry.message, logEntry); + }, 1000); } if (this.#settings.isLightningLoggerEnabled) { lightningLog(logEntry); @@ -218,22 +222,41 @@ export default class LoggerService { const consoleLoggingFunction = console[loggingLevel.toLowerCase()] ?? console.debug; const loggingLevelEmoji = LOGGING_LEVEL_EMOJIS[loggingLevel]; const qualifiedMessage = `${loggingLevelEmoji} ${loggingLevel}: ${message}`; - const formattedComponentLogEntryString = !componentLogEntry - ? '' - : '\n' + - JSON.stringify( - { - origin: { - component: componentLogEntry.originStackTrace?.componentName, - function: componentLogEntry.originStackTrace?.functionName, - metadataType: componentLogEntry.originStackTrace?.metadataType - }, - scenario: componentLogEntry.scenario, - timestamp: componentLogEntry.timestamp + // Clean up some extra properties for readability + console.debug('>>> original componentLogEntry: ', JSON.stringify(componentLogEntry, null, 2)); + const simplifiedLogEntry = !componentLogEntry + ? undefined + : { + customFieldMappings: componentLogEntry.fieldToValue.length === 0 ? undefined : componentLogEntry.fieldToValue, + originSource: { + metadataType: componentLogEntry.originStackTrace?.metadataType, + componentName: componentLogEntry.originStackTrace?.componentName, + functionName: componentLogEntry.originStackTrace?.functionName }, - (_, value) => value ?? undefined, - 2 - ); + error: componentLogEntry.error, + scenario: componentLogEntry.scenario, + tags: componentLogEntry.tags.length === 0 ? undefined : componentLogEntry.tags, + timestamp: !componentLogEntry.timestamp + ? undefined + : { + local: new Date(componentLogEntry.timestamp).toLocaleString(), + utc: componentLogEntry.timestamp + } + }; + if (simplifiedLogEntry?.error?.stackTrace) { + simplifiedLogEntry.error.errorSource = { + metadataType: simplifiedLogEntry.error.stackTrace.metadataType, + componentName: simplifiedLogEntry.error.stackTrace.componentName, + functionName: simplifiedLogEntry.error.stackTrace.functionName, + className: simplifiedLogEntry.error.stackTrace.className, + methodName: simplifiedLogEntry.error.stackTrace.methodName, + triggerName: simplifiedLogEntry.error.stackTrace.triggerName, + stackTraceString: simplifiedLogEntry.error.stackTrace.stackTraceString + }; + delete simplifiedLogEntry.error.stackTrace; + } + + const formattedComponentLogEntryString = !simplifiedLogEntry ? undefined : '\n' + JSON.stringify(simplifiedLogEntry, (_, value) => value ?? undefined, 2); consoleLoggingFunction(CONSOLE_OUTPUT_CONFIG.messagePrefix, CONSOLE_OUTPUT_CONFIG.messageFormatting, qualifiedMessage, formattedComponentLogEntryString); } diff --git a/nebula-logger/recipes/lwc/loggerLWCCreateLoggerImportDemo/loggerLWCCreateLoggerImportDemo.js b/nebula-logger/recipes/lwc/loggerLWCCreateLoggerImportDemo/loggerLWCCreateLoggerImportDemo.js index 5d9c81516..d3e8a9493 100644 --- a/nebula-logger/recipes/lwc/loggerLWCCreateLoggerImportDemo/loggerLWCCreateLoggerImportDemo.js +++ b/nebula-logger/recipes/lwc/loggerLWCCreateLoggerImportDemo/loggerLWCCreateLoggerImportDemo.js @@ -82,6 +82,7 @@ export default class LoggerLWCCreateLoggerImportDemo extends LightningElement { scenarioChange(event) { this.scenario = event.target.value; + this.logger.setScenario(this.scenario); } tagsStringChange(event) { @@ -156,7 +157,6 @@ export default class LoggerLWCCreateLoggerImportDemo extends LightningElement { saveLogExample() { console.log('running saveLog for btn'); - this.logger.setScenario(this.scenario); console.log(this.logger); // this.logger.saveLog('QUEUEABLE'); this.logger.saveLog(); diff --git a/nebula-logger/recipes/lwc/loggerLWCGetLoggerImportDemo/loggerLWCGetLoggerImportDemo.js b/nebula-logger/recipes/lwc/loggerLWCGetLoggerImportDemo/loggerLWCGetLoggerImportDemo.js index 831a16db8..6ffbd1733 100644 --- a/nebula-logger/recipes/lwc/loggerLWCGetLoggerImportDemo/loggerLWCGetLoggerImportDemo.js +++ b/nebula-logger/recipes/lwc/loggerLWCGetLoggerImportDemo/loggerLWCGetLoggerImportDemo.js @@ -19,6 +19,7 @@ export default class LoggerLWCGetLoggerImportDemo extends LightningElement { connectedCallback() { try { + this.logger.setScenario(this.scenario); this.logger.error('test error entry').setField({ SomeLogEntryField__c: 'some text from loggerLWCGetLoggerImportDemo' }); this.logger.warn('test warn entry').setField({ SomeLogEntryField__c: 'some text from loggerLWCGetLoggerImportDemo' }); this.logger.info('test info entry').setField({ SomeLogEntryField__c: 'some text from loggerLWCGetLoggerImportDemo' }); @@ -83,6 +84,7 @@ export default class LoggerLWCGetLoggerImportDemo extends LightningElement { scenarioChange(event) { this.scenario = event.target.value; + this.logger.setScenario(this.scenario); } tagsStringChange(event) { @@ -157,7 +159,6 @@ export default class LoggerLWCGetLoggerImportDemo extends LightningElement { saveLogExample() { console.log('running saveLog for btn'); - this.logger.setScenario(this.scenario); console.log(this.logger); // this.logger.saveLog('QUEUEABLE'); this.logger.saveLog(); diff --git a/package.json b/package.json index c024195d6..0ab8ba275 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "nebula-logger", - "version": "4.14.14", + "version": "4.14.16", "description": "The most robust logger for Salesforce. Works with Apex, Lightning Components, Flow, Process Builder & Integrations. Designed for Salesforce admins, developers & architects.", "author": "Jonathan Gillespie", "license": "MIT", diff --git a/sfdx-project.json b/sfdx-project.json index c42f5d7a8..8ee36204c 100644 --- a/sfdx-project.json +++ b/sfdx-project.json @@ -9,9 +9,9 @@ "path": "./nebula-logger/core", "definitionFile": "./config/scratch-orgs/base-scratch-def.json", "scopeProfiles": true, - "versionNumber": "4.14.14.NEXT", - "versionName": "New Apex Static Method & JavaScript Function Logger.setField()", - "versionDescription": "Added a new Apex static method Logger.setField() and LWC function logger.setField() so custom fields can be set once --> auto-populated on all subsequent LogEntryEvent__e records", + "versionNumber": "4.14.16.NEXT", + "versionName": "Improved JavaScript Console Output", + "versionDescription": "Added more details to the component log entry JSON that's printed using console statements. The stringified object now includes more details, such as the exception, tags, and scenario.", "releaseNotesUrl": "https://github.com/jongpie/NebulaLogger/releases", "unpackagedMetadata": { "path": "./nebula-logger/extra-tests"