Skip to content

Commit

Permalink
Nested exception support (#158)
Browse files Browse the repository at this point in the history
* Nested exception support

* Use BacktraceClient to unpack inner exceptions

* Use parentId

* Clean up the throwable wrapper

* Add support for suppressed exceptions + unit tests

* Add additional test for error.type attribute and rename variables in docs

* Add missing variable name

* Documentation - changed Report into report

* Start using reports to send inner exceptions

* Add error attributes to the report

* Add missing exception message

* Code review adjustements

* Adjusted code to code review suggestions. Null check

* Disable feature by default

* Enable in tests

---------

Co-authored-by: Konrad Dysput <[email protected]>
  • Loading branch information
konraddysput and Konrad Dysput authored Nov 27, 2024
1 parent eea053a commit 43d645b
Show file tree
Hide file tree
Showing 9 changed files with 578 additions and 157 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,11 @@
import org.junit.runner.RunWith;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Stack;
import java.util.concurrent.TimeUnit;

import backtraceio.library.common.BacktraceSerializeHelper;
Expand All @@ -34,12 +37,12 @@

@RunWith(AndroidJUnit4.class)
public class BacktraceClientSendTest {
private Context context;
private BacktraceCredentials credentials;
private final String resultMessage = "From request handler";
private final Map<String, Object> attributes = new HashMap<String, Object>() {{
put("test", "value");
}};
private Context context;
private BacktraceCredentials credentials;

@Before
public void setUp() {
Expand Down Expand Up @@ -91,7 +94,13 @@ public void sendExceptionWithManyCause() {

final BacktraceClient backtraceClient = new BacktraceClient(context, credentials);
final Waiter waiter = new Waiter();
final String mainExceptionExpectedMessage = "java.io.IOException: java.lang.IllegalArgumentException: New Exception";

final Stack<String> expectedExceptionMessages = new Stack<String>() {{
add("New Exception");
add("java.lang.IllegalArgumentException: New Exception");
add("java.io.IOException: java.lang.IllegalArgumentException: New Exception");
}};

RequestHandler rh = data -> {
String jsonString = BacktraceSerializeHelper.toJson(data);

Expand All @@ -100,7 +109,7 @@ public void sendExceptionWithManyCause() {
final JSONObject jsonObject = new JSONObject(jsonString);
final JSONObject exceptionProperties = jsonObject.getJSONObject("annotations").getJSONObject("Exception properties");
final String mainExceptionMessage = jsonObject.getJSONObject("annotations").getJSONObject("Exception").getString("message");

final String mainExceptionExpectedMessage = expectedExceptionMessages.pop();
assertEquals(mainExceptionExpectedMessage, mainExceptionMessage);
assertTrue(exceptionProperties.getJSONArray("stack-trace").length() > 0);
assertEquals(mainExceptionExpectedMessage, exceptionProperties.get("detail-message"));
Expand Down Expand Up @@ -277,6 +286,46 @@ public void onEvent(BacktraceResult backtraceResult) {
}
}

@Test
public void sendExceptionWithInnerException() {
// GIVEN
final int expectedNumberOfReports = 2;
final String innerExceptionMessage = "inner exception";
final String outerExceptionMessage = "outer exception";
final Exception innerException = new Exception(innerExceptionMessage);
final Exception outerException = new Exception(outerExceptionMessage, innerException);
final List<BacktraceData> reportData = new ArrayList<>();
BacktraceClient backtraceClient = new BacktraceClient(context, credentials);
backtraceClient.sendInnerExceptions(true);
backtraceClient.sendSuppressedExceptions(true);
final Waiter waiter = new Waiter();


backtraceClient.setOnRequestHandler(new RequestHandler() {
@Override
public BacktraceResult onRequest(BacktraceData data) {
reportData.add(data);
if (reportData.size() == expectedNumberOfReports) {
waiter.resume();
}
return new BacktraceResult(data.getReport(), data.getReport().exception.getMessage(),
BacktraceResultStatus.Ok);
}
});
backtraceClient.send(outerException);

try {
waiter.await(5, TimeUnit.SECONDS, 1);
} catch (Exception ex) {
fail(ex.getMessage());
}
assertEquals(expectedNumberOfReports, reportData.size());
BacktraceData outerExceptionData = reportData.get(0);
assertEquals(outerExceptionMessage, outerExceptionData.attributes.get("error.message"));
BacktraceData innerExceptionData = reportData.get(reportData.size() - 1);
assertEquals(innerExceptionMessage, innerExceptionData.attributes.get("error.message"));
}

@Test
public void sendMultipleReports() {
// GIVEN
Expand Down Expand Up @@ -313,4 +362,4 @@ public void onEvent(BacktraceResult backtraceResult) {
fail(ex.getMessage());
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;

Expand All @@ -31,12 +33,6 @@ public class UncaughtExceptionHandlerTest {
private Context context;
private BacktraceCredentials credentials;

@Before
public void setUp() {
context = InstrumentationRegistry.getInstrumentation().getContext();
credentials = new BacktraceCredentials("https://example-endpoint.com/", "");
}

private static void setRootHandler(Thread.UncaughtExceptionHandler customRootHandler, Thread.UncaughtExceptionHandler newRootHandler) {
try {
Field field = BacktraceExceptionHandler.class.getDeclaredField("rootHandler");
Expand All @@ -47,7 +43,6 @@ private static void setRootHandler(Thread.UncaughtExceptionHandler customRootHan
}
}


private static BacktraceExceptionHandler createBacktraceExceptionHandler(BacktraceClient client) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, InstantiationException {
Constructor<BacktraceExceptionHandler> constructor = BacktraceExceptionHandler.class.getDeclaredConstructor(BacktraceClient.class);
assertTrue(Modifier.isPrivate(constructor.getModifiers()));
Expand All @@ -57,6 +52,12 @@ private static BacktraceExceptionHandler createBacktraceExceptionHandler(Backtra
return exceptionHandler;
}

@Before
public void setUp() {
context = InstrumentationRegistry.getInstrumentation().getContext();
credentials = new BacktraceCredentials("https://example-endpoint.com/", "");
}

@Test()
public void testUncaughtException() throws InvocationTargetException, NoSuchMethodException, IllegalAccessException, InstantiationException {
// GIVEN
Expand Down Expand Up @@ -131,4 +132,93 @@ public void testUncaughtError() throws InvocationTargetException, NoSuchMethodEx
assertEquals("Unhandled Exception", testedReportData.getReport().attributes.get("error.type"));
assertTrue(testedReportData.getReport().exceptionTypeReport);
}

@Test()
public void testUncaughtInnerExceptionsGeneration() throws InvocationTargetException, NoSuchMethodException, IllegalAccessException, InstantiationException {
// GIVEN
final int expectedNumberOfExceptions = 2;
final Waiter waiter = new Waiter();
final String innerExceptionMessage = "Cause exception message";
final Exception cause = new IllegalArgumentException(innerExceptionMessage);
final String outerExceptionMessage = "Outer exception";
final Exception exception = new IllegalArgumentException(outerExceptionMessage, cause);
final BacktraceClient client = new BacktraceClient(context, credentials);
client.sendInnerExceptions(true);
client.sendSuppressedExceptions(true);

final List<BacktraceData> unhandledExceptionData = new ArrayList<>();
client.setOnRequestHandler(data -> {
unhandledExceptionData.add(data);
if (unhandledExceptionData.size() == expectedNumberOfExceptions) {
waiter.resume();
}
return new BacktraceResult(data.getReport(), data.getReport().message,
BacktraceResultStatus.Ok);
});

final BacktraceExceptionHandler handler = createBacktraceExceptionHandler(client);

// WHEN
handler.uncaughtException(Thread.currentThread(), exception);

// WAIT FOR THE RESULT FROM ANOTHER THREAD
try {
waiter.await(5, TimeUnit.SECONDS);
} catch (Exception ex) {
fail(ex.getMessage());
}

// THEN
assertEquals(expectedNumberOfExceptions, unhandledExceptionData.size());

final BacktraceData outerException = unhandledExceptionData.get(0);
final BacktraceData innerException = unhandledExceptionData.get(unhandledExceptionData.size() - 1);
assertEquals(outerExceptionMessage, outerException.attributes.get("error.message"));
assertEquals(innerExceptionMessage, innerException.attributes.get("error.message"));
}

@Test()
public void testUncaughtInnerExceptionsErrorAttributes() throws InvocationTargetException, NoSuchMethodException, IllegalAccessException, InstantiationException {
// GIVEN
final int expectedNumberOfExceptions = 2;
final Waiter waiter = new Waiter();
final String innerExceptionMessage = "Cause exception message";
final Exception cause = new IllegalArgumentException(innerExceptionMessage);
final String outerExceptionMessage = "Outer exception";
final Exception exception = new IllegalArgumentException(outerExceptionMessage, cause);
final BacktraceClient client = new BacktraceClient(context, credentials);
client.sendInnerExceptions(true);
client.sendSuppressedExceptions(true);

final List<BacktraceData> unhandledExceptionData = new ArrayList<>();
client.setOnRequestHandler(data -> {
unhandledExceptionData.add(data);
if (unhandledExceptionData.size() == expectedNumberOfExceptions) {
waiter.resume();
}
return new BacktraceResult(data.getReport(), data.getReport().message,
BacktraceResultStatus.Ok);
});

final BacktraceExceptionHandler handler = createBacktraceExceptionHandler(client);

// WHEN
handler.uncaughtException(Thread.currentThread(), exception);

// WAIT FOR THE RESULT FROM ANOTHER THREAD
try {
waiter.await(5, TimeUnit.SECONDS);
} catch (Exception ex) {
fail(ex.getMessage());
}

// THEN
final BacktraceData outerException = unhandledExceptionData.get(0);
final BacktraceData innerException = unhandledExceptionData.get(unhandledExceptionData.size() - 1);
assertEquals(outerException.attributes.get("error.trace"), innerException.attributes.get("error.trace"));
assertEquals(outerException.uuid, innerException.attributes.get("error.parent"));
assertNull(outerException.attributes.get("error.parent"));
assertEquals(BacktraceAttributeConsts.UnhandledExceptionAttributeType, outerException.attributes.get("error.type"));
assertEquals(BacktraceAttributeConsts.UnhandledExceptionAttributeType, innerException.attributes.get("error.type"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -197,23 +197,35 @@ public void send(String message, OnServerResponseEventListener serverResponseEve
}

/**
* Sending an exception to Backtrace API
* Sending an throwable to Backtrace API
*
* @param exception current exception
* @param throwable current throwable
*/
public void send(Exception exception) {
this.send(exception, null);
public void send(Throwable throwable) {
this.send(throwable, null);
}

/**
* Sending an exception to Backtrace API
* Sending an throwable to Backtrace API
*
* @param exception current exception
* @param throwable current throwable
* @param serverResponseEventListener event callback that will be executed after receiving a response from the server
*/
public void send(Exception exception, OnServerResponseEventListener
public void send(Throwable throwable, OnServerResponseEventListener
serverResponseEventListener) {
super.send(new BacktraceReport(exception), serverResponseEventListener);
this.send(throwable, null, serverResponseEventListener);
}

/**
* Sending an throwable to Backtrace API
*
* @param throwable current throwable
* @param attributes throwable attributes
* @param serverResponseEventListener event callback that will be executed after receiving a response from the server
*/
public void send(Throwable throwable, Map<String, Object> attributes, OnServerResponseEventListener
serverResponseEventListener) {
super.send(new BacktraceReport(throwable, attributes), serverResponseEventListener);
}

/**
Expand Down Expand Up @@ -292,4 +304,4 @@ public void disableAnr() {
this.anrWatchdog.stopMonitoringAnr();
}
}
}
}
Loading

0 comments on commit 43d645b

Please sign in to comment.