App Analytics

One of the more daunting aspects of app development is the lack of direct contact with your customers.  With Apple serving as an intermediary, it can be difficult to know what folks like (or dislike!) about your product.  While we get a fair share of constructive feedback about our products, people occasionally post to iTunes or elsewhere online which isn’t regularly monitored.  Thus, it’s possible that we miss a really good idea or could prioritize features better based on usage.

Some make a claim for analytics as a developer tool to combat piracy but that seems less clear to me.  Knowing – even expecting – that a popular app will attract the attention of pirates should be something most developers understand and appreciate.  It’s not like the problem is unique to the App Store, nor is there a unique iPhone solution to combat it.  Piracy exists for all major computer platforms including consoles.  I’d like to think that the majority of iPhone users are honest and would rather pay a nominal amount for an app rather than jump through the required hoops (possibly making their phone less secure) to obtain something illegally.

I’ve been playing around with Pinch Media and their analytics package.  I put together a quick test app to poke around with the API and see how it performed.  It was pretty cool.  The basic process of getting it into an app was easy:

  1. Get the library.
  2. Copy the lib and header into your project.
  3. Include three frameworks: libsqlite, SystemConfiguration and CoreLocation.
  4. Start the data capture on startup.
  5. Stop the data capture on terminate.

Rather than call directly into the Pinch API, I wrote a simple singleton to wrap the interface.  I didn’t want events being triggered during development in the simulator, or in the event we want to add an app option/preference to “opt in”.

The implementation file is a bit “loose” – especially the handling of the static singleton which I’d probably cleanup if attempting to productize this code.  However, for experimentation purposes, the wrapper was a snap and looks something like this:

Analytics.h


#import <Foundation/Foundation.h>
@class CLLocation;

@interface Analytics : NSObject
{
}

+ (id)initAnalytics:(NSString*)appCode useCoreLocation:(BOOL)coreLocation useOnlyWiFi:(BOOL)wiFi;
+ (void)endAnalytics;
+ (id)shared;
- (void)startSubWithName:(NSString*)name timeSession:(BOOL)timed;
- (void)endSubWithName:(NSString*)name;
- (void)setLocation:(CLLocation*)location;

@end

Analytics.m


#import "Analytics.h"
#import "Beacon.h"

#if (!TARGET_IPHONE_SIMULATOR)
static Analytics *obj_Analytics = nil;
#endif

@implementation Analytics

#if (!TARGET_IPHONE_SIMULATOR)

- (id)init
{
   self = [super init];
   obj_Analytics = self;
   return obj_Analytics;
}

+ (id)initAnalytics:(NSString*)appCode useCoreLocation:(BOOL)coreLocation useOnlyWiFi:(BOOL)wiFi
{
   return [Beacon initAndStartBeaconWithApplicationCode:appCode useCoreLocation:coreLocation useOnlyWiFi:wiFi];
}

+ (void)endAnalytics
{
   [obj_Analytics release];
   obj_Analytics = nil;

   [[Beacon shared] endBeacon];
}

+ (id)shared
{
   if (obj_Analytics == nil)
   {
      obj_Analytics = [[self alloc] init];
   }

   return obj_Analytics;
}

- (void)startSubWithName:(NSString*)name timeSession:(BOOL)timed
{
   [[Beacon shared] startSubBeaconWithName:name timeSession:timed];
}

- (void)endSubWithName:(NSString*)name
{
   [[Beacon shared] endSubBeaconWithName:name];
}

- (void)setLocation:(CLLocation*)location
{
   [[Beacon shared] setLocation:location];
}

#else

+ (id)initAnalytics:(NSString*)appCode useCoreLocation:(BOOL)coreLocation useOnlyWiFi:(BOOL)wiFi { return nil; }
+ (void)endAnalytics {}
+ (id)shared { return nil;}
- (void)startSubWithName:(NSString*)name timeSession:(BOOL)timed {}
- (void)endSubWithName:(NSString*)name {}
- (void)setLocation:(CLLocation*)location {}

#endif

@end

After hooking in the startup/shutdown code in your app delegate, the idea is you scatter “markers” throughout your app to track or even time various events using the startSubWithName:timeSession method.

To use the API, you’ll need to register with the Pinch Media site to obtain a unique app-id that will aggregate various statistics on their server.  The library collects a number of stats by default including:

  1. OS version
  2. Device type
  3. Demographic information (if known)
  4. Regional usage (if using CoreLocation)
  5. Lifecycle data (app version, time used, number of actions, etc.)

And – all the various actions you scattered too.  The default charts on the web site are good but basic.  You can export the data to CSV or access it online via a nifty API.

While running a test over a few minutes, I noticed the library (r69) leaked a small amount of memory during the asynchronous send.  During the transfer, I didn’t detect a noticeable performance impact but then, the test app wasn’t exactly doing real time calculations either.  Instruments reported about 1 kb in the library thread.  The library appears to periodically send data perhaps after a certain time period or once the event queue fills.    The data is also sent when the app terminates – which apparently can cause Springboard to “force quit” your app and lead to a misleading crash report.

Viewing the data online took a while.  It was about 24 hours for the test data to show up on the website, and nearly 48 hours for the custom events to appear.

Linking with the library (and the other frameworks) added about 200 kb to the binary, although that may be slightly misleading as libsqlite3 was already being linked into the tester app.

I’m glad that the library also offered a feature to enable developers to expose “opt in” features as users can sometimes be sensitive to profiling, even if it’s anonymous.  Before hooking up the service to your app, it’d be worth checking out their privacy policy and making sure you’re not passing user specific data (which Pinch discourages and won’t allow) to their site.

If you’re looking for some insight into what your customers are doing with your app, linking in Pinch Media’s analytics library may be the answer.  From an ease of integration point of view, it was definitely very easy and worked as advertised.

Leave a Reply

Your email address will not be published. Required fields are marked *