Irbisa · Cheatsheet
Analytics & Experiments
Event taxonomy, funnels, crash-free rate, ANR, tracing, remote config, A/B tests, sampling
What do DAU, MAU and D1/D7 retention mean, and which events does the app need to send to compute them?
What is the difference between a feature flag, remote config and an A/B test?
Your events are
checkout_button_clicked,cart_button_clickedand eighteen more like them. What is wrong with that taxonomy?// 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?>) {…New screens keep shipping without a screen-view event because someone forgot the call. Where should screen tracking live?
@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…How do you keep personal data out of analytics events in code, instead of relying on every developer to remember?
// 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}…Product asks at which checkout step people drop off, and your events cannot answer it. What must every step event carry?
How do you test that the app sends the right analytics events?
interface Analytics { fun log(event: AnalyticsEvent) } class FakeAnalytics : Analytics { val events = mutableListOf<AnalyticsEvent>() override fun log(event: AnalyticsEvent) { events += event }}…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?
@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…A batch upload times out, the client retries it, and purchases jump six percent overnight. How do you make retries safe?
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?
// 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…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?
Crashlytics says 99.9% crash-free, yet the store reviews say the app freezes. Where do those freezes show up?
// 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() }…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?
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)…You flipped a kill switch an hour ago, yet fresh installs still run the broken feature at first. Why?
class ConfigGate(private val rc: FirebaseRemoteConfig) { fun init() { rc.setConfigSettingsAsync(remoteConfigSettings { minimumFetchIntervalInSeconds = if (BuildConfig.DEBUG) 0 else 3600 fetchTimeoutInSeconds = 10 })…A 50/50 experiment reads 60/40 on the dashboard and the lift looks like noise. Where did assignment and exposure go wrong?
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?
// 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()…Product wants to read the 5% staged rollout as an A/B test against last week's build. What do you tell them?
The variant lifted checkout conversion 3% but cold start regressed 80 ms. Do you ship it?
Your analytics bill tripled the month you added an impression event. Which sampling cuts it without quietly breaking the funnel?
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?