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?
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;
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With