Kubesense

Android SDK Integration

This guide covers every feature and integration available in the Kubesense Android SDK (v2.1.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.1.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
        )
            // Single intake host for every feature. Host only — no scheme, no path.
            .useKubesenseRumEndpoint("<your-collector-host>")
            .setFirstPartyHosts(listOf("api.example.com"))
            .build()

        Kubesense.initialize(this, configuration, TrackingConsent.GRANTED)
    }
}

Register the Application class in AndroidManifest.xml:

<application
    android:name=".MyApplication"
    ...>
</application>

warning: Changed in 2.1.0 — one endpoint for every feature. The per-feature useCustomEndpoint(...) builders have been removed. Point the SDK at your collector once, on the core Configuration.Builder, with useKubesenseRumEndpoint(...); RUM, Logs, Traces, Session Replay, Profiling, and Flags all upload through it.

The value is a host, with an optional port — intake.example.com or intake.example.com:8443. Any scheme or path you include is stripped: uploads always go over HTTPS to https://<host>/rum/api/v1/.... This is the opposite of the old per-feature setters, which took a full URL and appended nothing to it. Upgrading from 2.0.0? See Migrating from 2.0.0.

Omit useKubesenseRumEndpoint(...) to upload to the site selected with useSite(...) — KubesenseSite.PROD (us2.kubesense.ai) by default.


Remote Configuration

info: New in 2.1.0, and enabled by default. Applications write no remote-configuration code — Kubesense.initialize() does all of it.

Sampling rates, feature switches, and privacy masking can be managed centrally and delivered to the SDK, so you can retune or switch off a feature fleet-wide without shipping an app release.

How it works

Launch N                                  Launch N+1
────────────────────────────────          ────────────────────────────
1. read cached config (local, ~1ms)       1. read cached config  ← new values
2. init SDK with cached-or-default        2. init SDK with them
3. app runs with the current config
4. async fetch of the config document
5. cache the response for next launch
  1. Kubesense.initialize() reads the document cached by the previous launch — a small local file read, never a network call — and applies its core settings before the upload pipeline starts.
  2. Each X.enable() skips the feature if it is switched off remotely, then applies its own settings over what your code passed.
  3. A fresh document is fetched on a background thread using an un-instrumented HTTP client, so the config request never shows up as a RUM resource or span.
  4. The fetch repeats on an interval, so long-running kiosk-style apps still converge.

warning: SDK configuration is frozen at enable() time by design, so changes apply at the next launch, not mid-session.

Opting out or retuning

Configuration.Builder(clientToken, env, variant)
    .setRemoteConfigurationEnabled(false)                  // stay on in-code settings only
    .setRemoteConfigurationRefreshPeriodMs(6 * 3600_000L)  // default; <= 0 fetches once
    .build()

Values below one minute are raised to one minute. A value of zero or less fetches once after initialization and never again.

Defaults always win

A broken or missing configuration can never take your app away from safe SDK defaults:

  • Absent values change nothing. The document is an override layer, not a second source of defaults — a missing key leaves the setting exactly as your code built it.
  • Bad values are dropped individually. A value resolves to "absent" when it has the wrong JSON type, names an unknown enum constant, or is out of range (sample rates outside 0..100, sizes and counts <= 0). A completely malformed payload falls back to pure defaults, and unknown keys are ignored, so the format is forward-compatible.
  • Network problems are non-events. An HTTP failure, timeout, or unreadable body leaves the existing cache untouched. Responses are parse-validated before overwriting the cache, and the cache is written via a temp file and rename.
  • First launch and offline launches run on pure SDK defaults.

What can be controlled

SectionSettings
featuresSwitch rum, logs, trace, sessionReplay, or profiling off entirely — enable() returns without registering the feature, so your code needs no branch of its own. Only an explicit false disables.
rumsessionSampleRate, telemetrySampleRate, trackUserInteractions, trackFrustrations, trackNonFatalAnrs, trackBackgroundEvents, trackAnonymousUser, collectAccessibility, vitalsUpdateFrequency, longTaskThresholdMs
sessionReplaysampleRate, startRecordingImmediately, imagePrivacy, touchPrivacy, textAndInputPrivacy, dynamicOptimizationEnabled, minRAMSizeMb, minCPUCoreNumber
logsnetworkInfoEnabled, logcatLogsEnabled, bundleWithRumEnabled, bundleWithTraceEnabled — applied in Logger.Builder.build(), so every logger picks them up
tracenetworkInfoEnabled
flagstrackExposures, rumIntegrationEnabled
profilinglaunchSampleRate
corebatchSize, uploadFrequency

Session Replay privacy values are applied after any app-local privacy logic, so masking can always be tightened centrally.


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

Enable RUM

