WP7 Application Crash Reporter

Update Feb. 26, 2011: Crash logger code has been modified to provide more details, including the OS version, current culture, current XAML page, and whether the app is obscured or locked.

Recently I found Andy Pennell’s LittleWatson Class, which is designed to log and report any unexpected crashes in your WP7 app. This can provide extremely useful data about problems your users have experienced and, if you tend to run your app without debugging enabled, crashes that occur during development.

Andy’s code is designed to send error reports via e-mail, but I’d prefer something a bit less intrusive. For Remote, I decided to write a class similar to LittleWatson that would submit the error reports to a script running on my web server.

CrashReporter

The first step is to add the CrashReporter class to your application.

using System;
using System.Globalization;
using System.IO;
using System.IO.IsolatedStorage;
using System.Net;
using System.Text;
using System.Windows;
using System.Windows.Navigation;
using Komodex.DACP;
using Komodex.WP7DACPRemote.DACPServerManagement;
using Microsoft.Phone.Controls;

namespace Komodex.WP7DACPRemote.Utilities
{
    // CrashReporter
    // Matt Isenhower, Komodex Systems LLC
    // http://blog.ike.to/2011/02/02/wp7-application-crash-reporter/

    public static class CrashReporter
    {
        // Error Report URL parameters
        private const string ErrorReportURL = "http://example.com/wp7/crashreporter/";
        private const string ProductName = "Remote";

        // The filename for crash reports in isolated storage 
        private const string ErrorLogFilename = "ApplicationErrorLog.log";

        private static readonly string LargeDashes = new string('=', 80);
        private static readonly string SmallDashes = new string('-', 80);
        private static string ErrorLogContent = null;
        private static PhoneApplicationFrame RootFrame = null;
        private static bool IsObscured = false;
        private static bool IsLocked = false;

        public static void Initialize(PhoneApplicationFrame frame)
        {
            RootFrame = frame;

            // Hook into exception events
            App.Current.UnhandledException += new EventHandler(App_UnhandledException);
            RootFrame.NavigationFailed += new NavigationFailedEventHandler(RootFrame_NavigationFailed);

            // Hook into obscured/unobscured events
            RootFrame.Obscured += new EventHandler(RootFrame_Obscured);
            RootFrame.Unobscured += new EventHandler(RootFrame_Unobscured);

            // Send previous log if it exists
            SendExceptionLog();
        }

        #region Events

        static void App_UnhandledException(object sender, ApplicationUnhandledExceptionEventArgs e)
        {
            LogException(e.ExceptionObject, "Unhandled Exception");
        }

        static void RootFrame_NavigationFailed(object sender, NavigationFailedEventArgs e)
        {
            LogException(e.Exception, "Navigation Failed");
        }

        static void RootFrame_Obscured(object sender, ObscuredEventArgs e)
        {
            IsObscured = true;
            IsLocked = e.IsLocked;
        }

        static void RootFrame_Unobscured(object sender, EventArgs e)
        {
            IsObscured = false;
            IsLocked = false;
        }

        #endregion

        #region Methods

        private static void LogException(Exception e, string type = null)
        {
            try
            {
                using (IsolatedStorageFile store = IsolatedStorageFile.GetUserStoreForApplication())
                {
                    using (TextWriter writer = new StreamWriter(store.OpenFile(ErrorLogFilename, FileMode.Append)))
                    {
                        writer.WriteLine(LargeDashes);

                        // Error type
                        writer.WriteLine(type ?? "Application Error");

                        // Application name
                        writer.WriteLine("-> Product: " + ProductName);

                        // Application version
                        writer.Write("-> Version: " + Utility.ApplicationVersion);
#if DEBUG
                        writer.Write(" (Debug)");
#endif
                        writer.WriteLine();

                        // Date and time
                        writer.WriteLine("-> Date: " + DateTime.Now.ToString());

                        // Unique report ID
                        writer.WriteLine("-> Report ID: " + Guid.NewGuid().ToString());

                        writer.WriteLine(SmallDashes);

                        try
                        {
                            writer.WriteLine("-> OS Version: {0} ({1})", Environment.OSVersion, Microsoft.Devices.Environment.DeviceType);
                            writer.WriteLine("-> Framework: " + Environment.Version.ToString());
                            writer.WriteLine("-> Culture: " + CultureInfo.CurrentCulture);
                            writer.WriteLine("-> Current page: " + RootFrame.CurrentSource);
                        }
                        catch (Exception ex)
                        {
                            writer.WriteLine(" -> Error getting device/page info: " + ex.ToString());
                        }

                        writer.WriteLine("-> Obscured: " + ((IsObscured) ? "Yes" : "No"));
                        writer.WriteLine("-> Locked: " + ((IsLocked) ? "Yes" : "No"));

                        writer.WriteLine(SmallDashes);

                        // Exception Info
                        writer.WriteLine("Exception Information:");
                        writer.WriteLine();
                        writer.WriteLine(e.ToString());

                        writer.WriteLine();
                    }
                }
            }
            catch { }
        }

        #endregion

        #region HTTP Request and Response

        private static void SendExceptionLog()
        {
            try
            {
                using (IsolatedStorageFile store = IsolatedStorageFile.GetUserStoreForApplication())
                {
                    if (!store.FileExists(ErrorLogFilename))
                        return;

                    using (TextReader reader = new StreamReader(store.OpenFile(ErrorLogFilename, FileMode.Open, FileAccess.Read, FileShare.None)))
                    {
                        ErrorLogContent = reader.ReadToEnd();
                    }

                    if (ErrorLogContent == null)
                        return;

                    string url = ErrorReportURL + "?p=" + ProductName + "&v=" + Utility.ApplicationVersion;
#if DEBUG
                    url += "&d=1";
#endif

                    HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
                    request.ContentType = "application/x-www-form-urlencoded";
                    request.Method = "POST";

                    request.BeginGetRequestStream(new AsyncCallback(GetRequestStringCallback), request);
                }
            }
            catch { }
        }

