Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added new Apex method & JS function Logger.setField() #772

Merged
merged 5 commits into from
Oct 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading