From 5d0a72bdc4ac7d461269cbfa10703c721f0edfef Mon Sep 17 00:00:00 2001 From: Jonathan Gillespie Date: Sun, 27 Oct 2024 14:25:36 -0400 Subject: [PATCH] Added more details in logger LWC to the component log entry JSON that's printed using console statements The stringified object now includes more details, such as the exception and tags Calling the console function now happens with a delay (using setTimeout()) so that additional details can be added to the log entry (using the builder methods) before the log entry is stringified & printed out --- README.md | 108 +++++++++--------- .../main/logger-engine/classes/Logger.cls | 2 +- .../lwc/logger/logEntryBuilder.js | 31 ++++- .../logger-engine/lwc/logger/loggerService.js | 57 ++++++--- .../loggerLWCCreateLoggerImportDemo.js | 2 +- .../loggerLWCGetLoggerImportDemo.js | 3 +- package.json | 2 +- sfdx-project.json | 6 +- 8 files changed, 129 insertions(+), 82 deletions(-) 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"