Google Analytics for iOS

Sat, Oct 13 2012

Build-Measure-Learn. While this concept is definitely not new, even though the Lean Startup movement seems to claim so, it is still very important. Unfortunately, there are not too many options when it comes to measuring the usage of mobile apps.

An obvious solution would be to run your own service, something that I have been doing for years. Every API call was logged with a few extra parameters about the user. I also built a small tool to crunch the numbers. If somebody needed more detailed or specific statistics, I had to query the database directly. Another downside to this approach is that hosting this kind of service could eventually become costly depending on the amount of users.

So, I wanted to switch to a third party solution that handles big amounts of data easily and provides better analysis tools. The only three options that seemed reasonable at the time (July 2012) were Mixpanel, Flurry, and Google Analytics.

Other developers on Twitter seemed generally happy with Mixpanel but a look at their pricing table made me gulp. While I personally don’t mind paying for services, it seemed unreasonable to try to convince people at my company to spend a few $1,000 every year just for mobile statistics.

I registered a free account for Flurry and integrated their SDK into my app. Everything looked perfectly fine but after one hour of using the app, data points still would not show up in their web interface. It did not seem very realiable and I stopped exploring it.

Google Analytics is probably the most popular solution for tracking websites and also provides many upsides for apps:

Once you start using Google Analytics for mobile apps, it becomes obvious very quickly that the service was not built for that use case. However, you can bend it to your will.

The Google Analytics iOS SDK was available in version 1.5.1 at the point of writing this article. Rumor has it that they will release a completely new version 2.0, soon but for now we are stuck with this.

While the documentation is very good, there are few things that I want to point out:

UPDATE Dec 2012: Google has since released version 2.0 which addresses many of the issues listed below. You might want to read the migration guide if you are already using version 1.

Dispatch Queue

One thing that should be printed in bold red letters:

Calling GANTracker methods on different threads can result in obscure SQLite errors. Be sure to make all your calls from the same thread.

Since calling anything from the main thread that is not UI-related is generally a bad idea (because it might freeze up the UI), a shared serial dispatch_queue_t should be used.

+ (dispatch_queue_t)sharedGoogleAnalyticsQueue
{
  static dispatch_queue_t ganQueue;
  static dispatch_once_t onceToken;
  dispatch_once(&onceToken, ^{
    ganQueue = dispatch_queue_create("GoogleAnalyticsQueue", DISPATCH_QUEUE_SERIAL);
  });    
  return ganQueue;
}

Even though calls through a dispatch queue might not result in calls on the same thread, my experience is that it causes no problems since the queue ensures that the actions are performed serially.

Additional Information About the User

Not only the pageviews and events should be measured, but also various information about the user and his device.

The iOS version is logged automatically.

Audience → Technology → Browser & OS → Operating System → iOS

While the type of the device (iPad, iPhone, or iPod Touch) is visible too, no information about the model is available. I also found the information about the carrier to not be sufficient, since Google Analytics only logs the used network and not the actual carrier if the user is connected through WiFi. Another very important piece of information that is missing is the app version.

Fortunately, the SDK provides the option to pass up to five parameters to log additional information about the user when the connection is initialized. This dispatch typically happens in the application: didFinishLaunchingWithOptions: method of the application delegate alongside the initialization of the tracker.

dispatch_async([DGLDeviceUtils sharedGoogleAnalyticsQueue], ^{
  [[GANTracker sharedTracker] startTrackerWithAccountID:@"UA-00000000-1" dispatchPeriod:-1 delegate:nil];

  [[GANTracker sharedTracker] setCustomVariableAtIndex:1 name:@"app_version" value:[DGLPreferences appVersion] scope:kGANSessionScope withError:nil];
  [[GANTracker sharedTracker] setCustomVariableAtIndex:2 name:@"device" value:[DGLDeviceUtils machine] scope:kGANSessionScope withError:nil];

  CTCarrier **carrier = [DGLDeviceUtils carrier];
  if (carrier != nil) {
    [[GANTracker sharedTracker] setCustomVariableAtIndex:3 name:@"carrier" value:[carrier carrierName] scope:kGANSessionScope withError:nil];
  }
});

Notice how the dispatchPeriod is disabled since that’s the only way to ensure that the dispatch of the logged events to the server always happens on the same queue. So [[GANTracker sharedTracker] dispatch] has to be called either after each tracking action or from an own method that’s regularly invoked through an NSTimer. That should be decided based on the number of events that are usally dispatched.

[NSTimer scheduledTimerWithTimeInterval:20.f target:self selector:@selector(eventSubmitAnalytics:) userInfo:nil repeats:YES];
- (void)eventSubmitAnalytics:(NSTimer *)timer
{
  dispatch_async([DGLDeviceUtils sharedGoogleAnalyticsQueue], ^{
    [[GANTracker sharedTracker] dispatch];
  });
}

In case of using an NSTimer, you might also want to call [[GANTracker sharedTracker] dispatch] in applicationDidEnterBackground: and applicationWillTerminate: of the application delegate.

Custom Reports

The regular reports for these variables list the occurence per session.

Audience → Custom → Custom Variables

Usually it is more interesting to know how many unique clients are using a specific device model or app version, which is why custom reports have to be created.

Select “Custom Reporting” in the top navigation bar and create a new one. Create a report tab for each custom variable, select “Flat Table” as type, “Custom Variable (Value X)” as dimension, and “Unique Visitors” as metric.

This feature is obviously very powerful and can be used for much more sophisticated statistics.

Pageviews

Tracking Pageviews is simple and straightforward:

dispatch_async([DGLDeviceUtils sharedGoogleAnalyticsQueue], ^{
  [[GANTracker sharedTracker] trackPageview:@"/foo/bar" withError:nil];
});

In an app it’s sometimes hard to tell what a pageview actually is, since the concept of a page is not quite the same as on a website. Is it a new pageview if the user is selecting another view through an UISegmentedControl or is hitting the “Back” button in a UINavigationController? That’s all up for you to decide.

Events

To be able to track every event is a very powerful tool for an app. Basically every single button click can be logged in order to find out how often certain features get used.

Does the UISegmentedControl ever get used to sort the data differently? How often does the share button get hit? Did the user actually send the mail or tweet he started to compose? Did the user finish watching the video? Which items get marked as a favorite most often?

dispatch_async([DGLDeviceUtils sharedGoogleAnalyticsQueue], ^{
  [[GANTracker sharedTracker] trackEvent:category action:action label:label value:value withError:nil];
});

The value is an NSInteger and only makes sense in cases like tracking the seconds watched in a video. Category, action, and label are of the type NSString. A few examples of possible usage:

[[GANTracker sharedTracker] trackEvent:@"List View" action:@"SegContr" label:segmentIndex value:0 withError:nil];
[[GANTracker sharedTracker] trackEvent:@"Item" action:@"Add Favorite" label:item.itemAlias value:0 withError:nil];
[[GANTracker sharedTracker] trackEvent:@"Item" action:@"Share Mail Sent" label:item.itemAlias value:0 withError:nil];
[[GANTracker sharedTracker] trackEvent:@"Item" action:@"Video" label:@"Stopped" value:secondsWatched withError:nil];

Summary

In my opinion Google Analytics is a very good tool for mobile statistics. Even though it’s clearly not built for it, it still satisfies every need – if you know to use it right.

Have I missed anything? Do have you have better ideas? Please leave a comment, I’m always happy to listen to feedback.

comments powered by Disqus