Rum.enable(
    RumConfiguration.Builder(applicationId = "<RUM_APPLICATION_ID>")
        // The intake host is set once on Configuration.Builder — see Core Setup
        .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.1.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.1.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.1.0")

// Optional: Material Design component support
implementation("ai.kubesense:kubesense-android-session-replay-material:2.1.0")

// Optional: Jetpack Compose support
implementation("ai.kubesense:kubesense-android-session-replay-compose:2.1.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.1.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.1.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.1.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.1.0")

// Recommended: enables flag evaluations to appear in RUM views
implementation("ai.kubesense:kubesense-android-rum:2.1.0")

Enable Flags

val flagsConfig = FlagsConfiguration.Builder()
    // .rumIntegrationEnabled(false)     // disable RUM correlation (default: enabled)
    // .trackExposures(false)            // disable exposure tracking (default: enabled)
    .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.1.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.1.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.1.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.1.0")
implementation("ai.kubesense:kubesense-android-compose:2.1.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.1.0")

// Coil 3
implementation("ai.kubesense:kubesense-android-coil3:2.1.0")

Also requires OkHttp integration (for network tracking):

implementation("ai.kubesense:kubesense-android-okhttp:2.1.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.1.0")
implementation("ai.kubesense:kubesense-android-trace:2.1.0")
implementation("ai.kubesense:kubesense-android-glide:2.1.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.1.0")
implementation("ai.kubesense:kubesense-android-okhttp:2.1.0")
implementation("ai.kubesense:kubesense-android-fresco:2.1.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.1.0")
implementation("ai.kubesense:kubesense-android-rum:2.1.0")
implementation("ai.kubesense:kubesense-android-apollo:2.1.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.1.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.1.0")
implementation("ai.kubesense:kubesense-android-rum-coroutines:2.1.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.1.0")
implementation("ai.kubesense:kubesense-android-trace-coroutines:2.1.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.1.0")
implementation("ai.kubesense:kubesense-android-rx:2.1.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.1.0")
implementation("ai.kubesense:kubesense-android-trace:2.1.0")
implementation("ai.kubesense:kubesense-android-sqldelight:2.1.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()

Identifying Users

Attach a user identity to every event so sessions, errors, and replays can be attributed to a person:

Kubesense.setUserInfo(
    id = "d0b4c2f1",
    name = "Jane Doe",
    email = "jane@example.com",
    extraInfo = mapOf("plan" to "enterprise")
)

// Add properties later without clearing the identity
Kubesense.addUserProperties(mapOf("plan" to "trial"))

// On sign-out
Kubesense.clearUserInfo()

A mobile number can be attached as of 2.1.0. It is the last parameter, after sdkCore, so pass it by name:

Kubesense.setUserInfo(id = "d0b4c2f1", name = "Jane Doe", mobile = "+15551234567")

note: mobile trails sdkCore deliberately. @JvmOverloads builds its overload set by dropping trailing parameters, so appending the parameter there keeps every pre-existing overload and positional call site compiling unchanged. The value travels in the user extras and arrives as a sibling of id, name, and email; an explicit mobile argument wins over a stale "mobile" key in extraInfo.


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

Migrating from 2.0.0

Removed: per-feature endpoint setters

useCustomEndpoint(...) is gone from every feature configuration builder, along with the two Flags-specific setters. Replace all of them with a single call on the core Configuration.Builder.

Removed in 2.1.0Replacement
RumConfiguration.Builder.useCustomEndpoint(url)Configuration.Builder.useKubesenseRumEndpoint(host)
LogsConfiguration.Builder.useCustomEndpoint(url)same
TraceConfiguration.Builder.useCustomEndpoint(url)same
SessionReplayConfiguration.Builder.useCustomEndpoint(url)same
ProfilingConfiguration.Builder.useCustomEndpoint(url)same
FlagsConfiguration.Builder.useCustomFlagEndpoint(url)same
FlagsConfiguration.Builder.useCustomExposureEndpoint(url)same
// 2.0.0 — a full intake URL per feature
Rum.enable(
    RumConfiguration.Builder(appId)
        .useCustomEndpoint("https://intake.example.com/rum/api/v1")
        .build()
)
Trace.enable(
    TraceConfiguration.Builder()
        .useCustomEndpoint("https://intake.example.com/rum/api/v1")
        .build()
)

// 2.1.0 — one host on the core configuration, no scheme and no path
val configuration = Configuration.Builder(clientToken, env, variant)
    .useKubesenseRumEndpoint("intake.example.com")
    .build()

Kubesense.initialize(this, configuration, TrackingConsent.GRANTED)
Rum.enable(RumConfiguration.Builder(appId).build())
Trace.enable(TraceConfiguration.Builder().build())

warning: Pass a host, not a URL. useKubesenseRumEndpoint strips any scheme and path you include and always uploads over HTTPS to https://<host>/rum/api/v1/.... Carrying an old full-URL value straight across still works — the path is discarded — but write it as a bare host to keep the intent clear.

Added: remote configuration, on by default

Existing apps pick it up with no code change. If you need the SDK to honour only what your code sets, opt out explicitly with .setRemoteConfigurationEnabled(false) — see Remote Configuration.

Added: mobile on setUserInfo

Purely additive. Every existing call site keeps compiling — see Identifying Users.

Fixed

  • Session Replay no longer drops views with non-color backgrounds.
  • Compose Session Replay no longer draws controls over container content.
  • Session Replay re-offers resources when the RUM session changes, so replays that span a session boundary keep their assets.

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 strings — EvaluationContext.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.