Mobile RUM - Android SDK
The Mobile RUM SDK provides a customizable suite of tools to analyze and optimize the performance of Android applications. Isolate ANR and network changes, quickly detect application crashes, identify slow or frozen frames, and more. To see an example of how to deploy the Mobile RUM Android SDK, navigate to our GitHub repository.
Prerequisites#
- Android SDK version: API 21+ (Android 5.0+). Check with:
1./sdkmanager --list compileSdk35 or newer and Kotlin (KGP) 2.0 or newer in the consuming app.- Core library desugaring enabled in your app module:
1android { 2 compileOptions { 3 coreLibraryDesugaringEnabled true 4 } 5} 6dependencies { 7 coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:2.1.5" 8} - Gradle project with mavenCentral() enabled.
- Internet reachability to your ingest target domain.
Note (Android 9+, API 28): Cleartext (HTTP) traffic is disabled by default; prefer HTTPS for your APIs and target. For local/dev HTTP, use a scoped Network Security Config (see Troubleshooting).
Install & Instrument Your Android Application#
1 Install Android Middleware SDK#
Add the SDK to your app module so Gradle can resolve and bundle the Mobile RUM library. This brings in the core telemetry runtime and OpenTelemetry interfaces we rely on. The latest release is 3.0.2.
1implementation 'io.github.middleware-labs:android-sdk:3.0.2'2 Configuration Methods (Optional)#
Use the following builder methods to tune what the SDK collects on-device. These switches only affect client-side collection, and they don’t require any server-side changes: Use these methods to tailor ingestion & monitoring:
1import static io.middleware.android.sdk.utils.Constants.APP_VERSION;
2 import io.middleware.android.sdk.Middleware;
3 import io.opentelemetry.api.common.Attributes;
4
5 class MyApplication extends Application {
6 private final String targetUrl = "<target-url>";
7 private final String rumAccessToken = "your-account-token";
8
9 @Override
10 public void onCreate() {
11 super.onCreate();
12
13 Middleware.builder()
14 .setTarget(targetUrl)
15 .setRumAccessToken(rumAccessToken)
16 .setServiceName("sample-android-app-1")
17 .setProjectName("Mobile-SDK-Android")
18 .setDeploymentEnvironment("PROD")
19 .setGlobalAttributes(Attributes.of(APP_VERSION, BuildConfig.VERSION_NAME))
20 // Optional tunables:
21 // .disableCrashReporting()
22 // .disableAnrDetection()
23 // .disableNetworkMonitor()
24 // .disableSlowRenderingDetection()
25 // .setSlowRenderingDetectionPollInterval(Duration.ofMillis(1000))
26 .build(this);
27 }
28 }Tip: Initialise as early as possible for best coverage of app start and early network calls.
Use these methods to tailor ingestion & monitoring:
| Method | Description |
|---|---|
setRumAccessToken(String) | Authorizes client to send telemetry to Middleware |
setTarget(String) | Sets target URL to recieve telemetry |
setService(String) | Sets service name of your application |
setDeploymentEnvironment(String) | Sets environment attribute on spans generated by instrumentation. Example: PROD, DEV |
disableCrashReporting() | Disables crash reporting which is enabled by default |
disableAnrDetection() | Disables Application Not Responding (ANR) detection which is enabled by default |
disableNetworkMonitor() | Disables network change detection which is enabled by default |
disableSlowRenderingDetection() | Disables slow or frozen frame render detection which is enabled by default |
setSlowRenderingDetectionPollInterval(Duration) | Sets default polling for slow or frozen render detection. Default detection interval is 1000 milliseconds |
setTracePropagationTargets(List<Pattern>) | Restricts which outbound request URLs carry trace headers. Matches every URL by default. See Distributed Tracing |
3 HTTP Instrumentation Config#
Out of the box, the SDK records UI and system signals. To gain request‑level visibility (method, URL, status, timings), instrument your OkHttp client. If you use Retrofit, wire the instrumented Call.Factory into your Retrofit.Builder. No handler changes are required.
HTTP instrumentation is not automatic on Android. Unlike the browser and iOS SDKs, the Android SDK cannot install itself into your network stack. Requests made through a plain OkHttpClient produce no HTTP events and carry no trace headers, so they will not correlate with backend traces. Every network call must go through the Call.Factory returned below. If none does, the SDK logs a warning about ten seconds after startup.
Integrate with OkHttp3 to monitor HTTP events across user devices:
1private Call.Factory buildOkHttpClient(Middleware middleware) {
2 return middleware.createRumOkHttpCallFactory(new OkHttpClient());
3}Retrofit: Supply the instrumented Call.Factory to Retrofit.Builder().callFactory(...) so your existing Retrofit stack is covered without further code changes.
Distributed Tracing#
End‑to‑end tracing links a RUM session to the backend traces it caused, so you can open a slow screen in the Session Explorer and see the server spans behind it.
It works by trace‑context propagation: the SDK creates a client span for each outgoing request and injects the W3C traceparent header (plus B3, for backends that read it). Your instrumented backend continues that same trace, and Middleware correlates the two by trace ID. For the full picture across RUM, traces, and logs, see Correlation with Data.
Requirements#
- Requests must go through the
Call.Factoryfrom Step 3 above. This is the single requirement, and the most common reason Android sessions show no backend traces. - Your backend services must be instrumented and must accept W3C Trace Context.
1public class MyApplication extends Application {
2
3 private Call.Factory httpClient;
4
5 @Override
6 public void onCreate() {
7 super.onCreate();
8
9 Middleware.builder()
10 .setTarget("<target-url>")
11 .setProjectName("Mobile-SDK-Android")
12 .setServiceName("sample-android-app-1")
13 .setRumAccessToken("<MW_API_KEY>")
14 .build(this);
15
16 // Wrap once, then use this everywhere in the app.
17 httpClient = Middleware.getInstance()
18 .createRumOkHttpCallFactory(new OkHttpClient());
19 }
20}With Retrofit, pass the wrapped factory rather than an OkHttpClient:
1Retrofit retrofit = new Retrofit.Builder()
2 .baseUrl("https://api.example.com/")
3 .callFactory(httpClient)
4 .build();Supported clients#
Only OkHttp is instrumented, including anything layered on it (Retrofit, Coil, and similar) as long as the wrapped factory is what they use. HttpURLConnection, Ktor, Volley, Cronet, and raw java.net clients are not instrumented and will not correlate.
Restricting which hosts receive trace headers#
By default every request through the wrapped client carries trace headers. To keep your trace IDs off third‑party APIs, list the hosts that should receive them:
1Middleware.builder()
2 // ... other configuration
3 .setTracePropagationTargets(Arrays.asList(
4 Pattern.compile("api\\.example\\.com"),
5 Pattern.compile("checkout\\.example\\.com")))
6 .build(this);Each pattern is searched for anywhere in the request URL, so api.example.com matches https://api.example.com/orders. Requests to other hosts are still timed and still appear in the session — they just travel without trace headers. Passing an empty list disables propagation entirely.
Verifying it works#
The span for each request records its outbound headers as attributes. In the Session Explorer, open a network event and look for http.request.header.traceparent.
- Present — the SDK propagated correctly. Any missing correlation is on the backend side; confirm that service is instrumented and accepts W3C Trace Context.
- Absent — the request did not go through the wrapped client. Revisit Step 3.
Custom Configurations#
Set Global Attributes#
Add stable keys (release, tenant, plan) that you will filter and chart by. Avoid PII—prefer IDs or hashed values. Setting app.version is especially useful for grouping by release in dashboards and matching ProGuard/R8 mapping uploads—see Android source maps.
Attach contextual metadata to all telemetry:
1Middleware.builder()
2 .setGlobalAttributes(
3 Attributes.builder()
4 .put("key", "value")
5 .put(StandardAttributes.APP_VERSION, BuildConfig.VERSION_NAME)
6 .build()
7 );Use for release, user cohort, tenant, etc.
Events#
Use lightweight business events to track user intent (e.g., add‑to‑cart, subscription start) or UX milestones (e.g., onboarding step completed). For multi‑step flows, wrap the sequence with a workflow span. Keep names consistent across releases for trend analysis.
Step 1: Set up Your Custom Event#
1Middleware.getInstance().addEvent("You clicked on Button", BUTTON_ATTRIBUES);Step 2: Start Custom Event Workflow#
1Span loginWorkflow = Middleware.getInstance().startWorkflow("User Login Flow");Step 3: End Custom Event Workflow#
1loginWorkflow.end();Error Reporting#
Crashes are captured automatically. Use addException(Throwable) for handled exceptions where your app catches an error, but you still want it visible in dashboards and correlated with the session/replay. Include attributes that help triage (feature flag, screen name, request id).
Use addException(Throwable) to report exceptions, errors, and display messages on the Middleware dashboard.
1Middleware.getInstance().addException(new RuntimeException("Something went wrong!"), Attributes.empty());Logs#
Client logs are enriched with session and device context and can be correlated with errors and replays. Use levels (debug/info/warn/error) to express severity; prefer structured messages and avoid logging secrets. Add custom logs that appear on your Middleware dashboard:
1Middleware logInstance = Middleware.getInstance();
2logInstance.d("TAG", "I am debug");
3logInstance.e("TAG", "I am error");
4logInstance.i("TAG", "I am info");
5logInstance.w("TAG", "I am warning");Session Replay#
Session Replay captures what the user saw and did. Use it to understand regressions and validate fixes. As of SDK 3.x, replay uses the next-generation recording pipeline (rrweb-based) and starts automatically when the SDK is initialised — no lifecycle hooks are required. Recordings play back frame-accurately in the Middleware session player alongside your web sessions. Replay data is batched and uploaded efficiently; keep privacy in mind for sensitive screens.
Tune capture rate and quality with RecordingOptions:
1RecordingOptions.Builder recordingOptions = new RecordingOptions.Builder();
2recordingOptions.setFrequency(RecordingFrequency.STANDARD); // LOW | STANDARD | HIGH
3recordingOptions.setQuality(RecordingQuality.STANDARD); // LOW | STANDARD | HIGH
4recordingOptions.setMaskAllTextInputs(true); // default: true
5recordingOptions.setMaskAllImages(true); // default: true
6
7Middleware.builder()
8 // ... target/token/etc.
9 .setRecordingOptions(recordingOptions.build())
10 .build(this);- Disable replay entirely with
.disableSessionRecording(). - Opt out of the new pipeline (fall back to the legacy recorder) with
.disableSessionRecordingV3().
Session Recording#
- Max duration: 4 hours per session.
- Idle timeout: 15 minutes of inactivity ends a recording; activity after the timeout starts a new session.
- Default: Recording is enabled; disable via
.disableSessionRecording().
Screen names#
Replay timelines and UI telemetry label screens with the Activity class name by default. Override it (for example on navigation changes) with:
1Middleware.getInstance().setScreenName("checkout");Privacy#
Masking happens on the device before upload. Mark views that may contain sensitive content (payments, OTPs, tokens) so they are masked in all recordings. Password fields and text inputs are masked by default (maskAllTextInputs), but you should still review high‑risk screens.
Mask specific views in session recordings:
1final Middleware instance = Middleware.getInstance();
2final TextView someTextView = findViewById(R.id.some_text_view);
3instance.addSanitizedElement(someTextView);You can also mark views declaratively — set the view tag or contentDescription to mw-no-capture (always mask) or mw-no-mask (never mask; wins over global rules):
1<ImageView
2 android:id="@+id/card_scan_preview"
3 android:tag="mw-no-capture" />For Jetpack Compose, use the replay modifiers:
1Text(
2 text = cardNumber,
3 modifier = Modifier.mwSessionReplayMask()
4)
5Image(
6 painter = logo,
7 modifier = Modifier.mwSessionReplayUnmask()
8)Consent (optional): If your app requests user consent, you may choose to disable Session Recording when consent is not granted (see Troubleshooting).
Default Attributes#
These tags are added automatically and appear in query builders and dashboards. Use them to slice data by app, service, or session.
The following Attributes are provided by the Android SDK by default:
| Method | Type | Description |
|---|---|---|
project.name, app | String | Defines the project name, used as projectName(String) |
service.name | String | Defines the service name, used as serviceName(String) |
session.id | String | Random session identifier generated by Middleware SDK |
rum.sdk.version | String | Middleware SDK version |
Resource Attributes#
Device and OS metadata help you spot model‑specific or version‑specific issues. When investigating performance, start by grouping by device.model.name and os.version.
Applied to all spans by default:
| Name | Type | Description |
|---|---|---|
env | String | Name of deployment environment. Example: DEV, PROD |
device.model.identifier | String | Device model identifier. Example: Moto-G30 |
device.model.name | String | Name of device. Example: ONEPLUS A600 |
device.manufacturer | String | Name of device manufacturer. Example: OnePlus |
os.name | String | Operating system name. Set to Android |
os.description | String | OS description. Example: Android Version 11 (Build RKQ1.201217.002 API level 30) |
os.type | String | OS type. Set to Linux |
os.version | String | OS version. Example: 11 |
Instrumentation Attributes#
Crash Reporting#
Crash Reporting is enabled by default and adds the following attributes to spans representing uncaught exceptions:
| Name | Type | Description |
|---|---|---|
thread.id | Integer | ID of the current managed thread (not OS thread ID) |
thread.name | String | Name of the thread |
exception.message | String | Exception message |
exception.type | String | Exception type |
exception.stacktrace | String | Stack trace for the exception |
exception.escaped | String | true for uncaught exceptions (crash) |
component | String | crash |
event.type | String | error |
Network Monitoring#
Produces spans named network.change with:
| Name | Type | Description |
|---|---|---|
network.status | String | lost or available |
network.connection.type | String | wifi, cell, unavailable, or unknown |
network.carrier.name | String | Carrier name |
Application Not Responding (ANR)#
ANRs indicate the UI thread was blocked long enough for Android to flag the app as unresponsive. The SDK emits a span when this threshold is crossed so you can correlate with recent network calls, disk I/O, or heavy work.
ANR detection creates spans whenever the main thread is unresponsive for > 5 s. Enabled by default. Attributes:
| Name | Type | Description |
|---|---|---|
exception.stacktrace | String | Stack trace for the ANR |
component | String | error |
event.type | String | error |
Slow Rendering Detection#
Jank degrades perceived quality. We report slow (>16 ms) and frozen (>700 ms) frames so you can track UI smoothness over time and catch regressions early. Use the poll interval to balance fidelity and overhead.
Slow rendering produces spans when a frame is > 16 ms; frozen when > 700 ms. During each interval, two spans can be emitted: slowRenders (slow frames) and frozenRenders (frozen frames). Enabled by default.
| Name | Type | Description |
|---|---|---|
count | Integer | Number of slow/frozen frames in a 1s interval (interval adjustable via slowRenderingDetectionPollInterval) |
HTTP Client Attributes#
The Android RUM agent instruments OkHttp. Activate via Step 3 above. Attributes:
| Name | Type | Description |
|---|---|---|
http.method | String | e.g., GET, POST, HEAD |
http.url | String | e.g., https://foo.bar/address?q=value#hash |
http.flavor | String | e.g., 1.0 |
http.status_code | Integer | e.g., 200, 404, 418 |
http.response_content_length | Integer | e.g., 3495 (bytes) |
http.user_agent | String | e.g., CERN-LineMode/2.15 libwww/2.17b3 |
net.transport | String | e.g., IP.TCP |
net.peer.name | String | e.g., example.com |
net.peer.port | Integer | e.g., 80, 8080, 443 |
component | String | http |
Activity Lifecycle Monitoring#
Enabled by default. Generates spans whenever an Activity changes state.
| Name | Type | Description |
|---|---|---|
component | String | ui |
activityName, activity.name | String | Activity class name (e.g., MainActivity) |
Fragment Lifecycle Monitoring#
Generates spans whenever a Fragment changes state. Possible states include: Created, Restarted, Resumed, Paused, Stopped, Destroyed.
| Name | Type | Description |
|---|---|---|
component | String | ui |
fragmentName | String | Fragment class name (e.g., MainFragment) |
App Start Monitoring#
Creates spans on Cold, Warm, and Hot app starts.
- Cold: App launched fresh after boot/kill
- Warm: Partial init still required; faster than cold
- Hot: App brought to foreground; already loaded
| Name | Type | Description |
|---|---|---|
component | String | appstart |
start.type | String | One of: cold, warm, hot |
Android‑specific Notes (Additive context)#
The following platform specifics are commonly required in production builds.
Manifest permissions#
1<uses-permission android:name="android.permission.INTERNET"/>
2<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>Network Security Config & HTTPS (API 28+)#
- On Android 9+ (API 28), cleartext HTTP is blocked by default. Prefer HTTPS for APIs and the Middleware target.
- For local/dev HTTP, create a Network Security Config (domain‑scoped) and/or use
usesCleartextTrafficonly for permitted hosts.
Retrofit/OkHttp integration tips#
- Prefer a single shared
OkHttpClient;if multiple clients exist, wrap each viacreateRumOkHttpCallFactory(...). - For Retrofit, either pass a custom
Call.Factoryor useclient(OkHttpClient)which sets the call factory internally. - Any client you forget to wrap is invisible to RUM and breaks Distributed Tracing for the calls it makes — nothing errors, the backend traces simply never correlate.
Consent gating (optional)#
If your app shows a privacy/consent screen, you may initialise Middleware after consent or disable Session Recording when consent is not granted:
1if (userConsented) {
2 // normal builder as shown above
3} else {
4 Middleware.builder()
5 .setTarget(targetUrl)
6 .setRumAccessToken(rumAccessToken)
7 .disableSessionRecording()
8 .build(this);
9}Troubleshooting#
Start here if data isn’t appearing or specific signals are missing.
- No data / network errors: Verify your target URL and HTTPS; on API 28+ cleartext HTTP is blocked unless explicitly allowed via Network Security Config.
- No HTTP events in Retrofit: Ensure your Retrofit
callFactoryorclient(OkHttpClient)uses the instrumented OkHttp instance. - No backend traces linked to a session: Open the network event in the Session Explorer and check for
http.request.header.traceparent. If it is missing, the request bypassed the instrumented client — see Distributed Tracing. If it is present, the gap is server-side: confirm the backend service is instrumented and accepts W3C Trace Context. - Trace headers reaching a third-party API: Narrow
setTracePropagationTargets(...)to your own domains. - Replay not starting: Confirm OS version supports replay and that lifecycle hooks call
startRecording/stopRecording. - High frozen/slow frame counts: Reduce main‑thread work. Slow > 16 ms, Frozen > 700 ms.
Need assistance or want to learn more about Middleware? Get in touch with us via our Contact Us or join our Slack channel.