Irbisa · Шпаргалка

Аналитика и эксперименты

Таксономия событий, воронки, crash-free rate, ANR, трейсинг, remote config, A/B, сэмплинг

Middle-разработчик · 20 вопросов

  1. 01

    Что такое DAU, MAU и retention D1/D7 и какие события приложение должно отправлять, чтобы их посчитать?

    DAU и MAU показывают число уникальных активных пользователей за день или за 30 дней; retention D7 означает долю пользователей, установивших приложение в определённый день и вернувшихся на седьмой день.

  2. 02

    Чем отличаются feature flag, remote config и A/B-тест?

    Feature flag служит переключателем вкл/выкл вокруг уже выпущенного кода, remote config представляет собой серверное хранилище, которое доставляет значения флагов и другие параметры, а A/B-тест случайно распределяет пользователей по вариантам и измеряет разницу.

  3. 03

    Ваши события: checkout_button_clicked, cart_button_clicked и ещё восемнадцать таких же. Что не так с этой таксономией?

    Эти двадцать имён сводятся к одному событию button_tapped со свойством button_id.

    // 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

    Новые экраны снова и снова уходят в релиз без события 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…
  5. 05

    Как не допустить персональные данные в события аналитики на уровне кода, а не надеяться, что каждый разработчик о них помнит?

    Сделайте схему событий единственным способом отправить данные: типизированные события с allowlist свойств, значения только enum, id и числа, и один слой очистки перед 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

    Продакт спрашивает, на каком шаге чекаута отваливаются люди, а ваши события не могут ответить. Что должно быть в каждом событии шага?

    Воронка склеивает события в одну попытку одного человека, поэтому каждому шагу нужны одна и та же идентичность пользователя, надёжный порядок и id самой попытки.

  7. 07

    Как проверить, что приложение отправляет правильные события аналитики?

    Спрячьте SDK вендора за собственным интерфейсом Analytics, проверяйте фейк в unit- и UI-тестах, а реальный пайплайн перед релизом смотрите вручную в Firebase DebugView.

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

    Пользователи сорок минут едут в тоннеле, а в конце принудительно закрывают приложение. Что должен делать клиент аналитики, чтобы эти события всё-таки дошли?

    Каждое событие нужно сохранить на диск сразу после track() (на фоновом писателе, не в главном потоке), а очередь должна переживать смерть процесса, повторять отправку с backoff и удалять строки только после того, как сервер их принял.

    @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

    Отправка батча упала по таймауту, клиент повторил её, и за ночь покупок стало на шесть процентов больше. Как сделать повторы безопасными?

    Давайте каждому событию собственный id в момент вызова track(), храните его вместе с событием, и пусть сервер отбрасывает дубликаты по этому id.

  10. 10

    Часы на устройстве пользователя спешат на два дня, и его события оказываются в будущем. Какое время использует анализ?

    Отправляйте оба: клиентское время нужно, чтобы упорядочить события внутри сессии, а серверное нужно, чтобы его скорректировать.

    // 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

    Дашборд показывает 99,9% crash-free сессий и 99,5% crash-free пользователей для одного релиза. Почему цифры разные и по какой из них ставить алерт?

    Они делят на разное: пользователь считается упавшим на всё окно, если упала хотя бы одна из его сессий, поэтому crash-free users почти всегда ниже.

  12. 12

    Crashlytics показывает 99,9% crash-free, а в отзывах пишут, что приложение зависает. Где видны эти зависания?

    Зависание означает решение ОС, что приложение не отвечает, поэтому запись о нём делает платформа, а не ваш обработчик крэшей: Play vitals и ApplicationExitInfo на 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

    В тикете поддержки пишут, что загрузка на одном телефоне молча не удалась, а дашборд крэшей пуст. Что записывать, чтобы следующий случай можно было воспроизвести?

    Отправляйте пойманную ошибку как non-fatal с отдельным типом исключения и окружайте её breadcrumbs и custom keys.

    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

    Вы выключили kill switch час назад, но свежие установки поначалу всё ещё запускают сломанную фичу. Почему?

    Пока загруженный конфиг не активирован, каждое чтение возвращает дефолты, вшитые в приложение.

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

    Эксперимент 50/50 на дашборде показывает 60/40, а прирост похож на шум. Где сломались распределение и экспозиция?

    Сплит, отличающийся от настроенного, называется sample ratio mismatch (SRM), и на мобильных чаще всего причина в событии экспозиции, а не в распределении по бакетам.

  16. 16

    Тап ощущается медленным и заканчивается спаном на бэкенде, но трейс приложения и трейс сервера остаются двумя несвязанными трейсами. Как их соединить?

    Передавайте контекст трейса по сети: приложение открывает корневой спан на тапе и отправляет его id в заголовке W3C traceparent, и спан сервера становится дочерним в том же трейсе, а не начинает новый.

    // 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

    Продакт хочет читать staged rollout на 5% как A/B-тест против сборки прошлой недели. Что вы ему скажете?

    У staged rollout нет контрольной группы: он сравнивает разных людей в разные недели, поэтому любая разница смешивает изменение с тем, кто обновляется первым, и со всем остальным, что случилось за неделю.

  18. 18

    Вариант поднял конверсию чекаута на 3%, но холодный старт стал медленнее на 80 мс. Выкатываете?

    Пока нет: сначала посмотрите результат по классам устройств.

  19. 19

    Счёт за аналитику вырос втрое в месяц, когда вы добавили событие показа. Какое сэмплирование его сократит и не сломает воронку незаметно?

    Сначала агрегируйте на устройстве, а если сэмплировать всё же нужно, сэмплируйте пользователей, а не события.

  20. 20

    Пятая часть пользователей отказывается от согласия на аналитику, и в тот же день график DAU проседает. Что ещё можно измерить и что делать с событиями в очереди?

    Измеряйте по когорте с согласием и сравнивайте доли, а не абсолютные числа, плюс смоделированные итоги из Consent Mode; события в очереди держите до ответа пользователя, отправляйте только разрешённые категории, а при отказе удаляйте.