Our ever expanding appetite for analytics

In our industry, data-driven decision making has really taken root. We are increasingly interested in the actions that our users are performing inside our apps. The aim being to get users to buy more products, share more photos, open one more post, etc - generally just stay in our apps for longer and help us achieve our business objectives. We achieve this by sending events when the user performs a task, these events can be as simple as a single value - event name or much more detailed by including multiple values that we believe provide details into why the user took that action. All of this work needs to be undertaken by us, the developer. A few years ago you may have been that type of developer who thought:

"Gee, I need to add even more events....pssst destroying my beautiful abstractions.....Jeff always coming over here, asking me to track this and track that.....I've got actual features to build, I'll do that at the end of the sprint"

(What? Are you asking if I used to have this thought?........no, never - I don't even know someone called Jeff 😏)

There was a tendency to think of analytics not as part of the feature but rather as an add on, as something that we could ship without. We may even have lied to ourselves that shipping without waiting for analytics to be implemented meant that we were being agile or lean. However doing so actually meant that our new feature was flying blind -

Is that increase/drop in daily active users connected to the new feature?

Are of users using it?

Should we continue developing it?

We can't empirically answer any questions without data so instead we need to go with gut reactions 😷.

I feel as if I have taken you on a long journey to say:

Data is the new king 👑

(Or at least really, really, important)

And like anything important in our projects we need to properly (unit) test it to make sure it's doing what we expect. Below is an example of how we can structure our analytics layer to allow it to be independent and unit tested.

Satisfy our analytics appetite 🍝

There are a number of really useful analytical tracking solutions out there and in the below examples I will be using Mixpanel - however this approach to handling analytical events can really be used with any solution. It all boils to the idea that we will be programming to an interface rather than an implementation. This way we can hide our solution (Mixpanel) behind an interface, have our separate analytical classes construct the events and then pass them via a delegate to our solution (or in the case of our unit tests a spy).

This is the interface that we are going to hide behind. It's important to note that this protocol shouldn't be treated as an interface that will cover all solutions/frameworks but rather as one that is focused on providing an abstraction above the available Mixpanel methods - it's simply to allow us to more easily unit test our events.

class AnalyticsManager: NSObject, AnalyticsDelegate {

Here we declare that AnalyticsManager will implement AnalyticsDelegate methods.

In the above code snippet we are creating 3 lazy instances of analytical registries. A registry is a class that groups together related events. Rather than implementing all events in the one class I have decided to split them over a number of different classes so that we can avoid having one god class. With each registry we set AnalyticsManager as the delegate. I could have made each registry a singleton but instead I choose to have them as properties on the AnalyticsManager (which is itself a singleton) with the idea being to group all possible analytical options into the one location and so (hopefully) improve readability of the project. The downside of this choice is that when triggering an event, the syntax is slightly longer:

The above methods pass parameters to the instance of Mixpanel that we have as a property without making any changes.

Ok, so that's the manager/delegate, lets look at these mysterious registries we keep talking about. To save on each registry having to implement the same init'er we will use a base/parent class that each registry will extend.

It's important to note that only the registry actually constructs the event and any associated dictionary meaning that any class that uses a registry should only be interested in passing the required data and not building the structure that the event will use - this is a similar approach as taken when building requests for an API call, described in this post.

A spy is fairly common pattern in unit testing and works by exposing parameters passed into it's method as properties. In this case our AnalyticsDelegateSpy conforms to the AnalyticsDelegate protocol and exposes two properties passedInEventName and passedInProperties - these properties will allow us to know what the output of an event method is. AnalyticsDelegateSpy will be substituted in place of AnalyticsManager when we create instances of our registries.

One of the most important ideas underpinning unit tests is that each unit test should be independent. So in the above code snippet we are creating a new instance of the spy and register before ever test. This ensures that any changes made in one unit test are discarded before the next unit test is executed.

In the above test, we are checking that the event name being used for this event actually matches the one we expect it. This is achieved by using the passedInEventName property declared in the spy and comparing that against the event name declared in the registry.

The above test is a little bit more involved than the previous as we need to first set up the data we are going check. I could have used XCTAssertTrue instead of XCTAssertEqual but I decided on the current implementation as it allows me to change the enabled variable without changing the assert.

And that's it, the sendNotificationEnabled is fully tested with just two unit tests. This pattern of two tests (Event name and Properties) will be used in all of the other tests that we see. This predictable and consistent pattern is one of the key attractions to this registry approach.

I'll leave going through this example as an exercise you, if you get lost look back at the simpler example - it's same pattern.

Coffee time ☕️

With this approach, we can create very simple to understand analytical registries that can then be 100% unit tested and encapsulate any event specific knowledge from the other parts of the our project. By splitting our analytics into smaller classes, each one with a tight focus we can also avoid the dreaded god class/object that contains all of our events. If we need to use a different analytical solution it should just be case of updating the delegate's methods with the overall pattern staying the same.