Статьи

Crashlytics и Android: чистый способ использования пользовательских отчетов о сбоях?

Я внедрял Firebase Crashlytics для отчетов о сбоях для приложения Android и наткнулся на их документацию для настройки отчетов .

Замечательно иметь такие вещи, как дополнительное ведение журнала и информацию о пользователях для нефатальных исключений , но я хотел использовать пользовательские ключи, которые можно было использовать для регистрации состояния приложения при возникновении исключения. Что действительно приятно, так это то, что информация о состоянии четко отображается в консоли Firebase .

Crashlytics и Android

Мои требования для реализации отчетов о сбоях были:

  • Отчеты о сбоях предназначены только для сборки выпуска, так как вы не хотите загрязнять Crashlytics ошибками на этапе отладки и разработки (тем более, что вы не можете их удалить).
  • Не смертельные исключения также могут быть зарегистрированы. Конечно, вам не нужно сообщать о каждом исключении, возможно, просто обстоятельства, такие как приложение оставлено в неожиданном состоянии или ошибка в логике и т. Д.
  • Во время разработки исключения будут просто регистрироваться в LogCat, как обычно.
  • Чтобы реализовать его в чистом виде, чтобы будущие изменения не обязательно должны были колебаться по всей базе кода.

Вот несколько возможных способов сделать это:

1. Используйте Crashlytics API

Как насчет использования Crashlytics API там, где это требуется?

1
2
3
4
5
6
7
8
9
try {
  // some operation
} catch (Exception ex) {
  Crashlytics.setUserIdentifier("user id");
  Crashlytics.setString("param 1", "some value");
  Crashlytics.setInt("param 2", 10);
  Crashlytics.log("message");
  Crashlytics.logException(ex);
}

Поскольку мы хотим получать отчеты о сбоях только для релизной версии, нам нужно будет вызывать ее только для сборки выпуска.

01
02
03
04
05
06
07
08
09
10
11
try {
  // some operation
} catch (Exception ex) {
  if (!BuildConfig.DEBUG) {
    Crashlytics.setUserIdentifier("test id");
    Crashlytics.setString("param 1", "some value");
    Crashlytics.setInt("param 2", 10);
    Crashlytics.log("message");
    Crashlytics.logException(ex);
  }
}

Что касается разработки, мы все еще хотим записать исключение. Правильно?

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
try {
  // some operation
} catch (Exception ex) {
  if (!BuildConfig.DEBUG) {
    Crashlytics.setUserIdentifier("test id");
    Crashlytics.setString("param 1", "some value");
    Crashlytics.setInt("param 2", 10);
    Crashlytics.log("message");
    Crashlytics.logException(ex);
  }
  else {
    // logging with SLF4J
    log.info("User ID=test id");
    log.info("param 1=some value");
    log.info("param 2= " + 10);
    log.error("message", ex);
  }
}

Наличие блоков кода, подобных этому, разбросанных по базе кодов, начинает выглядеть немного уродливо. А также, что если вы хотите заменить Crashlytics другим инструментом для создания отчетов о сбоях — требуется много изменений в коде!

2. Лесной путь

Если вы используете Timber для ведения журналов (как и я), другим способом будет инкапсуляция вызовов Crashlytics в Timber.Tree, которое устанавливается только для сборки выпуска. Эта идея уже обсуждалась в различных блогах и основана на примере приложения Timber .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
public class ReleaseTree extends Timber.Tree {
  @Override
  protected void log(int priority, @Nullable String tag, @NotNull String      message, @Nullable Throwable t) {
  
    // only pass on log level WARN or ERROR
    if (priority == Log.VERBOSE || priority == Log.DEBUG || priority == Log.INFO) {
      return;
    }
  
    Crashlytics.setInt("priority", priority);
    Crashlytics.setString("tag", tag);
    Crashlytics.setString("message", message);
  
    if (t == null) {
      Crashlytics.logException(new Exception(message));
    } else {
      Crashlytics.logException(t);
    }
  }
}

Таким образом, для отладочной сборки вы должны посадить дерево отладки.

1
Timber.plant(new Timber.DebugTree());

Для выпуска релиза посадите дерево, которое вместо этого использует Crashlytics.

1
Timber.plant(new ReleaseTree());

Проблема здесь в том, что переопределенный метод журнала передает только сообщение String и (возможно) Throwable. Итак, как бы мы передали дополнительные данные, такие как пользовательское состояние ключа и информация о пользователе и т. Д.

Ну, я полагаю, мы могли бы создать отформатированную строку (например, в формате JSON или свой собственный формат), содержащую всю эту информацию, и передать ее в параметре сообщения String. Но тогда простая передача отформатированной строки в Crashlytics приведет к длинному сообщению журнала в консоли, которое вам придется расшифровать, и вы не получите информацию о состоянии в этой красивой аккуратной таблице.

Например, вам придется расшифровать длинную строку, как это

1
|key1=test string|key2=true|key3=1|key4=2.1|key5=3.0|...|

