Site icon Hari Vignesh

Conquering the O11Y chaos: Effective analytics management in Android codebase

conquering the O11Y chaos

It is common for SaaS apps to have a lot of analytics code distributed across multiple layers. A couple of years back, at WeTransfer, we tried out different providers to see what was best suited for understanding our user behaviour. This led to the swapping of varying analytics providers in the code, GDPR complaints, etc. At a point, we settled with two providers, one used by the data team and another by the product team. Both had different types of data payload and needs. For example, the data team wanted to understand the business more, and the product team wanted to understand features more. Now, on top of this, we, developers had our own realtime logging and tracking system to measure the crashes, ANRs, performance tracing of the core features to measure objectives and SLOs. So if you think about it, we have three different types of tracking/tracing system in the codebase that contributed to significant amount of lines of code in the codebase that was quite equivalent to the feature logic.

I’ve always wondered where the right place to add tracking is. Someone asked this question at one conference, and the subject matter expert’s answer was quite interesting—”all layers.” Tracking code can sit at any layer (business, UI, or data), depending on what we track, and it is true if you think about it.

So imagine a codebase where three different tracking systems propagate different events sitting at all layers. We, developers faced some challenges in managing the codebase

Going back to the drawing board

My colleague Irena and I returned to the drawing board to conquer this chaos. First, we wondered if we could create a way of working for the team to address this, but soon, we realised that we might need separate APIs to declutter the codebase effectively. To resolve our problem, we drafted the objective of the API

Here’s the high-level API design we came up with

// base interface
interface AnalyticsEvent

// analytics provider 1
// can create different variants by extending Provider1
interface Provider1Event: AnalyticsEvent {
  val category: String
  val action: String
  val property: String?
}

// analytics provider 2
// can create different variants by extending Provider2
interface Provider2Event: AnalyticsEvent {
  val eventName: String
}

// event definition
class SomeClickedEvent(): Provider2Event {
  override val eventName = "some_clicked_event"
}

// call site, how we want all layers to invoke
analyticsManager.track(SomeClickedEvent())
Kotlin

Some key highlights

Tailoring Events to specific types

While the high level design looks promising, we had a couple of challenges to solve. One, for simple events like the click events there were no additional payloads or properties. But for more custom events that needs more state and properties, we converted the classes to data classes to host additional properties

data class someCustomEvent(
  val action: String,
  val context: String
): Provider2Event {
  override val eventName = "some_clicked_event"
}

// analytics tracker 
interface AnalyticsTracker<EventType> {
  fun <T : EventType> trackEvent(event: T)
}

// provider 1 implementation
class Provider2Tracker: AnalyticsEvent<Provider2Event> {
  override fun <T : Provider2Event> trackEvent(event: T) {
    // map to provider event and track
  }
}
Kotlin

Avoiding object mappers

To avoid writing a lot of object mappers from the event objects to the tracker payload, we leveraged the concept of Serialization. By using Moshi serializer, we mapped the data class or class properties to the respective map structure or json – depending upon the needs of the analytics provider

@JsonClass(generateAdapter = true)
data class someCustomEvent(
  @Json(name = "Action") val action: String,
  @Json(name = "Context") val context: String
): Provider2Event {
  override val eventName = "some_clicked_event"
}
Kotlin

Final thoughts

Recognizing the need for a more structured approach, we returned to the drawing board with clear objectives: to create an API that simplifies the tracking process, allows for easy swapping of providers, and provides a visual representation of events in the code. By designing small, modular classes for each event and unifying the call method, we significantly decluttered our codebase. Although this approach required some trade-offs, such as invoking the method twice for different providers, it proved to be an effective solution that met our immediate needs.

To handle more complex events requiring additional state and properties, we leveraged Moshi serialization, we avoided the need for cumbersome object mappers, further reducing the complexity of our code.

In conclusion, our approach not only brought order to the chaos but also made the codebase more maintainable and scalable. While the journey had its challenges, the solutions we implemented allowed us to manage our tracking systems more efficiently, ultimately contributing to a more robust and adaptable application.

Thank you for taking the time to read about our experience. We hope it provides valuable insights into managing analytics in complex codebases.

Special thanks to Irena for collaborating on designing the API

Exit mobile version