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

Flow and Process error reports #17

Merged
merged 1 commit into from
May 29, 2020
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
15 changes: 12 additions & 3 deletions force-app/main/default/classes/DataBuilder.cls
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ public with sharing class DataBuilder {
this.config = config;
}

public Map<String, Object> buildPayload(String level, String message, Map<String, Object> custom)
public Map<String, Object> buildPayload(String level, String message, Map<String, Object> custom, List<Telemetry> telemetry)
{
return buildPayloadStructure(level, buildMessageBody(message), custom);
return buildPayloadStructure(level, buildMessageBody(message, telemetry), custom);
}

public Map<String, Object> buildPayload(String level, Exception exc, Map<String, Object> custom)
Expand Down Expand Up @@ -86,14 +86,23 @@ public with sharing class DataBuilder {
return structure;
}

private Map<String, Object> buildMessageBody(String message)
private Map<String, Object> buildMessageBody(String message, List<Telemetry> telemetry)
{
Map<String, Object> messageMap = new Map<String, Object>();
messageMap.put('body', message);

Map<String, Object> body = new Map<String, Object>();
body.put('message', messageMap);

if (telemetry != null) {
List<Object> telemetryList = new List<Object>();

for (Telemetry t : telemetry) {
telemetryList.add(t.toMap());
}
body.put('telemetry', telemetryList);
}

return body;
}

Expand Down
6 changes: 6 additions & 0 deletions force-app/main/default/classes/ExceptionEmailParser.cls
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ public with sharing class ExceptionEmailParser {
return exData;
}

private static String fromName = 'ApexApplication';

public static String fromName() {
return fromName;
}

public static String parseEnvironment(String emailBody) {
return emailBody.split('\\n')[0];
}
Expand Down
77 changes: 77 additions & 0 deletions force-app/main/default/classes/FlowEmailParser.cls
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
public with sharing class FlowEmailParser {
public static List<Telemetry> parseTelemetry(String emailBody) {
List<String> lines = emailBody.split('\n');

return parseLines(lines);
}

private static String fromName = 'FlowApplication';

public static String fromName() {
return fromName;
}

// Flow email message blocks are generally multiline and separated by
// one or more lines of white space. There are an arbitrary number of blocks
// and an arbitrary number of lines within a block.
//
// This parser converts each block into a 'log' telemetry event.
// Telemetry messages don't render line breaks, so we use punctuation to
// format the message and improve readability.
private static List<Telemetry> parseLines(List<String> lines) {
List<Telemetry> telemetryList = new List<Telemetry>();
Boolean blank = true;
Boolean prevBlank = true;

// Init these to ensure any initial state below is safe.
String messageFirstLine = '';
List<String> messageLines = new List<String>();

for (String line : lines) {
String plain = stripHtmlTags(line);
blank = String.isBlank(plain);

// Lightweight state machine using blank, prevBlank.
if (prevBlank && blank) {
// continue to next block.
} else if (prevBlank && !blank) { // start new block
// Handle first line separately for better formatting.
messageFirstLine = plain + ' ';
messageLines = new List<String>();
} else if (!prevBlank && !blank) { // continue block
messageLines.add(plain);
} else { // end block
String message = messageFirstLine + String.join(messageLines, ', ');
telemetryList.add(addMessage(message));
}
prevBlank = blank;
}

return telemetryList;
}

private static Telemetry addMessage(String message) {
return new Telemetry(
'info',
'log',
'server',
Datetime.now().getTime(),
new Map<String, String>{ 'message' => message }
);
}

// Flow emails are delivered in html format only.
// This tag stripper is forgiving of <> characters within the content
// as it will only match innermost brackets where additional brackets exist.
// (Flow error emails can't be counted on to be valid html in all cases.)
//
// NB: String.stripHtmlTags() is not used, as it is scheduled for deprecation,
// doesn't remove all tags, and its implementation isn't open for review.
private static String stripHtmlTags(String html) {
String HTML_TAGS = '<[^<>]+>';
Pattern pattern = Pattern.compile(HTML_TAGS);
Matcher matcher = pattern.matcher(html);
return matcher.replaceAll('');
}

}
5 changes: 5 additions & 0 deletions force-app/main/default/classes/FlowEmailParser.cls-meta.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<ApexClass xmlns="urn:metadata.tooling.soap.sforce.com" fqn="FlowEmailParser">
<apiVersion>47.0</apiVersion>
<status>Active</status>
</ApexClass>
4 changes: 2 additions & 2 deletions force-app/main/default/classes/Notifier.cls
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ public with sharing class Notifier
this.dataBuilder = new DataBuilder(this.config);
}

