androidengineers.Book a session

Monitoring & Analytics

Exercise: Define KPI Dashboards

exercise50 minMedium

Design the analytics instrumentation and KPI dashboard for an article reading app. The app has a free tier (5 articles/month) and a paid pro tier (unlimited articles + offline).

Goal

  • Define 5 core KPIs with their measurement method
  • Design the event taxonomy for the checkout funnel
  • Implement the analytics layer in code
  • Describe what the "health dashboard" looks like

Step 1: Define KPIs

KPIDefinitionTargetHow measured
DAU/MAU ratioDaily actives / Monthly actives> 30%Firebase DAO cohort
Article completion ratearticle_read_complete / article_view> 60%Event funnel
Free-to-Pro conversionUsers on trial → paid in 14 days> 8%User property + subscription_started
Crash-free sessionsSessions without crashes> 99.5%Firebase Crashlytics
P90 article load time90th percentile of article_detail_load trace< 2sFirebase Performance

Step 2: Event Taxonomy

Reading Funnel

// User opens the app
data class AppOpened(val source: String) : AnalyticsEvent() {
    override val name = "app_opened"
    override val params = mapOf("source" to source)  // "notification", "direct", "widget"
}

// Article card visible in feed
data class ArticleImpression(val articleId: String, val position: Int) : AnalyticsEvent() {
    override val name = "article_impression"
    override val params = mapOf("article_id" to articleId, "list_position" to position)
}

// User taps article card
data class ArticleOpened(val articleId: String, val source: String) : AnalyticsEvent() {
    override val name = "article_view"
    override val params = mapOf("article_id" to articleId, "source" to source)
}

// User scrolled to 80%+ of article
data class ArticleReadComplete(
    val articleId: String,
    val timeSpentSeconds: Int,
    val wordsRead: Int
) : AnalyticsEvent() {
    override val name = "article_read_complete"
    override val params = mapOf(
        "article_id" to articleId,
        "time_spent_seconds" to timeSpentSeconds,
        "words_read" to wordsRead
    )
}

Monetization Funnel

// User hits paywall (free tier exhausted)
data class PaywallShown(val trigger: String) : AnalyticsEvent() {
    override val name = "paywall_shown"
    override val params = mapOf("trigger" to trigger)  // "article_limit", "offline_gate"
}

// User taps "Start free trial"
data class TrialStarted(val plan: String) : AnalyticsEvent() {
    override val name = "trial_started"
    override val params = mapOf("plan" to plan)  // "monthly", "annual"
}

// Subscription confirmed (server-side event mirrored to analytics)
data class SubscriptionStarted(
    val plan: String,
    val priceCents: Long,
    val isUpgrade: Boolean
) : AnalyticsEvent() {
    override val name = "subscription_started"
    override val params = mapOf(
        "plan" to plan,
        "price_cents" to priceCents,
        "is_upgrade" to isUpgrade
    )
}

// Subscription cancelled
data class SubscriptionCancelled(val plan: String, val reason: String?) : AnalyticsEvent() {
    override val name = "subscription_cancelled"
    override val params = mapOf("plan" to plan, "reason" to (reason ?: "unknown"))
}

Step 3: Analytics Implementation

@HiltViewModel
class ArticleDetailViewModel @Inject constructor(
    private val repository: ArticleRepository,
    private val analytics: AnalyticsTracker,
    private val perf: FirebasePerformance
) : ViewModel() {

    private val openedAt = System.currentTimeMillis()
    private var trace: Trace? = null

    fun onScreenOpened(articleId: String, source: String) {
        trace = perf.newTrace("article_detail_load").also { it.start() }
        analytics.track(AnalyticsEvent.ArticleOpened(articleId, source))
        loadArticle(articleId)
    }

    private fun loadArticle(id: String) = viewModelScope.launch {
        try {
            val article = repository.getArticle(id)
            trace?.stop()
            _state.value = UiState.Success(article)
        } catch (e: Exception) {
            trace?.putAttribute("error", e.javaClass.simpleName)
            trace?.stop()
            _state.value = UiState.Error(e.message ?: "")
        }
    }

    fun onScrolledToEnd(articleId: String, wordCount: Int) {
        val timeSpent = ((System.currentTimeMillis() - openedAt) / 1000).toInt()
        analytics.track(AnalyticsEvent.ArticleReadComplete(
            articleId = articleId,
            timeSpentSeconds = timeSpent,
            wordsRead = wordCount
        ))
    }
}

Step 4: Health Dashboard Definition

Daily Health Dashboard (checked every morning)

┌─────────────────────────────────────────────────────┐
│  App Health — $(Date)                               │
├──────────────┬───────────┬──────────────────────────┤
│ Metric       │ Today     │ vs 7d avg   │ Status     │
├──────────────┼───────────┼─────────────┼────────────┤
│ Crash-free   │ 99.7%     │ +0.1%       │ ✅ Good    │
│ ANR-free     │ 99.9%     │  0.0%       │ ✅ Good    │
│ DAU          │ 48,230    │ -2.1%       │ ⚠️ Watch   │
│ Article done │ 62%       │ +3%         │ ✅ Good    │
│ Paywall conv │ 7.8%      │ -0.4%       │ ⚠️ Watch   │
│ P90 load     │ 1.8s      │ +0.2s       │ ✅ Good    │
└──────────────┴───────────┴─────────────┴────────────┘

Alert Thresholds (PagerDuty / Slack)

// Pseudo-code: server-side alert rules
val alerts = listOf(
    Alert("crash_free_sessions", threshold = 99.0, comparator = LESS_THAN, severity = P1),
    Alert("anr_rate", threshold = 0.5, comparator = GREATER_THAN, severity = P1),
    Alert("dau_drop", threshold = 10.0, comparator = GREATER_THAN, unit = PERCENT_DROP, severity = P2),
    Alert("paywall_conversion", threshold = 5.0, comparator = LESS_THAN, severity = P2),
    Alert("p90_load_time_seconds", threshold = 3.0, comparator = GREATER_THAN, severity = P3)
)

Verification Checklist

[ ] Each KPI has exactly one measurement owner (who queries it each week)
[ ] Every funnel step has a corresponding analytics event
[ ] Analytics events use the abstraction layer (no direct Firebase calls in UI)
[ ] Debug: Firebase DebugView shows events in real time
[ ] User properties set at login: subscription_tier, days_since_signup
[ ] Remote Config: enable_pro_features flag wired with default = false
[ ] Dashboard reviewed in next sprint retro: are these the right KPIs?

YOUR LEARNING JOURNEY

0 of 177 available lessons completed

Progress saved in this browser. No account needed.
Exercise: Define KPI Dashboards | Android System Design | Android Engineers