Google Analytics for mobile apps in Unity

There are several solutions out there for integrating Google Analytics into Unity apps. However, as far as I can tell they all wrap the good old web tracking service. The guys at Google have long ago tailored a more suitable solution for mobile platforms: Mobile App Analytics. It provides a few mobile-only features (e.g.: user engagement, crashes and exceptions, device info) and has the added benefit of being official. Google have released SDKs for Android and iOS. In this post I will demonstrate how to integrate the Android SDK v2 into Unity apps. Be patient, iOS is on the road map.

Setup your Analytics Account

First of all you need to have a Google Analytics account. To use Mobile App Analytics you also need to add to your account an app tracking property and a profile. When you’re done you shall be the proud owner of a Tracking ID. It should look something like this: UA-123456-7.

Create an Android Plugin

I already covered the subject of creating an Android plugin in another post. I will expand on it to keep this post short.

Get the Analytics SDK

Download the Android SDK. (At the time of writing the version was 2.0 beta 5 and the download page warned that the SDKs are under “active development”.)  After downloading the ZIP file, extract libGoogleAnalyticsV2.jar from it (or whatever the JAR file is called) and add it to your Eclipse project.

This includes copying it to the libs directory and adding it to the build path. You may not see the JAR file in Eclipse until you refresh (F5). To add the JAR file to the build path right click on it and select Build Path => Add to Build Path.

MainActivity.java

We will subclass UnityPlayerActivity and use the EasyTracker class:

package com.companyname.appname;

import android.util.Log;

import com.google.analytics.tracking.android.EasyTracker;
import com.unity3d.player.UnityPlayerActivity;

public class MainActivity extends UnityPlayerActivity {
    @Override
    protected void onStart() {
        super.onStart();
        Log.d("Unity", "Starting Analytics activity");
        EasyTracker.getInstance().activityStart(this);
    }

    @Override
    protected void onStop() {
        super.onStop();
        Log.d("Unity", "Stopping Analytics activity");
        EasyTracker.getInstance().activityStop(this);
    }

    public void sendView(String screenName) {
        Log.d("Unity", "Sending Analytics view: " + screenName);
        EasyTracker.getTracker().sendView(screenName);
    }

    public void sendEvent(String category, String action, String label, Long value) {
        // value is optional
        Log.d("Unity", "Sending Analytics event: " + category + ", " + action + ", " + label + ", " + value);
        EasyTracker.getTracker().sendEvent(category, action, label, value);
    }

    public void sendTiming(String category, long intervalInMilliseconds, String name, String label) {
        // name and label are optional
        Log.d("Unity", "Sending Analytics timing: " + category + ", " + intervalInMilliseconds + ", " + name + ", " + label);
        EasyTracker.getTracker().sendTiming(category, intervalInMilliseconds, name, label);
    }
}

The calls to activityStart and activityStop are fundamental. They frame the session, allowing tracking of users, demographics and uncaught exceptions. Calling activityStart also sets the context for later sendXxxx calls to work.

The sendViewsendEvent and sendTiming methods allow manual sending of specific app information (screens, events and timings appropriately). These will be called by Unity scripts. By the way, don’t use trackXxxx methods – they’re deprecated.

There’s more stuff you can track which I haven’t covered here such as: campaigns, caught exceptions and in-app payments. See the dev guide for information on those.

You may want to know that EasyTracker is a wrapper for two other classes: GoogleAnalytics and Tracker. It makes life easier, but leaves out a few advanced features.

A note about onStop: When I pressed the Home button, my app was paused and onStop was called. When I called Application.Quit, it wasn’t. I don’t think it matters, but if it does to you feel free to read about session management.

analytics.xml

This is the configuration file for EasyTracker. Here we’ll setup our app’s Tracking ID as well as some other parameters:

<!--?xml version="1.0" encoding="utf-8" ?-->

    <!--Tracking ID-->
    UA-123456-7

    <!--Enable automatic activity tracking-->
    true

    <!--Enable automatic exception tracking-->
    true

    <!--Enable debug mode-->
    true

    <!--Set dispatch period in seconds-->
    120

