Analytics Logging System in NowInAndroid App

This blog will walk you through the step-by-step process of building a scalable and testable analytics logging architecture in Android.

We'll explore its design, implementation, and best practices, empowering you to create your own robust solution.

A big thanks to NowInAndroid Repository for providing the code example used in this blog, which was taken from their open-source project.

Overview of the Architecture

The architecture centers around the AnalyticsHelper interface, which abstracts the actual logging mechanism. Multiple implementations of this interface handle different scenarios, such as:

  1. NoOpAnalyticsHelper: A no-operation implementation for testing and previews.

  2. TestAnalyticsHelper: A mock implementation to verify logging behavior in unit tests.

  3. StubAnalyticsHelper: A simple implementation for debugging in local environments.

  4. FirebaseAnalyticsHelper: The actual implementation using Firebase Analytics for production.

This setup adheres to the Dependency Inversion Principle (DIP) of SOLID, enabling modular, scalable, and testable code.

Step-by-Step Implementation

1. Define the AnalyticsHelper Interface

The AnalyticsHelper interface ensures that all implementations conform to a standard contract:

interface AnalyticsHelper {
    fun logEvent(event: AnalyticsEvent)
}

This interface allows us to inject different implementations depending on the environment (e.g., production, testing).

2. Create the Implementations

a. NoOpAnalyticsHelper

This implementation does nothing and is useful for tests or when no logging is needed:

class NoOpAnalyticsHelper : AnalyticsHelper {
    override fun logEvent(event: AnalyticsEvent) = Unit
}

b. TestAnalyticsHelper

Used in unit tests to verify if specific events are logged:

class TestAnalyticsHelper : AnalyticsHelper {

    private val events = mutableListOf<AnalyticsEvent>()

    override fun logEvent(event: AnalyticsEvent) {
        events.add(event)
    }

    fun hasLogged(event: AnalyticsEvent) = event in events
}

c. StubAnalyticsHelper

Logs events to the console for debugging:

@Singleton
internal class StubAnalyticsHelper @Inject constructor() : AnalyticsHelper {
    override fun logEvent(event: AnalyticsEvent) {
        Log.d(TAG, "Received analytics event: $event")
    }
}

d. FirebaseAnalyticsHelper

The production implementation that logs events to Firebase Analytics:

internal class FirebaseAnalyticsHelper @Inject constructor(
    private val firebaseAnalytics: FirebaseAnalytics,
) : AnalyticsHelper {

    override fun logEvent(event: AnalyticsEvent) {
        firebaseAnalytics.logEvent(event.type) {
            for (extra in event.extras) {
                param(
                    key = extra.key.take(40),
                    value = extra.value.take(100),
                )
            }
        }
    }
}

3. Define the AnalyticsEvent Data Class

The AnalyticsEvent class represents each logged event:

data class AnalyticsEvent(
    val type: String,
    val extras: List<Param> = emptyList(),
) {
    data class Param(val key: String, val value: String)
}

This structure ensures consistency and type safety in event logging.

4. Add a CompositionLocal for Dependency Injection

Use Jetpack Compose's CompositionLocal to provide a default AnalyticsHelper:

val LocalAnalyticsHelper = staticCompositionLocalOf<AnalyticsHelper> {
    NoOpAnalyticsHelper()
}

This approach ensures that tests and previews do not require manual AnalyticsHelper injection.

How to use:

@Inject
lateinit var analyticsHelper: AnalyticsHelper

setContent {
    CompositionLocalProvider(
                LocalAnalyticsHelper provides analyticsHelper
) { NiaTheme() ... }
}

5. Provide Dependency Injection with Hilt

Use Hilt to provide the appropriate implementation of AnalyticsHelper:

@Module
@InstallIn(SingletonComponent::class)
object AnalyticsModule {

    @Provides
    fun provideAnalyticsHelper(
        firebaseAnalytics: FirebaseAnalytics,
    ): AnalyticsHelper = FirebaseAnalyticsHelper(firebaseAnalytics)
}

6. Create Extensions

Create extensions for your features AnalyticsExtensions:

internal fun AnalyticsHelper.logNewsResourceBookmarkToggled(newsResourceId: String, isBookmarked: Boolean) {
    val eventType = if (isBookmarked) "news_resource_saved" else "news_resource_unsaved"
    val paramKey = if (isBookmarked) "saved_news_resource_id" else "unsaved_news_resource_id"
    logEvent(
        AnalyticsEvent(
            type = eventType,
            extras = listOf(
                Param(key = paramKey, value = newsResourceId),
            ),
        ),
    )
}

and use it via analyticsHelper instance:

@Inject
lateinit var analyticsHelper: AnalyticsHelper

Advanced Insights for Senior Engineers

Design Decisions

  • Interface-Based Architecture: By using the AnalyticsHelper interface, we decouple the logging logic from the rest of the app, adhering to the Dependency Inversion Principle of SOLID.

  • Data Class for Events: The AnalyticsEvent data class ensures type safety and enforces a consistent structure for events.

Trade-offs and Challenges

  1. Dynamic Flexibility vs. Type Safety: Representing AnalyticsEvent.extras as a List<Param> provides flexibility but lacks the strong typing of a map.

  2. Data Integrity: Truncating keys and values for Firebase limits can cause discrepancies when comparing data across platforms.

Optimization Strategies

  1. Performance: Logging events on the main thread can cause bottlenecks. Always offload logging to background threads using coroutines:

    CoroutineScope(Dispatchers.IO).launch {
        analyticsHelper.logEvent(event)
    }
  2. Batching Logs: In high-traffic apps, batching events can reduce network overhead and improve performance.

Real-world Considerations

  • Error Handling: Implement a retry mechanism for failed logs due to network issues.

  • Avoid Logging PII: Ensure no personally identifiable information (PII) like emails or phone numbers is logged. Use hashed or anonymized values if needed.

Testing Strategies

  • Unit Testing: Use TestAnalyticsHelper to verify that specific events are logged during tests:

    val testHelper = TestAnalyticsHelper()
    val event = AnalyticsEvent(type = "test_event")
    
    testHelper.logEvent(event)
    assertTrue(testHelper.hasLogged(event))
  • Integration Testing: Use tools like Firebase Test Lab to inspect analytics logs in real-world scenarios.

Production-Ready Enhancements

  1. Global Event Queue: Add a queue to handle retrying failed logs during intermittent network issues.

  2. Custom Sampling Rates: Control the volume of logged events by introducing sampling rates for high-frequency events.

Conclusion

With a clean, modular architecture like this, your analytics logging system becomes a powerful tool for understanding user behavior. Start small, keep your implementation testable, and iterate as your app grows. Remember, analytics is not just about logging events—it's about creating actionable insights that drive meaningful decisions.

By following the steps and best practices outlined in this guide, you can create a robust analytics logging system tailored to your app's needs.

Akshay Nandwana
Founder AndroidEngineers

You can connect with me on:


Book 1:1 Session here
Click Here

Join our upcoming classes
https://www.androidengineers.in/courses

Love from our past students

Excellent list of questions really helped me to coverup all the topics before interview.

Saiteja Janjirala

10th Oct, 2024

I had an exceptional experience with the 1:1 mentorship session. Akshay was incredibly friendly and provided invaluable guidance on focusing on long-term goals. They also gave great interview tips, including a thorough resume review. Additionally, the discussion on Data Structures and Algorithms (DSA) was insightful and practical. Highly recommended for anyone looking to advance their career!

Nayab khan

11th Sep, 2024

Cleared my major points for what I am missing in the resume and also suggested what I can work on for further growth in the career.

Ketan Chaurasiya

7th Aug, 2024

What impressed me most was his personalized approach and practical tips that I could immediately apply. Akshay’s guidance not only improved my technical skills but also boosted my confidence in navigating my career path. His insights and encouragement have been a game-changer for me. I highly recommend Akshay’s mentorship to anyone looking to advance their Android development career.

Hardik Kubavat

5th Aug, 2024

2025© Made with   by Android Engineers.