Simple Installation Tracking for WP7 Apps

When I first started developing Remote, Microsoft had not yet provided a way for developers to see how many downloads their apps were getting. Microsoft now provides app download statistics via the App Hub, but the data is delayed by 6 days and only shows new installations – app upgrade statistics are not available. With this in mind, I decided to create a simple installation tracking system to monitor how well my app was doing in real time.

Looking through the App Hub Forums, I found a few other sales tracking systems. This one, based on Google AppEngine, tracks not only installations but usage activity and phone model and manufacturer information. This one is based on the Silverlight Analytics framework and uses Google Analytics for reporting.

My tracking code is much simpler and does not require any third-party frameworks, but it only reports new installations and upgrades. If you need to track feature usage information, you might be better off using one of the other solutions rather than trying to modify this code to your needs. Also, it’s worth noting that retrieving device manufacturer and model information requires an additional capability listing (“phone identity”) which may deter customers from wanting to try your app.

Implementation

Installation and upgrade notices will be sent to a publicly accessible web server via a standard HTTP POST request. A query string will be generated that takes the form of:

http://example.com/notify/?p=ProductName&v=1.1.0.0&t=u&pv=1.0.0.0&d=1

The components of this query string are as follows:

  • p – Product name
  • v – Version
  • t – Notification type, n for new installation and u for upgrade
  • pv – Previous version if this is an upgrade, not present otherwise (or if the previous version is unknown)
  • d – Debug, equal to 1 if this is a debug build, not present otherwise

Each time the app is launched, the FirstRunNotifier will check for a specific key stored in IsolatedStorageSettings.ApplicationSettings. If it exists, the value should contain a string with the last known installed version number. If that version number matches the current version number, no notification will be sent. If it is different (or if the key doesn’t exist), an installation or upgrade notice will be sent to the HTTP server.

FirstRunNotifier

The primary class is called FirstRunNotifier.

using System;
using System.Net;
using System.IO.IsolatedStorage;

namespace Komodex.WP7Tools
{
    // FirstRunNotifier
    // Copyright 2011 Matt Isenhower, Komodex Systems LLC
    // http://blog.ike.to/
    public static class FirstRunNotifier
    {
        // Notification URL parameters
        private const string NotificationURL = "http://example.com/wp7/notify/";
        private const string ProductName = "YourProductName";

        // Isolated storage key
        private const string FirstRunKey = "FirstRunCompleted";
        private static readonly IsolatedStorageSettings isolatedSettings = IsolatedStorageSettings.ApplicationSettings;

        public static void CheckFirstRun()
        {
            if (GetPreviousVersion() != Utility.ApplicationVersion)
                SendFirstRunNotification();
        }

        private static string GetPreviousVersion()
        {
            if (isolatedSettings.Contains(FirstRunKey))
                return isolatedSettings[FirstRunKey] as string;
            return null;
        }

        #region HTTP Request and Response

        private static void SendFirstRunNotification()
        {
            // Build the URL
            string url = NotificationURL + "?p=" + ProductName + "&v=" + Utility.ApplicationVersion;

            // Determine whether this is a new installation or an upgrade
            if (!isolatedSettings.Contains(FirstRunKey))
                url += "&t=n";
            else
            {
                url += "&t=u";
                string previousVersion = GetPreviousVersion();
                if (!string.IsNullOrEmpty(previousVersion))
                    url += "&pv=" + previousVersion;
            }

#if DEBUG
            url += "&d=1";
#endif

            // Submit the HTTP request
            try
            {
                HttpWebRequest webRequest = (HttpWebRequest)WebRequest.Create(url);
                webRequest.Method = "POST";
                webRequest.BeginGetResponse(new AsyncCallback(HandleResponse), webRequest);
            }
            catch { }
        }

        private static void HandleResponse(IAsyncResult result)
        {
            HttpWebRequest webRequest = (HttpWebRequest)result.AsyncState;

            try
            {
                WebResponse response = webRequest.EndGetResponse(result);
                response.GetResponseStream();

                // Update the version number stored in the isolated storage
                isolatedSettings[FirstRunKey] = Utility.ApplicationVersion;
                isolatedSettings.Save();
            }
            catch { }
        }

        #endregion
    }
}

CheckFirstRun is the only public member of this class and should be called each time your app is launched.

The GetPreviousVersion method attempts to retrieve the previously installed version number from isolated storage. The method will return the previous version number string, or null if the key doesn’t exist or if the value cannot be cast to a string.

On line 41, you might notice that the notification type (i.e., whether it is a new installation or an upgrade) is determined by checking whether the FirstRunKey exists in isolated storage. The reason this is used rather than checking GetPreviousVersion is that an earlier version of the notifier stored a boolean value in the isolated storage rather than a string. Because of that, GetPreviousVersion would return null for users upgrading from the earlier version regardless of whether the key exists, leading to inaccurate results. Similarly, if you are adding this class to an application you’ve already released in the Marketplace, you might want to use some other method to determine the notification type. (Otherwise, everyone who upgrades from the previous version of your app will be reported as a new installation.) For example, you could check for the presence of a specific key or file in isolated storage.

Make sure you test each possible new installation/upgrade scenario to make sure the code is working correctly before submitting your app to the Marketplace.

Server Issues

The version number stored in isolated storage is updated in the HandleResponse method only once the HTTP request has completed. If your server is unreachable or if an error occurs while recording the notification (indicated by a non-200 HTTP response) the isolated storage will not be updated and a notification will be attempted the next time the user launches your app.