Some of these parameters are self explanatory. And you can read about them and others here. A few notes though:

  • ga_debug – Enables writing of debug information to the log. The default is false (disabled). This may be useful for debugging, but be prepared to see error messages even when everything’s working smoothly (examples below.)  The output will be printed to logcat using the tag GAV2. For example, you can read both Unity’s logs and Analytics with the following command:
    adb logcat Unity:V GAV2:V *:S
  • ga_dispatchPeriod – The dispatch period in seconds. The default is 1800 (30 minutes). Yup, that’s a lot! It’s only 2 minutes on iOS. Feel free to reduce it. Setting it to 0 (zero) is useful for debugging. (But I suggest you don’t leave it that way in release versions.) Even then you can only find real-time information in the “Real-time” section in Analytics dashboard. Everything else is updated daily.
  • ga_appVersion – Your app version. No, this is not a mistake – it’s not used in the file above. No reason to! It defaults to the version found in the package, which in turn is determined by the bundle version in Unity’s player settings.

The analytics.xml file should generally be placed in the res/values directory in the project. However, the location doesn’t matter much until we copy it into the Unity project later. (It will reside in Assets/Plugins/Android/res/values.)

The Unity App

In your Unity app you’d probably want to call sendXxxx methods to track user engagement. The following Boo code calls the Java methods:

ifdef UNITY_ANDROID:
    static def Call(methodName as string, *args):
        jc = AndroidJavaClass("com.unity3d.player.UnityPlayer")
        jo = jc.GetStatic[of AndroidJavaObject]("currentActivity")
        jo.Call(methodName, *args)

    static def SendView(screenName as string):
        Call("sendView", screenName)

    static def SendEvent(category as string, action as string, label as string, value as long):
        joValue = AndroidJavaObject("java.lang.Long", value)
        Call("sendEvent", category, action, label, joValue)

    static def SendEvent(category as string, action as string, label as string):
        Call("sendEvent", category, action, label, null)

    static def SendTiming(category as string, intervalInMilliseconds as long, name as string, label as string):
        Call("sendTiming", category, intervalInMilliseconds, name, label)

The SendEvent method’s value parameter is optional. So are SendTiming method’s name and label parameters.

Adding the Plugin

To integrate the plugin we created into Unity:

  1. Copy the JAR file we created earlier to Assets/Plugins/Android.
  2. Copy libGoogleAnalyticsV2.jar to Assets/Plugins/Android.
  3. Copy analytics.xml to Assets/Plugins/Android/res/values. Create the subdirectories if needed.
  4. Edit your AndroidManifest.xml file in Assets/Plugins/Android and add the INTERNET and ACCESS_NETWORK_STATE permissions (see below).
AndroidManifest.xml

See this post for more information about this file. Here I will only mention the additions required by Google Analytics:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
  <uses-permission android:name="android.permission.INTERNET" />
  <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
  .....
</manifest>

Logcat

As I mentioned earlier it make take a while before you can see results online. So how can you tell if it’s working? Assuming you enabled debug mode (“ga_debug”) you should be able to get some feedback from the logs. However, some messages are quite confusing. Here are a few examples I encountered.

False error #1: Need to call initialize() and be in fallback mode to start dispatch

W/GAV2 ( 4211): Thread[main,5,main]: Need to call initialize() and be in fallback mode to start dispatch.
I/GAV2 ( 4211): Thread[main,5,main]: ExceptionReporter created, original handler is com.unity3d.player.ab

This error may appear when you call activityStart from your onStart method. I ignore it.

False error #2: connect: bindService returned false for Intent

I/GAV2 ( 3680): Thread[GAThread,5,main]: connecting to Analytics service
I/GAV2 ( 3680): Thread[GAThread,5,main]: connect: bindService returned false for Intent { act=com.google.android.gms.analytic
s.service.START (has extras) }
W/GAV2 ( 3680): Thread[GAThread,5,main]: Service unavailable (code=1), will retry.
I/GAV2 ( 3680): Thread[GAThread,5,main]: No campaign data found.
I/GAV2 ( 3680): Thread[GAThread,5,main]: putHit called

And then some:

I/GAV2 ( 3680): Thread[Service Reconnect,5,main]: connecting to Analytics service
I/GAV2 ( 3680): Thread[Service Reconnect,5,main]: connect: bindService returned false for Intent { act=com.google.android.gms
.analytics.service.START (has extras) }
W/GAV2 ( 3680): Thread[Service Reconnect,5,main]: Service unavailable (code=1), using local store.
I/GAV2 ( 3680): Thread[Service Reconnect,5,main]: falling back to local store
I/GAV2 ( 3680): Thread[GAThread,5,main]: Sending hit to store

It appears when the Analytics service tries to dispatch the data. This may be a few seconds after the app started, or maybe later if ga_dispatchPeriod is high. Anyway, it seems to happen consistently for me. It’s fine to ignore it, assuming it’s followed by good stuff.

Good stuff

Hopefully you’ll see something like this:

V/GAV2 ( 4211): Thread[GAThread,5,main]: dispatch running...
I/GAV2 ( 4211): Thread[GAThread,5,main]: User-Agent: GoogleAnalytics/2.0 (Linux; U; Android 4.0.4; en-us; GT-P7500 Build/IMM76D)
I/GAV2 ( 4211): Host: ssl.google-analytics.com
I/GAV2 ( 4211): GET /collect?................. HTTP/1.1
V/GAV2 ( 4211): Thread[GAThread,5,main]: sent 1 of 1 hits

Hits sent? You’re good to go.

A word on legality

The Google Analytics Terms of Service prohibit sending of any personally identifiable information (PII) to Google Analytics servers. So don’t.

Tagged , , , . Bookmark the permalink.

