Skip to content

Commit 50449ff

Browse files
authored
Bugfix: Added automatic string truncation for LogEntryEvent__e fields (#610)
* Fixed #589 and fixed #592 by updating LoggerDataStore.EventBus to support using Database.DmlOptions for enabling automatic field truncation on platform events * Fixed #587 by updating LogEntryEventBuilder to automatically truncate any provided String parameters before calling applyDataMaskRules(). Also removed several internal uses of truncateFieldValue() in LogEntryEventBuilder, now that LoggerDataStore will automatically truncate platform event string fields
1 parent 579286d commit 50449ff

13 files changed

Lines changed: 281 additions & 57 deletions

File tree

README.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,15 @@
55

66
The most robust logger for Salesforce. Works with Apex, Lightning Components, Flow, Process Builder & Integrations. Designed for Salesforce admins, developers & architects.
77

8-
## Unlocked Package - v4.12.5
8+
## Unlocked Package - v4.12.6
99

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

14-
`sf package install --wait 20 --security-type AdminsOnly --package 04t5Y000001Mk5PQAS`
14+
`sf package install --wait 20 --security-type AdminsOnly --package 04t5Y000001Mk5UQAS`
1515

16-
`sfdx force:package:install --wait 20 --securitytype AdminsOnly --package 04t5Y000001Mk5PQAS`
16+
`sfdx force:package:install --wait 20 --securitytype AdminsOnly --package 04t5Y000001Mk5UQAS`
1717

1818
---
1919

docs/apex/Logger-Engine/LoggerDataStore.md

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -486,7 +486,7 @@ The instance of `Database.SaveResult`, generated by the platform when publishing
486486

487487
###### `publishRecords(List<SObject> platformEvents)``List<Database.SaveResult>`
488488

489-
Publishes a list of platform event records, using `EventBus.publish(List&lt;SObject&gt; records);
489+
Publishes a list of platform event records, using `EventBus.publish(List&lt;SObject&gt; records)`
490490

491491
####### Parameters
492492

@@ -504,6 +504,27 @@ List&lt;Database.SaveResult&gt;
504504

505505
The instance of `List&lt;Database.SaveResult&gt;`, generated by the platform when publishing the platform event records
506506

507+
###### `publishRecords(List<SObject> platformEvents, Database.DmlOptions dmlOptions)``List<Database.SaveResult>`
508+
509+
Publishes a list of platform event records, using `EventBus.publish(List&lt;SObject&gt; records)`. The provided instance of `Database.DmlOptions` is used to control truncating of string fields before publishing the records.
510+
511+
####### Parameters
512+
513+
| Param | Description |
514+
| ---------------- | --------------------------------------------------------------------------------- |
515+
| `platformEvents` | The list of platform event records to publish |
516+
| `dmlOptions` | An instance of `Database.DmlOptions`, used to control truncating of string fields |
517+
518+
####### Return
519+
520+
**Type**
521+
522+
List&lt;Database.SaveResult&gt;
523+
524+
**Description**
525+
526+
The instance of `List&lt;Database.SaveResult&gt;`, generated by the platform when publishing the platform event records
527+
507528
---
508529

509530
#### LoggerDataStore.JobQueue class

docs/apex/Test-Utilities/LoggerMockDataStore.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ Utility class used to mock any data-related operations for the database, event b
5050

5151
###### `deliver(LoggerSObjectHandler sobjectHandlerInstance)``void`
5252

53+
###### `getLastUsedDmlOptions()``Database.DmlOptions`
54+
5355
###### `getPublishCallCount()``Integer`
5456

5557
###### `getPublishedPlatformEvents()``List<SObject>`
@@ -58,6 +60,8 @@ Utility class used to mock any data-related operations for the database, event b
5860

5961
###### `publishRecords(List<SObject> platformEvents)``List<Database.SaveResult>`
6062

63+
###### `publishRecords(List<SObject> platformEvents, Database.DmlOptions dmlOptions)``List<Database.SaveResult>`
64+
6165
---
6266

6367
#### LoggerMockDataStore.MockJobQueue class

nebula-logger/core/main/logger-engine/classes/LogEntryEventBuilder.cls

Lines changed: 51 additions & 41 deletions
Large diffs are not rendered by default.

nebula-logger/core/main/logger-engine/classes/Logger.cls

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
global with sharing class Logger {
1616
// There's no reliable way to get the version number dynamically in Apex
1717
@TestVisible
18-
private static final String CURRENT_VERSION_NUMBER = 'v4.12.5';
18+
private static final String CURRENT_VERSION_NUMBER = 'v4.12.6';
1919
private static final System.LoggingLevel FALLBACK_LOGGING_LEVEL = System.LoggingLevel.DEBUG;
2020
private static final Set<String> IGNORED_APEX_CLASSES = initializeIgnoredApexClasses();
2121
private static final List<LogEntryEventBuilder> LOG_ENTRIES_BUFFER = new List<LogEntryEventBuilder>();
@@ -54,6 +54,17 @@ global with sharing class Logger {
5454
set;
5555
}
5656

57+
private static final Database.DmlOptions PLATFORM_EVENT_DML_OPTIONS {
58+
get {
59+
if (PLATFORM_EVENT_DML_OPTIONS == null) {
60+
PLATFORM_EVENT_DML_OPTIONS = new Database.DmlOptions();
61+
PLATFORM_EVENT_DML_OPTIONS.AllowFieldTruncation = true;
62+
}
63+
return PLATFORM_EVENT_DML_OPTIONS;
64+
}
65+
set;
66+
}
67+
5768
private static final String USER_SESSION_ID {
5869
get {
5970
if (USER_SESSION_ID == null) {
@@ -3210,7 +3221,7 @@ global with sharing class Logger {
32103221

32113222
switch on getSaveMethod(saveMethodName) {
32123223
when EVENT_BUS {
3213-
List<Database.SaveResult> saveResults = LoggerDataStore.getEventBus().publishRecords(logEntryEvents);
3224+
List<Database.SaveResult> saveResults = LoggerDataStore.getEventBus().publishRecords(logEntryEvents, PLATFORM_EVENT_DML_OPTIONS);
32143225
LoggerEmailSender.sendErrorEmail(Schema.LogEntryEvent__e.SObjectType, saveResults);
32153226
}
32163227
when QUEUEABLE {
@@ -3478,7 +3489,7 @@ global with sharing class Logger {
34783489
*/
34793490
global void execute(System.QueueableContext queueableContext) {
34803491
Logger.setAsyncContext(queueableContext);
3481-
LoggerDataStore.getEventBus().publishRecords(this.logEntryEvents);
3492+
LoggerDataStore.getEventBus().publishRecords(this.logEntryEvents, PLATFORM_EVENT_DML_OPTIONS);
34823493
}
34833494
}
34843495

nebula-logger/core/main/logger-engine/classes/LoggerDataStore.cls

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,7 @@ public without sharing class LoggerDataStore {
281281
/**
282282
* @description Class used to centralize the handling of any platform event publishing operations
283283
*/
284+
@SuppressWarnings('PMD.StdCyclomaticComplexity')
284285
public virtual class EventBus {
285286
@SuppressWarnings('PMD.EmptyStatementBlock')
286287
protected EventBus() {
@@ -296,13 +297,62 @@ public without sharing class LoggerDataStore {
296297
}
297298

298299
/**
299-
* @description Publishes a list of platform event records, using `EventBus.publish(List<SObject> records);
300+
* @description Publishes a list of platform event records, using `EventBus.publish(List<SObject> records)`
300301
* @param platformEvents The list of platform event records to publish
301302
* @return The instance of `List<Database.SaveResult>`, generated by the platform when publishing the platform event records
302303
*/
303304
public virtual List<Database.SaveResult> publishRecords(List<SObject> platformEvents) {
305+
return this.publishRecords(platformEvents, null);
306+
}
307+
308+
/**
309+
* @description Publishes a list of platform event records, using `EventBus.publish(List<SObject> records)`. The provided
310+
* instance of `Database.DmlOptions` is used to control truncating of string fields before publishing the records.
311+
* @param platformEvents The list of platform event records to publish
312+
* @param dmlOptions An instance of `Database.DmlOptions`, used to control truncating of string fields
313+
* @return The instance of `List<Database.SaveResult>`, generated by the platform when publishing the platform event records
314+
*/
315+
public virtual List<Database.SaveResult> publishRecords(List<SObject> platformEvents, Database.DmlOptions dmlOptions) {
316+
if (dmlOptions?.AllowFieldTruncation == true) {
317+
this.truncateStringFields(platformEvents);
318+
}
304319
return System.EventBus.publish(platformEvents);
305320
}
321+
322+
@SuppressWarnings('PMD.CognitiveComplexity, PMD.CyclomaticComplexity, PMD.StdCyclomaticComplexity')
323+
private void truncateStringFields(List<SObject> platformEvents) {
324+
if (platformEvents == null || platformEvents.isEmpty()) {
325+
return;
326+
}
327+
328+
Schema.SObjectType sobjectType = platformEvents.get(0)?.getSObjectType();
329+
if (sobjectType == null) {
330+
return;
331+
}
332+
333+
Map<String, Schema.SObjectField> stringFieldNameToField = new Map<String, Schema.SObjectField>();
334+
for (Schema.SObjectField field : sobjectType.getDescribe().fields.getMap().values()) {
335+
if (field.getDescribe().getType() == Schema.DisplayType.STRING && field.getDescribe().isCreateable()) {
336+
stringFieldNameToField.put(field.getDescribe().getName(), field);
337+
}
338+
}
339+
for (SObject platformEvent : platformEvents) {
340+
for (String populatedFieldName : platformEvent.getPopulatedFieldsAsMap().keySet()) {
341+
if (stringFieldNameToField.containsKey(populatedFieldName) && platformEvent.get(populatedFieldName) != null) {
342+
Schema.SObjectField field = stringFieldNameToField.get(populatedFieldName);
343+
if (field == null) {
344+
continue;
345+
}
346+
347+
Integer fieldMaxLength = field.getDescribe().getLength();
348+
String stringValue = (String) platformEvent.get(populatedFieldName);
349+
// String.abbreviate() will throw a StringException if the target length is less than 4
350+
String truncatedStringValue = fieldMaxLength < 4 ? stringValue?.left(fieldMaxLength) : stringValue?.abbreviate(fieldMaxLength);
351+
platformEvent.put(populatedFieldName, truncatedStringValue);
352+
}
353+
}
354+
}
355+
}
306356
}
307357