(не делайте этого, это простой пример)

вместо:

Crashlytics и Android

Если вы хотите в полной мере использовать API Crashlytics для создания пользовательских отчетов , вам придется проанализировать строку сообщения и выполнить соответствующие вызовы Crashlytics.

Это выполнимо, но начинает снова становиться немного запутанным из-за необходимости форматировать и анализировать строку пользовательских данных.

3. Просто войдите

Поскольку я уже хочу регистрировать исключение где-нибудь в зависимости от сборки, могу ли я просто включить API-интерфейс Crashlytics в протоколирование?

В любом случае я использовал SLF4J для регистрации, в моем случае библиотеку SLF4J-Timber (но она также будет работать для SLF4J-Android ).

1
2
3
4
5
6
7
Logger log = LoggerFactory.getLogger(this.getClass());
  
try {
  // some operation
} catch (Exception ex) {
  log.error("message", ex);
}

Я не могу расширить регистратор SLF4J , полученный из LoggerFactory, но я могу использовать шаблон декоратора для создания оболочки для этого регистратора.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public abstract class LoggerWrapper {
  
  private final Logger log;
  
  public LoggerWrapper(Class clazz)
  {
    log = LoggerFactory.getLogger(clazz);
  }
  
  public LoggerWrapper(String className)
  {
    log = LoggerFactory.getLogger(className);
  }
  
  public Logger getLogger()
  {
    return log;
  }
  
  public void trace(String msg)
  {
    log.trace(msg);
  }
  
  public void trace(String format, Object arg)
  {
    log.trace(format, arg);
  }
  
  // all the other delegated log methods
.
  
.
  
.
  
}

(Подсказка: в Android Studio для обработки делегирования методов в классе оболочки просто используйте Code -> Delegate Methods… для генерации кода, и вам нужно только делегировать методы, которые вы собираетесь использовать.)

Теперь я могу создать подкласс оболочки журнала и добавить дополнительные методы ошибок для передачи пользовательской информации о Crashlytics.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
public class ErrorLogger extends LoggerWrapper {
  
  public ErrorLogger(Class clazz) {
    super(clazz);
  }
  
  public ErrorLogger(String className) {
    super(className);
  }
  
  // additional methods for custom crash reporting
  
  @Override
  public void error(String userId, Map<String, String> stringState,   Map<String, Boolean> boolState, Map<String, Integer> intState, Map<String, Double> doubleState, Map<String, Float> floatState, List<String> messages, Throwable t)
  {
    // Crashlytics calls here using data from the parameters
    // e.g.
    // stringState.entrySet().stream().forEach(entry -&gt; Crashlytics.setString(entry.getKey(), entry.getValue()));
  }
}

К сожалению, это может быть немного грязно, так как вам придется передавать разные параметры для каждого типа данных состояния (String, Boolean, Integer, Double, Float). Это делается для того, чтобы вы знали, вызывать ли Crashlytics.setString (), Crashlytics.setBool (), Crashlytics.setInt () и т. Д.

Как насчет инкапсуляции этих пользовательских данных в класс данных, чтобы нам нужно было передать только один параметр.

Вот тот, который я использую:

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
/**
 * Attempt at generic non-fatal error report, modelled on Crashlytics error reporting methods.
 */
public class ErrorReport {
 
    private String id; // could be user ID, for instance
    private final Map<String, String> stateString;
    private final Map<String, Boolean> stateBool;
    private final Map<String, Integer> stateInt;
    private final Map<String, Double> stateDouble;
    private final Map<String, Float> stateFloat;
    private final List<String> messages;
    private Throwable exception;
 
    public ErrorReport() {
        stateString = new HashMap<>();
        stateBool = new HashMap<>();
        stateInt = new HashMap<>();
        stateDouble = new HashMap<>();
        stateFloat = new HashMap<>();
        messages = new ArrayList<>();
    }
 
    public Optional<String> getId() {
        return Optional.ofNullable(id);
    }
 
    public ErrorReport setId(String id) {
        this.id = id;
        return this;
    }
 
    public Map<String, String> getStateString() {
        return Collections.unmodifiableMap(stateString);
    }
 
    public Map<String, Boolean> getStateBool() {
        return Collections.unmodifiableMap(stateBool);
    }
 
    public Map<String, Integer> getStateInt() {
        return Collections.unmodifiableMap(stateInt);
    }
 
    public Map<String, Double> getStateDouble() {
        return Collections.unmodifiableMap(stateDouble);
    }
 
    public Map<String, Float> getStateFloat() {
        return Collections.unmodifiableMap(stateFloat);
    }
 
    public ErrorReport addState(String key, String value)
    {
        stateString.put(key, value);
        return this;
    }
 
    public ErrorReport addState(String key, Boolean value)
    {
        stateBool.put(key, value);
        return this;
    }
 
    public ErrorReport addState(String key, Integer value)
    {
        stateInt.put(key, value);
        return this;
    }
 
