Before you start
Create the project with the Android integration type.
| What you need | Why it matters |
|---|---|
| Android project in Logister | Locks the project type so collected Android data is interpreted as mobile telemetry. |
| Backend token issuer | Keeps the server project API token out of the APK and returns short-lived mobile ingest tokens to the app. |
| Logister instance base URL | Builds the /api/v1/ingest_events endpoint used by the SDK. It must use https outside local development; see Endpoint and transport. |
| Android app using Gradle | Installs the SDK from Maven Central with your normal Android dependency flow. |
| Optional GitHub App installation | Lets Logister use mobile source context and CI deployment records for source excerpts and deploy links. |
What is supported
Choose each app-backed collector explicitly.
| Capability | SDK surface | Where it appears |
|---|---|---|
| Handled error | captureExceptionAsync | Issues, as a reported exception with its type and stack frames. Messages and cause chains are included only with LogisterExceptionDataPolicy.FULL. |
| Unhandled error | automaticCrashCapture(true) | Issues as an unhandled exception. |
| Historical app exit | applicationExitCapture(true) on Android 11+ | ANR, Java/native crash, and low-memory exit evidence on the next start. |
| Impact | sessionTracking and installationTracking | Affected-session and installation counts when collected. |
| Trail | breadcrumbs and addBreadcrumb | Bounded activity before the selected occurrence. |
| Offline delivery | offlineQueue | Bounded retry storage; queued is kept distinct from server accepted. See Offline queue. |
| Logs, metrics, transactions, spans, check-ins | Explicit capture methods | Explore › Events, Explore › Charts, Performance, and Monitors. |
| Source context | repository, commitSha, branch | Source lookup and deployment context when a GitHub repository is connected. |
Install
Install org.logister:logister-android from Maven Central.
Privacy-safe automatic crash capture and durable pre-auth delivery are available in package version 0.3.0. Make sure your app resolves dependencies from Maven Central, then add the library to the Android app module. If you already use the SDK, read Upgrading to 0.7.0 first: several defaults changed in that release.
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
}
}dependencies {
implementation("org.logister:logister-android:0.7.0")
}- View the Maven Central package page for artifact metadata and available versions.
- Open the Maven repository path when you need to verify synced versions directly.
- Use GitHub for source, examples, releases, and issue tracking.
Upgrading
Review the changed defaults before upgrading to 0.7.0.
Version 0.7.0 changes how exceptions, redaction, payload size, endpoints, and delivery behave by default. Most apps need no code change. Check each row against your configuration, then run the verification checklist on a test build. If you use the optional OkHttp module, org.logister:logister-android-okhttp, upgrade it to the same version as the core artifact; both Android artifacts share one release version.
| Area | Default in 0.7.0 | What to do |
|---|---|---|
Manual captureException | TYPE_AND_STACKTRACE: exception type and bounded stack frames, without messages or cause chains. | Set exceptionDataPolicy(LogisterExceptionDataPolicy.FULL) only if you need messages and causes and they are known not to carry personal data or secrets. See Exception data. |
| Sensitive keys | The shared Logister redaction rule, including contains-matching on token, secret, password, passwd, apikey, authorization, credential, and cookie. The key is kept and its value becomes "[REDACTED]". | Expect keys such as accessToken, apiKey, x-api-key, refreshToken, clientSecret, and idToken to show "[REDACTED]". Add your own keys with addSensitiveKey. See Redaction. |
| URLs inside strings | Scheme, host, path, and non-sensitive query parameters are kept; userinfo and sensitive query values become [REDACTED]; fragments are dropped. | Review messages and context strings that contain URLs with other query parameters. |
| Event size | Each event is fitted to the ingest API's per-event limits. Anything cut is marked with context._truncated: true. | Keep caller context small. Lower the key cap with maxEventKeys(...) if needed. See Event limits. |
| Capture threading | Asynchronous capture builds and sanitizes the envelope on the client executor and never throws into the calling thread. | Check isDropped on the returned response or discardedEventCount() to find events that could not be built. |
| Endpoint scheme | build() rejects plain http unless the host is loopback or 10.0.2.2, or allowInsecureHttp(true) is set. | Use an https base URL. Enable allowInsecureHttp(true) only in debug builds. See Endpoint and transport. |
| Redirects and responses | The default transport does not follow redirects and reads at most 64 KiB of a response body. | Set baseUrl to the final Logister URL, not one that redirects. |
| Retry backoff | Computed backoff uses equal jitter. Retry-After is honored exactly. | No change needed. |
| Exception cause chains | Frames shared with the enclosing exception are omitted (recorded as frames_in_common), and chains are capped at 150 frames in total. | No change needed. Applies when causes are captured. |
| Crash persistence | Delivery never holds the offline queue's storage lock, so a crash envelope is persisted immediately; the follow-up delivery runs on a dedicated background thread. | Keep offlineQueue enabled with automaticCrashCapture. See Offline queue. |
| Client startup | The installation pseudonym is cached in memory, and legacy-state cleanup runs on the client executor instead of in build(). | No change needed. |
Configure
Create one client with app and release defaults.
Use the Kotlin facade with a LogisterTokenProvider. Do not compile a long-lived Logister project API key into the app; your backend should mint short-lived mobile ingest tokens with POST /api/v1/mobile_ingest_tokens.
import org.logister.android.LogisterExceptionDataPolicy
import org.logister.android.LogisterToken
import org.logister.android.LogisterTokenProvider
import org.logister.android.logisterClient
class AppBackendTokenProvider : LogisterTokenProvider {
override fun fetchToken(): LogisterToken {
return LogisterToken("short-lived-mobile-token", System.currentTimeMillis() / 1000 + 900)
}
}
val client = logisterClient(
baseUrl = "https://your-logister-host.example",
tokenProvider = AppBackendTokenProvider()
) {
environment("production")
release("${BuildConfig.VERSION_NAME}+${BuildConfig.VERSION_CODE}")
repository("acme/android-app")
commitSha(BuildConfig.GIT_SHA)
branch(BuildConfig.GIT_BRANCH)
packageName(BuildConfig.APPLICATION_ID)
appVersion(BuildConfig.VERSION_NAME)
buildNumber(BuildConfig.VERSION_CODE.toString())
buildType(BuildConfig.BUILD_TYPE)
application(myApplication)
// Required when one app configures multiple logical Logister clients.
storageNamespace("primary-mobile-project")
exceptionDataPolicy(LogisterExceptionDataPolicy.TYPE_AND_STACKTRACE)
sessionTracking(true)
installationTracking(true, rotationDays = 90)
breadcrumbs(capacity = 50)
offlineQueue(enabled = true, maxEvents = 30, maxBytes = 512 * 1024, maxAgeDays = 7)
automaticCrashCapture(true, LogisterExceptionDataPolicy.TYPE_AND_STACKTRACE)
applicationExitCapture(true)
}Android package name is the best default service identity. Release should include enough app version/build information to separate user-visible releases from CI or hotfix builds.
exceptionDataPolicy and the policy passed to automaticCrashCapture both default to TYPE_AND_STACKTRACE; the sample sets them explicitly so the choice is visible in review. The base URL must use https; see the next section.
Endpoint and transport
Point the client at an https endpoint that does not redirect.
The SDK checks baseUrl when the client is built. Plain http is accepted only for localhost, loopback addresses, and the Android emulator host 10.0.2.2. For any other http host, build() throws IllegalArgumentException unless you call allowInsecureHttp(true). Use that option only in a debug build, for example against a development server on your local network.
val client = logisterClient(
// Your app's own build field, e.g. a local http URL in debug builds.
baseUrl = BuildConfig.LOGISTER_BASE_URL,
tokenProvider = AppBackendTokenProvider()
) {
if (BuildConfig.DEBUG) {
allowInsecureHttp(true)
}
}| Transport behavior | What it means for setup |
|---|---|
| Redirects are not followed. | The mobile token is only sent to the configured host. Set baseUrl to the final Logister URL, including the scheme and hostname your instance serves directly. |
| Response bodies are read up to 64 KiB. | No action needed. Anything past that limit is not read. |
| Retryable responses are retried with backoff. | With offlineQueue enabled, token-provider failures and 408, 425, 429, and 5xx responses are queued with bounded, jittered exponential backoff. Retry-After is honored exactly. |
| Permanent client rejections are discarded. | A rejected event does not block later events. Check the event payload against the HTTP API if events are rejected. |
Verify it
Build a release variant with your production base URL and confirm build() succeeds and one test event reaches the project. If events never arrive, request the base URL with curl -I and confirm it answers directly rather than with a 3xx redirect.
Source and deployments
Attach source context in the app, and record deployments from CI/CD.
The Android SDK adds repository, commit_sha, and branch to event context when you set them on the client builder. If the project is connected to the GitHub App, Logister can use those values to resolve stack frames and link to source.
Deployment records should be sent from CI/CD, not from the mobile app. After a Play, Firebase App Distribution, internal, or enterprise deploy succeeds, POST a deployment envelope to /api/v1/deployments with the release, environment, repository, commit SHA, branch, and workflow URL.
{
"deployment": {
"release": "1.4.0+42",
"environment": "production",
"repository": "acme/android-app",
"commit_sha": "4f8c2d1a9b7e6c5d4a3b2c1d0e9f8a7b6c5d4e3f",
"branch": "main",
"release_tag": "android-v1.4.0",
"workflow_run_url": "https://github.com/acme/android-app/actions/runs/123"
}
}Verify it
Send one test exception from a build using the same release. Open the event detail and check for source lookup status, deployment context, and GitHub links. If source is missing, confirm the GitHub App is installed, linked to the project, and the repository is connected; then check that CI sent a deployment record or the app event has repository and commit_sha.
Events
Send errors, logs, metrics, and transactions from Kotlin.
import org.logister.android.captureExceptionAsync
import org.logister.android.captureMetricAsync
import org.logister.android.captureMessageAsync
import org.logister.android.captureTransactionAsync
client.captureMessageAsync("Checkout opened") {
context("screen_name", "Checkout")
sessionId("session-123")
}
client.captureMetricAsync("cart.item_count", 3, "count")
client.captureTransactionAsync("screen.load", 184.2) {
context("screen_name", "Checkout")
}
try {
runCheckout()
} catch (exception: Exception) {
client.captureExceptionAsync(exception) {
mechanism("handled_exception")
handled(true)
}
}Captured exceptions include the exception type and bounded stack frames; messages and causes depend on the exception data policy. Metric values should use stable names and units, and transactions should describe app work such as screen loads, local processing, sync jobs, or API calls.
Each ...Async method returns a Future<LogisterResponse> and does its work on the client's background executor: the envelope is built, redacted, and fitted to the event limits there, not on the calling thread. Capture never throws into your code. If an envelope cannot be built or the executor has shut down, the response is dropped (isDropped == true) and counted in discardedEventCount(). Do not call Future.get() on the Android main thread.
Exception data
Choose how much of each exception to send.
Manual captureException calls and automatic crash capture both default to TYPE_AND_STACKTRACE. Throwable messages and causes can carry personal data or secrets, so the default leaves them out.
| Policy | Sends | When to use it |
|---|---|---|
LogisterExceptionDataPolicy.TYPE_AND_STACKTRACE (default) | Exception type and bounded stack frames. No throwable messages, no cause chains. | Most apps, and any app that has not reviewed what its exception messages contain. |
LogisterExceptionDataPolicy.FULL | Type, stack frames, messages, and cause chains. Cause chains omit frames shared with the enclosing exception (recorded as frames_in_common) and are capped at 150 frames in total. | Only when your exception messages and causes are known not to carry personal data or secrets. |
To include messages and causes in manual captures, set the policy on the client builder:
val client = logisterClient(
baseUrl = "https://your-logister-host.example",
tokenProvider = AppBackendTokenProvider()
) {
exceptionDataPolicy(LogisterExceptionDataPolicy.FULL)
// Automatic crashes keep their own policy argument.
automaticCrashCapture(true, LogisterExceptionDataPolicy.TYPE_AND_STACKTRACE)
}Messages that are included are still redacted with the shared redaction rule.
Verify it
Capture a test exception with a recognizable message, such as IllegalStateException("policy check"). With the default policy, the event detail shows the type and frames but not the message. After switching to FULL, the message and any causes appear.
Spans and check-ins
Use spans for waterfalls and check-ins for recurring Android work.
import org.logister.android.checkInAsync
import org.logister.android.captureSpanAsync
import org.logister.android.logisterSpan
client.captureSpanAsync(
logisterSpan("trace-123", "GET /checkout", 42.5) {
spanId("span-456")
parentSpanId("span-root")
kind("http")
status("ok")
context("screen_name", "Checkout")
}
)
client.checkInAsync("daily-sync", "ok") {
durationMs(812.4)
context("expected_interval_seconds", 86_400)
}Use shared trace IDs when a screen action triggers multiple spans. Check-ins are useful only for recurring work whose declared interval the app can reasonably meet and where a missed run is actionable. Android background scheduling is inexact, so use transaction and error events for WorkManager jobs that may be delayed by device state.
Mobile context
Send safe Android metadata that helps debugging without collecting secrets.
| Field | Recommended value | Why it helps |
|---|---|---|
platform | android | Separates Android telemetry from server and iOS data. |
service | Application ID or package name | Identifies the app sending events. |
release | Version name plus version code | Supports release filters and regression review. |
environment | production, staging, or build flavor | Filters telemetry by deployment channel. |
repository | GitHub owner/repo, such as acme/android-app | Lets source lookup find the connected repository. |
commit_sha | The app build commit | Resolves stack frames and deployment context to exact source. |
branch | Release branch or GitHub ref name | Fallback when a commit or deployment record is missing. |
session_id | App-generated session identifier | Connects related mobile events without sending raw user data. |
screen_name | Stable screen or route name | Makes activity, transactions, and spans easier to scan. |
android_api_level | SDK integer | Helps spot OS-version-specific failures. |
device_model | Sanitized manufacturer/model | Helps debug device-specific issues. |
Stability issues
Triage by failure type, release, impact, and affected cohort.
Issues on an Android project is a stability-specific list. Use the recommended, impact, or newest sort and filter by mechanism, release/build, Play track, environment, build type, device, Android/API version, screen, foreground state, and time range. The detail view keeps the Java/Kotlin cause chain, breadcrumbs, occurrences, app/device context, and raw event together.
Missing is not zero
Affected installation and session counts appear only when those opt-in identifiers were captured. Manual captureExceptionAsync calls are labeled reported exceptions unless you explicitly use an unhandled mechanism.
R8 mappings
Upload each release's private mapping.txt.
- Open Releases → Artifacts and choose Upload a mapping. Managers see the upload form; other members see the inventory.
- Select the build's
mapping.txtand enter the exact package name and version code. - Send an event from that build and confirm the stack says Deobfuscated.
Mappings are project- and build-scoped. If the event has no build number or no matching artifact, Logister shows Build unknown or Mapping missing rather than treating obfuscated frames as trustworthy. Artifacts also shows which observed builds each mapping covers, and the R8 mapping step in Settings → Setup links to the same upload.
Google Play
Import Play vitals without merging them into SDK metrics.
The optional Google Play panel is under Settings → Integrations. Put the service-account JSON in the host's secret store under an environment variable whose name starts with LOGISTER_INTEGRATION_ (for example LOGISTER_INTEGRATION_GOOGLE_PLAY_CREDENTIALS), enter that name as the credential reference, select permitted tracks, save, and queue an import. Other names are rejected unless the operator lists them in LOGISTER_INTEGRATION_CREDENTIAL_ENV_ALLOWLIST. Logister stores crash/ANR rates, anomalies, release tracks, source, freshness, and time zone as a separate Play snapshot because Play and SDK populations can differ. Play rate rows are version-code scoped, so Logister resolves each selected track to its active version codes before filtering those rows.
Verify it
Check that the panel shows a successful import time and the expected package. A missing secret reference, reporting permission, or package association leaves SDK issues working and reports the Play import error separately. Credential failures show a generic reason, such as Credential is not configured or Credential could not be parsed; check the variable on the web and worker hosts.
Upgrading with an older variable name
If an existing Play integration uses a name such as GOOGLE_PLAY_REPORTING_CREDENTIALS, either rename the secret to a LOGISTER_INTEGRATION_ name and update the credential reference, or add the old name to LOGISTER_INTEGRATION_CREDENTIAL_ENV_ALLOWLIST on the web and worker processes and restart them. Until then, imports fail with Credential reference is not an allowed environment variable and the enabled setting cannot be saved.
Privacy defaults
Keep Android instrumentation explicit and bounded.
Sessions, the rotating installation pseudonym, breadcrumbs, the uncaught-exception handler, historical app exits, and offline delivery are disabled until enabled with an Application. Automatic crashes and manual captureException calls both use TYPE_AND_STACKTRACE by default, which omits throwable messages and cause chains (see Exception data). Every payload is redacted before it is persisted or sent (see Redaction). Historical exits omit Android's raw description. The SDK never reads Android ID, advertising ID, IMEI, or a hardware serial.
- Do not send API tokens, authorization headers, cookies, payment data, medical data, or raw request/response bodies. Redaction is a safety net, not a reason to send them.
- Automatic crashes are synchronously persisted before the previous Android crash handler runs and therefore require
offlineQueue. Persistence does not wait for network delivery; see Offline queue. - Keep breadcrumb capacity and offline queue event, byte, and age limits bounded.
- Flush after authentication and call
clearSessionBoundQueuedEvents()during logout or account replacement. - Use the app's privacy review before enabling installation tracking or sending user IDs, raw URLs, or custom breadcrumb data.
Redaction
Sensitive values become "[REDACTED]" before persistence or delivery.
The SDK recursively redacts every payload with the default redaction rule shared by all Logister SDKs. It applies to context, breadcrumbs, exception data, span attributes, and messages. The SDK's own identity fields (uuid, event_type, occurred_at, and trace, span, request, and session ids) are never rewritten.
| Rule | Behavior |
|---|---|
| Sensitive keys | Keys are lowercased and every non-alphanumeric character is removed. A key is sensitive when the result equals password, passwd, pwd, secret, token, apikey, authorization, proxyauthorization, cookie, setcookie, sessiontoken, sessionkey, credential, credentials, privatekey, clientsecret, accesstoken, refreshtoken, idtoken, authtoken, xapikey, xauthtoken, xaccesstoken, xcsrftoken, xxsrftoken, signature, ssn, creditcard, cardnumber, or cvv, or contains token, secret, password, passwd, apikey, authorization, credential, or cookie. So accessToken, x-api-key, and client_secret all match. The key is kept and its value becomes "[REDACTED]". |
| Sensitive values in strings | Bearer <credential> and Basic <credential>, JWT-shaped values, and lgst_ keys are scrubbed. |
| URLs in strings | Scheme, host, and path are kept. Userinfo and the value of any sensitive query parameter become [REDACTED]; other query parameters are kept; fragments are dropped. |
Add keys of your own with payloadPolicy. Custom keys are normalized the same way. addSensitiveKey adds to the exact-match list; sensitiveKeys(...) replaces it. The contains rule always applies.
val client = logisterClient(
baseUrl = "https://your-logister-host.example",
tokenProvider = AppBackendTokenProvider()
) {
payloadPolicy(
LogisterPayloadPolicy.builder()
.addSensitiveKey("customer_email")
.build()
)
}For anything the key rules cannot express, a synchronous beforeSend(...) hook may further redact or discard an envelope. Keep it free of network and disk I/O. It cannot replace the SDK-owned UUID, occurrence time, or evidence provenance.
Verify it
Send a test message with context("access_token", "test-value") and context("customer_email", "test@example.com") after adding the custom key. The event detail should show both keys with the value "[REDACTED]".
Event limits
Events are fitted to the server's per-event limits.
Before an event is queued or sent, the SDK fits it to the Logister ingest API's per-event limits so a large crash or context does not cause the server to reject the whole event.
| Limit | Value |
|---|---|
| Object keys, counted at every level (stack frames included) | 512 |
| Estimated size | About 1,000,000 bytes |
| Nesting depth | 16 |
| Array items | 1,000 |
| Message | 64 KiB |
When content has to be cut, the SDK keeps it in this order:
- Required event fields.
- The SDK's own context.
- The top exception's frames.
- ANR diagnostics.
- The newest breadcrumbs.
- Exception causes.
- Caller context: scalars first, then small objects before large ones.
Envelope and event fields never count against the item budget. context has its own key budget, and each context entry has its own nested budget, so a large context cannot push out the event's UUID or evidence. When anything is cut, the event carries context._truncated: true. An oversized envelope is reduced step by step and, as a last resort, keeps only its required fields. A non-finite number does not fail the event.
To lower the key cap below the server's limit, set it on the payload policy:
payloadPolicy(
LogisterPayloadPolicy.builder()
.maxEventKeys(256)
.build()
)Verify it
Open the raw event in the event detail. context._truncated appears only when the SDK had to cut content. If it appears on ordinary events, move large or high-cardinality caller context out of the event.
Offline queue
Crash reports are persisted independently of network delivery.
offlineQueue(enabled = true, ...) stores events in an atomic, app-private file under noBackupFilesDir. It holds at most the configured maxEvents and maxBytes and expires entries after maxAgeDays. The queue is scoped by endpoint, application/service, optional storageNamespace, and process policy. Tokens are never stored.
- Crash persistence. Delivery never holds the queue's storage lock, so an uncaught-exception crash is persisted immediately without waiting for a slow in-flight request. The follow-up delivery runs on a dedicated background thread.
- Retries. Token-provider failures and retryable
408,425,429, and5xxresponses are queued with bounded, jittered exponential backoff andRetry-After. A permanent client rejection is discarded so it cannot block later events. - Queued is not accepted. A queued response has
isQueued == trueandisAccepted == false. It becomes accepted only after a later server response succeeds. - Flushing. Call
flushQueuedEventsAsync()after authentication when the app may not immediately emit another event. - Clearing.
clearSessionBoundQueuedEvents()removes events that containsession_idoruser_idand keeps anonymous automatic crashes; call it on logout or account replacement.clearQueuedEvents()removes everything.
Verify it
Read the LogisterResponse from a test capture off the main thread. isAccepted means the server took the event; isQueued means it is stored for retry. For a queued event, call flushQueuedEventsAsync() once delivery should succeed and confirm the event appears in Issues. healthSnapshot() reports bounded queue, drop, and delivery state while you test.
Verification
Confirm one event from the app before widening instrumentation.
Gradle resolves org.logister:logister-android
client can send one captureMessageAsync call
Events page shows the Android log event
Issues shows a test exception when captureExceptionAsync is called
Test exception shows type and frames, and its message only with FULL
build() succeeds with the production https base URL
A context key such as access_token shows "[REDACTED]"
context._truncated is absent on ordinary events
Responses are isAccepted, or isQueued until a later flush succeeds
Explore › Charts can filter by environment, release, service, or screen_name
Performance shows transactions or spans after timing events arrive
Event detail shows source context when repository and commitSha are configured
Event detail shows deployment context after CI posts /api/v1/deploymentsPackage releases
Android SDK releases are versioned separately from the Rails app.
The Android repository publishes signed artifacts to Maven Central from semantic version tags. The current release path also creates a GitHub release for the same tag. Use the package manager version that matches your app's compatibility needs, and update through Gradle dependency management or Dependabot as new versions are released.
- SDK source: github.com/taimoorq/logister-android
- Maven Central: org.logister:logister-android
- Maven repository path: repo1.maven.org/maven2/org/logister/logister-android
Follow a request into the backend
Logister 3.7+ supports explicit app/backend connections; 3.8 adds captured HTTP outcome, timing, app build and issue ownership to related requests. Use SDK 0.6.0+ for the opt-in HTTP wrapper and origin allowlist. SDK 0.6.1 adds optional method, status code, failure category, attempt and duration-scope metadata. Verify the required release is available in the package channel before upgrading.
HttpURLConnection measures through your response-consumption callback; OkHttp measures through response headers. DNS/connect failures before OkHttp network interception produce one failure record. Later response-body failures and transport retries invisible to the interceptor are outside that span. Client and backend durations measure different work; do not subtract them to estimate network latency. The enrichment does not capture additional request/response bodies or headers, or join user identities across projects.
- Connect the mobile project to its backend with exact environment mappings and enable related requests for both projects and the instance.
- Configure the wrapper with an exact backend-origin allowlist and exclude token-issuer URLs. Keep each project's own release and environment.
- Trigger a synthetic failed call, attach its request handle to the handled error, then inspect Related requests and Inspect connected impact.
Read the SDK wrapper example, connection setup and impact and coverage limits. Installation examples above follow the verified published-version catalog; feature minimum versions may be newer.