Monday 16 October 2017

In-app analytics for iOS apps: Architecture tips

In the last few months we added a third party app analytics solution i.e. Firebase to My Day To-Do and soon we realised that Firebase may not work every time. Hence we tried to abstract the analytics (decouple/separate) solution from the rest of the app code i.e. the app code can log analytics data without knowing about what analytics solution is in use. In this post I will talk about our analytics solution and provide some code examples for an iOS app with Firebase analytics. The aim of the code is to provide an example of how to abstract the analytics part from the rest of the app. Hence without further a due let's get to it.

Background


In June 2017, we added a third-party analytics solution i.e. Firebase analytics to My Day To-Do for tracking app usage i.e. we wanted to know what aspects of our product do people use the most. At this stage I am the only developer at My Day To-Do and it was the first time I had worked with an analytics solution. Anyway I added an analytics solution and it was all fine and dandy but then I relied that Firebase may not work in all situations (e.g. China). At this point, I thought why not abstract the analytics solution from the rest of the app, so the app code does not care about what analytics solution is being used. This lead me to  embark on a quest to restructure the app analytics solution in My Day To-Do.

Problem


Design an analytics solution in the app such that you can change the existing (third-party?) analytics solution without effecting the code for the rest of the app.


Solution

First let's examine how Firebase Analytics was initially integrated into My Day To-Do,

Firebase and My Day To-Do


Firebase Analytics let's you log events and when I first added it to my app, I had  a method called logEventWithFirebase in one of the classes coupled with the rest of the app code. The Firebase event that I used the most was select_content which accepts 3 parameters (id, type, name). The code to log a select_content event is the same every time, the only thing that changes are the 3 parameters, so when designing that I thought let's just store the 3 parameter values for different select_content events in one location. 

Analytics Constants


My goal for this solution was to have a file that houses (stores) all the different events that we are logging making it easier to track the events being logged. To accomplish that goal, I created a class called AnalyticsConstants where I defined a struct called ANALYTICS_CONSTANT with three properties id, name and type. The AnalyticsConstants class has several static properties of type ANALYTICS_CONSTANT that correspond to the events we are logging with Firebase. Confusing? yeah I am confused by that statement too, so how about we just look at some code to get an idea

struct ANALYTICS_CONSTANT {
    var id = ""
    var type = ""
    var name = ""
    init(id:String, name: String, type: String) {
        self.id = id
        self.name = name
        self.type = type
    }
}
static let A_TODO_VISIT = ANALYTICS_CONSTANT(id: "a_tab", name: "TodoTabVisit", type: "PageView")
btw that's Swift code from the My Day To-Do iOS app 

this is what the logEventForFirebase function look like,

func logEventForFirebase(analyticsConstant: AnalyticsConstants.ANALYTICS_CONSTANT) 
{
    Analytics.logEvent(AnalyticsEventSelectContent, parameters: [
        AnalyticsParameterItemName: analyticsConstant.name as NSObject,
        AnalyticsParameterItemID: "id-\(analyticsConstant.id)" as NSObject,
        AnalyticsParameterContentType:  analyticsConstant.type as NSObject
    ])
}
and now for how it's called in code

logEventForFirebase(AnalyticsConstants.A_TODO_VISIT)

At this point we have events being logged with Firebase, all the different moving parts are in place and it all works, but I noticed something. I noticed that despite a large number of downloads from China the app usage in China is fairly insignificant compared to say 'Germany' which has far fewer downloads. The reason being that Firebase Analytics is not accurate when it comes to reporting app usage in China, therefore I may need to change the analytics solution from Firebase to something else. At this point, I decided to re-think this entire process, actually let's go back to the basics and examine this entire process starting with...

Why we build apps? 
  • to solve a problem 
  • so an app is a solution to a problem 

Fantastic, so how do we build apps?
  • we design the UI 
  • code the logical app flow and 
  • add a means of storing data etc etc
