App Architecture: Analytics

Photo by Hunter Harritt on Unsplash

It’s been a while since the last time I covered app architecture topics. Today I am back with a new one. This time it’s analytics and everything around it.

The problem

We engineers often treat analytics as a secondary piece of software. We spend so much time on the architecture design of the main parts and so little on designing analytics as part of the program that it always leads to a bad cocktail. This cocktail brings a bad smell to the code, some workarounds, and forces us to make exceptions to the architecture rules.

In practice, developers spend time thinking about general architecture; no one thinks about analytics at that moment. Then business comes and asks to track this and that. Send a couple of events to a third-party analytics vendor, and devs start thinking about how to do that and integrate analytics. After some time, business asks to add more events for a new vendor. Now you need to not only track it, but also manage vendors and decide which event goes where.

In code, it looks like one AnalyticsRepository or AnalyticsManager class that contains all analytics-related stuff, a huge list of methods where each of them calls due to tracking events. A god object class engineers drag across the codebase and make different components and layers depend on it.

And here we are, holding a bad cocktail in our hand.

The idea

So how to stop making this cocktail? I came to a simple idea: the app should not know which analytics vendors exist.

Feature code doesn’t need to care if an event goes to Firebase, Mixpanel, AppsFlyer or all of them. It should only say what happened, in the app’s own words. “Ticket purchased”, “Settings screen opened”. Where the event goes and in what format each vendor wants it is a different job, and it should live in a different place.

Instead of one god object that knows everything, let’s split the work into small pieces, and each piece has one job:

The Event

Event describes what happened, in the app’s words. It knows nothing about vendors.

No Firebase, no AppsFlyer, no SDK imports. Only facts about what the user did.

The user properties

Events are about what happened. User properties are about who the user is: plan type, language, favorite fare. Same idea here. The property knows nothing about vendors, and the app talks to one small service.

Each vendor sets it in its own way. Firebase calls setUserProperty, Mixpanel puts it into the people profile. That stays behind the service, the same way as with events.

The Factory

Factory decides what one vendor receives for an event. If it doesn’t know the event, it just says “not mine”.

This factory lives in the tickets feature, next to TicketPurchased. It knows only about ticket events, nothing about the rest of the app.

The Tracker

Tracker makes one call to one vendor SDK. This is the only place where the vendor’s rules live.

AppsFlyer wants revenue as af_revenue, so only this class knows that. If AppsFlyer changes the rules tomorrow, you fix one class.

The Dispatcher

Dispatcher only passes calls from the app to the vendors. It doesn’t filter or change anything.

Each vendor is called off the main thread. If one vendor crashes, the others still get the event.

And how the app uses it

A settings screen that sets properties can’t track events or restart vendors. It gets only what it needs.

The ViewModel knows only one small interface with one method. It has no idea that AppsFlyer exists.

Each feature keeps its own factories next to its events. The checkout feature has factories for checkout events, and DI collects them from all modules. There is no central place with a list of all events in the app, so when you add a new feature, you don’t touch some shared analytics file.

Now when business comes and asks to add a new vendor, you write factories for it. When they want to remove a vendor, you delete its factories. Your events and the code that tracks them stay the same.

And there is no god object anymore. Instead of oneAnalyticsManager, we have a few small interfaces: for tracking events, user properties, identity, start-up and consent. A ViewModel that only tracks events depends only on the interface for tracking events. Nothing else is dragged across the codebase.

Each ingredient stays separate.

The big picture

Now let’s see how all these pieces live together in a modular app.

The rules are simple:

  • :analytics:core knows nothing about vendors or Android.
  • Vendor modules know only their own SDK.
  • Feature modules own their events and decide how they reach each vendor.
  • Only :app sees everything, because it builds the whole graph.

And this is how one event goes through the system:

Each feature puts its factories into its own Koin module:

And the app module collects them from all features:

When you add a new feature, it brings its own Koin module with its factories. You only add that module tostartKoin, and the analytics setup doesn’t change.

What you get

Let’s put everything together. This is what you get when analytics is designed as a part of the architecture, not as a workaround.

  • The app doesn’t know vendors. Feature code only says what happened. Firebase, AppsFlyer or Mixpanel stay behind small interfaces.
  • No god object. Instead of one AnalyticsManager, there are small services for events, user properties, identity, start-up and consent. Each class depends only on what it uses.
  • No central list of events. Each feature owns its events and factories. When you add a new feature, you don’t touch any shared analytics file.
  • Easy to add or remove a vendor. New vendor means new factories. Removing a vendor means deleting its factories. Events and the code that tracks them stay the same.
  • Vendor rules live in one place. If a vendor changes its API or rules, you fix its tracker or factory. Nothing else changes.
  • Made for modular apps. Features plug in their factories through DI, so modules stay independent.
  • Analytics never breaks the app. Vendors are called off the main thread. If one vendor crashes, the others still get the event, and you get the error report.
  • Numbers stay numbers. Parameters keep their types all the way to the vendor, so dashboards can sum and compare them.
  • Consent and identity in one place. User sign-in and consent changes go through their own small services, and every vendor gets them the same way.
  • Easy to test. Since the ViewModel depends only on a small interface, a fake is a few lines:

No mocks of vendor SDKs, no Firebase in unit tests. You just check that the right event was tracked.

Meet Herald

Everything I showed you above is not just an idea on paper. I built an open-source library around it, and its name is Herald.

Analytics should not be the reason for business logic to change. And no more bad cocktail in your code.

Thanks for reading.


App Architecture: Analytics was originally published in ProAndroidDev on Medium, where people are continuing the conversation by highlighting and responding to this story.