Using Application Insights to track Unhandled Exceptions in Windows Phone and Windows Store Apps
Application Insights is a preview service offered as part of Visual Studio Online. If you don’t have access to the preview yet then keep an eye on Brian Harry’s blog for updates.
If you are lucky enough to have access and you are building a Windows Phone or Windows 8 application, then you can use this quick guide to allow you to track crashes in your apps.
Automatic exception logging in apps isn’t (yet?) a feature of Application Insights, but its perfectly possibly to use the in-build event logging functionality to do this. To make this happen you need to have first setup Application Insights for your app. I’m going to assume you’ve done this already as you’ve got this far. If you haven’t then check out these articles on MSDN on how to do so for Windows Phone and Windows Store Apps and then come back here.
The logging API for Application Insights is really simple.
ClientAnalyticsChannel.Default.LogEvent("MyCategory/MyEvent");
This will log an event “MyEvent” that will be categorised under the title “MyCategory”. You may have many event categories that you want to log across your app and you unhandled exception events will just be another category. You can choose what you want to call it, but for this example we are just using ‘Exceptions’ as the category name. You’ll need to call this code from two places, the UnhandledException handler and the UnobservedTaskException handler although you can use a different category name to separate these out.
The event name is slightly more problematic. You want to have a unique event captured for each execution path that leads to an exception. In order to do this you can generate a hash based on the stack trace of the exception. Don’t forget that, in this world of async/await and AggregateException, you’ll need to include the child elements of the AggregateException and their InnerException as well.
The following method should give you a string that when hashed results in a unique id by which to classify your exception.
private static string GetFullDetail(Exception ex)
{
string thisException = string.Format("{0}: {1} - {2}", ex.GetType().Name, ex.Message, ex.StackTrace);
if (ex.InnerException != null)
{
// Recursively get inner exception details
string innerExceptionDetails = GetFullDetail(ex.InnerException);
if (string.IsNullOrEmpty(innerExceptionDetails))
{
thisException = string.Format("{0}\n\nINNER: {1}", thisException, innerExceptionDetails);
}
}
var aggregateException = ex as AggregateException;
if (aggregateException != null && aggregateException.InnerExceptions != null)
{
int count = 1;
if (var aggregateInner in aggregateException.InnerExceptions)
{
// Don't include the aggregate that's just a duplicate of the inner.
if (aggregateInner != ex.InnerException)
{
// Recursively get aggregate inner exceptions
string aggregateInnerExceptionDetails = GetFullDetail(aggregateInner);
if (!string.IsNullOrEmpty(aggregateInnerExceptionDetails))
{
thisException = string.Format("{0}\n\nAGGREGATE({1}): {2}", thisException, count, aggregateInnerExceptionDetails);
}
}
count++;
}
}
return thisException;
}
By calling this method where you handle your exceptions you can use it as the event id when you call the logging code.
private static void HandleException(Exception ex)
{
string exceptionDetail = GetFullDetails(ex);
string hashCode = exceptionDetail.GetHashCode().ToString();
Dictionary<string, object> dimensions = new Dictionary<string, object>();
dimensions.Add("HashCode", hashCode);
dimensions.Add("StackTrace", exceptionDetail);
dimensions.Add("Type", ex.GetType().ToString());
dimensions.Add("Message", ex.Message);
string eventMessage = string.Format("Exception/{0}/{1} - {2}",
Assembly.GetExecutingAssembly().GetName().Version.ToString(4),
detail.HashCode,
ex.Message);
ClientAnalyticsChannel.Default.LogEvent(eventMessage, dimensions);
}
The method above is doing three things.
-
- Calling out first method to generate a unique and complete stack trace, we are then hashing this and using this as our identifier in 3.
- Adding dimensions for StackTrace, Type and Message, along with the HashCode. This allows us to extract the information we need to identify what is causing the crash.
- Constructing the ‘eventMessage’ and logging the event with Application Insights.
You’ll see at the end where we construct the ‘eventMessage’ variable, the event log path is split into 2 categories and an identifying event. Here we are further splitting the events down by the appVersion. This makes it easier for us to focus on where the issues are in the current version of the app. Take a look at the screenshot below which makes this clearer.
Hopefully this gives a useful insight in to how to instrument your Store Apps so as to be able to track and fix the exceptions that are occurring in them.
Comments
- Anonymous
August 29, 2014
The comment has been removed - Anonymous
November 01, 2015
There was also a ! (not) operator missing on the first IsNullOrEmpty if statement. Here's a fixed version of the GetFullDetail method. public static string GetFullDetail(Exception ex) { string thisException = string.Format("{0}: {1} - {2}", ex.GetType().Name, ex.Message, ex.StackTrace); if (ex.InnerException != null) { // Recursively get inner exception details string innerExceptionDetails = GetFullDetail(ex.InnerException); if (!string.IsNullOrEmpty(innerExceptionDetails)) { thisException = string.Format("{0}nnINNER: {1}", thisException, innerExceptionDetails); } } var aggregateException = ex as AggregateException; if (aggregateException != null && aggregateException.InnerExceptions != null) { int count = 1; foreach (var aggregateInner in aggregateException.InnerExceptions) { // Don't include the aggregate that's just a duplicate of the inner. if (aggregateInner != ex.InnerException) { // Recursively get aggregate inner exceptions string aggregateInnerExceptionDetails = GetFullDetail(aggregateInner); if (!string.IsNullOrEmpty(aggregateInnerExceptionDetails)) { thisException = string.Format("{0}nnAGGREGATE({1}): {2}", thisException, count, aggregateInnerExceptionDetails); } } count++; } } return thisException; }