    public ErrorReport addState(String key, Double value)
    {
        stateDouble.put(key, value);
        return this;
    }
 
    public ErrorReport addState(String key, Float value)
    {
        stateFloat.put(key, value);
        return this;
    }
 
    public List<String> getMessages() {
        return Collections.unmodifiableList(messages);
    }
 
    public ErrorReport addMessage(String message)
    {
        messages.add(message);
        return this;
    }
 
    public ErrorReport setException(Throwable exception)
    {
        this.exception = exception;
        return this;
    }
 
    public Optional<Throwable> getException() {
        return Optional.ofNullable(exception);
    }
}

Теперь мне просто нужно передать это в подкласс оболочки журнала и выполнить соответствующие вызовы Crashlytics.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import com.crashlytics.android.Crashlytics;
 
public class ErrorLogger extends LoggerWrapper {
 
    public ErrorLogger(Class clazz) {
        super(clazz);
    }
 
    public ErrorLogger(String className) {
        super(className);
    }
 
    // additional methods for error reporting
 
    @Override
    public void error(ErrorReport errorReport)
    {
        // send non-fatal error to crash reporting
        errorReport.getId().ifPresent(id -> Crashlytics.setUserIdentifier(id));
        errorReport.getStateString().entrySet().stream().forEach(entry -> Crashlytics.setString(entry.getKey(), entry.getValue()));
        errorReport.getStateBool().entrySet().stream().forEach(entry -> Crashlytics.setBool(entry.getKey(), entry.getValue()));
        errorReport.getStateInt().entrySet().stream().forEach(entry -> Crashlytics.setInt(entry.getKey(), entry.getValue()));
        errorReport.getStateDouble().entrySet().stream().forEach(entry -> Crashlytics.setDouble(entry.getKey(), entry.getValue()));
        errorReport.getStateFloat().entrySet().stream().forEach(entry -> Crashlytics.setFloat(entry.getKey(), entry.getValue()));
        errorReport.getMessages().stream().forEach(m -> Crashlytics.log(m));
        errorReport.getException().ifPresent(ex -> Crashlytics.logException(ex));
    }
}

Конечно, для отладочной сборки у меня был бы другой подкласс оболочки журнала, где дополнительный метод журнала просто регистрирует данные в LogCat.

01
02
03
04
05
06
07
08
09
10
11
12
private static final String MAP_FORMAT_STRING = "{} = {}";
// additional methods for error reporting
public void error(ErrorReport errorReport)
{
  errorReport.getStateString().entrySet().stream().forEach(entry -&gt; getLogger().error(MAP_FORMAT_STRING, entry.getKey(), entry.getValue()));
  errorReport.getStateBool().entrySet().stream().forEach(entry -&gt; getLogger().error(MAP_FORMAT_STRING, entry.getKey(), entry.getValue()));
  errorReport.getStateInt().entrySet().stream().forEach(entry -&gt; getLogger().error(MAP_FORMAT_STRING, entry.getKey(), entry.getValue()));
  errorReport.getStateDouble().entrySet().stream().forEach(entry -&gt; getLogger().error(MAP_FORMAT_STRING, entry.getKey(), entry.getValue()));
  errorReport.getStateFloat().entrySet().stream().forEach(entry -&gt; getLogger().error(MAP_FORMAT_STRING, entry.getKey(), entry.getValue()));
  errorReport.getMessages().stream().forEach(m -&gt; getLogger().error(m));
  errorReport.getException().ifPresent(ex -&gt; getLogger().error("Exception:", ex));
}

Теперь мне просто нужно использовать мой подкласс оболочки журнала вместо SLF4J Logger.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
ErrorLogger log = new ErrorLogger(this.getClass());
// instead of ...
// Logger log = LoggerFactory.getLogger(this.getClass());
  
try {
  // some operation
} catch (Exception ex) {
  ErrorReport errorReport = new ErrorReport();
  errorReport.setId("test id")
     .addState("param 1", "some value")
     .addState("param 2", true)
     .addState("param 3", 10)
     .addMessage("message")
     .setException(ex);
  log.error(errorReport);
}

Это только один из возможных способов, которые я выбрал. Пожалуйста, дайте мне знать, если у кого-то есть другие хорошие способы сделать это.

Дополнительные вещи, чтобы сделать

Я сохранил примеры кода в этом посте настолько простыми, насколько это возможно, но вам, вероятно, придется внести дополнительные изменения, чтобы включить эти идеи. Например, вы можете отключить Crashlytics в отладочной сборке, либо программно, либо в манифесте.

1
<meta-data android:name="firebase_crash_collection_enabled" android:value="false" />

Также вам решать, как разделить разные версии классов для использования в разных сборках. Я просто использую разные исходники для выпуска и отладки.

Опубликовано на Java Code Geeks с разрешения Дэвида Вонга, партнера нашей программы JCG . См. Оригинальную статью здесь: Crashlytics и Android: чистый способ использования пользовательских отчетов о сбоях?

Мнения, высказанные участниками Java Code Geeks, являются их собственными.