Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Group Java/Android stack traces into unique buckets

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?

like image 448
caw Avatar asked Mar 15 '15 16:03

caw


1 Answers

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;
    }
}
like image 145
kichik Avatar answered Oct 11 '22 12:10

kichik