问题 将Java / Android堆栈跟踪分成独特的存储桶


在Java或Android中记录未处理异常的堆栈跟踪时(例如通过ACRA),通常会将堆栈跟踪视为普通长字符串。

现在,所有提供崩溃报告和分析的服务(例如Google Play Developer Console,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

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


12675
2018-03-15 16:43


起源

好问题。如果你得到一个好的答案,我们可以把它折叠成ACRA - William
@William那太棒了!您可以在这里检查是否有任何答案 我的图书馆 为ACRA工作。对我来说,没有一个答案能够“开箱即用”,但这些想法足以使我的图书馆工作: JavaCrashId.from(exception) 加上应用程序版本代码似乎对我来说是一个崩溃指纹。 - caw


答案:


如果您正在寻找一种方法来获取异常的唯一值而忽略特定于操作系统的类,则可以进行迭代 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;
    }
}

5
2018-03-24 00:31



谢谢!虽然这不起作用(例如 continue 在嵌套循环中,它被证明是最有价值的起点。我用你的答案中的想法创建了一个Java和PHP库: github.com/delight-im/Java-Crash-ID - caw
谢谢。我更新了代码,以防其他人尝试使用它。 - kichik


我想你已经知道了答案,但你也许正在寻找确认。你已经暗示过......

如果您承诺明确区分异常及其原因/ Stacktrace,那么答案可能会变得更容易掌握。

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

我所看到的正是你在问题中隐含的内容。 在同一行代码(意思是相同的应用程序版本)上发生相同的异常但是在不同版本的平台上(意味着不同的Java / Android编译)被记录为两个独特的崩溃。 而且我认为这就是你要找的东西。

我希望我可以复制粘贴崩溃报告,但我想我会被解雇:)而不是我会给你审查数据:

一个 java.lang.NullPointerException 发生在 ICantSayTheControllerName.java 我们申请的2.4.8版本第117行的课程;但是在这种崩溃状态的两个不同(唯一)分组中,对于那些使用Android 4.4.2设备的用户来说,原因就在于此 android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2540) 但是对于那些使用Android 4.4.4的用户来说,原因就在于此 android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2404)。 *请注意由于平台编译的不同,ActivityThread.java中行号的细微差别。

这确保了App Version Number,Exception和Cause / Stacktrace是特定崩溃的唯一标识符的三个值;换句话说,根据这三个信息的唯一值完成崩溃报告的分组。我几乎想要建立一个数据库和一个主要的类比,但我离题了。

另外,我把Crittercism作为一个例子,因为这是他们所做的;它们几乎是行业标准;我相信他们所做的至少与崩溃报告和分析中的其他领导者相提并论。 (不,我不为他们工作)。

我希望这个现实世界的例子能够澄清或证实你的想法。

-serkan


4
2018-03-19 07:03



谢谢,谢尔坎!你是对的,我已经非常确定这些是制作独特崩溃报告的因素。因此,问题更多的是如何实际 得到 那些决定崩溃报告是否唯一的值。这意味着:不同的应用程序版本会生成不同的崩溃报告,这很明显。但是从堆栈跟踪本身,你如何推断​​这个跟踪是否是新的?你看哪一行?顶部的?或者那些 Caused by ...?或者只有你的一个包装? - caw


我知道这不是银弹,而只是我的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. 然后我创建了一个 ExceptionReport 从 AppException 并将其发送到我的服务器(作为json / xml) ExceptionReport包含以下数据:

    • appClientInfo
    • 异常类型  // ui,数据库,webservice,首选项......
    • 起源  //从stacktrace获取原点: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")

0
2018-03-24 18:28



很好的答案,这深刻地解释了