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

Improve ANR error message information #553

Merged
merged 9 commits into from
Aug 14, 2019
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Changelog

## TBD

* Improve ANR error message information
[#553](https://github.com/bugsnag/bugsnag-android/pull/553)

## 4.17.2 (2019-08-01)

### Bug fixes
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.bugsnag.android

import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test

class ErrorNameTest {

private lateinit var error: Error

@Before
fun setUp() {
val config = Configuration("api-key")
val exception = RuntimeException("Example message", RuntimeException("Another"))
error = Error.Builder(config, exception, null, Thread.currentThread(), false).build()
}

@Test
fun exceptionTypeConverted() {
assertTrue(error.exception is BugsnagException)
assertTrue(error.exceptions.exception is BugsnagException)
assertTrue(error.exceptions.exception.cause is RuntimeException)
}

@Test
fun defaultExceptionName() {
assertEquals("java.lang.RuntimeException", error.exceptionName)
}

@Test
fun defaultExceptionMessage() {
assertEquals("Example message", error.exceptionMessage)
}

@Test
fun overrideExceptionName() {
error.exceptionName = "Foo"
assertEquals("Foo", error.exceptionName)
}

@Test
fun overrideExceptionMessage() {
error.exceptionMessage = "Some custom message"
assertEquals("Some custom message", error.exceptionMessage)
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -78,16 +78,6 @@ public void testShouldIgnoreClass() {
assertTrue(error.shouldIgnoreClass());
}

@Test
public void testGetExceptionName() {
assertEquals("java.lang.RuntimeException", error.getExceptionName());
}

@Test
public void testGetExceptionMessage() {
assertEquals("Example message", error.getExceptionMessage());
}

@Test
public void testBasicSerialization() throws JSONException, IOException {
error.setAppData(client.getAppData().getAppData());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ public void setUp() throws Exception {

@Test
public void testBasicException() throws JSONException, IOException {
Exceptions exceptions = new Exceptions(config, new RuntimeException("oops"));
RuntimeException oops = new RuntimeException("oops");
Exceptions exceptions = new Exceptions(config, new BugsnagException(oops));
JSONArray exceptionsJson = streamableToJsonArray(exceptions);

assertEquals(1, exceptionsJson.length());
Expand All @@ -45,7 +46,7 @@ public void testBasicException() throws JSONException, IOException {
@Test
public void testCauseException() throws JSONException, IOException {
Throwable ex = new RuntimeException("oops", new Exception("cause"));
Exceptions exceptions = new Exceptions(config, ex);
Exceptions exceptions = new Exceptions(config, new BugsnagException(ex));
JSONArray exceptionsJson = streamableToJsonArray(exceptions);

assertEquals(2, exceptionsJson.length());
Expand All @@ -68,7 +69,7 @@ public void testNamedException() throws JSONException, IOException {
Error error = new Error.Builder(config, "RuntimeException",
"Example message", frames, BugsnagTestUtils.generateSessionTracker(),
Thread.currentThread()).build();
Exceptions exceptions = new Exceptions(config, error.getException());
Exceptions exceptions = new Exceptions(config, new BugsnagException(error.getException()));

JSONObject exceptionJson = streamableToJsonArray(exceptions).getJSONObject(0);
assertEquals("RuntimeException", exceptionJson.get("errorClass"));
Expand All @@ -79,19 +80,4 @@ public void testNamedException() throws JSONException, IOException {
assertEquals("Class.java", stackframeJson.get("file"));
assertEquals(123, stackframeJson.get("lineNumber"));
}

@Test
public void testCustomExceptionSerialization() throws JSONException, IOException {
Exceptions exceptions = new Exceptions(config, new CustomException("Failed serialization"));

JSONObject exceptionJson = streamableToJsonArray(exceptions).getJSONObject(0);
assertEquals("CustomizedException", exceptionJson.get("errorClass"));
assertEquals("Failed serialization", exceptionJson.get("message"));

JSONObject stackframeJson = exceptionJson.getJSONArray("stacktrace").getJSONObject(0);
assertEquals("MyFile.run", stackframeJson.get("method"));
assertEquals("MyFile.java", stackframeJson.get("file"));
assertEquals(408, stackframeJson.get("lineNumber"));
assertEquals(18, stackframeJson.get("offset"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ public class BugsnagException extends Throwable {
/**
* The name of the exception (used instead of the exception class)
*/
private final String name;
private String name;
private String message;

private String type;

Expand All @@ -27,11 +28,24 @@ public BugsnagException(@NonNull String name,
@NonNull String message,
@NonNull StackTraceElement[] frames) {
super(message);

super.setStackTrace(frames);
setStackTrace(frames);
this.name = name;
}

BugsnagException(@NonNull Throwable exc) {
super(exc.getMessage());

if (exc instanceof BugsnagException) {
this.message = ((BugsnagException) exc).getMessage();
this.name = ((BugsnagException) exc).getName();
this.type = ((BugsnagException) exc).getType();
} else {
this.name = exc.getClass().getName();
}
setStackTrace(exc.getStackTrace());
initCause(exc.getCause());
}

/**
* @return The name of the exception (used instead of the exception class)
*/
Expand All @@ -40,11 +54,38 @@ public String getName() {
return name;
}

/**
* Sets the name of the error displayed in the bugsnag dashboard
*
* @param name the new name
*/
public void setName(@NonNull String name) {
this.name = name;
}

/**
* @return The error message, which is the exception message by default
*/
@NonNull
public String getMessage() {
return message != null ? message : super.getMessage();
}

/**
* Sets the message of the error displayed in the bugsnag dashboard
*
* @param message the new message
*/
public void setMessage(@NonNull String message) {
this.message = message;
}

@NonNull
String getType() {
return type;
}

void setType(String type) {
void setType(@NonNull String type) {
this.type = type;
}
}
35 changes: 25 additions & 10 deletions bugsnag-android-core/src/main/java/com/bugsnag/android/Error.java
Original file line number Diff line number Diff line change
Expand Up @@ -44,18 +44,23 @@ public class Error implements JsonStream.Streamable {
private String[] projectPackages;
private final Exceptions exceptions;
private Breadcrumbs breadcrumbs;
private final Throwable exception;
private final BugsnagException exception;
private final HandledState handledState;
private final Session session;
private final ThreadState threadState;
private boolean incomplete = false;

Error(@NonNull Configuration config, @NonNull Throwable exception,
Error(@NonNull Configuration config, @NonNull Throwable exc,
HandledState handledState, @NonNull Severity severity,
Session session, ThreadState threadState) {
this.threadState = threadState;
this.config = config;
this.exception = exception;

if (exc instanceof BugsnagException) {
this.exception = (BugsnagException) exc;
} else {
this.exception = new BugsnagException(exc);
}
this.handledState = handledState;
this.severity = severity;
this.session = session;
Expand Down Expand Up @@ -311,20 +316,30 @@ public void setMetaData(@NonNull MetaData metaData) {
*/
@NonNull
public String getExceptionName() {
if (exception instanceof BugsnagException) {
return ((BugsnagException) exception).getName();
} else {
return exception.getClass().getName();
}
return exception.getName();
}

/**
* Sets the class name from the exception contained in this Error report.
*/
public void setExceptionName(@NonNull String exceptionName) {
exception.setName(exceptionName);
}

/**
* Get the message from the exception contained in this Error report.
*/
@NonNull
public String getExceptionMessage() {
String localizedMessage = exception.getLocalizedMessage();
return localizedMessage != null ? localizedMessage : "";
String msg = exception.getMessage();
return msg != null ? msg : "";
}

/**
* Sets the message from the exception contained in this Error report.
*/
public void setExceptionMessage(@NonNull String exceptionMessage) {
exception.setMessage(exceptionMessage);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@
* Unwrap and serialize exception information and any "cause" exceptions.
*/
class Exceptions implements JsonStream.Streamable {
private final Throwable exception;
private final BugsnagException exception;
private String exceptionType;
private String[] projectPackages;

Exceptions(Configuration config, Throwable exception) {
Exceptions(Configuration config, BugsnagException exception) {
this.exception = exception;
exceptionType = Configuration.DEFAULT_EXCEPTION_TYPE;
projectPackages = config.getProjectPackages();
Expand All @@ -39,7 +39,7 @@ public void toStream(@NonNull JsonStream writer) throws IOException {
writer.endArray();
}

Throwable getException() {
BugsnagException getException() {
return exception;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package com.bugsnag.android

import android.app.ActivityManager
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.Mockito
import org.mockito.junit.MockitoJUnitRunner

@RunWith(MockitoJUnitRunner::class)
class AnrDetailsCollectorTest {

private companion object {
private const val PID_EXAMPLE = 5902
}

private val collector = AnrDetailsCollector()
private val stateInfo = ActivityManager.ProcessErrorStateInfo()
private lateinit var error: Error

@Mock
lateinit var am: ActivityManager

@Before
fun setUp() {
stateInfo.pid = PID_EXAMPLE
stateInfo.tag = "com.bugsnag.android.example/.ExampleActivity"
stateInfo.shortMsg = "ANR Input dispatching timed out"
stateInfo.longMsg = "ANR in com.bugsnag.android.example"

error = Error.Builder(
Configuration("f"),
RuntimeException(),
null,
Thread.currentThread(),
true
).build()
}

@Test
fun exceptionReturnsNull() {
Mockito.`when`(am.processesInErrorState).thenThrow(RuntimeException())
assertNull(collector.captureProcessErrorState(am, 0))
}

@Test
fun emptyListReturnsNull() {
Mockito.`when`(am.processesInErrorState).thenReturn(listOf())
assertNull(collector.captureProcessErrorState(am, 0))
}

@Test
fun differentPidReturnsNull() {
Mockito.`when`(am.processesInErrorState).thenReturn(listOf(stateInfo))
val captureProcessErrorState = collector.captureProcessErrorState(am, PID_EXAMPLE)
assertEquals(stateInfo, captureProcessErrorState)
fractalwrench marked this conversation as resolved.
Show resolved Hide resolved
}

@Test
fun samePidReturnsObj() {
val second = ActivityManager.ProcessErrorStateInfo()
Mockito.`when`(am.processesInErrorState).thenReturn(listOf(stateInfo, second))
val captureProcessErrorState = collector.captureProcessErrorState(am, PID_EXAMPLE)
assertEquals(stateInfo, captureProcessErrorState)
}

@Test
fun anrDetailsAltered() {
collector.mutateError(error, stateInfo)
assertEquals(stateInfo.shortMsg, error.exceptionMessage)
}
}
Loading