Android SDK Integration
This guide covers every feature and integration available in the Kubesense Android SDK (v2.0.0): adding dependencies, initializing the core SDK, and enabling RUM, distributed tracing, session replay, NDK crash reporting, profiling, WebView tracking, and feature flags — plus the supported third-party integrations (OkHttp, Jetpack Compose, image loaders, Apollo, Cronet, coroutines, RxJava, SQLDelight, and more).
| Field | Value |
|---|---|
| Min SDK | 23 (Android 6.0) — auto-instrumented integrations require minSdk 29 |
| Target / Compile SDK | 36 |
| Language | Java 11 source/target compatibility, Kotlin |
| Distribution | Maven Central, group ai.kubesense |
Core Setup
1. Configure repositories
Ensure Maven Central and Google's Maven repository are available in your project-level settings.gradle.kts:
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
}
}note: The SDK is published to Maven Central under the ai.kubesense group. No custom Maven repository is required.
2. Add the core dependency
// module-level build.gradle.kts
dependencies {
implementation("ai.kubesense:kubesense-android-core:2.0.0")
}3. Initialize the SDK
Call Kubesense.initialize() in Application.onCreate() before enabling any feature:
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
val configuration = Configuration.Builder(
clientToken = "<CLIENT_TOKEN>",
env = "production",
variant = BuildConfig.VERSION_NAME
)
.setFirstPartyHosts(listOf("api.example.com"))
.build()
Kubesense.initialize(this, configuration, TrackingConsent.GRANTED)
}
}Register the Application class in AndroidManifest.xml:
<application
android:name=".MyApplication"
...>
</application>info: Configure your intake endpoint per feature. Point each feature at your Kubesense / Kubecol collector with useCustomEndpoint(...) on that feature's configuration builder — RumConfiguration.Builder, TraceConfiguration.Builder, SessionReplayConfiguration.Builder, etc. Each value must be the full intake URL for that feature (the SDK does not append a path). See each feature section below.
Features
Real User Monitoring (RUM)
Track user sessions, screen views, interactions, errors, network resources, and long tasks.
Dependency
implementation("ai.kubesense:kubesense-android-rum:2.0.0")Enable RUM
Rum.enable(
RumConfiguration.Builder(applicationId = "<RUM_APPLICATION_ID>")
// Full intake URL of your Kubesense / Kubecol collector for RUM
.useCustomEndpoint("https://<your-collector-host>/rum/api/v1")
.trackUserInteractions()
.trackLongTasks(longTaskThresholdMs = 100L)
.useViewTrackingStrategy(ActivityViewTrackingStrategy(trackExtras = true))
.build()
)Manual view tracking
// Start a view
GlobalRumMonitor.get().startView(key = this, name = "HomeScreen")
// Stop a view
GlobalRumMonitor.get().stopView(key = this)Manual action tracking
GlobalRumMonitor.get().addAction(
type = RumActionType.TAP,
name = "Login Button",
attributes = emptyMap()
)Manual error reporting
GlobalRumMonitor.get().addError(
message = "Something went wrong",
source = RumErrorSource.SOURCE,
throwable = exception,
attributes = mapOf("order_id" to "12345")
)Custom global attributes
GlobalRumMonitor.get().addAttribute("user_tier", "premium")
GlobalRumMonitor.get().removeAttribute("user_tier")note: RUM exposes a much larger configuration surface — view-tracking strategies (Activity / Fragment / Navigation), user-interaction and action tracking, long tasks and ANRs, vitals and frame metrics, session sampling, event mappers, and feature-flag evaluations. Configure these on RumConfiguration.Builder before calling Rum.enable(...).
Distributed Tracing
Instrument operations to measure latency end-to-end and correlate traces with RUM sessions.
Dependency
implementation("ai.kubesense:kubesense-android-trace:2.0.0")Enable Trace and register a tracer
Trace.enable(TraceConfiguration.Builder().build())
// Register a global tracer once, after Trace.enable(...)
GlobalKubesenseTracer.registerIfAbsent(
KubesenseTracing.newTracerBuilder()
.build()
)Create and use spans
val tracer = GlobalKubesenseTracer.get()
val span = tracer.buildSpan("process-payment").start()
try {
// your operation
} finally {
span.finish()
}Or use the withinSpan helper, which starts and finishes a span around the block automatically:
withinSpan("process-payment") {
// your operation — `this` is the KubesenseSpan
setTag("order_id", orderId)
}OpenTelemetry support
implementation("ai.kubesense:kubesense-android-trace-otel:2.0.0")Register a Kubesense-backed OpenTelemetry instance and use the standard OpenTelemetry API:
GlobalOpenTelemetry.set(
KubesenseOpenTelemetry("<service-name>")
)See the OkHttp OpenTelemetry section for propagating trace context over HTTP.
Session Replay
Capture a visual replay of user sessions so you can reproduce exactly what a user experienced.
Dependencies
implementation("ai.kubesense:kubesense-android-session-replay:2.0.0")
// Optional: Material Design component support
implementation("ai.kubesense:kubesense-android-session-replay-material:2.0.0")
// Optional: Jetpack Compose support
implementation("ai.kubesense:kubesense-android-session-replay-compose:2.0.0")note: Session Replay requires RUM to be enabled first.
Enable Session Replay
SessionReplay.enable(
SessionReplayConfiguration.Builder(sampleRate = 100f)
.setImagePrivacy(ImagePrivacy.MASK_LARGE_ONLY)
.setTextAndInputPrivacy(TextAndInputPrivacy.MASK_SENSITIVE_INPUTS)
.setTouchPrivacy(TouchPrivacy.HIDE)
.addExtensionSupport(MaterialExtensionSupport()) // if using Material
.addExtensionSupport(ComposeExtensionSupport()) // if using Compose
.build()
)Privacy levels
Privacy is configured independently for images, text/inputs, and touches:
| Setter | Values |
|---|---|
setImagePrivacy(...) | MASK_NONE, MASK_LARGE_ONLY, MASK_ALL |
setTextAndInputPrivacy(...) | MASK_SENSITIVE_INPUTS, MASK_ALL_INPUTS, MASK_ALL |
setTouchPrivacy(...) | SHOW, HIDE |
warning: The older single-axis setPrivacy(SessionReplayPrivacy.…) API (ALLOW / MASK / MASK_USER_INPUT) is deprecated in favour of the three granular setters above.
You can also override privacy per view at runtime with View.setSessionReplayHidden(...), View.setSessionReplayImagePrivacy(...), View.setSessionReplayTextAndInputPrivacy(...), and View.setSessionReplayTouchPrivacy(...) (or the Modifier.sessionReplay* variants in Compose).
NDK Crash Reporting
Capture crashes that originate from native C/C++ code via NDK.
Dependency
implementation("ai.kubesense:kubesense-android-ndk:2.0.0")Enable NDK crash reporting
Call NdkCrashReports.enable() after Kubesense.initialize(...):
Kubesense.initialize(this, configuration, TrackingConsent.GRANTED)
NdkCrashReports.enable()Profiling
Record CPU and memory profiles to identify performance bottlenecks at runtime.
Dependency
implementation("ai.kubesense:kubesense-android-profiling:2.0.0")Enable Profiling
Profiling is an experimental feature and requires Android 15 (API 35) or later. Call Profiling.enable() after the SDK is initialized:
@OptIn(ExperimentalProfilingApi::class)
fun initProfiling() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
Profiling.enable(ProfilingConfiguration.DEFAULT)
}
}note: On supported devices, once enabled the SDK also samples an application-launch profile on subsequent launches (via the bundled KubesenseProfilingContentProvider).
WebView Tracking
Bridge RUM events captured inside a WebView into the native mobile session, giving you a unified view of web and native interactions.
Dependency
implementation("ai.kubesense:kubesense-android-webview:2.0.0")Setup
webView.settings.javaScriptEnabled = true
WebViewTracking.enable(
webView = webView,
allowedHosts = listOf("example.com")
)The web page must also be instrumented with the Kubesense Browser SDK, and the hosts you pass to allowedHosts must match the URLs the page loads.
Feature Flags
Evaluate feature flags and experiments, and automatically correlate flag evaluations with RUM session data.
Dependency
implementation("ai.kubesense:kubesense-android-flags:2.0.0")
// Recommended: enables flag evaluations to appear in RUM views
implementation("ai.kubesense:kubesense-android-rum:2.0.0")Enable Flags
val flagsConfig = FlagsConfiguration.Builder()
// .rumIntegrationEnabled(false) // disable RUM correlation (default: enabled)
// .trackExposures(false) // disable exposure tracking (default: enabled)
// .useCustomFlagEndpoint("https://your-proxy.example.com/flags")
.build()
Flags.enable(flagsConfig)Evaluate flags
val client = FlagsClient.Builder().build()
// Set the targeting context (call this after user authentication)
client.setEvaluationContext(
EvaluationContext(
targetingKey = userId, // use a persistent UUID for anonymous users
attributes = mapOf(
"plan" to "premium",
"email" to "user@example.com"
)
)
)
// Evaluate flags
val isEnabled: Boolean = client.resolveBooleanValue("new-checkout-flow", false)
val theme: String = client.resolveStringValue("app-theme", "light")
val maxRetries: Int = client.resolveIntValue("max-retry-count", 3)
val discount: Double = client.resolveDoubleValue("discount-rate", 0.0)
// Structured flags
val defaultConfig = JSONObject("""{"timeout": 30, "retries": 3}""")
val config = client.resolveStructureValue("api-config", defaultConfig)Detailed resolution (with error info)
val result = client.resolve("feature-enabled", false)
if (result.errorCode != null) {
Log.e("Flags", "Resolution failed: ${result.errorMessage}")
} else {
val value = result.value
val variant = result.variant // e.g. "control" or "treatment"
val reason = result.reason // e.g. ResolutionReason.TARGETING_MATCH
}Retrieve an existing client
val client = FlagsClient.get() // default client
val analyticsClient = FlagsClient.get("analytics") // named clientOpenFeature provider
Use the standard OpenFeature API instead of the native client:
implementation("ai.kubesense:kubesense-android-flags-openfeature:2.0.0")import ai.kubesense.android.flags.FlagsClient
import ai.kubesense.android.flags.openfeature.asOpenFeatureProvider
import dev.openfeature.kotlin.sdk.OpenFeatureAPI
import dev.openfeature.kotlin.sdk.ImmutableContext
import dev.openfeature.kotlin.sdk.Value
// Register the provider
val provider = FlagsClient.Builder().build().asOpenFeatureProvider()
OpenFeatureAPI.setProviderAndWait(provider)
// Set evaluation context
OpenFeatureAPI.setEvaluationContext(
ImmutableContext(
targetingKey = userId,
attributes = mapOf("plan" to Value.String("premium"))
)
)
// Evaluate a flag
val client = OpenFeatureAPI.getClient()
val isEnabled = client.getBooleanValue("new-checkout-flow", false)When to use OpenFeature vs. FlagsClient directly:
| FlagsClient | OpenFeature Provider | |
|---|---|---|
| API style | Kubesense-native, instance-based context | Vendor-neutral, global context |
| Type system | Kotlin-native types | OpenFeature Value types |
| Structured flags | Returns JSONObject | Returns Value.Structure |
| Best for | Full Kubesense integration | Vendor-neutral or multi-provider setups |
Integrations
OkHttp
Automatically track all HTTP requests made through OkHttp as RUM resources and APM trace spans.
Dependency
implementation("ai.kubesense:kubesense-android-okhttp:2.0.0")Setup
val okHttpClient = OkHttpClient.Builder()
.addInterceptor(
KubesenseInterceptor.Builder(
tracedHosts = listOf("api.example.com", "cdn.example.com")
).build()
)
.eventListenerFactory(KubesenseEventListener.Factory())
.build()info: Pass your backend hostnames in tracedHosts to inject distributed tracing headers. Requests to other hosts are tracked as RUM resources without trace headers.The eventListenerFactory(...) supplies detailed resource timing (DNS, connect, TTFB, download). If you also need to trace requests that are the result of redirects, add the interceptor as a network interceptor too: .addNetworkInterceptor(TracingInterceptor.Builder(tracedHosts).build()).
OpenTelemetry propagation
implementation("ai.kubesense:kubesense-android-okhttp-otel:2.0.0")Attach an OpenTelemetry parent span to an outgoing OkHttp request with the addParentSpan extension:
val request = Request.Builder()
.url("https://api.example.com/items")
.addParentSpan(parentSpan) // io.opentelemetry.api.trace.Span
.build()Jetpack Compose
Track user interactions and screen views in Compose-based UIs.
Dependency
implementation("ai.kubesense:kubesense-android-rum:2.0.0")
implementation("ai.kubesense:kubesense-android-compose:2.0.0")Automatic tap tracking
Enable Compose tap tracking on the RUM configuration:
RumConfiguration.Builder(applicationId)
.trackUserInteractions()
.enableComposeActionTracking()
.build()Navigation view tracking (Navigation for Compose)
val navController = rememberNavController().apply {
NavigationViewTrackingEffect(
navController = this,
trackArguments = true,
destinationPredicate = AcceptAllNavDestinations()
)
}If your app mixes Compose and Fragment navigation, exclude Compose host activities from ActivityViewTrackingStrategy:
RumConfiguration.Builder(applicationId)
.useViewTrackingStrategy(
ActivityViewTrackingStrategy(
trackExtras = true,
componentPredicate = object : ComponentPredicate<Activity> {
override fun accept(component: Activity) = component !is MyComposeActivity
override fun getViewName(component: Activity): String? = null
}
)
)
.build()Click tracking
Button(
onClick = trackClick(targetName = "Confirm Order") {
// your click logic
}
) {
Text("Confirm")
}Swipe / scroll tracking
val swipeableState = rememberSwipeableState(...)
val interactionSource = remember { MutableInteractionSource() }.apply {
TrackInteractionEffect(
targetName = "Product Card",
interactionSource = this,
interactionType = InteractionType.Swipe(swipeableState, Orientation.Horizontal),
attributes = mapOf("card_id" to productId)
)
}Coil
Track image loading requests made by Coil as RUM resources and APM traces.
Dependency
// Coil 2
implementation("ai.kubesense:kubesense-android-coil:2.0.0")
// Coil 3
implementation("ai.kubesense:kubesense-android-coil3:2.0.0")Also requires OkHttp integration (for network tracking):
implementation("ai.kubesense:kubesense-android-okhttp:2.0.0")Coil 2 setup
val imageLoader = ImageLoader.Builder(context)
.okHttpClient(okHttpClient) // okHttpClient configured with KubesenseInterceptor
.build()
Coil.setImageLoader(imageLoader)
// Per-request: listen for cache errors
imageView.load(imageUri) {
listener(KubesenseCoilRequestListener())
}Coil 3 setup
val imageLoader = ImageLoader.Builder(context)
.components {
add(OkHttpNetworkFetcherFactory(okHttpClient))
}
.build()
SingletonImageLoader.setSafe { imageLoader }
// Per-request: listen for loading failures
imageView.load(imageUri) {
listener(KubesenseCoilRequestListener())
}The KubesenseInterceptor on OkHttpClient creates RUM Resource and APM Trace events for network calls. KubesenseCoilRequestListener creates RUM Error events for disk cache and decoding failures.
Glide
Track image loading requests made by Glide.
Dependency
implementation("ai.kubesense:kubesense-android-rum:2.0.0")
implementation("ai.kubesense:kubesense-android-trace:2.0.0")
implementation("ai.kubesense:kubesense-android-glide:2.0.0")Setup
Extend KubesenseGlideModule and annotate with @GlideModule:
@GlideModule
class CustomGlideModule : KubesenseGlideModule(
firstPartyHosts = listOf("cdn.example.com"),
sampleRate = 20f // sample 20% of requests (default)
)This automatically tracks Glide's network requests (RUM Resource + APM Trace events) and disk cache / transformation errors (RUM Error events).
Fresco
Track image loading requests made by Fresco.
Dependency
implementation("ai.kubesense:kubesense-android-rum:2.0.0")
implementation("ai.kubesense:kubesense-android-okhttp:2.0.0")
implementation("ai.kubesense:kubesense-android-fresco:2.0.0")Setup
val config = OkHttpImagePipelineConfigFactory
.newBuilder(context, okHttpClient) // okHttpClient with KubesenseInterceptor
.setMainDiskCacheConfig(
DiskCacheConfig.newBuilder(context)
.setCacheEventListener(KubesenseFrescoCacheListener())
.build()
)
.build()
Fresco.initialize(context, config)Apollo (GraphQL)
Track Apollo Kotlin GraphQL queries and mutations.
note: Supports Apollo version 4+ only. Subscription operations are not tracked.
Dependency
implementation("ai.kubesense:kubesense-android-okhttp:2.0.0")
implementation("ai.kubesense:kubesense-android-rum:2.0.0")
implementation("ai.kubesense:kubesense-android-apollo:2.0.0")Setup
val apolloClient = ApolloClient.Builder()
.serverUrl("https://graphql.example.com/graphql")
.addInterceptor(KubesenseApolloInterceptor())
.okHttpClient(okHttpClient) // okHttpClient with KubesenseInterceptor
.build()Send GraphQL payloads (optional)
GraphQL payload sending is disabled by default. Enable it explicitly:
.addInterceptor(KubesenseApolloInterceptor(sendGraphQLPayloads = true))Cronet
Track network requests made through Cronet as RUM resources.
Dependency
implementation("ai.kubesense:kubesense-android-cronet:2.0.0")
implementation("com.google.android.gms:play-services-cronet:<version>")Setup
Replace CronetEngine.Builder with KubesenseCronetEngine.Builder:
@OptIn(ExperimentalRumApi::class)
val cronetEngine: CronetEngine = KubesenseCronetEngine.Builder(application)
// your Cronet configuration
.build()Kotlin Coroutines
Propagate RUM context and trace spans across coroutine boundaries.
RUM Coroutines
implementation("ai.kubesense:kubesense-android-rum:2.0.0")
implementation("ai.kubesense:kubesense-android-rum-coroutines:2.0.0")Report Flow errors to the RUM dashboard automatically:
suspend fun loadData() {
flow { emit(fetchFromNetwork()) }
.sendErrorToKubesense()
.collect { data -> render(data) }
}Trace Coroutines
implementation("ai.kubesense:kubesense-android-trace:2.0.0")
implementation("ai.kubesense:kubesense-android-trace-coroutines:2.0.0")Wrap coroutine blocks in a trace span:
fun doWork() {
GlobalScope.launchTraced("fetch-user", Dispatchers.IO) {
// creates and finishes a span around this block
}
runBlockingTraced("sync-data", Dispatchers.IO) {
// …
}
}
suspend fun doSuspendWork() {
withContextTraced("process-items", Dispatchers.Default) {
// …
}
val deferred = asyncTraced("fetch-config", Dispatchers.IO) { fetchConfig() }
val result = deferred.awaitTraced("fetch-config-await")
}RxJava
Propagate errors from RxJava streams to the RUM dashboard.
Dependency
implementation("ai.kubesense:kubesense-android-rum:2.0.0")
implementation("ai.kubesense:kubesense-android-rx:2.0.0")Kotlin extension
Observable.create<MyData> { emitter -> /* ... */ }
.sendErrorToKubesense()
.subscribe { data -> /* ... */ }Java API
Observable.create(emitter -> { /* ... */ })
.doOnError(new KubesenseRumErrorConsumer())
.subscribe(data -> { /* ... */ });Works with Observable, Flowable, Single, Maybe, and Completable.
SQLDelight
Detect database corruption and trace SQLDelight transactions.
Dependency
implementation("ai.kubesense:kubesense-android-rum:2.0.0")
implementation("ai.kubesense:kubesense-android-trace:2.0.0")
implementation("ai.kubesense:kubesense-android-sqldelight:2.0.0")Setup
Pass KubesenseSqliteCallback to AndroidSqliteDriver:
val database = AppDatabase(
AndroidSqliteDriver(
schema = AppDatabase.Schema,
context = context,
callback = KubesenseSqliteCallback(AppDatabase.Schema)
)
)Database corruption is detected automatically and a RUM Error event is created.
Traced transactions
// No return value
database.queries.transactionTraced("<SPAN_NAME>") {
// your queries
}
// With return value
val result = database.queries.transactionTracedWithResult("<SPAN_NAME>") {
// your queries
queryResult()
}Picasso
Track network requests made by Picasso by passing the instrumented OkHttpClient as its downloader:
// okHttpClient must be configured with KubesenseInterceptor (see OkHttp section)
val picasso = Picasso.Builder(context)
.downloader(OkHttp3Downloader(okHttpClient))
.build()
Picasso.setSingletonInstance(picasso)Retrofit
Track network requests made by Retrofit by passing the instrumented OkHttpClient:
// okHttpClient must be configured with KubesenseInterceptor (see OkHttp section)
val retrofit = Retrofit.Builder()
.client(okHttpClient)
.baseUrl("https://api.example.com/")
.addConverterFactory(GsonConverterFactory.create())
.build()Tracking Consent
The SDK respects user privacy via TrackingConsent. Pass the current consent state at initialization and update it any time the user changes their preference:
// At initialization
Kubesense.initialize(context, configuration, TrackingConsent.PENDING)
// After the user accepts
Kubesense.setTrackingConsent(TrackingConsent.GRANTED)
// After the user declines
Kubesense.setTrackingConsent(TrackingConsent.NOT_GRANTED)| Value | Behaviour |
|---|---|
GRANTED | Data is collected and uploaded |
NOT_GRANTED | Data is not collected; existing data is discarded |
PENDING | Data is collected locally but not uploaded until consent is granted |
Best Practices
- Initialize early — call
Kubesense.initialize()and all*.enable()calls inApplication.onCreate()to capture events from the very first screen. - Enable RUM before Session Replay and Flags — both features correlate with RUM, so RUM must be enabled first.
- Use persistent targeting keys — for anonymous users, generate a UUID once and store it in
SharedPreferences. Transition to the real user ID after authentication. - All flag attributes must be strings —
EvaluationContext.attributesis aMap<String, String>; convert numbers and booleans before passing them. - Provide flag defaults — always supply a sensible default value so your UI degrades gracefully when the network is unavailable.
- Pass your server hostnames to
KubesenseInterceptor— only requests to listed hosts receive distributed tracing headers, and those hosts should matchsetFirstPartyHosts(...)on the coreConfiguration. - Apply the granular Session Replay privacy setters — mask sensitive inputs and images by default (
TextAndInputPrivacy.MASK_SENSITIVE_INPUTS,ImagePrivacy.MASK_LARGE_ONLY) and relax only where needed. - Set
TrackingConsent.PENDINGuntil the user consents — this buffers events locally and only uploads them afterGRANTEDis set, ensuring GDPR compliance.