Group Java/Android stack traces into unique buckets
Asked Answered
B

3

12

When logging stack traces for unhandled exceptions in Java or Android (e.g. via ACRA), you usually get the stack trace as a plain long string.

Now all services that offer crash reporting and analysis (e.g. Google Play Developer Console, Crashlytics) group those stack traces into unique buckets. This is obviously helpful -- otherwise, you could have tens of thousands of crash reports in your list, but only a dozen of them may be unique.

Example:

java.lang.RuntimeException: An error occured while executing doInBackground()
at android.os.AsyncTask$3.done(AsyncTask.java:200)
at java.util.concurrent.FutureTask$Sync.innerSetException(FutureTask.java:274)
at java.util.concurrent.FutureTask.setException(FutureTask.java:125)
at java.util.concurrent.FutureTask$Sync.innerRun(FutureTask.java:308)
at java.util.concurrent.FutureTask.run(FutureTask.java:138)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1088)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:581)
at java.lang.Thread.run(Thread.java:1027)
Caused by: java.lang.ArrayIndexOutOfBoundsException
at com.my.package.MyClass.i(SourceFile:1059)
...

The stack trace above may appear in multiple variants, e.g. the platform classes like AsyncTask may appear with varying line numbers due to different platform versions.

What's the best technique to get a unique identifier for every crash report?

What's clear is that with every new application version that you publish, crash reports should be handled separatedly, because the compiled source is different. In ACRA, you can consider using the field APP_VERSION_CODE.

But otherwise, how do you identify reports with unique causes? By taking the first line and searching for the first occurrence of a custom (non-platform) class and looking up the file and line number?

Borstal answered 15/3, 2015 at 16:43 Comment(2)
Good question. If you get a good answer we can fold it into ACRAExtrorse
@Extrorse That would be great! You may check whether any of the answers here or my library work for ACRA. For me, none of the answers worked "out of the box", but the ideas were enough to make my library work: JavaCrashId.from(exception) plus the app version code seem to be working for me as a crash fingerprint.Borstal
L
5

If you're looking for a way to get a unique value for exceptions while ignoring OS specific classes, you can iterate getStackTrace() and hash every frame that's not from a known OS class. I think it also makes sense to add the cause exception to the hash. It may create some false negatives, but that would be better than having false positives if the exception you hash is something generic like ExecutionException.

import com.google.common.base.Charsets;
import com.google.common.hash.HashCode;
import com.google.common.hash.Hasher;
import com.google.common.hash.Hashing;

public class Test
{

    // add more system packages here
    private static final String[] SYSTEM_PACKAGES = new String[] {
        "java.",
        "javax.",
        "android."
    };

    public static void main( String[] args )
    {
        Exception e = new Exception();
        HashCode eh = hashApplicationException( e );
        System.out.println( eh.toString() );
    }

    private static HashCode hashApplicationException( Throwable exception )
    {
        Hasher md5 = Hashing.md5().newHasher();
        hashApplicationException( exception, md5 );
        return md5.hash();
    }

    private static void hashApplicationException( Throwable exception, Hasher hasher )
    {
        for( StackTraceElement stackFrame : exception.getStackTrace() ) {
            if( isSystemPackage( stackFrame ) ) {
                continue;
            }

            hasher.putString( stackFrame.getClassName(), Charsets.UTF_8 );
            hasher.putString( ":", Charsets.UTF_8 );
            hasher.putString( stackFrame.getMethodName(), Charsets.UTF_8 );
            hasher.putString( ":", Charsets.UTF_8 );
            hasher.putInt( stackFrame.getLineNumber() );
        }
        if( exception.getCause() != null ) {
            hasher.putString( "...", Charsets.UTF_8 );
            hashApplicationException( exception.getCause(), hasher );
        }
    }

