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.