Irbisa · Cheatsheet

Analytics & Experiments

Event taxonomy, funnels, crash-free rate, ANR, tracing, remote config, A/B tests, sampling

Middle Developer · 20 questions

  1. 01

    What do DAU, MAU and D1/D7 retention mean, and which events does the app need to send to compute them?

    DAU and MAU count distinct users active in a day or a 30-day window; D7 retention is the share of users who installed on a given day and came back on day seven.

  2. 02

    What is the difference between a feature flag, remote config and an A/B test?

    A feature flag is an on/off gate around code you already shipped, remote config is the server-side store that delivers flag values and other parameters, and an A/B test randomly assigns users to variants and measures the difference.

  3. 03

    Your events are checkout_button_clicked, cart_button_clicked and eighteen more like them. What is wrong with that taxonomy?

    Those twenty names are one event, button_tapped, with a button_id property.

    // wrong: one name per button, the vocabulary grows with the UIanalytics.log(AnalyticsEvent("checkout_button_clicked", emptyMap()))analytics.log(AnalyticsEvent("cart_button_clicked", emptyMap())) // right: a closed set of typed events, the varying part in propertiessealed class AnalyticsEvent(val name: String, val props: Map<String, Any?>) {…
  4. 04

    New screens keep shipping without a screen-view event because someone forgot the call. Where should screen tracking live?

    In the navigation layer, registered once: the router already sees every destination change, so a new screen is tracked without anyone remembering a line.

    @Composablefun TrackScreenViews(nav: NavHostController, analytics: Analytics) {  LaunchedEffect(nav) {    nav.currentBackStackEntryFlow      .distinctUntilChangedBy { it.id }        // a new entry, not a recomposition      .mapNotNull { it.destination.route }     // "order/{orderId}": the pattern, not the value…
  5. 05

    How do you keep personal data out of analytics events in code, instead of relying on every developer to remember?

    Make the event schema the only way to send data: typed events with an allowlist of properties, values limited to enums, ids and numbers, and one scrubbing layer in front of every vendor SDK.

    // Only declared keys reach a vendor; the string length cap is a safety net, not a PII filterenum class Prop(val allowsText: Boolean = false) {  SCREEN(allowsText = true), BUTTON_ID(allowsText = true),  RESULTS_COUNT, SEARCH_LENGTH, VALUE_CENTS}…
  6. 06

    Product asks at which checkout step people drop off, and your events cannot answer it. What must every step event carry?

    A funnel joins events into one person's single attempt, so every step needs the same user identity, a reliable order and an id for that attempt.

  7. 07

    How do you test that the app sends the right analytics events?

    Hide the vendor SDK behind your own Analytics interface, assert on a fake in unit and UI tests, and check the real pipeline by hand in Firebase DebugView before release.

    interface Analytics { fun log(event: AnalyticsEvent) } class FakeAnalytics : Analytics {  val events = mutableListOf<AnalyticsEvent>()  override fun log(event: AnalyticsEvent) { events += event }}…
  8. 08

    Users ride a tunnel for forty minutes and force-quit the app at the end. What must the analytics client do so those events still arrive?

    Each event must be persisted to disk right after track() (on a background writer, never the main thread), and the queue must survive process death, retry with backoff and delete rows only after the server accepts them.

    @Entity(tableName = "events")data class QueuedEvent(  @PrimaryKey val insertId: String,   // minted once at track(), reused on every retry  val name: String,  val payloadJson: String,  val clientTs: Long…
  9. 09

    A batch upload times out, the client retries it, and purchases jump six percent overnight. How do you make retries safe?

    Give every event its own id when track() is called, store it with the event, and let the server drop duplicates by that id.

  10. 10

    A user's device clock is two days fast and their events land in the future. Which timestamp does analysis use when a device clock is two days fast?

    Send both: client time to order events within a session, server time to correct it.

    // wrong: two wall-clock reads; an NTP correction between them breaks the durationval startedAt = System.currentTimeMillis()val wrongMs = System.currentTimeMillis() - startedAt   // can come out negative // right: monotonic clock for elapsed time, wall clock only for "when"val startedElapsed = SystemClock.elapsedRealtime()     // counts from boot, NTP cannot move it…
  11. 11

    The dashboard shows 99.9% crash-free sessions and 99.5% crash-free users for the same release. Why do they differ, and which one do you alert on?

    They divide by different things: a user counts as crashed for the whole window if any one of their sessions crashed, so crash-free users is almost always the lower number.

  12. 12

    Crashlytics says 99.9% crash-free, yet the store reviews say the app freezes. Where do those freezes show up?

    A freeze is the OS deciding the app is unresponsive, so the record comes from the platform, not your crash handler: Play vitals and ApplicationExitInfo on Android.

    // Read once on the next launch: the system kept the reason the last process diedfun reportPreviousExits(context: Context) {  if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) return  val am = context.getSystemService(ActivityManager::class.java)  am.getHistoricalProcessExitReasons(context.packageName, 0, 5)    .filter { it.timestamp > lastReportedExitTs() }…
  13. 13

    A support ticket says an upload silently failed on one phone, and the crash dashboard shows nothing. What do you record so the next one is reproducible?

    Report the caught error as a non-fatal with a distinct exception type, and attach breadcrumbs and custom keys around it.

    class UploadFailed(cause: Throwable) : Exception("upload failed", cause) suspend fun upload(order: Order, step: UploadStep, attempt: Int) {  val crashlytics = Firebase.crashlytics  try {    api.upload(order)…
  14. 14

    You flipped a kill switch an hour ago, yet fresh installs still run the broken feature at first. Why?

    Until a fetched config is activated, every read returns the defaults compiled into the app.

    class ConfigGate(private val rc: FirebaseRemoteConfig) {  fun init() {    rc.setConfigSettingsAsync(remoteConfigSettings {      minimumFetchIntervalInSeconds = if (BuildConfig.DEBUG) 0 else 3600      fetchTimeoutInSeconds = 10    })…
  15. 15

    A 50/50 experiment reads 60/40 on the dashboard and the lift looks like noise. Where did assignment and exposure go wrong?

    A split that differs from the configured one is a sample ratio mismatch (SRM), and on mobile the most common cause is the exposure event, not the bucketing.

  16. 16

    A tap feels slow and ends in a backend span, but the app trace and the server trace are two unrelated traces. How do you join them?

    Propagate trace context over the network: the app opens a root span at the tap and sends its ids in a W3C traceparent header, so the server's span becomes a child in the same trace instead of starting a new one.

    // The OkHttp instrumentation captures the caller's context when the Call is created,// so it survives enqueue() onto OkHttp's threads and injects `traceparent`val callFactory: Call.Factory = OkHttpTelemetry.builder(openTelemetry).build()  .newCallFactory(okHttpClient) val retrofit = Retrofit.Builder()…
  17. 17

    Product wants to read the 5% staged rollout as an A/B test against last week's build. What do you tell them?

    A staged rollout has no control group: it compares different people in different weeks, so every difference mixes the change with who updates early and with whatever else happened that week.

  18. 18

    The variant lifted checkout conversion 3% but cold start regressed 80 ms. Do you ship it?

    Not yet: check the result by device tier first.

  19. 19

    Your analytics bill tripled the month you added an impression event. Which sampling cuts it without quietly breaking the funnel?

    Aggregate on the device first, and if you still must sample, sample users, not events.

  20. 20

    A fifth of users decline analytics consent and the DAU chart drops that day. What can you still measure, and what happens to events already queued?

    Measure from the consented cohort and compare rates, not counts, plus modelled totals from Consent Mode; hold queued events until the user answers, send only the granted categories, and delete them on refusal.