Windows Phone 7 Analytics with Mixpanel

Having tried out the Microsoft’s WP7 analytics partner solution, PreEmptive, I was left underwhelmed by the tooling, UI and speed of reporting.  I like Google Analytics, but getting that working looked like a fair bit of inventive hackery and involves referencing the Silverlight Analytics Framework and who needs “yet another dll”™ in their app?  I’d also heard a few reports that the Google Analytics approach was pretty slow.  So inspired by Nick Gravelyn I thought I’d give Mixpanel a shot.

The beauty of the Mixpanel’s RESTful API is in its simplicity – there’s one method and practically everything you need to know about it is contained on this one page.  Each call logs an event and each event can have a collection of properties.  For example we log a “launch” event when our application starts up with the following properties: istrial, version, locale.  You don’t have to predefine any events or properties – just send them.

When you send an event, there are 2 properties you’re always going to need: token and distinct_id.  Token is the token Mixpanel generates for you when you create a project on their site and distinct_id is an arbitrary string you provide to define what makes a user unique.  On Windows Phone it makes sense to use the DeviceUniqueId from DeviceExtendedProperties.  This is returned as a byte array, so you’ll need to Base64 encode it to use as the distinct_id.

Finally Mixpanel wants your event object serialized into JSON (for which we can use Json.Net) and then Base64 encoded into a string.

Our TrackingEvent class inherits from Dictionary<string, object> to embody the name-value-pair nature of an event’s properties and encapsulates all of the serializing and encoding behaviour into a GetData method.  It looks a little something like this:

public class TrackingEvent : Dictionary<string, object>{
    private const string DistinctIdProperty = "distinct_id";

    // TODO: Your Mixpanel token goes here
    private const string MixpanelToken = "";

    private const string TokenProperty = "token";

    private readonly string eventName;

    public TrackingEvent(string eventName)
        this.eventName = eventName;

    public string GetData()
        if (!this.ContainsKey(TokenProperty))
            this.Add(TokenProperty, MixpanelToken);

        if (!this.ContainsKey(DistinctIdProperty))
            this.Add(DistinctIdProperty, GetDeviceUniqueId());

        JProperty[] properties =
            this.Select(keyValuePair => new JProperty(keyValuePair.Key, keyValuePair.Value.ToString())).ToArray();

        var json = new JObject(
            new JProperty("event", this.eventName), new JProperty("properties", new JObject(properties)));

        return Convert.ToBase64String(Encoding.UTF8.GetBytes(json.ToString()));

    private static string GetDeviceUniqueId()
        object deviceUniqueId;
        return DeviceExtendedProperties.TryGetValue("DeviceUniqueId", out deviceUniqueId)
                   ? Convert.ToBase64String((byte[])deviceUniqueId)
                   : "anon";

Next we need to actually send the HTTP request – for this we use the always excellent Reactive Extensions (Rx) which means we can easily make the call asynchronously using the thread pool and process the response in a terse and elegant manner.

For the purpose of this example, we’ll demonstrate this via a static Track() method and then explain what’s happening:

public static class Analytics
    public static void Track(TrackingEvent trackingEvent)
        var uriBuilder = new UriBuilder("")
               Query = string.Format(CultureInfo.InvariantCulture, "data={0}", trackingEvent.GetData()) 

        WebRequest webRequest = WebRequest.Create(uriBuilder.Uri);

            (webRequest.BeginGetResponse, webRequest.EndGetResponse)()
                    response => Debug.WriteLine("Response: {0}", response), 
                    exception => Debug.WriteLine("Error: {0}", exception.Message));

    private static string GetResponse(WebResponse webResponse)
        if (webResponse.ContentLength > 0)
            using (var streamReader = new StreamReader(webResponse.GetResponseStream()))
                return streamReader.ReadToEnd();

        return string.Empty;

If  you’re not familiar with Rx the FromAsyncPattern call might look a bit daunting, but it’s incredibly elegant.  It wraps the asynchronous Begin/End pattern and produces an IObservable which you can subscribe to.  Because it’s an IObservable we can instruct Rx to execute the subscription on the thread pool.  Next we use a Select to project the WebResponse returned into a string via the GetResponse() method and then subscribe to the result.

Mixpanel simply returns a 1 or a 0 to tell you whether your request was logged, so day-to-day you don’t really care about the response, but during development it’s handy to see what’s coming back.  It’s worth noting that in my discussions with Mixpanel the 1 or 0 is really just an indication of whether they managed to deserialize your JSON (at time of writing), so it’s possible you could get a 1 back but still not get data logged because you forgot to set your token property.  Luckily Mixpanel stats are “real time” – so it’s easy to check that you’re data’s getting through.

Finally we just make a call to our Track method in our codebase whenever we want to track an event:

    new TrackingEvent("launch")
            { "istrial", new LicenseInformation().IsTrial() },
            { "locale", Thread.CurrentThread.CurrentUICulture.Name },

Be aware that some values you might want to track, such as IsTrial, have overhead to obtain and should ideally be done once per session and stored, but you get the idea.  It’s probably also worth thinking about disabling tracking when you’re in debug/developing to save wasting data points or skewing your production figures.

It’s early days with Mixpanel, but so far we’re liking what we see.  If you’re planning to try Mixpanel and found this useful then feel free to signup via this referral link and we’ll both get an extra 5,000 data points and this delightful carriage clock… (clock not included).