Crashlytics和Android:使用自定义崩溃报告的干净方法?

我正在实施Firebase Crashlytics来为Android应用程序提供崩溃报告,并且偶然发现了他们的文档以自定义报告

具有非致命异常的 其他日志记录用户信息之类的功能很棒,但是我想使用的是自定义键 ,可以在异常发生时用于记录应用程序状态。 真正的好处是状态信息清晰地显示在Firebase控制台中

Crashlytics和Android

我实施崩溃报告的要求是:

  • 崩溃报告仅适用于发行版,因为您确实不希望在调试和开发阶段使用错误来污染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和Android

如果要充分利用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 -&gt; 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 -&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。

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值