308358
/**

nebula-logger/core/main/logger-engine/lwc/logger/logger.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import { LightningElement, api } from 'lwc';
77
import { createLoggerService } from './loggerService';
88

9-
const CURRENT_VERSION_NUMBER = 'v4.12.5';
9+
const CURRENT_VERSION_NUMBER = 'v4.12.6';
1010

1111
export default class Logger extends LightningElement {
1212
#loggerService = createLoggerService();

nebula-logger/core/tests/logger-engine/classes/LogEntryEventBuilder_Tests.cls

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,23 @@ private class LogEntryEventBuilder_Tests {
279279
System.Assert.areEqual(message, builder.getLogEntryEvent().Message__c);
280280
}
281281

282+
@IsTest
283+
static void it_should_truncate_message_field_when_string_is_too_long() {
284+
LogEntryEventBuilder builder = new LogEntryEventBuilder(getUserSettings(), System.LoggingLevel.INFO, true, new Set<String>());
285+
System.Assert.isFalse(builder.getLogEntryEvent().MessageTruncated__c);
286+
System.Assert.isFalse(builder.getLogEntryEvent().MessageMasked__c);
287+
System.Assert.isNull(builder.getLogEntryEvent().Message__c);
288+
String excessivelyLongMessage = 'a'.repeat(Schema.LogEntryEvent__e.Message__c.getDescribe().getLength() + 1);
289+
290+
builder.setMessage(excessivelyLongMessage);
291+
292+
String expectedTruncatedMessage = excessivelyLongMessage.left(Schema.LogEntryEvent__e.Message__c.getDescribe().getLength());
293+
System.Assert.areNotEqual(expectedTruncatedMessage, excessivelyLongMessage);
294+
System.Assert.isTrue(builder.getLogEntryEvent().MessageTruncated__c);
295+
System.Assert.isFalse(builder.getLogEntryEvent().MessageMasked__c);
296+
System.Assert.areEqual(expectedTruncatedMessage, builder.getLogEntryEvent().Message__c);
297+
}
298+
282299
@IsTest
283300
static void it_should_not_apply_data_mask_rule_when_rule_disabled() {
284301
LoggerSettings__c userSettings = getUserSettings();
@@ -1118,6 +1135,26 @@ private class LogEntryEventBuilder_Tests {
11181135
System.Assert.areEqual(request.getMethod(), builder.getLogEntryEvent().HttpRequestMethod__c);
11191136
}
11201137

1138+
@IsTest
1139+
static void it_should_truncate_http_request_body_field_when_string_is_too_long() {
1140+
LogEntryEventBuilder builder = new LogEntryEventBuilder(getUserSettings(), System.LoggingLevel.INFO, true, new Set<String>());
1141+
System.Assert.isNull(builder.getLogEntryEvent().HttpRequestBody__c);
1142+
String excessivelyLongRequestBody = 'a'.repeat(Schema.LogEntryEvent__e.HttpRequestBody__c.getDescribe().getLength() + 1);
1143+
System.HttpRequest request = new System.HttpRequest();
1144+
request.setBody(excessivelyLongRequestBody);
1145+
1146+
builder.setHttpRequestDetails(request);
1147+
1148+
System.Assert.areEqual(
1149+
excessivelyLongRequestBody.left(Schema.LogEntryEvent__e.HttpRequestBody__c.getDescribe().getLength()),
1150+
builder.getLogEntryEvent().HttpRequestBody__c,
1151+
'Expected string length ' +
1152+
Schema.LogEntryEvent__e.HttpRequestBody__c.getDescribe().getLength() +
1153+
' for field HttpRequestBody__c, actual length is ' +
1154+
builder.getLogEntryEvent().HttpRequestBody__c.length()
1155+
);
1156+
}
1157+
11211158
@IsTest
11221159
static void it_should_skip_setting_http_response_fields_when_response_is_null() {
11231160
LogEntryEventBuilder builder = new LogEntryEventBuilder(getUserSettings(), System.LoggingLevel.INFO, true, new Set<String>());
@@ -1189,6 +1226,26 @@ private class LogEntryEventBuilder_Tests {
11891226
System.Assert.areEqual(response.getStatusCode(), builder.getLogEntryEvent().HttpResponseStatusCode__c);
11901227
}
11911228

1229+
@IsTest
1230+
static void it_should_truncate_http_response_body_field_when_string_is_too_long() {
1231+
LogEntryEventBuilder builder = new LogEntryEventBuilder(getUserSettings(), System.LoggingLevel.INFO, true, new Set<String>());
1232+
System.Assert.isNull(builder.getLogEntryEvent().HttpResponseBody__c);
1233+
String excessivelyLongResponseBody = 'a'.repeat(Schema.LogEntryEvent__e.HttpResponseBody__c.getDescribe().getLength() + 1);
1234+
System.HttpResponse response = new System.HttpResponse();
1235+
response.setBody(excessivelyLongResponseBody);
1236+
1237+
builder.setHttpResponseDetails(response);
1238+
1239+
System.Assert.areEqual(
1240+
excessivelyLongResponseBody.left(Schema.LogEntryEvent__e.HttpResponseBody__c.getDescribe().getLength()),
1241+
builder.getLogEntryEvent().HttpResponseBody__c,
1242+
'Expected string length ' +
1243+
Schema.LogEntryEvent__e.HttpResponseBody__c.getDescribe().getLength() +
1244+
' for field HttpResponseBody__c, actual length is ' +
1245+
builder.getLogEntryEvent().HttpResponseBody__c.length()
1246+
);
1247+
}
1248+
11921249
@IsTest
11931250
static void it_should_set_tags_string_for_list_of_tags() {
11941251
LogEntryEventBuilder builder = new LogEntryEventBuilder(getUserSettings(), System.LoggingLevel.INFO, true, new Set<String>());

nebula-logger/core/tests/logger-engine/classes/LoggerDataStore_Tests.cls

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -525,6 +525,46 @@ private class LoggerDataStore_Tests {
525525
}
526526
}
527527

528+
@IsTest
529+
static void it_should_not_automatically_truncate_string_fields_when_publishing_platform_event_record_list_without_truncation_dml_options() {
530+
List<LogEntryEvent__e> logEntryEventsToPublish = new List<LogEntryEvent__e>();
531+
LogEntryEvent__e logEntryEvent = (LogEntryEvent__e) LoggerMockDataCreator.createDataBuilder(Schema.LogEntryEvent__e.SObjectType)
532+
.populateRequiredFields()
533+
.getRecord();
534+
logEntryEvent.TransactionId__c = 'a'.repeat(Schema.LogEntryEvent__e.TransactionId__c.getDescribe().getLength() + 1);
535+
logEntryEventsToPublish.add(logEntryEvent);
536+
System.Assert.areEqual(0, System.Limits.getPublishImmediateDml());
537+
538+
List<Database.SaveResult> results = LoggerDataStore.getEventBus().publishRecords(logEntryEventsToPublish);
539+
540+
System.Assert.areEqual(logEntryEventsToPublish.size(), results.size());
541+
System.Assert.isFalse(results.get(0).isSuccess());
542+
System.Assert.areEqual(System.StatusCode.STRING_TOO_LONG, results.get(0).getErrors().get(0).getStatusCode());
543+
}
544+
545+
@IsTest
546+
static void it_should_automatically_truncate_string_fields_when_publishing_platform_event_record_list_with_truncation_dml_options() {
547+
List<LogEntryEvent__e> logEntryEventsToPublish = new List<LogEntryEvent__e>();
548+
String originalValue = 'a'.repeat(Schema.LogEntryEvent__e.TransactionId__c.getDescribe().getLength() + 1);
549+
String expectedTruncatedValue = originalValue.abbreviate(Schema.LogEntryEvent__e.TransactionId__c.getDescribe().getLength());
550+
LogEntryEvent__e logEntryEvent = (LogEntryEvent__e) LoggerMockDataCreator.createDataBuilder(Schema.LogEntryEvent__e.SObjectType)
551+
.populateRequiredFields()
552+
.getRecord();
553+
logEntryEvent.TransactionId__c = originalValue;
554+
logEntryEventsToPublish.add(logEntryEvent);
555+
System.Assert.areEqual(0, System.Limits.getPublishImmediateDml());
556+
557+
Database.DmlOptions truncationDmlOptions = new Database.DmlOptions();
558+
truncationDmlOptions.AllowFieldTruncation = true;
559+
List<Database.SaveResult> results = LoggerDataStore.getEventBus().publishRecords(logEntryEventsToPublish, truncationDmlOptions);
560+
561+
System.Assert.areEqual(logEntryEventsToPublish.size(), results.size());
562+
System.Assert.isTrue(results.get(0).isSuccess());
563+
System.Assert.areEqual(1, System.Limits.getPublishImmediateDml(), results.toString());
564+
System.Assert.areNotEqual(originalValue, logEntryEvent.TransactionId__c);
565+
System.Assert.areEqual(expectedTruncatedValue, logEntryEvent.TransactionId__c);
566+
}
567+
528568
// Queueable tests
529569
@IsTest
530570
static void it_should_enqueue_job() {
@@ -555,7 +595,7 @@ private class LoggerDataStore_Tests {
555595
LoggerDataStore.setMock(mockEventBusInstance);
556596
System.Assert.areEqual(mockEventBusInstance, LoggerDataStore.getEventBus());
557597

558-
LoggerDataStore.getEventBus().publishRecord(new Schema.User()); // Not an actual platform event, but for mocking/test purposes, Schema.User is fine
598+
LoggerDataStore.getEventBus().publishRecord(new Schema.User()); // Not an actual platform event, but for mocking/test purposes, User is fine
559599

560600
System.Assert.areEqual(1, mockEventBusInstance.callCount);
561601
}

nebula-logger/core/tests/logger-engine/classes/Logger_Tests.cls

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1019,6 +1019,26 @@ private class Logger_Tests {
10191019
System.Assert.areEqual(0, System.Limits.getDmlStatements());
10201020
}
10211021

1022+
@IsTest
1023+
static void it_should_pass_dml_options_to_data_store_when_saving_via_event_bus() {
1024+
LoggerDataStore.setMock(LoggerMockDataStore.getEventBus());
1025+
System.Assert.areEqual(0, Logger.getBufferSize());
1026+
System.Assert.areEqual(0, LoggerMockDataStore.getEventBus().getPublishedPlatformEvents().size());
1027+
System.Assert.areEqual(Logger.SaveMethod.EVENT_BUS.name(), Logger.getUserSettings().DefaultSaveMethod__c);
1028+
System.Assert.isTrue(Logger.getUserSettings().IsSavingEnabled__c);
1029+
System.Assert.isTrue(Logger.isDebugEnabled());
1030+
Logger.debug('test log entry');
1031+
System.Assert.areEqual(1, Logger.getBufferSize());
1032+
1033+
Logger.saveLog();
1034+
1035+
System.Assert.areEqual(0, Logger.getBufferSize());
1036+
System.Assert.areEqual(1, LoggerMockDataStore.getEventBus().getPublishedPlatformEvents().size());
1037+
Database.DmlOptions lastUsedDmlOptions = LoggerMockDataStore.getEventBus().getLastUsedDmlOptions();
1038+
System.Assert.isNotNull(lastUsedDmlOptions);
1039+
System.Assert.areEqual(true, lastUsedDmlOptions?.AllowFieldTruncation);
1040+
}
1041+
10221042
@IsTest
10231043
static void it_should_save_via_event_bus_when_defaulted() {
10241044
// TODO eliminate references to Log__c, find alternative way to assert on expected data

0 commit comments

Comments
 (0)