15 Responses to Google Analytics for mobile apps in Unity

  1. Ofer Reichman says:

    The dudes at Mass Threat published a solution that claims to work with Mobile App Analytics. I haven’t tested it, but I looked at the code and it seems they took a different approach. Instead of using the SDK they simulated its behavior by accessing the URL directly. It seems to support only screens at this point.

    I guess this solution has the advantage of working on both mobile platforms. However, it’s missing out on some great features that the SDKs offer, such as session management and uncaught exceptions. And, as it seems, they only support screens. Dunno, events are quite useful!

  2. Amir says:

    Hello.

    I almost 3 days on this issue trying to send hit to google analytics, and going round and round but cant find what im doing wrong,

    Im getting an error : Exception: java.lang.NoSuchMethodError: no method with name=’sendEvent’ signature='(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Object;)V’ in class Lcom/unity3d/player/UnityPlayerNativeActivity;

    this is my java code:

    @Override
    protected void onStart()
    {
    super.onStart();
    Log.d(“Unity”, “Starting Analytics activity”);
    EasyTracker.getInstance().activityStart(this);
    }

    @Override
    protected void onStop()
    {
    super.onStop();
    Log.d(“Unity”, “Stopping Analytics activity”);
    EasyTracker.getInstance().activityStop(this);
    }

    public void sendView(String screenName) {
    Log.d(“Unity”, “Sending Analytics view: ” + screenName);
    EasyTracker.getTracker().sendView(screenName);
    }

    public void sendEvent(String category, String action, String label, Long value)
    {
    // value is optional
    Log.d(“Unity”, “Sending Analytics event: ” + category + “, ” + action + “, ” + label + “, ” + value);
    EasyTracker.getTracker().sendEvent(category, action, label, value);
    }

    public void sendTiming(String category, long intervalInMilliseconds, String name, String label)
    {
    // name and label are optional
    Log.d(“Unity”, “Sending Analytics timing: ” + category + “, ” + intervalInMilliseconds + “, ” + name + “, ” + label);
    EasyTracker.getTracker().sendTiming(category, intervalInMilliseconds, name, label);
    }

    and this is my unity code:

    private static AndroidJavaClass mUnity;
    private static AndroidJavaClass mGAWrapper;
    private static AndroidJavaObject mCurrentActivity;

    public static void InitTracker(GoogleAnalyticsController controller,string _id, int dispatchPeriod, bool _isDryRun)
    {
    controller.message+=”init “;
    // Set Unity Class
    mUnity = new AndroidJavaClass(“com.unity3d.player.UnityPlayer”);
    // Set Jaca Class Wrapper
    mGAWrapper = new AndroidJavaClass(“com.unityplugin.gawrapper.GAWrapperActivity”);
    // Set current activity
    mCurrentActivity = mUnity.GetStatic(“currentActivity”);

    controller.message+=”init1.1 “;
    }

    public static void SendEvent(string category,string action, string label,long value)
    {
    AndroidJavaObject joValue =new AndroidJavaObject(“java.lang.Long”, value);
    mCurrentActivity.Call(“sendEvent”, category, action, label, joValue);
    }

    If you colud tell me what im doing wrong ?

    Thanks

    • Ofer Reichman says:

      Amir, can you please tell me what class are you extending?
      And did you create your own AndroidManifest.xml file in Assets/Plugins/Android?

      • Amir says:

        I’m extending UnityPlayerActivity Class although when im trying UnityPlayerNativeActivity its gives the same error.

        I have my own Manifest in the library, Assets/Plugins/Android.

        this is the code there.

      • Amir says:

        I’m extending UnityPlayerActivity Class although when im trying UnityPlayerNativeActivity its gives the same error.

        I have my own Manifest in the library, Assets/Plugins/Android.

  3. Amir says:

    I did it.

    My problem was my Manfiest, thanks for your help,
    Great Article !

  4. Thanks for this Ofer! This worked like a charm.

    FYI, I updated the scripts for GA 3.0, and they look like this:


    package com.yourcompany.yourgame;

    import android.util.Log;

    import com.google.analytics.tracking.android.EasyTracker;
    import com.google.analytics.tracking.android.Fields;
    import com.google.analytics.tracking.android.MapBuilder;
    import com.google.analytics.tracking.android.Tracker;
    import com.unity3d.player.UnityPlayerActivity;

    public class MainActivity extends UnityPlayerActivity {
    @Override
    protected void onStart() {
    super.onStart();
    Log.d("Unity", "Starting Analytics activity");
    EasyTracker.getInstance(this).activityStart(this);
    }

    @Override
    protected void onStop() {
    super.onStop();
    Log.d("Unity", "Stopping Analytics activity");
    EasyTracker.getInstance(this).activityStop(this);
    }

    public void sendView(String screenName) {
    Log.d("Unity", "Sending Analytics view: " + screenName);
    Tracker easyTracker = EasyTracker.getInstance(this);

    easyTracker.set(Fields.SCREEN_NAME, screenName);
    easyTracker.send(MapBuilder.createAppView().build());
    }

    public void sendEvent(String category, String action, String label, Long value) {
    // value is optional
    Log.d("Unity", "Sending Analytics event: " + category + ", " + action + ", " + label + ", " + value);
    EasyTracker.getInstance(this).send(MapBuilder.createEvent(category, action, label, value).build());
    }

    public void sendTiming(String category, long intervalInMilliseconds, String name, String label) {
    // name and label are optional
    Log.d("Unity", "Sending Analytics timing: " + category + ", " + intervalInMilliseconds + ", " + name + ", " + label);
    EasyTracker.getInstance(this).send(MapBuilder.createTiming(category, intervalInMilliseconds, name, label).build());
    }
    }

    I also made a subclass of UnityPlayerNativeActivity, which was essentially a clone:


    public class MainNativeActivity extends UnityPlayerNativeActivity {

    Then I changed Unity’s generated UnityPlayerProxyActivity:


    String classNames[] = { "com.yourcompany.yourgame.MainActivity", "com.yourcompany.yourgame.MainNativeActivity" };

    Best,
    Patrick

    • Ofer Reichman says:

      10x, Patrick! didn’t even know v3 was out…
      anyway, I can’t believe what a mess they’ve made of the interface.

  5. Cloud says:

    Integrate Google Analytics into Unity sounds awesome!!!
    However, I’ve encountered this error msg, and currently I have no idea why I couldn’t get access to it.

    “`
    10-09 15:57:03.300: INFO/Unity(5873): Exception: java.lang.NoSuchMethodError: no method with name=” signature='(Lcom.unity3d.player.UnityPlayerActivity;)V’ in class Ltw/com/starktech/unity/gaPlugin/UnityGAActivity;
    at UnityEngine.AndroidJNISafe.CheckException () [0x00000] in :0
    at UnityEngine.AndroidJNISafe.GetMethodID (IntPtr obj, System.String name, System.String sig) [0x00000] in :0
    at UnityEngine._AndroidJNIHelper.GetConstructorID (IntPtr jclass, System.String signature) [0x00000] in :0
    at UnityEngine.AndroidJNIHelper.GetConstructorID (IntPtr javaClass, System.String signature) [0x00000] in :0
    at UnityEngine._AndroidJNIHelper.GetConstructorID (IntPtr jclass, System.Object[] args) [0x00000] in :0
    at UnityEngine.AndroidJNIHelper.GetConstructorID (IntPtr jclass, System.Object[] args) [0x00000] in :0
    at UnityEngine.AndroidJavaObject._AndroidJavaObject (System.String className, System.Object[] args) [0x00000] in <

    Do you guys have any idea on that?
    BTW, I use GA 3.0.
    I just extend the `UnityPlayerActivity` but not `UnityNativePlayerActivity`, Should I switch to using that?
    Thank you and wait for you guys' reply. 🙂

    • Ofer Reichman says:

      I’m not sure what causes this, but I’m pretty sure you don’t need to extend UnityNativePlayerActivity, and I don’t suppose it’s GA 3.0.

      I’d first check to see that your plugin is integrated properly. make sure that you copied your plugin’s JAR to the right place and that the manifest file (AndroidManifest.xml) is updated.

      I explained it in more detail here: http://oferei.com/2013/06/serverless-instagram-authentication/

      And btw, it’s just me (singular) 🙂

  6. hieudev says:

    could you rewrite the boo code into csharp or javascript? sorry for my dumbness and thanks for the tutorial.

    • Ofer Reichman says:

      I translated it to C# for you, but I haven’t checked if it compiles (or works). But it should look something like this:


      #if UNITY_ANDROID
      class Analytics
      {
          static void SendView(string screenName)
      {
              AndroidJavaClass jc = new AndroidJavaClass("com.unity3d.player.UnityPlayer");
              AndroidJavaObject jo = jc.GetStatic("currentActivity");
              jo.Call("sendView", screenName);

          static void SendEvent(string category, string action, string label, long value)
      {
              AndroidJavaClass jc = new AndroidJavaClass("com.unity3d.player.UnityPlayer");
              AndroidJavaObject jo = jc.GetStatic("currentActivity");
              AndroidJavaObject joValue = new AndroidJavaObject("java.lang.Long", value);
              jo.Call("sendEvent", category, action, label, joValue);
      }
       
          static void SendEvent(string category, string action, string label)
      {
              AndroidJavaClass jc = new AndroidJavaClass("com.unity3d.player.UnityPlayer");
              AndroidJavaObject jo = jc.GetStatic
      ("currentActivity");
              jo.Call("sendEvent", category, action, label, null);

          static void SendTiming(string category, long intervalInMilliseconds, string name, string label)
      {
              AndroidJavaClass jc = new AndroidJavaClass("com.unity3d.player.UnityPlayer");
              AndroidJavaObject jo = jc.GetStatic("currentActivity");
              jo.Call("sendTiming", category, intervalInMilliseconds, name, label);
      }
      }
      #endif

  7. Simon says:

    Hello Ofer. Thank you very much for writing this article. I had analytics up and running within a couple hours. I do however get a warning when building, but it seems like it works anyways.

    I assume I’ve made some mistake in my manifest file, but this is the warning I get when building: Unable to find unity activity in manifest. You need to make sure orientation attribute is set to fullSensor manually.

    • Ofer Reichman says:

      Glad to hear!
      I don’t remember encountering this warning myself. But this answer seems to explain the reason for the warning (“you are not using the standard unity activities”) and the implications (your editor orientation settings would not apply). Hope that helps.

      (Are you extending UnityPlayerActivity? Maybe something to do with your AndroidManifest.xml file?)

Leave a Reply to Ofer Reichman Cancel reply

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