Skip to content

Commit

Permalink
Added new Apex method & JavaScript function Logger.setField() (#772)
Browse files Browse the repository at this point in the history
* Added a new Apex static method Logger.setField() so custom fields can be set once per transaction --> auto-populated on all subsequent LogEntryEvent__e records

* Added new JavaScript function logger.setField() so custom fields can be set once per component instance  --> auto-populated on all subsequent LogEntryEvent__e records
  • Loading branch information
jongpie authored Oct 16, 2024
1 parent ac1e38c commit 5453428
Show file tree
Hide file tree
Showing 16 changed files with 277 additions and 48 deletions.
63 changes: 45 additions & 18 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, 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)
[![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`

---

Expand Down Expand Up @@ -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<Schema.SObjectField, Object> 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<Schema.SObjectField, Object>{
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<Schema.SObjectField, 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 method overloads `LogEntryEventBuilder.setField(Schema.SObjectField field, Object fieldValue)` or `LogEntryEventBuilder.setField(Map<Schema.SObjectField, Object> 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 LoggerDemo 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({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();
}
}
Expand Down
2 changes: 1 addition & 1 deletion docs/apex/Logger-Engine/LogEntryEventBuilder.md
Original file line number Diff line number Diff line change
Expand Up @@ -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&apos;s `LogEntryEvent__e` record
Sets a field value on the builder&apos;s `LogEntryEvent__e` record

##### Parameters

Expand Down
21 changes: 21 additions & 0 deletions docs/apex/Logger-Engine/Logger.md
Original file line number Diff line number Diff line change
Expand Up @@ -4948,6 +4948,27 @@ Stores additional details about the current transacation&apos;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<Schema.SObjectField, Object> fieldToValue)``void`

Sets multiple field values oon every generated `LogEntryEvent__e` record

##### Parameters

| Param | Description |
| -------------- | ---------------------------------------------------------------------- |
| `fieldToValue` | An instance of `Map&lt;Schema.SObjectField, Object&gt;` containing the |

#### `setParentLogTransactionId(String parentTransactionId)``void`

Relates the current transaction&apos;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 &amp; queueable jobs.
Expand Down
2 changes: 1 addition & 1 deletion jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@ module.exports = {
'^lightning/empApi$': '<rootDir>/config/jest/mocks/lightning/empApi',
'^lightning/navigation$': '<rootDir>/config/jest/mocks/lightning/navigation'
},
modulePathIgnorePatterns: ['recipes'],
// modulePathIgnorePatterns: ['recipes'],
testPathIgnorePatterns: ['<rootDir>/temp/']
};
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<Schema.SObjectField, Object>` 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')
Expand Down
49 changes: 47 additions & 2 deletions nebula-logger/core/main/logger-engine/classes/Logger.cls
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,14 @@
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<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.';
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<String, SaveMethod> SAVE_METHOD_NAME_TO_SAVE_METHOD = new Map<String, SaveMethod>();
private static final Map<Schema.SObjectField, Object> TRANSACTION_FIELD_TO_VALUE = new Map<Schema.SObjectField, Object>();
private static final String TRANSACTION_ID = System.UUID.randomUUID().toString();

private static AsyncContext currentAsyncContext;
Expand Down Expand Up @@ -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<Schema.SObjectField, Object>{ field => fieldValue });
}

/**
* @description Sets multiple field values oon every generated `LogEntryEvent__e` record
* @param fieldToValue An instance of `Map<Schema.SObjectField, Object>` containing the
* the fields & values to populate on each `LogEntryEvent__e` record in the current transaction
*/
global static void setField(Map<Schema.SObjectField, Object> 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
Expand Down Expand Up @@ -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;

Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand All @@ -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';
Expand All @@ -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();
Expand All @@ -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();
Expand All @@ -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();
Expand Down Expand Up @@ -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();
Expand Down
9 changes: 9 additions & 0 deletions nebula-logger/core/main/logger-engine/lwc/logger/logger.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading

0 comments on commit 5453428

Please sign in to comment.