Utility Class

I decided to put the code for retrieving the current application version in a separate Utility class because I need it in several places throughout the app. If you’ve already done something similar to retrieve the version number, you could replace the Utility.ApplicationVersion references in the FirstRunNotifier with your own method/property. Otherwise, just add the following Utility class to your project:

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
    }
}

There are a few other ways to determine the current application version number, but this parses it out from the executing assembly’s FullName.

App.xaml.cs

Now we just need to call the CheckFirstRun method when the application is launched. You can add it to the Application_Launching method in App.xaml.cs:

// ...
using Komodex.WP7Tools

// ...

private void Application_Launching(object sender, LaunchingEventArgs e)
{
    FirstRunNotifier.CheckFirstRun();
}

If you are using an alternate method to determine whether to send a new installation or upgrade notification (as described above), you should make sure that CheckFirstRun is called before other code that could potentially interfere with that logic.

Listening for Notifications

On the server side, all you need is a script to record each new installation or upgrade in a database. I use a Linux web server so I am using a PHP script with a MySQL database, but if you use a Windows-based server you could recreate the server-side code in ASP.NET pretty easily.

Database Table Structure

I have a few extra columns in the table (such as the query string and user agent) for my own debugging/logging purposes. You could remove these, but you might find them useful if anything goes wrong.

CREATE TABLE `notifications` (
  `id` int(11) NOT NULL auto_increment,
  `date` datetime default NULL,
  `ip` varchar(15) default NULL,
  `querystring` varchar(255) default NULL,
  `useragent` varchar(255) default NULL,
  `product` varchar(255) default NULL,
  `version` varchar(255) default NULL,
  `debug` tinyint(1) default NULL,
  `isupgrade` tinyint(1) default NULL,
  `previousversion` varchar(255) default NULL,
  PRIMARY KEY  (`id`)
) ENGINE=MyISAM AUTO_INCREMENT=1 DEFAULT CHARSET=latin1;

Notification Logger

Once you’ve created the database and notifications table, you can place the following PHP script in a file on your web server. I would suggest saving it as index.php in its own directory which, depending on your server configuration, may allow you to omit the filename in the NotificationURL property.

Also, make sure you update the username, password, and database name values with the necessary values for your server.


Reports

Now that your app is reporting new installations and your web server is recording them, you’ll need some way of tracking the number of installations. I created a small PHP page that displays the number of installations and the dollar amount before and after Microsoft’s 30%. The page will auto-refresh itself every 60 seconds so you can leave it open in a tab or its own browser window.

This PHP file should be placed in a password-protected directory on your web server. Update the MySQL values as necessary and set the dollar amount on line 16.








<?= $count ?> - <?= $actualmoney ?>



        
 

The SQL query will only count distinct IP addresses and does not count rows that have been marked as “debug.” This also makes it easy to filter out any rows that aren’t actually sales by just changing their debug value to 1. (For example, you could manually mark all the installations performed by Microsoft’s testing center as debug installations so they don’t interfere with your final numbers.)

Graphs

This graph shows the cumulative sales and upgrades over time since the app’s release. I am using the Highcharts.com JavaScript library to create the graph, and the following PHP file to generate the data for it:

 $lasttime, 'count' => 0);
    $lasttime = $time;
    
    // Add the version count to the array
    $versions[$v][] = array('time' => $time, 'count' => $versioncounts[$v]);

    if ($r['isupgrade']) {
        $pv = $r['previousversion'];
        // If the previous version isn't listed, assume it was the first known version
        if (!$pv) $pv = $firstversion;
        // Lower the count for the previous version
        $versioncounts[$pv]--;
        // The previous version count will be added to the $versions array below
    }
    
    // Output the values for the other version numbers to smooth out the chart display
    foreach ($versioncounts as $key => $value) {
        if ($key == $v)
            continue;
        $versions[$key][] = array('time' => $time, 'count' => $value);
    }
}

// Uncomment the following line to reverse the graph stacking order
//$versions = array_reverse($versions);
?>



    
        
        Sales Graph
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
    
    
        
        

Make sure you download the Highcharts JavaScript files from their site and include them in the directory you are hosting the graph from.

Other graph types, such as daily sales, upgrade percentages, etc. are possible, and should be pretty easy to make with this JS library. If I make any more graphs I will update this blog post.

Caveats

How accurate is the data provided by this system?
For me, the accuracy level is “good enough.” If someone uninstalls and reinstalls the app, that would show up as a new installation. If someone purchases your app and installs it on multiple devices, that will show up as a new installation (which would make income reporting inaccurate).

You could improve the accuracy by tracking unique device or user IDs but then your app’s required capabilities would have to list “phone identity,” “user identity,” or both. I’m just looking for an approximate number of installations so I don’t need that level of accuracy, and so far the numbers reported by this system have been extremely close to what the App Hub is reporting.

What about upgrades?
If a user installs an upgrade but doesn’t actually run the app, the upgrade will not be reported.

Will this track trial installations or trial-to-full version upgrades?
Not currently, but it wouldn’t be too difficult to add. I will update this post if I add that at some point in the future.

What if I change my app’s price?
Price changes aren’t currently handled by this code but my suggestion would be to add a price column to the notifications table and set it in the notification logger code. Then you could perform a sum operation in your SQL query to see the total amount for the various price points.


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.