EUM мониторинг Android-приложений
Какой способ подключения выбрать
Рекомендуемый способ — открытая библиотека dd-sdk-android 3.x. Новые приложения подключайте по ней: описание начинается с раздела Введение.
Android-агент Proto Observability (пакет ru.proto_group:pobp-sdk-android) — устаревший способ инструментации. Он по-прежнему поддерживается для приложений, которые уже его используют, и описан в конце страницы, в разделе Android-агент Proto Observability (устаревший способ). Для таких приложений рекомендуется переход на dd-sdk-android.
На этой странице:
- Введение
- Требования
- Какие модули подключать
- Адрес приёма данных
- Подключение зависимостей
- Инициализация
- Отслеживание экранов и действий
- Сетевые запросы и сквозная трассировка
- Пользовательские данные, события и ошибки
- Исходящий трафик приложения
- Проверка подключения
- Область применения инструкции
- Диагностика неполадок
- Android-агент Proto Observability (устаревший способ)
Введение
Библиотека dd-sdk-android (открытый исходный код, лицензия Apache 2.0) передаёт данные RUM в формате, который Proto Observability Platform принимает и обрабатывает штатно. Код библиотеки не меняется: всё, что нужно для передачи данных в платформу, задаётся параметрами инициализации.
Что при этом важно знать:
- Учётная запись Datadog не требуется. Токен клиента (
clientToken) и идентификатор приложения (applicationId) передаются в запросах как есть; платформа их не проверяет и не обращается к внешним сервисам для их валидации. Библиотека требует только, чтобы значения были непустыми, а токен состоял из латинских букв, цифр и печатных символов. - По умолчанию библиотека отправляет данные в облако Datadog. Адрес приёма ProtoOBP нужно задать явно (
useCustomEndpoint) в каждом включённом модуле. Модуль без собственного адреса передаёт данные на серверы Datadog. - Адрес приёма подставляется целиком. Библиотека не дописывает путь к значению
useCustomEndpoint, поэтому указывать нужно полный URL вместе с путём. - Данные передаются только по HTTPS. Адрес
http://библиотека не использует. - Синхронизация времени обращается к внешним NTP-серверам, и параметра, который её отключает, нет. Обращение исключается подменой библиотеки синхронизации при сборке приложения, см. Синхронизация времени.
Инструкция проверена на версии dd-sdk-android 3.14.1. Для приложений, которые уже используют Android-агент Proto Observability, см. раздел Android-агент Proto Observability (устаревший способ) и Переход на dd-sdk-android. Для Flutter-приложений библиотека подключается через плагин, см. Flutter.
Состав атрибутов, которые собираются и становятся доступны в интерфейсе, приведён на странице Собираемые данные RUM и Mobile.
Требования
| Параметр | Значение |
|---|---|
| Версия библиотеки | 3.14.1 (проверена), ветка 3.x |
| Минимальная версия Android | 6.0 (minSdk 23) |
compileSdk приложения | 35 и новее: этого требуют библиотеки AndroidX, от которых зависит dd-sdk-android |
| Java и Kotlin | Целевая версия байт-кода (jvmTarget) 11 и новее. Проверено с Kotlin 2.2 и Android Gradle Plugin 8.10 |
| Репозиторий | Maven Central (mavenCentral()), группа com.datadoghq |
| Сетевой доступ приложения | HTTPS к адресу приёма ProtoOBP с сертификатом, которому доверяет Android: TLS 1.3 или TLS 1.2 с наборами шифров ECDHE и AES-GCM |
Какие модули подключать
Библиотека состоит из независимых модулей. Для передачи данных RUM в платформу нужны два:
| Модуль | Подключать | Назначение и примечания |
|---|---|---|
dd-sdk-android-rum | да | Экраны, действия пользователя, ошибки, длительные операции главного потока, время запуска. Ядро dd-sdk-android-core подключается вместе с ним |
dd-sdk-android-okhttp | да, если приложение использует OkHttp или Retrofit | Сетевые запросы и заголовки трассировки. Подключает за собой модуль dd-sdk-android-trace |
dd-sdk-android-trace | через dd-sdk-android-okhttp | Нужен для заголовков трассировки сетевых запросов, см. Сетевые запросы и сквозная трассировка. Ручные спаны этого модуля платформа принимает с версии 204; для их приёма лицензия должна включать модуль APM |
dd-sdk-android-compose | по необходимости | Экраны и нажатия в Jetpack Compose |
dd-sdk-android-logs | нет | Логи приложения. Передача логов настраивается отдельно, см. Область применения |
dd-sdk-android-ndk | нет | Падения нативного кода. Символы NDK платформа не принимает |
dd-sdk-android-session-replay | нет | Запись сессий. Платформой не поддерживается |
dd-sdk-android-webview | нет | Связка с веб-страницами внутри WebView |
dd-sdk-android-profiling | нельзя | Модуль обращается к серверам Datadog для проверки квоты, и этот адрес не переопределяется настройками |
dd-sdk-android-flags | нет | Функциональные флаги. Платформой не поддерживается; без собственных адресов обращается к серверам Datadog |
Модуль dd-sdk-android-rum не тянет за собой остальные: логи, нативные падения, запись сессий и WebView не собираются, пока соответствующий модуль не подключён и не включён в коде.
Gradle-плагин dd-sdk-android-gradle-plugin не подключайте. Он выгружает файлы соответствия R8/ProGuard и символы NDK на серверы Datadog. Без плагина библиотека пишет в журнал сообщение уровня INFO о том, что стектрейсы не будут деобфусцированы, — на работу это не влияет.
Адрес приёма данных
Данные RUM принимаются по адресу:
https://<адрес ProtoOBP>/api/v2/rum
Равнозначный адрес, если на вашем периметре уже настроена маршрутизация по этому префиксу:
https://<адрес ProtoOBP>/mobile/api/v2/rum
Адрес указывается без косой черты в конце и без параметров запроса. Библиотека дописывает к адресу строку ?ddsource=android как есть, поэтому параметры после ? в самом адресе испортятся и не дойдут до платформы.
Перед подключением уточните у администратора платформы:
- полный адрес приёма для вашего контура;
- что лицензия платформы включает модуль мониторинга цифрового опыта (DEM) — без него запросы отклоняются с кодом 403;
- что приём RUM включён в конфигурации компонента приёма данных.
Подключение зависимостей
В settings.gradle.kts должен быть подключён репозиторий Maven Central:
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
}
}
В build.gradle.kts модуля приложения:
android {
compileSdk = 35 // или новее
defaultConfig {
minSdk = 23
}
}
dependencies {
implementation("com.datadoghq:dd-sdk-android-rum:3.14.1")
// Если приложение использует OkHttp или Retrofit:
implementation("com.datadoghq:dd-sdk-android-okhttp:3.14.1")
}
Для Groovy-скриптов (build.gradle) строки те же в форме implementation "com.datadoghq:dd-sdk-android-rum:3.14.1".
Инициализация
Библиотеку нужно инициализировать как можно раньше — в Application.onCreate():
import android.app.Application
import com.datadog.android.Datadog
import com.datadog.android.core.configuration.Configuration
import com.datadog.android.privacy.TrackingConsent
import com.datadog.android.rum.Rum
import com.datadog.android.rum.RumConfiguration
import com.datadog.android.trace.Trace
import com.datadog.android.trace.TraceConfiguration
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
// Адрес ProtoOBP. Пути /api/v2/rum и /api/v2/spans добавляются ниже явно:
// библиотека сама их не дописывает.
val protoObp = "https://protoobp.example.ru"
val configuration = Configuration.Builder(
// Произвольная непустая строка: платформа токен не проверяет.
clientToken = "protoobp",
// Окружение — попадает в данные как тег и доступно для фильтрации.
env = "prod",
// Вариант сборки (flavor), если используется; иначе пустая строка.
variant = "",
// Название приложения в интерфейсе платформы.
// Если не задано, используется имя пакета приложения.
service = "my-android-app"
)
// Падения приложения в этой схеме не собираются, см. «Область применения».
.setCrashReportsEnabled(false)
.build()
Datadog.initialize(this, configuration, TrackingConsent.GRANTED)
Rum.enable(
RumConfiguration.Builder(applicationId = "my-android-app")
// 100 — собирать все сессии, 10 — 10 % сессий.
.setSessionSampleRate(100f)
// Полный адрес приёма ProtoOBP.
.useCustomEndpoint("$protoObp/api/v2/rum")
// Внутренняя телеметрия библиотеки платформой не используется — отключаем.
.setTelemetrySampleRate(0f)
// Имена нажатий, см. «Отслеживание экранов и действий».
.trackUserInteractions(interactionPredicate = TextInteractionPredicate())
.build()
)
// Модуль трассировки (подключается вместе с dd-sdk-android-okhttp) нужен
// для заголовков трассировки сетевых запросов.
// Адрес обязателен: без него данные модуля уходят на серверы Datadog.
Trace.enable(
TraceConfiguration.Builder()
.useCustomEndpoint("$protoObp/api/v2/spans")
.build()
)
}
}
Класс приложения указывается в AndroidManifest.xml атрибутом android:name элемента <application>.
Данные о пользователе, собственные атрибуты, действия и ошибки передаются отдельными вызовами после инициализации, см. Пользовательские данные, события и ошибки.
Параметры, которые не нужно менять относительно значений по умолчанию:
| Параметр | Значение по умолчанию | Почему оставить |
|---|---|---|
Configuration.Builder.useSite(…) | DatadogSite.US1 | При заданных адресах приёма в сеть не попадает |
Configuration.Builder.setRemoteConfigurationId(…) | не задан | Включение приводит к запросам к серверам Datadog, адрес не переопределяется |
RumConfiguration.Builder.trackBackgroundEvents(…) | false | События в фоне увеличивают объём данных без пользы для анализа экранов |
TraceConfiguration.Builder.setStatsComputationEnabled(…) | false | Экспериментальная статистика на устройстве; без собственного адреса отправляется на серверы Datadog |
Отслеживание экранов и действий
Экраны. По умолчанию каждая Activity становится экраном, имя экрана — полное имя класса. Стратегию отслеживания задаёт метод useViewTrackingStrategy в RumConfiguration.Builder:
| Стратегия | Что считается экраном |
|---|---|
ActivityViewTrackingStrategy(trackExtras = false) | Activity (значение по умолчанию) |
FragmentViewTrackingStrategy(trackArguments = false) | Fragment |
MixedViewTrackingStrategy(trackExtras = false) | Activity и Fragment |
NavigationViewTrackingStrategy(R.id.nav_host, trackArguments = false) | Пункты назначения Navigation Component |
Выберите одну стратегию. Для Jetpack Compose экраны отмечаются через NavigationViewTrackingEffect из модуля dd-sdk-android-compose, а Activity, в которых размещён Compose, исключаются из стратегии через componentPredicate. Иначе один и тот же экран попадёт в данные дважды.
Чтобы задать понятные имена экранов или исключить служебные Activity, передайте в стратегию собственный ComponentPredicate:
object ScreenNames : ComponentPredicate<Activity> {
override fun accept(component: Activity): Boolean = component !is SplashActivity
override fun getViewName(component: Activity): String? = when (component) {
is CatalogActivity -> "Catalog"
is ProductActivity -> "Product"
else -> null // имя класса
}
}
RumConfiguration.Builder(applicationId = "my-android-app")
.useViewTrackingStrategy(ActivityViewTrackingStrategy(trackExtras = false, componentPredicate = ScreenNames))
Действия. Нажатия, прокрутки и свайпы собираются по умолчанию. В версии 3.x имя нажатия остаётся пустым, пока не задан InteractionPredicate: в интерфейсе действие отображается как tap без указания элемента. Простейший вариант — текст нажатого элемента:
class TextInteractionPredicate : InteractionPredicate {
override fun getTargetName(target: Any): String? = (target as? TextView)?.text?.toString()
}
Если в тексте элементов бывают персональные данные (имена, номера), возвращайте вместо текста собственные имена по идентификатору элемента.
Сетевые запросы и сквозная трассировка
Запросы OkHttp собираются как события ресурсов (адрес, метод, код ответа, длительность, размер) через перехватчик из модуля dd-sdk-android-okhttp:
import com.datadog.android.okhttp.DatadogEventListener
import com.datadog.android.okhttp.DatadogInterceptor
import com.datadog.android.trace.TracingHeaderType
// Домены вашего бэкенда: к запросам на них добавляются заголовки трассировки.
val tracedHosts = mapOf("api.example.ru" to setOf(TracingHeaderType.TRACECONTEXT))
val okHttpClient = OkHttpClient.Builder()
.addInterceptor(DatadogInterceptor.Builder(tracedHosts).build())
// Детализация длительности: DNS, соединение, TLS, первый байт, загрузка.
.eventListenerFactory(DatadogEventListener.Factory())
.build()
Если в клиенте есть другие перехватчики, DatadogInterceptor добавляется первым. Для Retrofit этот клиент передаётся в Retrofit.Builder().client(okHttpClient).
К запросам на домены из tracedHosts добавляются заголовки трассировки. Агенты Proto Observability на бэкенде принимают заголовки стандарта W3C Trace Context (traceparent, tracestate) без дополнительной настройки, поэтому рекомендуется тип TracingHeaderType.TRACECONTEXT. Конструктор со списком доменов DatadogInterceptor.Builder(listOf("api.example.ru")) отправляет и traceparent, и заголовки x-datadog-* — такой вариант тоже работает.
Домены указываются без схемы (api.example.ru, а не https://api.example.ru), поддомены включаются автоматически, допускаются маски вида *.example.ru.
Заголовки трассировки добавляются, только если включён модуль трассировки (Trace.enable в примере инициализации). Без него запросы собираются как события ресурсов, но без заголовков и без связи с трейсами бэкенда, а в журнале библиотеки появляется предупреждение you did not enable the TracingFeature. Спаны самих сетевых запросов модуль трассировки не отправляет, пока включён RUM: они передаются в составе событий ресурсов.
Если на серверах из списка установлены агенты Proto Observability, обработка запроса на бэкенде продолжает трейс, начатый на устройстве пользователя: сетевой запрос с мобильного устройства и его обработка на бэкенде объединяются в единую цепочку — трейс распределённой транзакции, начавшейся на устройстве. В трейсе бэкенда запрос приложения отображается отдельным спаном с данными события RUM (адрес, код ответа, длительность, сессия).
Объединение работает в платформе версии 204 и новее. Библиотека версии 3.x передаёт 128-битные идентификаторы трейсов; в более ранних версиях платформы трейс бэкенда записывается, но с сетевым запросом мобильного приложения не связывается.
Кроме заголовков трассировки перехватчик всегда добавляет к запросам на домены из списка заголовок baggage с идентификатором сессии RUM и идентификатором пользователя из setUserInfo, см. Персональные данные.
Пользовательские данные, события и ошибки
Кроме автоматически собираемых данных (экраны, нажатия, сетевые запросы) приложение может передавать собственные: кто пользователь, к какой организации он относится, в каком контексте работает, какие бизнес-события и ошибки происходят. Для этого используются штатные методы библиотеки, отдельная интеграция не требуется. Вызывайте их после Datadog.initialize и Rum.enable.
Сводка: что вызывать и где это видно в платформе.
| Что передаётся | Вызов | Поле события | Где видно в платформе |
|---|---|---|---|
| Пользователь | Datadog.setUserInfo(id, name, email, extraInfo) | usr.id, usr.name, usr.email, usr.<поле> | Список и карточка сессии; число пользователей на дашбордах считается по usr.id; теги usr.* у всех событий |
| Организация (клиент B2B) | Datadog.setAccountInfo(id, name, extraInfo) | account.id, account.name, account.<поле> | Теги account.* у всех событий |
| Атрибуты сессии | GlobalRumMonitor.get().addAttribute(key, value) | context.<ключ> | Теги context.* у всех последующих событий |
| Атрибуты экрана | GlobalRumMonitor.get().addViewAttributes(map) | context.<ключ> | Теги context.* у событий текущего экрана |
| Бизнес-событие | GlobalRumMonitor.get().addAction(RumActionType.CUSTOM, name, attributes) | action.target.name, context.<ключ> | Событие action: custom (<имя>) в трейсе сессии, поля — теги context.* |
| Ошибка | GlobalRumMonitor.get().addError(…), addErrorWithStacktrace(…) | error.type, error.message, error.stack, error.source | Ошибки мобильного приложения на дашборде, событие error: <тип> в трейсе сессии |
| Функциональный флаг | GlobalRumMonitor.get().addFeatureFlagEvaluation(name, value) | feature_flags.<имя> | Теги feature_flags.* у событий экрана |
| Отметка времени на экране | GlobalRumMonitor.get().addTiming(name) | view.custom_timings.<имя> | Метрика custom_<имя> у события экрана, в наносекундах от открытия экрана |
Все поля доступны для поиска и фильтрации событий, а через семантический слой — для собственных дашбордов и выборок. Вложенные значения разворачиваются в ключи через точку.
Данные пользователя
Вызывайте после входа пользователя в приложение и при смене пользователя. id обязателен, остальное — по необходимости. Произвольные поля в extraInfo попадают в то же поле usr рядом с id, name и email.
Datadog.setUserInfo(
id = "1234",
name = "Василий Пупкин",
email = "vass@pupkine.com",
extraInfo = mapOf("tariff" to "business_pro")
)
// Дополнить уже заданные данные, не перезаписывая id, name и email:
Datadog.addUserProperties(mapOf("segment" to "corporate"))
// При выходе пользователя из приложения:
Datadog.clearUserInfo()
Данные пользователя добавляются ко всем событиям, отправленным после вызова, включая события текущего экрана. События, отправленные раньше, не меняются.
Даже без setUserInfo библиотека присваивает каждой установке приложения анонимный идентификатор usr.anonymous_id (метод trackAnonymousUser в RumConfiguration.Builder, по умолчанию включён). Он позволяет связать сессии одного устройства до входа пользователя. Число пользователей на дашбордах считается только по usr.id.
Данные организации
Если приложением пользуются сотрудники разных компаний-клиентов, организацию удобно передавать отдельно от пользователя:
Datadog.setAccountInfo(id = "acc-7", name = "ООО Ромашка", extraInfo = mapOf("region" to "msk"))
Datadog.addAccountExtraInfo(mapOf("plan" to "enterprise"))
Datadog.clearAccountInfo()
Атрибуты
Атрибуты бывают трёх видов:
- атрибуты сессии добавляются ко всем последующим событиям: тип клиента, тарифный план, роль, признак участия в эксперименте;
- атрибуты экрана действуют только до ухода с текущего экрана: вариант оформления, источник перехода;
- атрибуты события передаются вместе с конкретным действием или ошибкой, как в примерах ниже.
val rum = GlobalRumMonitor.get()
// Атрибуты сессии
rum.addAttribute("client_segment", "corporate")
rum.addAttribute("ab_group", "B")
rum.removeAttribute("ab_group")
// Атрибуты текущего экрана
rum.addViewAttributes(mapOf("screen_variant" to "B"))
Все три вида попадают в поле context события и доступны в платформе под ключами вида context.client_segment. Значение может быть строкой, числом, логическим значением, коллекцией или вложенной структурой Map.
Пользовательские действия
Действие с типом CUSTOM — основной способ передать бизнес-событие вместе с произвольным набором полей:
GlobalRumMonitor.get().addAction(
RumActionType.CUSTOM,
"payment_confirmed",
mapOf(
"payment_type" to "p2p",
"amount" to 12500,
"currency" to "RUB"
)
)
Для действий с длительностью используются парные вызовы — платформа получит длительность действия:
GlobalRumMonitor.get().startAction(RumActionType.CUSTOM, "document_upload", emptyMap())
// … загрузка …
GlobalRumMonitor.get().stopAction(RumActionType.CUSTOM, "document_upload", mapOf("pages" to 3))
Имя действия (payment_confirmed) становится значением поля action.target.name, в трейсе сессии событие называется action: custom (payment_confirmed). Поля из атрибутов попадают в context этого события.
Ошибки
Падения приложения в этой схеме не собираются (setCrashReportsEnabled(false)), а ошибки, которые приложение обработало само, передаются явно. Библиотека фиксирует у каждой ошибки экран, сессию, пользователя и версию приложения, в контексте которых она произошла.
Если есть объект исключения — тип (полное имя класса), сообщение и стектрейс определяются по нему:
try {
transferService.send(request)
} catch (e: IOException) {
GlobalRumMonitor.get().addError(
"Не удалось отправить перевод",
RumErrorSource.SOURCE,
e,
mapOf("payment_type" to "p2p")
)
}
Если объекта нет — сообщение и стектрейс задаются явно, а тип ошибки передаётся служебным атрибутом RumAttributes.INTERNAL_ERROR_TYPE (_dd.error_type):
GlobalRumMonitor.get().addErrorWithStacktrace(
"Не удалось списать средства",
RumErrorSource.CUSTOM,
Thread.currentThread().stackTrace.joinToString("\n"),
mapOf(
RumAttributes.INTERNAL_ERROR_TYPE to "TransferFailed",
"payment_type" to "p2p"
)
)
Параметр источника указывает происхождение ошибки: RumErrorSource.SOURCE — ошибка в коде приложения, NETWORK — сетевая, CUSTOM — прочие. Тип ошибки используется для группировки, поэтому его лучше делать стабильным и не включать в него переменные значения: номера заказов, идентификаторы. Стектрейсы сборок, обработанных R8/ProGuard, приходят в обфусцированном виде.
Функциональные флаги
Если приложение использует функциональные флаги или A/B-эксперименты, передайте результат вычисления флага — события экрана можно будет сравнивать по вариантам:
GlobalRumMonitor.get().addFeatureFlagEvaluation("new_checkout", true)
Собственные замеры времени
Отметка времени внутри экрана — например, момент, когда отрисован каталог или загрузились данные:
GlobalRumMonitor.get().addTiming("catalog_rendered")
Значение — время от открытия экрана до вызова, в наносекундах. В платформе оно доступно как метрика custom_catalog_rendered события экрана. Имя должно быть уникальным в пределах экрана.
Персональные данные
Имя, email и произвольные поля пользователя хранятся в аналитическом контуре платформы вместе с остальными событиями RUM и доступны всем, у кого есть права на просмотр данных приложения. Передавайте только те персональные данные, которые необходимы для разбора обращений пользователей, и не помещайте в атрибуты платёжные и паспортные данные, пароли и токены. Если email или имя передавать нельзя, достаточно id — внутреннего идентификатора пользователя в вашей системе.
Идентификатор пользователя (id из setUserInfo) и идентификатор сессии дополнительно передаются в заголовке baggage запросов на домены вашего бэкенда из списка трассировки. Отключить этот заголовок настройкой нельзя; учитывайте его, если запросы проходят через внешние прокси или журналируются вместе с заголовками.
Исходящий трафик приложения
При настройке по этой инструкции, включая подмену библиотеки синхронизации из раздела Синхронизация времени, приложение обращается по сети только к адресу приёма ProtoOBP. Без подмены добавляются запросы к NTP-серверам.
| Источник трафика | Поведение по умолчанию | Настройка в этой инструкции |
|---|---|---|
| Отправка данных модулей | Серверы Datadog | useCustomEndpoint с адресом ProtoOBP в каждом включённом модуле |
| Синхронизация времени | NTP-серверы 0…3.datadog.pool.ntp.org, UDP 123: при запуске, затем повторно раз в 30 минут | Параметра нет; подмена библиотеки при сборке, см. Синхронизация времени |
| Внутренняя телеметрия библиотеки | Около 20 % сессий; отправляется на адрес приёма RUM, а не во внешние сервисы | setTelemetrySampleRate(0f) |
| Удалённая конфигурация | Выключена | Не включать (setRemoteConfigurationId не вызывать) |
| Статистика трассировки на устройстве | Выключена | Не включать |
| Проверка квоты профилирования | Только при подключённом dd-sdk-android-profiling | Модуль не подключать |
| Функциональные флаги | Только при подключённом dd-sdk-android-flags | Модуль не подключать |
| Файлы соответствия R8/ProGuard, символы NDK | Только при подключённом Gradle-плагине, во время сборки | Плагин не подключать |
Синхронизация времени
Библиотека корректирует время событий по NTP-серверам 0.datadog.pool.ntp.org … 3.datadog.pool.ntp.org. Это имена в пуле NTP Pool Project; серверы пула принадлежат участникам проекта, а не Datadog. С устройства уходят DNS-запрос с этим именем и UDP-пакет NTP на порт 123 — без данных приложения, токенов и идентификаторов. В отличие от iOS-версии, в dd-sdk-android нет параметра, который заменяет или отключает синхронизацию. Если NTP-серверы недоступны, библиотека использует часы устройства, данные при этом не теряются.
Подмена библиотеки синхронизации при сборке. Синхронизацию выполняет библиотека com.lyft.kronos:kronos-android, которую dd-sdk-android подключает как зависимость. Средствами Gradle её можно заменить модулем с тем же программным интерфейсом, который отдаёт часы устройства и не обращается к сети. Код dd-sdk-android при этом не меняется, а сам способ работает на любом устройстве и в любой сети. Способ проверен на версии 3.14.1 (Kronos 0.0.1-alpha11): после замены приложение не открывает ни одного UDP-сокета, время событий соответствует часам устройства.
Модуль kronos-device-clock в проекте (settings.gradle.kts: include(":kronos-device-clock")), файл kronos-device-clock/build.gradle.kts:
plugins {
id("com.android.library")
id("org.jetbrains.kotlin.android")
}
android {
namespace = "com.lyft.kronos"
compileSdk = 35
defaultConfig { minSdk = 23 }
}
Файл kronos-device-clock/src/main/kotlin/com/lyft/kronos/Kronos.kt:
// Программный интерфейс Kronos 0.0.1-alpha11 без сетевой синхронизации.
package com.lyft.kronos
import android.content.Context
import android.os.SystemClock
interface Clock {
fun getCurrentTimeMs(): Long
fun getElapsedTimeMs(): Long
}
data class KronosTime(val posixTimeMs: Long, val timeSinceLastNtpSyncMs: Long?)
interface KronosClock : Clock {
override fun getCurrentTimeMs(): Long = getCurrentTime().posixTimeMs
fun getCurrentNtpTimeMs(): Long?
fun getCurrentTime(): KronosTime
fun sync(): Boolean
fun syncInBackground()
fun shutdown()
}
interface SyncListener {
fun onStartSync(host: String)
fun onSuccess(ticksDelta: Long, responseTimeMs: Long)
fun onError(host: String, throwable: Throwable)
}
object DefaultParam {
val NTP_HOSTS = emptyList<String>()
const val CACHE_EXPIRATION_MS = 60_000L
const val MIN_WAIT_TIME_BETWEEN_SYNC_MS = 60_000L
const val TIMEOUT_MS = 6_000L
const val MAX_NTP_RESPONSE_TIME_MS = 5_000L
}
private class DeviceClock : KronosClock {
override fun getCurrentTimeMs(): Long = System.currentTimeMillis()
override fun getElapsedTimeMs(): Long = SystemClock.elapsedRealtime()
override fun getCurrentNtpTimeMs(): Long? = null
override fun getCurrentTime(): KronosTime = KronosTime(System.currentTimeMillis(), null)
override fun sync(): Boolean = false
override fun syncInBackground() = Unit
override fun shutdown() = Unit
}
object AndroidClockFactory {
@JvmStatic
fun createDeviceClock(): Clock = DeviceClock()
@JvmStatic
@JvmOverloads
@Suppress("UNUSED_PARAMETER")
fun createKronosClock(
context: Context,
syncListener: SyncListener? = null,
ntpHosts: List<String> = DefaultParam.NTP_HOSTS,
requestTimeoutMs: Long = DefaultParam.TIMEOUT_MS,
minWaitTimeBetweenSyncMs: Long = DefaultParam.MIN_WAIT_TIME_BETWEEN_SYNC_MS,
cacheExpirationMs: Long = DefaultParam.CACHE_EXPIRATION_MS,
maxNtpResponseTimeMs: Long = DefaultParam.MAX_NTP_RESPONSE_TIME_MS
): KronosClock = DeviceClock()
}
В build.gradle.kts модуля приложения:
configurations.configureEach {
resolutionStrategy.dependencySubstitution {
substitute(module("com.lyft.kronos:kronos-android"))
.using(project(":kronos-device-clock"))
}
}
Проверить замену можно командой ./gradlew :app:dependencies --configuration releaseRuntimeClasspath: в выводе должна быть строка com.lyft.kronos:kronos-android:0.0.1-alpha11 -> project :kronos-device-clock, а kronos-java отсутствовать.
При обновлении dd-sdk-android сверяйте версию Kronos в зависимостях библиотеки: если она изменится, программный интерфейс модуля нужно сверить с новой версией.
Блокировка в сети — только для управляемых устройств. Если приложение работает только на корпоративных устройствах, обращение можно закрыть и без изменения сборки: запретить разрешение имён *.datadog.pool.ntp.org или исходящий UDP 123. Это возможно, когда организация управляет сетью устройств:
- устройства под управлением MDM (Android Enterprise) с постоянно включённым корпоративным VPN без трафика в обход туннеля либо с принудительным частным DNS-сервером (Private DNS), в котором эти имена закрыты;
- устройства выходят в сеть только через корпоративный Wi‑Fi или выделенную точку доступа (APN) оператора с вашими DNS и файрволом.
Для приложений, которые пользователи устанавливают на собственные устройства и используют в произвольных сетях (домашний Wi‑Fi, мобильный интернет), блокировка в сети неприменима: исключить обращение можно только подменой библиотеки.
С часами устройства время событий берётся с устройства; явный сдвиг часов (события «из будущего» или отставание больше суток) платформа версии 204 и новее корректирует при приёме данных.
Проверка подключения
На время проверки включите подробный журнал библиотеки до вызова
Datadog.initialize:Datadog.setVerbosity(Log.DEBUG)В Logcat (тег
Datadog) появятся строки об отправке пакетов данных и кодах ответа.Соберите и запустите приложение, пройдите по нескольким экранам, выполните сетевые запросы.
Подождите 1–2 минуты: данные передаются пачками, а не по каждому событию.
В интерфейсе платформы откройте раздел Мобильные приложения через меню. В списке появится приложение с названием, заданным в параметре
service.Перейдите на дашборд приложения. Заполняются просмотры экранов, сессии и пользователи, ошибки, сетевые запросы, разрезы по версиям приложения и операционным системам.
Перед выпуском приложения уберите Datadog.setVerbosity(Log.DEBUG).
Область применения инструкции
Инструкция описывает передачу данных RUM: просмотры экранов, действия пользователя, сетевые запросы, ошибки, длительные операции главного потока, время запуска приложения, а также пользовательские события и атрибуты.
В этой схеме подключения не передаются:
- падения приложения —
setCrashReportsEnabled(false), модульdd-sdk-android-ndkне подключается. Исключение — зависания (ANR), после которых система завершила приложение: на Android 11 и новее библиотека сообщает о них при следующем запуске как о падении независимо от этой настройки, и они поступают в платформу как ошибки RUM; - логи приложения — передача логов настраивается отдельно, обратитесь к команде Proto Observability за параметрами для вашего контура;
- запись сессий (Session Replay);
- ручные спаны модуля
dd-sdk-android-traceв платформе до версии 204.
Диагностика неполадок
Данные приходят, но в журнале библиотеки unexpected HTTP error (status code = 200); the batch was dropped.
Библиотека считает успешным только ответ 202, а платформа до версии 204 отвечает 200. Данные при этом доставлены и отображаются; из-за такого ответа библиотека отправляет накопленные пакеты реже, поэтому они появляются в платформе с задержкой. Платформа версии 204 и новее отвечает 202.
Данные не появляются, в журнале библиотеки ответ 200.
Проверьте, что в useCustomEndpoint указан полный адрес вместе с путём /api/v2/rum, без косой черты в конце и без параметров запроса. Адрес https://<адрес>/mobile/ (с косой чертой) принимается, но данные не обрабатываются.
В журнале библиотеки адреса browser-intake-datadoghq.com или аналогичные.
У одного из включённых модулей не задан useCustomEndpoint, и его данные уходят на серверы Datadog. Задайте адрес или отключите модуль.
Ответ 403, тело Module not licensed.
Лицензия платформы не включает модуль мониторинга цифрового опыта (DEM). Обратитесь к администратору платформы.
Ответ 402, тело License expired.
Срок действия лицензии платформы истёк. Обратитесь к администратору платформы.
Ответ 404 на адрес /api/v2/spans.
Приложение создаёт ручные спаны модуля трассировки, а платформа их не принимает: приём добавлен в версии 204. Модуль трассировки при этом нужен для заголовков сетевых запросов, отключать его не требуется; спаны сетевых запросов на этот адрес не отправляются. В версии 204 и новее ответ 403 на этот адрес означает, что лицензия не включает модуль APM.
Запросы не уходят, в журнале ошибки SSLHandshakeException, CLEARTEXT communication not enabled или Trust anchor for certification path not found.
Адрес приёма должен работать по HTTPS с сертификатом, которому доверяет Android: TLS 1.3 или TLS 1.2 с наборами шифров ECDHE и AES-GCM (другие наборы библиотека не использует). Для внутренних контуров с собственным удостоверяющим центром добавьте его сертификат в network_security_config приложения (элемент <trust-anchors>).
Сетевой запрос в событиях RUM есть, а в трейсе бэкенда его нет.
Речь о спане запроса приложения в трейсе бэкенда. Проверьте по порядку: вызван Trace.enable (без него заголовки не добавляются, в журнале — you did not enable the TracingFeature); домен запроса указан в DatadogInterceptor.Builder без схемы; на сервере установлен агент Proto Observability; версия платформы — 204 или новее.
Нажатия в данных есть, но без названия элемента.
Не задан InteractionPredicate, см. Отслеживание экранов и действий.
Один и тот же экран отображается дважды.
Одновременно работают две стратегии отслеживания экранов (например, Activity и Compose-навигация). Оставьте одну.
Приложение в списке называется как имя пакета.
Название берётся из параметра service в Configuration.Builder; если он не задан, используется имя пакета приложения.
Android-агент Proto Observability (устаревший способ)
Android-агент Proto Observability (POBP Android SDK, пакет ru.proto_group:pobp-sdk-android) — устаревший способ инструментации Android-приложений. Он по-прежнему поддерживается для приложений, которые уже его используют. Новые приложения подключайте через библиотеку dd-sdk-android, как описано выше; для действующих приложений рекомендуется переход на dd-sdk-android.
Подключение и инициализация агента
Для подключения агента используется SDK для автоматической инструментации (POBP Android SDK).
В файле build.gradle.kts добавьте репозиторий:
repositories {
mavenCentral()
maven {
url = uri("https://repo.proto.group/repository/maven-releases/")
credentials {
username = "customer-name" // получите реквизиты доступа от Proto
password = "your-password" // получите реквизиты доступа от Proto
}
}
}
В файле build.gradle в корне проекта добавьте репозиторий в разделе subprojects:
subprojects {
repositories {
google()
mavenCentral()
maven {
url = uri("https://repo.proto.group/repository/maven-releases/")
credentials {
username = "customer-name" // получите реквизиты доступа от Proto
password = "your-password" // получите реквизиты доступа от Proto
}
}
}
}
В файле build.gradle в папке проекта добавьте Android SDK в разделе dependencies
dependencies {
// POBP RUM SDK Android - обязательный модуль
implementation "ru.proto_group:pobp-sdk-android:1.15.4"
// Если используется Android Compose
implementation "ru.proto_group:pobp-sdk-android-compose:1.15.4"
// Если используется Glide
implementation "ru.proto_group:pobp-sdk-android-glide:1.15.4"
}
Инициализируйте SDK и сделайте необходимые import:
class SampleApplication : Application() {
override fun onCreate() {
super.onCreate()
// POBP RUM start
val pobpAppname = "My Android App" // название приложения для отображения в интерфейсе
// список доменов для трейсинга - к запросам на эти домены будут добавляться заголовки для трейсинга
val pobpTracedHosts = listOf(
"www.your-site.ru",
"api.your-site.ru"
)
// адрес для приема данных от RUM - данные должны проксироваться на https://proto-backend-server/mobile
val pobpEndpoint = "https://your-site.ru/mobile"
val configuration = Configuration.Builder(
rumEnabled = true, // обязательно true
tracesEnabled = true, // обязательно true
logsEnabled = false, // обязательно false
crashReportsEnabled = false // обязательно false
)
.sampleRumSessions(100F) // 100F - все пользователи, 10F - 10% пользовательских сессиий
.sampleTelemetry(0F) // обязательно указать 0F
.setFirstPartyHosts(pobpTracedHosts)
.trackLongTasks(10000) // обязательно
.useCustomRumEndpoint(pobpEndpoint)
.useCustomTracesEndpoint(pobpEndpoint)
.build()
val credentials = Credentials(
clientToken = "1", // обязательно 1
envName = "prod", // окружение, поддержка этого тега в интерфейсе появится позднее
variant = NO_VARIANT , // обязательно
rumApplicationId = pobpAppname, // обязательно
serviceName = pobpAppname // обязательно
)
Pobp.initialize(this, credentials, configuration, TrackingConsent.GRANTED) // обязательно
// добавление данных о текущем пользователе - опционально
Pobp.setUserInfo(
id = "1234", // строка, опционально
name = "Василий Пупкин", // строка, опционально
email = "vass@pupkine.com" // строка, опционально
)
val monitor = RumMonitor.Builder().build()
GlobalRum.registerIfAbsent(monitor)
// RUM end
}
}
Трейсинг запросов okhttp
Необходимо добавить interceptor, если у вас уже есть другие interceptor, то необходимо добавить PobpInterceptor самым первым.
val okHttpClient = OkHttpClient.Builder()
.addInterceptor(PobpInterceptor())
.eventListenerFactory(PobpEventListener.Factory())
.build()
Трейсинг запросов retrofit
Если вы используете Retrofit, позвольте ему использовать ваш OkHttpClient, и вы сможете получать информацию о сетевых запросах, сделанных с помощью Retrofit.
val retrofitClient = Retrofit.Builder()
.client(okHttpClient)
// …
.build()
Трейсинг запросов glide
Подключите модуль Pobp для Glide в файле build.gradle.
Следуя документации Glide, вам нужно создать свой собственный модуль GlideAppModule с интеграцией Pobp путем расширения модуля PobpGlideModule, как показано ниже. Это позволит автоматически отслеживать сетевые запросы Glide, а также собирать ошибки дискового кэша и преобразования изображений (создавая события RUM Error).
Трейсы сетевых запросов собираются с применение настраиваемого сэмплирования. По умолчанию применяется сэмплирование на уровне 20%.
// список доменов для трейсинга - к запросам на эти домены будут добавляться заголовки для трейсинга
val pobpTracedHosts = listOf(
"www.your-site.ru",
"api.your-site.ru"
)
@GlideModule
class CustomGlideModule :
PobpGlideModule(
pobpTracedHosts, traceSamplingRate = 20f
)
Сквозная трассировка запросов от Android-приложения
Трейсы транзакции от Android к инструментированным бэкендам, то есть бэкендам, на которых установлены агенты Proto OBP, будут объединяться в единую цепочку вызовов - трейс распределенной транзакции, начавшейся на Android устройстве.

Переход на dd-sdk-android
Данные Android-агента Proto Observability и библиотеки dd-sdk-android 3.x хранятся в одних и тех же таблицах и отображаются в одном разделе. Если задать service таким же, как serviceName в прежней конфигурации, история приложения продолжится без разрыва.
| Android-агент Proto Observability | dd-sdk-android 3.x |
|---|---|
ru.proto_group:pobp-sdk-android:1.15.4 | com.datadoghq:dd-sdk-android-rum:3.14.1 + com.datadoghq:dd-sdk-android-okhttp:3.14.1 |
Pobp.initialize(context, credentials, configuration, consent) | Datadog.initialize(context, configuration, consent) + Rum.enable(…) + Trace.enable(…) |
Credentials(clientToken, envName, variant, rumApplicationId, serviceName) | Configuration.Builder(clientToken, env, variant, service), RumConfiguration.Builder(applicationId) |
Configuration.Builder(rumEnabled, tracesEnabled, logsEnabled, crashReportsEnabled) | Модули включаются вызовами Rum.enable, Trace.enable; падения — setCrashReportsEnabled(false) |
.useCustomRumEndpoint("https://<адрес>/mobile") | RumConfiguration.Builder.useCustomEndpoint("https://<адрес>/api/v2/rum") — полный путь |
.useCustomTracesEndpoint("https://<адрес>/mobile") | TraceConfiguration.Builder.useCustomEndpoint("https://<адрес>/api/v2/spans") |
.sampleRumSessions(100F) | RumConfiguration.Builder.setSessionSampleRate(100f) |
.sampleTelemetry(0F) | RumConfiguration.Builder.setTelemetrySampleRate(0f) |
.trackLongTasks(10000) | RumConfiguration.Builder.trackLongTasks(…); по умолчанию включено, порог 100 мс |
.setFirstPartyHosts(hosts) | DatadogInterceptor.Builder(mapOf(host to setOf(TracingHeaderType.TRACECONTEXT))) |
Pobp.setUserInfo(id, name, email) | Datadog.setUserInfo(id, name, email, extraInfo) |
GlobalRum.registerIfAbsent(RumMonitor.Builder().build()) | не нужно; монитор доступен как GlobalRumMonitor.get() |
PobpInterceptor(), PobpEventListener.Factory() | DatadogInterceptor.Builder(…).build(), DatadogEventListener.Factory() |
Заголовки x-protoobp-* | traceparent (W3C) — агенты бэкенда принимают без настройки |