我正在实施Firebase Crashlytics来为Android应用程序提供崩溃报告,并且偶然发现了他们的文档以自定义报告 。
具有非致命异常的 其他日志记录和用户信息之类的功能很棒,但是我想使用的是自定义键 ,可以在异常发生时用于记录应用程序状态。 真正的好处是状态信息清晰地显示在Firebase控制台中 。
我实施崩溃报告的要求是:
- 崩溃报告仅适用于发行版,因为您确实不希望在调试和开发阶段使用错误来污染Crashlytics(尤其是因为您无法删除它们)。
- 也可以报告非致命异常。 当然,您不需要报告所有异常,可能只需要报告诸如应用程序处于意外状态或逻辑错误等情况。
- 在开发过程中,异常将照常记录到LogCat中。
- 为了以一种干净的方式实现它,因此将来的更改不一定会遍及整个代码库。
以下是一些可能的方法:
1.使用Crashlytics API
仅在需要的地方调用Crashlytics API怎么样?
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);
}
因为我们只希望发布版本的崩溃报告,所以我们只需要针对发布版本有条件地调用它。
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);
}
}
在开发过程中,我们仍然要记录异常。 对?
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.Tree仅用于发行版本。 这个想法已经在各种博客中讨论过,并且基于Timber示例应用程序 。
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);
}
}
}
因此,对于调试版本,您将植入调试树。
Timber.plant(new Timber.DebugTree());
对于发行版,请植入使用Crashlytics的Tree。
Timber.plant(new ReleaseTree());
这里的问题是,重写的日志方法仅传递String消息,并且(可能)传递Throwable。 那么我们如何传递其他数据,例如自定义键状态和用户信息等。
好吧,我想我们可以创建一个包含所有信息的格式化字符串(例如JSON或您自己的自定义格式),然后将其传递给String消息参数。 但是,仅将格式化的String传递给Crashlytics时,最终会在控制台中显示一条长日志消息,您必须对其进行解密,并且您不会在该漂亮的整洁表中获得状态信息。
例如,您必须像这样解密一个长字符串
|key1=test string|key2=true|key3=1|key4=2.1|key5=3.0|...|
(不要这样做,这只是一个简单的示例)
代替:
如果要充分利用Crashlytics API进行自定义报告 ,则必须解析消息字符串并进行适当的Crashlytics调用。
这是可行的,但由于需要格式化和解析自定义数据字符串,因此又变得有些混乱。
3.只需登录
由于我已经想根据构建将日志记录到某个地方,我可以将Crashlytics API合并到日志记录中吗?
无论如何,我一直在使用SLF4J进行日志记录,在我的情况下是SLF4J-Timber库(但对于SLF4J-Android也可以使用)。
Logger log = LoggerFactory.getLogger(this.getClass());
try {
// some operation
} catch (Exception ex) {
log.error("message", ex);
}
我无法扩展从LoggerFactory检索到的SLF4J Logger ,但可以使用装饰器模式为该Logger创建包装器。
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信息。
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 -> Crashlytics.setString(entry.getKey(), entry.getValue()));
}
}
不幸的是,这可能有点混乱,因为您必须为每种类型的状态数据(字符串,布尔值,整数,双精度,浮点)传递不同的参数。 这样一来,您便知道是否要调用Crashlytics.setString(),Crashlytics.setBool(),Crashlytics.setInt()等。
如何将自定义数据封装到数据类中,这样我们只需要传递一个参数即可。
这是我使用的一个:
/**
* 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调用即可。
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。
private static final String MAP_FORMAT_STRING = "{} = {}";
// additional methods for error reporting
public void error(ErrorReport errorReport)
{
errorReport.getStateString().entrySet().stream().forEach(entry -> getLogger().error(MAP_FORMAT_STRING, entry.getKey(), entry.getValue()));
errorReport.getStateBool().entrySet().stream().forEach(entry -> getLogger().error(MAP_FORMAT_STRING, entry.getKey(), entry.getValue()));
errorReport.getStateInt().entrySet().stream().forEach(entry -> getLogger().error(MAP_FORMAT_STRING, entry.getKey(), entry.getValue()));
errorReport.getStateDouble().entrySet().stream().forEach(entry -> getLogger().error(MAP_FORMAT_STRING, entry.getKey(), entry.getValue()));
errorReport.getStateFloat().entrySet().stream().forEach(entry -> getLogger().error(MAP_FORMAT_STRING, entry.getKey(), entry.getValue()));
errorReport.getMessages().stream().forEach(m -> getLogger().error(m));
errorReport.getException().ifPresent(ex -> getLogger().error("Exception:", ex));
}
现在,我只需要使用日志包装器子类而不是SLF4J Logger。
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 。
<meta-data android:name="firebase_crash_collection_enabled" android:value="false" />
同样由您决定如何分离类的不同版本以用于不同的版本。 我只是使用不同的源集进行发布和调试。
翻译自: https://www.javacodegeeks.com/2019/03/crashlytics-android-clean-way-custom-crash-reports.html