Show more

Making Analytic Tools Swappable, Mockable, and Testable

As iOS Developers at TextNow, our dedication to our users requires us to ensure that the use of analytic tools can be maintained and relied on while not interfering with the user’s experience. Whether we are tracking how features perform, obtaining developer metrics, or simply logging data to understand how users interact with our app, analytics has become an integral part of TextNow and many other popular apps and continues to gain traction in the community.

How analytics are implemented varies from app to app, but one thing always stays the same: when the business’s needs change and a new analytics tool is introduced, the code is always impacted. Attempting to minimize horizontal change was the number one reason we opted for this framework.

Horizontal change is making many of the same changes across many files in a project.

Our aim when starting this project was to create a layer between us and the analytic tools that we use. We also aimed to solve the following problems that we saw with current implementation of analytic frameworks:

1. Lack of testability
2. Lack of mockability
3. View controller bloat
4. Difficulty juggling multiple tools

In this article, we will show how we built this analytic layer and the design decisions around the features we added to it.

If you are looking to implement this framework and would like to learn how please visit our GitHub page.

High level component diagram

To help with understanding how each piece fits together, we thought it’d be best to present it in a diagram. We suggest referring to this as you move through the article to give context to each component.


High level component diagram

To help with understanding how each piece fits together, we thought it’d be best to present it in a diagram. We suggest referring to this as you move through the article to give context to each component.

We kicked off our implementation with the most important part of the framework: the layer between us and our tools. We have generalized all analytic tools as EventTrackers. EventTrackers are responsible for understanding how to dispatch events to any tool you decide to use.

Creating Events

AnalyticsEvent wraps up event names and provides us with the supportedTrackers variable. The added variable lifts the responsibility of which event dispatches to which tool from our code (or view controllers in many cases), to our event implementation. Pulling out the responsibility of which tool an event tracks to from our view controller helps us reduce horizontal edits when tools change, and aids in bloat reduction in our view controllers.

Not only does the supportedTrackers variable reduce bloat, but this makes managing multiple tools much easier. From simply looking at it, you can see which event is dispatched to which tool and quickly make large edits in only one or two files.

Adding additional data to our events

Using multiple tools

As we saw previously in the AnalyticsEvent, we have supportedTrackers which is an array of strings to allow the core engine to know which tool each event is supposed to be dispatched to. We call these strings EventTrackerKeys and they are what the core of this framework uses to hash the initialized EventTrackers and store them.

The Core

The core of this framework is what puts all the pieces together. It holds reference to our EventTracker(s), it dispatches events (and parameters) to the proper tools and performs validation on events to ensure they are tracked properly.

  1. Send events (and parameters) to the EventTracker(s), keyed by the event’s supportedTrackers variable:

Keyed by our EventTrackerKey, we store our EventTracker(s) in a pre-initialized hash table. We don’t need clients reading the eventTrackers array, and we also don’t want clients to worry about thread safety so we implement the method addEventTracker(:) .


2. Send events (and parameters) to the EventTracker(s), keyed by the event’s supportedTrackers variable:

We want to expose two simple methods to the client, track an event, and track an event with parameters. Under the hood, they will execute very similar code and in order to support our generics, the parameters must have a type:

Now that our code that dispatches events is in one place, we can take a look at adding some error handling to it.

3. Validate that each of the AnalyticsEvent’s supportedTrackers have been added to the core using addEventTracker(:):

By default, we want to be aggressive about failing test conditions, but we also want to allow clients to define their own failure handlers if they are so inclined.

Now that we have the default FailureHandler abiding by the protocol we can write the initializers for the core.

In many cases it takes some time for certain tools to initialize and tracking events before this time can cause unanticipated results. We want to prevent that by adding the following conditions:

Preventing events from being dispatched that are EventTracker-less or contain an EventTracker that has not been added to our core will ensure that events that are sent to the core actually get tracked. Using these two conditions we can be certain that events are being properly sent to their respective EventTracker(s).

4. Validate the AnalyticsEvent follows the EventTracker‘s event name support with the isEventNameSupported(:) method:

Adding the final condition will ensure the event name that is being tracked is supported by it’s keyed EventTracker. As mentioned above, this will avoid issues when dispatching events to your analytic tool(s) that are unsupported.

Final Touches

Now that we have the logic in place, we will make one final addition to the core to ensure that it is mockable:

Clients can either use the singleton pattern that the framework has baked in: public static let shared: AnalyticsScope = Analytics() or they can manage access to it themselves.

Putting all of this together we have our final engine that will process all events/parameters and store our EventTracker(s).

Wrap up

Analytics will continue to change throughout the life of our applications, and we need to be prepared for it. Becoming reliant on a single tool can make it harder to move toward something new that may serve our apps better. Adding a layer between us and the analytic tools we use will most certainly pay off. If not for the ability to swap out tools, we should be doing it for the testability. We want to be able to rely on the data that we track even if we are the primary consumers of that data.

If you want to know more about how to implement this framework, please check out our GitHub page. The steps to install are in the README and there is more information on how you might use this framework in the Wiki. Along with the docs there is a sample app that implements this framework to give you some ideas on how to best implement it.

If you want to learn more about the work we do at TextNow and where you might fit in, check out our careers page.

Similar posts

No Comments

Leave a Reply

Your email address will not be published. Required fields are marked with *

This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.