Skip to content

Commit

Permalink
Custom Field Mappings Support for Lightning Components (#749)
Browse files Browse the repository at this point in the history
* Added support for custom field mappings in JavaScript via a new function setField() in logEntryBuilder.js
  * This is equivalent to the Apex method overloads setField() in LogEntryEventBuilder.cls that were introduced in v4.13.14

* Updated the demo lighting components in recipes metadata to set a custom field on LogEntry__c
  * This provides a quick & easy way to verify that the functionality works for lightning components

* Updated README.md to have details about using custom field mappings in JavaScript (lightning components)
  • Loading branch information
jongpie authored Aug 28, 2024
1 parent 4fe4c36 commit 2894401
Show file tree
Hide file tree
Showing 15 changed files with 266 additions and 63 deletions.
37 changes: 28 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,15 @@

The most robust observability solution for Salesforce experts. Built 100% natively on the platform, and designed to work seamlessly with Apex, Lightning Components, Flow, Process Builder & integrations.

## Unlocked Package - v4.14.5
## Unlocked Package - v4.14.6

[![Install Unlocked Package in a Sandbox](./images/btn-install-unlocked-package-sandbox.png)](https://test.salesforce.com/packaging/installPackage.apexp?p0=04t5Y0000015oRXQAY)
[![Install Unlocked Package in Production](./images/btn-install-unlocked-package-production.png)](https://login.salesforce.com/packaging/installPackage.apexp?p0=04t5Y0000015oRXQAY)
[![Install Unlocked Package in a Sandbox](./images/btn-install-unlocked-package-sandbox.png)](https://test.salesforce.com/packaging/installPackage.apexp?p0=04t5Y0000015oRhQAI)
[![Install Unlocked Package in Production](./images/btn-install-unlocked-package-production.png)](https://login.salesforce.com/packaging/installPackage.apexp?p0=04t5Y0000015oRhQAI)
[![View Documentation](./images/btn-view-documentation.png)](https://jongpie.github.io/NebulaLogger/)

`sf package install --wait 20 --security-type AdminsOnly --package 04t5Y0000015oRXQAY`
`sf package install --wait 20 --security-type AdminsOnly --package 04t5Y0000015oRhQAI`

`sfdx force:package:install --wait 20 --securitytype AdminsOnly --package 04t5Y0000015oRXQAY`
`sfdx force:package:install --wait 20 --securitytype AdminsOnly --package 04t5Y0000015oRhQAI`

---

Expand Down Expand Up @@ -590,9 +590,12 @@ Once you've implementing log entry tagging within Apex or Flow, you can choose h
## Adding Custom Fields to Nebula Logger's Data Model

As of `v4.13.14`, Nebula Logger supports defining, setting, and mapping custom fields within Nebula Logger's data model. This is helpful in orgs that want to extend Nebula Logger's included data model by creating their own org/project-specific fields.
Nebula Logger supports defining, setting, and mapping custom fields within Nebula Logger's data model. This is helpful in orgs that want to extend Nebula Logger's included data model by creating their own org/project-specific fields.

This feature requires that you populate your custom fields yourself, and is only available in Apex currently. The plan is to add in a future release the ability to also set custom fields via JavaScript & Flow.
This feature requires that you populate your custom fields yourself, and is only available in Apex & JavaScript currently. The plan is to add in a future release the ability to also set custom fields via Flow.

- `v4.13.14` added this functionality for Apex
- `v4.14.6` added this functionality for JavaScript (lightning components)

### Adding Custom Fields to the Platform Event `LogEntryEvent__e`

Expand All @@ -604,19 +607,35 @@ 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)

- Populate your field(s) in Apex by calling the instance method overloads `LogEntryEventBuilder.setField(Schema.SObjectField field, Object fieldValue)` or `LogEntryEventBuilder.setField(Map<Schema.SObjectField, Object> fieldToValue)`
- In Apex, populate your field(s) by calling the instance method overloads `LogEntryEventBuilder.setField(Schema.SObjectField field, Object fieldValue)` or `LogEntryEventBuilder.setField(Map<Schema.SObjectField, Object> fieldToValue)`

```apex
Logger.info('hello, world')
// Set a single field
.setField(LogEntryEvent__e.SomeCustomTextField__c, 'some text value')
// Set multiple fields
.setFields(new Map<Schema.SObjectField, Object>{
.setField(new Map<Schema.SObjectField, Object>{
LogEntryEvent__e.AnotherCustomTextField__c => 'another text value',
LogEntryEvent__e.SomeCustomDatetimeField__c => System.now()
});
```

- In JavaScript, populate your field(s) by calling the instance function `LogEntryEventBuilder.setField(Object fieldToValue)`

```javascript
import { createLogger } from 'c/logger';

export default class LoggerLWCImportDemo extends LightningElement {
logger;

async connectedCallback() {
this.logger = await createLogger();
this.logger.info('Hello, world').setField({ SomeCustomTextField__c: 'some text value', SomeCustomNumbertimeField__c: 123 });
this.logger.saveLog();
}
}
```

### Adding Custom Fields to the Custom Objects `Log__c`, `LogEntry__c`, and `LoggerScenario__c`

If you want to store the data in one of Nebula Logger's custom objects, you can follow the above steps, and also...
Expand Down
4 changes: 4 additions & 0 deletions docs/apex/Logger-Engine/ComponentLogger.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,10 @@ Context about the user&apos;s browser, automatically captured by Nebula Logger

(Optional) A JavaScript Error to log

###### `fieldToValue``Map<String, Object>`

(Optional) A map containing key-value pairs of fields to set on `LogEntryEvent__e`

###### `loggingLevel``String`

The name of the `LoggingLevel` enum value
Expand Down
14 changes: 14 additions & 0 deletions docs/lightning-components/LogEntryBuilder.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
- [.setRecord(record)](#LogEntryBuilder+setRecord) [<code>LogEntryBuilder</code>](#LogEntryBuilder)
- [.setScenario(scenario)](#LogEntryBuilder+setScenario) [<code>LogEntryBuilder</code>](#LogEntryBuilder)
- [.setError(error)](#LogEntryBuilder+setError) [<code>LogEntryBuilder</code>](#LogEntryBuilder)
- [.setField(fieldToValue)](#LogEntryBuilder+setField) [<code>LogEntryBuilder</code>](#LogEntryBuilder)
- [.parseStackTrace(error)](#LogEntryBuilder+parseStackTrace) [<code>LogEntryBuilder</code>](#LogEntryBuilder)
- [.addTag(tag)](#LogEntryBuilder+addTag) [<code>LogEntryBuilder</code>](#LogEntryBuilder)
- [.addTags(tags)](#LogEntryBuilder+addTags) [<code>LogEntryBuilder</code>](#LogEntryBuilder)
Expand Down Expand Up @@ -94,6 +95,19 @@ Sets the log entry event's exception fields
| ----- | ------------------ | -------------------------------------------------------------------------------- |
| error | <code>Error</code> | The instance of a JavaScript `Error` object to use, or an Apex HTTP error to use |

<a name="LogEntryBuilder+setField"></a>

### logEntryBuilder.setField(fieldToValue) [<code>LogEntryBuilder</code>](#LogEntryBuilder)

Sets multiple field values on the builder's `LogEntryEvent__e` record

**Kind**: instance method of [<code>LogEntryBuilder</code>](#LogEntryBuilder)
**Returns**: [<code>LogEntryBuilder</code>](#LogEntryBuilder) - The same instance of `LogEntryBuilder`, useful for chaining methods

| Param | Type | Description |
| ------------ | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| fieldToValue | <code>Object</code> | 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"}` |

<a name="LogEntryBuilder+parseStackTrace"></a>

### logEntryBuilder.parseStackTrace(error) [<code>LogEntryBuilder</code>](#LogEntryBuilder)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,11 @@ public inherited sharing class ComponentLogger {
continue;
}

LogEntryEvent__e logEntryEvent = logEntryEventBuilder.setTimestamp(componentLogEntry.timestamp).addTags(componentLogEntry.tags).getLogEntryEvent();
Map<Schema.SObjectField, Object> customFieldToFieldValue = getCustomFieldToFieldValue(componentLogEntry.fieldToValue);
LogEntryEvent__e logEntryEvent = logEntryEventBuilder.setTimestamp(componentLogEntry.timestamp)
.setField(customFieldToFieldValue)
.addTags(componentLogEntry.tags)
.getLogEntryEvent();

if (componentLogEntry.recordId != null) {
logEntryEventBuilder.setRecord(componentLogEntry.recordId);
Expand All @@ -83,6 +87,23 @@ public inherited sharing class ComponentLogger {
}
}

private static Map<Schema.SObjectField, Object> getCustomFieldToFieldValue(Map<String, Object> fieldNameToValue) {
Map<Schema.SObjectField, Object> resolvedFieldToFieldValue = new Map<Schema.SObjectField, Object>();

if (fieldNameToValue == null || fieldNameToValue.isEmpty()) {
return resolvedFieldToFieldValue;
}

Map<String, Schema.SObjectField> fieldNameToField = Schema.LogEntryEvent__e.SObjectType.getDescribe().fields.getMap();
for (String fieldName : fieldNameToValue.keySet()) {
Schema.SObjectField field = fieldNameToField.get(fieldName);
if (field != null) {
resolvedFieldToFieldValue.put(field, fieldNameToValue.get(fieldName));
}
}
return resolvedFieldToFieldValue;
}

private static void setBrowserDetails(LogEntryEvent__e logEntryEvent, ComponentLogEntry componentLogEntry) {
logEntryEvent.BrowserAddress__c = LoggerDataStore.truncateFieldValue(Schema.LogEntryEvent__e.BrowserAddress__c, componentLogEntry.browser?.address);
logEntryEvent.BrowserFormFactor__c = LoggerDataStore.truncateFieldValue(
Expand Down Expand Up @@ -285,6 +306,11 @@ public inherited sharing class ComponentLogger {
@AuraEnabled
public ComponentError error { get; set; }

/**
* @description (Optional) A map containing key-value pairs of fields to set on `LogEntryEvent__e`
*/
@AuraEnabled
public Map<String, Object> fieldToValue { get; set; }
/**
* @description The name of the `LoggingLevel` enum value
*/
Expand Down
2 changes: 1 addition & 1 deletion nebula-logger/core/main/logger-engine/classes/Logger.cls
Original file line number Diff line number Diff line change
Expand Up @@ -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.5';
private static final String CURRENT_VERSION_NUMBER = 'v4.14.6';
private static final System.LoggingLevel FALLBACK_LOGGING_LEVEL = System.LoggingLevel.DEBUG;
private static final List<LogEntryEventBuilder> LOG_ENTRIES_BUFFER = new List<LogEntryEventBuilder>();
private static final String MISSING_SCENARIO_ERROR_MESSAGE = 'No logger scenario specified. A scenario is required for logging in this org.';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,28 @@ describe('logger lwc import tests', () => {
expect(logEntry.browser.windowResolution).toEqual(window.innerWidth + ' x ' + window.innerHeight);
});

it('sets multiple custom fields when using recommended import approach', async () => {
getSettings.mockResolvedValue({ ...MOCK_GET_SETTINGS });
const logger = await createLogger();
await logger.getUserSettings();
const logEntryBuilder = logger.info('example log entry');
const logEntry = logEntryBuilder.getComponentLogEntry();
const firstFakeFieldName = 'SomeField__c';
const firstFieldMockValue = 'something';
const secondFakeFieldName = 'AnotherField__c';
const secondFieldMockValue = 'another value';
expect(logEntry.fieldToValue[firstFakeFieldName]).toBeFalsy();
expect(logEntry.fieldToValue[secondFakeFieldName]).toBeFalsy();

logEntryBuilder.setField({
[firstFakeFieldName]: firstFieldMockValue,
[secondFakeFieldName]: secondFieldMockValue
});

expect(logEntry.fieldToValue[firstFakeFieldName]).toEqual(firstFieldMockValue);
expect(logEntry.fieldToValue[secondFakeFieldName]).toEqual(secondFieldMockValue);
});

it('sets recordId when using recommended import approach', async () => {
getSettings.mockResolvedValue({ ...MOCK_GET_SETTINGS });
const logger = await createLogger();
Expand Down Expand Up @@ -658,6 +680,29 @@ describe('logger lwc legacy markup tests', () => {
expect(logEntry.browser.windowResolution).toEqual(window.innerWidth + ' x ' + window.innerHeight);
});

it('sets multiple custom fields when using deprecated markup approach', async () => {
getSettings.mockResolvedValue({ ...MOCK_GET_SETTINGS });
const logger = createElement('c-logger', { is: Logger });
document.body.appendChild(logger);
await flushPromises();
const logEntryBuilder = await logger.info('example log entry');
const logEntry = logEntryBuilder.getComponentLogEntry();
const firstFakeFieldName = 'SomeField__c';
const firstFieldMockValue = 'something';
const secondFakeFieldName = 'AnotherField__c';
const secondFieldMockValue = 'another value';
expect(logEntry.fieldToValue[firstFakeFieldName]).toBeFalsy();
expect(logEntry.fieldToValue[secondFakeFieldName]).toBeFalsy();

logEntryBuilder.setField({
[firstFakeFieldName]: firstFieldMockValue,
[secondFakeFieldName]: secondFieldMockValue
});

expect(logEntry.fieldToValue[firstFakeFieldName]).toEqual(firstFieldMockValue);
expect(logEntry.fieldToValue[secondFakeFieldName]).toEqual(secondFieldMockValue);
});

it('sets recordId when using deprecated markup approach', async () => {
getSettings.mockResolvedValue({ ...MOCK_GET_SETTINGS });
const logger = createElement('c-logger', { is: Logger });
Expand Down
42 changes: 26 additions & 16 deletions nebula-logger/core/main/logger-engine/lwc/logger/logEntryBuilder.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import FORM_FACTOR from '@salesforce/client/formFactor';
import { log as lightningLog } from 'lightning/logger';
import { LoggerStackTrace } from './loggerStackTrace';

const CURRENT_VERSION_NUMBER = 'v4.14.5';
const CURRENT_VERSION_NUMBER = 'v4.14.6';

const LOGGING_LEVEL_EMOJIS = {
ERROR: '⛔',
Expand All @@ -19,27 +19,19 @@ const LOGGING_LEVEL_EMOJIS = {
};

const ComponentBrowser = class {
address = null;
formFactor = null;
language = null;
screenResolution = null;
userAgent = null;
windowResolution = null;

constructor() {
this.address = window.location.href;
this.formFactor = FORM_FACTOR;
this.language = window.navigator.language;
this.screenResolution = window.screen.availWidth + ' x ' + window.screen.availHeight;
this.userAgent = window.navigator.userAgent;
this.windowResolution = window.innerWidth + ' x ' + window.innerHeight;
}
address = window.location.href;
formFactor = FORM_FACTOR;
language = window.navigator.language;
screenResolution = window.screen.availWidth + ' x ' + window.screen.availHeight;
userAgent = window.navigator.userAgent;
windowResolution = window.innerWidth + ' x ' + window.innerHeight;
};

// JavaScript equivalent to the Apex class ComponentLogger.ComponentLogEntry
const ComponentLogEntry = class {
browser = new ComponentBrowser();
error = null;
fieldToValue = {};
loggingLevel = null;
message = null;
originStackTrace = null;
Expand Down Expand Up @@ -138,6 +130,24 @@ const LogEntryBuilder = class {
return this;
}

/**
* @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"}`
* @return {LogEntryBuilder} The same instance of `LogEntryBuilder`, useful for chaining methods
*/
setField(fieldToValue) {
if (!fieldToValue) {
return this;
}

Object.keys(fieldToValue).forEach(fieldName => {
this.#componentLogEntry.fieldToValue[fieldName] = fieldToValue[fieldName];
});

return this;
}

/**
* @description Parses the provided error's stack trace and sets the log entry's origin & stack trace fields
* @param {Error} error The instance of a JavaScript `Error` object with a stack trace to parse
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,60 @@ private class ComponentLogger_Tests {
System.Assert.isNull(publishedLogEntryEvent.BrowserWindowResolution__c);
}

@IsTest
static void it_should_save_component_log_entry_with_valid_custom_fields() {
LoggerStackTrace.ignoreOrigin(ComponentLogger_Tests.class);
LoggerDataStore.setMock(LoggerMockDataStore.getEventBus());
ComponentLogger.ComponentLogEntry componentLogEntry = createMockComponentLogEntry();
// Realistically, people shouldn't/wouldn't set fields like HttpRequestBody__c or HttpRequestMethod__c...
// But to avoid adding an extra field just for test purposes, we'll use some existing fields
componentLogEntry.fieldToValue = new Map<String, Object>{
Schema.LogEntryEvent__e.HttpRequestBody__c.getDescribe().getName() => 'some value',
Schema.LogEntryEvent__e.HttpRequestMethod__c.getDescribe().getName() => 'another value'
};
System.Assert.areEqual(0, Logger.saveLogCallCount);
System.Assert.areEqual(0, LoggerMockDataStore.getEventBus().getPublishCallCount());
System.Assert.areEqual(0, LoggerMockDataStore.getEventBus().getPublishedPlatformEvents().size());

ComponentLogger.saveComponentLogEntries(new List<ComponentLogger.ComponentLogEntry>{ componentLogEntry }, null);

LogEntryEvent__e publishedLogEntryEvent = (LogEntryEvent__e) LoggerMockDataStore.getEventBus().getPublishedPlatformEvents().get(0);
System.Assert.areEqual('some value', publishedLogEntryEvent.HttpRequestBody__c);
System.Assert.areEqual('another value', publishedLogEntryEvent.HttpRequestMethod__c);
}

@IsTest
static void it_should_save_component_log_entry_with_invalid_custom_fields() {
LoggerStackTrace.ignoreOrigin(ComponentLogger_Tests.class);
LoggerDataStore.setMock(LoggerMockDataStore.getEventBus());
ComponentLogger.ComponentLogEntry componentLogEntry = createMockComponentLogEntry();
componentLogEntry.fieldToValue = new Map<String, Object>{ 'Some Fake Field That Definitely Will Never Exist' => 'some value' };
System.Assert.areEqual(0, Logger.saveLogCallCount);
System.Assert.areEqual(0, LoggerMockDataStore.getEventBus().getPublishCallCount());
System.Assert.areEqual(0, LoggerMockDataStore.getEventBus().getPublishedPlatformEvents().size());

ComponentLogger.saveComponentLogEntries(new List<ComponentLogger.ComponentLogEntry>{ componentLogEntry }, null);

LogEntryEvent__e publishedLogEntryEvent = (LogEntryEvent__e) LoggerMockDataStore.getEventBus().getPublishedPlatformEvents().get(0);
System.Assert.isNotNull(publishedLogEntryEvent);
}

@IsTest
static void it_should_save_component_log_entry_without_custom_fields() {
LoggerStackTrace.ignoreOrigin(ComponentLogger_Tests.class);
LoggerDataStore.setMock(LoggerMockDataStore.getEventBus());
ComponentLogger.ComponentLogEntry componentLogEntry = createMockComponentLogEntry();
componentLogEntry.fieldToValue = null;
System.Assert.areEqual(0, Logger.saveLogCallCount);
System.Assert.areEqual(0, LoggerMockDataStore.getEventBus().getPublishCallCount());
System.Assert.areEqual(0, LoggerMockDataStore.getEventBus().getPublishedPlatformEvents().size());

ComponentLogger.saveComponentLogEntries(new List<ComponentLogger.ComponentLogEntry>{ componentLogEntry }, null);

LogEntryEvent__e publishedLogEntryEvent = (LogEntryEvent__e) LoggerMockDataStore.getEventBus().getPublishedPlatformEvents().get(0);
System.Assert.isNotNull(publishedLogEntryEvent);
}

@IsTest
static void it_should_save_component_log_entry_with_queueable_job() {
LoggerStackTrace.ignoreOrigin(ComponentLogger_Tests.class);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
access="global"
implements="forceCommunity:availableForAllPageTypes,flexipage:availableForAllPageTypes,force:appHostable,force:lightningQuickAction,lightning:availableForFlowScreens"
>
<c:logger aura:id="logger" />

<aura:attribute name="logMessage" type="String" default="Something to log" />

<c:logger aura:id="logger" />

<lightning:card title="Nebula Logger for Aura Components" iconName="custom:custom19">
<div class="slds-var-m-around_medium">This component demonstrates how to use Nebula Logger in Aura components</div>
<div class="slds-var-m-around_medium">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
/* eslint-disable no-console, no-unused-expressions */
({
saveLogExample: function (component) {
console.log("start of aura cmp's saveLog function");
console.log("start of aura cmp's saveLogExample function");

const logger = component.find('logger');
console.log(logger);
logger.info(component.get('{!v.logMessage}'));
logger.info(component.get('{!v.logMessage}')).setField({ SomeLogEntryField__c: 'some text from loggerAuraEmbedDemo' });
logger.saveLog();
}
});
Loading

0 comments on commit 2894401

Please sign in to comment.