Houston

Houston, we’ve had a problem… - a tracking helper

This is an introduction into one of my open source projects.

Houston

In my development past, we were mainly working on a specific product. In the products there was always some kind of tracking involved. (Note: I am talking about tracking for marketing/growth events. Not from a technical point of view (e.g. crash-reporting).)

Since this is coming with a lot of pain, I created a library to solve some of them.

The problems I am describing mainly occur in product development, not that much - but not excluding - in project work.

No pain - No gain ?!?

Of course the marketing team needs tracking in order to create user funnels and make sure they can do their job correctly. From a developer point of view this is fine as long as there’s ONE tool for it. In my experience it never stayed just one. We had to use several, because all of them had their own benefits. Meaning we had to implement (at least) 2 different tools, which also changed from time to time, because of very different reasons. Some became to expensive. Some, where a legal problem. Other ones, where just outdated.

That leads to a huge pain, when it comes to replace or remove a certain tracking tool, because everywhere - where you wanted to track a certain event - you had to touch the code again and again. This is not only time-consuming but also a very boring and frustrating job.

This is just one examples of how event-tracking would be done.

Example firebase analytics for android and some other imaginary tool.

1
2
3
4
5
6
7
8
9
10
11
12
13
// on successful login
loginHandler.onLoginSuccess { result ->
firebaseAnalytics.logEvent("login_result") {
param("result", if (result.isSuccess) "success" else "failed")
}
someOtherTool.send(
Event("login_result")
.add("type", "login_result")
.add("value", if (result.isSuccess) "success" else "failed")
.build()
)
// do business logic
}

Now try to imagine you have around 100 of those events. Removing is a pain, also it’s pretty much bloating the code.

If you’re working with internal libraries which are released independently of the release artifact itself, this is even worse. For every event-change you would need to release a new library in order to being able to build a new version of the release artifact.

How abstraction helps

The solution is abstracting the whole event-tracking. With that, you can create events within the code which are tracking-tool independent.

Tracking Tool abstraction

In order to abstract all tracking tools, you need to have a look how they mainly work. You will never get a 100% match, since every tracking tool has it’s own specifics (I’ll come later to an option for that).

Generalized we can say that every tracking tool needs some kind of initialization, where you add a key or some kind of secret to the instance. And every tracking tool has some method to send events.

Initialization we can use a constructor for and for event-sending we’ll create a method. So our abstracted interface looks like this:

1
2
3
interface TrackingTool {
fun send(id: String, data: Map<String, String>)
}

For the above example of Google (Firebase) Analytics, the implementation might look something like this:

1
2
3
4
5
6
7
8
9
10
class FirebaseAnalyticsTrackingTool(application: Application) : TrackingTool {

private val analytics: FirebaseAnalytics = FirebaseAnalytics.getInstance(application)

override fun send(id: String, data: Map<String, String>) = analytics.logEvent(id, generateBundle(data))

fun generateBundle(data: Map<String, String>): Bundle? {
// ... generate a bundle object from the received data.
}
}

Wrapping it together

Now we have (if required) multiple TrackingTools implemented. Now we can iterate through them, every time an event is sent and forward it to the implementations.

Our events in code could look something like this:

1
2
3
4
5
loginHandler.onLoginSuccess { result ->
TrackingAbstraction.send("login_result")
.with("result", if (result.isSuccess) "success" else "failed")
.push()
}

This piece of code is independent of any tracking tool.

Initialization

On order to set up the abstraction, we need to tell our tool, which tracking integrations we want to use.

1
2
3
4
5
TrackingAbstraction
.add(GoogleAnalyticsTrackingTool(/* parameters */))
.add(SomeOtherTracking(/* parameters */))
.initialize()
)

Further ideas

Enabling and disabling events during runtime.

For legal reasons users need to be able to opt in and out of sending any events. This can be done easily, by extending the TrackingAbstraction with an option to enable/disable the tracking. And then before we forward the events to the implementation we skip if tracking is disabled.

Backgrounding the actual send call

Most of the tracking frameworks collect their events and only send the items once in a while to the target backend. However, there are some which send those directly. For these cases, backgrounding the event sending is mandatory.

Wrap up

The library is no magic, nor you reduce a lot of code using it. I am very convinced about its benefits, and it paid off in production apps several times, I wanted to share this with you.