Now we can look at the UI design, logical app flow, storing data etc as smaller problems that we need to solve to solve a bigger problem i.e. building an app. Therefore it's not too difficult to think of our app as a big solution composed of a number of smaller solutions, consequently we can see adding app analytics as another smaller solution.

A central location


After thinking about it that way, it would make sense to add analytics to our app via a single class that stores all the methods to log analytics data which the rest of the app code can call. This leads to the thought of having the AnalyticsHelper, which looks like this,

static func logEvent(analyticsConstant: AnalyticsConstants.ANALYTICS_CONSTANT) 
{

    Analytics.logEvent(AnalyticsEventSelectContent, parameters: [
        AnalyticsParameterItemName: analyticsConstant.name as NSObject,
        AnalyticsParameterItemID: "id-\(analyticsConstant.id)" as NSObject,
        AnalyticsParameterContentType:  analyticsConstant.type as NSObject
    ])
}
So whenever we need to log an event for our app, all we do is call AnalyticsHelper.logEvent that's all. The logEvent method can log the event to any analytics service, it could be Firebase, Fabric, Google analytics or whichever other solution, the app code does not need to know that. 

Having said that the app does have one dependency and that's AnalyticsConstants, but that's unavoidable and needless to say that AnalyticsHelper and AnalyticsConstants need to know about each other. Therefore it would only make sense to store them under the same folder for ease of understanding, so let's create a folder in our app called Analytics and have both the files in there.

How does it all fit together?

In the interest of clarity I think it would make sense to look how all this code fits together in an iOS app, let's look at our viewController

import UIKit
class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
    }
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }
    @IBAction func todoVisit(_ sender: Any) {
        AnalyticsHelper.logEvent(AnalyticsConstants.A_TODO_VIST)
    }
}
It's fairly self-explanatory, it's has a an event that's triggered when the button is pressed and we are logging that event using our AnalyticsHelper. Next let's look at our AnalyticsConstants class

import Firebase
class AnalyticsHelper {

    static func logEvent(analyticsConstant: AnalyticsConstants.ANALYTICS_CONSTANT) 
    {

        Analytics.logEvent(AnalyticsEventSelectContent, parameters: [
           AnalyticsParameterItemName: analyticsConstant.name as NSObject,
           AnalyticsParameterItemID: "id-\(analyticsConstant.id)" as NSObject,
           AnalyticsParameterContentType:  analyticsConstant.type as NSObject
        ])
    }
}
and finally the AnalyticsConstants class

import Foundation
/*
 Acronyms
 A = access i.e. access a section
 P = push i.e. button push 
 */
class AnalyticsConstants {

    struct ANALYTICS_CONSTANT {
        var id = ""
        var type = ""
        var name = ""
        init(id:String, name: String, type: String) {
            self.id = id
            self.name = name
            self.type = type
        }
    }
    static let A_TODO_VISIT = ANALYTICS_CONSTANT(id: "a_tab", name: "TodoTabVisit", type: "PageView")
    static let P_LOGIN = ANALYTICS_CONSTANT(id: "p_login", name: "Login", type: "Button")
}

The only additional property is if we were tracking user login, also I have just named the variables with the starting letter for the type of event it's associated to i.e. "button push" and "access" certain sections of the app.

p.s. if you need to know more about that entire China situation, My Day To-Do download numbers and Firebase Analytics, you can read my previous blogpost where it's extensively discussed.

Summary


I would encourage anyone adding an analytics solution to their app to use or at least consider a viable alternative to the solution discussed here. The feasibility of a third-party analytics solution for a product can change over time therefore I think it's best to try and prepare for this by ensuring that the Analytics solution code is not tightly coupled with the app code. 

Finally, I am working on my app full-time right now so if you find my blog posts useful and want to support me you can 

    • Or download the Lite(free) version and leave us an App Store review

    No comments:

    Post a Comment