        private static void GetRequestStringCallback(IAsyncResult asyncResult)
        {
            HttpWebRequest request = (HttpWebRequest)asyncResult.AsyncState;

            string postData = "log=" + HttpUtility.UrlEncode(ErrorLogContent);

            byte[] bytes = Encoding.UTF8.GetBytes(postData);

            try
            {
                Stream postStream = request.EndGetRequestStream(asyncResult);
                postStream.Write(bytes, 0, bytes.Length);
                postStream.Close();

                request.BeginGetResponse(new AsyncCallback(GetResponseCallback), request);
            }
            catch { }
        }

        private static void GetResponseCallback(IAsyncResult asyncResult)
        {
            HttpWebRequest request = (HttpWebRequest)asyncResult.AsyncState;

            try
            {
                HttpWebResponse response = (HttpWebResponse)request.EndGetResponse(asyncResult);
                Stream streamResponse = response.GetResponseStream();
                StreamReader reader = new StreamReader(streamResponse);
                string responseString = reader.ReadToEnd();

                streamResponse.Close();
                reader.Close();
                response.Close();

                // Delete the log file
                using (IsolatedStorageFile store = IsolatedStorageFile.GetUserStoreForApplication())
                {
                    store.DeleteFile(ErrorLogFilename);
                }
            }
            catch { }
        }

        #endregion
    }
}

Initialize is the only public member of this class and should be called from the App constructor in App.xaml.cs (see below).

The crash log is only cleared once CrashReporter has successfully sent the log to your web server, so if your server is down or unreachable the app will try to submit the log again the next time it is launched.

Utility Class

This class makes use of the Utility class I covered in my previous blog post. The relevant parts of this class appear below:

using System.Reflection;
 
namespace Komodex.WP7Tools
{
    public static class Utility
    {
        #region Application Version
 
        private static string _ApplicationVersion = null;
        public static string ApplicationVersion
        {
            get
            {
                if (_ApplicationVersion == null)
                    _ApplicationVersion = GetApplicationVersion();
                return _ApplicationVersion;
            }
        }
 
        private static string GetApplicationVersion()
        {
            string assemblyInfo = Assembly.GetExecutingAssembly().FullName;
            return assemblyInfo.Split('=')[1].Split(',')[0];
        }
 
        #endregion
    }
}

App.xaml.cs

Once the CrashReporter and Utility classes have been added to your application, you can call the CrashReporter’s Initialize method from the constructor in App.xaml.cs. CrashReporter.Initialize must be called after RootFrame has been initialized, so it should be placed near the end of the constructor as follows:

// ...
using Komodex.WP7Tools
 
// ...
public App()
{
    // ...

    // Standard Silverlight initialization
    InitializeComponent();

    // Phone-specific initialization
    InitializePhoneApplication();

    // Crash reporter initialization
    CrashReporter.Initialize(RootFrame);
}

Listening for Crash Reports

The following PHP script can save each crash report to a file, e-mail you for each crash report, or both.

If you want to log each crash report to a file, make sure the PHP script has permission to create files. (This may require the use of chmod, depending on your server’s configuration.) The script will automatically attempt to create a directory called ./reports/productname/ beneath the current directory. If you use this method, make sure you either password protect the reports directory or configure your web server to deny access to that directory (e.g., put deny from all in an .htaccess file inside the reports directory).

If you want to send each crash report by e-mail, set $send_email to true on line 8 and enter your e-mail address on line 9. This script assumes PHP’s mail() has been configured properly on your server.


Testing

Once everything has been configured, the next step is to test it out. The easiest way is to intentionally trigger an exception in your app. After the app closes, relaunch it and the log should be sent to your server.

================================================================================
Unhandled Exception
-> Product: Remote
-> Version: 1.3.0.1
-> Date: 2/24/2011 11:37:55 PM
-> Report ID: 362e374e-5a98-4653-890a-f5a8c65c615c
--------------------------------------------------------------------------------
-> OS Version: Microsoft Windows CE 7.0.7004 (Device)
-> Framework: 3.7.10218.0
-> Culture: en-US
-> Current page: /NowPlaying/NowPlayingPage.xaml
-> Obscured: No
-> Locked: No
--------------------------------------------------------------------------------
Exception Information:

System.ArgumentOutOfRangeException:
Parameter name: index
   at System.ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument argument, ExceptionResource resource)
   at System.Collections.Generic.List`1.RemoveAt(Int32 index)
   at Komodex.WP7DACPRemote.MainPage.btnAddLibrary_Click(Object sender, RoutedEventArgs e)
   at System.Windows.Controls.Primitives.ButtonBase.OnClick()
   at System.Windows.Controls.Button.OnClick()
   at System.Windows.Controls.Primitives.ButtonBase.OnMouseLeftButtonUp(MouseButtonEventArgs e)
   at System.Windows.Controls.Control.OnMouseLeftButtonUp(Control ctrl, EventArgs e)
   at MS.Internal.JoltHelper.FireEvent(IntPtr unmanagedObj, IntPtr unmanagedObjArgs, Int32 argsTypeIndex, String eventName)

That’s it! If you find this code useful or make any improvements to it, please let me know! Also, follow me on Twitter and check out my app, Remote for Windows Phone 7.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.