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..2e800e3fe 100644 --- a/nebula-logger/recipes/lwc/loggerLWCCreateLoggerImportDemo/loggerLWCCreateLoggerImportDemo.js +++ b/nebula-logger/recipes/lwc/loggerLWCCreateLoggerImportDemo/loggerLWCCreateLoggerImportDemo.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 { 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() { this.logger = await createLogger(); 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"