Showing posts with label windows phone 7. Show all posts
Showing posts with label windows phone 7. Show all posts

Sunday, January 8, 2012

Windows Phone 7: Getting Started with Analytics

Initially, I went with PreEmptive's RTI which was great at first, although I didn't like running the tool to generate the XAP nor the restrictions of using attributes.  It was worth it though for a while.  When RTI got too latent and the freeness waned, I looked to MSAF.  I was directed to a great article to get started "Statistics for your Windows Phone application..." by Mark Monster.  This may be enough for you to get up and running.  Again, it's a great start but turned out to not be enough for me.

So what happened?
Initially, with a couple statistics being logged, everything seemed promising so I started sprinkling my version of Mark's AnalyticsTracker throughout my code mostly replacing the spots where I had used RTI.  I launched the debugger on the Emulator and....it exited.  I thought that was weird.  Since I was using the debugger, I expected any exception to break into Visual Studio.  You may have experienced this before--an exception occurs early in your program and it seems as if the debugger's not all hooked up by the time it happens.  What was a little more perplexing was that, occasionally, it would launch without issue.  This made me suspect a race condition...but with what?

The Race is on
Considering what code I had just added, I figured the AnalyticsTracker was being used before the AnalyticsService had started.  This was easy to verify by making the tracker check for the existence of the service and verify it was initialized.

So what to do about the events that occur too early?  I'm not too fond of just rearranging things until it works as it seems like a lurking issue that may show up on just the right device.  Instead, I made my AnalyticsTracker buffer the events that occur early on.

Here's the updated AnaylticsTracker class:

   /// <summary>
   /// Creates an analytics tracker which can be used to send events through the analytics provider.
   /// NOTE: Instances of this class should not be kept around and reused over time b/c initialization in
   /// the constructor may fail if it's created too soon and, if it does, events will be deferred continually.
   /// You can use it in such a scenario but only if you call <see cref="Connect"/> when <see cref="IsConnected"/> is false.
   /// </summary>
   public class AnalyticsTracker
   {
      /// <summary>
      /// Indicates whether tracking is enabled.  For debugging mostly.
      /// </summary>
      public static bool Enabled { get; set; }

      static readonly Queue<AnalyticsEvent> deferred = new Queue<AnalyticsEvent>();

      static AnalyticsTracker()
      {
         Enabled = true;
      }

      /// <summary>
      /// Creates a default instance.
      /// </summary>
      public AnalyticsTracker()
      {
         Connect();
      }

      /// <summary>
      /// [Re]attempts to connect to the analytics service.
      /// </summary>
      public void Connect()
      {
         if (!Enabled)
            return;

         // If already connected, we're good.
         if (IsConnected)
            return;

         var service = Application.Current.ApplicationLifetimeObjects.OfType<AnalyticsService>().SingleOrDefault();
         if (service != null && !service.IsStarted)
            return;

         try
         {
            CompositionInitializerEx.SatisfyImports(this);
            // Flush any premature events.
            Flush();
         }
         catch (Exception ex)
         {
            Debug.WriteLine("Error creating analytics event. Deferring. {0}", ExceptionFormatter.FormatMessage(ex));
         }
      }

      /// <summary>
      /// Indicates whether this tracker is connected to the service.
      /// </summary>
      public bool IsConnected
      {
         get { return Log != null; }
      }

      /// <summary>
      /// Import from the analytics service provider.  Do not access directly.
      /// </summary>
      [Import("Log")]
      public Action<AnalyticsEvent> Log { get; set; }

      /// <summary>
      /// Logs or defers logging of an event.
      /// </summary>
      /// <remarks>If the analytics service is not yet up, the events are queued.</remarks>
      public void Track(string category, string name, string actionValue = null)
      {
         var analyticsEvent = new AnalyticsEvent { Category = category, Name = name, ObjectName = actionValue };
         if (Log == null)
         {
            lock (deferred)
               deferred.Enqueue(analyticsEvent);
         }
         else
            Log(analyticsEvent);
      }

      /// <summary>
      /// Track an exception.
      /// </summary>
      public void Track(Exception error)
      {
         Track("Errors", ExceptionFormatter.FormatName(error), error.ToString());
      }

      /// <summary>
      /// Flush deferred events.
      /// </summary>
      void Flush()
      {
         // Check before locking unnecessarily.
         if (deferred.Count <= 0) 
            return;
         
         lock (deferred)
         {
            while (deferred.Count > 0)
               Log(deferred.Dequeue());
         }
      }
   }

