Rafael Sanches

June 13, 2011

Google Analytics lags on Android. How to make it more responsive!

Filed under: analytics, android, maintainability, performance — Tags: , , , — mufumbo @ 5:55 am

Google Analytics can be your best friend in order to track your mobile user behavior. Unfortunately the current Android implementation has performance limitations and the most problematic is that it uses SQLite to store your events.

Everyone who wants to write a responsive app knows that you can’t do SQLite operations in the UI Thread. Having to wrap the Google Analytics calls into a separated thread can be painful, so I wrote a very simple helper to handle it inside threads. I have many tracking events inside “button click” and it was taking about 200ms to execute, it’s too much on the UI Thread. It’s also too much if you have “onCreate” because it will take long time to open your new activity.

This helper is also very wrong because it maintains a static reference to the context. I do this in order to have better numbers on visit and “time on site”. You can just remove the static reference if you don’t like that.

Notice that my implementation has this: “Thread.sleep(3000);”
It means that I don’t want repetitive Google Analytics SQLite to be competing with my app inserts or gets.

This LAG happens because SQLite uses the internal memory which can be very slow depending on many factors, including concurrent SQLite operations or just internal memory without many space.

I hope it helps someone. Here’s the complete code:

package com.mufumbo.android.helper;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import android.content.Context;
import android.util.Log;

import com.google.android.apps.analytics.GoogleAnalyticsTracker;

public class GAHelper {
    String activity;
    static GoogleAnalyticsTracker tracker;
    static int instanceCount = 0;
    long start;

    // Limit the number of events due to outofmemory exceptions of analytics sdk
    final static int MAX_EVENTS_BEFORE_DISPATCH = 200;
    static int eventCount = 0;

    static final ExecutorService tpe = Executors.newSingleThreadExecutor();

    public GAHelper(final Context c, final String activity) {
        this.activity = activity;
        instanceCount++;
        if (tracker == null) {
            tpe.submit(new Runnable() {
                @Override
                public void run() {
                    tracker = GoogleAnalyticsTracker.getInstance();
                    tracker.start(Constants.GOOGLE_ANALYTICS_ID, Constants.GOOGLE_ANALYTICS_DELAY, c.getApplicationContext());
                }
            });
        }
    }

    public void onResume() {
        this.trackPageView("/"+this.activity);
    }

    public synchronized void destroy () {
        instanceCount--;
        if (instanceCount <= 0) {
            tpe.submit(new Runnable() {
                @Override
                public void run() {
                    Log.i(Constants.TAG, "destroying GA");
                    if (tracker != null)
                        tracker.stop();
                    instanceCount = 0;
                }
            });
        }
    }

    protected void tick() throws InterruptedException {
        Thread.sleep(3000);
        this.start = System.currentTimeMillis();
    }

    public void log (final String l) {
        if (Dbg.IS_DEBUG) {
            Dbg.debug("['"+(System.currentTimeMillis()-start)+"']["+eventCount+"] Logging on '"+this.activity+"': "+l);
            if (l.contains(" ")) {
                Log.e(Constants.TAG, "DO NOT TRACK WITH SPACES: "+l, new Exception());
            }
        }

    }

    public void trackClick(final String button) {
        checkDispatch();
        tpe.submit(new Runnable() {
            @Override
            public void run() {
                try {
                    tick();
                    tracker.trackEvent(
                            "clicks",  // Category
                            activity+"-button",  // Action
                            button, // Label
                            1);
                    log("trackClick:"+button);
                } catch (final Exception e) {
                    Log.e(Constants.TAG, "Error tracking", e);
                }
            }
        });
    }

    public void trackEvent (final String category, final String action, final String label, final int count) {
        checkDispatch();
        tpe.submit(new Runnable() {
            @Override
            public void run() {
                try {
                    tick();
                    tracker.trackEvent(
                            category,  // Category
                            action,  // Action
                            activity+"-"+label, // Label
                            1);
                    log("trackEvent:"+category + "#"+action+"#"+label+"#"+count);
                } catch (final Exception e) {
                    Log.e(Constants.TAG, "Error tracking", e);
                }
            }
        });
    }

    public void trackPopupView (final String popup) {
        checkDispatch();
        tpe.submit(new Runnable() {
            @Override
            public void run() {
                try {
                    tick();
                    final String page = "/"+activity+"/"+popup;
                    tracker.trackPageView(page);
                    log("trackPageView:"+page);
                } catch (final Exception e) {
                    Log.e(Constants.TAG, "Error tracking", e);
                }
            }
        });
    }

    public void trackPageView (final String page) {
        checkDispatch();
        tpe.submit(new Runnable() {
            @Override
            public void run() {
                try {
                    tick();
                    tracker.trackPageView(page);
                    log("trackPageView:"+page);
                } catch (final Exception e) {
                    Log.e(Constants.TAG, "Error tracking", e);
                }
            }
        });
    }

    public void checkDispatch() {
        eventCount++;
        if (eventCount >= MAX_EVENTS_BEFORE_DISPATCH)
            dispatch();
    }

    public void dispatch(){
        eventCount = 0;
        tpe.submit(new Runnable() {
            @Override
            public void run() {
                try {
                    tick();
                    tracker.dispatch();
                    log("dispatched");
                } catch (final Exception e) {
                    Log.e(Constants.TAG, "Error dispatching", e);
                }
            }
        });
    }
}

1 Comment »

  1. No notes on how to use the code. Tbh, I would have rather copied your code and looked over an example, than having to try to understand it. If I start reading it, I may just as well write my own…

    Comment by Radu — April 9, 2013 @ 1:08 pm


RSS feed for comments on this post. TrackBack URI

Leave a comment

Blog at WordPress.com.