    private static boolean isSystemPackage( StackTraceElement stackFrame )
    {
        for( String ignored : SYSTEM_PACKAGES ) {
            if( stackFrame.getClassName().startsWith( ignored ) ) {
                return true;
            }
        }

        return false;
    }
}
Laresa answered 24/3, 2015 at 0:31 Comment(2)
Thanks! While this does not work as it is (e.g. the continue in the nested loop), it proved to be the most valuable starting point. I've created a library for Java and PHP with the ideas from your answer: github.com/delight-im/Java-Crash-IDBorstal
Thanks. I updated the code in case some one else will try to use it.Laresa
E
4

I think you already know the answer, but you're looking for a confirmation perhaps. You've already hinted it...

If you commit to a making clear distinction between an Exception and its Cause/Stacktrace, then, the answer may become simpler to grasp.

In order to double check my answer, I looked through our Android application crash reports in Crittercism - an analytics company which I respect and work with. (Btw, I work for PayPal and I used to lead one of their Android products and Crittercism was one of our preferred way of reporting and analyzing crashes).

What I saw was exactly what you implied in your question. The same exception occurring on the same line of code (meaning the same application version) however on different versions of the platform (meaning different Java/Android compilations) is recorded as two unique crashes. And I think that's what you're looking for.

I wish I could copy paste the crash reports here but I think I would get fired for that :) instead I'll give you the censored data:

A java.lang.NullPointerException happened in ICantSayTheControllerName.java class on line 117 of 2.4.8 version of our application; but in two different (unique) grouping of this crash states, for those users using Android 4.4.2 device, the cause was on android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2540) however for those users using Android 4.4.4 the cause was on android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2404). *note the subtle differences in line number in ActivityThread.java due to different compilation of the platform.

This ensured me that App Version Number, Exception and the Cause/Stacktrace are the three values of what makes an unique identifier of a particular crash; in other words, grouping of crash reports are done based on unique values of these three information. I almost want to make a database and a primary-key analogy but I digress.

Also, I took Crittercism as an example because this is what they do; they're pretty much an industry standard; I believe what they do is at least at par with other leaders in crash reporting and analytics. (and no I don't work for them).

I hope this real-world example clarifies or confirms your thoughts.

-serkan

Exanthema answered 19/3, 2015 at 7:3 Comment(1)
Thanks, serkan! You're right, I was already quite sure that these are the factors that make a unique crash report. Thus the question was more about how to actually get those values that decide if a crash report is unique or not. That means: Different app versions make different crash reports, this is obvious. But from the stack trace itself, how do you infer if this trace is new or not? Which lines to you look at? The ones at the top? Or the ones in Caused by ...? Or only the ones from your one package?Borstal
L
0

I know that is not the silver bullet, but just my 2 cents:

  1. all exceptions in my projects extends abstract class AppException
  2. all other platform exceptions (RuntimeException, IOException...) are wrapped by AppException before the report is sent or logged to file.

AppException class looks like this:

public abstract class AppException extends Exception {

    private AppClientInfo appClientInfo; // BuildVersion, AndroidVersion etc...

    [...] // other stuff
}
  1. then i create an ExceptionReport from AppException and send it to my server (as json/xml) ExceptionReport contains the following data:

    • appClientInfo
    • type of exception // ui, database, webservice, preferences...
    • origin // get origin from stacktrace: MainActivity:154
    • stacktrace as html // all lines which start with "com.mycompany.myapp" are highlighted.

Now on the server side i can sort, group (ignore duplicates) and publish the report. If type of exception is critical, a new ticket can be created.


How do i recognize the duplicates?

Example:

  • appClientInfo: "android" : "4.4.2", "appversion" : "2.0.1.542"
  • type of exception: "type" : "database"
  • origin: "SQLiteProvider.java:423"

Now i can calculate the unique ID in this naive fashion:

UID = HASH("4.4.2" + "2.0.1.542" + "database" + "SQLiteProvider.java:423")
Loiret answered 24/3, 2015 at 18:28 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.