Set up an Android application
This page takes an Android app from nothing to its first session in FuseScope. You need the Admin or Editor role, and an Android app written in Kotlin or Java that you can build and run.
The SDK, io.fuseone.fusescope:android, is a thin distribution of
OpenTelemetry Android. It
records sessions, screens, crashes, ANRs (Application Not Responding), app start and
OkHttp requests linked to backend traces. It is in beta, 0.1.0-beta.2, and its options
may change before 1.0. Native (NDK) crashes are not captured yet; they are planned.
0.1.0-beta.2 needs FuseScope 0.156.0 or later to keep one
R8 mapping per build;
0.1.0-beta.1 works with 0.155.0 or later.
If you don’t know what RUM records yet, start with Real User Monitoring.
The steps at a glance
Section titled “The steps at a glance”- Check the requirements:
minSdk23,compileSdk37 and one Gradle property. - Create the application in FuseScope, with the package names allowed to send data, and copy its public key.
- Add the SDK with Gradle.
- Start it in
Application.onCreate. - Trace OkHttp requests to link them to backend traces.
- Run the app and watch the data arrive.
- Upload your R8 mappings with each release, so stack traces show your original code.
Requirements
Section titled “Requirements”| Requirement | |
|---|---|
minSdk |
23 (Android 6.0) or higher. Below 26, turn on core library desugaring. |
compileSdk |
37 |
| Build tools | The SDK is built and tested with Android Gradle Plugin 9.4.0 and Kotlin 2.4.10. The mapping plugin needs Gradle running on JDK 17 or later. |
| Gradle property | android.useFullClasspathForDexingTransform=true, see below. |
Add the property to the project’s gradle.properties:
android.useFullClasspathForDexingTransform=trueCreate the application
Section titled “Create the application”A RUM application is one app in one environment, such as shop-android in
production. Open Frontend in the sidebar, then New application, and pick the
platform Android (Android SDK · Java and Kotlin). The platform is chosen once, at
creation.
The New application sheet is the same as for the web, with Package names and tracing instead of origins:
- Name: the gateway sends it as
service.name; it is how the app appears in Services and the explorers. Use lowercase letters, digits and dashes, such asshop-android. - Environment: sent as
deployment.environment.name. Create one application per environment, so each has its own key and numbers. - Allowed package names: the Android
applicationIdvalues allowed to send data with this application’s key. The SDK sends the app’sapplicationIdas theapp.package.nameresource attribute, and the gateway refuses any request whose package is missing or not in the list.com.example.shopallows exactly that package.com.example.shop.*allows its sub-packages, such ascom.example.shop.debug, but notcom.example.shopitself; add both if a build type uses anapplicationIdSuffix.*allows any package. Avoid it in production: anyone who extracts your key from the APK could send data from their own app.
- Link requests to backend traces: see Link requests to backend traces.
- Session sample rate, Drop query strings and fragments, Keep these query parameters, Wait for consent and Hash user IDs: the same settings as for the web; see Set up a web application. The SDK ones are written into the generated snippet.
Create application saves it and opens the Install step with the application’s
public key (rum_pub_…). Copy it now: it is shown once. FuseScope stores only a
hash of it. The key is public and write-only: it is safe to ship in an APK or app
bundle, where anyone can read it, since it can only send data for this application, from
its allowed packages, within its rate limit. To replace a lost or leaked key, see
Keys.
Add the SDK
Section titled “Add the SDK”The SDK, its Compose add-on and the mapping plugin are on Maven Central. Resolve plugins and dependencies from it:
pluginManagement { repositories { google() mavenCentral() gradlePluginPortal() }}
dependencyResolutionManagement { repositories { google() mavenCentral() }}Then, in the app module:
plugins { id("com.android.application") id("io.fuseone.fusescope.android.mappings") version "0.1.0-beta.2" // optional, uploads R8 mappings}
dependencies { implementation("io.fuseone.fusescope:android:0.1.0-beta.2") implementation("io.fuseone.fusescope:android-compose:0.1.0-beta.2") // optional, Compose and Navigation screens}| Artifact | What it is |
|---|---|
io.fuseone.fusescope:android |
The SDK. Kotlin package io.fuseone.fusescope.android. |
io.fuseone.fusescope:android-compose |
Optional. Views for Jetpack Compose and Navigation screens; see Compose screens. |
io.fuseone.fusescope.android.mappings |
Optional Gradle plugin. Uploads mapping.txt after each minified build; see R8 mappings. |
The SDK is Apache-2.0 licensed, like OpenTelemetry.
Start the SDK
Section titled “Start the SDK”Call FuseScope.init in your Application.onCreate, before anything else. The
Install step generates the call from the application’s settings; for the example
above it reads:
import android.app.Applicationimport io.fuseone.fusescope.android.FuseScopeimport io.fuseone.fusescope.android.FuseScopeConfig
class ShopApplication : Application() { override fun onCreate() { super.onCreate() FuseScope.init( this, FuseScopeConfig( applicationId = "01a16431-12ca-7064-bae9-6ac64cf20a29", key = "rum_pub_…", endpoint = "https://fusescope.example.com", environment = "production", ), ) }}Register the class in the manifest, if it isn’t already:
<application android:name=".ShopApplication" … >applicationIdis the RUM application’s id (a UUID), not your AndroidapplicationId.endpointis the FuseScope URL your users’ devices reach. If you opened FuseScope throughlocalhostor an internal host name, use its public URL. The SDK sends to<endpoint>/rum/v1/tracesand<endpoint>/rum/v1/logs. On the emulator, the host machine ishttp://10.0.2.2.serviceandreleaseare left out: the gateway names the service after the application, andreleasedefaults to the app’sversionName, as the mapping upload does.- With Link requests to backend traces set, the snippet also passes those URLs’
hosts as
traceparentHosts; see Link requests to backend traces. - A second
initcall, or an invalid configuration, is logged under theFuseScopetag and ignored; the app keeps running.
Configuration
Section titled “Configuration”FuseScopeConfig |
Default | |
|---|---|---|
applicationId |
required | The RUM application id from the Install step. |
key |
required | The public key, rum_pub_…. |
endpoint |
required | The FuseScope base URL. |
service |
none | service.name. When unset, the gateway uses the application’s name; set it only to override that. |
environment |
none | deployment.environment.name. When unset, the gateway uses the application’s environment. |
release |
versionName |
service.version: the Release filter, and the release R8 mappings are looked up by. |
sessionSampleRate |
1.0 |
Share of sessions recorded, 0 to 1. Decided once per session. |
traceparentHosts |
the endpoint’s host |
Hosts whose requests carry traceparent; see Link requests to backend traces. |
requireConsent |
false |
Collect nothing until FuseScope.setConsent(true). |
diskBuffering |
false |
Keep data on disk while the device is offline. Data is then exported 30 s or more after it is recorded. |
privacy |
PrivacyConfig() |
See Privacy defaults. |
session |
SessionConfig() |
inactivityTimeoutMillis (15 min) and maxDurationMillis (4 h). |
instrumentations |
InstrumentationsConfig() |
crashes, anrs, views and appStart on; lifecycleSpans (upstream Created, Resumed, Paused spans) and slowRendering off. |
FuseScopeConfig( // … sessionSampleRate = 0.5, privacy = PrivacyConfig(queryAllowlist = listOf("page")), instrumentations = InstrumentationsConfig(slowRendering = true),)PrivacyConfig, SessionConfig and InstrumentationsConfig are in
io.fuseone.fusescope.android. All three are Java-friendly (@JvmOverloads), and so are
the FuseScope functions (@JvmStatic).
Consent
Section titled “Consent”With Wait for consent on, the snippet sets requireConsent = true: the SDK records
nothing until the user agrees. Call setConsent from your consent screen:
FuseScope.setConsent(true) // starts collectionFuseScope.setConsent(false) // flushes what was recorded and stopsYour app stores the user’s choice and calls setConsent(true) again on the next launch;
the SDK doesn’t remember it.
Identify the user
Section titled “Identify the user”Users are anonymous unless you call setUser, for example after sign-in:
FuseScope.setUser(account.id) // user.id = SHA-256 of account.idFuseScope.setUser(null) // on sign-outWith Hash user IDs on (the default), user.id is the hex SHA-256 of the id, the same
value @fusescope/browser sends for the same id, so web and Android sessions of one user
match. To find a user’s sessions, hash their id the same way and search for the hash in
Sessions. The user applies to spans started after
the call; the current view keeps its previous value. setUser may be called before
collection starts, such as before consent: it is applied when it starts.
Views, errors and actions
Section titled “Views, errors and actions”The SDK records the following on its own:
- Views: one
viewspan per Activity or Fragment, ending when the next one starts or the app goes to the background. See Views. - Crashes: uncaught Java and Kotlin exceptions, flushed before the process dies.
- ANRs: the main thread blocked for about 10 seconds, with the main thread’s stack.
- App start: cold, warm and hot starts, as the
app_startvital.
Name a screen yourself, report a handled error or a custom action with:
FuseScope.startView("orders/{id}") // a route template, not the filled-in path
try { checkout()} catch (e: IOException) { FuseScope.addError(e) // fusescope.rum.error.source = manual}
FuseScope.addAction( "checkout", Attributes.of(AttributeKey.stringKey("cart.size"), "3"),)startView replaces the automatic view of the current Activity, which comes back when
that Activity resumes. Attributes and AttributeKey are from
io.opentelemetry.api.common. For your own spans and logs, FuseScope.openTelemetry is
the SDK’s OpenTelemetry instance, a no-op while collection is off.
Calls made before collection starts are dropped, except setUser. FuseScope.flush()
exports pending data now; it blocks, so call it off the main thread. FuseScope.shutdown()
flushes and stops; init can then be called again.
Compose screens
Section titled “Compose screens”A Compose app often has a single Activity, so the automatic views see one screen. With
io.fuseone.fusescope:android-compose, start a view per Navigation destination, named by
its route template:
import io.fuseone.fusescope.android.compose.FuseScopeNavigationViews
@Composablefun ShopNavHost() { val navController = rememberNavController() FuseScopeNavigationViews(navController) NavHost(navController, startDestination = "home") { composable("home") { HomeScreen() } composable("orders/{id}") { OrderScreen() } }}The view is named orders/{id}, never orders/1234. Outside Compose, such as with a
Fragment-based NavController, call navController.trackFuseScopeViews(); it returns
the listener, to remove when you no longer need it. For a screen without Navigation,
call FuseScopeView("settings") in the composable.
Link requests to backend traces
Section titled “Link requests to backend traces”Make your HTTP calls through FuseScope.callFactory, an OkHttp Call.Factory that
records each request as a span and adds the W3C traceparent header:
val client = OkHttpClient()val http = FuseScope.callFactory(client)
http.newCall(Request.Builder().url("https://api.example.com/cart").build()).execute()
// With Retrofitval retrofit = Retrofit.Builder() .baseUrl("https://api.example.com/") .callFactory(FuseScope.callFactory(client)) .build()The backend’s trace then becomes a child of the app’s request, and View backend trace works from sessions, views and errors. The backend must be instrumented with OpenTelemetry and send its traces to FuseScope; see Instrument applications. There is no CORS on Android, so the API needs no change.
Only requests to the hosts in traceparentHosts carry traceparent. By default that is
the endpoint’s host alone, so list your API hosts:
FuseScopeConfig( // … traceparentHosts = listOf("api.example.com", "*.example.com"),)- An entry is an exact host (
api.example.com),*.example.comfor its subdomains only (notexample.comitself), or*for every host. Hosts are matched alone, whatever the scheme or port. - An invalid entry, such as a URL or a host with a port, makes
initlog an error and do nothing. - Requests to other hosts are still traced as spans, just without the header, so third-party APIs never receive your trace ids.
- The Install step fills
traceparentHostsfrom the hosts of the application’s Link requests to backend traces URLs. - The factory can be created before
init. While collection is off, requests go through your client unchanged. - Requests of an unsampled session carry no
traceparent, so they don’t change how your backend samples its traces. - Requests made with a plain
OkHttpClient, or another HTTP library, are not traced.
Privacy defaults
Section titled “Privacy defaults”- Query strings and fragments are dropped from URL attributes, span names, error
messages and stack traces:
https://api.example.com/search?q=bootsbecomeshttps://api.example.com/search.privacy.queryAllowlistkeeps named parameters;privacy.dropQuery = falsekeeps them all. The gateway drops them again on the server, following the application’s policy. - No user identity unless you call
setUser, and then hashed (privacy.hashUserId). - No persistent device identifier. OpenTelemetry Android’s
app.installation.idis removed unless you setprivacy.installationId = true. - Consent hook:
requireConsentandsetConsent. - No screen content, text or input is recorded.
Check that data arrives
Section titled “Check that data arrives”Run the app, on a device or the emulator, and move through a few screens. The
application’s Install step updates by itself: it reads Waiting for first data…
until the gateway accepts the first request, then Receiving data, with what arrived
in the last 5 minutes (Sessions, Views, Errors and the vitals, counted under
Web Vitals) and the SDK seen, such as io.fuseone.fusescope:android 0.1.0-beta.2
Supported. Open shop-android opens the application’s
overview.
The SDK exports in batches, every few seconds. Putting the app in the background ends the current view.
Troubleshoot rejected requests
Section titled “Troubleshoot rejected requests”Requests the gateway refuses are listed under the status, with how many were rejected in the last 5 minutes:
| Message | Cause | Fix |
|---|---|---|
| package com.example.shop.debug is not allowed | The app’s applicationId is not in Allowed package names, often a debug build with an applicationIdSuffix. The gateway answers 403. |
Add the package, or com.example.shop.*, in the application’s Settings. |
no app.package.name in the resource |
The client sent no package name: a hand-configured OpenTelemetry client. | Add the attribute; see Use another OpenTelemetry client. |
| the key is not valid for this application | The key was revoked, or its rotation overlap ended. The gateway answers 401. |
Ship the current key; see Keys. |
| rate limited | Too many requests from one client or for the whole key. The gateway answers 429. |
Usually a loop. Behind a proxy, check the client IP setting. |
If nothing arrives and nothing is rejected, filter Logcat by the FuseScope tag, then
check:
- The app crashes at start in debug: add
android.useFullClasspathForDexingTransform=true. - No request to
/rum/v1/:initis not called (is theApplicationclass in the manifest?), the session was not sampled, orrequireConsentis on andsetConsent(true)hasn’t been called. - Requests fail without reaching FuseScope:
endpointis wrong or unreachable from the device, or the/rum/path is not exposed; see Self-hosted FuseScope. The Android app sends noOriginheader, so the gateway’s CORS settings don’t apply. - Cleartext HTTP: Android blocks
http://URLs by default. Use HTTPS, or a network security config for a local test server.
Upload R8 mappings
Section titled “Upload R8 mappings”Release builds minified with R8 have obfuscated stack traces, such as
a.b.c(SourceFile:12). With the io.fuseone.fusescope.android.mappings plugin applied,
configure where to upload:
fusescope { endpoint.set("https://fusescope.example.com") applicationId.set("01a16431-12ca-7064-bae9-6ac64cf20a29") // the API key comes from FUSESCOPE_API_KEY}Export an Editor service account key as FUSESCOPE_API_KEY in your CI, then build:
./gradlew :app:bundleRelease uploads mapping.txt for the release the SDK reports. See
R8 mappings for the details.
Related
Section titled “Related”- R8 mappings: original stack traces.
- Applications and Overview: read what arrives.
- Errors: crashes, ANRs and handled errors.
- Keys: rotate or revoke the public key.
- SDK README: every attribute the SDK sends.