From 576fdf2344a66338f956c1f27536471ce11ac1e1 Mon Sep 17 00:00:00 2001 From: Jonathan Gillespie Date: Tue, 24 Sep 2024 22:13:18 -0400 Subject: [PATCH 1/4] Added a new Apex static method Logger.setField() so custom fields can be set once per transaction --> auto-populated on all LogEntryEvent__e records --- .../Logger-Engine/LogEntryEventBuilder.md | 2 +- docs/apex/Logger-Engine/Logger.md | 21 +++++++ .../classes/LogEntryEventBuilder.cls | 4 +- .../main/logger-engine/classes/Logger.cls | 47 ++++++++++++++- .../logger-engine/lwc/logger/loggerService.js | 2 +- .../logger-engine/classes/Logger_Tests.cls | 60 +++++++++++++++++++ sfdx-project.json | 6 +- 7 files changed, 134 insertions(+), 8 deletions(-) diff --git a/docs/apex/Logger-Engine/LogEntryEventBuilder.md b/docs/apex/Logger-Engine/LogEntryEventBuilder.md index 30c2448f7..da9ee226c 100644 --- a/docs/apex/Logger-Engine/LogEntryEventBuilder.md +++ b/docs/apex/Logger-Engine/LogEntryEventBuilder.md @@ -402,7 +402,7 @@ The same instance of `LogEntryEventBuilder`, useful for chaining methods #### `setField(Schema.SObjectField field, Object fieldValue)` → `LogEntryEventBuilder` -Sets a field values on the builder's `LogEntryEvent__e` record +Sets a field value on the builder's `LogEntryEvent__e` record ##### Parameters diff --git a/docs/apex/Logger-Engine/Logger.md b/docs/apex/Logger-Engine/Logger.md index 92b44730d..0c2785cb6 100644 --- a/docs/apex/Logger-Engine/Logger.md +++ b/docs/apex/Logger-Engine/Logger.md @@ -4948,6 +4948,27 @@ Stores additional details about the current transacation's async context | -------------------- | ------------------------------------------------------ | | `schedulableContext` | - The instance of `System.SchedulableContext` to track | +#### `setField(Schema.SObjectField field, Object fieldValue)` → `void` + +Sets a field value on every generated `LogEntryEvent__e` record + +##### Parameters + +| Param | Description | +| ------------ | -------------------------------------------------------- | +| `field` | The `Schema.SObjectField` token of the field to populate | +| `fieldValue` | The `Object` value to populate in the provided field | + +#### `setField(Map fieldToValue)` → `void` + +Sets multiple field values oon every generated `LogEntryEvent__e` record + +##### Parameters + +| Param | Description | +| -------------- | ---------------------------------------------------------------------- | +| `fieldToValue` | An instance of `Map<Schema.SObjectField, Object>` containing the | + #### `setParentLogTransactionId(String parentTransactionId)` → `void` Relates the current transaction's log to a parent log via the field Log**c.ParentLog**c This is useful for relating multiple asynchronous operations together, such as batch & queueable jobs. diff --git a/nebula-logger/core/main/logger-engine/classes/LogEntryEventBuilder.cls b/nebula-logger/core/main/logger-engine/classes/LogEntryEventBuilder.cls index 6d8938bf2..d93a099c3 100644 --- a/nebula-logger/core/main/logger-engine/classes/LogEntryEventBuilder.cls +++ b/nebula-logger/core/main/logger-engine/classes/LogEntryEventBuilder.cls @@ -629,7 +629,7 @@ global with sharing class LogEntryEventBuilder { } /** - * @description Sets a field values on the builder's `LogEntryEvent__e` record + * @description Sets a field value on the builder's `LogEntryEvent__e` record * @param field The `Schema.SObjectField` token of the field to populate * on the builder's `LogEntryEvent__e` record * @param fieldValue The `Object` value to populate in the provided field @@ -646,7 +646,7 @@ global with sharing class LogEntryEventBuilder { /** * @description Sets multiple field values on the builder's `LogEntryEvent__e` record * @param fieldToValue An instance of `Map` containing the - * the fields & values to populate the builder's `LogEntryEvent__e` record + * the fields & values to populate on the builder's `LogEntryEvent__e` record * @return The same instance of `LogEntryEventBuilder`, useful for chaining methods */ @SuppressWarnings('PMD.AvoidDebugStatements') diff --git a/nebula-logger/core/main/logger-engine/classes/Logger.cls b/nebula-logger/core/main/logger-engine/classes/Logger.cls index a95882434..5a0256b33 100644 --- a/nebula-logger/core/main/logger-engine/classes/Logger.cls +++ b/nebula-logger/core/main/logger-engine/classes/Logger.cls @@ -22,6 +22,7 @@ global with sharing class Logger { 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(); + private static final Map TRANSACTION_FIELD_TO_VALUE = new Map(); private static final String TRANSACTION_ID = System.UUID.randomUUID().toString(); private static AsyncContext currentAsyncContext; @@ -250,6 +251,30 @@ global with sharing class Logger { return parentLogTransactionId; } + /** + * @description Sets a field value on every generated `LogEntryEvent__e` record + * @param field The `Schema.SObjectField` token of the field to populate + * on each `LogEntryEvent__e` record in the current transaction + * @param fieldValue The `Object` value to populate in the provided field + */ + global static void setField(Schema.SObjectField field, Object fieldValue) { + setField(new Map{ field => fieldValue }); + } + + /** + * @description Sets multiple field values oon every generated `LogEntryEvent__e` record + * @param fieldToValue An instance of `Map` containing the + * the fields & values to populate on each `LogEntryEvent__e` record in the current transaction + */ + global static void setField(Map fieldToValue) { + if (getUserSettings().IsEnabled__c == false) { + return; + } + + TRANSACTION_FIELD_TO_VALUE.putAll(fieldToValue); + TRANSACTION_FIELD_TO_VALUE.remove(null); + } + /** * @description Indicates if logging has been enabled for the current user, based on the custom setting LoggerSettings__c * @return Boolean @@ -3390,8 +3415,9 @@ global with sharing class Logger { return logEntryEventBuilder; } - private static void finalizeEntry(LogEntryEvent__e logEntryEvent) { + setTransactionFields(logEntryEvent); + logEntryEvent.ParentLogTransactionId__c = getParentLogTransactionId(); logEntryEvent.TransactionScenario__c = transactionScenario; @@ -3403,6 +3429,25 @@ global with sharing class Logger { } } + @SuppressWarnings('PMD.AvoidDebugStatements') + private static void setTransactionFields(LogEntryEvent__e logEntryEvent) { + for (Schema.SObjectField field : TRANSACTION_FIELD_TO_VALUE.keySet()) { + Object value = TRANSACTION_FIELD_TO_VALUE.get(field); + + try { + Schema.DescribeFieldResult fieldDescribe = field.getDescribe(); + if (fieldDescribe.getSoapType() == Schema.SoapType.STRING) { + value = LoggerDataStore.truncateFieldValue(field, (String) value); + } + + logEntryEvent.put(field, value); + } catch (System.Exception ex) { + LogMessage logMessage = new LogMessage('Could not set field {0} with value {1}', field, value); + System.debug(System.LoggingLevel.WARN, logMessage.getMessage()); + } + } + } + private static Boolean hasValidStartAndEndTimes(LoggerSettings__c settings) { Datetime nowish = System.now(); Boolean isStartTimeValid = settings.StartTime__c == null || settings.StartTime__c <= nowish; 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 5ccfa3f66..e5cbc0961 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.13'; +const CURRENT_VERSION_NUMBER = 'v4.14.14'; const CONSOLE_OUTPUT_CONFIG = { messagePrefix: `%c Nebula Logger ${CURRENT_VERSION_NUMBER} `, 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 5ea0c6a26..2bd16ad8b 100644 --- a/nebula-logger/core/tests/logger-engine/classes/Logger_Tests.cls +++ b/nebula-logger/core/tests/logger-engine/classes/Logger_Tests.cls @@ -1019,6 +1019,66 @@ private class Logger_Tests { System.Assert.isNull(Logger.getParentLogTransactionId()); } + // Start setField() test methods + @IsTest + static void it_should_set_single_transaction_field_on_all_entries_when_events_published() { + LoggerDataStore.setMock(LoggerMockDataStore.getEventBus()); + System.Assert.areEqual(0, LoggerMockDataStore.getEventBus().getPublishedPlatformEvents().size()); + // HttpRequestBody__c is used as an example here since it's provided out of the box, and doesn't + // have a default value set. + // But realisticially, this functionality is intended to be used with custom fields that are added to + // Nebula Logger's data model. + Schema.SObjectField field = Schema.LogEntryEvent__e.HttpRequestBody__c; + String fieldValue = 'Some_value'; + Integer countOfEntriesToAdd = 3; + + for (Integer i = 0; i < countOfEntriesToAdd; i++) { + LogEntryEventBuilder builder = Logger.info('hello, world'); + System.Assert.isNull(builder.getLogEntryEvent().get(field), 'Field ' + field + ' should be null until the event is published'); + // Call setField() after some entries have been added (and before other entries are added) + // to ensure that all entries have the fields populated on publish + if (i == 1) { + Logger.setField(field, fieldValue); + } + } + Logger.saveLog(); + + System.Assert.areEqual(countOfEntriesToAdd, LoggerMockDataStore.getEventBus().getPublishedPlatformEvents().size()); + for (LogEntryEvent__e publishedLogEntryEvent : (List) LoggerMockDataStore.getEventBus().getPublishedPlatformEvents()) { + System.Assert.areEqual(fieldValue, publishedLogEntryEvent.get(field)); + } + } + + @IsTest + static void it_should_set_map_of_transaction_fields_on_all_entries_when_events_published() { + LoggerDataStore.setMock(LoggerMockDataStore.getEventBus()); + System.Assert.areEqual(0, LoggerMockDataStore.getEventBus().getPublishedPlatformEvents().size()); + // HttpRequestBody__c is used as an example here since it's provided out of the box, and doesn't + // have a default value set. + // But realisticially, this functionality is intended to be used with custom fields that are added to + // Nebula Logger's data model. + Schema.SObjectField field = Schema.LogEntryEvent__e.HttpRequestBody__c; + String fieldValue = 'Some_value'; + Integer countOfEntriesToAdd = 3; + + for (Integer i = 0; i < countOfEntriesToAdd; i++) { + LogEntryEventBuilder builder = Logger.info('hello, world'); + System.Assert.isNull(builder.getLogEntryEvent().get(field), 'Field ' + field + ' should be null until the event is published'); + // Call setField() after some entries have been added (and before other entries are added) + // to ensure that all entries have the fields populated on publish + if (i == 1) { + Logger.setField(new Map{ field => fieldValue }); + } + } + Logger.saveLog(); + + System.Assert.areEqual(countOfEntriesToAdd, LoggerMockDataStore.getEventBus().getPublishedPlatformEvents().size()); + for (LogEntryEvent__e publishedLogEntryEvent : (List) LoggerMockDataStore.getEventBus().getPublishedPlatformEvents()) { + System.Assert.areEqual(fieldValue, publishedLogEntryEvent.get(field)); + } + } + // End setField() test methods + @IsTest static void it_should_return_quiddity_level() { List acceptableDefaultQuidditiesForTests = new List{ diff --git a/sfdx-project.json b/sfdx-project.json index d2f8fa772..bda065d4f 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.13.NEXT", - "versionName": "New getLogger() JS Function", - "versionDescription": "Added a new function getLogger() in c/logger that can be called synchronously (createLogger() was async). This simplifies how developers use it, and avoids some lingering JS stack trace issues that occur in async functions.", + "versionNumber": "4.14.14.NEXT", + "versionName": "New Apex Static Method Logger.setField()", + "versionDescription": "Added a new Apex static method Logger.setField() so custom fields can be set once per transaction --> auto-populated on all LogEntryEvent__e records", "releaseNotesUrl": "https://github.com/jongpie/NebulaLogger/releases", "unpackagedMetadata": { "path": "./nebula-logger/extra-tests" From 9d437dda3720f82f386d0c40a82eea6302a8ab35 Mon Sep 17 00:00:00 2001 From: Jonathan Gillespie Date: Sat, 28 Sep 2024 18:29:38 -0400 Subject: [PATCH 2/4] Added JavaScript support for setting fields once per component instance, using logger.setField() --- README.md | 55 ++++++++++++++----- jest.config.js | 2 +- .../main/logger-engine/classes/Logger.cls | 2 +- .../lwc/logger/__tests__/logger.test.js | 55 ++++++++++++++++--- .../main/logger-engine/lwc/logger/logger.js | 9 +++ .../logger-engine/lwc/logger/loggerService.js | 13 +++++ .../loggerLWCCreateLoggerImportDemo.test.js | 12 +++- .../loggerLWCCreateLoggerImportDemo.js | 7 ++- .../loggerLWCGetLoggerImportDemo.test.js | 11 +++- .../loggerLWCGetLoggerImportDemo.js | 6 +- package.json | 2 +- sfdx-project.json | 4 +- 12 files changed, 140 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index 6b5701092..c6eb4335c 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.13 +## Unlocked Package - v4.14.14 [![Install Unlocked Package in a Sandbox](./images/btn-install-unlocked-package-sandbox.png)](https://test.salesforce.com/packaging/installPackage.apexp?p0=04t5Y0000015oW3QAI) [![Install Unlocked Package in Production](./images/btn-install-unlocked-package-production.png)](https://login.salesforce.com/packaging/installPackage.apexp?p0=04t5Y0000015oW3QAI) @@ -657,28 +657,55 @@ The first step is to add a field to the platform event `LogEntryEvent__e` ![Custom Field on LogEntryEvent__e](./images/custom-field-log-entry-event.png) -- In Apex, populate your field(s) by calling the instance method overloads `LogEntryEventBuilder.setField(Schema.SObjectField field, Object fieldValue)` or `LogEntryEventBuilder.setField(Map fieldToValue)` +- In Apex, you have 2 ways to populate your custom fields - ```apex Logger.info('hello, world') - // Set a single field - .setField(LogEntryEvent__e.SomeCustomTextField__c, 'some text value') - // Set multiple fields - .setField(new Map{ - LogEntryEvent__e.AnotherCustomTextField__c => 'another text value', - LogEntryEvent__e.SomeCustomDatetimeField__c => System.now() - }); + 1. Set the field once per transaction - every `LogEntryEvent__e` logged in the transaction will then automatically have the specified field populated with the same value. + - This is typically used for fields that are mapped to an equivalent `Log__c` or `LoggerScenario__c` field. + + - How: call the static method overloads `Logger.setField(Schema.SObjectField field, Object fieldValue)` or `Logger.setField(Map fieldToValue)` + + 2. Set the field on a specific `LogEntryEvent__e` record - other records will not have the field automatically set. + - This is typically used for fields that are mapped to an equivalent `LogEntry__c` field. + - How: call the instance method overloads `LogEntryEventBuilder.setField(Schema.SObjectField field, Object fieldValue)` or `LogEntryEventBuilder.setField(Map fieldToValue)` + + ```apex + // Set My_Field__c on every log entry event created in this transaction with the same value + Logger.setField(LogEntryEvent__e.My_Field__c, 'some value that applies to the whole Apex transaction'); + + // Set fields on specific entries + Logger.warn('hello, world - "a value" set for Some_Other_Field__c').setField(LogEntryEvent__e.Some_Other_Field__c, 'a value') + Logger.warn('hello, world - "different value" set for Some_Other_Field__c').setField(LogEntryEvent__e.Some_Other_Field__c, 'different value') + Logger.info('hello, world - no value set for Some_Other_Field__c'); + + Logger.saveLog(); ``` -- In JavaScript, populate your field(s) by calling the instance function `LogEntryEventBuilder.setField(Object fieldToValue)` +- In JavaScript, you have 2 ways to populate your custom fields. These are very similar to the 2 ways available in Apex (above). + + 1. Set the field once per component - every `LogEntryEvent__e` logged in your component will then automatically have the specified field populated with the same value. + - This is typically used for fields that are mapped to an equivalent `Log__c` or `LoggerScenario__c` field. + + - How: call the `logger` LWC function `logger.setField(Object fieldToValue)` + + 2. Set the field on a specific `LogEntryEvent__e` record - other records will not have the field automatically set. + - This is typically used for fields that are mapped to an equivalent `LogEntry__c` field. + - How: call the instance function `LogEntryEventBuilder.setField(Object fieldToValue)` ```javascript import { getLogger } from 'c/logger'; - export default class loggerLWCGetLoggerImportDemo extends LightningElement { + export default class LoggerLWCImportDemo extends LightningElement { logger = getLogger(); - async connectedCallback() { - this.logger.info('Hello, world').setField({ SomeCustomTextField__c: 'some text value', SomeCustomNumbertimeField__c: 123 }); + connectedCallback() { + // Set My_Field__c on every log entry event created in this component with the same value + this.logger.setField(LogEntryEvent__e.My_Field__c, 'some value that applies to any subsequent entry'); + + // Set fields on specific entries + this.logger.warn('hello, world - "a value" set for Some_Other_Field__c').setField({ Some_Other_Field__c: 'a value' }); + this.logger.warn('hello, world - "different value" set for Some_Other_Field__c').setField({ Some_Other_Field__c: 'different value' }); + this.logger.info('hello, world - no value set for Some_Other_Field__c'); + this.logger.saveLog(); } } diff --git a/jest.config.js b/jest.config.js index b337f2c15..1bcc6b282 100644 --- a/jest.config.js +++ b/jest.config.js @@ -6,6 +6,6 @@ module.exports = { '^lightning/empApi$': '/config/jest/mocks/lightning/empApi', '^lightning/navigation$': '/config/jest/mocks/lightning/navigation' }, - modulePathIgnorePatterns: ['recipes'], + // modulePathIgnorePatterns: ['recipes'], testPathIgnorePatterns: ['/temp/'] }; diff --git a/nebula-logger/core/main/logger-engine/classes/Logger.cls b/nebula-logger/core/main/logger-engine/classes/Logger.cls index 5a0256b33..73b278609 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.13'; + private static final String CURRENT_VERSION_NUMBER = 'v4.14.14'; 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/__tests__/logger.test.js b/nebula-logger/core/main/logger-engine/lwc/logger/__tests__/logger.test.js index e05f5b1b5..a026e2d6c 100644 --- a/nebula-logger/core/main/logger-engine/lwc/logger/__tests__/logger.test.js +++ b/nebula-logger/core/main/logger-engine/lwc/logger/__tests__/logger.test.js @@ -543,7 +543,6 @@ describe('logger lwc recommended sync getLogger() import approach tests', () => // getLogger() is built to be sync, but internally, some async tasks must execute // before some sync tasks are executed await flushPromises('Resolve async task queue'); - await logger.getUserSettings(); const logEntry = logger.info('example log entry').getComponentLogEntry(); @@ -555,13 +554,36 @@ describe('logger lwc recommended sync getLogger() import approach tests', () => expect(logEntry.browser.windowResolution).toEqual(window.innerWidth + ' x ' + window.innerHeight); }); - it('sets multiple custom fields', async () => { + it('sets multiple custom component fields on subsequent entries', async () => { + getSettings.mockResolvedValue({ ...MOCK_GET_SETTINGS }); + const logger = getLogger(); + // getLogger() is built to be sync, but internally, some async tasks must execute + // before some sync tasks are executed + await flushPromises('Resolve async task queue'); + const firstFakeFieldName = 'SomeField__c'; + const firstFieldMockValue = 'something'; + const secondFakeFieldName = 'AnotherField__c'; + const secondFieldMockValue = 'another value'; + + const previousLogEntry = logger.info('example log entry from before setField() is called').getComponentLogEntry(); + logger.setField({ + [firstFakeFieldName]: firstFieldMockValue, + [secondFakeFieldName]: secondFieldMockValue + }); + const subsequentLogEntry = logger.info('example log entry from after setField() is called').getComponentLogEntry(); + + expect(previousLogEntry.fieldToValue[firstFakeFieldName]).toBeUndefined(); + expect(previousLogEntry.fieldToValue[secondFakeFieldName]).toBeUndefined(); + expect(subsequentLogEntry.fieldToValue[firstFakeFieldName]).toEqual(firstFieldMockValue); + expect(subsequentLogEntry.fieldToValue[secondFakeFieldName]).toEqual(secondFieldMockValue); + }); + + it('sets multiple custom entry fields on a single entry', async () => { getSettings.mockResolvedValue({ ...MOCK_GET_SETTINGS }); const logger = getLogger(); // getLogger() is built to be sync, but internally, some async tasks must execute // before some sync tasks are executed await flushPromises('Resolve async task queue'); - await logger.getUserSettings(); const logEntryBuilder = logger.info('example log entry'); const logEntry = logEntryBuilder.getComponentLogEntry(); const firstFakeFieldName = 'SomeField__c'; @@ -586,7 +608,6 @@ describe('logger lwc recommended sync getLogger() import approach tests', () => // getLogger() is built to be sync, but internally, some async tasks must execute // before some sync tasks are executed await flushPromises('Resolve async task queue'); - await logger.getUserSettings(); const logEntryBuilder = logger.info('example log entry'); const logEntry = logEntryBuilder.getComponentLogEntry(); expect(logEntry.recordId).toBeFalsy(); @@ -603,7 +624,6 @@ describe('logger lwc recommended sync getLogger() import approach tests', () => // getLogger() is built to be sync, but internally, some async tasks must execute // before some sync tasks are executed await flushPromises('Resolve async task queue'); - await logger.getUserSettings(); const logEntryBuilder = logger.info('example log entry'); const logEntry = logEntryBuilder.getComponentLogEntry(); expect(logEntry.record).toBeFalsy(); @@ -620,7 +640,6 @@ describe('logger lwc recommended sync getLogger() import approach tests', () => // getLogger() is built to be sync, but internally, some async tasks must execute // before some sync tasks are executed await flushPromises('Resolve async task queue'); - await logger.getUserSettings(); const logEntryBuilder = logger.info('example log entry'); const logEntry = logEntryBuilder.getComponentLogEntry(); expect(logEntry.error).toBeFalsy(); @@ -1147,7 +1166,29 @@ describe('logger lwc deprecated async createLogger() import tests', () => { expect(logEntry.browser.windowResolution).toEqual(window.innerWidth + ' x ' + window.innerHeight); }); - it('sets multiple custom fields when using deprecated async createLogger() import approach', async () => { + it('sets multiple custom component fields on subsequent entries when using deprecated async createLogger() import approach', async () => { + getSettings.mockResolvedValue({ ...MOCK_GET_SETTINGS }); + const logger = await createLogger(); + await logger.getUserSettings(); + const firstFakeFieldName = 'SomeField__c'; + const firstFieldMockValue = 'something'; + const secondFakeFieldName = 'AnotherField__c'; + const secondFieldMockValue = 'another value'; + + const previousLogEntry = logger.info('example log entry from before setField() is called').getComponentLogEntry(); + logger.setField({ + [firstFakeFieldName]: firstFieldMockValue, + [secondFakeFieldName]: secondFieldMockValue + }); + const subsequentLogEntry = logger.info('example log entry from after setField() is called').getComponentLogEntry(); + + expect(previousLogEntry.fieldToValue[firstFakeFieldName]).toBeUndefined(); + expect(previousLogEntry.fieldToValue[secondFakeFieldName]).toBeUndefined(); + expect(subsequentLogEntry.fieldToValue[firstFakeFieldName]).toEqual(firstFieldMockValue); + expect(subsequentLogEntry.fieldToValue[secondFakeFieldName]).toEqual(secondFieldMockValue); + }); + + it('sets multiple custom entry fields on a single entry when using deprecated async createLogger() import approach', async () => { getSettings.mockResolvedValue({ ...MOCK_GET_SETTINGS }); const logger = await createLogger(); await logger.getUserSettings(); diff --git a/nebula-logger/core/main/logger-engine/lwc/logger/logger.js b/nebula-logger/core/main/logger-engine/lwc/logger/logger.js index fcabcaabd..21f614dac 100644 --- a/nebula-logger/core/main/logger-engine/lwc/logger/logger.js +++ b/nebula-logger/core/main/logger-engine/lwc/logger/logger.js @@ -22,6 +22,15 @@ export default class Logger extends LightningElement { return this.#loggerService.getUserSettings(); } + /** + * @description Sets multiple field values on the builder's `LogEntryEvent__e` record + * @param {Object} fieldToValue An object containing the custom field name as a key, with the corresponding value to store. + * Example: `{"SomeField__c": "some value", "AnotherField__c": "another value"}` + */ + setField(fieldToValue) { + this.#loggerService.setField(fieldToValue); + } + /** * @description Sets the scenario name for the current transaction - this is stored in `LogEntryEvent__e.Scenario__c` * and `Log__c.Scenario__c`, and can be used to filter & group logs 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 e5cbc0961..534cd90e1 100644 --- a/nebula-logger/core/main/logger-engine/lwc/logger/loggerService.js +++ b/nebula-logger/core/main/logger-engine/lwc/logger/loggerService.js @@ -49,6 +49,7 @@ export class BrowserContext { export default class LoggerService { static hasInitialized = false; + #componentFieldToValue = {}; #componentLogEntries = []; #settings; #scenario; @@ -69,6 +70,17 @@ export default class LoggerService { return this.#settings; } + /** + * @description Sets multiple field values on every generated `LogEntryEvent__e` record + * @param {Object} fieldToValue An object containing the custom field name as a key, with the corresponding value to store. + * Example: `{"SomeField__c": "some value", "AnotherField__c": "another value"}` + */ + setField(fieldToValue) { + if (!!fieldToValue && typeof fieldToValue === 'object' && !Array.isArray(fieldToValue)) { + Object.assign(this.#componentFieldToValue, fieldToValue); + } + } + setScenario(scenario) { this.#scenario = scenario; } @@ -176,6 +188,7 @@ export default class LoggerService { .setMessage(message) .setScenario(this.#scenario); const logEntry = logEntryBuilder.getComponentLogEntry(); + Object.assign(logEntry.fieldToValue, this.#componentFieldToValue); const loggingLevelCheckTask = providedLoggingLevel => { if (this._meetsUserLoggingLevel(providedLoggingLevel)) { diff --git a/nebula-logger/recipes/lwc/loggerLWCCreateLoggerImportDemo/__tests__/loggerLWCCreateLoggerImportDemo.test.js b/nebula-logger/recipes/lwc/loggerLWCCreateLoggerImportDemo/__tests__/loggerLWCCreateLoggerImportDemo.test.js index 59fab0024..35e8f42a3 100644 --- a/nebula-logger/recipes/lwc/loggerLWCCreateLoggerImportDemo/__tests__/loggerLWCCreateLoggerImportDemo.test.js +++ b/nebula-logger/recipes/lwc/loggerLWCCreateLoggerImportDemo/__tests__/loggerLWCCreateLoggerImportDemo.test.js @@ -1,4 +1,5 @@ import { createElement } from 'lwc'; +import { getLogger } from 'c/logger'; import loggerLWCCreateLoggerImportDemo from 'c/loggerLWCCreateLoggerImportDemo'; import getSettings from '@salesforce/apex/ComponentLogger.getSettings'; @@ -15,6 +16,10 @@ const MOCK_GET_SETTINGS = { userLoggingLevel: { ordinal: 2, name: 'FINEST' } }; +jest.mock('lightning/logger', () => ({ log: jest.fn() }), { + virtual: true +}); + jest.mock( '@salesforce/apex/ComponentLogger.getSettings', () => { @@ -36,10 +41,11 @@ describe('logger demo tests', () => { it('mounts and saves log correctly in one go', async () => { getSettings.mockResolvedValue({ ...MOCK_GET_SETTINGS }); const demo = createElement('c-logger-demo', { is: loggerLWCCreateLoggerImportDemo }); - document.body.appendChild(demo); - await flushPromises(); + document.body.appendChild(demo); + await flushPromises('Resolve async tasks from mounting component'); - expect(demo.logger?.getBufferSize()).toBe(0); + expect(demo.logger).toBeDefined(); + expect(demo.logger.getBufferSize()).toBe(0); }); }); diff --git a/nebula-logger/recipes/lwc/loggerLWCCreateLoggerImportDemo/loggerLWCCreateLoggerImportDemo.js b/nebula-logger/recipes/lwc/loggerLWCCreateLoggerImportDemo/loggerLWCCreateLoggerImportDemo.js index 201cd9d3e..5d9c81516 100644 --- a/nebula-logger/recipes/lwc/loggerLWCCreateLoggerImportDemo/loggerLWCCreateLoggerImportDemo.js +++ b/nebula-logger/recipes/lwc/loggerLWCCreateLoggerImportDemo/loggerLWCCreateLoggerImportDemo.js @@ -4,20 +4,21 @@ //------------------------------------------------------------------------------------------------// /* eslint-disable no-console */ -import { LightningElement, wire } from 'lwc'; +import { LightningElement, api, wire } from 'lwc'; import returnSomeString from '@salesforce/apex/LoggerLWCDemoController.returnSomeString'; import throwSomeError from '@salesforce/apex/LoggerLWCDemoController.throwSomeError'; import { createLogger } from 'c/logger'; export default class LoggerLWCCreateLoggerImportDemo extends LightningElement { - someBoolean = false; + @api logger; message = 'Hello, world!'; scenario = 'Some demo scenario'; + someBoolean = false; tagsString = 'Tag-one, Another tag here'; - logger; async connectedCallback() { + /* eslint-disable-next-line @lwc/lwc/no-api-reassignments */ this.logger = await createLogger(); console.log('>>> start of connectedCallback()'); try { diff --git a/nebula-logger/recipes/lwc/loggerLWCGetLoggerImportDemo/__tests__/loggerLWCGetLoggerImportDemo.test.js b/nebula-logger/recipes/lwc/loggerLWCGetLoggerImportDemo/__tests__/loggerLWCGetLoggerImportDemo.test.js index 1497c856e..83ddec160 100644 --- a/nebula-logger/recipes/lwc/loggerLWCGetLoggerImportDemo/__tests__/loggerLWCGetLoggerImportDemo.test.js +++ b/nebula-logger/recipes/lwc/loggerLWCGetLoggerImportDemo/__tests__/loggerLWCGetLoggerImportDemo.test.js @@ -15,6 +15,10 @@ const MOCK_GET_SETTINGS = { userLoggingLevel: { ordinal: 2, name: 'FINEST' } }; +jest.mock('lightning/logger', () => ({ log: jest.fn() }), { + virtual: true +}); + jest.mock( '@salesforce/apex/ComponentLogger.getSettings', () => { @@ -36,10 +40,11 @@ describe('logger demo tests', () => { it('mounts and saves log correctly in one go', async () => { getSettings.mockResolvedValue({ ...MOCK_GET_SETTINGS }); const demo = createElement('c-logger-demo', { is: loggerLWCGetLoggerImportDemo }); - document.body.appendChild(demo); - await flushPromises(); + document.body.appendChild(demo); + await flushPromises('Resolve async tasks from mounting component'); - expect(demo.logger?.getBufferSize()).toBe(0); + expect(demo.logger).toBeDefined(); + expect(demo.logger.getBufferSize()).toBe(0); }); }); diff --git a/nebula-logger/recipes/lwc/loggerLWCGetLoggerImportDemo/loggerLWCGetLoggerImportDemo.js b/nebula-logger/recipes/lwc/loggerLWCGetLoggerImportDemo/loggerLWCGetLoggerImportDemo.js index 9194e9a0e..831a16db8 100644 --- a/nebula-logger/recipes/lwc/loggerLWCGetLoggerImportDemo/loggerLWCGetLoggerImportDemo.js +++ b/nebula-logger/recipes/lwc/loggerLWCGetLoggerImportDemo/loggerLWCGetLoggerImportDemo.js @@ -4,18 +4,18 @@ //------------------------------------------------------------------------------------------------// /* eslint-disable no-console */ -import { LightningElement, wire } from 'lwc'; +import { LightningElement, api, wire } from 'lwc'; import returnSomeString from '@salesforce/apex/LoggerLWCDemoController.returnSomeString'; import throwSomeError from '@salesforce/apex/LoggerLWCDemoController.throwSomeError'; import { getLogger } from 'c/logger'; export default class LoggerLWCGetLoggerImportDemo extends LightningElement { - someBoolean = false; + @api logger = getLogger(); message = 'Hello, world!'; scenario = 'Some demo scenario'; + someBoolean = false; tagsString = 'Tag-one, Another tag here'; - logger = getLogger(); connectedCallback() { try { diff --git a/package.json b/package.json index d803f002e..c024195d6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "nebula-logger", - "version": "4.14.13", + "version": "4.14.14", "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 bda065d4f..7adb097d9 100644 --- a/sfdx-project.json +++ b/sfdx-project.json @@ -10,8 +10,8 @@ "definitionFile": "./config/scratch-orgs/base-scratch-def.json", "scopeProfiles": true, "versionNumber": "4.14.14.NEXT", - "versionName": "New Apex Static Method Logger.setField()", - "versionDescription": "Added a new Apex static method Logger.setField() so custom fields can be set once per transaction --> auto-populated on all LogEntryEvent__e records", + "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", "releaseNotesUrl": "https://github.com/jongpie/NebulaLogger/releases", "unpackagedMetadata": { "path": "./nebula-logger/extra-tests" From 9c0fc2ea2aa840a6d9e71698c10908c72ad8746e Mon Sep 17 00:00:00 2001 From: GitHub Action Bot Date: Fri, 11 Oct 2024 23:10:54 +0000 Subject: [PATCH 3/4] Created new core unlocked package version --- README.md | 8 ++++---- sfdx-project.json | 1 + 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index c6eb4335c..76818e9eb 100644 --- a/README.md +++ b/README.md @@ -7,13 +7,13 @@ The most robust observability solution for Salesforce experts. Built 100% native ## Unlocked Package - v4.14.14 -[![Install Unlocked Package in a Sandbox](./images/btn-install-unlocked-package-sandbox.png)](https://test.salesforce.com/packaging/installPackage.apexp?p0=04t5Y0000015oW3QAI) -[![Install Unlocked Package in Production](./images/btn-install-unlocked-package-production.png)](https://login.salesforce.com/packaging/installPackage.apexp?p0=04t5Y0000015oW3QAI) +[![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) [![View Documentation](./images/btn-view-documentation.png)](https://github.com/jongpie/NebulaLogger/wiki) -`sf package install --wait 20 --security-type AdminsOnly --package 04t5Y0000015oW3QAI` +`sf package install --wait 20 --security-type AdminsOnly --package 04t5Y0000015oWIQAY` -`sfdx force:package:install --wait 20 --securitytype AdminsOnly --package 04t5Y0000015oW3QAI` +`sfdx force:package:install --wait 20 --securitytype AdminsOnly --package 04t5Y0000015oWIQAY` --- diff --git a/sfdx-project.json b/sfdx-project.json index 7adb097d9..c42f5d7a8 100644 --- a/sfdx-project.json +++ b/sfdx-project.json @@ -198,6 +198,7 @@ "Nebula Logger - Core@4.14.11-updated-behavior-of-logger.setasynccontext()": "04t5Y0000015oUgQAI", "Nebula Logger - Core@4.14.12-replaced-httprequestendpoint__c-with-httprequestendpointaddress__c": "04t5Y0000015oV0QAI", "Nebula Logger - Core@4.14.13-new-getlogger()-js-function": "04t5Y0000015oW3QAI", + "Nebula Logger - Core@4.14.14-new-apex-static-method-&-javascript-function-logger.setfield()": "04t5Y0000015oWIQAY", "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", From 34de48964b89f4f50ae871de9532bfc921a7c72f Mon Sep 17 00:00:00 2001 From: Jonathan Gillespie Date: Mon, 14 Oct 2024 22:28:18 -0400 Subject: [PATCH 4/4] [skip ci] Updated some content in README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 76818e9eb..9bcd2dac7 100644 --- a/README.md +++ b/README.md @@ -694,12 +694,12 @@ The first step is to add a field to the platform event `LogEntryEvent__e` ```javascript import { getLogger } from 'c/logger'; - export default class LoggerLWCImportDemo extends LightningElement { + export default class LoggerDemo extends LightningElement { logger = getLogger(); connectedCallback() { // Set My_Field__c on every log entry event created in this component with the same value - this.logger.setField(LogEntryEvent__e.My_Field__c, 'some value that applies to any subsequent entry'); + this.logger.setField({My_Field__c, 'some value that applies to any subsequent entry'}); // Set fields on specific entries this.logger.warn('hello, world - "a value" set for Some_Other_Field__c').setField({ Some_Other_Field__c: 'a value' });