UPDATE 7 NOV 2011: Have a look at this post: WP7: Northern Lights WP7 Toolkit v0.0.1 for the latest version of this code example.
In the previous post I already mentioned the great talk of Jeff Wilcox on TechEd Australia (video here).
In his talk he mentions LittleWatson, a supporting class that Andy Pennell wrote. This class enables you to catch unhandled exceptions and let the user report these application errors to you, the developer.
A big downside of this class, in my opinion, is that it needs the user to send the report. I therefore took it up me to extend the class and add the possibility to automatically send the error report to an HTTP endpoint.
Here are the classes.
The main class.
LittleWatsonManager.c
namespace MyApp.Utils { using System; using System.IO; using System.IO.IsolatedStorage; using System.Net; using System.Text; ////// LittleWatsonManager class. /// ////// Text to user: Send application error reports automatically and anonymously to southernsun to help us improve the application. /// public class LittleWatsonManager { #region Fields private static readonly LittleWatsonManager instance = new LittleWatsonManager(); private const string Filename = "LittleWatson.txt"; private const string SettingsFilename = "LittleWatsonSettings.txt"; private bool allowAnonymousHttpReporting = true; #endregion #region Constructor ////// Initializes static members of the LittleWatsonManager class. /// static LittleWatsonManager() { } ////// Prevents a default instance of the LittleWatsonManager class from being created. /// private LittleWatsonManager() { this.allowAnonymousHttpReporting = this.GetSetting(); } #endregion #region Properties ////// Gets DataManager instance. /// public static LittleWatsonManager Instance { get { return LittleWatsonManager.instance; } } ////// Gets or sets a value indicating whether error reports are allowed to send anonymously to a http endpoint. /// public bool AllowAnonymousHttpReporting { get { return this.allowAnonymousHttpReporting; } set { this.allowAnonymousHttpReporting = value; this.SetSetting(this.allowAnonymousHttpReporting); } } #endregion #region Public Methods ////// Report exception. /// /// The exception to report. public static void SaveExceptionForReporting(Exception ex) { if (ex == null) { return; } try { using (IsolatedStorageFile store = IsolatedStorageFile.GetUserStoreForApplication()) { using (TextWriter output = new StreamWriter(store.OpenFile(Filename, FileMode.OpenOrCreate & FileMode.Truncate))) { output.WriteLine(Serializer.WriteFromObject(new ExceptionContainer() { Message = ex.Message, StackTrace = ex.StackTrace })); } } } catch { } } /// /// Check for previous logged exception. /// ///Return the exception if found. public static ExceptionContainer GetPreviousException() { try { using (IsolatedStorageFile store = IsolatedStorageFile.GetUserStoreForApplication()) { if (store.FileExists(Filename)) { using (TextReader reader = new StreamReader(store.OpenFile(Filename, FileMode.Open, FileAccess.Read, FileShare.None))) { string data = reader.ReadToEnd(); try { return Serializer.ReadToObject(data); } catch { } } store.DeleteFile(Filename); } } } catch { } return null; } /// /// Send error report (exception) to HTTP endpoint. /// /// Exception to send. public void SendExceptionToHttpEndpoint(ExceptionContainer exception) { if (!this.AllowAnonymousHttpReporting) { return; } try { string uri = "http://www.yourwebsite.com/data/post.php"; HttpWebRequest webRequest = (HttpWebRequest)WebRequest.Create(uri); webRequest.Method = "POST"; webRequest.ContentType = "application/x-www-form-urlencoded"; webRequest.BeginGetRequestStream( r => { try { HttpWebRequest request1 = (HttpWebRequest)r.AsyncState; Stream postStream = request1.EndGetRequestStream(r); string info = string.Format("{0}{1}{2}", exception.Message, Environment.NewLine, exception.StackTrace); string postData = "&exception=" + HttpUtility.UrlEncode(info); byte[] byteArray = Encoding.UTF8.GetBytes(postData); postStream.Write(byteArray, 0, byteArray.Length); postStream.Close(); request1.BeginGetResponse( s => { try { HttpWebRequest request2 = (HttpWebRequest)s.AsyncState; HttpWebResponse response = (HttpWebResponse)request2.EndGetResponse(s); Stream streamResponse = response.GetResponseStream(); StreamReader streamReader = new StreamReader(streamResponse); string response2 = streamReader.ReadToEnd(); streamResponse.Close(); streamReader.Close(); response.Close(); } catch { } }, request1); } catch { } }, webRequest); } catch { } } #endregion #region Private Methods private bool GetSetting() { try { using (IsolatedStorageFile store = IsolatedStorageFile.GetUserStoreForApplication()) { using (TextReader reader = new StreamReader(store.OpenFile(SettingsFilename, FileMode.OpenOrCreate, FileAccess.Read, FileShare.None))) { string content = reader.ReadToEnd(); if (!string.IsNullOrEmpty(content)) { try { return Serializer.ReadToObject(content); } catch { } } } } } catch { } return true; } private void SetSetting(bool value) { try { using (IsolatedStorageFile store = IsolatedStorageFile.GetUserStoreForApplication()) { using (TextWriter output = new StreamWriter(store.OpenFile(SettingsFilename, FileMode.OpenOrCreate & FileMode.Truncate, FileAccess.Write, FileShare.None))) { try { output.WriteLine(Serializer.WriteFromObject (value)); } catch { } } } } catch { } } #endregion } }
We make our own ExceptionContainer that can be serialized.
ExceptionContainer.c
namespace MyApp.Utils { using System; using System.IO; using System.IO.IsolatedStorage; using System.Net; using System.Text; ////// ExceptionContainer class. /// public class ExceptionContainer { ////// Gets or sets the message. /// public string Message { get; set; } ////// Gets or sets the stacktrace. /// public string StackTrace { get; set; } } }
The supporting Serializer class.
Serializer.c
namespace MyApp.Utils { using System.IO; using System.Runtime.Serialization.Json; using System.Text; ////// Serializer class. /// public class Serializer { ////// Serialize object. /// ///The Object type. /// The object to serialize. ///The serialized object. public static string WriteFromObject(T obj) { MemoryStream ms = new MemoryStream(); DataContractJsonSerializer ser = new DataContractJsonSerializer(typeof(T)); ser.WriteObject(ms, obj); byte[] json = ms.ToArray(); ms.Close(); return Encoding.UTF8.GetString(json, 0, json.Length); } /// /// Deserialize object. /// ///The object type. /// The serialized object. ///The deserialized object. public static T ReadToObject(string json) { MemoryStream ms = new MemoryStream(Encoding.UTF8.GetBytes(json)); DataContractJsonSerializer ser = new DataContractJsonSerializer(typeof(T)); T obj = (T)ser.ReadObject(ms); ms.Close(); return obj; } } }
The HTTP endpoint that processes the error report, a PHP file.
$result = "App exception report:" . "\r\n"; if (get_magic_quotes_gpc()) { $result .= stripslashes($_POST['exception']); } else { $result .= $_POST['exception']; } $to = "info@youraddress.com"; $subject = "Exception report"; $headers = "From: info@youraddress.com\r\n"; mail($to, $subject, $result, $headers); ?>
You hook the manager with the UnhandledExceptionHandler of your App.xaml.cs
LittleWatsonManager.SaveExceptionForReporting(e.ExceptionObject as Exception);
And then add some code in your MainPage to report previous errors to your website:
ExceptionContainer exception = LittleWatsonManager.GetPreviousException(); if (exception != null) { if (LittleWatsonManager.Instance.AllowAnonymousHttpReporting) { LittleWatsonManager.Instance.SendExceptionToHttpEndpoint(exception); } else { // show popup. this.notification.Show("Unhandled exception found", new SolidColorBrush(Colors.Red), null); } }
To summary what happens. An unhanled exception happens and is caught by your apps unhandled exception handler which contains the SaveExceptionForReporting() method to save the exception. The next time the user starts your application the code in your MainPage.xaml.cs will check if there was any exception that needs to be reporting. Depending on the settings this exception will be pushed to a HTTP endpoint as a POST request with a variable ‘exception’ that contains the original exception message and stacktrace. In my example the message is then e-mailed to my personal e-mailaddress. If the user opt-outs on this, you can still show him a popup and ask him to send the error report. You should allow the user to change these settings. This can achieved by changing the AllowAnonymousHttpReporting boolean of the manager.
You should make a settings page that lets the user select the option to “Send application error reports automatically and anonymously to ‘companyname’ to help us improve the application”.
So that’s it. Hope you like it!
Let me know if you have any questions.
What is that about “(action != null)” on twitter – there are no such code in what you show?
this refers to my previous blog post:
http://bjorn.kuiper.nu/2011/10/01/wp7-notify-user-of-new-application-version/
I blogged both posts this weekend.