Analytics turns user behavior into product decisions. Well-designed events and funnels answer "why are users dropping off?" — but only if you define them with precision before you build.
Event Design Principles
Naming Convention
Use noun_verb or object_action format:
article_view
article_read_complete
search_query_submitted
cart_item_added
checkout_started
checkout_completed
payment_failed
Avoid generic names (click, tap, button_pressed) — they tell you nothing.
Parameter Taxonomy
Every event should have:
- What: the object (article_id, product_id)
- Context: where it happened (screen_name, section)
- User state: relevant user properties (subscription_tier, is_onboarded)
sealed class AnalyticsEvent {
abstract val name: String
abstract val params: Map<String, Any>
data class ArticleViewed(
val articleId: String,
val title: String,
val source: String, // "feed", "search", "deeplink"
val readingTime: Int // estimated reading time in seconds
) : AnalyticsEvent() {
override val name = "article_view"
override val params = mapOf(
"article_id" to articleId,
"article_title" to title,
"source" to source,
"estimated_reading_time" to readingTime
)
}
data class SearchPerformed(
val query: String,
val resultCount: Int,
val hasFilters: Boolean
) : AnalyticsEvent() {
override val name = "search_query_submitted"
override val params = mapOf(
"query_length" to query.length,
"result_count" to resultCount,
"has_filters" to hasFilters
)
}
}
Analytics Abstraction Layer
Never call Firebase Analytics directly from UI code — use an abstraction so you can swap providers and unit test:
interface AnalyticsTracker {
fun track(event: AnalyticsEvent)
fun setUserProperty(name: String, value: String)
fun setUserId(id: String)
}
class FirebaseAnalyticsTracker(
private val analytics: FirebaseAnalytics
) : AnalyticsTracker {
override fun track(event: AnalyticsEvent) {
analytics.logEvent(event.name) {
event.params.forEach { (key, value) ->
when (value) {
is String -> param(key, value)
is Long -> param(key, value)
is Double -> param(key, value)
is Int -> param(key, value.toLong())
is Boolean -> param(key, if (value) 1L else 0L)
}
}
}
}
override fun setUserProperty(name: String, value: String) {
analytics.setUserProperty(name, value)
}
override fun setUserId(id: String) {
analytics.setUserId(id)
}
}
// Debug/test implementation
class LoggingAnalyticsTracker : AnalyticsTracker {
override fun track(event: AnalyticsEvent) {
Log.d("Analytics", "[${event.name}] ${event.params}")
}
override fun setUserProperty(name: String, value: String) {}
override fun setUserId(id: String) {}
}
Tracking in ViewModels
@HiltViewModel
class ArticleViewModel @Inject constructor(
private val repository: ArticleRepository,
private val analytics: AnalyticsTracker
) : ViewModel() {
fun onArticleOpened(articleId: String) = viewModelScope.launch {
val article = repository.getArticle(articleId)
analytics.track(AnalyticsEvent.ArticleViewed(
articleId = article.id,
title = article.title,
source = "feed",
readingTime = article.estimatedReadingTime
))
}
}
Funnel Definition
A funnel tracks a multi-step user journey:
Checkout Funnel:
1. cart_view (users who saw the cart)
2. checkout_started (tapped "Checkout")
3. payment_info_entered (filled payment form)
4. checkout_completed (order placed)
Drop-off rate:
cart_view → checkout_started: 40% conversion (60% abandon)
checkout_started → payment_entered: 80%
payment_entered → completed: 85%
Product decision: fix the 60% cart-to-checkout drop-off
// Instrument each funnel step
fun onCartViewed() = analytics.track(AnalyticsEvent.CartViewed(itemCount = items.size))
fun onCheckoutTapped() = analytics.track(AnalyticsEvent.CheckoutStarted(cartValue = cart.total))
fun onPaymentEntered() = analytics.track(AnalyticsEvent.PaymentInfoEntered)
fun onOrderPlaced(orderId: String) = analytics.track(AnalyticsEvent.CheckoutCompleted(orderId))
User Properties for Segmentation
// Set once at login; used to segment all events
analytics.setUserId(user.id)
analytics.setUserProperty("subscription_tier", user.tier.name) // FREE, PRO, ENTERPRISE
analytics.setUserProperty("days_since_signup", user.daysSinceSignup.toString())
analytics.setUserProperty("content_language", user.preferredLanguage)
Key Takeaways
| Practice | Rule |
|---|---|
noun_verb naming | Consistent, searchable, queryable in BigQuery |
| Abstraction layer | Never call Firebase directly from UI; allows mocking in tests |
| User properties | Set at login; enable cohort analysis in analytics dashboard |
| Funnel instrumentation | Every step of a critical journey must be an event |
| Avoid PII in events | No names, emails, or exact locations in event params |
| Debug view | Enable Firebase DebugView on device to validate events in real time |