Трассировка стека группы Java/Android в уникальные ведра
При регистрации трассировки стека для необработанных исключений в Java или Android (например, через ACRA) вы обычно получаете трассировку стека в виде простой длинной строки.
Теперь все службы, предоставляющие отчеты и анализ сбоев (например, Google Play Developer Console, Crashlytics), группируют эти трассировки стека в уникальные ведра. Это, очевидно, полезно - иначе вы могли бы иметь десятки тысяч отчетов о сбоях в своем списке, но только дюжина из них могут быть уникальными.
Пример:
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)
...
Трассировка стека выше может отображаться в нескольких вариантах, например. классы платформы, такие как AsyncTask
, могут появляться с различными номерами строк из-за разных версий платформы.
Какой лучший способ получить уникальный идентификатор для каждого отчета о сбоях?
Ясно, что при каждой новой версии приложения, которую вы публикуете, отчеты о сбоях должны обрабатываться отдельно, потому что скомпилированный источник отличается. В ACRA вы можете использовать поле APP_VERSION_CODE
.
Но в противном случае, как вы определяете отчеты с уникальными причинами? Выбрав первую строку и выполнив поиск первого вхождения пользовательского (не-платформенного) класса и просмотрев файл и номер строки?
Ответы
Ответ 1
Если вы ищете способ получить уникальное значение для исключений при игнорировании классов, специфичных для ОС, вы можете выполнять итерацию getStackTrace()
и хеш для каждого фрейма, который не относится к известному классу ОС. Я думаю, что имеет смысл добавить исключение причины в хэш. Он может создавать некоторые ложные негативы, но это было бы лучше, чем ложные срабатывания, если исключение, которое вы используете, является чем-то общим, например 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;
}
}
Ответ 2
Я думаю, что вы уже знаете ответ, но вы ищете подтверждение, возможно. Вы уже намекали на это...
Если вы обязуетесь делать четкое различие между Исключением и его причиной /Stacktrace, тогда ответ может стать проще понять.
Чтобы дважды проверить свой ответ, я просмотрел наши отчеты о сбоях приложений для Android в Crittercism - аналитической компании, с которой я уважаю и работаю. (Кстати, я работаю в PayPal, и я использовал один из своих продуктов для Android, а Crittercism был одним из наших предпочтительных способов отчетности и анализа сбоев).
То, что я видел, было именно тем, что вы подразумевали в своем вопросе. То же самое исключение, встречающееся в одной строке кода (что означает одну и ту же версию приложения), однако, на разных версиях платформы (что означает разные компиляции Java/Android) записывается как два уникальных сбоя. И я думаю, что что вы ищете.
Мне жаль, что я не могу скопировать в него отчеты о сбоях, но я думаю, что меня уволят за это:) вместо этого я дам вам цензурные данные:
A java.lang.NullPointerException
произошло в классе ICantSayTheControllerName.java
в строке 117 версии 2.4.8 нашего приложения; но в двух разных (уникальных) группировках этих состояний сбоев, для тех пользователей, которые используют устройство Android 4.4.2, причина была на android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2540)
, однако для пользователей, использующих Android 4.4.4, причина была на android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2404)
. * обратите внимание на тонкие различия в количестве строк в ActivityThread.java из-за различной компиляции платформы.
Это обеспечило мне, что номер версии приложения, исключение и причина/стоп-трасса - это три значения того, что делает уникальный идентификатор конкретного сбоя; другими словами, группировка отчетов о сбоях производится на основе уникальных значений этих трех данных. Я почти хочу создать базу данных и аналогию с первичным ключом, но я отвлекся.
Кроме того, я принял Crittercism в качестве примера, потому что это то, что они делают; они в значительной степени являются отраслевым стандартом; Я считаю, что они делают это, по крайней мере, наравне с другими лидерами в отчетах и анализе сбоев. (и я не работаю для них).
Я надеюсь, что этот реальный пример прояснит или подтвердит ваши мысли.
-serkan
Ответ 3
Я знаю, что это не серебряная пуля, а только мои 2 цента:
- все исключения в моих проектах расширяются
abstract class AppException
- все другие исключения платформы (RuntimeException, IOException...) завернуты в
AppException
до отправки отчета или записи в файл.
Класс AppException выглядит следующим образом:
public abstract class AppException extends Exception {
private AppClientInfo appClientInfo; // BuildVersion, AndroidVersion etc...
[...] // other stuff
}
-
тогда я создаю ExceptionReport
из AppException
и отправляю его на свой сервер (как json/xml)
ExceptionReport содержит следующие данные:
- appClientInfo
- тип исключения//ui, database, webservice, preferences...
- origin//получить начало из stacktrace: MainActivity: 154
- stacktrace как html//выделены все строки, начинающиеся с "com.mycompany.myapp".
Теперь на стороне сервера я могу сортировать, группировать (игнорировать дубликаты) и публиковать отчет. Если тип исключения критический, может быть создан новый билет.
Как распознать дубликаты?
Пример:
- appClientInfo:
"android" : "4.4.2", "appversion" : "2.0.1.542"
- тип исключения:
"type" : "database"
- происхождение:
"SQLiteProvider.java:423"
Теперь я могу рассчитать уникальный идентификатор наивным образом:
UID = HASH("4.4.2" + "2.0.1.542" + "database" + "SQLiteProvider.java:423")