---
title: "Navigation"
description: "Learn more about Sentry's navigation integrations for the Android SDK."
url: https://docs.sentry.io/platforms/android/integrations/navigation/
---

# Navigation for Android

The `sentry-android-navigation` and `sentry-android-navigation3` libraries provide support for Sentry's Android navigation instrumentation. Use them to automatically add breadcrumbs, track screen names, and start navigation transactions whenever users navigate around your app.

This page covers:

* **[Navigation 2](https://developer.android.com/guide/navigation/)** (`androidx.navigation`) for classic Views and Jetpack Compose via `SentryNavigationListener`
* **[Navigation 3](https://developer.android.com/guide/navigation/navigation-3)** (`androidx.navigation3`) for Jetpack Compose via `SentryNavEffect`

If you're using `NavController`, follow the Navigation 2 setup below. If you're using `NavDisplay`, skip to [Navigation 3](https://docs.sentry.io/platforms/android/integrations/navigation.md#navigation-3).

## [Navigation 2](https://docs.sentry.io/platforms/android/integrations/navigation.md#navigation-2)

`SentryNavigationListener` for Navigation 2 integrations lets you observe destination changes reported by your `NavController`. Upon every destination change the listener:

* starts a new navigation transaction and binds it to the current [Sentry scope](https://docs.sentry.io/platforms/android/enriching-events/scopes.md)†
* emits a breadcrumb
* updates the currently tracked screen name

`SentryNavigationListener` can be used with:

* [Classic Views](https://docs.sentry.io/platforms/android/integrations/navigation.md#classic-views)
* [Jetpack Compose](https://docs.sentry.io/platforms/android/integrations/navigation.md#jetpack-compose)

† A new navigation transaction will *not* be bound if another unfinished transaction is already bound to the current scope.

### [Classic Views](https://docs.sentry.io/platforms/android/integrations/navigation.md#classic-views)

#### [Install](https://docs.sentry.io/platforms/android/integrations/navigation.md#install)

Install the [Android SDK](https://docs.sentry.io/platforms/android.md), then add the `sentry-android-navigation` dependency using Gradle:

```groovy
implementation 'io.sentry:sentry-android:8.61.0'
implementation 'io.sentry:sentry-android-navigation:8.61.0'
```

*Other available variations of the above snippet: kotlin*

#### [Configure](https://docs.sentry.io/platforms/android/integrations/navigation.md#configure)

Registration of the `SentryNavigationListener` should happen in the appropriate lifecycle callbacks of the entity that owns your `NavController`:

```kotlin
import androidx.navigation.NavController
import io.sentry.android.navigation.SentryNavigationListener

private val navController = findNavController(R.id.nav_host)
private val sentryNavListener = SentryNavigationListener(
  enableNavigationBreadcrumbs = true, // enabled by default
  enableNavigationTracing = true  // enabled by default
)

override fun onResume() {
  super.onResume()
  navController.addOnDestinationChangedListener(sentryNavListener)
}

override fun onPause() {
  super.onPause()
  navController.removeOnDestinationChangedListener(sentryNavListener)
}
```

*Other available variations of the above snippet: java*

#### [Verify](https://docs.sentry.io/platforms/android/integrations/navigation.md#verify)

The snippet below includes a sample `Fragment` with example navigation events. It captures an intentional message, so you can test that everything is working as soon as you set it up:

```kotlin
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.navigation.NavController
import io.sentry.android.navigation.SentryNavigationListener
import io.sentry.Sentry

class HomeFragment : Fragment() {

  private val sentryNavListener = SentryNavigationListener()

  override fun onCreateView(
    inflater: LayoutInflater,
    container: ViewGroup?,
    savedInstanceState: Bundle?
  ): View {
    // generated databinding
    return FragmentHomeBinding.inflate(inflater).apply {
        this.sendMessage.setOnClickListener {
            Sentry.captureMessage("Some message.")
        }
        this.openFirstFragment.setOnClickListener {
          findNavController().navigate(R.id.fragment_a)
        }
        this.openSecondFragment.setOnClickListener {
          findNavController().navigate(R.id.fragment_b)
        }
    }.root
  }

  override fun onResume() {
    super.onResume()
    findNavController().addOnDestinationChangedListener(sentryNavListener)
  }

  override fun onPause() {
    super.onPause()
    findNavController().removeOnDestinationChangedListener(sentryNavListener)
  }
}
```

### [Jetpack Compose](https://docs.sentry.io/platforms/android/integrations/navigation.md#jetpack-compose)

#### [Auto-Installation with the Sentry Android Gradle Plugin](https://docs.sentry.io/platforms/android/integrations/navigation.md#auto-installation-with-the-sentry-android-gradle-plugin)

If you're using the [Sentry Android Gradle Plugin](https://docs.sentry.io/platforms/android/configuration/gradle.md), it will automatically register `SentryNavigationListener` with any `NavHostController`s you create via [`rememberNavController`](https://developer.android.com/reference/kotlin/androidx/navigation/compose/rememberNavController.composable).

Be sure not to call Sentry's `withSentryObservableEffect` method (discussed [below](https://docs.sentry.io/platforms/android/integrations/navigation.md#configure-1)) on a `NavHostController` that the Sentry Android Gradle Plugin already auto-instruments. Doing so can register multiple `SentryNavigationListener`s and produce duplicate navigation data.

##### [Install](https://docs.sentry.io/platforms/android/integrations/navigation.md#install-1)

Add the Sentry Android Gradle plugin in your `build.gradle` file:

```groovy
plugins {
  id "io.sentry.android.gradle" version "6.24.0"
}
```

*Other available variations of the above snippet: kotlin*

Learn more about the Sentry Android Gradle plugin in our [Gradle](https://docs.sentry.io/platforms/android/configuration/gradle.md) documentation. Details about auto-installation can be found in the [Auto Instrumentation](https://docs.sentry.io/platforms/android/configuration/gradle.md#tracing-auto-instrumentation) section.

##### [Disable](https://docs.sentry.io/platforms/android/integrations/navigation.md#disable)

If you want to disable the plugin's automatic Compose instrumentation, including Navigation 2 instrumentation, add the following to your `build.gradle` file:

```groovy
import java.util.EnumSet
import io.sentry.android.gradle.extensions.InstrumentationFeature

sentry {
  tracingInstrumentation {
    enabled = true
    features = EnumSet.allOf(InstrumentationFeature) - InstrumentationFeature.COMPOSE
  }
}
```

*Other available variations of the above snippet: kotlin*

#### [Manual Installation](https://docs.sentry.io/platforms/android/integrations/navigation.md#manual-installation)

##### [Install](https://docs.sentry.io/platforms/android/integrations/navigation.md#install-2)

Sentry captures data via a `NavHostController.withSentryObservableEffect` extension function that registers a `SentryNavigationListener` under the hood. To use it, install the Android SDK, then add the `sentry-compose-android` dependency in Gradle:

```groovy
implementation 'io.sentry:sentry-android:8.61.0'
implementation 'io.sentry:sentry-compose-android:8.61.0'
```

*Other available variations of the above snippet: kotlin*

##### [Configure](https://docs.sentry.io/platforms/android/integrations/navigation.md#configure-1)

Configuration should happen in the `@Composable` function that owns your `NavHost`:

```kotlin
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.rememberNavController
import io.sentry.compose.withSentryObservableEffect

val navController = rememberNavController().withSentryObservableEffect(
  enableNavigationBreadcrumbs = true, // enabled by default
  enableNavigationTracing = true  // enabled by default
)

NavHost(
  navController = navController
) {
  ...
}
```

#### [Verify](https://docs.sentry.io/platforms/android/integrations/navigation.md#verify-1)

This snippet includes a sample `Activity` with example navigation events between composables. It captures an intentional message, so you can test that everything is working as soon as you set it up:

```kotlin
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.navigation.NavHostController
import androidx.navigation.NavType
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import io.sentry.Sentry
import io.sentry.compose.withSentryObservableEffect

class ComposeActivity : ComponentActivity() {

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    setContent {
      val navController = rememberNavController().withSentryObservableEffect()
      SampleNavigation(navController)
    }
  }
}

@Composable
fun SampleNavigation(navController: NavHostController) {
  NavHost(
    navController = navController,
    startDestination = "first"
  ) {
    composable("first") {
      First(
        navigateToSecond = { navController.navigate("second") }
      )
    }
    composable("second") {
      Second()
    }
  }
}

@Composable
fun First(
  navigateToSecond: () -> Unit
) {
  Column(
    verticalArrangement = Arrangement.Center,
    horizontalAlignment = Alignment.CenterHorizontally,
    modifier = Modifier.fillMaxSize()
  ) {
    Button(
      onClick = { navigateToSecond() },
      modifier = Modifier.padding(top = 32.dp)
    ) {
      Text("Navigate to Second")
    }
    Button(
      onClick = { Sentry.captureMessage("Some message from Compose.") },
      modifier = Modifier.padding(top = 32.dp)
    ) {
      Text("Message from Compose")
    }
  }
}

@Composable
fun Second() {
  Column(
    verticalArrangement = Arrangement.Center,
    horizontalAlignment = Alignment.CenterHorizontally,
    modifier = Modifier.fillMaxSize()
  ) {
    Text("Second Screen")
  }
}
```

### [Scrub Sensitive Info from the Recorded Breadcrumb/Transaction](https://docs.sentry.io/platforms/android/integrations/navigation.md#scrub-sensitive-info-from-the-recorded-breadcrumbtransaction)

`SentryNavigationListener` captures route arguments as additional data on breadcrumbs and transactions. If the arguments contain PII, you can strip them out with `BeforeBreadcrumbCallback` and `BeforeSendTransactionCallback`.†

To do so, [manually initialize](https://docs.sentry.io/platforms/android/manual-setup.md#configuration-via-sentryoptions) the SDK and add the following snippet:

```kotlin
import io.sentry.android.core.SentryAndroid
import io.sentry.SentryOptions.BeforeBreadcrumbCallback
import io.sentry.SentryOptions.BeforeSendTransactionCallback
import io.sentry.android.navigation.SentryNavigationListener

SentryAndroid.init(this) { options ->
  options.beforeBreadcrumb = BeforeBreadcrumbCallback { breadcrumb, hint ->
    if (SentryNavigationListener.NAVIGATION_OP == breadcrumb.category) {
      breadcrumb.data.remove("from_arguments")
      breadcrumb.data.remove("to_arguments")
    }
    breadcrumb
  }

  options.beforeSendTransaction = BeforeSendTransactionCallback { transaction, _ ->
    val trace = transaction.contexts.trace
    if (SentryNavigationListener.NAVIGATION_OP == trace?.operation) {
      trace.setData("arguments", null)
    }
    transaction
  }
}
```

† The snippet above removes route arguments, but it doesn't scrub route names. Define routes with stable names and placeholders, such as `profile/{user_id}`, and pass only sanitized values as arguments. Avoid putting PII directly in route names, as they can appear in breadcrumb `from` and `to` fields, transaction names, and screen names.

## [Navigation 3](https://docs.sentry.io/platforms/android/integrations/navigation.md#navigation-3)

Navigation 3 support is available in `sentry-android-navigation3` `8.60.0` and later.

See the [Limitations](https://docs.sentry.io/platforms/android/integrations/navigation.md#limitations) section below for additional restrictions.

Android's [Navigation 3 library](https://developer.android.com/guide/navigation/navigation-3) expects apps to manage their own back stack and to push and pop entries in order to navigate. `SentryNavEffect` instruments the back stack passed to your `NavDisplay`. By default, every time the top of the back stack changes, `SentryNavEffect` will:

* start a new navigation transaction and binds it to the current [Sentry scope](https://docs.sentry.io/platforms/android/enriching-events/scopes.md)†
* emit a breadcrumb
* update the currently tracked screen name
* record the latest 5 entries from the back stack as event context

See [Customize Nav3 Options](https://docs.sentry.io/platforms/android/integrations/navigation.md#customize-nav3-options) to learn how to modify those defaults.

† A new navigation transaction will *not* be started if another unfinished transaction is already bound to the current scope.

### [Install](https://docs.sentry.io/platforms/android/integrations/navigation.md#install-3)

Install the Android SDK and add the `sentry-android-navigation3` library:

```groovy
dependencies {
  implementation 'io.sentry:sentry-android:8.61.0'
  implementation 'io.sentry:sentry-android-navigation3:8.61.0'
}
```

*Other available variations of the above snippet: kotlin*

### [Configure](https://docs.sentry.io/platforms/android/integrations/navigation.md#configure-2)

To use `SentryNavEffect`, add it to the composable that owns your `NavDisplay` and pass your back stack to both:

```kotlin
import androidx.navigation3.runtime.NavKey
import androidx.navigation3.runtime.rememberNavBackStack
import androidx.navigation3.ui.NavDisplay
import io.sentry.compose.navigation3.SentryNavEffect
import kotlinx.serialization.Serializable

@Composable
fun AppNavigation() {
  // Create your back stack like usual.
  val backStack = rememberNavBackStack(Home)

  // Place SentryNavEffect in the same composable as your NavDisplay and call the
  // effect first. Provide a BackStackEntryMapper with the values you want to
  // show up in Sentry. Omit PII and other sensitive info.
  SentryNavEffect(
    backStack = backStack,
    backStackEntryMapper = { entry ->
      when (entry) {
        is Home -> SentryBackStackEntry("Home")
        is ProductDetail ->
          SentryBackStackEntry("ProductDetail", mapOf("product_id" to entry.productId))
        ...
      }
    }
  )

  // Configure your NavDisplay like usual.
  NavDisplay(
    backStack = backStack,
    ...
  )
}
```

**Note:** Be sure to call `SentryNavEffect` before `NavDisplay` so the effect's transaction-creating machinery can be set up before any spans are produced by the initial nav destination. Otherwise, initial destination spans may be lost or parented under the wrong transaction.

Provide a `BackStackEntryMapper` that maps each entry in your back stack to the name and optional arguments you want displayed in Sentry. Keep names stable and arguments lightweight and performant. Avoid using `::class.simpleName` or anything else that R8 will obfuscate.

Values returned from `BackStackEntryMapper` are **not** scrubbed by the Sentry SDK of PII or other potentially sensitive information before being sent to Sentry. Only return names and arguments known to be safe, or sanitize them yourself in your `BackStackEntryMapper`.

Multiple, simultaneously active `SentryNavEffect` instances writing to the same [Sentry scope](https://docs.sentry.io/platforms/android/enriching-events/scopes.md) are ***not*** supported. Violating this restriction can result in duplicated or interleaved data and undefined transaction behavior.

### [Customize Nav3 Options](https://docs.sentry.io/platforms/android/integrations/navigation.md#customize-nav3-options)

Use `SentryNavOptions` to disable breadcrumbs or transactions, or to limit how much of the back stack Sentry stores with captured events.

```kotlin
import io.sentry.compose.navigation3.SentryNavEffect
import io.sentry.compose.navigation3.SentryNavOptions

SentryNavEffect(
  backStack = backStack,
  ...
  options = SentryNavOptions {
    enableNavigationBreadcrumbs = true
    enableNavigationTransactions = true
    captureBackStack = true
    maxCapturedBackStackEntries = 5
  },
)
```

If you want to turn screen names on or off, configure the main SDK options when you initialize the Sentry SDK:

```kotlin
import io.sentry.android.core.SentryAndroid

SentryAndroid.init(this) { options ->
  options.isEnableScreenTracking = true
}
```

### [Verify](https://docs.sentry.io/platforms/android/integrations/navigation.md#verify-2)

The snippet below defines `Home` and `ProductDetail` routes and a composable for displaying them. It adds both routes to an in-memory back stack and captures a message from the `ProductDetail` destination so you can verify the navigation data the Sentry SDK emits.

The snippet assumes the default `SentryNavOptions`. Screen names additionally require screen tracking to be enabled.

```kotlin
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.remember
import androidx.navigation3.runtime.NavKey
import androidx.navigation3.runtime.entryProvider
import androidx.navigation3.ui.NavDisplay
import io.sentry.Sentry
import io.sentry.compose.navigation3.SentryBackStackEntry
import io.sentry.compose.navigation3.SentryNavEffect
import kotlinx.coroutines.delay

data object Home : NavKey

data class ProductDetail(val productId: String) : NavKey

@Composable
fun AppNavigation() {
  val backStack = remember { mutableStateListOf<NavKey>(Home) }

  SentryNavEffect(
    backStack = backStack,
    backStackEntryMapper = { entry ->
      when (entry) {
        is Home -> SentryBackStackEntry("Home")
        is ProductDetail -> SentryBackStackEntry(
          name = "ProductDetail",
          arguments = mapOf("product_id" to entry.productId),
        )
        else -> null
      }
    },
  )

  NavDisplay(
    backStack = backStack,
    onBack = { backStack.removeLastOrNull() },
    entryProvider = entryProvider {
      entry<Home> {
        Button(
          onClick = {
            backStack.add(ProductDetail(productId = "nav3-smoke-test"))
          },
        ) {
          Text("Open product detail")
        }
      }
      entry<ProductDetail> {
        val parentSpan = Sentry.getSpan()

        // Generate a fake span so that the nav transaction isn't dropped after its idle timeout
        // expires.
        LaunchedEffect(parentSpan) {
          val span =
            parentSpan?.startChild(
              "test.navigation.product_detail",
              "ProductDetail work",
            )
          try {
            delay(100)
          } finally {
            span?.finish()
          }
        }

        Button(onClick = { Sentry.captureMessage("Navigation 3 smoke test") }) {
          Text("Send test message")
        }
      }
    },
  )
}
```

Tap **Open product detail**, then **Send test message**. In Sentry, open the event for `Navigation 3 smoke test` and verify the following:

* In **Breadcrumbs**, look for a navigation breadcrumb whose destination is `ProductDetail`. It should include `product_id` under `to_arguments`.

- In **Contexts**, check the Navigation context for a back stack containing `/ProductDetail` followed by `/Home`, with `product_id` under `entry_arguments`. Also check the App context for View Names containing `/ProductDetail`.

* If tracing is enabled and the transaction is sampled, open the [Traces](https://sentry.io/orgredirect/organizations/:orgslug/traces/) view after the [idle timeout](https://docs.sentry.io/platforms/android/configuration/options.md#idleTimeout) and inspect the `ProductDetail` transaction. Emitted nav data should appear under **Attributes → Arguments**, **Contexts → Navigation**, and **Breadcrumbs**.

### [Using kotlinx.serialization](https://docs.sentry.io/platforms/android/integrations/navigation.md#using-kotlinxserialization)

If your Nav3 back stack uses `@Serializable` keys, you can use [Kotlin serialization](https://kotlinlang.org/docs/serialization.html) to produce stable entry names. For performance reasons, don't serialize keys in their entirety as `arguments` if they might be large, deeply nested, or contain PII or other sensitive information.

```kotlin
import androidx.navigation3.runtime.NavKey
import io.sentry.compose.navigation3.BackStackEntryMapper
import io.sentry.compose.navigation3.SentryBackStackEntry
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
@SerialName("Home")
data class Home(val userName: String) : NavKey

@Serializable
@SerialName("ProductDetail")
data class ProductDetail(val userName: String, val productId: String) : NavKey

val backStackItemMapper = BackStackEntryMapper<NavKey> { entry ->
  when (entry) {
    is Home -> SentryBackStackEntry(Home.serializer().descriptor.serialName)
    is ProductDetail -> SentryBackStackEntry(
      name = ProductDetail.serializer().descriptor.serialName,
      arguments = mapOf("product_id" to entry.productId)
    )
    ...
  }
}
```

### [Limitations](https://docs.sentry.io/platforms/android/integrations/navigation.md#limitations)

`SentryNavEffect` treats the top entry in your back stack as the current screen. That works well for most Nav3 setups, but it means that `SentryNavEffect` does ***not***:

* understand the concept of Nav3 [Scenes](https://developer.android.com/guide/navigation/navigation-3/scenes) or multi-pane layouts
* track multiple back stacks
* permit multiple, simultaneously active `SentryNavEffect` instances
* make special accommodations for [predictive back](https://developer.android.com/guide/navigation/custom-back/predictive-back-gesture) gestures (e.g., spans produced by predictively rendered composables may appear under the current destination's transaction)

## [Navigation 2 and Navigation 3 Interoperability](https://docs.sentry.io/platforms/android/integrations/navigation.md#navigation-2-and-navigation-3-interoperability)

Navigation 2 and Navigation 3 can coexist in the same app. Their Sentry integrations observe navigation independently, but both write to the same Sentry scope by default.

If both integrations observe the same navigation flow – for instance, a Nav2 destination hosts a Nav3 flow – Sentry can receive duplicate breadcrumbs or conflicting screen names and transactions. Choose one integration to report each flow, or configure the integrations so they don't report the same data to the same scope.

If you're using the Sentry Android Gradle Plugin, be sure to account for automatically enabled Navigation 2 instrumentation when deciding which integration should report a flow.
