Home » Android » android – How to effectively group non fatal exceptions in Crashlytics (Fabrics)?

android – How to effectively group non fatal exceptions in Crashlytics (Fabrics)?

Posted by: admin June 15, 2020 Leave a comment

Questions:

We are using Crashlytics in our app as the crash reporting tool.
For Android native crashes, it’s working fine and grouping the crashes correctly.
Our app also has few components in react-native. For the crashes which occur in these components, we catch them and then log them to Crashlytics as non-fatal exceptions.

public class PlatformNativeModuleCallExceptionhandler implements 
NativeModuleCallExceptionHandler {
@Override
public void handleException(Exception e) {
    try {
        .
        .
        .
        Crashlytics.logException(new Exception(exceptionString));
    } catch (Exception ex) {}
}

Crashes are getting logged in Crashlytics dashboard, but it’s showing all the crashes inside a single tab. These might be different crashes of the same or different react-native components.

enter image description here

Due to this we are not able to find out the instances of a particular crash. Need to manually go through each instance of the crash.

I guess it takes the name of the class where exception gets created, in this case PlatformNativeModuleCallExceptionHandler.
I tried creating my own custom exception class but that also did not help.

Does anybody know how we can group the non fatal exceptions better here?
All the similar crashes should be grouped together with their total instances.

How to&Answers:

Crashlytics uses the method and crash line number to group crashes, so if you have an exception handler method for all of your non-fatals, they’ll be grouped together. There isn’t currently a workaround for this.

Answer:

Crashlytics groups by the line number that the exception was generated on and labels it with the exception type. If you know all the types of the exceptions you can generate each one on a different line. And you could also map your strings to custom Exception types to make it more easy to identify them in Crashlytics.

Here’s an example:

public void crashlyticsIsGarbage(String exceptionString) {
    Exception exception = null;
    switch(exceptionString) {
        case "string1": exception = new String1Exception(exceptionString);
        case "string2": exception = new String2Exception(exceptionString);
        case "string3": exception = new String3Exception(exceptionString);
        case "string4": exception = new String4Exception(exceptionString);
        default: exception = new Exception(exceptionString);
    }
    Crashlytics.logException(exception);
}

class String1Exception extends Exception { String1Exception(String exceptionString) { super(exceptionString); } }
class String2Exception extends Exception { String2Exception(String exceptionString) { super(exceptionString); } }
class String3Exception extends Exception { String3Exception(String exceptionString) { super(exceptionString); } }
class String4Exception extends Exception { String4Exception(String exceptionString) { super(exceptionString); } }

BTW, Crashlytics will ignore the message string in the Exception.

Answer:

I resolved this by setting a custom stack trace to the exception. A new Exception(exceptionMessage) will create the exception there itself, what we did was to throw an exception which in catch called my counterpart of handleException() with the actual stack trace furnished in the exceptionMessage. Some parsing and the exceptionMessage can be used to set the stack trace on the newly created exception using exception.setStackTrace(). Actually, this was required in my project only because it is cross-language, for regular projects, simply passing the exception thrown and caught at the place of interest should work.

Answer:

Best way I’ve found to do this is to manually chop the shared parts of the stacktrace off:


private fun buildCrashlyticsSyntheticException(message: String): Exception {
  val stackTrace = Thread.currentThread().stackTrace
  val numToRemove = 8
  val lastToRemove = stackTrace[numToRemove - 1]
  // This ensures that if the stacktrace format changes, we get notified immediately by the app
  // crashing (as opposed to silently mis-grouping crashes for an entire release).
  check(lastToRemove.className == "timber.log.Timber" && lastToRemove.methodName == "e",
    { "Got unexpected stacktrace: $stackTrace" })
  val abbreviatedStackTrace = stackTrace.takeLast(stackTrace.size - numToRemove).toTypedArray()
  return SyntheticException("Synthetic Exception: $message", abbreviatedStackTrace)
}

class SyntheticException(
  message: String,
  private val abbreviatedStackTrace: Array<StackTraceElement>
) : Exception(message) {
  override fun getStackTrace(): Array<StackTraceElement> {
    return abbreviatedStackTrace
  }
}

This way the message can be parameterized Timber.e("Got a weird error $error while eating a taco") and all of that line’s calls will be grouped together.

Obviously, numToRemove will need to change depending on your exact mechanism for triggering nonfatals.

Answer:

I was looking into this just now, because the documentation says:

Logged Exceptions are grouped by Exception type and message.

Warning:
Developers should avoid using unique values, such as user ID, product ID, and timestamps, in the Exception message field. Using unique values in these fields will cause a high cardinality of issues to be created. In this case, Crashlytics will limit the reporting of logged errors in your app. Unique values should instead be added to Logs and Custom Keys.

But my experience was different. From what I found out, what Alexizamerican said in his answer is true, with a small caveat:

Issues are grouped by the method and line where the exception was created, with the caveat that it is the root cause of the exception that is being taken into account here.

By root cause I mean this:

public static Throwable getRootCause(Throwable throwable) {
    Throwable cause = throwable;
    while (cause.getCause() != null) {
        cause = cause.getCause();
    }
    return cause;
}

Therefore, if you did:

@Override
public void handleException(Exception e) {
    // ...
    Crashlytics.logException(e);
}

That should correctly group the exceptions together.

Furthermore, if you did:

@Override
public void handleException(Exception e) {
    // ...
    Crashlytics.logException(new Exception(exceptionString, e));
}

That would also correctly group the exceptions, because it would look at e or its cause, or the cause of that, and so on, until it reaches an exception that doesn’t have any other cause, and look at the stack trace where it was created.

Finally, unlike what miguel said, exception type or message doesn’t affect grouping at all in my experience. If you have FooException("foo") at some particular line in a particular method, and you replace it with BarException("bar"), the two will be grouped together, because the line and method didn’t change.