
Sentry Android Sdk
- 1.8k installs
- 243 repo stars
- Updated July 27, 2026
- getsentry/sentry-for-ai
sentry-android-sdk is an agent skill for Full Sentry SDK setup for Android. Use when asked to "add Sentry to Android", "install sentry-android", "setup Sentry in
About
The sentry-android-sdk skill Full Sentry SDK setup for Android. Use when asked to "add Sentry to Android", "install sentry-android", "setup Sentry in Android", or configure error monitoring, tracing, profiling, session replay, or logging for Android applications. Supports Kotlin and Java codebases. It covers user asks to "add Sentry to Android" or "set up Sentry" in an Android app. Key workflows include user wants error monitoring, crash reporting, ANR detection, tracing, profiling, session replay, or logging in Android. Developers invoke sentry-android-sdk when the task matches the triggers and reference files in SKILL.md for grounded, stepwise execution. Reference files and progressive disclosure keep context focused while preserving concrete commands, configuration fields, and validation checks copied from the upstream documentation. Reference files and progressive disclosure keep context focused while preserving concrete commands, configuration fields, and validation checks copied from the upstream documentation.
- User asks to "add Sentry to Android" or "set up Sentry" in an Android app
- User wants error monitoring, crash reporting, ANR detection, tracing, profiling, session replay, or logging in Android
- User mentions sentry-android , io.sentry:sentry-android , mobile crash tracking, or Sentry for Kotlin/Java Android
- User wants to monitor native NDK crashes, application not responding ANR events, or app startup performance
- ✅ Error Monitoring - captures uncaught exceptions, ANRs, and native NDK crashes automatically
Sentry Android Sdk by the numbers
- 1,823 all-time installs (skills.sh)
- +44 installs in the week ending Jul 28, 2026 (Skillselion tracking)
- Ranked #403 of 2,184 Testing & QA skills by installs in the Skillselion catalog
- Security screen: HIGH risk (skills.sh audit)
- Data as of Jul 28, 2026 (Skillselion catalog sync)
sentry-android-sdk capabilities & compatibility
- Capabilities
- user asks to "add sentry to android" or "set up · user wants error monitoring, crash reporting, an · user mentions sentry android , io.sentry:sentry · user wants to monitor native ndk crashes, applic · ✅ error monitoring captures uncaught exception
- Use cases
- documentation
npx skills add https://github.com/getsentry/sentry-for-ai --skill sentry-android-sdkAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1.8k |
|---|---|
| repo stars | ★ 243 |
| Security audit | 3 / 3 scanners passed |
| Last updated | July 27, 2026 |
| Repository | getsentry/sentry-for-ai ↗ |
What problem does sentry-android-sdk solve for developers using the documented workflows?
Full Sentry SDK setup for Android. Use when asked to "add Sentry to Android", "install sentry-android", "setup Sentry in Android", or configure error monitoring, tracing, profiling, session replay, or
Who is it for?
Developers working with sentry-android-sdk patterns described in the skill documentation.
Skip if: Skip when docs are empty or the task is outside the skill documented scope.
When should I use this skill?
Use when Full Sentry SDK setup for Android. Use when asked to "add Sentry to Android", "install sentry-android", "setup Sentry in Android", or configure error monitoring, tracing, profiling
What you get
Actionable sentry-android-sdk guidance grounded in SKILL.md workflows and reference files.
- Sentry Crons check-in integration
- job monitoring alert configuration
By the numbers
- Supports 3 check-in statuses: IN_PROGRESS, OK, and ERROR
Files
All Skills > SDK Setup > Android SDK
Sentry Android SDK
Opinionated wizard that scans your Android project and guides you through complete Sentry setup — error monitoring, tracing, profiling, session replay, logging, and more.
Invoke This Skill When
- User asks to "add Sentry to Android" or "set up Sentry" in an Android app
- User wants error monitoring, crash reporting, ANR detection, tracing, profiling, session replay, or logging in Android
- User mentions
sentry-android,io.sentry:sentry-android, mobile crash tracking, or Sentry for Kotlin/Java Android - User wants to monitor native (NDK) crashes, application not responding (ANR) events, or app startup performance
Note: SDK versions and APIs below reflect current Sentry docs at time of writing (io.sentry:sentry-android:8.33.0, Gradle plugin6.1.0).
Always verify against docs.sentry.io/platforms/android/ before implementing.
---
Phase 1: Detect
Run these commands to understand the project before making any recommendations:
# Detect project structure and build system
ls build.gradle build.gradle.kts settings.gradle settings.gradle.kts 2>/dev/null
# Check AGP version and existing Sentry
grep -r '"com.android.application"' build.gradle* app/build.gradle* 2>/dev/null | head -3
grep -ri sentry build.gradle* app/build.gradle* 2>/dev/null | head -10
# Check app-level build file (Groovy vs KTS)
ls app/build.gradle app/build.gradle.kts 2>/dev/null
# Detect Gradle version catalog (libs.versions.toml) — modern AGP projects
ls gradle/libs.versions.toml 2>/dev/null
# Check for existing Sentry entries in the version catalog
grep -iE 'sentry|io\.sentry' gradle/libs.versions.toml 2>/dev/null | head -10
# Check if build files reference the catalog (alias/libs.* usage)
grep -E 'alias\(libs\.|libs\.[a-zA-Z]' build.gradle build.gradle.kts app/build.gradle app/build.gradle.kts 2>/dev/null | head -5
# Detect Kotlin vs Java
find app/src/main -name "*.kt" 2>/dev/null | head -3
find app/src/main -name "*.java" 2>/dev/null | head -3
# Check minSdk, targetSdk
grep -E 'minSdk|targetSdk|compileSdk|minSdkVersion|targetSdkVersion' app/build.gradle app/build.gradle.kts 2>/dev/null | head -6
# Detect Jetpack Compose
grep -E 'compose|androidx.compose' app/build.gradle app/build.gradle.kts 2>/dev/null | head -5
# Detect OkHttp (popular HTTP client — has dedicated integration)
grep -E 'okhttp|retrofit' app/build.gradle app/build.gradle.kts 2>/dev/null | head -3
# Detect Room or SQLite
grep -E 'androidx.room|androidx.sqlite' app/build.gradle app/build.gradle.kts 2>/dev/null | head -3
# Detect Timber (logging library)
grep -E 'timber' app/build.gradle app/build.gradle.kts 2>/dev/null | head -3
# Detect Jetpack Navigation
grep -E 'androidx.navigation' app/build.gradle app/build.gradle.kts 2>/dev/null | head -3
# Detect Apollo (GraphQL)
grep -E 'apollo' app/build.gradle app/build.gradle.kts 2>/dev/null | head -3
# Check existing Sentry initialization
grep -r "SentryAndroid.init\|io.sentry.Sentry" app/src/ 2>/dev/null | head -5
# Check Application class
find app/src/main -name "*.kt" -o -name "*.java" 2>/dev/null | xargs grep -l "Application()" 2>/dev/null | head -3
# Adjacent backend (for cross-linking)
ls ../backend ../server ../api 2>/dev/null
find .. -maxdepth 2 \( -name "go.mod" -o -name "requirements.txt" -o -name "Gemfile" \) 2>/dev/null | grep -v node_modules | head -5What to determine:
| Question | Impact |
|---|---|
build.gradle.kts present? | Use Kotlin DSL syntax in all examples |
gradle/libs.versions.toml present? | Add Sentry to the version catalog; reference via libs.* in build files |
Catalog already has sentry entries? | Reuse the existing version ref; don't duplicate or hardcode versions |
minSdk < 26? | Note Session Replay requires API 26+ — silent no-op below that |
| Compose detected? | Recommend sentry-compose-android and Compose-specific masking |
| OkHttp present? | Recommend sentry-okhttp interceptor or Gradle plugin bytecode auto-instrumentation |
| Room/SQLite present? | Recommend sentry-android-sqlite or plugin bytecode instrumentation |
| Timber present? | Recommend sentry-android-timber integration |
| Jetpack Navigation? | Recommend sentry-android-navigation for screen tracking |
Already has SentryAndroid.init()? | Skip install, jump to feature config |
| Application subclass exists? | That's where SentryAndroid.init() goes |
---
Phase 2: Recommend
Present a concrete recommendation based on what you found. Don't ask open-ended questions — lead with a proposal:
Recommended (core coverage — always set up these):
- ✅ Error Monitoring — captures uncaught exceptions, ANRs, and native NDK crashes automatically
- ✅ Tracing — auto-instruments Activity lifecycle, app start, HTTP requests, and database queries
- ✅ Session Replay — records screen captures and user interactions for debugging (API 26+)
Optional (enhanced observability):
- ⚡ Profiling — continuous UI profiling (recommended) or transaction-based sampling
- ⚡ Logging — structured logs via
Sentry.logger(), with optional Timber bridge - ⚡ User Feedback — collect user-submitted bug reports from inside the app
Recommendation logic:
| Feature | Recommend when... |
|---|---|
| Error Monitoring | Always — non-negotiable baseline for any Android app |
| Tracing | Always for Android — app start time, Activity lifecycle, network latency matter |
| Session Replay | User-facing production app on API 26+; visual debugging of user issues |
| Profiling | Performance-sensitive apps, startup time investigations, production perf analysis |
| Logging | App uses structured logging or you want log-to-trace correlation in Sentry |
| User Feedback | Beta or customer-facing app where you want user-submitted bug reports |
Propose: "For your [Kotlin / Java] Android app (minSdk X), I recommend setting up Error Monitoring + Tracing + Session Replay. Want me to also add Profiling and Logging?"
---
Phase 3: Guide
Determine Your Setup Path
| Project type | Recommended setup | Complexity |
|---|---|---|
| New project, no existing Sentry | Gradle plugin (recommended) | Low — plugin handles most config |
| Existing project, no Sentry | Gradle plugin or manual init | Medium — add dependency + Application class |
| Manual full control | SentryAndroid.init() in Application | Medium — explicit config, most flexible |
Option 1: Wizard (Recommended)
You need to run this yourself — the wizard opens a browser for login
and requires interactive input that the agent can't handle.
Copy-paste into your terminal:
>
```
npx @sentry/wizard@latest -i android
```
>
It handles login, org/project selection, Gradle plugin setup, dependency
installation, DSN configuration, and ProGuard/R8 mapping upload.
>
Once it finishes, come back and skip to [Verification](#verification).
If the user skips the wizard, proceed with Option 2 (Manual Setup) below.
---
Option 2: Manual Setup
Using a Gradle Version Catalog (gradle/libs.versions.toml)
If Phase 1 detected gradle/libs.versions.toml, add Sentry to the catalog first, then reference it from your build files. This keeps versions centralized and matches modern AGP project conventions.
Step 1 — Add entries to `gradle/libs.versions.toml`
[versions]
sentry = "8.33.0"
sentryGradlePlugin = "6.1.0"
[libraries]
sentry-android = { module = "io.sentry:sentry-android", version.ref = "sentry" }
sentry-bom = { module = "io.sentry:sentry-bom", version.ref = "sentry" }
# Optional integrations — add only the ones your project uses:
sentry-android-timber = { module = "io.sentry:sentry-android-timber" }
sentry-android-fragment = { module = "io.sentry:sentry-android-fragment" }
sentry-compose-android = { module = "io.sentry:sentry-compose-android" }
sentry-android-navigation = { module = "io.sentry:sentry-android-navigation" }
sentry-okhttp = { module = "io.sentry:sentry-okhttp" }
sentry-android-sqlite = { module = "io.sentry:sentry-android-sqlite" }
sentry-kotlin-extensions = { module = "io.sentry:sentry-kotlin-extensions" }
[plugins]
sentry-android-gradle = { id = "io.sentry.android.gradle", version.ref = "sentryGradlePlugin" }Note: Optional integration entries omitversion.ref— their versions come from the BOM at resolution time. Onlysentry-bomneeds the version ref.
If the catalog already defines a sentry version, reuse it instead of adding a duplicate entry.Step 2 — Reference the catalog from `build.gradle[.kts]`
Project-level build.gradle.kts:
plugins {
alias(libs.plugins.sentry.android.gradle) apply false
}App-level app/build.gradle.kts:
plugins {
id("com.android.application")
alias(libs.plugins.sentry.android.gradle)
}
dependencies {
implementation(platform(libs.sentry.bom))
implementation(libs.sentry.android)
// implementation(libs.sentry.okhttp)
// implementation(libs.sentry.compose.android)
}Groovy DSL (app/build.gradle) equivalent:
plugins {
id "com.android.application"
alias libs.plugins.sentry.android.gradle
}
dependencies {
implementation platform(libs.sentry.bom)
implementation libs.sentry.android
}Then continue with the sentry {} configuration block from Path A, Step 2 below. The rest of the setup (Application class init, manifest registration, verification) is identical.
---
Path A: Gradle Plugin (Recommended)
The Sentry Gradle plugin is the easiest setup path. It:
- Uploads ProGuard/R8 mapping files automatically on release builds
- Injects source context into stack frames
- Optionally instruments OkHttp, Room/SQLite, File I/O, Compose navigation, and
android.util.Logvia bytecode transforms (zero source changes)
Step 1 — Add the plugin to `build.gradle[.kts]` (project-level)
Groovy DSL (build.gradle):
plugins {
id "io.sentry.android.gradle" version "6.1.0" apply false
}Kotlin DSL (build.gradle.kts):
plugins {
id("io.sentry.android.gradle") version "6.1.0" apply false
}Step 2 — Apply plugin + add dependencies in `app/build.gradle[.kts]`
Groovy DSL:
plugins {
id "com.android.application"
id "io.sentry.android.gradle"
}
android {
// ...
}
dependencies {
// Use BOM for consistent versions across sentry modules
implementation platform("io.sentry:sentry-bom:8.33.0")
implementation "io.sentry:sentry-android"
// Optional integrations (add what's relevant):
// implementation "io.sentry:sentry-android-timber" // Timber bridge
// implementation "io.sentry:sentry-android-fragment" // Fragment lifecycle tracing
// implementation "io.sentry:sentry-compose-android" // Jetpack Compose support
// implementation "io.sentry:sentry-android-navigation" // Jetpack Navigation
// implementation "io.sentry:sentry-okhttp" // OkHttp interceptor
// implementation "io.sentry:sentry-android-sqlite" // Room/SQLite tracing
// implementation "io.sentry:sentry-kotlin-extensions" // Coroutine context propagation
}
sentry {
org = "YOUR_ORG_SLUG"
projectName = "YOUR_PROJECT_SLUG"
authToken = System.getenv("SENTRY_AUTH_TOKEN")
// Enable auto-instrumentation via bytecode transforms (no source changes needed)
tracingInstrumentation {
enabled = true
features = [InstrumentationFeature.DATABASE, InstrumentationFeature.FILE_IO,
InstrumentationFeature.OKHTTP, InstrumentationFeature.COMPOSE]
}
// Upload ProGuard mapping and source context on release
autoUploadProguardMapping = true
includeSourceContext = true
}Kotlin DSL (app/build.gradle.kts):
plugins {
id("com.android.application")
id("io.sentry.android.gradle")
}
dependencies {
implementation(platform("io.sentry:sentry-bom:8.33.0"))
implementation("io.sentry:sentry-android")
// Optional integrations:
// implementation("io.sentry:sentry-android-timber")
// implementation("io.sentry:sentry-android-fragment")
// implementation("io.sentry:sentry-compose-android")
// implementation("io.sentry:sentry-android-navigation")
// implementation("io.sentry:sentry-okhttp")
// implementation("io.sentry:sentry-android-sqlite")
// implementation("io.sentry:sentry-kotlin-extensions")
}
sentry {
org = "YOUR_ORG_SLUG"
projectName = "YOUR_PROJECT_SLUG"
authToken = System.getenv("SENTRY_AUTH_TOKEN")
tracingInstrumentation {
enabled = true
features = setOf(
InstrumentationFeature.DATABASE,
InstrumentationFeature.FILE_IO,
InstrumentationFeature.OKHTTP,
InstrumentationFeature.COMPOSE,
)
}
autoUploadProguardMapping = true
includeSourceContext = true
}Step 3 — Initialize Sentry in your Application class
If you don't have an Application subclass, create one:
// MyApplication.kt
import android.app.Application
import io.sentry.SentryLevel
import io.sentry.android.core.SentryAndroid
import io.sentry.android.replay.SentryReplayOptions
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
SentryAndroid.init(this) { options ->
options.dsn = "YOUR_SENTRY_DSN"
// Tracing — lower to 0.1–0.2 in high-traffic production
options.tracesSampleRate = 1.0
// Profiling — use continuous UI profiling (recommended, SDK ≥ 8.7.0)
options.profileSessionSampleRate = 1.0
// Session Replay (API 26+ only; silent no-op below API 26)
options.sessionReplay.sessionSampleRate = 0.1 // 10% of all sessions
options.sessionReplay.onErrorSampleRate = 1.0 // 100% on error
// Structured logging
options.logs.isEnabled = true
// Environment
options.environment = BuildConfig.BUILD_TYPE
}
}
}Java equivalent:
// MyApplication.java
import android.app.Application;
import io.sentry.android.core.SentryAndroid;
public class MyApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
SentryAndroid.init(this, options -> {
options.setDsn("YOUR_SENTRY_DSN");
options.setTracesSampleRate(1.0);
options.setProfileSessionSampleRate(1.0);
options.getSessionReplay().setSessionSampleRate(0.1);
options.getSessionReplay().setOnErrorSampleRate(1.0);
options.getLogs().setEnabled(true);
options.setEnvironment(BuildConfig.BUILD_TYPE);
});
}
}Step 4 — Register Application in `AndroidManifest.xml`
<application
android:name=".MyApplication"
... >---
Path B: Manual Setup (No Gradle Plugin)
Use this if you can't use the Gradle plugin (e.g., non-standard build setups).
Step 1 — Add dependency in `app/build.gradle[.kts]`
dependencies {
implementation(platform("io.sentry:sentry-bom:8.33.0"))
implementation("io.sentry:sentry-android")
}Step 2 — Initialize in Application class (same as Path A, Step 3)
Step 3 — Configure ProGuard/R8 manually
The Sentry SDK ships a ProGuard rules file automatically. For manual mapping upload, install sentry-cli and add to your CI:
sentry-cli releases files "my-app@1.0.0+42" upload-proguard \
--org YOUR_ORG --project YOUR_PROJECT \
app/build/outputs/mapping/release/mapping.txt---
Quick Reference: Full-Featured SentryAndroid.init()
SentryAndroid.init(this) { options ->
options.dsn = "YOUR_SENTRY_DSN"
// Environment and release
options.environment = BuildConfig.BUILD_TYPE // "debug", "release", etc.
options.release = "${BuildConfig.APPLICATION_ID}@${BuildConfig.VERSION_NAME}+${BuildConfig.VERSION_CODE}"
// Tracing — sample 100% in dev, lower to 10–20% in production
options.tracesSampleRate = 1.0
// Continuous UI profiling (recommended over transaction-based)
options.profileSessionSampleRate = 1.0
// Session Replay (API 26+; silent no-op on API 21–25)
options.sessionReplay.sessionSampleRate = 0.1
options.sessionReplay.onErrorSampleRate = 1.0
options.sessionReplay.maskAllText = true // mask text for privacy
options.sessionReplay.maskAllImages = true // mask images for privacy
// Structured logging
options.logs.isEnabled = true
// Error enrichment
options.isAttachScreenshot = true // capture screenshot on error
options.isAttachViewHierarchy = true // attach view hierarchy JSON
// ANR detection (5s default; watchdog + ApplicationExitInfo API 30+)
options.isAnrEnabled = true
// NDK native crash handling (enabled by default)
options.isEnableNdk = true
// Send PII: IP address, user data
options.sendDefaultPii = true
// Trace propagation (backend distributed tracing)
options.tracePropagationTargets = listOf("api.yourapp.com", ".*\\.yourapp\\.com")
// Verbose logging — disable in production
options.isDebug = BuildConfig.DEBUG
}---
For Each Agreed Feature
Walk through features one at a time. Load the reference file for each, follow its steps, then verify before moving on:
| Feature | Reference | Load when... |
|---|---|---|
| Error Monitoring | ${SKILL_ROOT}/references/error-monitoring.md | Always (baseline) |
| Tracing & Performance | ${SKILL_ROOT}/references/tracing.md | Always for Android (Activity lifecycle, network) |
| Profiling | ${SKILL_ROOT}/references/profiling.md | Performance-sensitive production apps |
| Session Replay | ${SKILL_ROOT}/references/session-replay.md | User-facing apps (API 26+) |
| Logging | ${SKILL_ROOT}/references/logging.md | Structured logging / log-to-trace correlation |
| Metrics | ${SKILL_ROOT}/references/metrics.md | Custom metric tracking (SDK ≥ 8.30.0) |
| Crons | ${SKILL_ROOT}/references/crons.md | Scheduled jobs, WorkManager check-ins |
For each feature: Read ${SKILL_ROOT}/references/<feature>.md, follow steps exactly, verify it works.
---
Integration Reference
Built-in (Auto-Enabled)
These integrations activate automatically when SentryAndroid.init() is called:
| Integration | What it does |
|---|---|
UncaughtExceptionHandlerIntegration | Captures all uncaught Java/Kotlin exceptions |
AnrIntegration | ANR detection via watchdog thread (5s) + ApplicationExitInfo (API 30+) |
NdkIntegration | Native (C/C++) crash capture via sentry-native |
ActivityLifecycleIntegration | Auto-instruments Activity create/resume/pause for TTID/TTFD |
AppStartMetrics | Measures cold/warm/hot app start time |
NetworkBreadcrumbsIntegration | Records connectivity changes as breadcrumbs |
SystemEventsBreadcrumbsIntegration | Records battery, screen on/off, etc. |
AppLifecycleIntegration | Records foreground/background transitions |
UserInteractionIntegration | Breadcrumbs for taps, swipes, input events |
CurrentActivityIntegration | Tracks active Activity for context |
Optional Integrations
Add the artifact to your dependencies {} block (versions managed by BOM):
| Integration | Artifact | When to add |
|---|---|---|
| Timber | io.sentry:sentry-android-timber | App uses Timber for logging |
| Fragment | io.sentry:sentry-android-fragment | App uses Jetpack Fragments (lifecycle tracing) |
| Compose | io.sentry:sentry-compose-android | App uses Jetpack Compose (navigation + masking) |
| Navigation | io.sentry:sentry-android-navigation | App uses Jetpack Navigation Component |
| OkHttp | io.sentry:sentry-okhttp | App uses OkHttp or Retrofit |
| Room/SQLite | io.sentry:sentry-android-sqlite | App uses Room or raw SQLite |
| Apollo 3 | io.sentry:sentry-apollo-3 | App uses Apollo GraphQL v3 |
| Apollo 4 | io.sentry:sentry-apollo-4 | App uses Apollo GraphQL v4 |
| Kotlin Extensions | io.sentry:sentry-kotlin-extensions | Kotlin coroutines context propagation |
| Ktor Client | io.sentry:sentry-ktor-client | App uses Ktor HTTP client |
| LaunchDarkly | io.sentry:sentry-launchdarkly-android | App uses LaunchDarkly feature flags |
Gradle Plugin Bytecode Instrumentation
The plugin can inject instrumentation automatically (no source changes):
| Feature | Instruments | Enable via |
|---|---|---|
DATABASE | Room DAO, SupportSQLiteOpenHelper | tracingInstrumentation.features |
FILE_IO | FileInputStream, FileOutputStream | tracingInstrumentation.features |
OKHTTP | OkHttpClient.Builder automatically | tracingInstrumentation.features |
COMPOSE | NavHostController auto-instrumentation | tracingInstrumentation.features |
LOGCAT | android.util.Log capturing | tracingInstrumentation.features |
---
Configuration Reference
Core SentryOptions (via SentryAndroid.init)
| Option | Type | Default | Purpose |
|---|---|---|---|
dsn | String | — | Required. Project DSN; SDK silently disabled if empty |
environment | String | — | e.g., "production", "staging". Env: SENTRY_ENVIRONMENT |
release | String | — | App version, e.g., "my-app@1.0.0+42". Env: SENTRY_RELEASE |
dist | String | — | Build variant / distribution identifier |
sendDefaultPii | Boolean | false | Include PII: IP address, user data |
sampleRate | Double | 1.0 | Error event sampling (0.0–1.0) |
maxBreadcrumbs | Int | 100 | Max breadcrumbs per event |
isAttachStacktrace | Boolean | true | Auto-attach stack traces to message events |
isAttachScreenshot | Boolean | false | Capture screenshot on error |
isAttachViewHierarchy | Boolean | false | Attach JSON view hierarchy as attachment |
isDebug | Boolean | false | Verbose SDK output. Never use in production |
isEnabled | Boolean | true | Disable SDK entirely (e.g., for testing) |
beforeSend | SentryOptions.BeforeSendCallback | — | Modify or drop error events before sending |
beforeBreadcrumb | SentryOptions.BeforeBreadcrumbCallback | — | Filter breadcrumbs before storage |
Tracing Options
| Option | Type | Default | Purpose |
|---|---|---|---|
tracesSampleRate | Double | 0.0 | Transaction sample rate (0–1). Use 1.0 in dev |
tracesSampler | TracesSamplerCallback | — | Per-transaction sampling; overrides tracesSampleRate |
tracePropagationTargets | List<String> | [".*"] | Hosts/URLs to receive sentry-trace and baggage headers |
isEnableAutoActivityLifecycleTracing | Boolean | true | Auto-instrument Activity lifecycle |
isEnableTimeToFullDisplayTracing | Boolean | false | TTFD spans (requires Sentry.reportFullyDisplayed()) |
isEnableUserInteractionTracing | Boolean | false | Auto-instrument user gestures as transactions |
Profiling Options
| Option | Type | Default | Purpose |
|---|---|---|---|
profileSessionSampleRate | Double | 0.0 | Continuous profiling sample rate (SDK ≥ 8.7.0, API 22+) |
profilesSampleRate | Double | 0.0 | Legacy transaction profiling rate (mutually exclusive with continuous) |
isProfilingStartOnAppStart | Boolean | false | Auto-start profiling session on app launch |
ANR Options
| Option | Type | Default | Purpose |
|---|---|---|---|
isAnrEnabled | Boolean | true | Enable ANR watchdog thread |
anrTimeoutIntervalMillis | Long | 5000 | Milliseconds before reporting ANR |
isAnrReportInDebug | Boolean | false | Report ANRs in debug builds (noisy in debugger) |
NDK Options
| Option | Type | Default | Purpose |
|---|---|---|---|
isEnableNdk | Boolean | true | Enable native crash capture via sentry-native |
isEnableScopeSync | Boolean | true | Sync Java scope (user, tags) to NDK layer |
isEnableTombstoneFetchJob | Boolean | true | Fetch NDK tombstone files for enrichment |
Session Replay Options (options.sessionReplay)
| Option | Type | Default | Purpose |
|---|---|---|---|
sessionSampleRate | Double | 0.0 | Fraction of all sessions to record |
onErrorSampleRate | Double | 0.0 | Fraction of error sessions to record |
maskAllText | Boolean | true | Mask all text in replays |
maskAllImages | Boolean | true | Mask all images in replays |
quality | SentryReplayQuality | MEDIUM | Video quality: LOW, MEDIUM, HIGH |
Logging Options (options.logs)
| Option | Type | Default | Purpose |
|---|---|---|---|
isEnabled | Boolean | false | Enable Sentry.logger() API (SDK ≥ 8.12.0) |
setBeforeSend | BeforeSendLogCallback | — | Filter/modify log entries before sending |
---
Environment Variables
| Variable | Purpose | Notes |
|---|---|---|
SENTRY_DSN | Data Source Name | Set in CI; SDK reads from environment at init |
SENTRY_AUTH_TOKEN | Upload ProGuard mappings and source context | Never commit — use CI/CD secrets |
SENTRY_ORG | Organization slug | Used by Gradle plugin sentry.org |
SENTRY_PROJECT | Project slug | Used by Gradle plugin sentry.projectName |
SENTRY_RELEASE | Release identifier | Falls back from options.release |
SENTRY_ENVIRONMENT | Environment name | Falls back from options.environment |
You can also configure DSN and many options via AndroidManifest.xml meta-data:
<application>
<meta-data android:name="io.sentry.dsn" android:value="YOUR_DSN" />
<meta-data android:name="io.sentry.traces-sample-rate" android:value="1.0" />
<meta-data android:name="io.sentry.environment" android:value="production" />
<meta-data android:name="io.sentry.anr.enable" android:value="true" />
<meta-data android:name="io.sentry.attach-screenshot" android:value="true" />
<meta-data android:name="io.sentry.attach-view-hierarchy" android:value="true" />
</application>⚠️ Manifest meta-data is a convenient alternative but does not support the full option set. For complex configuration (session replay, profiling, hooks), use SentryAndroid.init().---
Verification
After setup, verify Sentry is receiving events:
Test error capture:
// In an Activity or Fragment
try {
throw RuntimeException("Sentry Android SDK test")
} catch (e: Exception) {
Sentry.captureException(e)
}Test tracing:
val transaction = Sentry.startTransaction("test-task", "task")
val span = transaction.startChild("test-span", "description")
span.finish()
transaction.finish()Test structured logging (SDK ≥ 8.12.0):
Sentry.logger().info("Sentry logging test")
Sentry.logger().error("Error log test", Exception("test error"))Check the Sentry dashboard:
- Issues → your test exception should appear within seconds
- Traces → look for
test-tasktransaction with child span - Replays → session recording visible after app interaction (requires API 26+)
- Logs → structured log entries visible under Logs tab
If nothing appears: 1. Set options.isDebug = true — SDK logs to Logcat 2. Verify DSN is correct and matches your Sentry project 3. Check that your Application class is registered in AndroidManifest.xml as android:name 4. Confirm the device/emulator has internet connectivity 5. For NDK crashes, ensure isEnableNdk = true (default) and build with NDK support
---
Phase 4: Cross-Link
After completing Android setup, check for a backend or web frontend missing Sentry coverage:
# Adjacent backend directories
ls ../backend ../server ../api 2>/dev/null
cat ../go.mod 2>/dev/null | head -3
cat ../requirements.txt ../pyproject.toml 2>/dev/null | head -5
cat ../Gemfile 2>/dev/null | head -3
ls ../backend/package.json ../server/package.json 2>/dev/null
# iOS counterpart app
ls ../ios ../YourApp-iOS 2>/dev/null
find .. -maxdepth 3 -name "*.xcodeproj" 2>/dev/null | head -3If a backend or related platform exists without Sentry, suggest the matching skill:
| Detected | Suggest skill |
|---|---|
Go backend (go.mod) | sentry-go-sdk |
Python backend (requirements.txt, pyproject.toml) | sentry-python-sdk |
Ruby backend (Gemfile) | sentry-ruby-sdk |
| Node.js backend | @sentry/node — see docs.sentry.io/platforms/javascript/guides/express/ |
iOS app (.xcodeproj) | sentry-cocoa-sdk |
React Native (package.json with react-native) | sentry-react-native-sdk |
| React / Next.js web | sentry-react-sdk or sentry-nextjs-sdk |
Distributed tracing setup — if the backend skill is added, configure tracePropagationTargets in Android to propagate trace context to your API:
options.tracePropagationTargets = listOf(
"api.yourapp.com",
".*\\.yourapp\\.com"
)This links mobile transactions to backend traces in the Sentry waterfall view.
---
Troubleshooting
| Issue | Solution |
|---|---|
| Events not appearing in Sentry | Set isDebug = true, check Logcat for SDK errors; verify DSN is correct and matches your project |
SentryAndroid.init() not called | Confirm android:name=".MyApplication" is set in AndroidManifest.xml; Application class not abstract |
| Gradle plugin not found | Add the plugin to project-level build.gradle.kts first, then apply false; verify version 6.1.0 |
| ProGuard mapping not uploading | Set SENTRY_AUTH_TOKEN env var; ensure autoUploadProguardMapping = true in sentry {} block |
| NDK crashes not captured | Verify isEnableNdk = true (default); ensure project has NDK configured in android.ndkVersion |
| ANR reported in debugger | Set isAnrReportInDebug = false (default); ANR watchdog fires when debugger pauses threads |
| Session replay not recording | Requires API 26+; verify sessionSampleRate > 0 or onErrorSampleRate > 0; check Logcat for replay errors |
| Session replay shows blank screen | PixelCopy (default) requires hardware acceleration; try SentryReplayOptions.screenshotQuality = CANVAS |
| Replay masking misaligned | Views with translationX/Y or clipToPadding=false can offset masks; report to github.com/getsentry/sentry-java |
beforeSend not firing | beforeSend only intercepts managed (Java/Kotlin) events; NDK native crashes bypass it |
| OkHttp spans not appearing | Add SentryOkHttpInterceptor to your OkHttpClient, or use Gradle plugin OKHTTP bytecode instrumentation |
| Spans not attached to transaction | Ensure TransactionOptions().setBindToScope(true) when starting transaction; child spans look for scope root |
| Tracing not recording | Verify tracesSampleRate > 0; Activity instrumentation requires isEnableAutoActivityLifecycleTracing = true (default) |
| Continuous profiling not working | SDK ≥ 8.7.0 required; API 22+ required; set profileSessionSampleRate > 0; don't also set profilesSampleRate |
| Both profiling modes set | profilesSampleRate and profileSessionSampleRate are mutually exclusive — use only one |
| TTFD spans missing | Set isEnableTimeToFullDisplayTracing = true and call Sentry.reportFullyDisplayed() when screen is ready |
| Kotlin coroutine scope lost | Add sentry-kotlin-extensions dependency; use Sentry.cloneMainContext() to propagate trace context |
| Release build stack traces unreadable | ProGuard mapping not uploaded; confirm Gradle plugin autoUploadProguardMapping = true and auth token set |
| Source context not showing in Sentry | Enable includeSourceContext = true in sentry {} block (Gradle plugin required) |
| BOM version conflict | Use implementation(platform("io.sentry:sentry-bom:8.33.0")) and omit versions from all other io.sentry:* entries |
| Version catalog alias unresolved | After editing gradle/libs.versions.toml, sync Gradle; alias names use - in TOML and . in build files (e.g., sentry-android → libs.sentry.android) |
| Duplicate Sentry version in catalog | Reuse the existing [versions] sentry = "..." entry; don't add a second key, and don't hardcode the version in build.gradle when the catalog is in use |
SENTRY_AUTH_TOKEN exposed | Auth token is build-time only — never pass it to SentryAndroid.init() or embed in the APK |
Crons / Monitors — Sentry Android SDK
Minimum SDK: io.sentry:sentry-android (any version — uses core Java SDK APIs)Status: Stable (Sentry.captureCheckIn());CheckInUtilsis@ApiStatus.Experimental
Docs: https://docs.sentry.io/platforms/java/crons/ (Android-specific page returns 404)
---
Overview
Sentry Crons lets you monitor scheduled jobs, background sync workers, and periodic tasks. A check-in signals to Sentry that a job started (IN_PROGRESS) and completed (OK) or failed (ERROR). If a check-in is missed or overdue, Sentry fires an alert.
Key facts for Android:
- Uses the core Java SDK — no Android-specific integration or module exists
- No
SentryWorker, no WorkManager adapter, no AlarmManager wrapper — all wiring is manual durationis in seconds as a Double, not millisecondsCheckInUtilshelper is@ApiStatus.Experimental(API may change between releases)- Rate limit: 6 check-ins per minute per monitor per environment
---
Pattern A — CheckInUtils.withCheckIn() (Recommended)
CheckInUtils is the simplest API. It sends IN_PROGRESS on entry, automatically calculates duration, and sends OK on success or ERROR on exception.
Note:CheckInUtilscarries@ApiStatus.Experimental. Prefer Pattern B for production code where you need stable guarantees.
import io.sentry.CheckInUtils
import io.sentry.MonitorConfig
import io.sentry.MonitorSchedule
// Minimal — slug only
CheckInUtils.withCheckIn("nightly-sync") {
performNightlySync()
}
// With upsert config — creates or updates the monitor automatically
val monitorConfig = MonitorConfig(MonitorSchedule.crontab("0 2 * * *")).apply {
setCheckinMargin(5L) // alert if check-in is 5+ minutes late
setMaxRuntime(60L) // alert if running for 60+ minutes
setTimezone("UTC")
}
CheckInUtils.withCheckIn("nightly-sync", "production", monitorConfig) {
performNightlySync()
}Full signature:
CheckInUtils.withCheckIn(
monitorSlug: String, // required — must match slug in Sentry dashboard
environment: String?, // null → uses SDK default environment
monitorConfig: MonitorConfig?, // null → no upsert; monitor must exist in dashboard
callable: Callable<T>
): T---
Pattern B — Manual Two-Step (Full Control)
Send IN_PROGRESS before the job starts, then OK or ERROR when it finishes. This gives you full control over duration and error handling.
import io.sentry.CheckIn
import io.sentry.CheckInStatus
import io.sentry.MonitorConfig
import io.sentry.MonitorSchedule
import io.sentry.Sentry
fun runDatabaseBackup() {
val startedAt = SystemClock.elapsedRealtime()
// 1. Signal that the job has started
val startCheckIn = CheckIn("db-backup", CheckInStatus.IN_PROGRESS)
val checkInId = Sentry.captureCheckIn(startCheckIn)
val status: CheckInStatus
try {
performBackup()
status = CheckInStatus.OK
} catch (e: Exception) {
Sentry.captureException(e) // also capture the error for Sentry Issues
status = CheckInStatus.ERROR
}
// 2. Signal completion — link via checkInId
val elapsed = SystemClock.elapsedRealtime() - startedAt
val done = CheckIn(checkInId, "db-backup", status).apply {
// ⚠️ Duration is in SECONDS (Double), not milliseconds
setDuration(elapsed / 1000.0)
setMonitorConfig(
MonitorConfig(MonitorSchedule.crontab("0 3 * * *")).apply {
setTimezone("UTC")
setMaxRuntime(45L)
setCheckinMargin(10L)
}
)
}
Sentry.captureCheckIn(done)
}⚠️ Duration is in SECONDS, not milliseconds. Always divide elapsed milliseconds by 1000.0:```kotlin
setDuration(SystemClock.elapsedRealtime() - startMs) / 1000.0) // CORRECT
setDuration(SystemClock.elapsedRealtime() - startMs) // WRONG — 1000× too large
```
---
Pattern C — Heartbeat (Simplest)
Use when you only need to detect missed schedules — not overruns. No IN_PROGRESS check-in, just a success or failure on completion.
fun runHeartbeatJob() {
try {
performWork()
Sentry.captureCheckIn(CheckIn("heartbeat-job", CheckInStatus.OK))
} catch (e: Exception) {
Sentry.captureCheckIn(CheckIn("heartbeat-job", CheckInStatus.ERROR))
throw e
}
}---
Pattern D — WorkManager (Manual Wiring)
WorkManager is the recommended Android API for deferrable background work. There is no SDK adapter — wire check-ins manually:
import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters
class SyncWorker(ctx: Context, params: WorkerParameters) : CoroutineWorker(ctx, params) {
override suspend fun doWork(): Result {
val t0 = SystemClock.elapsedRealtime()
val start = CheckIn("workmanager-sync", CheckInStatus.IN_PROGRESS)
val id = Sentry.captureCheckIn(start)
return try {
performSync()
Sentry.captureCheckIn(
CheckIn(id, "workmanager-sync", CheckInStatus.OK).apply {
setDuration((SystemClock.elapsedRealtime() - t0) / 1000.0) // SECONDS
setMonitorConfig(
MonitorConfig(MonitorSchedule.interval(15, MonitorScheduleUnit.MINUTE)).apply {
setMaxRuntime(10L)
setCheckinMargin(3L)
}
)
}
)
Result.success()
} catch (e: Exception) {
Sentry.captureException(e)
Sentry.captureCheckIn(
CheckIn(id, "workmanager-sync", CheckInStatus.ERROR).apply {
setDuration((SystemClock.elapsedRealtime() - t0) / 1000.0) // SECONDS
}
)
Result.failure()
}
}
}---
Configuration Reference
CheckIn Object
// Constructors
CheckIn(monitorSlug: String, status: CheckInStatus)
CheckIn(checkInId: SentryId, monitorSlug: String, status: CheckInStatus)
// Key setters
fun setDuration(durationSeconds: Double) // ⚠️ SECONDS, not milliseconds
fun setMonitorConfig(config: MonitorConfig)
fun setRelease(release: String)
fun setEnvironment(environment: String)CheckInStatus Enum
| Status | Serialized | Sentry UI Color | Use When |
|---|---|---|---|
IN_PROGRESS | "in_progress" | Yellow | Job has started but not finished |
OK | "ok" | Green | Job completed successfully |
ERROR | "error" | Red | Job failed |
Monitor Schedule
// Crontab — standard 5-field cron expression
MonitorSchedule.crontab("0 3 * * *") // daily at 3:00 AM
MonitorSchedule.crontab("*/15 * * * *") // every 15 minutes
MonitorSchedule.crontab("0 9 * * MON-FRI") // weekdays at 9:00 AM
// Interval — simple repeating interval
MonitorSchedule.interval(30, MonitorScheduleUnit.MINUTE)
MonitorSchedule.interval(6, MonitorScheduleUnit.HOUR)
MonitorSchedule.interval(1, MonitorScheduleUnit.DAY)`MonitorScheduleUnit` values: MINUTE, HOUR, DAY, WEEK, MONTH, YEAR
MonitorConfig Options
| Method | Type | Description |
|---|---|---|
setCheckinMargin(minutes) | Long | Tolerance (minutes) before Sentry marks a check-in as missed |
setMaxRuntime(minutes) | Long | Alert if job runs for longer than this many minutes |
setTimezone(tz) | String | IANA timezone, e.g. "America/Chicago" |
setFailureIssueThreshold(n) | Long | Consecutive failures before opening an issue |
setRecoveryThreshold(n) | Long | Consecutive successes before auto-resolving the issue |
Global Defaults via SentryOptions.Cron
Set defaults applied to every new MonitorConfig:
SentryAndroid.init(this) { options ->
with(options.cron) {
defaultTimezone = "UTC"
defaultCheckinMargin = 5L // 5-minute window
defaultMaxRuntime = 30L // alert after 30 minutes
defaultFailureIssueThreshold = 3L // open issue after 3 failures
defaultRecoveryThreshold = 2L // resolve after 2 successes
}
}---
Best Practices
1. Always capture `IN_PROGRESS` for long-running jobs — heartbeat (OK only) can't detect overruns; two-step check-ins can 2. Use `setDuration()` in seconds — divide elapsed milliseconds by 1000.0; forgetting this is the #1 crons mistake in Android 3. Set `maxRuntime` with a buffer — if your job typically runs 10 minutes, set maxRuntime = 15 to avoid alert noise 4. Capture exceptions separately — CheckIn.ERROR status marks the schedule failed, but Sentry.captureException() creates a full Sentry Issue with stack trace 5. Keep monitor slugs stable — changing a slug creates a new monitor in the dashboard; rename via the UI, not by changing the slug in code 6. Use `setEnvironment()` per check-in — if you run the same job in staging and production, separate them by environment to avoid cross-contamination 7. Set `checkinMargin` conservatively — too tight causes alert noise from slight scheduling jitter; 5–10 minutes is a safe starting point 8. Test with `debug = true` — Sentry.captureCheckIn() is a synchronous HTTP call; verify check-ins appear in the dashboard before deploying 9. Avoid `CheckInUtils` in public APIs — it is @ApiStatus.Experimental and may change; prefer the manual two-step pattern in library code
---
Known Limitations
| Limitation | Details |
|---|---|
| No Android-specific integration | No SentryWorker, no WorkManager adapter — all wiring is manual |
| Android docs page missing | https://docs.sentry.io/platforms/android/crons/ returns HTTP 404; use the Java docs page |
CheckInUtils is experimental | @ApiStatus.Experimental — API may change between SDK releases |
| No auto-error on crash | If the app crashes mid-job, no ERROR check-in is sent automatically; consider a Thread.UncaughtExceptionHandler |
| Duration must be in seconds | setDuration() accepts Double seconds — a common mistake is passing milliseconds directly |
| Rate limited | 6 check-ins per minute per monitor per environment (Sentry platform limit) |
| Spring/Quartz integrations are server-side only | @SentryCheckIn, SentryJobListener, sentry-quartz are for JVM servers — do not use on Android |
---
Troubleshooting
| Issue | Solution |
|---|---|
| Monitor shows "missed" immediately | Check that IN_PROGRESS was sent before the job started and that checkinMargin is set generously enough |
| Duration value looks wrong (1000× too large) | setDuration() expects seconds — divide SystemClock.elapsedRealtime() delta by 1000.0 |
| No check-in events in Sentry dashboard | Confirm Sentry.isEnabled() returns true and the DSN is correct; check-ins are sent synchronously via captureCheckIn() |
CheckInUtils method not found | Ensure SDK ≥ 8.x; CheckInUtils is in the core io.sentry:sentry dependency bundled in sentry-android |
| Monitor shows "error" even on success | Verify the second CheckIn uses the SentryId returned by the first captureCheckIn() call, not a new CheckIn(slug, OK) |
| Two monitors created for the same job | Monitor slug changed between deployments — rename via the Sentry UI, not by changing the slug in code |
| Rate limit errors in logs | Reduce check-in frequency; the limit is 6/minute/monitor/environment |
Error Monitoring & Crash Reporting — Sentry Android SDK
Minimum SDK: io.sentry:sentry-android ≥ 7.0.0 (≥ 8.0.0 recommended)NDK support:io.sentry:sentry-android-ndk(included insentry-androidBOM)
Languages: Kotlin and Java — all examples in Kotlin with Java equivalents where they differ
Android error monitoring covers three crash layers: Java/Kotlin exceptions (JVM), ANR (Application Not Responding) events, and native C/C++ crashes via NDK. All three are handled automatically after initialization.
---
Table of Contents
1. Core Capture APIs 2. Automatic Crash Handling 3. ANR Detection 4. NDK / Native Crash Capture 5. Scope Management 6. Context Enrichment — Tags, User, Breadcrumbs 7. Event Filtering — beforeSend, beforeBreadcrumb 8. Fingerprinting & Grouping 9. Attachments — Screenshots & View Hierarchy 10. Release Health & Sessions 11. Configuration Reference 12. Troubleshooting
---
1. Core Capture APIs
All methods are static on io.sentry.Sentry. Returns a SentryId that can be referenced for user feedback.
Sentry.captureException()
import io.sentry.Sentry
import io.sentry.SentryLevel
// Basic capture
try {
aMethodThatMightFail()
} catch (e: Exception) {
Sentry.captureException(e)
}
// With local scope (applies only to this event)
try {
processPayment(order)
} catch (e: Exception) {
Sentry.captureException(e) { scope ->
scope.setTag("component", "payment")
scope.setLevel(SentryLevel.FATAL)
scope.setTransaction("CheckoutFlow")
}
}Java equivalent:
Sentry.captureException(e, scope -> {
scope.setTag("component", "payment");
scope.setLevel(SentryLevel.FATAL);
});Sentry.captureMessage()
// Default level is INFO
Sentry.captureMessage("User completed onboarding")
// Explicit level: DEBUG | INFO | WARNING | ERROR | FATAL
Sentry.captureMessage("Payment provider unreachable", SentryLevel.WARNING)
Sentry.captureMessage("Database connection pool exhausted", SentryLevel.FATAL)Sentry.captureEvent()
For fully constructed SentryEvent objects — advanced use cases:
import io.sentry.SentryEvent
import io.sentry.protocol.Message
val event = SentryEvent()
event.level = SentryLevel.WARNING
val msg = Message()
msg.message = "Custom structured event"
event.message = msg
event.setTag("build_type", BuildConfig.BUILD_TYPE)
Sentry.captureEvent(event)Sentry.captureUserFeedback()
import io.sentry.UserFeedback
val feedback = UserFeedback(Sentry.lastEventId)
feedback.name = "Jane Smith"
feedback.email = "jane@example.com"
feedback.comments = "The app froze when I tapped the submit button."
Sentry.captureUserFeedback(feedback)Error Levels
| Level | Android use case |
|---|---|
FATAL | App crash, unrecoverable state |
ERROR | Feature broken, user action failed |
WARNING | Degraded state, non-critical failure |
INFO | Informational, notable events |
DEBUG | Development diagnostics |
---
2. Automatic Crash Handling
The SDK installs UncaughtExceptionHandlerIntegration at init time, which wraps Thread.defaultUncaughtExceptionHandler.
How it works
Unhandled exception thrown
│
▼
UncaughtExceptionHandlerIntegration.uncaughtException()
├── wraps throwable in ExceptionMechanismException (mechanism: "AppExitReason")
├── creates SentryEvent at FATAL level
├── captures event and BLOCKS until flushed to disk
└── calls original defaultUncaughtExceptionHandler (re-throws to Android runtime)Crashes are persisted to disk immediately and transmitted on the next app launch. This ensures crash data is never lost even when the process dies.
Startup crash handling
If the app crashes within ~2 seconds of SDK init, SentryAndroid.init() blocks for up to 5 seconds to flush the crash before the process exits:
SentryAndroid.init(this) { options ->
options.startupCrashFlushTimeoutMillis = 5_000L // default: 5 s
}Disable uncaught exception handler
options.isEnableUncaughtExceptionHandler = false---
3. ANR Detection
ANR (Application Not Responding) events fire when the main thread is blocked for more than 5 seconds.
How it works
AnrIntegration runs a watchdog thread (ANRWatchDog) that posts a Runnable to the main Looper every 500 ms. If the main thread hasn't processed the runnable within anrTimeoutIntervalMillis, an ANR event is reported.
ANRWatchDog (background thread)
├── Posts Runnable to MainLooper every 500 ms
└── If (now - lastResponseTime) > anrTimeoutIntervalMillis
→ Captures ANR event with mechanism type "ANR"
→ Background ANRs use mechanism "anr_background"Configuration
SentryAndroid.init(this) { options ->
options.isAnrEnabled = true // default: true
options.anrTimeoutIntervalMillis = 5_000L // default: 5000 ms
options.isAnrReportInDebug = false // default: false (suppress during development)
options.isAttachAnrThreadDump = true // attach thread dump as text attachment
}Via AndroidManifest.xml:
<meta-data android:name="io.sentry.anr.enable" android:value="true" />
<meta-data android:name="io.sentry.anr.timeout-interval-millis" android:value="5000" />
<meta-data android:name="io.sentry.anr.report-debug" android:value="false" />ANR v2 — ApplicationExitInfo (Android 11+)
On API 30+, the SDK reads ActivityManager.getHistoricalProcessExitReasons() to report ANRs from the previous app run that the watchdog couldn't capture in real-time:
options.isReportHistoricalAnrs = true // default: false — report previous-run ANRs---
4. NDK / Native Crash Capture
Native crashes (SIGSEGV, SIGABRT, C++ exceptions) are captured by sentry-android-ndk, which wraps the sentry-native C SDK.
Dependency
// build.gradle.kts — included automatically with the BOM
dependencies {
implementation(platform("io.sentry:sentry-bom:8.33.0"))
implementation("io.sentry:sentry-android") // includes ndk
}How it works
1. SentryNdk.init() loads native libs on a background thread (2 s timeout) 2. Registers native signal handlers via sentry-native 3. On crash: writes crash report to disk 4. On next app launch: sentry-android-core reads and transmits the report
NDK Scope Sync
Java scope changes (user, tags, breadcrumbs) are propagated to the native layer so NDK crashes include the same context as Java events:
options.isEnableScopeSync = true // default: trueNative tombstones (Android 11+)
options.isTombstoneEnabled = true // report native tombstones via ApplicationExitInfoVia manifest:
<meta-data android:name="io.sentry.tombstone.enable" android:value="true" />Disable NDK
options.isEnableNdk = false---
5. Scope Management
Sentry uses a three-layer scope hierarchy. Data merges in order: Global → Isolation → Current (current takes precedence for single-value fields).
| Scope | Lifespan | How to access |
|---|---|---|
| Global | Entire app lifetime | Sentry.configureScope(ScopeType.GLOBAL) { } |
| Isolation | Per-request/session | Sentry.configureScope { } (default) |
| Current | Block-level | Sentry.withScope { } |
Sentry.configureScope() — persist across events
import io.sentry.Sentry
import io.sentry.protocol.User
// Set data for all subsequent events in this session
Sentry.configureScope { scope ->
scope.setTag("app.variant", BuildConfig.FLAVOR)
val user = User().apply {
id = "42"
email = "jane@example.com"
username = "jane_doe"
}
scope.setUser(user)
}
// Clear user on logout
Sentry.configureScope { scope ->
scope.setUser(null)
}Sentry.withScope() — temporary, discarded after block
// Tags/context here do NOT affect events outside this block
Sentry.withScope { scope ->
scope.setTag("transaction_id", orderId)
scope.setLevel(SentryLevel.WARNING)
Sentry.captureMessage("Order processing failed")
}
// scope is discarded — subsequent events are unaffectedSentry.pushScope() — try-with-resources (Java)
try (ISentryLifecycleToken ignored = Sentry.pushScope()) {
Sentry.configureScope(scope -> scope.setTag("local", "value"));
Sentry.captureMessage("Event with local tag");
} // scope is automatically poppedShorthand static methods
Sentry.setUser(user) // → configureScope { scope.setUser(user) }
Sentry.setTag("key", "value") // → configureScope { scope.setTag(...) }
Sentry.removeTag("key")
Sentry.setExtra("key", "value") // deprecated — use setContexts
Sentry.setFingerprint(listOf("my-key"))
Sentry.setTransaction("CheckoutFlow")---
6. Context Enrichment — Tags, User, Breadcrumbs
Tags — indexed and searchable
Tags are key/value string pairs indexed in Sentry, enabling full-text search and filtering.
Constraints: key ≤ 32 chars; value ≤ 200 chars; no newlines in values.
Sentry.configureScope { scope ->
scope.setTag("app.variant", "google-play")
scope.setTag("feature.flag", "checkout_v2")
scope.setTag("subscription.tier", "premium")
}
// Or static shorthand
Sentry.setTag("server.region", "eu-west-1")User identity
import io.sentry.protocol.User
val user = User().apply {
id = "12345"
username = "jane_doe"
email = "jane@example.com"
ipAddress = "{{auto}}" // Sentry resolves from request
data = mapOf(
"subscription" to "premium",
"region" to "eu"
)
}
Sentry.setUser(user)
// Clear on logout
Sentry.setUser(null)Custom contexts (structured, non-searchable)
// Use for rich structured data; use tags for searchable data
Sentry.configureScope { scope ->
scope.setContexts("order", mapOf(
"id" to "ORD-9821",
"total" to 129.99,
"item_count" to 3
))
scope.setContexts("device_capability", mapOf(
"has_nfc" to true,
"ram_gb" to 4
))
}Note: The key "type" is reserved — don't use it as a context key.Breadcrumbs — event trail
Breadcrumbs buffer until the next event is captured. They do not create Sentry issues on their own.
import io.sentry.Breadcrumb
import io.sentry.SentryLevel
// Manual breadcrumb
val breadcrumb = Breadcrumb().apply {
category = "auth"
message = "User authenticated: ${user.email}"
level = SentryLevel.INFO
type = "user"
setData("method", "biometric")
}
Sentry.addBreadcrumb(breadcrumb)
// Simple string
Sentry.addBreadcrumb("Tapped checkout button")
// String with category
Sentry.addBreadcrumb("Database query completed", "db")Breadcrumb fields:
| Field | Type | Description |
|---|---|---|
message | String | Human-readable description |
category | String | Dot-separated origin (e.g., ui.click, http, auth) |
level | SentryLevel | DEBUG, INFO, WARNING, ERROR, FATAL |
type | String | default, http, navigation, user, error |
data | Map<String, Any> | Arbitrary additional metadata |
Automatic breadcrumbs (Android-specific)
The SDK automatically collects these breadcrumbs — all enabled by default:
| Source | Category | What it records |
|---|---|---|
| Activity lifecycle | ui.lifecycle | Created, started, resumed, paused, stopped, destroyed |
| App foreground/background | app.lifecycle | Foreground, background transitions |
| System events | device.* | Battery, screen on/off, network, locale, timezone changes |
| Network connectivity | network.event | Connected, disconnected, type changes |
| User interactions | ui.click | Touch events on views (if enabled) |
| OkHttp requests | http | URL, method, status code (with SentryOkHttpInterceptor) |
Disable individual sources:
options.isEnableActivityLifecycleBreadcrumbs = false
options.isEnableAppLifecycleBreadcrumbs = false
options.isEnableSystemEventBreadcrumbs = false
options.isEnableNetworkEventBreadcrumbs = false
options.enableAllAutoBreadcrumbs(false) // disable all at onceVia manifest:
<meta-data android:name="io.sentry.breadcrumbs.activity-lifecycle" android:value="false" />
<meta-data android:name="io.sentry.breadcrumbs.app-lifecycle" android:value="false" />
<meta-data android:name="io.sentry.breadcrumbs.system-events" android:value="false" />
<meta-data android:name="io.sentry.breadcrumbs.network-events" android:value="false" />Max breadcrumbs (default: 100):
options.maxBreadcrumbs = 50---
7. Event Filtering — beforeSend, beforeBreadcrumb
beforeSend — filter/mutate error events
Called immediately before an error event is transmitted. Return null to drop.
Important: beforeSend only applies to Java/Kotlin events. Native NDK crashes bypass this hook entirely.SentryAndroid.init(this) { options ->
options.beforeSend = SentryOptions.BeforeSendCallback { event, hint ->
// Drop events from test/CI environments
if (event.environment == "test") return@BeforeSendCallback null
// Fingerprint based on throwable type
if (event.throwable is SQLiteException) {
event.fingerprints = listOf("database-error")
}
// Scrub PII before sending
event.user?.email = null
// Attach build metadata
event.setTag("build_number", BuildConfig.VERSION_CODE.toString())
event
}
}Java equivalent:
options.setBeforeSend((event, hint) -> {
if ("test".equals(event.getEnvironment())) return null;
return event;
});beforeSendTransaction — filter performance transactions
options.beforeSendTransaction = SentryOptions.BeforeSendTransactionCallback { tx, hint ->
// Drop health check transactions
if (tx.transaction == "/health") return@BeforeSendTransactionCallback null
tx
}beforeBreadcrumb — filter/mutate breadcrumbs
options.beforeBreadcrumb = SentryOptions.BeforeBreadcrumbCallback { breadcrumb, hint ->
// Drop noisy logger breadcrumbs
if (breadcrumb.category == "com.third_party.SpammyLib") return@BeforeBreadcrumbCallback null
// Scrub sensitive URLs
breadcrumb.data?.let { data ->
(data["url"] as? String)?.let { url ->
data["url"] = url.replace(Regex("token=[^&]*"), "token=REDACTED")
}
}
breadcrumb
}ignoreErrors (ignored exception types)
options.addIgnoredError("android.os.NetworkOnMainThreadException")
options.addIgnoredError("java.net.UnknownHostException")---
8. Fingerprinting & Grouping
Fingerprinting controls how Sentry groups events into issues. Default grouping is by stack trace.
SDK-level fingerprinting
// Static fingerprint via scope (all matching events → one issue)
Sentry.configureScope { scope ->
scope.setFingerprint(listOf("database-connection-error"))
}
Sentry.setFingerprint(listOf("database-connection-error"))
// Dynamic fingerprinting in beforeSend
options.beforeSend = SentryOptions.BeforeSendCallback { event, hint ->
when (event.throwable) {
is SQLiteException -> event.fingerprints = listOf("db-error", "sqlite")
is IOException -> event.fingerprints = listOf("io-error", "{{ default }}")
}
event
}
// Per-capture fingerprint
Sentry.captureException(e) { scope ->
scope.setFingerprint(listOf("payment-gateway-error", "stripe"))
}Fingerprint variables
| Variable | Resolves to |
|---|---|
{{ default }} | Sentry's default stack-trace hash |
{{ error.type }} | Exception class name |
{{ error.value }} | Exception message |
{{ transaction }} | Current transaction name |
{{ level }} | Event severity |
Server-side rules (Sentry project settings)
# Group all DB errors together
error.type:SQLiteException -> database-error
error.type:IOException -> io-error, {{ transaction }}---
9. Attachments — Screenshots & View Hierarchy
Screenshots
Captures a PNG of the current UI when an error occurs. Disabled by default (may contain PII).
options.isAttachScreenshot = trueVia manifest:
<meta-data android:name="io.sentry.attach-screenshot" android:value="true" />Fine-grained control (SDK ≥ 6.24.0):
options.setBeforeScreenshotCaptureCallback { event, hint, debounce ->
// Always capture for crashes; respect debounce for other events
if (event.isCrashed) return@setBeforeScreenshotCaptureCallback true
if (debounce) return@setBeforeScreenshotCaptureCallback false
event.level == SentryLevel.FATAL
}View hierarchy
Captures a JSON representation of the Android view tree when an error occurs.
options.isAttachViewHierarchy = trueVia manifest:
<meta-data android:name="io.sentry.attach-view-hierarchy" android:value="true" />Fine-grained control:
options.setBeforeViewHierarchyCaptureCallback { event, hint, debounce ->
if (event.isCrashed) true
else if (debounce) false
else event.level == SentryLevel.FATAL
}Debounce: View hierarchy is captured at most once every 2 seconds (max 3 times per window) to prevent overhead during rapid error cascades.
Jetpack Compose node names
Add the Sentry Kotlin Compiler Plugin to include composable function names in view hierarchy JSON:
// build.gradle.kts
plugins {
id("io.sentry.kotlin.compiler.gradle") version "6.1.0"
}Manual file attachments
Sentry.captureException(e) { scope ->
scope.addAttachment(Attachment(
logContents.toByteArray(),
"debug.log",
"text/plain"
))
scope.addAttachment(Attachment(
File(context.filesDir, "config.json")
))
}---
10. Release Health & Sessions
Session tracking enables crash-free rate metrics per release version.
SentryAndroid.init(this) { options ->
options.release = "${BuildConfig.APPLICATION_ID}@${BuildConfig.VERSION_NAME}+${BuildConfig.VERSION_CODE}"
options.isEnableAutoSessionTracking = true // default: true
options.sessionTrackingIntervalMillis = 30_000L // default: 30 s background threshold
}Sessions are sent automatically. No additional API calls required.
A session ends when the app goes to the background for longer than sessionTrackingIntervalMillis. Each session maps to a release, so Sentry can compute:
- Crash-free session rate — % of sessions without a fatal crash
- Crash-free user rate — % of users without a crash per release
---
11. Configuration Reference
SentryAndroid.init() — common options
SentryAndroid.init(this) { options ->
// ── Core ──────────────────────────────────────────────────────────────
options.dsn = "https://publicKey@o0.ingest.sentry.io/0"
options.environment = "production"
options.release = "com.myapp@1.2.3+456"
options.dist = "456" // build number
options.isDebug = false
// ── Sampling ─────────────────────────────────────────────────────────
options.sampleRate = 1.0 // error event sample rate (0.0–1.0)
// ── Crash handling ────────────────────────────────────────────────────
options.isEnableUncaughtExceptionHandler = true // default
options.startupCrashFlushTimeoutMillis = 5_000L
// ── ANR ──────────────────────────────────────────────────────────────
options.isAnrEnabled = true
options.anrTimeoutIntervalMillis = 5_000L
options.isAnrReportInDebug = false
options.isAttachAnrThreadDump = false
options.isReportHistoricalAnrs = false // Android 11+
// ── NDK ──────────────────────────────────────────────────────────────
options.isEnableNdk = true
options.isEnableScopeSync = true
options.isTombstoneEnabled = false // Android 11+
// ── Enrichment ────────────────────────────────────────────────────────
options.isAttachScreenshot = false // PII risk — disabled by default
options.isAttachViewHierarchy = false
options.isAttachThreads = false // all thread dumps on every event
options.isAttachStacktrace = true // stack traces on captureMessage
// ── Breadcrumbs ───────────────────────────────────────────────────────
options.maxBreadcrumbs = 100
options.isEnableActivityLifecycleBreadcrumbs = true
options.isEnableAppLifecycleBreadcrumbs = true
options.isEnableSystemEventBreadcrumbs = true
options.isEnableNetworkEventBreadcrumbs = true
// ── Filtering ────────────────────────────────────────────────────────
options.beforeSend = SentryOptions.BeforeSendCallback { event, _ -> event }
options.beforeBreadcrumb = SentryOptions.BeforeBreadcrumbCallback { crumb, _ -> crumb }
// ── Sessions ─────────────────────────────────────────────────────────
options.isEnableAutoSessionTracking = true
options.sessionTrackingIntervalMillis = 30_000L
}AndroidManifest.xml — key meta-data entries
| Key | Type | Default | Description |
|---|---|---|---|
io.sentry.dsn | String | — | DSN (required for auto-init) |
io.sentry.environment | String | — | Environment name |
io.sentry.release | String | — | Release version string |
io.sentry.debug | Boolean | false | Enable SDK debug logging |
io.sentry.sample-rate | Float | — | Error event sample rate |
io.sentry.enabled | Boolean | true | Master on/off switch |
io.sentry.auto-init | Boolean | true | Auto-init via ContentProvider |
io.sentry.anr.enable | Boolean | true | ANR watchdog |
io.sentry.anr.timeout-interval-millis | Integer | 5000 | ANR threshold |
io.sentry.anr.report-debug | Boolean | false | Report ANR during debug |
io.sentry.ndk.enable | Boolean | true | NDK crash capture |
io.sentry.ndk.scope-sync.enable | Boolean | true | NDK scope sync |
io.sentry.attach-screenshot | Boolean | false | Screenshot on error |
io.sentry.attach-view-hierarchy | Boolean | false | View hierarchy on error |
io.sentry.breadcrumbs.activity-lifecycle | Boolean | true | Activity breadcrumbs |
io.sentry.breadcrumbs.app-lifecycle | Boolean | true | App lifecycle breadcrumbs |
io.sentry.breadcrumbs.system-events | Boolean | true | System event breadcrumbs |
io.sentry.breadcrumbs.network-events | Boolean | true | Network breadcrumbs |
---
12. Troubleshooting
| Issue | Cause | Solution |
|---|---|---|
| Events not appearing in Sentry | DSN incorrect or SDK not initialized | Verify DSN; set options.isDebug = true; check Logcat for [Sentry] output |
| Crash events missing | UncaughtExceptionHandler disabled | Ensure isEnableUncaughtExceptionHandler = true; check for competing crash handlers (Firebase, Crashlytics) |
| NDK crashes not reported | NDK module not included or isEnableNdk = false | Add sentry-android-ndk dependency; confirm isEnableNdk = true |
| ANR events missing in release builds | isAnrReportInDebug = false suppresses debug builds | Expected; for release builds, verify isAnrEnabled = true and that anrTimeoutIntervalMillis is reached |
beforeSend not filtering native crashes | beforeSend only applies to JVM events | NDK crashes bypass beforeSend; to suppress NDK entirely: isEnableNdk = false |
| Screenshots are blank or black | Activity window not attached at crash time | Best-effort capture; ensure crash occurs with an active Activity |
| View hierarchy missing composable names | Sentry Kotlin Compiler Plugin not added | Add io.sentry.kotlin.compiler.gradle plugin to build.gradle.kts |
| Duplicate events | Multiple SentryAndroid.init() calls | Call init() once in Application.onCreate() only |
| ANR not firing in debug session | Debugger pauses the main thread, which blocks the watchdog | isAnrReportInDebug defaults to false; this is expected behavior |
| Session tracking not working | release not set | isEnableAutoSessionTracking requires a non-null release to be meaningful |
captureException returns zero-ID | SDK not initialized or enabled = false | Verify SentryAndroid.init() was called in Application.onCreate() before the crash |
| Historical ANRs not reported | Feature not enabled | Set isReportHistoricalAnrs = true; requires Android 11+ (API 30) |
| Tags or user missing from NDK crashes | Scope sync disabled | Ensure isEnableScopeSync = true (default) |
| Events rejected with HTTP 413 | Payload too large | Reduce attachment sizes; avoid large context objects; limit maxBreadcrumbs |
Logging — Sentry Android SDK
Minimum SDK: io.sentry:sentry-android:8.12.0Status: Stable
Enabled by default: No — must opt in explicitly
Docs: https://docs.sentry.io/platforms/android/logs/
---
Overview
Sentry Structured Logs capture application log events as searchable, filterable records in the Sentry Logs UI. Unlike breadcrumbs (which are attached to errors), structured logs are standalone events with typed attributes and are queryable independently.
Logging is opt-in — the feature is entirely inert until options.logs.isEnabled = true is set.
---
Enabling Logs
Code (preferred — allows dynamic configuration):
SentryAndroid.init(this) { options ->
options.dsn = "https://YOUR_KEY@sentry.io/YOUR_PROJECT_ID"
options.logs.isEnabled = true
}AndroidManifest.xml:
<application>
<meta-data
android:name="io.sentry.logs.enabled"
android:value="true" />
</application>---
Configuration
SentryAndroid.init(this) { options ->
options.logs.isEnabled = true
// Optional: filter or mutate logs before they are sent
options.logs.setBeforeSend { event ->
// Drop TRACE logs in production
if (!BuildConfig.DEBUG && event.level == SentryLogLevel.TRACE) {
return@setBeforeSend null // null = drop the event
}
// Redact PII
val body = event.body ?: return@setBeforeSend event
if (body.contains("password", ignoreCase = true)) {
event.setBody("[REDACTED]")
}
event
}
}Configuration Reference
| Option | Type | Default | Description |
|---|---|---|---|
logs.isEnabled | Boolean | false | Master switch — must be true for all logging features |
logs.setBeforeSend | BeforeSendLogCallback? | null | Filter or mutate log events before transmission. Return null to drop. |
---
Code Examples
Basic Level Methods
import io.sentry.Sentry
import io.sentry.SentryLogLevel
import io.sentry.SentryAttribute
import io.sentry.SentryAttributes
import io.sentry.logger.SentryLogParameters
// Six log levels — all accept printf-style format strings
Sentry.logger().trace("Entering %s.%s()", className, methodName)
Sentry.logger().debug("Cache lookup for key: %s", cacheKey)
Sentry.logger().info("User %s logged in", userId)
Sentry.logger().warn("Rate limit approaching: %d / %d requests used", current, max)
Sentry.logger().error("Payment failed after %d retries", retries)
Sentry.logger().fatal("Database unavailable: %s", host)Java:
Sentry.logger().info("User %s logged in", userId);
Sentry.logger().error("Payment failed after %d retries", retries);Structured Attributes
Attach typed key-value pairs to make log events queryable by attribute values in Sentry:
Sentry.logger().log(
SentryLogLevel.INFO,
SentryLogParameters.create(
SentryAttributes.of(
SentryAttribute.stringAttribute("user.id", userId),
SentryAttribute.stringAttribute("order.id", orderId),
SentryAttribute.stringAttribute("payment.method", "card"),
SentryAttribute.booleanAttribute("is_retry", false),
SentryAttribute.integerAttribute("item_count", 3),
SentryAttribute.doubleAttribute("total_usd", 49.99)
)
),
"Checkout completed for user %s",
userId
)Attribute factory methods:
SentryAttribute.stringAttribute("key", "value") // String
SentryAttribute.booleanAttribute("key", true) // Boolean
SentryAttribute.integerAttribute("key", 42) // Int / Long
SentryAttribute.doubleAttribute("key", 3.14) // Double / Float
SentryAttribute.named("key", anyObject) // type inferred at runtimeFrom a map:
val attrs = SentryAttributes.fromMap(mapOf(
"user.id" to userId,
"request.id" to requestId,
"duration" to durationMs
))
Sentry.logger().log(SentryLogLevel.INFO, SentryLogParameters.create(attrs), "API call completed")Custom Timestamp (Backfilling)
val ts = SentryAutoDateProvider().now()
Sentry.logger().log(
SentryLogLevel.INFO,
SentryLogParameters.create(
ts,
SentryAttributes.of(
SentryAttribute.stringAttribute("event", "purchase_complete"),
SentryAttribute.stringAttribute("order.id", orderId)
)
),
"Order %s fulfilled",
orderId
)Production Pattern — Screen Lifecycle
class ProductDetailActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val productId = intent.getStringExtra("product_id")
Sentry.logger().info(
SentryLogParameters.create(
SentryAttributes.of(
SentryAttribute.stringAttribute("screen", "ProductDetail"),
SentryAttribute.stringAttribute("product.id", productId ?: "unknown")
)
),
"Screen opened: ProductDetail"
)
}
}Production Pattern — API Call Logging
suspend fun fetchUserProfile(userId: String): UserProfile {
Sentry.logger().debug("Fetching profile for user %s", userId)
val t0 = SystemClock.elapsedRealtime()
return try {
val profile = api.getUser(userId)
Sentry.logger().info(
SentryLogParameters.create(
SentryAttributes.of(
SentryAttribute.stringAttribute("user.id", userId),
SentryAttribute.integerAttribute("duration_ms", (SystemClock.elapsedRealtime() - t0).toInt())
)
),
"Profile fetch succeeded for user %s",
userId
)
profile
} catch (e: Exception) {
Sentry.logger().error(
SentryLogParameters.create(
SentryAttributes.of(
SentryAttribute.stringAttribute("user.id", userId),
SentryAttribute.integerAttribute("duration_ms", (SystemClock.elapsedRealtime() - t0).toInt()),
SentryAttribute.stringAttribute("error", e.message ?: "unknown")
)
),
"Profile fetch failed for user %s",
userId
)
throw e
}
}---
Timber Integration
SentryTimberIntegration bridges Timber logs into the Sentry Logs pipeline. Requires sentry-android-timber.
Dependency:
implementation("io.sentry:sentry-android-timber:8.33.0")Setup:
SentryAndroid.init(this) { options ->
options.logs.isEnabled = true
options.addIntegration(
SentryTimberIntegration(
minEventLevel = SentryLevel.ERROR, // Timber.e() → Sentry error events
minBreadcrumbLevel = SentryLevel.INFO, // Timber.i() → breadcrumbs
minLogsLevel = SentryLogLevel.DEBUG, // Timber.d() → Sentry structured logs
)
)
}
// Plant Timber separately — SDK does not plant for you
if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree())Timber priority → SentryLogLevel mapping:
| Timber Priority | SentryLogLevel |
|---|---|
Log.VERBOSE | TRACE |
Log.DEBUG | DEBUG |
Log.INFO | INFO |
Log.WARN | WARN |
Log.ERROR | ERROR |
Log.ASSERT | FATAL |
Each Timber-sourced log carries sentry.origin = "auto.log.timber" for filtering in beforeSend.
---
Logcat Auto-Integration (Gradle Plugin)
With the Sentry Android Gradle plugin, android.util.Log calls are bytecode-instrumented at build time and forwarded to Sentry Logs automatically — no source code changes needed:
// android/build.gradle (module)
sentry {
autoInstallation.enabled = true
tracingInstrumentation {
features = [InstrumentationFeature.LOGCAT]
}
}Without the plugin, only manual Sentry.logger() calls and the Timber integration capture logs.
---
Auto-Attached Attributes
The SDK automatically enriches every log event with the following attributes:
| Attribute Key | Value | Notes |
|---|---|---|
sentry.origin | "manual" / "auto.log.timber" / "auto.log.logcat" | Per integration |
sentry.message.template | Original format string | e.g. "User %s logged in" |
sentry.message.parameter.0 … .N | Format argument values | |
sentry.sdk.name | "sentry.java.android" | |
sentry.sdk.version | "8.33.0" | |
sentry.environment | Configured environment | |
sentry.release | Configured release string | |
sentry.replay_id | Active replay session UUID | Only when Session Replay is recording |
sentry._internal.replay_is_buffering | "true" | Only in onErrorSampleRate buffer mode |
user.id | From scope.user.id | Added regardless of sendDefaultPii since 8.33.0 |
user.name | From scope.user.username | |
user.email | From scope.user.email |
8.33.0 change:user.id,user.name,user.emailnow attach to logs regardless of
sendDefaultPii. UsebeforeSendto suppress PII if needed.
---
Log Levels Reference
| Level | SentryLogLevel constant | OTel Severity | When to Use |
|---|---|---|---|
trace | TRACE (1) | 1 | Step-by-step internals, loop iterations |
debug | DEBUG (5) | 5 | Development diagnostics |
info | INFO (9) | 9 | Business events, user actions, state transitions |
warn | WARN (13) | 13 | Recoverable errors, approaching limits |
error | ERROR (17) | 17 | Failures requiring investigation |
fatal | FATAL (21) | 21 | Unrecoverable failures — app or subsystem down |
---
Batch Processing
The SDK batches log events for efficiency:
| Parameter | Value |
|---|---|
| Flush delay | 5 seconds after the first queued event |
| Max batch size | 100 events per HTTP envelope |
| Max queue size | 1,000 events (events silently dropped above this) |
| Background flush | Yes — AndroidLoggerBatchProcessor flushes when app goes to background |
---
Best Practices
1. Enable in `Application.onCreate()` — set options.logs.isEnabled = true before any log calls 2. Use structured attributes — pass typed key-value pairs instead of embedding values in message strings; they become queryable columns in the Logs UI 3. Use `beforeSend` for level filtering — drop TRACE/DEBUG in production to reduce quota usage 4. Redact PII in `beforeSend` — since 8.33.0 user attributes attach regardless of sendDefaultPii 5. Prefer `Sentry.logger()` over Timber for new code — direct API provides full type safety for attributes 6. Plant Timber separately — SentryTimberIntegration does not call Timber.plant(); you must do it yourself 7. Don't call `Sentry.logger()` before `SentryAndroid.init()` — calls before init are silently dropped 8. Keep queue headroom — avoid bursts that exceed 1,000 queued events; excess is silently dropped
---
Troubleshooting
| Issue | Solution |
|---|---|
| Logs not appearing in Sentry | Verify options.logs.isEnabled = true is set in SentryAndroid.init() — the feature is disabled by default |
| Logs appearing but missing attributes | Ensure you pass a SentryLogParameters with SentryAttributes.of(...) — plain format args are not queryable as attributes |
| Timber logs not forwarded | Add SentryTimberIntegration to options.addIntegration() and plant a Timber.DebugTree() separately |
| Timber logs not reaching Sentry as structured logs | Check minLogsLevel on SentryTimberIntegration — default is INFO; set to DEBUG to capture debug-level logs |
| Logs disappear silently under heavy load | Queue is capped at 1,000 events; use beforeSend to drop verbose levels before they queue up |
user.id appearing in logs unexpectedly | Since SDK 8.33.0, user fields attach regardless of sendDefaultPii; use beforeSend to strip them |
Logcat Log.* calls not captured | Requires the Sentry Android Gradle plugin with InstrumentationFeature.LOGCAT enabled at build time |
beforeSend not called | Check that options.logs.isEnabled = true — beforeSend is only invoked when the feature is enabled |
| Logs lost on crash | Known limitation: in-memory queued events may not reach Sentry before the process is killed |
Metrics — Sentry Android SDK
Minimum SDK: io.sentry:sentry-android:8.30.0Enabled by default: Yes — no opt-in required
Docs: https://docs.sentry.io/platforms/android/metrics/
---
Overview
Sentry Metrics lets you track counters, distributions, and gauges from your Android app. Metrics are sent to Sentry and appear in the Metrics tab where you can create charts and alerts.
No Set metric type. Unlike some observability platforms, this SDK does not have a set() method for tracking unique values. Only Counter, Distribution, and Gauge are available.---
Configuration
Metrics is enabled by default. To disable or add filtering:
SentryAndroid.init(this) { options ->
// Disable metrics entirely
options.metrics.setEnabled(false)
// Or filter metrics before they are sent
options.metrics.setBeforeSend { metric, _ ->
// Drop debug-prefixed metrics in production
if (!BuildConfig.DEBUG && metric.name.startsWith("debug.")) {
return@setBeforeSend null // null = drop the metric
}
metric
}
}Via AndroidManifest.xml:
<meta-data android:name="io.sentry.metrics.enabled" android:value="false" />Configuration Reference
| Option | Type | Default | Description |
|---|---|---|---|
metrics.isEnabled | Boolean | true | Master switch — metrics are ON by default |
metrics.setBeforeSend | BeforeSendMetricCallback? | null | Filter or mutate metrics before transmission. Return null to drop. |
---
Code Examples
Counter
Track how many times something happens. Default increment is 1.0.
import io.sentry.Sentry
import io.sentry.metrics.MetricsUnit
import io.sentry.metrics.SentryMetricsParameters
import io.sentry.SentryAttribute
import io.sentry.SentryAttributes
// Simple event count
Sentry.metrics().count("ui.button_tap")
// With explicit increment value
Sentry.metrics().count("feature.items_processed", 25.0)
// With unit
Sentry.metrics().count("network.bytes_sent", 8_192.0, MetricsUnit.Information.BYTE)
// With attributes (custom dimensions for filtering/grouping)
Sentry.metrics().count(
"feature.used",
1.0,
null, // no unit for dimensionless counters
SentryMetricsParameters.create(
SentryAttributes.of(
SentryAttribute.stringAttribute("feature", "dark_mode"),
SentryAttribute.stringAttribute("user_tier", "premium")
)
)
)Distribution
Record individual measurements — each call adds one data point to a histogram. Use for durations, sizes, and similar measurements.
// Response time
Sentry.metrics().distribution("api.response_time", 187.5, MetricsUnit.Duration.MILLISECOND)
// File size
Sentry.metrics().distribution("image.upload_size", fileBytes.toDouble(), MetricsUnit.Information.BYTE)
// With attributes
Sentry.metrics().distribution(
"api.response_time",
elapsed.toDouble(),
MetricsUnit.Duration.MILLISECOND,
SentryMetricsParameters.create(
SentryAttributes.of(
SentryAttribute.stringAttribute("endpoint", "/api/v2/users"),
SentryAttribute.stringAttribute("http.method", "GET"),
SentryAttribute.integerAttribute("http.status", responseCode)
)
)
)Gauge
Capture a point-in-time snapshot of a value that can go up or down. Use for current levels, queue depths, and resource utilization.
// Active downloads
Sentry.metrics().gauge("app.active_downloads", activeDownloads.toDouble())
// Memory usage
val usedMemory = (Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory()) / 1_048_576.0
Sentry.metrics().gauge("app.memory_used_mb", usedMemory)
// Queue depth
Sentry.metrics().gauge("queue.pending_tasks", pendingTasks.toDouble())Backfilling with Custom Timestamp
Sentry.metrics().distribution(
"checkout.duration",
checkoutDurationMs.toDouble(),
MetricsUnit.Duration.MILLISECOND,
SentryMetricsParameters.create(
SentryAutoDateProvider().now(), // custom timestamp
SentryAttributes.of(
SentryAttribute.stringAttribute("payment_method", "card"),
SentryAttribute.booleanAttribute("coupon_applied", true)
)
)
)Practical Example — Tracking Checkout Flow
class CheckoutViewModel : ViewModel() {
fun checkout(cart: Cart) {
val t0 = SystemClock.elapsedRealtime()
viewModelScope.launch {
try {
val order = repository.submitOrder(cart)
// Count successful checkouts
Sentry.metrics().count(
"checkout.success",
1.0,
null,
SentryMetricsParameters.create(
SentryAttributes.of(
SentryAttribute.stringAttribute("payment_method", cart.paymentMethod),
SentryAttribute.integerAttribute("item_count", cart.items.size)
)
)
)
// Record checkout duration
Sentry.metrics().distribution(
"checkout.duration",
(SystemClock.elapsedRealtime() - t0).toDouble(),
MetricsUnit.Duration.MILLISECOND
)
// Record order value
Sentry.metrics().distribution("order.value_usd", cart.totalUsd)
} catch (e: Exception) {
Sentry.metrics().count("checkout.failure")
throw e
}
}
}
}---
Units Reference — MetricsUnit
Use MetricsUnit constants for well-known units. Pass null for dimensionless values.
Custom unit strings are not supported. OnlyMetricsUnit.*constants produce correct rendering in the Sentry UI. Passing an arbitrary string (e.g.,"frames") is accepted but displays incorrectly.
Duration:
MetricsUnit.Duration.NANOSECOND
MetricsUnit.Duration.MICROSECOND
MetricsUnit.Duration.MILLISECOND // most common for Android
MetricsUnit.Duration.SECOND
MetricsUnit.Duration.MINUTE
MetricsUnit.Duration.HOUR
MetricsUnit.Duration.DAY
MetricsUnit.Duration.WEEKInformation (data size):
MetricsUnit.Information.BYTE
MetricsUnit.Information.KILOBYTE
MetricsUnit.Information.KIBIBYTE
MetricsUnit.Information.MEGABYTE
MetricsUnit.Information.MEBIBYTE
MetricsUnit.Information.GIGABYTE
MetricsUnit.Information.GIBIBYTE
MetricsUnit.Information.TERABYTEFraction:
MetricsUnit.Fraction.RATIO // 0.0 to 1.0
MetricsUnit.Fraction.PERCENT // 0.0 to 100.0---
Batch Processing
Metrics are batched and sent asynchronously:
| Parameter | Value |
|---|---|
| Flush delay | 5 seconds after the first queued event |
| Max batch size | 1,000 metrics per HTTP envelope |
| Max queue size | 10,000 metrics (above this, new metrics are silently dropped) |
| Background flush | Yes — AndroidMetricsBatchProcessor flushes immediately on app background |
---
Best Practices
1. Use consistent key names — metric names should be snake_case.namespaced, e.g. checkout.duration not checkoutDuration 2. *Use `MetricsUnit. constants** — custom strings render incorrectly in the UI 3. **Pass null unit for dimensionless metrics** — don't leave unit as an empty string 4. **Use attributes for dimensions** — prefer Sentry.metrics().count("api.call", 1.0, null, paramsWithEndpointAttr) over separate metric keys per endpoint 5. **Use beforeSend` to drop debug metrics in production — avoids quota usage and noise 6. Counter vs. Distribution — use Counter for event occurrences (how many times), Distribution for measurements (how long, how large) 7. Gauge for instantaneous state — gauges are best for values that fluctuate: queue depth, connection pool size, active sessions 8. Avoid high-cardinality attribute values** — user IDs, UUIDs, or timestamps as attribute values create unbounded series in the Sentry backend
---
Troubleshooting
| Issue | Solution |
|---|---|
| Metrics not appearing in Sentry | Verify SDK ≥ 8.30.0; check that metrics were not disabled via options.metrics.setEnabled(false) or manifest io.sentry.metrics.enabled = false |
set() method not found | The Set type does not exist in this SDK — only Counter, Distribution, and Gauge are available |
| Metrics silently dropped | Queue is capped at 10,000 events; use beforeSend to filter high-volume metrics |
| Unit renders incorrectly in Sentry UI | Only MetricsUnit.* constants are supported; passing arbitrary strings is not |
beforeSend dropping too many metrics | Ensure the return is metric (not null) for events you want to keep; a null return drops the event |
| Metrics data delayed by ~5s | Expected — the batch processor holds events for 5 seconds before flushing |
| High memory on metrics-heavy screens | Metrics are in-memory until flushed; avoid extremely high burst rates on low-memory devices |
Profiling — Sentry Android SDK
Minimum SDK: io.sentry:sentry-android ≥ 7.0.0 for transaction-based profilingio.sentry:sentry-android ≥ 8.7.0 for continuous/UI profiling (recommended)Requirement: Profiling requires tracing to be enabled. Only transactions sampled for tracing can be profiled.
Platform requirement: Android API 22+ (silently disabled on older devices)
Android profiling samples the call stack at ~101 Hz using Debug.startMethodTracingSampling() to surface hot code paths and slow functions. Profiles are attached to transactions and visible as flame graphs in Sentry.
---
Table of Contents
1. How Profiling Works 2. Continuous / UI Profiling (Recommended) 3. Transaction-Based Profiling (Legacy) 4. Choosing a Mode 5. What Data Is Captured 6. Performance Overhead 7. Configuration Reference 8. Known Limitations 9. Troubleshooting
---
1. How Profiling Works
Transaction starts (sampled)
│
├── Profiler starts: Debug.startMethodTracingSampling(file, 3MB, ~9901 μs)
│ ├── Samples JS/Kotlin/Java call stack at ~101 Hz
│ └── Writes binary trace to local file
│
├── Transaction body executes
│ ├── Your code: Kotlin/Java frames
│ ├── Auto-instrumented spans (OkHttp, SQLite, etc.)
│ └── Collected: frames_slow, frames_frozen, screen_frame_rate
│
└── Transaction finishes
├── Profiler stops: Debug.stopMethodTracing()
├── Profile data deserialized into ProfilingTraceData
└── Attached to transaction envelope → uploaded to SentrySampling relationship
profilesSampleRate is relative to `tracesSampleRate` — not to all transactions:
All transactions
└─ × tracesSampleRate → Traced transactions
└─ × profilesSampleRate → Profiled transactionsExample: tracesSampleRate = 0.2 + profilesSampleRate = 0.5 → 10% of all transactions are profiled.
---
2. Continuous / UI Profiling (Recommended)
Available since SDK 8.7.0. Profiles the entire app session (or only during active traces) rather than individual transactions. No 30-second hard cap.
Setup
import io.sentry.ProfileLifecycle
import io.sentry.android.core.SentryAndroid
SentryAndroid.init(this) { options ->
options.dsn = "YOUR_DSN"
// Tracing is required for TRACE lifecycle; optional for MANUAL
options.tracesSampleRate = 1.0
// Fraction of app sessions to profile continuously (evaluated once per session)
// 1.0 = all sessions — development/testing only
options.profileSessionSampleRate = 1.0
// TRACE: profile starts/stops automatically with root spans
// MANUAL: you call Sentry.startProfiler() / Sentry.stopProfiler()
options.profileLifecycle = ProfileLifecycle.TRACE
// MANUAL lifecycle only: start profiling from the very first frame
// options.isProfilingStartOnAppStart = true
}TRACE lifecycle
Profiler starts automatically when the first sampled root span begins, and stops when all root spans finish. No code changes needed beyond the options above.
App launches
└─ First traced transaction starts → profiler starts
├─ Activity load transaction
├─ User taps → another transaction
└─ All transactions finish → profiler stops
└─ Profile chunks emitted every 60 sMANUAL lifecycle
You control start/stop explicitly. Does not require tracing.
import io.sentry.Sentry
// @ApiStatus.Experimental — API may change
Sentry.startProfiler()
// ... do work you want to profile ...
Sentry.stopProfiler()Note:startProfiler()andstopProfiler()are@ApiStatus.Experimental. Pin your SDK version if API stability matters.
Profile chunks
Continuous profiling emits profile data in 60-second rolling chunks. Each chunk is a separate envelope item correlated to active transactions via profilerId:
Session start → profilerId generated (shared across all chunks)
Every 60 s → ProfileChunk(profilerId, chunkId, frames) uploaded
Each transaction → contexts.profile.profiler_id = profilerId (links tx → profile)---
3. Transaction-Based Profiling (Legacy)
Available since SDK 7.0.0. Profiles are scoped to individual sampled transactions with a 30-second hard cap.
Setup
SentryAndroid.init(this) { options ->
options.dsn = "YOUR_DSN"
// Both must be set — profiling is relative to tracing
options.tracesSampleRate = 1.0
options.profilesSampleRate = 1.0 // 1.0 = profile all traced transactions
// Or: dynamic profiling sampler
// options.profilesSampler = ProfilesSamplerCallback { ctx ->
// if (ctx.transactionContext.name.contains("Checkout")) 1.0 else 0.1
// }
}Java equivalent:
options.setTracesSampleRate(1.0);
options.setProfilesSampleRate(1.0);30-second hard cap
Transaction profiling uses Debug.startMethodTracingSampling() which is limited to 30 seconds. Profiles for transactions longer than 30 s are automatically truncated:
Android SDK constants:
BUFFER_SIZE_BYTES = 3_000_000 (3 MB ring buffer)
PROFILING_TIMEOUT_MILLIS = 30_000 (30 s hard cap)
Sampling interval ≈ 9,901 μs (101 Hz)This is the primary reason to prefer continuous profiling. Transactions that exceed 30 s (e.g., long-running background work) have incomplete profiles in the transaction-based mode.
Mutual exclusivity
Setting profilesSampleRate (even to 0.0) disables continuous profiling. The two modes cannot be used simultaneously:
// ✅ Continuous profiling only
options.profileSessionSampleRate = 0.5 // set this
// options.profilesSampleRate not set
// ✅ Transaction profiling only (legacy)
options.profilesSampleRate = 0.5 // set this
// options.profileSessionSampleRate not set
// ❌ Both set — continuous profiling silently disabled
options.profilesSampleRate = 0.5
options.profileSessionSampleRate = 0.5 // ignored---
4. Choosing a Mode
| Continuous / UI Profiling | Transaction-Based (Legacy) | |
|---|---|---|
| SDK version | ≥ 8.7.0 | ≥ 7.0.0 |
| Duration limit | None (60 s chunks) | 30 s hard cap |
| Tracing required | TRACE lifecycle: yes. MANUAL: no | Yes |
| Overhead | Higher (always sampling) | Lower (transaction-gated) |
| Recommended for | All new projects | Existing projects not yet on 8.7+ |
| App start coverage | ✅ isProfilingStartOnAppStart = true | ❌ Profile starts with first transaction |
| API stability | startProfiler/stopProfiler are experimental | Stable |
Choose continuous profiling unless you are constrained to SDK < 8.7.0 or need to minimize profiling overhead on low-end devices.
---
5. What Data Is Captured
In a profile
| Data | Description |
|---|---|
| Call stack samples | Java/Kotlin stack frames sampled at ~101 Hz |
| Flame graph | Aggregated time-per-function view |
| Timeline | Stack samples correlated with transaction spans |
| Thread info | Main thread, background threads, system threads |
| Frame metrics | frames_slow, frames_frozen, screen_frame_rate |
| Function names | Readable names require ProGuard mapping upload |
What profiles are linked to
Each profile chunk (continuous) or profile (transaction-based) is correlated with transactions via profilerId. In the Sentry UI you can:
- View flame graph alongside the transaction's span waterfall
- Identify which functions executed during slow spans
- Click from a slow span to the corresponding stack samples
What is NOT captured
- Memory allocations (use Android Studio Memory Profiler)
- Network traffic details (captured separately by OkHttp spans)
- GPU rendering details (slow/frozen frames are a separate measurement)
---
6. Performance Overhead
Profiling adds CPU overhead. Debug.startMethodTracingSampling() uses sampling (not full instrumentation), which keeps overhead lower than full instrumentation profilers.
| Factor | Impact |
|---|---|
| Transaction-based profiling | Low-medium — only active during traced transactions |
Continuous (TRACE lifecycle) | Medium — always running while any transaction is active |
Continuous (MANUAL lifecycle) | Higher — runs from startProfiler() to stopProfiler() |
| Low-end Android devices (< 4GB RAM) | More significant — test on representative devices |
Recommendations:
- Use
profilesSampleRate = 1.0orprofileSessionSampleRate = 1.0only in development/testing - In production: keep
profilesSampleRate ≤ 0.1orprofileSessionSampleRate ≤ 0.1 - On low-end devices: reduce to
0.01–0.05 - Use
TRACElifecycle instead ofMANUALto gate profiling to active transactions
---
7. Configuration Reference
SentryAndroid.init() — profiling options
SentryAndroid.init(this) { options ->
// ── Required: tracing ────────────────────────────────────────────────
options.tracesSampleRate = 0.2 // required for TRACE lifecycle and transaction profiling
// ── Continuous profiling (recommended, SDK ≥ 8.7.0) ──────────────────
options.profileSessionSampleRate = 0.1 // 10% of sessions profiled
options.profileLifecycle = ProfileLifecycle.TRACE // or MANUAL
options.isProfilingStartOnAppStart = false // MANUAL only: start from first frame
// ── Transaction-based profiling (legacy) ──────────────────────────────
// options.profilesSampleRate = 0.1 // mutually exclusive with continuous
// options.profilesSampler = ProfilesSamplerCallback { ctx -> 0.1 }
}AndroidManifest.xml — profiling keys
| Key | Type | Default | Description |
|---|---|---|---|
io.sentry.traces.profiling.sample-rate | Float | null | Transaction profiling rate (legacy) |
io.sentry.traces.profiling.session-sample-rate | Float | null | Continuous profiling session rate |
io.sentry.traces.profiling.lifecycle | String | "manual" | "manual" or "trace" |
io.sentry.traces.profiling.start-on-app-start | Boolean | false | Auto-start profiler at app launch |
Via manifest (continuous profiling):
<meta-data android:name="io.sentry.traces.profiling.session-sample-rate" android:value="0.1" />
<meta-data android:name="io.sentry.traces.profiling.lifecycle" android:value="trace" />ProfileLifecycle enum
| Value | Profiler starts | Profiler stops | Requires tracing |
|---|---|---|---|
TRACE | First sampled root span | All root spans finished | Yes |
MANUAL | Sentry.startProfiler() | Sentry.stopProfiler() | No |
Manual start/stop (MANUAL lifecycle only)
// @ApiStatus.Experimental
Sentry.startProfiler()
// ... work to profile ...
Sentry.stopProfiler()---
8. Known Limitations
Android API minimum
Profiling uses Debug.startMethodTracingSampling(), which requires API 21+; effective minimum is API 22. Silently disabled on older devices with no error or warning.
Transaction profiling: 30-second hard cap
Profiles for transactions longer than 30 s are automatically truncated. The transaction itself continues normally, but the profile has missing data for anything after 30 s. Use continuous profiling to avoid this.
3 MB ring buffer
Transaction profiling uses a 3 MB buffer. Very high-frequency call stacks on low-memory devices may overflow the buffer, causing early profile termination.
ProGuard/R8 obfuscation
Release builds with ProGuard/R8 enabled produce obfuscated frame names (e.g., a.b(), c.d()). Upload mapping files to see readable names:
// build.gradle.kts
sentry {
autoUploadProguardMapping = true
}Background transactions
If a transaction is abandoned while the app is backgrounded, the profile may be truncated. DEADLINE_EXCEEDED transactions may not have complete profiles.
Continuous profiling API stability
Sentry.startProfiler() and Sentry.stopProfiler() are @ApiStatus.Experimental. The _experiments config block used in some SDKs is not used in Android — use options.profileSessionSampleRate, options.profileLifecycle, and options.isProfilingStartOnAppStart directly.
Continuous profiling chunk duration
Chunks are emitted on a fixed 60-second cycle (MAX_CHUNK_DURATION_MILLIS = 60_000). This is not configurable.
---
9. Troubleshooting
| Issue | Likely cause | Solution |
|---|---|---|
| No profiles appearing | profilesSampleRate or profileSessionSampleRate not set, or tracesSampleRate = 0 | Set both sample rates > 0; verify DSN is correct |
Frame names appear obfuscated (a.b, c.d) | ProGuard/R8 mapping not uploaded | Configure autoUploadProguardMapping = true in the Sentry Gradle plugin |
| Transaction profiling stops at 30 s | Hard-coded 30 s cap | Switch to continuous profiling (profileSessionSampleRate) |
| Continuous profiling has no data | TRACE lifecycle but no transactions sampled | Verify tracesSampleRate > 0; check Sentry DSN and that events appear in Sentry |
startProfiler() has no effect | TRACE lifecycle selected instead of MANUAL | Set options.profileLifecycle = ProfileLifecycle.MANUAL before calling startProfiler() |
| Profiling causes visible app slowdown | Sample rate too high on low-end device | Reduce profilesSampleRate or profileSessionSampleRate to ≤ 0.05 on low-end devices |
profileSessionSampleRate ignored | profilesSampleRate is also set | Remove profilesSampleRate — the two modes are mutually exclusive |
| Profiles appear for some transactions only | Expected — sample rate controls the fraction | Increase rate if you want broader coverage |
| App start not profiled | isProfilingStartOnAppStart = false (default) | Set options.isProfilingStartOnAppStart = true with MANUAL lifecycle |
| API < 22 device not profiling | Silently disabled | Profiling requires Android API 22+; check device API level |
| Profile data incomplete for long sessions | 60 s chunk boundary | Normal behavior — chunks are separate envelope items; use the trace waterfall to correlate |
Session Replay — Sentry Android SDK
Minimum SDK: io.sentry:sentry-android:8.33.0Minimum Android API: 26 (Oreo) — silently disabled on API 21-25
Status: Production-ready
Docs: https://docs.sentry.io/platforms/android/session-replay/
---
⚠️ API Level Requirement
Session Replay requires Android API 26 (Oreo) or higher. On devices running API 21–25, replay is silently skipped with an INFO log — no crash, no error, no recording. Apps with minSdk = 21 (covering ~4–5% of devices) have zero replay coverage on those devices.
// Guard in ReplayIntegration.kt (line 128):
// if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { return }---
How Android Replay Works
Android Session Replay is screenshot-based, not DOM-based:
| Dimension | Web Replay | Android Replay |
|---|---|---|
| Recording method | DOM serialization | Native view hierarchy screenshots |
| Frame rate | Variable / mutation-driven | ~1 frame per second |
| Privacy mechanism | CSS-based DOM masking | Native pixel masking over screenshots |
| Touch recording | Full pointer events | Tap breadcrumbs only |
| Text in replay | Selectable, searchable | Pixel-only — text is in screenshots |
---
Installation
sentry-android bundles session replay — no separate dependency needed:
dependencies {
implementation("io.sentry:sentry-android:8.33.0")
}For a lighter build using explicit modules:
dependencies {
implementation("io.sentry:sentry-android-core:8.33.0")
implementation("io.sentry:sentry-android-replay:8.33.0")
}For Jetpack Compose masking support:
dependencies {
implementation("io.sentry:sentry-compose-android:8.33.0")
}---
Basic Setup
SentryAndroid.init(this) { options ->
options.dsn = "https://YOUR_KEY@sentry.io/YOUR_PROJECT_ID"
// Continuous recording — record 10% of all sessions
options.sessionReplay.sessionSampleRate = 0.1
// Error-triggered — buffer 30s and upload when an error occurs
options.sessionReplay.onErrorSampleRate = 1.0
// Production-recommended: both modes together
options.sessionReplay.sessionSampleRate = 0.05 // 5% continuous baseline
options.sessionReplay.onErrorSampleRate = 0.75 // 75% on error
}Via AndroidManifest.xml:
<meta-data android:name="io.sentry.session-replay.session-sample-rate" android:value="0.1" />
<meta-data android:name="io.sentry.session-replay.on-error-sample-rate" android:value="1.0" />---
Configuration Reference
Accessed via options.sessionReplay (Kotlin) or options.getSessionReplay() (Java).
| Option | Type | Default | Description |
|---|---|---|---|
sessionSampleRate | Double? | 0.0 | Fraction of all sessions recorded continuously (0.0–1.0) |
onErrorSampleRate | Double? | 0.0 | Fraction captured when an error occurs; buffers 30s prior |
quality | SentryReplayQuality | MEDIUM | Video encoding quality preset |
screenshotStrategy | ScreenshotStrategyType | PIXEL_COPY | Frame capture method (PIXEL_COPY or CANVAS) |
maskAllText | Boolean | true | Mask all TextView subclasses (includes Button, EditText, etc.) |
maskAllImages | Boolean | true | Mask all ImageView subclasses |
frameRate | Int | 1 | Frames per second |
errorReplayDuration | Long (ms) | 30_000 | Pre-error ring buffer size (30 seconds) |
sessionSegmentDuration | Long (ms) | 5_000 | How often video segments are uploaded |
sessionDuration | Long (ms) | 3_600_000 | Maximum session length (1 hour) |
networkDetailAllowUrls | List<String> | [] | URL prefixes for network request/response capture |
networkDetailDenyUrls | List<String> | [] | URL prefixes excluded from network capture |
networkCaptureBodies | Boolean | true | Capture HTTP request/response bodies |
networkRequestHeaders | List<String> | [Content-Type, Content-Length, Accept] | Request headers to capture |
networkResponseHeaders | List<String> | [Content-Type, Content-Length, Accept] | Response headers to capture |
debug | Boolean | false | Enable verbose replay diagnostic logging |
trackConfiguration | Boolean | true | Track device orientation and configuration changes |
---
Code Examples
Video Quality
SentryAndroid.init(this) { options ->
// Choose based on bandwidth/battery vs. fidelity trade-off
options.sessionReplay.quality = SentryReplayQuality.LOW // 80% res, 50 kbps, JPEG q10
options.sessionReplay.quality = SentryReplayQuality.MEDIUM // 100% res, 75 kbps, JPEG q30 (default)
options.sessionReplay.quality = SentryReplayQuality.HIGH // 100% res, 100 kbps, JPEG q50
}Screenshot Strategy
SentryAndroid.init(this) { options ->
// Default — stable, supports all custom masking
options.sessionReplay.screenshotStrategy = ScreenshotStrategyType.PIXEL_COPY
// Experimental — always masks ALL text and images; custom masking rules are ignored
options.sessionReplay.screenshotStrategy = ScreenshotStrategyType.CANVAS
}Canvas strategy warning: WhenCANVASis active,addMaskViewClass(), XML tags, Kotlin extensions, and Compose modifiers are completely ignored. Canvas always masks all text and images. Use only when total masking is acceptable.
Development Configuration
SentryAndroid.init(this) { options ->
if (BuildConfig.DEBUG) {
options.sessionReplay.sessionSampleRate = 1.0 // record all sessions in dev
options.sessionReplay.debug = true // verbose diagnostic logging
} else {
options.sessionReplay.sessionSampleRate = 0.05
options.sessionReplay.onErrorSampleRate = 1.0
}
}---
Privacy Masking
Default Masked Types
These view classes are masked by default:
android.widget.TextView (+ ALL subclasses: Button, EditText, CheckBox, RadioButton, Switch...)
android.widget.ImageView
android.webkit.WebView
android.widget.VideoView
androidx.camera.view.PreviewView
androidx.media3.ui.PlayerView
com.google.android.exoplayer2.ui.PlayerView
com.google.android.exoplayer2.ui.StyledPlayerViewMasking is hierarchical. Masking TextView also masks every subclass automatically.Disabling Default Masking
// Only if you are certain your UI contains no PII in text or images
options.sessionReplay.maskAllText = false
options.sessionReplay.maskAllImages = falseClass-Based Masking
// Mask custom view class and all subclasses
options.sessionReplay.addMaskViewClass("com.example.ui.CreditCardInputView")
options.sessionReplay.addMaskViewClass("com.example.ui.SsnField")
// Unmask a subclass whose parent class is masked
options.sessionReplay.addUnmaskViewClass("com.example.ui.PublicLabelButton")Instance-Based Masking via XML
<!-- Using the standard tag attribute -->
<View android:tag="sentry-mask" ... />
<TextView android:tag="sentry-unmask" android:text="@string/public_product_name" />
<!-- Using the dedicated Sentry privacy tag (preferred — doesn't conflict with other tag uses) -->
<View ...>
<tag android:id="@id/sentry_privacy" android:value="mask" />
</View>
<TextView android:text="@string/safe_label">
<tag android:id="@id/sentry_privacy" android:value="unmask" />
</TextView>Instance-Based Masking via Kotlin Extensions
// On any View instance
creditCardInputView.sentryReplayMask()
productTitleLabel.sentryReplayUnmask()Jetpack Compose Masking
Requires: io.sentry:sentry-compose-android:8.33.0Compose 1.8+ note: A masking regression for Compose 1.8+ was fixed in SDK 8.32.x — use SDK ≥ 8.33.0 with Compose 1.8.
// Mask an entire composable subtree
@Composable
fun PaymentForm() {
Column(modifier = Modifier.sentryReplayMask()) {
CreditCardField() // masked via parent
CvvField() // masked via parent
ExpiryDateField() // masked via parent
}
}
// Unmask safe elements inside an otherwise-masked region
@Composable
fun ProductCard(product: Product) {
Column(modifier = Modifier.sentryReplayMask()) {
Text(
text = product.name,
modifier = Modifier.sentryReplayUnmask() // product name is safe to show
)
Text(
text = userPaymentInfo,
// inherits parent mask — sensitive data stays masked
)
}
}
// Masking via Sentry tag modifier (alternative)
Text(
text = "Public info",
modifier = Modifier.sentryTag("sentry-unmask")
)Compose + view flattening: Compose can optimize away wrapper composables. If masking is unexpectedly dropped, verify the composable is not being flattened by the Compose compiler.
Masking Priority Order (first match wins)
1. View-level unmask — sentry-unmask tag / .sentryReplayUnmask() / Compose .sentryReplayUnmask() 2. View-level mask — sentry-mask tag / .sentryReplayMask() / Compose .sentryReplayMask() 3. Class-level unmask — addUnmaskViewClass(className) 4. Class-level mask — addMaskViewClass(className) / maskAllText / maskAllImages
ViewGroup inheritance: A masked parent does NOT automatically mask its children. Each child must be independently masked. An unmasked parent does NOT override class-level masks on children.
---
Network Capture
Requires SentryOkHttpInterceptor or SentryOkHttpEventListener from sentry-android-okhttp (or sentry-okhttp):
SentryAndroid.init(this) { options ->
options.sessionReplay.networkDetailAllowUrls = listOf(
"https://api.myapp.com/v2" // capture details for this prefix
)
options.sessionReplay.networkDetailDenyUrls = listOf(
"https://api.myapp.com/v2/auth", // exclude auth endpoints (tokens)
"https://api.myapp.com/v2/payment" // exclude payment endpoints
)
options.sessionReplay.networkCaptureBodies = true
options.sessionReplay.networkRequestHeaders = listOf("X-Request-ID", "Accept-Language")
options.sessionReplay.networkResponseHeaders = listOf("X-Response-Time", "X-Cache")
}Retrofit and HttpURLConnection apps have no network capture in replay — only OkHttp is supported.---
Session Lifecycle
App launches / comes to foreground
│
▼
SDK init → ReplayIntegration.start()
│
│ [recording at 1 fps, uploading segments every 5s]
│
├── app goes background ─────────► pause() + flush current segment
│ < 30s background
├── app returns to foreground ───► resume() (SAME replay ID)
│ > 30s background
├── app returns to foreground ───► stop() + start() (NEW replay ID)
│
├── 60 minutes elapsed ──────────► session ends, new one begins
│
└── error occurs (onErrorSampleRate mode)
│
└─► last 30s of buffered frames are uploaded---
Debug Masking Overlay
During development, enable a colored overlay to visually inspect which regions are masked:
// Get the ReplayIntegration instance
val replay = Sentry.getCurrentScopes().options.integrations
.filterIsInstance<ReplayIntegration>()
.firstOrNull()
replay?.enableDebugMaskingOverlay() // shows colored rectangles over masked regions
replay?.disableDebugMaskingOverlay()---
Best Practices
1. Always set both sample rates for production — sessionSampleRate for baseline coverage, onErrorSampleRate = 1.0 for debugging every error 2. Start with `MEDIUM` quality — then adjust down to LOW if bandwidth or battery becomes a concern 3. Use `maskAllText = true` (default) — erring on the side of more masking is always safer for user privacy 4. Use XML `sentry_privacy` tag over `android:tag` — the dedicated tag doesn't conflict with other usages of android:tag 5. Use Compose `Modifier.sentryReplayMask()` at the layout level — mask entire sections (payment forms, PII screens) rather than individual fields 6. Never enable Canvas strategy in production without testing — it ignores all custom masking rules 7. Set `debug = true` during development — helps verify masking is applied correctly before releasing 8. Test on an API 26 device — replay silently does nothing on API 21-25; don't test exclusively on newer devices and assume all users have coverage 9. Exclude auth and payment URLs from network capture — networkDetailDenyUrls prevents tokens and card data from appearing in replay
---
Known Limitations
| Limitation | Details |
|---|---|
| API 26+ hard requirement | Silent no-op on API 21-25; no fallback or warning unless debug = true |
| Canvas strategy ignores all masking | addMaskViewClass, XML tags, Kotlin extensions, and Compose modifiers have zero effect with CANVAS |
| PixelCopy masking misalignment | Mask overlay can misalign on views with setTranslationX/Y, setScaleX/Y, or inside RecyclerView with ItemAnimator |
| 60-minute session cutoff | Sessions longer than 1 hour are truncated; a new session starts but replay continuity is broken |
| Compose 1.8 masking regression | Fixed in 8.32.x — requires SDK ≥ 8.33.0 for reliable masking with Compose 1.8+ |
| OkHttp required for network capture | networkDetailAllowUrls / networkCaptureBodies only work with Sentry OkHttp integration installed |
| Trailing frame loss on crash | In-memory frames from the current segment are lost if the app is killed mid-segment; completed segments are persisted to disk |
| No Retrofit/HttpURLConnection network capture | Only OkHttp-based networking is captured in replay |
---
Troubleshooting
| Issue | Solution |
|---|---|
| No replays appearing in Sentry | Check sessionSampleRate or onErrorSampleRate > 0; verify device is API 26+ (enable debug = true to see skip log on API 21-25) |
| Masking not applied to custom view | Add class to addMaskViewClass("com.example.MyView") or apply sentry-mask tag |
| Masking applied but view content still visible | Check masking priority order; a view-level unmask overrides class-level masks |
| Compose masking not working | Add sentry-compose-android dependency; ensure SDK ≥ 8.33.0 for Compose 1.8+ support |
| Canvas strategy masking all content | Expected — Canvas strategy masks all text and images regardless of masking configuration |
| Network requests not captured in replay | Add SentryOkHttpInterceptor and add the API base URL to networkDetailAllowUrls |
| High battery drain | Reduce quality to LOW; replay runs at 1 fps but PixelCopy is GPU-accelerated |
| Replay ID not linked to log events | Replay ID is auto-attached to logs and metrics when replay is active; verify both features are enabled |
Related skills
How it compares
Pick sentry-android-sdk for Android scheduled job heartbeat monitoring rather than general Sentry crash reporting or APM performance tracing.
FAQ
Who is sentry-android-sdk for?
Developers and software engineers working with sentry-android-sdk patterns described in the skill documentation.
When should I use sentry-android-sdk?
When Full Sentry SDK setup for Android. Use when asked to "add Sentry to Android", "install sentry-android", "setup Sentry in Android", or configure error monitoring, tracing, profiling.
Is sentry-android-sdk safe to install?
Review the Security Audits panel on this page before installing in production.