Kubesense

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).

FieldValue
Min SDK23 (Android 6.0) — auto-instrumented integrations require minSdk 29
Target / Compile SDK36
LanguageJava 11 source/target compatibility, Kotlin
DistributionMaven 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:

SetterValues
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 client

OpenFeature 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:

FlagsClientOpenFeature Provider
API styleKubesense-native, instance-based contextVendor-neutral, global context
Type systemKotlin-native typesOpenFeature Value types
Structured flagsReturns JSONObjectReturns Value.Structure
Best forFull Kubesense integrationVendor-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()
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()

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)
ValueBehaviour
GRANTEDData is collected and uploaded
NOT_GRANTEDData is not collected; existing data is discarded
PENDINGData is collected locally but not uploaded until consent is granted

Best Practices

  • Initialize early — call Kubesense.initialize() and all *.enable() calls in Application.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 stringsEvaluationContext.attributes is a Map<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 match setFirstPartyHosts(...) on the core Configuration.
  • 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.PENDING until the user consents — this buffers events locally and only uploads them after GRANTED is set, ensuring GDPR compliance.