Irbisa · Шпаргалка
Аналитика и эксперименты
Таксономия событий, воронки, crash-free rate, ANR, трейсинг, remote config, A/B, сэмплинг
Что такое DAU, MAU и retention D1/D7 и какие события приложение должно отправлять, чтобы их посчитать?
Чем отличаются feature flag, remote config и A/B-тест?
Ваши события:
checkout_button_clicked,cart_button_clickedи ещё восемнадцать таких же. Что не так с этой таксономией?// 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?>) {…Новые экраны снова и снова уходят в релиз без события screen view, потому что кто-то забыл вызов. Где должен жить трекинг экранов?
@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…Как не допустить персональные данные в события аналитики на уровне кода, а не надеяться, что каждый разработчик о них помнит?
// 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}…Продакт спрашивает, на каком шаге чекаута отваливаются люди, а ваши события не могут ответить. Что должно быть в каждом событии шага?
Как проверить, что приложение отправляет правильные события аналитики?
interface Analytics { fun log(event: AnalyticsEvent) } class FakeAnalytics : Analytics { val events = mutableListOf<AnalyticsEvent>() override fun log(event: AnalyticsEvent) { events += event }}…Пользователи сорок минут едут в тоннеле, а в конце принудительно закрывают приложение. Что должен делать клиент аналитики, чтобы эти события всё-таки дошли?
@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…Отправка батча упала по таймауту, клиент повторил её, и за ночь покупок стало на шесть процентов больше. Как сделать повторы безопасными?
Часы на устройстве пользователя спешат на два дня, и его события оказываются в будущем. Какое время использует анализ?
// 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…Дашборд показывает 99,9% crash-free сессий и 99,5% crash-free пользователей для одного релиза. Почему цифры разные и по какой из них ставить алерт?
Crashlytics показывает 99,9% crash-free, а в отзывах пишут, что приложение зависает. Где видны эти зависания?
// 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() }…В тикете поддержки пишут, что загрузка на одном телефоне молча не удалась, а дашборд крэшей пуст. Что записывать, чтобы следующий случай можно было воспроизвести?
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)…Вы выключили kill switch час назад, но свежие установки поначалу всё ещё запускают сломанную фичу. Почему?
class ConfigGate(private val rc: FirebaseRemoteConfig) { fun init() { rc.setConfigSettingsAsync(remoteConfigSettings { minimumFetchIntervalInSeconds = if (BuildConfig.DEBUG) 0 else 3600 fetchTimeoutInSeconds = 10 })…Эксперимент 50/50 на дашборде показывает 60/40, а прирост похож на шум. Где сломались распределение и экспозиция?
Тап ощущается медленным и заканчивается спаном на бэкенде, но трейс приложения и трейс сервера остаются двумя несвязанными трейсами. Как их соединить?
// 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()…Продакт хочет читать staged rollout на 5% как A/B-тест против сборки прошлой недели. Что вы ему скажете?
Вариант поднял конверсию чекаута на 3%, но холодный старт стал медленнее на 80 мс. Выкатываете?
Счёт за аналитику вырос втрое в месяц, когда вы добавили событие показа. Какое сэмплирование его сократит и не сломает воронку незаметно?
Пятая часть пользователей отказывается от согласия на аналитику, и в тот же день график DAU проседает. Что ещё можно измерить и что делать с событиями в очереди?