Building Automated Analytics Logging for iOS Apps
Analytics is often the first tool developers add to their iOS app. A standard approach is to write logging code like this:
Let’s call this manual event-tracking. With manual event-tracking, you write logging code for each analytics event you care about. A logEvent:
for signing in, a logEvent:
for inviting a friend, a logEvent:
for opening the Settings view, and so forth.
Here, we’ll describe a different approach: automatic event-tracking. With automatic event-tracking, client-side events are automatically logged. Usage can be analyzed without having to define events upfront, without writing code, without going through the App Store, and with virtually no performance overhead.
In this post, we’ll provide a blueprint for building automatic event-tracking into your own app. Heap’s analytics library for iOS offers automatic event-tracking, but this information should be useful to you even if you don’t use Heap.
The Problem with Manual Event-Tracking
Let’s say you’ve launched your new iOS app. You took painstaking care to craft the perfect launch. You designed a beautiful landing page, appeared in major press outlets, and generated 5,000 signups in 2 days. Woo.
But after the initial fanfare subsides, you start to think: “Where did all my visitors drop off in our signup form?” “Which source of traffic had the highest conversion rate?”
Oh, right. In the frenzy of shipping, you forgot to instrument your signup flow.
Now your next two weeks will be spent:
Mourning launch data that’s forever lost.
Instrumenting your signup flow with logging code.
Waiting for your app to get approved by Apple.
Waiting.
Waiting some more for data to trickle in.
Analyzing your data.
This is what happens when a bug in your code breaks logging. You lose data forever.
This is the fundamental issue with manual event-tracking. Tagging each event of interest in advance is brittle and often results in stale data. And since you don’t know what you don’t know, every new question requires new logging code, resulting in a lengthy feedback loop.
How Do We Build Automatic Event-Tracking?
On web, there’s only one event abstraction, so the answer is obvious: log every DOM event.
On iOS, we have more options:
Log every touch event, in the form of a UIEvent or UIGestureRecognizer.
Log every call to an action method with target-action or Interface Builder.
Log every appearance of a view controller.
Log every notification issued through NSNotificationCenter.
The correct automated framework will map as closely as possible to typical logging calls. There should also be enough context around each event, so that we can easily pinpoint salient interactions post-hoc.
Where do iOS developers normally place logging calls? A GitHub search for Flurry’s logEvent:
method surfaces lots of code like this:
There’s an obvious connection here between action methods and logging calls. Source.
This is only a single example, but the pattern is clear: analytics calls are typically made in action methods. If we log action methods as they occur, then we can save ourselves the overhead of sprinkling analytics code throughout our app.
(Note that Heap’s iOS library not only auto-logs action methods, but also UIEvents and view controllers. The general approach is the same for all three event types, but in this blog post, we’ll focus on implementing the former.)
In UIKit, every user interaction on a control passes a [sendAction:to:from:forEvent:]
message to the global UIApplication object. If we extend sendAction:to:from:forEvent: to log each occurrence with its parameters, we’ll have a complete picture of the user’s flow through our app.
There’s one problem, though. The sendAction:to:from:forEvent: method is a built-in Cocoa method. We can’t modify built-in methods, so how can we hope to log each occurrence? We’ll need to use method swizzling.
What is Method Swizzling?
Simply put, method swizzling allows you to extend an existing method implementation.
Subclassing and categories are common techniques for extending methods. But method swizzling is unique in that it works without forcing you to modify existing calls to the original method and without requiring access to the original method’s implementation. This means we can modify the behavior of built-in Cocoa methods.
This is very powerful. And it’s exactly how we’ll auto-log sendAction:
.
Method swizzling is made possible by Objective-C’s flexible runtime. With a single #import \<objc/runtime.h>
, we gain access to various low-level goodies. In particular, we’ll use method\_exchangeImplementations
, which (unsurprisingly) lets you swap the implementations of two methods.
The final result looks like:
Here’s what’s happening step-by-step:
First, we extend UIApplication with an EventAutomator category that contains a new method
heap_sendAction:
. This method will replace the built-insendAction:
. (A unique prefix is crucial in order to avoid namespace collisions.)
Then, in our category’s +load method, we swap our method with the original using
method_exchangeImplementations
. This works because the Objective-C runtime special-cases+load
to be invoked for every category within a class, even if+load
is defined multiple times.Finally, we implement
heap_sendAction:
to log the action selector argument and then call the originalsendAction:
method.
At first glance, heap_sendAction:
appears to be infinitely recursive. But remember that we swapped the implementation of heap_sendAction:
with sendAction:
. Thus, at runtime, sending the heap_sendAction:
message actually calls code within sendAction:
Method swizzling can be a bit of a mind-bender.
Note that this implementation of method swizzling is incomplete. For instance, what if the method we’re swizzling is defined within a superclass? Then the swap will ultimately fail, because the original method is missing from the class.
To avoid hairy edge cases like this, just use a method swizzling library. You should have little reason to deviate from existing implementations like JRswizzle or NSHipster’s.
The results in action
Though we’ve only written a few lines of code, this works! Watch it in action on Coffee Timer:
Remember: we’re getting this data without having written any logging code!
Feel free to dig into the underlying source code.
Method swizzling is central to building out an automatic analytics framework. Using the code we just wrote, we won’t need to worry about defining events of interest upfront. And whenever we need to analyze a new interaction, we won’t need to go through the bother of shipping new code to the App Store.
The Heap iOS library further expands upon this idea by auto-logging other types of events, including all UIEvents and every appearance of a view controller.
Performance optimizations
If you’re wondering whether automatic event-tracking affects your app’s performance, know this: the performance impact is negligible. Our approach needs a few refinements, though.
Here are some of the performance optimizations we implement in Heap’s iOS library. If you’re considering building your own analytics, we recommend you implement them as well.
Batch network requests.
Network requests power up a device’s cell radio. A naive approach to automatic event-tracking – where every event is transmitted as soon as it occurs – would keep the radio powered for the entirety of a session. This is wasteful.
The solution is to transmit bursts of data at fixed time intervals (say, every 15 seconds). This keeps the radio triggered for a much smaller portion of time and conserves battery life, at the cost of a minor decrease in data recency.
Using the Coffee Timer app as our testbed, we see that automatic event-tracking puts minimal strain on battery life and CPU. The graphs below profile the energy/CPU usage of two highly-active sessions. Heap is active during one of these sessions and disabled during the other.
Can you tell which set of metrics correspond to which session? Neither can we. Even with frantic user input, our automatic event-tracker rarely exceeds 1% CPU usage.
Performance stats for nearly identical 1-minute sessions. Rate of activity was very high (~90 interactions).
Heap is on top, vanilla app is on bottom.
Process events off the main thread.
Method swizzling itself is lightweight, as it’s just a pointer swap at runtime. But before you send events to your servers, you’ll likely need to perform some client-side preprocessing. For instance, the Heap iOS library munges events into a URL format, tags events with metadata such as App Version, Carrier, and Device Model, and sanitizes event properties.
Don’t do this work on the main thread. Instead, push processing work onto a background queue (using Grand Central Dispatch or a NSOperationQueue). This unblocks UI rendering on the main thread and ensures your app remains snappy even under heavy load.
The future of mobile analytics
This approach works for iOS apps built on Cocoa. It’s extremely performant, provides a comprehensive view of a user’s activity, and saves you the hassle of writing and maintaining analytics code.
But there’s plenty to be done to make this more broadly applicable. How do we handle games built on Cocos2d or Unity? How is automatic event-tracking implemented in Android apps? We hope to answer these questions in a follow-up post, as well as open-source our own iOS integration.
Let us know what you think on Twitter. And of course, if you’d prefer to use a battle-tested automatic event-tracking tool for iOS, consider using Heap.
Interested in learning more about Heap Engineering? Meet our team to get a feel for what it’s like to work at Heap!