using System; using System.Collections.Generic; using System.Net; using System.Windows; using System.Windows.Controls; using System.Windows.Documents; using System.Windows.Ink; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Animation; using System.Windows.Shapes; using System.Diagnostics; using System.Globalization; using System.IO; using System.IO.IsolatedStorage; using System.Text; public class LocalyticsSession { #region library constants private const int maxStoredSessions = 10; private const int maxNameLength = 100; private const string libraryVersion = "windowsphone_2.2"; private const string directoryName = "localytics"; private const string sessionFilePrefix = "s_"; private const string uploadFilePrefix = "u_"; private const string metaFileName = "m_meta"; private const string serviceURLBase = "http://analytics.localytics.com/api/v2/applications/"; #endregion #region private members private string appKey; private string sessionUuid; private string sessionFilename; private bool isSessionOpen = false; private bool isSessionClosed = false; private double sessionStartTime = 0; #endregion #region static members private static bool isUploading = false; private static IsolatedStorageFile localStorage = null; #endregion #region private methods #region Storage /// /// Caches the reference to the app's isolated storage /// private static IsolatedStorageFile getStore() { if (localStorage == null) { localStorage = IsolatedStorageFile.GetUserStoreForApplication(); } return localStorage; } /// /// Tallies up the number of files whose name starts w/ sessionFilePrefix in the localytics dir /// private static int getNumberOfStoredSessions() { IsolatedStorageFile store = getStore(); if (store.DirectoryExists(LocalyticsSession.directoryName) == false) { return 0; } return store.GetFileNames(LocalyticsSession.directoryName + @"\" + LocalyticsSession.sessionFilePrefix + "*").Length; } /// /// Gets a stream pointing to the requested file. If the file does not exist it is created. /// If the file does exist the stream points to the end of the file /// /// Name of the file (w/o directory) to create private static IsolatedStorageFileStream getStreamForFile(string filename) { IsolatedStorageFile store = getStore(); store.CreateDirectory(LocalyticsSession.directoryName); // does nothing if dir exists return new IsolatedStorageFileStream(LocalyticsSession.directoryName + @"\" + filename, FileMode.Append, store); } /// /// Appends a string to the end of a text file /// /// Text to append /// Name of file to append to private static void appendTextToFile(string text, string filename) { IsolatedStorageFileStream file = getStreamForFile(filename); TextWriter writer = new StreamWriter(file); writer.Write(text); writer.Close(); file.Close(); } /// /// Reads a file and returns its contents as a string /// /// file to read (w/o directory prefix) /// the contents of the file private static string GetFileContents(string filename) { IsolatedStorageFile store = getStore(); var file = store.OpenFile(LocalyticsSession.directoryName + @"\" + filename, FileMode.Open); TextReader reader = new StreamReader(file); string contents = reader.ReadToEnd(); reader.Close(); file.Close(); return contents; } #endregion #region upload /// /// Goes through all the upload files and collects their contents for upload /// /// A string containing the concatenated private static string GetUploadContents() { StringBuilder contents = new StringBuilder(); IsolatedStorageFile store = getStore(); if (store.DirectoryExists(LocalyticsSession.directoryName)) { string[] files = store.GetFileNames(LocalyticsSession.directoryName + @"\" + LocalyticsSession.uploadFilePrefix + "*"); foreach (string file in files) { if (file.StartsWith(LocalyticsSession.uploadFilePrefix)) // workaround for GetFileNames bug { contents.Append(GetFileContents(file)); } } } return contents.ToString(); } /// /// loops through all the files in the directory deleting the upload files /// private static void DeleteUploadFiles() { IsolatedStorageFile store = getStore(); if (store.DirectoryExists(LocalyticsSession.directoryName)) { string[] files = store.GetFileNames(LocalyticsSession.directoryName + @"\" + LocalyticsSession.uploadFilePrefix + "*"); foreach (string file in files) { if (file.StartsWith(LocalyticsSession.uploadFilePrefix)) // workaround for GetfileNames returning extra files { store.DeleteFile(LocalyticsSession.directoryName + @"\" + file); } } } } /// /// Rename any open session files. This way events recorded during uploaded get written safely to disk /// and threading difficulties are missed. /// private void renameOrAppendSessionFiles() { IsolatedStorageFile store = getStore(); if (store.DirectoryExists(LocalyticsSession.directoryName)) { string[] files = store.GetFileNames(LocalyticsSession.directoryName + @"\" + LocalyticsSession.sessionFilePrefix + "*"); string destinationFilename = LocalyticsSession.uploadFilePrefix + Guid.NewGuid().ToString(); bool addedHeader = false; foreach (string file in files) { if (file.StartsWith(LocalyticsSession.sessionFilePrefix)) // work around for GetFileNames returning extra files { // Any time sessions are appended, an upload header should be added. But only one is needed regardless of number of files added if (!addedHeader) { appendTextToFile(GetBlobHeader(), destinationFilename); addedHeader = true; } appendTextToFile(GetFileContents(file), destinationFilename); store.DeleteFile(LocalyticsSession.directoryName + @"\" + file); } } } } /// /// Runs on a seperate thread and is responsible for renaming and uploading files as appropriate /// private void BeginUpload() { LogMessage("Beginning upload."); try { renameOrAppendSessionFiles(); // begin the upload string url = LocalyticsSession.serviceURLBase + this.appKey + "/uploads"; LogMessage("Uploading to: " + url); HttpWebRequest myRequest = (HttpWebRequest)WebRequest.Create(url); myRequest.Method = "POST"; myRequest.ContentType = "application/json"; myRequest.BeginGetRequestStream(new AsyncCallback(httpRequestCallback), myRequest); } catch (Exception e) { LogMessage("Swallowing exception: " + e.Message); } } private static void httpRequestCallback(IAsyncResult asynchronousResult) { try { HttpWebRequest request = (HttpWebRequest)asynchronousResult.AsyncState; Stream postStream = request.EndGetRequestStream(asynchronousResult); String contents = GetUploadContents(); byte[] byteArray = Encoding.UTF8.GetBytes(contents); postStream.Write(byteArray, 0, byteArray.Length); postStream.Close(); request.BeginGetResponse(new AsyncCallback(GetResponseCallback), request); } catch (Exception e) { LogMessage("Swallowing exception: " + e.Message); } } private static void GetResponseCallback(IAsyncResult asynchronousResult) { try { HttpWebRequest request = (HttpWebRequest)asynchronousResult.AsyncState; HttpWebResponse response = (HttpWebResponse)request.EndGetResponse(asynchronousResult); Stream streamResponse = response.GetResponseStream(); StreamReader streamRead = new StreamReader(streamResponse); string responseString = streamRead.ReadToEnd(); LogMessage("Upload complete. Response: " + responseString); DeleteUploadFiles(); streamResponse.Close(); streamRead.Close(); response.Close(); } catch (WebException e) { Debug.WriteLine("WebException raised."); Debug.WriteLine("\n{0}", e.Message); Debug.WriteLine("\n{0}", e.Status); } catch (Exception e) { Debug.WriteLine("Exception raised!"); Debug.WriteLine("Message : " + e.Message); } finally { LocalyticsSession.isUploading = false; } } #endregion #region Data Looklups /// /// Retreives a unique identifier for this device. According to Microsoft, this identifier is /// unique across all carriers and devices /// private static string GetDeviceId() { byte[] id = (byte[])Microsoft.Phone.Info.DeviceExtendedProperties.GetValue("DeviceUniqueId"); return Convert.ToBase64String(id); } /// /// Gets the sequence number for the next upload blob. /// /// Sequence number as a string private static string GetSequenceNumber() { // open the meta file and read the next sequence number. IsolatedStorageFile store = getStore(); string metaFile = LocalyticsSession.directoryName + @"\" + LocalyticsSession.metaFileName; if (!store.FileExists(metaFile)) { SetNextSequenceNumber("1"); return "1"; } var file = store.OpenFile(LocalyticsSession.directoryName + @"\" + LocalyticsSession.metaFileName, FileMode.Open); TextReader reader = new StreamReader(file); string installID = reader.ReadLine(); string sequenceNumber = reader.ReadLine(); reader.Close(); file.Close(); return sequenceNumber; } /// /// Sets the next sequence number in the metadata file. Creates the file if its not already there /// /// Next sequence number private static void SetNextSequenceNumber(string number) { IsolatedStorageFile store = getStore(); string metaFile = LocalyticsSession.directoryName + @"\" + LocalyticsSession.metaFileName; if (!store.FileExists(metaFile)) { // Create a new metadata file consisting of a unique installation ID and a sequence number appendTextToFile(Guid.NewGuid().ToString() + Environment.NewLine + number, LocalyticsSession.metaFileName); } else { var fileIn = store.OpenFile(metaFile, FileMode.Open); TextReader reader = new StreamReader(fileIn); string installId = reader.ReadLine(); reader.Close(); fileIn.Close(); // overwite the file w/ the old install ID and the new sequence number var fileOut = store.OpenFile(metaFile, FileMode.Truncate); TextWriter writer = new StreamWriter(fileOut); writer.WriteLine(installId); writer.Write(number); writer.Close(); fileOut.Close(); } } /// /// Gets the timestamp of the storage file containing the sequence numbers. This allows processing to /// ignore duplicates or identify missing uploads /// /// A string containing a Unixtime private static string GetPersistStoreCreateTime() { IsolatedStorageFile store = getStore(); string metaFile = LocalyticsSession.directoryName + @"\" + LocalyticsSession.metaFileName; if (!store.FileExists(metaFile)) { SetNextSequenceNumber("1"); } DateTimeOffset dto = store.GetCreationTime(metaFile); int secondsSinceUnixEpoch = (int)Math.Round((dto.DateTime - new DateTime(1970, 1, 1).ToLocalTime()).TotalSeconds); return secondsSinceUnixEpoch.ToString(); } /// /// Gets the Installation ID out of the metadata file /// private static string GetInstallId() { IsolatedStorageFile store = getStore(); var file = store.OpenFile(LocalyticsSession.directoryName + @"\" + LocalyticsSession.metaFileName, FileMode.Open); TextReader reader = new StreamReader(file); string installID = reader.ReadLine(); reader.Close(); file.Close(); return installID; } private static string _version; /// /// Retreives the Application Version from teh WMAppManifest.xml file /// /// User generated app version public static string GetAppVersion() { if (!string.IsNullOrEmpty(_version)) return _version; var manifest = new Uri("WMAppManifest.xml", UriKind.Relative); var si = Application.GetResourceStream(manifest); if (si != null) { using (var sr = new StreamReader(si.Stream)) { bool haveApp = false; while (!sr.EndOfStream) { string line = sr.ReadLine(); if (!haveApp) { int i = line.IndexOf("AppPlatformVersion=\"", StringComparison.InvariantCulture); if (i >= 0) { haveApp = true; line = line.Substring(i + 20); } } int y = line.IndexOf("Version=\"", StringComparison.InvariantCulture); if (y >= 0) { int z = line.IndexOf("\"", y + 9, StringComparison.InvariantCulture); if (z >= 0) { // We have the version, no need to read on. _version = line.Substring(y + 9, z - y - 9); break; } } } } } else { _version = "Unknown"; } return _version; } /// /// Gets the current date/time as a string which can be inserted in the DB /// /// A formatted string with date, time, and timezone information private static string GetDatestring() { DateTime dt = DateTime.Now.ToUniversalTime(); // reformat the time to: YYYY-MM-DDTHH:MM:SS // use a StringBuilder to avoid creating multiple StringBuilder datestring = new StringBuilder(); datestring.Append(dt.Year); datestring.Append("-"); datestring.Append(dt.Month.ToString("D2")); datestring.Append("-"); datestring.Append(dt.Day.ToString("D2")); datestring.Append("T"); datestring.Append(dt.Hour.ToString("D2")); datestring.Append(":"); datestring.Append(dt.Minute.ToString("D2")); datestring.Append(":"); datestring.Append(dt.Second.ToString("D2")); return datestring.ToString(); } /// /// Gets the current time in unixtime /// /// The current time in unixtime private static double GetTimeInUnixTime() { return Math.Round(((DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0)).TotalSeconds), 0); } #endregion /// /// Constructs a blob header for uploading /// /// A string containing a blob header private string GetBlobHeader() { StringBuilder blobString = new StringBuilder(); //{ "dt":"h", // data type, h for header // "pa": int, // persistent store created at // "seq": int, // blob sequence number, incremented on each new blob, // // remembered in the persistent store // "u": string, // A unique ID for the blob. Must be the same if the blob is re-uploaded! // "attrs": { // "dt": "a" // data type, a for attributes // "au":string // Localytics Application Id // "du":string // Device UUID // "s":boolean // Whether the app has been stolen (optional) // "j":boolean // Whether the device has been jailbroken (optional) // "lv":string // Library version // "av":string // Application version // "dp":string // Device Platform // "dll":string // Locale Language (optional) // "dlc":string // Locale Country (optional) // "nc":string // Network Country (iso code) (optional) // "dc":string // Device Country (iso code) (optional) // "dma":string // Device Manufacturer (optional) // "dmo":string // Device Model // "dov":string // Device OS Version // "nca":string // Network Carrier (optional) // "dac":string // Data Connection Type (optional) // "mnc":int // mobile network code (optional) // "mcc":int // mobile country code (optional) // "tdid":string // Telephony Device Id (meid or imei) (optional) // "wmac":string // hashed wifi mac address (optional) // "emac":string // hashed ethernet mac address (optional) // "bmac":string // hashed bluetooth mac address (optional) // "iu":string // install id // "udid":string } } // client side hashed version of the udid blobString.Append("{\"dt\":\"h\","); blobString.Append("\"pa\":" + GetPersistStoreCreateTime() + ","); string sequenceNumber = GetSequenceNumber(); blobString.Append("\"seq\":" + sequenceNumber + ","); SetNextSequenceNumber((int.Parse(sequenceNumber) + 1).ToString()); blobString.Append("\"u\":\"" + Guid.NewGuid().ToString() + "\","); blobString.Append("\"attrs\":"); blobString.Append("{\"dt\":\"a\","); blobString.Append("\"au\":\"" + this.appKey + "\","); blobString.Append("\"du\":\"" + GetDeviceId() + "\","); blobString.Append("\"lv\":\"" + LocalyticsSession.libraryVersion + "\","); blobString.Append("\"av\":\"" + GetAppVersion() + "\","); blobString.Append("\"dp\":\"Windows Phone\","); blobString.Append("\"dll\":\"" + CultureInfo.CurrentCulture.TwoLetterISOLanguageName + "\","); blobString.Append("\"dma\":\"" + Microsoft.Phone.Info.DeviceStatus.DeviceManufacturer + "\","); blobString.Append("\"dmo\":\"" + Microsoft.Phone.Info.DeviceStatus.DeviceName + "\","); blobString.Append("\"dov\":\"" + Environment.OSVersion.Version.Build.ToString() + "\","); blobString.Append("\"iu\":\"" + GetInstallId() + "\""); blobString.Append("}}"); blobString.Append(Environment.NewLine); return blobString.ToString(); } /// /// Formats an input string for YAML /// /// string sorrounded in quotes, with dangerous characters escaped private static string EscapeString(string input) { string escapedSlahes = input.Replace("\\", "\\\\"); return "\"" + escapedSlahes.Replace("\"", "\\\"") + "\""; } /// /// Outputs a message to the debug console /// private static void LogMessage(string msg) { Debug.WriteLine("(localytics) " + msg); } #endregion #region public methods /// /// Creates a Localytics Session object /// /// The key unique for each application generated at www.localytics.com public LocalyticsSession(string appKey) { this.appKey = appKey; // Store the time and sequence number } /// /// Opens or resumes the Localytics session. /// public void open() { if (this.isSessionOpen || this.isSessionClosed) { LogMessage("Session is already opened or closed."); return; } try { if (getNumberOfStoredSessions() > LocalyticsSession.maxStoredSessions) { LogMessage("Local stored session count exceeded."); return; } this.sessionUuid = Guid.NewGuid().ToString(); this.sessionFilename = LocalyticsSession.sessionFilePrefix + this.sessionUuid; this.sessionStartTime = GetTimeInUnixTime(); // Format of an open session: //{ "dt":"s", // This is a session blob // "ct": long, // seconds since Unix epoch // "u": string // A unique ID attached to this session // "nth": int, // This is the nth session on the device. (not required) // "new": boolean, // New vs returning (not required) // "sl": long, // seconds since last session (not required) // "lat": double, // latitude (not required) // "lng": double, // longitude (not required) // "c0" : string, // custom dimensions (not required) // "c1" : string, // "c2" : string, // "c3" : string } StringBuilder openstring = new StringBuilder(); openstring.Append("{\"dt\":\"s\","); openstring.Append("\"ct\":" + GetTimeInUnixTime().ToString() + ","); openstring.Append("\"u\":\"" + this.sessionUuid + "\""); openstring.Append("}"); openstring.Append(Environment.NewLine); appendTextToFile(openstring.ToString(), this.sessionFilename); this.isSessionOpen = true; LogMessage("Session opened."); } catch (Exception e) { LogMessage("Swallowing exception: " + e.Message); } } /// /// Closes the Localytics session. /// public void close() { if (this.isSessionOpen == false || this.isSessionClosed == true) { LogMessage("Session not closed b/c it is either not open or already closed."); return; } try { //{ "dt":"c", // close data type // "u":"abec86047d-ae51", // unique id for teh close // "ss": session_start_time, // time the session was started // "su":"696c44ebf6f", // session uuid // "ct":1302559195, // client time // "ctl":114, // session length (optional) // "cta":60, // active time length (optional) // "fl":["1","2","3","4","5","6","7","8","9"], // Flows (optional) // "lat": double, // lat (optional) // "lng": double, // lng (optional) // "c0" : string, // custom dimensions (otpinal) // "c1" : string, // "c2" : string, // "c3" : string } StringBuilder closeString = new StringBuilder(); closeString.Append("{\"dt\":\"c\","); closeString.Append("\"u\":\"" + Guid.NewGuid().ToString() + "\","); closeString.Append("\"ss\":" + this.sessionStartTime.ToString() + ","); closeString.Append("\"su\":\"" + this.sessionUuid + "\","); closeString.Append("\"ct\":" + GetTimeInUnixTime().ToString()); closeString.Append("}"); closeString.Append(Environment.NewLine); appendTextToFile(closeString.ToString(), this.sessionFilename); // the close blob this.isSessionOpen = false; this.isSessionClosed = true; LogMessage("Session closed."); } catch (Exception e) { LogMessage("Swallowing exception: " + e.Message); } } /// /// Creates a new thread which collects any files and uploads them. Returns immediately if an upload /// is already happenning. /// public void upload() { if (isUploading) { return; } isUploading = true; try { // Do all the upload work on a seperate thread. System.Threading.ThreadStart uploadJob = new System.Threading.ThreadStart(BeginUpload); System.Threading.Thread uploadThread = new System.Threading.Thread(uploadJob); uploadThread.Start(); } catch (Exception e) { LogMessage("Swallowing exception: " + e.Message); } } /// /// Records a specific event as having occured and optionally records some attributes associated with this event. /// This should not be called inside a loop. It should not be used to record personally identifiable information /// and it is best to define all your event names rather than generate them programatically. /// /// The name of the event which occured. E.G. 'button pressed' /// Key value pairs that record data relevant to the event. public void tagEvent(string eventName, Dictionary attributes = null) { if (this.isSessionOpen == false) { LogMessage("Event not tagged because session is not open."); return; } //{ "dt":"e", // event data time // "ct":1302559181, // client time // "u":"48afd8beebd3", // unique id // "su":"696c44ebf6f", // session id // "n":"Button Clicked", // event name // "lat": double, // lat (optional) // "lng": double, // lng (optional) // "attrs": // event attributes (optional) // { // "Button Type":"Round" // }, // "c0" : string, // custom dimensions (optional) // "c1" : string, // "c2" : string, // "c3" : string } try { StringBuilder eventString = new StringBuilder(); eventString.Append("{\"dt\":\"e\","); eventString.Append("\"ct\":" + GetTimeInUnixTime().ToString() + ","); eventString.Append("\"u\":\"" + Guid.NewGuid().ToString() + "\","); eventString.Append("\"su\":\"" + this.sessionUuid + "\","); eventString.Append("\"n\":" + EscapeString(eventName)); if (attributes != null) { eventString.Append(",\"attrs\": {"); bool first = true; foreach (string key in attributes.Keys) { if (!first) { eventString.Append(","); } eventString.Append(EscapeString(key) + ":" + EscapeString(attributes[key])); first = false; } eventString.Append("}"); } eventString.Append("}"); eventString.Append(Environment.NewLine); appendTextToFile(eventString.ToString(), this.sessionFilename); // the close blob LogMessage("Tagged event: " + EscapeString(eventName)); } catch (Exception e) { LogMessage("Swallowing exception: " + e.Message); } } #endregion }