public HttpResponse log(String level, String message, Map<String, Object> custom, SendMethod method)
public HttpResponse log(String level, String message, Map<String, Object> custom, List<Telemetry> telemetry, SendMethod method)
{
return send(dataBuilder.buildPayload(level, message, custom), method);
return send(dataBuilder.buildPayload(level, message, custom, telemetry), method);
}

public HttpResponse log(String level, Exception exc, Map<String, Object> custom, SendMethod method)
Expand Down
14 changes: 10 additions & 4 deletions force-app/main/default/classes/Rollbar.cls
Original file line number Diff line number Diff line change
Expand Up @@ -41,20 +41,26 @@ global with sharing class Rollbar {
}

global static HttpResponse log(String level, String message) {
return log(level, message, null, null);
return log(level, message, null, null, null);
}

global static HttpResponse log(String level, String message, Map<String, Object> custom) {
return log(level, message, custom, null);
return log(level, message, custom, null, null);
}

global static HttpResponse log(String level, String message, SendMethod method) {
return log(level, message, null, method);
return log(level, message, null, null, method);
}

global static HttpResponse log(String level, String message, Map<String, Object> custom, SendMethod method) {
return log(level, message, custom, null, method);
}

// Keep this unpublished (public, not global) until Telemetry interface can be frozen,
// which might be never.
public static HttpResponse log(String level, String message, Map<String, Object> custom, List<Telemetry> telemetry, SendMethod method) {
Rollbar instance = initializedInstance();
return instance.notifier.log(level, message, custom, method);
return instance.notifier.log(level, message, custom, telemetry, method);
}

