As a company that makes decisions based on data, having accurate data is essential to our operations. We lose a lot of money and time whenever something goes wrong with our data trackings. In this article, I’m going to share how the iOS team at Tokopedia keeps the app data accurate.
Rules for accurate data
Two rules are required in every app to provide accurate data regardless of what analytics library you use:
1. The analytics event must contain the right data.
2. The event must be sent at the right time.
Failing to satisfy one of them will result in inaccuracies and debugging headaches. Let’s jump straight ahead for the solution, starting from the first rule.
Sending the right data
Common ways to make mistakes
We use Google Analytics for our data analysis and Firebase Analytics SDK is used to transmit the data. The SDK has a simple interface and pretty easy to use.
Even though we can send practically any valid JSON as parameters, there are three mandatory fields that we must fill in each event, according to our data analysts. Those are category, action, and label fields. We rarely put other keys besides those mentioned previously.
This is how we typically use the SDK to send the analytics data.
While the above code is pretty simple, there are many ways to make mistakes as shown below.
The first problem feels especially dangerous to me because typos are usually hard to notice unless you are very careful. The third one will crash your app.
Is there a way to prevent these mistakes from happening? Fortunately yes.
Swift to the rescue
To solve this problem, we can leverage Swift’s strict type system instead of using dictionaries. First, let’s create a new type that represents our events.
We will create a new analytics client that receives this new type, and also transforms the Event type to a dictionary type that Firebase Analytics knows how to consume.
Let’s see how we’re going to send the analytics events using this new method.
This new code is simple to use and very safe. It is impossible to make the mistakes mentioned previously because the code won’t even compile.
By using our own data type, we have prevented a group of problems from happening. There is a lot that we can do just by utilizing Swift’s type system.
When the compiler can’t save you
There is one more mistake that can happen though, and that is putting the wrong values like this.
This mistake sadly cannot be prevented in compile-time, and the only way to verify this is by manual verification, having a data analyst inspect the emitted events from Firebase Analytic’s DebugView.
It’s not a big deal though in our case. Usually, when the values are decided, they very rarely change. Validating the events at the end of feature development is enough.
- There are a lot of ways to make mistakes when using dictionary type to send analytics data.
- We can leverage Swift’s type system to reduce a lot of those possible mistakes.
- We do manual verification on mistakes that can’t be prevented by Swift such as putting wrong dictionary values.
Sending the data at the right moment
Sending the data at the wrong time can screw up your data analysis, so it is very important to make sure they are also sent at the right time.
One way to do this is by having the developers or testers run the user journeys either manually or through UI automation, and see the data submitted to the analytics dashboard. This approach is simple but does not scale well when you have hundreds of screens. Manual verification is time-consuming and prone to errors. While UI automation is definitely helpful, it can often be flaky.
Unit testing has become a common practice to prevent behavior regressions at scale. We can use the same technique to prevent data regression. Some changes in the code are required to make testing simple and easy.
We’ll see how it works in practice.
Let’s make an app
This is the perfect time to show you my mad design skills.
This is an app to purchase an iPhone, and you can only buy a maximum of three iPhones at once. There are a few rules:
1. When the decrease button is tapped and quantity is > 1, send events with these properties:
2. When the increase button is tapped and quantity is < 3, send events with these properties:
3. When the purchase button is tapped, send an event with these properties:
Label: "quantity - \(quantity)"
That’s the simplest example I can make to show the essence of this technique.
Good old MVC and why it doesn’t work
A straightforward way to implement this is by using the MVC architecture, an architecture that has been recommended by Apple for years.
This architecture is hard to unit test even though the implementation is simple. You need to instantiate the view controller and call the functions directly to test this.
This could work in this example because the feature is super simple. Testing would become a nightmare in the real world though when the features are much more complex.
MVVM to the rescue
The key to test this feature easily is by decoupling business logic from the UI. I’ll use a very simple version of MVVM to demonstrate this.
Note: You can use whatever architecture you prefer. It doesn’t have to be MVVM as long as the business logic is separated from the UI.
Let’s start writing the view model
The view models contain the methods that will be called from the view controller. The business logic will be processed here, which is what we will do later.
The view model also receives two closures that will tell the view controller to:
1. Change the displayed quantity.
2. Send an analytics event.
This is what the view controller will look like
There are three responsibilities of this view controller:
1. Delegate each user's interactions to the view model.
2. Change the displayed quantity when requested by the view model.
3. Send the analytics event to the analytics client when requested by the view model.
The view controller now has the minimum amount of logic that is not worth covering with unit test.
Let’s fill up the view model implementation.
The contents are very similar compared to the MVC version of the view controller. Pretty much all logic is moved from the view controller to the view model.
Let’s move on to testing
This view model is pretty much a plain Swift object, which will be very easy to test. First, let me explain the test fixture
The setup is relatively simple. The view model will be recreated at each test case to reset the state. The callback results from the view model will be stored so they can be asserted later as shown in lines 8 and 9.
We will start with the simplest test case. When the user taps the plus button, increment the quantity and send the analytics event
Verifying negative case is also easy. We can verify that the events will be sent only when the quantity changes.
We’ll move on to the more complex test case, purchasing 3 iPhones. What the user needs to do is tap the increment button twice and tap the purchase button.
While this approach works, the test method is quite verbose even for a simple test case, and will feel tedious to write for the more complex ones.
It doesn’t have to be this verbose
Let’s improve the test ergonomics. By conforming our Event to Equatable, we can reduce a lot of unnecessary typings of XCTAssertEqual.
It’s much better now, but it still takes a lot of space to type all of these. What’s worse is we are duplicating the event properties both in the test case and the view model.
There’s one weird trick to simplify the code and remove the duplication at the same time. We can use Swift’s extensions.
Now the test codes will be much simpler as shown below.
We removed a lot of lines from the test code, the test is now much easier to write and understand. As a bonus, we can also simplify our view model using the same technique.
- There needs to be an automated way to verify the sequents of events at scale.
- The MVC architecture is difficult to unit test since it couples the UI and business logic.
- We used alternative architectures such as MVVM to separate business logic from the UI and test the business logic separately.
Where to go from here
The techniques mentioned above have already reduced a lot of possible mistakes for us, but no technique is perfect.
If you want to see all the events fired for the whole journey from the home screen all the way to the product, cart, and payment screen, unit tests might not be a good solution. The testing technique above only validates the things happening on a screen, so it will be difficult to verify the events happening across screens.
This is where UI tests could be useful. With some effort in toolings, you can record the events happening across the screens and save them in a file. This file can be compared to the results of subsequent runs to prevent regression. You have to be aware of the drawback though since UI tests can be flaky and take time to execute.
All of the sample codes above can be found in my Github repo.