Issues with XAML Approach
There is a TrackAction trigger you can use in XAML.  I didn't like how this ended up impacting the organization of my data.  If you use this approach, you might want to take a look at the data coming into your provider before settling on it especially considering localization.

Other Things to Watch Out for
The Composition namespace.  This was frustrating.  Somehow I ended up adding a reference to the wrong Composition container.  If MEF can't find your imports, keep this in mind.  I noticed they included a CompositionInitializerEx in the WebAnalytics namespace.  I'd stick with this one to be sure you're using what they're using.
Don't use localized strings.  I touched on this above.  If any of the strings you use for your analytics are localized, you might find it hard to aggregate the data disregarding language.  For example, it'd be easy enough to track the current tool your users are choosing by its name (say CurrentTool.Name) but consider using the type instead if the tool's name is localized (e.g., CurrentTool.GetType().Name or CurrentTool.ToString()).

Wrapping Up
I hope this saves you time if you have any of these issues.  There are many more things to consider once you get analytics pumping.  For example, you might want to log the language your app is launched with.  By default, there's something logged automatically but there are issues.  I'll try to cover this in a more specific post.  In the meantime, try to think ahead about what you'll want to know.


Saturday, September 24, 2011

Windows Phone 7: Using the Compass

Want to add a compass to your Windows Phone 7 app? Want to get that AR app started? Most of us are in the same boat--we want to use the compass, motion, gyro, etc. but we can't. We've installed Mango but, without manufacturer-specific drivers, we're out of luck. If you're really anxious to get started anyway, let me share what I did and the lessons learned.

My first attempt was to create a set of "watchers".  I created a CompassVirtualWatcher and a CompassSensorWatcher both derived from an abstract base class, CompassWatcher.  The Sensor watcher implemented the base class functions using the actual Mango Compass API.  The Virtual watcher used a set of values that moved relatively randomly to simulate the compass function.

Having dealt with WP7 sensor APIs before, I knew the events would likely be coming in on a different thread.  Since I implemented the watchers using Observable (Microsoft.Phone.Reactive), it was easy to leverage ObserverOnDispatcher (or so I thought) to interact with the UI during updates.

The idea proved useful.  I was able to add a compass to my "Jack of Tools" app, play with it and see it seem to function.  I even simulated variant accuracy and the need for calibration so my calibration UI would pop up.

Once convinced the app worked, I eyed the CompassSensorWatcher for probably an hour just trying to catch any issues I might not see with the virtual watcher.  This included considering the potential threading issues once again but eventually I was convinced I had done my best.

Of course, the moment it hit real hardware with a compass, it locked up.  Now what?  I knew it must be something about working with the actual hardware but just could not figure out how to test that.  I posted in several places begging for drivers, emulators, ideas, etc. to no benefit.  Hence the reason for making this post.

Finally, an epiphany:  I didn't necessarily need compass hardware to recreate my bug.  Any hardware might be enough.  That is where the FauxCompass came in (my Flux Capacitor caliber idea of the year).  I would create an object derived from SensorBase<T> that implemented compass like functionality using the Accelerometer.

Of course, this was not so simple:

  1. SensorBase has only an internal constructor so there would be no deriving from that.
  2. Compass is sealed so there would be no deriving from it either.
  3. CompassReading has a public constructor but no public property setters so there'd be no implementing anything that returned CompassReadings.
This wasn't a show stopper but it made the implementation of the FauxCompass a little less elegant.  In the end, the signature is just like the Compass but using FauxCompassReading instead.  So, to switch between the regular Compass and the Faux Compass requires changing the code in four spots:  the member declaration, the constructor call, the observable declaration, and observable target's signature.

Once in place, I ran my app, switched to the Compass view, and it locked up.  "Damn it!  Now what?!" I thought.  You're probably already there but it took me a few seconds to really get it--that's exactly what I wanted.  My FauxCompass had repeated the bug that the real compass was producing and in short time.  I used it to fix the Compass bug in minutes.

So what did the FauxCompass do?  It consumed Accelerometer readings and converted them to FauxCompassReadings.  Turning the phone on one axis, produced headings from 0 to 359; turning on another axis, varied the accuracy resulting in a CalibrationNeeded event when it hit 20.

I hope this helps someone else waste less time with their compass app.  Please let me know if it does, if it doesn't, or if you have another solution.