global static HttpResponse log(Exception exc) {
Expand Down
62 changes: 46 additions & 16 deletions force-app/main/default/classes/RollbarExceptionEmailHandler.cls
Original file line number Diff line number Diff line change
@@ -1,23 +1,15 @@
global class RollbarExceptionEmailHandler implements Messaging.InboundEmailHandler {
global Messaging.InboundEmailResult handleInboundEmail(Messaging.inboundEmail email,

global Messaging.InboundEmailResult handleInboundEmail(Messaging.InboundEmail email,
Messaging.InboundEnvelope env){
// Create an InboundEmailResult object for returning the result of the

// Create an InboundEmailResult object for returning the result of the
// Apex Email Service
Messaging.InboundEmailResult result = new Messaging.InboundEmailResult();

String emailBody = '';

// Add the email plain text into the local variable
emailBody = email.plainTextBody;

Rollbar.init();

try {
try {
ExceptionData exData = ExceptionEmailParser.parse(emailBody);
Rollbar.log(exData);
parseAndSend(email);
} catch(Exception exc) {
exc.getStackTraceString(); // without those calls strack trace string is not populated
throw new ExceptionEmailParsingException('Unable to process unhandled exception email', exc);
Expand All @@ -26,16 +18,54 @@ global class RollbarExceptionEmailHandler implements Messaging.InboundEmailHandl
wrapper.getStackTraceString(); // without those calls strack trace string is not populated

Map<String, String> custom = new Map<String, String>();
custom.put('email_body', emailBody);
custom.put('email_body', getUnknownEmailBody(email));

Rollbar.log(wrapper, custom);
}

// Set the result to true. No need to send an email back to the user
// Set the result to true. No need to send an email back to the user
// with an error message
result.success = true;

// Return the result for the Apex Email Service
return result;
}
}

private void parseAndSend(Messaging.InboundEmail email) {
String emailBody = '';

Rollbar.init();

// The fromName field is the most reliable known way to differentiate
// Exception and Flow email types.
if (email.fromName == ExceptionEmailParser.fromName()) {
emailBody = email.plainTextBody;
ExceptionData exData = ExceptionEmailParser.parse(emailBody);

Rollbar.log(exData);
} else if (email.fromName == FlowEmailParser.fromName()) {
emailBody = email.htmlBody;
List<Telemetry> telemetry = FlowEmailParser.parseTelemetry(emailBody);

Rollbar.log('error', email.subject, null, telemetry, SendMethod.SYNC);
} else {
// Unknown email type.
// TODO: Use emailBody and fromName in notifier.diagnostic

Rollbar.log('error', email.subject, null, null, SendMethod.SYNC);
}
}

// Salesforce emails may contain either plain or html parts, but in the
// case of Exception and Flow emails, they don't contain both.
// This helper prefers plain, but will return html is it's the only part found.
private String getUnknownEmailBody(Messaging.InboundEmail email) {
String emailBody = email.plainTextBody;

// Use html Body if no plain body is found.
if (emailBody == null || emailBody == '') {
emailBody = email.htmlBody;
}
return emailBody;
}
}
44 changes: 44 additions & 0 deletions force-app/main/default/classes/Telemetry.cls
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
public with sharing class Telemetry {
public String level { get; set; }
public String type { get; set; }
public String source { get; set; }
public Long timestamp { get; set; }
public Map<String, String> body { get; set; }

public Telemetry(
String level,
String type,
String source,
Long timestamp,
Map<String, String> body
) {
this.level = level;
this.type = type;
this.source = source;
this.timestamp = timestamp;
this.body = body;
}
public Telemetry() {}

public static Telemetry fromMap(Map<String, Object> inMap) {
return new Telemetry(
(String)inMap.get('level'),
(String)inMap.get('type'),
(String)inMap.get('source'),
(Long)inMap.get('timestamp_ms'),
(Map<String, String>)inMap.get('body')
);
}

public Map<String, Object> toMap() {
Map<String, Object> outMap = new Map<String, Object>();

outMap.put('level', this.level);
outMap.put('type', this.type);
outMap.put('source', this.source);
outMap.put('timestamp_ms', this.timestamp);
outMap.put('body', this.body);

return outMap;
}
}
5 changes: 5 additions & 0 deletions force-app/main/default/classes/Telemetry.cls-meta.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<ApexClass xmlns="urn:metadata.tooling.soap.sforce.com" fqn="Telemetry">
<apiVersion>47.0</apiVersion>
<status>Active</status>
</ApexClass>
45 changes: 44 additions & 1 deletion force-app/main/default/tests/DataBuilderTest.cls
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ public class DataBuilderTest
DataBuilder subject = new DataBuilder(new Config('foo', 'bar'));
String expected = 'Message built in DataBuilderTest';

Map<String, Object> result = subject.buildPayload('info', expected, null);
Map<String, Object> result = subject.buildPayload('info', expected, null, null);

Map<String, Object> data = (Map<String, Object>)result.get('data');

Expand All @@ -21,6 +21,49 @@ public class DataBuilderTest
System.assertEquals(expected, ((Map<String, Object>)body.get('message')).get('body'));
}

@isTest
static void testBuildMessagePayloadWithTelemetry()
{
DataBuilder subject = new DataBuilder(new Config('foo', 'bar'));

List<Telemetry> telemetryList = new List<Telemetry>();
telemetryList.add(new Telemetry(
'info',
'log',
'server',
987654321,
new Map<String, String>{ 'message' => 'first' }
));
telemetryList.add(new Telemetry(
'info',
'log',
'server',
987654321,
new Map<String, String>{ 'message' => 'second' }
));

Map<String, Object> result = subject.buildPayload('info', 'Telemetry test', null, telemetryList);

Map<String, Object> data = (Map<String, Object>)result.get('data');
Map<String, Object> body = (Map<String, Object>)data.get('body');

List<Map<String, Object>> telemetryObjects = new List<Map<String, Object>>();
for (Object instance : (List<Object>)body.get('telemetry')) {
telemetryObjects.add((Map<String, Object>)instance);
}

for (Map<String, Object> telemetry : telemetryObjects) {
System.assertEquals((String)telemetry.get('level'), 'info');
System.assertEquals((String)telemetry.get('type'), 'log');
System.assertEquals((String)telemetry.get('source'), 'server');
System.assertEquals((Long)telemetry.get('timestamp_ms'), 987654321);
}
Map<String, String> messageBody = (Map<String, String>)telemetryObjects.get(0).get('body');
System.assertEquals(messageBody.get('message'), 'first');
messageBody = (Map<String, String>)telemetryObjects.get(1).get('body');
System.assertEquals(messageBody.get('message'), 'second');
}

@isTest
static void testBuildExceptionPayloadAnonymousBlock()
{
Expand Down
Loading