Posted: (updated: )

mapping google analytics to an android game (with libGDX)

I'm working on a simple libGDX-based android game, and am trying to integrate Google Analytics (GA) into it so I can see how people (will) actually play the game, and which parts of the game they (will) use (or not use). GA's heritage is in tracking usage of a web site, so it does not directly map to tracking apps or gameplay. I found the documentation lacking, and the mapping from app concepts to GA features to be complicated and not entirely straight-forward (and its seems I'm not alone).

This post reflects what I learned from setting up Google Analytics in my first app. While I hope this will help others figure out GA, this is by no means definitive nor authoritative. I am using version 1.4.2 of the GA library.

I highly recommend setting up GA on a real website first to understand both the basic usage model of the tracking code, and also to understand how tracking data gets presented in the GA UI. A big part of mapping your app transitions and actions to the GA concepts is making sure the information you want to know shows up in the UI in a reasonable way. (You'll also learn that it takes 12 to 24 hours for results to show up in GA --- which makes for a somewhat ridiculous edit-compile-debug cycle. :)

The GA documentation is written in the classic dense technical style that does not make much sense until you already understand how it works (similar to most Linux man pages ...). Still, its worth reading the devguide to get an overview of what features are provided. And please read Alexander Lucas's Android Developer post to get your app setup correctly. Also, please pay attention to the "Abiding by the TOS" section at the end of that post. You are not allowed to send personally identifying information to GA servers.

Google Analytics Review

To review, and to get the terminology set for this post, there are four three basic concepts in GA:

  1. page views
  2. custom variables
  3. events
  4. ecommerce tracking

GA support for ecommerce tracking is not something I needed to deal with, so I ignored it. If you're supporting in-app purchases, you'll probably want to learn more about ecommerce tracking in GA. Otherwise, I think you can skip it.

Page views and events are simple enough.

Custom Variables

Custom variables are particularly unclear in the official documentation. As I understand it, each tracked page view can have at most 5 custom variables associated with it. The variables enable you to parametrize your tracked pages.

When you set a custom variable, you can set it to one of three lifetimes (or scopes):

  1. for the lifetime of this app's installation (a "visitor variable" -- GA will actually store it persistently, analogous to a cookie in a web browser). This variable will be passed on every pageview in this and all future sessions.
  2. for the lifetime of the current "session" (a "session variable" -- as long as this app is running). This variable will be passed on every subsequent pageview in this session.
  3. for the lifetime of the next "page" (a "page variable" -- associated only with the next call to trackPageView).

For a longer treatment on custom variables I found Justin Cutroni's explanation very good.

Its not at all clear to me what happens if you mix up your variable indexes (i.e., if you put a page variable in the same slot as a session variable). So, don't mix them up.

Note that custom variables show up in the GA UI in the "Audience" section, so even for page-level variables, consider the values you assign from the point of view of dividing up all your users.

Mapping apps to GA

There is quite a bit of overlap in how you can map actions or events in your app to these GA primitives. For example, when a button is clicked in your app, should that be a page view or an event or a change in a custom variable? Any of these can impart the same amount of information. The difference is in how useful that information will be --- that is how it will show up in the GA UI.

Game flow as Page Views

Given that the GA UI has good tools for showing how long a user was "on" a particular page, and good tools for showing the aggregate "flow" of many users through a set of pages, I think it makes sense to map the high-level flow through your game to page views. So, a 'start' page when the app starts up, the 'mainMenu' page for the main menu, and 'options' or 'playgame' or 'exit' pages for the various places the main menu can take you. Then once in the game, new pages views can cover major changes like going to a new level.

This mapping is not always clear. For example, if there are different ways to start the game (say starting a new game, and resuming a saved game), you probably don't want those to map to "newGame" and "resumeGame" pages (since really playing the game should be tracked the same way regardless of how the user started the game). But, you still want to know the breakdown of 'new' vs. 'resume' statistics, so that may make sense to track as a custom variable on the 'gameplay' page (vs. as two page views: 'newGame' followed by 'gameplay').

Game Events

Some game actions are better represented as events than pages. For example, in my game there is an "undo" button which will undo the last move. I'm not interested in seeing the undo as a pageview, as that would require a second pageview to get "back" to the game page, and would distort the time-spent-on-'game' pageview statistics. So, I use an Event. I use a category I call "History", with action "undo" and set the label to the kind of item that actually was undone (e.g, "dice", "checker", etc). The category/action/label distinction is very game-specific, and depends on what level of detail you want to see. (The GA UI will aggregate numbers at each level.)

If you want to track something "optional" within a game, for example, say there were several secrets hidden on a level. You could track those secrets being found with an Event, or as a custom variable on the 'levelXComplete' pageview. In many ways a page-level custom variable can often be interchanged for an Event, the big difference (as far as I can see) is that Events implicitly have a counter associated with them and can more compactly represent 100s of hits than a custom variable which uses a string for its data.

App Startup

By default the google analytics client library implicitly tracks basics like the make/model and screen resolution of the user's device, the network they're attached to, language and country, so you don't need to track those yourself.

I reserve one custom variable for my application instance UUID so I can track (in a non-personally identifiable way) a single installation of the app across sessions and time.

In my app, if any unusual conditions are detected at startup (e.g., a config file is missing or corrupted, or some audio files were not found), I have analytics track a "startupErrors" pageview before going to the normal "start" landing page. This "startupErrors" page has page-scoped custom variables for these basic errors. There is no corresponding data shown in the app's UI, so this page "view" is even more fictional than normal page views.

Implementation

To implement GA in libGDX I needed to isolate the Android-specific code from my game logic that issues tracking calls. I made a simple interface that maps very closely to the bits of the libGoogleAnalytics API that I use. On the desktop side I just implement this API with a simple class that logs each call, so I can at least see what tracking would be doing. (As far as I can tell, Google Analytics is not supported for non-Android Java applications.)

public interface AnalyticsEngine {
    void initialize();
    void setEnabled(boolean enabled);
    void trackPageView(String path);
    void setCustomVar(int slot, String label, String val, int scope);
    void trackEvent(String category, String subCategory, String label, int value);
    void dispatch();
}

If you're not using libGDX, you can ignore this interface and just invoke the libGoogleAnalytics methods directly.

Note, I'm assuming you have already handled the prerequisite steps: fetch and install the libGoogleAnalytics library, customize your application's manifest (if necessary), and setup an account at Google Analytics.

Initialization

To initialize the tracker, look up the global singleton, and start a session:

   GoogleAnalyticsTracker tracker = GoogleAnalyticsTracker.getInstance();
   tracker.startNewSession(gaAccount, appActivity);

Where gaAccount is the GA "UX-account-magic" String and appActivity is your Android application's current Activity.

Dispatch

GA can be configured to either dispatch any accumulated data periodically or explicitly when the dispatch method is invoked. Periodic updates are configured by the optional integer parameter to the 3-argument startNewSession (the timeout is in seconds), for explicit dispatch, use the 2-argument startNewSession method.

I chose to make the dispatch explicit. Periodic dispatch makes sense in a web browser where you don't have a lot of control over the life-cycle of the web page. In an Android application I have a control over start/suspend, etc and explicitly manage dispatch calls. On certain updates (E.g., suspend) I make sure to dispatch any pending data, otherwise I let updates accumulate a bit before invoking the underlying dispatch. I also have more direct control over the thread that is executing the dispatch logic this way, too.

Custom Variables

Because the custom variables have constraints and can overlap in their usage of indexes, I organized mine in an enum to keep them together and sane:

private enum CustomVar {
   // Visitor scope (all pages for all time)
   APP_ID(1, "AId", SCOPE_VISITOR), // installation ID

   // App startup page:
   // (1 is 'visitor-level' AId)
   APP_VER(2, "AVer", SCOPE_PAGE), // app version       

   // Custom Vars for the "startup error" page
   // 1 is 'visitor-level' AId 
   ERR_UUID(2, "UUIDErr", SCOPE_PAGE),
   ERR_UCE(3, "UCEErr", SCOPE_PAGE),
   ERR_EXCSTORE(4, "EStoreErr", SCOPE_PAGE),
   ERR_SND(5, "SndErr", SCOPE_PAGE),  
   ;

   public final int slot;
   public final String label;
   public final int scope;

   private CustomVar(int index, String label, VarScope scope) {
      this.slot = index;
      this.label = label;
      this.scope = scope;
   }
}

Testing

When debugging the tracker.setDebug and tracker.setDryRun methods are very handy. The setDebug method will send all GA updates to the log before sending them out on the network. The setDryRun method will prevent any updates from going out on the network.

Also, you can create a "test" property in your google analytics account to test your code against. That way you don't have to worry about polluting your real property with test/dev junk data.

Open Questions:

  • Is the library safe to invoke from my render thread? (Is the local API async?) I side-stepped the question by running the analytics code in a separate thread.
  • How should I version Analytics? Should I put a version in the URL? Currently I prefix all page views "URLs" with "/v1". If I make changes to name or meaning of data I'm collecting, I can change the prefix to "/v2", etc...

More Reading

For more details, here are some links that I found useful.

And of course once you understand GA, you can read the official documentation:

Acknowledgements & Feedback

Thanks to @iganapolsky for pointers to the forthcoming (as of July, 2012) v2 GA API which looks like it will be quite different (but will provide a lot more support for app semantics).

Comments or Questions?

Subscribe via RSS | Atom Feed