【问题标题】:Group Java/Android stack traces into unique buckets将 Java/Android 堆栈跟踪分组到唯一的存储桶中
【发布时间】:2015-03-15 16:43:30
【问题描述】:

在 Java 或 Android 中记录未处理异常的堆栈跟踪时(例如通过 ACRA),您通常会以纯长字符串形式获取堆栈跟踪。

现在,所有提供崩溃报告和分析的服务(例如 Google Play 开发者控制台、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

但除此之外,您如何识别具有独特原因的报告?通过第一行搜索自定义(非平台)类的第一次出现并查找文件和行号?

【问题讨论】:

  • 好问题。如果你得到一个好的答案,我们可以将它折叠到 ACRA
  • @William 那太好了!您可以检查此处的任何答案或my library 是否适用于 ACRA。对我来说,没有一个答案是“开箱即用”的,但这些想法足以让我的库工作:JavaCrashId.from(exception) 加上应用程序版本代码似乎对我来说是一个崩溃指纹。

标签: java android stack-trace acra


【解决方案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;
    }
}

【讨论】:

  • 谢谢!虽然这不起作用(例如嵌套循环中的continue),但事实证明它是最有价值的起点。我根据您的回答为 Java 和 PHP 创建了一个库:github.com/delight-im/Java-Crash-ID
  • 谢谢。我更新了代码以防其他人尝试使用它。
【解决方案2】:

我想你已经知道答案了,但你可能正在寻找一个确认。你已经暗示过了……

如果您致力于明确区分异常及其原因/堆栈跟踪,那么答案可能会变得更容易掌握。

为了仔细检查我的答案,我在 Crittercism 中查看了我们的 Android 应用程序崩溃报告 - 我尊重并与之合作的一家分析公司。 (顺便说一句,我在 PayPal 工作,曾经领导过他们的一款 Android 产品,Crittercism 是我们报告和分析崩溃的首选方式之一)。

我所看到的正是您在问题中所暗示的。 在同一行代码(意味着相同的应用程序版本)上发生的相同异常但在不同版本的平台(意味着不同的 Java/Android 编译)上被记录为两次独特的崩溃。我认为这是你在找什么。

我希望我可以在这里复制粘贴崩溃报告,但我想我会因此被解雇:) 相反,我会给你审查数据:

java.lang.NullPointerException 发生在我们应用程序 2.4.8 版本的第 117 行的 ICantSayTheControllerName.java 类中;但在此崩溃状态的两个不同(唯一)分组中,对于使用 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

【讨论】:

  • 谢谢,塞尔坎!你是对的,我已经很确定这些是制作独特崩溃报告的因素。因此,问题更多的是关于如何实际获取那些决定崩溃报告是否唯一的值。这意味着:不同的应用程序版本会生成不同的崩溃报告,这一点很明显。但是从堆栈跟踪本身,您如何推断此跟踪是否是新的?你看哪几行?上面的那些?还是Caused by ... 中的那些?还是只有您的一个包裹中的那些?
【解决方案3】:

我知道这不是灵丹妙药,而只是我的 2 美分:

  1. 我的项目中的所有异常都扩展abstract class AppException
  2. 在报告发送或记录到文件之前,所有其他平台异常(RuntimeException、IOException...)都由AppException 包装。

AppException 类如下所示:

public abstract class AppException extends Exception {

    private AppClientInfo appClientInfo; // BuildVersion, AndroidVersion etc...

    [...] // other stuff
}
  1. 然后我从 AppException 创建一个 ExceptionReport 并将其发送到我的服务器(作为 json/xml) ExceptionReport 包含以下数据:

    • appClientInfo
    • 异常类型 // ui、数据库、webservice、首选项...
    • origin // 从堆栈跟踪中获取来源:MainActivity:154
    • stacktrace as html // 所有以“com.mycompany.myapp”开头的行都被高亮显示。

现在在服务器端,我可以对报告进行排序、分组(忽略重复项)和发布报告。如果异常类型很关键,则可以创建新票证。


我如何识别重复项?

例子:

  • appClientInfo: "android" : "4.4.2", "appversion" : "2.0.1.542"
  • 异常类型:"type" : "database"
  • 来源:"SQLiteProvider.java:423"

现在我可以用这种幼稚的方式计算唯一 ID:

UID = HASH("4.4.2" + "2.0.1.542" + "database" + "SQLiteProvider.java:423")

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2018-08-10
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-04-03
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多