Simplifying Mobile Analytics Tracking at Scale
By
Afonso Rosa

Introduction
Analytics Tracking is not normally the first thing you implement for a new iOS Application. Before you even know what to track, you must have something to track. Typically tracking is an afterthought. More often than not, it is forgotten until someone asks questions about how the app is performing.
When you finally start working on tracking, you look at your code’s architecture and design patterns and wonder where and how you are going to insert it. This can be particularly challenging in big projects, especially when there are multiple teams working in the same codebase. Having tracking code in the middle of your classes and structures can damage code readability and trigger more conflicts during merge requests.
This article will show you how to implement Analytics Tracking in such an environment. By leveraging Aspect-Oriented Programming, we can keep our classes, structs and protocols tracking-free. This allows for parallel work within a given screen and can easily separate feature development from analytics coding.
The FARFETCH Approach
Bigger projects can bring unique problems, such as having repeated code, complex patterns only senior developers understand, and constant merge conflicts, among others.
Now, imagine we have two tasks:
- Update what a View is currently sending to its Tracking Provider
- Move some components of that View to a new location.
If the Tracking code is inside the View class, you can perform these tasks in sequence. Or you can combine them and, most likely, encounter conflicts when trying to merge both merge requests into the codebase.
We tried to avoid such problems by applying Aspect-Oriented Programming. By separating the tracking code from the View class, we could work on both tasks at the same time and merge them with minimal conflicts.
Two processes were very important to achieve our goals: Hooks and Swizzles.
Hooks would capture the calls of methods we wished to track, and the Swizzles would make sure we were in the correct context. Aspects don’t deal well with inheritance, hence the need for Swizzles. But we’ll discuss this later.
Afterwards, we moved all of this code into Tracking Handlers, classes whose sole purpose was to centralize, process and dispatch everything tracking related.
However, there still seemed to be some repeated code. This was especially true for code related to sending information to Analytics Providers. Therefore, everything that was generic or similar was centralized in Dispatchers, and for each screen, we created Tracking Coordinators.
These Coordinators have the responsibility to separate information for each provider and call the correct Dispatcher, delegating provider-specific processing to each instance.
Figure 1 below helps illustrate how we've organized the different aspects and how they are related. We’ll break out each of these in the following sections.


Hooks are blocks of code that are hooked to the methods, which will give us the information we need in order to Track a specific screen or action. This way, any extra processing, setup or data sending is performed elsewhere and not in the different classes and structs belonging to our project. The connection is established in runtime. Every time one of the hooked methods is called, the respective block will run.

You’re probably wondering, "What about methods with the same signature?”. This could happen, in which case we couldn’t tell which block would run.
However, thanks to our Aspect-Oriented Programming, our hooks are connected to their namespace. Therefore, you only need to worry about methods in classes with inheritance.
Unfortunately, if you have a child class, its namespace is also part of the parent class, making it impossible to know who called the hook. Thus we decided to implement Swizzles!

You're probably asking, "What are Swizzles?”. They are basically copies of a method’s original implementations. Whenever we’re in a child class that needs specific tracking for a method shared with its parent, we create a swizzle with a different name, saving the original implementation in an unsafe, mutable pointer.
This way, every time the original function in the child is called, the swizzled method is also called. Then we hook these new functions. Since they have original signatures, they won’t be called from the parent class or siblings.
One thing we need to be careful about is the memory they keep. The pointers to the original implementations are strongly referenced and will stay in memory until the app dies. That’s why, whenever we’re done with a particular screen, we should always deswizzle the methods. This frees their allocated memory and makes sure the hooks aren’t active anymore.
Swizzles are connected to the methods in the +load stage. Keep this in mind if you need to handle lots of tracking in the child class. If you don’t, you may not need a swizzle and you can reduce your time to launch.

Handlers

Depending on the screen, hooks and swizzles can require many lines of code. Therefore, we created Tracking Handlers. They are classes with a one-to-one relationship to a page, and they contain all the logic related to Tracking. Thus all the hooks, swizzles and tracking-specific processing we need is placed here.
It’s on the Handler initializer that we swizzle and hook the methods we need. In the deinit, we unswizzle everything. The Tracking Handler also implements the Application State Observer delegate, allowing us to know when the user sent the app to background or foreground.
All original implementations of the swizzle methods are stored here. We also store a Tracking Raw Info struct for the information we need to send.
This seems really cool, right? But how do we send the data to the different analytics providers? Well, that’s where the Coordinators get into the game.
Coordinators

