How we improved the quality of app analytics data by 10xPosted on by Tamas Karai
You might have read our previous blog post about data driven development in apps, although if you haven’t, you can find that blog post here. This post is a more detailed overview showcasing the technical details and motivations behind building our new mobile analytics framework.
More events, more complexity
Implementing analytics has always been a huge problem in apps. You can’t rollback old features and release new ones as easily as on the web. First you have to decide what events should be tracked before implementing them one by one. A new analytics event could be as easy as writing one line of code, but adding key-value pairs to represent the context may increase the complexity a lot. Later you might need more analytics tools to be integrated, like Google Analytics, Mixpanel or Flurry, so your analytics code might get a lot bigger and harder to maintain over time. As long as an application handles a relatively low number of analytics events and distributes them to only a few analytics providers, the analytics code remains fairly maintainable. Although if an application has more than 500 different events, routing them to five or more different analytics frameworks, then maintaining the integrity of the analytics code becomes a lot harder. Of course writing unit tests for all the events could prevent breaking the analytics all the time, but when your application handles a lot of events, even maintaining the unit tests could become a huge overhead.
Making decisions without sufficient analytics data is risky. The more data the app collects, the more precise assumptions can be made about how to improve the product. Without a bullet-proof analytics framework, it’s very easy to make wrong decisions using corrupt data. So we laid down our initial principles of how we want to track user behavior in apps:
- Low or minimum effort to add new analytics events (one event = one line of code)
- Collect event parameters automatically and consistently
- Log all events in the same order as they were triggered
- Uniform naming conventions
- Log user interaction automatically
In the following section I’m going to share how these principles helped us implementing our analytics framework.
Implementing our principles
Implementing a new analytics event should cost no more than writing one line of code.
[[SKYCoreAnalytics sharedInstance] logCustomEventName:@"AppStart"];
Sounds great! But how are we going to send this event and more importantly, how are we going to collect all the necessary key-value pairs, also known as event context to represent the current state of the app when the event is triggered?
Well, after an event is triggered the analytics framework bubbles up the event by following the view hierarchy path. Each element in the view hierarchy can add its name and custom key-value pairs to the event, then the event is passed to the current view’s ancestor. On iOS we’ve used the UIResponder chain to bubble up the events and their context all the way up to the top, although on Android we had to come up with a custom solution to connect fragments and activities. The bubbling ends when it reaches the top of the view hierarchy, where no parent is found to continue. Actually this last item mostly extends the collected parameters with application wide properties, like locale, market, language or login information.
Automatic user interaction logging is also added to the most often used UI elements, like buttons, switches and textboxes. We followed the one line principle here as well, because these items will automatically trigger an analytics event if their analytics name is set. For example a button will send a tapped event or a list will send a selected event if their analytics name is set.
UIButton *button = [UIButton new];
button.coreAnalyticsName = @"Button";
It’s also crucial to log all these events in the same order as they were triggered, so the collected properties always represent the current state of the application. First of all, the framework should guarantee that bubbling up in the view hierarchy is synchronous. Secondly event handlers sending analytics events should be called before every other event handler registered on the same UI element.
Finally, when the event bubbling has been finished the analytics framework will send the event details with all the collected key-value pairs to a mediator class that distributes the collected data to all registered analytics tools. These tools can transform the received data to the required format. For Google Analytics the category and action strings are required and optionally the received parameters could be used to set custom dimension values. Mixpanel only requires a key that will be the name of the event, but the collected key-value pairs can be set for every Mixpanel event.
The first results of implementing our new analytics framework were impressive. We managed to reduce our codebase managing Google Analytics events from 3800 lines of code to only 140, and the codebase creating Mixpanel events from 4400 lines of code to 125. But the real advantage of introducing this framework is that we could easily decouple our analytics framework from all the tools, including our custom analytics stack as well.
Of course we faced lots of questions while we were implementing the framework. Probably the biggest one was whether we should allow adding complex data classes and structures to the event context or only relatively simple objects, like strings, dates and numbers. We eventually chose the latter option, since it was more convenient for us to convert analytics data to Google Analytics and Mixpanel events. However our custom analytics stack uses a strict protocol buffers schema as a contract between the client and the server, so in this case a schema object has to be created from the context properties every time an event is fired. Fortunately this extra computation can be done in a background thread, only the event bubbling has to run on the main thread in order to keep the right order of analytics events.
Another big decision was whether we enable custom routing for the event context bubbling or if the view hierarchy should be strictly used all the time. A good example is popovers in iOS, where the parent is always the presenter view controller, not the actual view that triggered opening it. Using only the view hierarchy for collecting all the necessary event properties won’t include the view’s context. Although if it was possible to override the next item in the bubbling, then the popover could point to the view that triggered it as a next step and all the necessary properties would be collected.
We chose to always use the view hierarchy for bubbling up, because this way it is simpler to predict how the properties are collected. It is also easier to test the event context bubbling if no custom routing is enabled.
We named this new analytics framework Core Analytics. It has definitely made our analytics code a lot simpler and more maintainable, because tests are only needed to cover the main functionality of the framework. It isn’t necessary to write and maintain unit tests for every single analytics event. It’s also scalable, since collecting analytics data is distributed between views. Core Analytics has become an essential tool for us to make data driven decisions to reach our goal to deliver personalized content and features for travellers.
Tamas Karai is a software engineer working on the Skyscanner iOS application. He’s passionate about building a clean and scalable architecture for apps.
This blogpost is part of our apps series on Codevoyagers. Check out our previous posts: