Sunday, March 3, 2013

Google Analytics EasyTracker: detailed stack-traces

If you're using Google Analytics to capture crashes in your application you may have noticed that uncaught exceptions give you very little information (just the exception message):
An error occured while executing doInBackground() 

Google Analytics documentation does mention that you can use your own custom ExceptionReporter / ExceptionParserDecompiling the library though you can see how it is all implemented - I should note that this article is based on Google Analytics v2.0 b4 and may or may not work with other versions.

Instead of creating a new ExceptionReporter and attaching it as a default handler, will assign a custom ExceptionParser in the default ExceptionReporter. That is mainly because from looking at the code the suggested way will chain handlers, possibly producing duplicate reports.

The replacement will go the Application class, before anything else (you could do it if you want in any other activity, although once is enough). First EasyTracker.getInstance().setContext() call initializes the structures, calling EasyTracker.loadParameters() method - among other things it will also create an ExceptionParser and set it as a default exception handler - Thread.setDefaultUncaughtExceptionHandler()Should note that the uncaught exception handler is only set when you have set a ga_trackingId and ga_reportUncaughtExceptions = true in your analytics.xml config file.
package com.your.package;

import com.google.analytics.tracking.android.EasyTracker;
import com.google.analytics.tracking.android.ExceptionReporter;

public class Application extends android.app.Application {
  /*
   * (non-Javadoc)
   * @see android.app.Application#onCreate()
   */
  public void onCreate() {
    [...]

    /*
     * Google Analytics...
     */
    EasyTracker.getInstance().setContext(this);

    // Change uncaught exception parser...
    // Note: Checking uncaughtExceptionHandler type can be useful if clearing ga_trackingId during development to disable analytics - avoid NullPointerException.
    Thread.UncaughtExceptionHandler uncaughtExceptionHandler = Thread.getDefaultUncaughtExceptionHandler();
    if (uncaughtExceptionHandler instanceof ExceptionReporter) {
      ExceptionReporter exceptionReporter = (ExceptionReporter) uncaughtExceptionHandler;
      exceptionReporter.setExceptionParser(new AnalyticsExceptionParser());
    }
  }
}

My custom AnalyticsExceptionParser uses Apache Commons Lang ExceptionUtils, but just as well you could use your own implementation.
package com.your.package.util;

import org.apache.commons.lang3.exception.ExceptionUtils;
import com.google.analytics.tracking.android.ExceptionParser;

public class AnalyticsExceptionParser implements ExceptionParser {
  /*
   * (non-Javadoc)
   * @see com.google.analytics.tracking.android.ExceptionParser#getDescription(java.lang.String, java.lang.Throwable)
   */
  public String getDescription(String p_thread, Throwable p_throwable) {
     return "Thread: " + p_thread + ", Exception: " + ExceptionUtils.getStackTrace(p_throwable);
  }
}
Now this will produce you a complete stack trace with class names and line numbers, that should be very helpful to figure out where the problem happened exactly.


* * *
You can apply similar to EasyTracker.getTracker() setting your custom ExceptionParser with EasyTracker.getTracker().setExceptionParser(...) for caught exceptions, although not necessary as probably in those cases you will want to send the exception plus some other contextual data to figure out the context, and for what you can use sendException(String description, boolean fatal) e.g.:

String response = null;
try {
  [...]
  response = doSomething();
}
catch (Throwable t) {
  // Analytics...
  EasyTracker.getTracker().sendException("response = " + response + ", " + ExceptionUtils.getStackTrace(t), false);

  // Logging...
  if (BuildConfig.DEBUG) {
    Log.e(TAG, ExceptionUtils.getStackTrace(t));
  }
}