Coordinators are also classes with a one-to-one relationship to a screen. There is always a protocol associated with them. The protocol implemented here receives the data from the delegate called in the respective Tracking Handler. Therefore, after we swizzle, hook and process everything we need, the Handler calls the delegate and sends the information to the Coordinator.
The Coordinator then creates the required dictionaries with this data, sending them to the respective Providers through their Dispatchers. This ensures we can send the same data to all of the providers without having to create everything anew.
The Dispatchers add the generic information every event that Provider requires, such as the timestamp and/or other attributes, and send everything to their Providers. Providers can be any Analytics Tracking framework, such as Firebase, Localytics and our very own Omnitracking.
With this approach, we end up with lean View Controllers without any Tracking code. All of the tracking complexity is contained in either the Tracking Handler or the Tracking Coordinator.
Inheritance, Generics and Protocols
You might be wondering, "Yeah, all the Tracking code is centralized, but this could be a huge boilerplate when you just need to track a couple things.” You would be correct: if you just need to check if the user entered and left the page, you are creating four new classes/structs/protocols just for this. We reached the same conclusion.
Moreover, when we had to explain to newcomers how this works, it was also quite hard for them to understand. This is why we used Inheritance, Generics and Protocols to substantially reduce the amount of code needed to implement tracking with our approach.
BaseTrackingHandler
One of the biggest and more complex classes we have related to tracking is the Tracking Handler. So we started here: a Base Tracking Handler that uses Generics. This way, whoever inherits from it can specify their respective View Controller, Tracking Delegate and Tracking Raw Info Struct.

This class already implements the necessary protocols each Tracking Handler needs, including checks if the user went to the background or returned and the protocol with all the basic functions a Handler should have. Moreover, it already has the swizzles and hooks necessary to track the user entering and leaving the screen.
Using this, when a new Tracking Handler is created, you just need to connect it to the respective View Controller, Tracking Delegate and Tracking Raw Info.
Then you can focus on developing only the specific analytics of that page.
Furthermore, it keeps all the references to the original implementations of the swizzle methods. Hence in the dealloc, it clears everything. We don't need to implement it in the child Handlers.
We also used Generics in the Swizzle Blocks. They occupied quite a few lines of code, and mostly repeated code. Therefore, we created internal swizzle functions that can receive any selector from any class and swizzle it.
The only thing we couldn’t generalize were the parameters. Thus there’s a block for each combination of parameter types we've required. While not ideal, most of the cases were repeated and allowed us to reduce the amount of code considerably.
TrackingDelegateProtocol
There are three interactions that are required for every page:
- Enter
- Exit
- Exit To Background
These are always needed to keep track of a screen and, in some cases, the only ones needed.
With this in mind, the TrackingDelegateProtocol was created with the respective methods. This makes it unnecessary to create new Delegates when you don’t need any specific analytics on a given page. And since the Base Tracking Handler already implements this protocol, that is three fewer methods you need to create in every delegate that implements it.

TrackingPageCoordinator Protocol
For the Tracking Coordinator, we created a protocol covering everything required of any page. This included common variables, such as a time tracker, to calculate the amount of time a user spent on the screen. It also included methods with default implementations, an example being the Did Enter View.
This code would have been repeated throughout all the Coordinators. Thus this protocol allowed us to reduce the duplicated code and streamline the boilerplate.

Templates
Although our approach considerably reduced the setup necessary for tracking a screen, creating the specifics of each could still benefit from a little automation. Therefore, we created Xcode Templates for every file, Tracking Handler, Tracking Delegate, Tracking Coordinator, Tracking Raw Info and Tracking Page for one of our providers.
With these templates, when you need to analyse a new screen, you just need to download the templates (if you don’t already have them) and create the new files with them.
Conclusion
While absolutely necessary, Analytics Tracking might not be a developer's favorite challenge. It can create a huge boilerplate, especially if you are trying to keep your View Controllers and similars clean of tracking code.
However, with a little investment in Aspect-Oriented Programming, generic base classes, setup, and configuration, it can make the implementation cleaner and faster. Like a good friend of mine likes to say "You need to sharpen your knives, before you start cooking”.