Decoupled Analytics
4 min read

Decoupled Analytics

Decoupled Analytics

While writing my first app, I've decided to try some analytics services. The most obvious one is, of course, Google Analytics (well, it's an android app after all!). Then, I found out that other services can provide analytics too, like Mixpanel. Unfortunately, most code I found online advocates adding the analytics code somewhere in the Application class (which makes testing a bit tedious).

To solve this issue, I've decided to decouple the analytics code from my application code and wrap it in a more limited API. The idea is that an interface (or abstract class) would define the API and I would have concrete implementations for it.

TL;DR: If you want some flexibility or just to try out analytics engines, don't embed it in your own code. Instead create a wrapper and use it. It makes your code cleaner and is easier to test. Also, Dagger is extremely useful.

The interface

To define the API, I thought what kind of metrics I want from the app. For a first phase, I only want two metrics:

  1. Events - a ping to tell me if something happened - track users' behaviour:

    public void sendEvent(
        String category, String label, String action
    );
    
  2. Timing - a value to tell me how much time it took to perform an action - profiling:

    public void sendTimeInterval(
        String category, String label, String variable, long ms
    );
    ``
    

The inspiration for this API is drawn from the GA requirements for the two types. Although initially the methods were part of an interface, the fact that we need a Context reference allowed me to create an abstract class. The abstract class resulted from this definition would be:

public abstract class AnalyticsProvider {

    public final static String TAG = "AnalyticsProvider";

    private Context mContext;

    public AnalyticsProvider(Context context) {
        this.mContext = context;
    }

    public Context getContext() {
        return mContext;
    }

    /**
      * Send an event to the tracker
      *
      * @param category the event category
      * @param label the event label
      * @param action the event action
      */
    public abstract void sendEvent(String category,
        String label, String action);

    /**
      * Send a timer notification to the tracker
      *
      * @param category the event category
      * @param label the event label
      * @param variable the event action
      * @param ms the value (in ms)
      */
    public abstract void sendTimeInterval(String category,
        String label, String variable, long ms);

    /**
      * Force flushing the data
      */
    public abstract void flush();
}

The implementations

For testing, I've created an inner class in AnalyticsProvider named VoidProvider. The code for both the abstract and inner classes is provided below.

An useful provider

The VoidProvider is useful for testing. For deployed apps, we need to implement e.g. a google analytics provider. Below, in the snippets section I've provided a simple implementation.

Usage

Now that we have the providers, we need a nice way to use them. Luckily, Dagger makes this a breeze:

@Provides
@Singleton
AnalyticsProvider provideAnalyticsProvider(Application context) {
    return new CompositeProvider(context);
}

Then, in your application class you can have something like:

class MyApp extends Application {
    @Inject
    AnalyticsProvider mProvider;

    // ...

    public AnalyticsProvider getAnalytics() {
        return mProvider;
    }

    // ...
}

Moreover, you can override the module code with something different for debug and production if you have configured your application that way (e.g. have look at U+2020).

Usage 2

Once the provider has been set up, you can just call it in your code like:

MyApp.get(mContext)
    .getAnalyticsProvider()
    .sendTimeInterval(TAG,
        "load",
        "global",
        delta
    );

All you need is access to a Context variable (which is available in most places).

Code snippets

The base class and implementation

public abstract class AnalyticsProvider {

    public final static String TAG = "AnalyticsProvider";

    private Context mContext;

    public AnalyticsProvider(Context context) {
        this.mContext = context;
    }

    public Context getContext() {
        return mContext;
    }

    /**
      * Send an event to the tracker
      *
      * @param category the event category
      * @param label the event label
      * @param action the event action
      */
    public abstract void sendEvent(String category, String label, String action);

    /**
      * Send a timer notification to the tracker
      *
      * @param category the event category
      * @param label the event label
      * @param variable the event action
      * @param ms the value (in ms)
      */
    public abstract void sendTimeInterval(String category, String label, String variable, long ms);

    /**
      * Force flushing the data
      */
    public abstract void flush();

    /**
      * A void provider which sends no data.
      */
    public final static class VoidProvider extends AnalyticsProvider {

        public VoidProvider(Context context){
            super(context);
            Log.d(TAG, "Created new VoidProvider");
        }

        /**
          * Only log the event (debug)
          *
          * @param category the event category
          * @param label the event label
          * @param action the event action
          */
        @Override
        public void sendEvent(String category, String label,
            String action) {
            Log.d(TAG, "sendEvent called");
        }

        /**
          * Only log the event (debug)
          *
          * @param category the event category
          * @param label the event label
          * @param variable the event variable
          * @param ms the value (in ms)
          */
        @Override
        public void sendTimeInterval(String category,
            String label, String variable, long ms) {
            Log.d(TAG, "sendTimeInterval called");
        }

        /**
          * Nothing happens here
          */
        @Override
        public void flush() {
            // empty
        }
    }
}

A concrete implementation

public class GoogleAnalyticsProvider extends AnalyticsProvider {
    Tracker mTracker;

    public GoogleAnalyticsProvider(Context context) {
        super(context);
        GoogleAnalytics analytics = GoogleAnalytics.getInstance(context);
        mTracker = analytics.newTracker(
            context.getResources().getString(R.string.ga_id));
    }

    /**
      * Send an event to the tracker
      *
      * @param category the event category
      * @param action   the event action
      * @param label    the event label
      */
    @Override
    public void sendEvent(String category,
        String action, String label) {
        mTracker.send(new HitBuilders.EventBuilder()
                        .setAction(action)
                        .setCategory(category)
                        .setLabel(label)
                        .build()
        );
    }

    /**
      * Send a timer notification to the tracker
      *
      * @param category the event category
      * @param label    the event label
      * @param variable the event action
      * @param ms       the value (in ms)
      */
    @Override
    public void sendTimeInterval(String category,
        String label, String variable, long ms) {
        mTracker.send(new HitBuilders.TimingBuilder()
                        .setCategory(category)
                        .setLabel(label)
                        .setVariable(variable)
                        .build()
        );

    }

    /**
      * Force flushing the data
      */
    @Override
    public void flush() {
        // GA tracker doesn't have a flush mechanism
    }
}

The Dagger module

@Module(
        complete = false,
        library = true
)
public final class DataModule {

    /**
      * Provide an instance of the AnalyticsProvider
      *
      * @param context the Context defined in the main module
      * @return an instance of the GoogleAnalyticsProvider in this case
      */
    @Provides
    @Singleton
    AnalyticsProvider provideAnalyticsProvider(Application context) {
        return new GoogleAnalyticsProvider(context);
    }
}

HTH,