Трассировка стека группы 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
}
  1. тогда я создаю 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")