July 21, 2020

Adding A Layer Between You And Your Analytic Tools Using Swift

Written by quinton

Tips & Tricks

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.

The first two methods are used to track a single event, or an event with parameters. The third method here was a feature to check if an event name is supported by the wrapped analytic tool. In many cases you will swap which events dispatch to which tools and some tools have specific guidelines on the format of their event strings. Often these tools fail too gracefully, not alerting you that an event has not been tracked due to the malformation of the string.

For example, Adjust uses a very specific id string for its events:

As we can see, contracting EventTrackers to implement isEventNameSupported will prevent us from tracking events to tools like Adjust that do not support all string configurations.

Creating Events

All analytic tools, at the very least, have some string-based event that allows you to track certain metrics.

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

In many cases, especially for developer metrics, you will want to track more than just a simple string. For this scenario, we have created the AnalyticsParameter protocol. This allows clients to implement any parameter type that they might need for any type of tool.

Many tools use a simple [AnyHashable: Any] hash table but there are situations where we may require a parameter to be a different type. For example, Leanplum (a mobile engagement platform) has a method track(event: String, withValue: Double) which tracks an event with a Double as the ‘parameter’. Providing the client-defined parameter types was a decision made to allow features (like the one Leanplum has created) to still be usable with this layer.

AnalyticsParameter gives us the freedom to track absolutely any type of data.

An example showing how you can leverage this flexibility can be seen below:

We have removed more bloat with this implementation by delegating the responsibility of unwrapping to a type that your tool can understand out of our view controllers and into our implementation of parameters.

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.