如何在 Crashlytics (Fabrics) 中有效地对非致命异常进行分组?

How to effectively group non fatal exceptions in Crashlytics (Fabrics)?

我们在我们的应用程序中使用 Crashlytics 作为崩溃报告工具。 对于 Android 本机崩溃,它工作正常并且正确地对崩溃进行了分组。 我们的应用程序在 react-native 中的组件也很少。对于这些组件中发生的崩溃,我们会捕获它们,然后将它们作为非致命异常记录到 Crashlytics。

public class PlatformNativeModuleCallExceptionhandler implements 
NativeModuleCallExceptionHandler {
@Override
public void handleException(Exception e) {
    try {
        .
        .
        .
        Crashlytics.logException(new Exception(exceptionString));
    } catch (Exception ex) {}
}

崩溃记录在 Crashlytics 仪表板中,但它在单个选项卡中显示所有崩溃。这些可能是相同或不同的 react-native 组件的不同崩溃。

因此,我们无法找出特定崩溃的实例。需要手动检查每个崩溃实例。

我想它采用了创建异常的 class 的名称,在本例中为 PlatformNativeModuleCallExceptionHandler。 我尝试创建自己的自定义异常 class 但这也没有帮助。

有谁知道我们如何在这里更好地对非致命异常进行分组? 所有类似的崩溃应该与其总实例一起分组。

Crashlytics 使用方法和崩溃行号对崩溃进行分组,因此如果您有针对所有非致命事件的异常处理程序方法,它们将被分组在一起。目前没有解决方法。

Crashlytics 按生成异常的行号分组,并用异常类型对其进行标记。如果您知道异常的所有类型,您可以在不同的行上生成每一个。您还可以将字符串映射到自定义异常类型,以便在 Crashlytics 中更容易识别它们。

这是一个例子:

public void crashlyticsIsGarbage(String exceptionString) {
    Exception exception = null;
    switch(exceptionString) {
        case "string1": exception = new String1Exception(exceptionString);
        case "string2": exception = new String2Exception(exceptionString);
        case "string3": exception = new String3Exception(exceptionString);
        case "string4": exception = new String4Exception(exceptionString);
        default: exception = new Exception(exceptionString);
    }
    Crashlytics.logException(exception);
}

class String1Exception extends Exception { String1Exception(String exceptionString) { super(exceptionString); } }
class String2Exception extends Exception { String2Exception(String exceptionString) { super(exceptionString); } }
class String3Exception extends Exception { String3Exception(String exceptionString) { super(exceptionString); } }
class String4Exception extends Exception { String4Exception(String exceptionString) { super(exceptionString); } }

顺便说一句,Crashlytics 将忽略异常中的消息字符串。

我通过为异常设置自定义堆栈跟踪来解决这个问题。 new Exception(exceptionMessage) 将在那里自己创建异常,我们所做的是抛出一个异常,该异常在 catch 中调用了我的 handleException() 的对应物,并在 exceptionMessage 中提供了实际的堆栈跟踪。一些解析和 exceptionMessage 可用于使用 exception.setStackTrace() 在新创建的异常上设置堆栈跟踪。实际上,这在我的项目中是必需的,只是因为它是跨语言的,对于常规项目,只需传递在感兴趣的地方抛出和捕获的异常就可以了。

我刚才正在研究这个,因为 documentation says:

Logged Exceptions are grouped by Exception type and message.

Warning: Developers should avoid using unique values, such as user ID, product ID, and timestamps, in the Exception message field. Using unique values in these fields will cause a high cardinality of issues to be created. In this case, Crashlytics will limit the reporting of logged errors in your app. Unique values should instead be added to Logs and Custom Keys.

但我的经历不同。根据我的发现, 是正确的,但有一个小警告:

问题按创建异常的方法和行分组,需要注意的是这是此处考虑的异常的根本原因。

根本原因是指​​:

public static Throwable getRootCause(Throwable throwable) {
    Throwable cause = throwable;
    while (cause.getCause() != null) {
        cause = cause.getCause();
    }
    return cause;
}

因此,如果您这样做了:

@Override
public void handleException(Exception e) {
    // ...
    Crashlytics.logException(e);
}

这应该正确地将异常分组在一起。

此外,如果您这样做了:

@Override
public void handleException(Exception e) {
    // ...
    Crashlytics.logException(new Exception(exceptionString, e));
}

这也将正确地对异常进行分组,因为它会查看 e 或其原因,或该原因的原因,依此类推,直到它遇到没有任何其他原因的异常,并查看创建它的堆栈跟踪。

最后,与 不同,根据我的经验,异常类型或消息根本不影响分组。如果您在特定方法的某些特定行有 FooException("foo"),并且将其替换为 BarException("bar"),则两者将组合在一起,因为行和方法没有更改。

我发现这样做的最佳方法是手动切断堆栈跟踪的共享部分:


private fun buildCrashlyticsSyntheticException(message: String): Exception {
  val stackTrace = Thread.currentThread().stackTrace
  val numToRemove = 8
  val lastToRemove = stackTrace[numToRemove - 1]
  // This ensures that if the stacktrace format changes, we get notified immediately by the app
  // crashing (as opposed to silently mis-grouping crashes for an entire release).
  check(lastToRemove.className == "timber.log.Timber" && lastToRemove.methodName == "e",
    { "Got unexpected stacktrace: $stackTrace" })
  val abbreviatedStackTrace = stackTrace.takeLast(stackTrace.size - numToRemove).toTypedArray()
  return SyntheticException("Synthetic Exception: $message", abbreviatedStackTrace)
}

class SyntheticException(
  message: String,
  private val abbreviatedStackTrace: Array<StackTraceElement>
) : Exception(message) {
  override fun getStackTrace(): Array<StackTraceElement> {
    return abbreviatedStackTrace
  }
}

这样消息可以被参数化Timber.e("Got a weird error $error while eating a taco")并且该行的所有调用将被组合在一起。

显然,numToRemove 需要根据您触发非致命事件的确切机制进行更改。