26 comments :

  1. This has just save me who knows how many hours of desperate work, thank you!!!

    ReplyDelete
  2. You're more than welcome. 2.0 b5 was just released the other day and reading the comments this might be fixed and would not need the workaround, but I haven't tested it yet, will come back later, or maybe you can confirm, thanks.

    https://developers.google.com/analytics/devguides/collection/android/changelog

    ReplyDelete
  3. Thanks
    But in case of null-pointer exception
    its not working

    ReplyDelete
  4. Well, check if you're not catching that yourself - cause then the unchaught handler won't.

    ReplyDelete
  5. AND check you got ga_reportUncaughtExceptions=true in your analytics.xml, although I assume you have that alreay (the code I wrote goes around it if you don't so it doesn't crash if you did't, maybe I should've let it crash, see the comments in the code).

    ReplyDelete
  6. Thank you so much.Its really great.

    ReplyDelete
  7. Analytics 2.0 b5 gives the line number, but not the stacktrace.

    ReplyDelete
  8. This comment has been removed by the author.

    ReplyDelete
  9. Is this still work after implement with proguard?

    ReplyDelete
  10. Of course, with configuration you can choose to keep the class name and the line numbers:

    http://proguard.sourceforge.net/index.html#manual/examples.html

    This is a stack trace using:
    -keepattributes SourceFile,LineNumberTable

    Thread: Thread-86757, Exception: java.lang.NullPointerException at
    com.dandar3.xbmc4xbox.remote.a.d.a(IabHelper.java:836) at
    [...]
    java.lang.Thread.run(Thread.java:856)

    ReplyDelete
  11. Thanks Mate for helping, i use

    -keepattributes *Annotation*,EnclosingMethod,SourceFile,LineNumberTable

    Hope it works!

    ReplyDelete
  12. Hi Dan Dar, may i ask how to add this 2 line to the Thread.UncaughtExceptionHandler?

    System.runFinalizersOnExit(true);
    System.exit(0);

    Because i faced some problem, that is when the app is crashing, my app activity still remain there with blank UI. so i need system.exit(0) to ensure close the app.

    any idea on this?

    Thanks a lot :)

    ReplyDelete
  13. @davidbilly
    You can try it yourself - change your code to generate an exception that would be reported. Test it with your code in Eclipse and Emulator, wait a couple of minutes and see the exception in Google Analytics. Then package it up and install the .apk on our phone or in the emulator and try the same. That'll confirm that it works like expected before you release it to the users.

    ReplyDelete
  14. @davidbilly
    I'm not going to go into a debate whether to use or not use System.runFinalizersOnExit() and System.exit() [you can find a lot of those on the forums], but if I remember correctly when looking at the Google Analytics reporter library (which you can decompile yourself), it chains the existing handler.

    So you can probably set your default handler to do the "exit" that you want, then create a new ExceptionReporter that'll do the reporting and after that it will call your handler, see:

    https://developers.google.com/analytics/devguides/collection/android/v2/exceptions#exception-reporter

    ReplyDelete
  15. Sorry for late reply, everything is works, @dan dar3 thanks for helping.

    The only thing is happened on google console, i can't click the exception to view full message.

    hread: main, Exception: java.lang.RuntimeException: Unable to resume activity {com.appleaf.mediatapv2/com.slidingmenu.FragmentChangeActivity}: java.lang.NullPointerException at android.app.ActivityThread.performResumeActivity(ActivityThread.java:2456) at android.app.ActivityThread.handleResumeActivity(ActivityThread.java:2484) at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:1998) at android.app.ActivityThread.access$600(ActivityThread.java:127) at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1159) at android.os.Handler.dispatchMessage(Handler.java:99) at android.os.Looper.loop(Looper.java:137) at android.app.ActivityThread.main(ActivityThread.java:4507) at java.lang.reflect.Method.invokeNative(Native Method) at java.lang.reflect.Method.invoke(Method.java:511) at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:980) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:747) at dalvik.system.NativeStart.main(Native Method) Caused by: java.lang.NullPointerException at android.app.Instrumentation.callActivityOnNewIntent(Instrumentation.java:1126) at android.app.ActivityThread.deliverNewIntents(ActivityThread.java:2054) at android.app.ActivityThread.performResumeActivity(ActivityThread.java:2439) ... 12 more

    you see the 12 more, when i click on it , is empty on google console, bad google. other than this everything is fine :)

    ReplyDelete
  16. I'm afraid that's standard behavior for Throwable.printStackTrace() - I guess it's because the whole thing could be very long and usually you are interested in the top part of each exception:

    http://docs.oracle.com/javase/6/docs/api/java/lang/Throwable.html#printStackTrace%28%29

    StringUtils.getStackTrace() uses one of its variants to print it to a StringWriter:

    http://svn.apache.org/viewvc/commons/proper/lang/trunk/src/main/java/org/apache/commons/lang3/exception/ExceptionUtils.java?view=markup

    You could write your own stacktrace printer, but most of the time you can figure out what happened in there from existing stack trace (if you stare at it long enough :-)

    Another useful thing is the "Secondary dimension" drop down (see top screenshot) where you can get the screen name where it happened or the Android version or even device I believe, if it's an odd issue that you can't replicate unless in those conditions.

    ReplyDelete
  17. Hey Dan,
    Just wanted to thank you as well for the post, works great. I can also confirm that the new StandardExceptionParser didn't work either.
    Also for stacktrace, I just use Log.getStackTrace no need for other libraries.

    Cheers!

    ReplyDelete
  18. Hi Samir,

    You mean the new v3 one? That's a pity, I was going to give it a go in spite of the effort - so much for a "rewrite"...

    https://developers.google.com/analytics/devguides/collection/android/changelog

    Ah thanks for that, I guess I'm too used from using Apache Commons with other Java projects, that's a good find alright, thanks!

    http://developer.android.com/reference/android/util/Log.html#getStackTraceString(java.lang.Throwable)

    ReplyDelete
  19. Hi Dan. First of all, I want to thank you for this blog post, it really helped me with setting up crash reporting via google analytics in my Android app. I'm seeing some weird behavior in the analytics dashboard, though. It seems that it removes all the line break characters, so the stack trace looks like this.
    Which is pretty hard to read. Have you seen anything like this?

    I tried using both "Log.getStackTraceString(throwable)" and source code from ExceptionUtils:

    StringWriter sw = new StringWriter();
    PrintWriter pw = new PrintWriter(sw, true);
    throwable.printStackTrace(pw);
    return sw.getBuffer().toString();

    ...to retrieve full stack trace, but both of them show the exact same result: stack trace without line breaks.

    ReplyDelete
  20. @Anton Cherkashyn
    I think the problem is not with what you submit as stack-trace, but with the presentation on the website. The stack trace contains the new-line, but they are not displayed as break-lines in HTML. If you're thinking to replace new-lines with BR tags before submitting, I believe those will be escaped and it will make it even a more unreadable mess.

    I'm fairly ok with reading the stack traces on the web (and if not I guess I would use that as more incentive to eliminate them :-), but otherwise you could try using the Google Analytics Query Explorer (web) and say start with looking at dimensions = ga:exceptionDescription,ga:appVersion and metrics = ga:exceptions. This one seems to display new lines as break-lines and it might be more per your liking.

    http://ga-dev-tools.appspot.com/explorer/

    ReplyDelete
  21. Hi Dan, i have a problem ..
    I added the code in my app but when i run my app, it hangs and does not get the error .. it stuck ... i do no what it the problem :( any help

    ReplyDelete
  22. Hi Dan, does your approach to display detailed stack-traces work at this time? Is a description of an exception can be only up to 100 characters? In the Analytics developer page they said that "A description of the exception (up to 100 characters). Accepts null ." https://developers.google.com/analytics/devguides/collection/android/v4/exceptions

    ReplyDelete
    Replies
    1. Hi Hiệp, I don't know, might be the intended length, may not be implemented, method is bypassing the code that is doing that etc. Measured one of the recent exceptions I caught and it is 1639 characters so, there you go.

      Delete
  23. great job! you saved my project=)

    ReplyDelete
  24. This comment has been removed by a blog administrator.

    ReplyDelete