diff --git a/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml b/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml
index cf0bc55e5e0..9b103274623 100644
--- a/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml
+++ b/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml
@@ -102,12 +102,12 @@
android:exported="false" />
diff --git a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/GitHubService.kt b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/GitHubService.kt
index 1e8b5975c71..d5a1b8e4217 100644
--- a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/GitHubService.kt
+++ b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/GitHubService.kt
@@ -1,9 +1,11 @@
package io.sentry.samples.android
+import okhttp3.ResponseBody
import retrofit2.Call
import retrofit2.http.GET
import retrofit2.http.Path
import retrofit2.http.Query
+import retrofit2.http.Url
interface GitHubService {
@@ -14,6 +16,10 @@ interface GitHubService {
@Path("user") user: String,
@Query("per_page") perPage: Int,
): List
+
+ @GET fun routeWorkRequest(@Url url: String): Call
+
+ @GET suspend fun routeWorkRequestAsync(@Url url: String): ResponseBody
}
class Repo {
diff --git a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/GithubAPI.kt b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/GithubAPI.kt
index eb75569fff6..df3ee7d33fa 100644
--- a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/GithubAPI.kt
+++ b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/GithubAPI.kt
@@ -7,7 +7,7 @@ import okhttp3.OkHttpClient
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
-object GithubAPI {
+object GitHubApi {
private val client =
OkHttpClient.Builder()
.eventListener(SentryOkHttpEventListener())
diff --git a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MainActivity.kt b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MainActivity.kt
index 31bf86c7db2..54214b8ff55 100644
--- a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MainActivity.kt
+++ b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MainActivity.kt
@@ -900,7 +900,10 @@ fun IntegrationsScreen() {
OutlinedButton(
onClick = {
activity.startActivity(
- Intent(activity, io.sentry.samples.android.navigation.Nav2SetupActivity::class.java)
+ Intent(
+ activity,
+ io.sentry.samples.android.navigation.nav2.Nav2SetupActivity::class.java,
+ )
)
},
modifier = Modifier,
diff --git a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/SecondActivity.kt b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/SecondActivity.kt
index d46573855d6..b8dec7e1059 100644
--- a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/SecondActivity.kt
+++ b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/SecondActivity.kt
@@ -67,7 +67,7 @@ class SecondActivity : AppCompatActivity() {
currentSpan?.startChild("updateRepos", javaClass.simpleName)
?: Sentry.startTransaction("updateRepos", "task")
- GithubAPI.service
+ GitHubApi.service
.listRepos(binding.editRepo.text.toString())
.enqueue(
object : Callback> {
diff --git a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/ThirdFragment.kt b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/ThirdFragment.kt
index 28b30e78acb..a99185eb48f 100644
--- a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/ThirdFragment.kt
+++ b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/ThirdFragment.kt
@@ -15,7 +15,7 @@ class ThirdFragment : Fragment(R.layout.third_fragment) {
val span = Sentry.getSpan()
val child = span?.startChild("calc")
- GithubAPI.service
+ GitHubApi.service
.listRepos("getsentry")
.enqueue(
object : Callback> {
diff --git a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/compose/ComposeActivity.kt b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/compose/ComposeActivity.kt
index b27da13c770..4fd1d74ef7a 100644
--- a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/compose/ComposeActivity.kt
+++ b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/compose/ComposeActivity.kt
@@ -53,7 +53,7 @@ import io.sentry.android.replay.sentryReplayUnmask
import io.sentry.compose.SentryTraced
import io.sentry.compose.SentryUserFeedbackButton
import io.sentry.compose.withSentryObservableEffect
-import io.sentry.samples.android.GithubAPI
+import io.sentry.samples.android.GitHubApi
import io.sentry.samples.android.R as IR
import io.sentry.samples.android.SharedState
import kotlinx.coroutines.launch
@@ -160,7 +160,7 @@ fun Github(user: String = "getsentry", perPage: Int = 30) {
LaunchedEffect(perPage) {
result =
try {
- GithubAPI.service.listReposAsync(user.text, perPage).random().full_name
+ GitHubApi.service.listReposAsync(user.text, perPage).random().full_name
} catch (e: Throwable) {
"error"
}
@@ -190,7 +190,7 @@ fun Github(user: String = "getsentry", perPage: Int = 30) {
scope.launch {
result =
try {
- GithubAPI.service.listReposAsync(user.text, perPage).random().full_name
+ GitHubApi.service.listReposAsync(user.text, perPage).random().full_name
} catch (e: Throwable) {
"error"
}
diff --git a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/Nav2Routes.kt b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/Nav2Routes.kt
deleted file mode 100644
index 7b278159d81..00000000000
--- a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/Nav2Routes.kt
+++ /dev/null
@@ -1,400 +0,0 @@
-package io.sentry.samples.android.navigation
-
-import android.os.Bundle
-import android.view.ViewGroup
-import androidx.core.os.bundleOf
-import androidx.navigation.NavDestination
-import io.sentry.Sentry
-import io.sentry.protocol.SentryTransaction
-import io.sentry.samples.android.R
-import kotlin.coroutines.resume
-import kotlinx.coroutines.suspendCancellableCoroutine
-
-internal object Nav2RouteNames {
-
- const val LANDING = "Landing"
- const val HOME = "Home"
- const val PRODUCT_LIST = "ProductList"
- const val DEEP_LINK = "DeepLink"
- const val PRODUCT_DETAIL = "ProductDetail"
- const val CHECKOUT = "Checkout"
- const val CONFIRMATION = "Confirmation"
- const val PROMO_DIALOG = "PromoDialog"
- const val SHARE_SHEET = "ShareSheet"
-}
-
-internal object Nav2Args {
-
- const val ROUTE_NAME = "route_name"
- const val PRODUCT_ID = "product_id"
- const val SOURCE = "source"
- const val CAMPAIGN = "campaign"
- const val ORDER_ID = "order_id"
- const val PROMO_ID = "promo_id"
- const val SCENARIO = "scenario"
-}
-
-internal data class Nav2DisplayedArgument(val key: String, val label: String = key)
-
-internal data class Nav2RouteSpec(
- val routeName: String,
- val title: String,
- val description: String? = null,
- val displayedArguments: List = emptyList(),
-)
-
-internal object Nav2RouteSpecs {
- val landing =
- Nav2RouteSpec(
- routeName = Nav2RouteNames.LANDING,
- title = "Landing",
- description =
- "Activity ui.load transactions are configured when the Sentry SDK initializes, so this " +
- "sample cannot truly disable them at launch time. Instead, we cancel and clear the " +
- "current ui.load transaction when you land here.",
- )
-
- val home =
- Nav2RouteSpec(
- routeName = Nav2RouteNames.HOME,
- title = "Home",
- description =
- "Start a product flow, then use the Sentry UI to inspect route " +
- "transactions, breadcrumbs, and screen tracking.",
- )
-
- val deepLink =
- Nav2RouteSpec(
- routeName = Nav2RouteNames.DEEP_LINK,
- title = "Deep Link (Fragments)",
- description =
- "Simulates opening a fragment deep link that builds a synthetic backstack before landing " +
- "on a detail destination.",
- )
-
- val productList =
- Nav2RouteSpec(
- routeName = Nav2RouteNames.PRODUCT_LIST,
- title = "Product List",
- description = "This route starts the product journey.",
- )
-
- val productDetail =
- Nav2RouteSpec(
- routeName = Nav2RouteNames.PRODUCT_DETAIL,
- title = "Product Detail",
- description = "",
- displayedArguments =
- listOf(
- Nav2DisplayedArgument(Nav2Args.PRODUCT_ID, "productId"),
- Nav2DisplayedArgument(Nav2Args.SOURCE),
- Nav2DisplayedArgument(Nav2Args.CAMPAIGN),
- ),
- )
-
- val checkout =
- Nav2RouteSpec(
- routeName = Nav2RouteNames.CHECKOUT,
- title = "Checkout",
- description = "",
- displayedArguments = listOf(Nav2DisplayedArgument(Nav2Args.PRODUCT_ID, "productId")),
- )
-
- val confirmation =
- Nav2RouteSpec(
- routeName = Nav2RouteNames.CONFIRMATION,
- title = "Confirmation",
- description = "End of the product flow.",
- displayedArguments = listOf(Nav2DisplayedArgument(Nav2Args.ORDER_ID, "orderId")),
- )
-
- val promoDialog =
- Nav2RouteSpec(
- routeName = Nav2RouteNames.PROMO_DIALOG,
- title = "Promo Dialog",
- description =
- "This modal is a real Nav destination, so its breadcrumbs and route transaction should " +
- "stand on their own.",
- displayedArguments = listOf(Nav2DisplayedArgument(Nav2Args.PROMO_ID, "promoId")),
- )
-
- val shareSheet =
- Nav2RouteSpec(
- routeName = Nav2RouteNames.SHARE_SHEET,
- title = "Share Sheet",
- description =
- "This sheet stays attached to the current route so you can compare an overlay against a " +
- "real destination.",
- displayedArguments = listOf(Nav2DisplayedArgument(Nav2Args.PRODUCT_ID, "productId")),
- )
-
- fun get(routeName: String): Nav2RouteSpec =
- when (routeName) {
- Nav2RouteNames.LANDING -> landing
- Nav2RouteNames.HOME -> home
- Nav2RouteNames.DEEP_LINK -> deepLink
- Nav2RouteNames.PRODUCT_LIST -> productList
- Nav2RouteNames.PRODUCT_DETAIL -> productDetail
- Nav2RouteNames.CHECKOUT -> checkout
- Nav2RouteNames.CONFIRMATION -> confirmation
- Nav2RouteNames.PROMO_DIALOG -> promoDialog
- Nav2RouteNames.SHARE_SHEET -> shareSheet
- else -> Nav2RouteSpec(routeName = routeName, title = routeName)
- }
-}
-
-internal sealed class Nav2Destination(
- val id: Int,
- val routeName: String,
- val arguments: Bundle = Bundle.EMPTY,
-) {
-
- data object Landing : Nav2Destination(R.id.nav2_landing, Nav2RouteNames.LANDING)
-
- data object Home : Nav2Destination(R.id.nav2_home, Nav2RouteNames.HOME)
-
- data object ProductList : Nav2Destination(R.id.nav2_product_list, Nav2RouteNames.PRODUCT_LIST)
-
- data object DeepLink : Nav2Destination(R.id.nav2_deep_link, Nav2RouteNames.DEEP_LINK)
-
- data class ProductDetail(
- val productId: String,
- val source: String,
- val campaign: String = "",
- ) :
- Nav2Destination(
- R.id.nav2_product_detail,
- Nav2RouteNames.PRODUCT_DETAIL,
- bundleOf(
- Nav2Args.PRODUCT_ID to productId,
- Nav2Args.SOURCE to source,
- Nav2Args.CAMPAIGN to campaign,
- ),
- )
-
- data class Checkout(val productId: String) :
- Nav2Destination(
- R.id.nav2_checkout,
- Nav2RouteNames.CHECKOUT,
- bundleOf(Nav2Args.PRODUCT_ID to productId),
- )
-
- data class Confirmation(val orderId: String) :
- Nav2Destination(
- R.id.nav2_confirmation,
- Nav2RouteNames.CONFIRMATION,
- bundleOf(Nav2Args.ORDER_ID to orderId),
- )
-
- data class PromoDialog(val promoId: String, val scenario: Nav2Scenario) :
- Nav2Destination(
- R.id.nav2_promo_dialog,
- Nav2RouteNames.PROMO_DIALOG,
- bundleOf(Nav2Args.PROMO_ID to promoId, Nav2Args.SCENARIO to scenario.name),
- )
-
- data class ShareSheet(val productId: String, val scenario: Nav2Scenario) :
- Nav2Destination(
- R.id.nav2_share_sheet,
- Nav2RouteNames.SHARE_SHEET,
- bundleOf(Nav2Args.PRODUCT_ID to productId, Nav2Args.SCENARIO to scenario.name),
- )
-}
-
-internal enum class Nav2Scenario(val label: String, val showTab: Boolean = true) {
- LANDING(Nav2RouteNames.LANDING, showTab = false),
- COMPOSE("Compose"),
- FRAGMENTS("Fragments"),
- DEEP_LINK("Deep Link (Fragments)"),
- PERFORMANCE("Performance"),
-}
-
-/**
- * Optional work the Nav2 sample app can perform when a route becomes active / when navigating to a
- * new destination.
- */
-internal enum class RouteWorkOption(val label: String, val tagName: String) {
-
- /**
- * Executes an HTTP request in the new nav destination.
- *
- * For composables, the request is executed in a composable *Effect.
- */
- HTTP_REQUEST("HTTP request", "http_request"),
-
- /**
- * Generates a child span in the new nav destination.
- *
- * For composables, the span is generated directly in the composable body (i.e., during
- * (re)composition), rather than via an *Effect. That's bad practice generally, but it lets us
- * test whether our nav transactions can pick up work done in the destination during composition.
- */
- MANUAL_CHILD_SPAN("Manual child span", "manual_child_span"),
-}
-
-internal fun Nav2Destination.routeSpec(): Nav2RouteSpec = Nav2RouteSpecs.get(routeName)
-
-internal fun Nav2Destination.displayRoute(): String = routeSpec().displayRoute(arguments)
-
-internal fun Nav2RouteSpec.displayArguments(arguments: Bundle?): List> =
- displayedArguments.mapNotNull { argument ->
- arguments
- ?.getString(argument.key)
- ?.takeIf { value -> value.isNotEmpty() }
- ?.let { value -> argument.label to value }
- }
-
-internal fun Nav2RouteSpec.displayArguments(
- arguments: Map
-): List> = displayedArguments.mapNotNull { argument ->
- arguments[argument.key]
- ?.toString()
- ?.takeIf { value -> value.isNotEmpty() }
- ?.let { value -> argument.label to value }
-}
-
-internal fun Nav2RouteSpec.displayRoute(arguments: Bundle?): String =
- displayRoute(displayArguments(arguments))
-
-internal fun Nav2RouteSpec.displayRoute(arguments: Map): String =
- displayRoute(displayArguments(arguments))
-
-private fun Nav2RouteSpec.displayRoute(displayArguments: List>): String =
- if (displayArguments.isEmpty()) {
- "/$routeName"
- } else {
- "/$routeName { ${displayArguments.toDisplayString()} }"
- }
-
-internal fun List>.toDisplayString(): String =
- joinToString(", ") { (label, value) -> "$label=$value" }
-
-internal fun MutableList.resetTo(destination: Nav2Destination) {
- clear()
- add(destination)
-}
-
-internal fun Nav2Destination.matches(destination: NavDestination, arguments: Bundle?): Boolean =
- id == destination.id && argumentsMatch(arguments)
-
-private fun Nav2Destination.argumentsMatch(arguments: Bundle?): Boolean =
- when (this) {
- Nav2Destination.Home,
- Nav2Destination.Landing,
- Nav2Destination.ProductList,
- Nav2Destination.DeepLink -> true
- is Nav2Destination.ProductDetail ->
- arguments?.getString(Nav2Args.PRODUCT_ID) == productId &&
- arguments.getString(Nav2Args.SOURCE) == source &&
- arguments.getString(Nav2Args.CAMPAIGN).orEmpty() == campaign
- is Nav2Destination.Checkout -> arguments?.getString(Nav2Args.PRODUCT_ID) == productId
- is Nav2Destination.Confirmation -> arguments?.getString(Nav2Args.ORDER_ID) == orderId
- is Nav2Destination.PromoDialog -> arguments?.getString(Nav2Args.PROMO_ID) == promoId
- is Nav2Destination.ShareSheet -> arguments?.getString(Nav2Args.PRODUCT_ID) == productId
- }
-
-internal fun NavDestination.toNav2Destination(arguments: Bundle?): Nav2Destination? =
- when (id) {
- R.id.nav2_landing -> Nav2Destination.Landing
- R.id.nav2_home -> Nav2Destination.Home
- R.id.nav2_product_list -> Nav2Destination.ProductList
- R.id.nav2_deep_link -> Nav2Destination.DeepLink
- R.id.nav2_product_detail ->
- Nav2Destination.ProductDetail(
- productId = arguments?.getString(Nav2Args.PRODUCT_ID).orEmpty(),
- source = arguments?.getString(Nav2Args.SOURCE).orEmpty(),
- campaign = arguments?.getString(Nav2Args.CAMPAIGN).orEmpty(),
- )
- R.id.nav2_checkout ->
- Nav2Destination.Checkout(arguments?.getString(Nav2Args.PRODUCT_ID).orEmpty())
- R.id.nav2_confirmation ->
- Nav2Destination.Confirmation(arguments?.getString(Nav2Args.ORDER_ID).orEmpty())
- R.id.nav2_promo_dialog ->
- Nav2Destination.PromoDialog(
- promoId = arguments?.getString(Nav2Args.PROMO_ID).orEmpty(),
- scenario =
- arguments?.getString(Nav2Args.SCENARIO).orEmpty().toNav2Scenario()
- ?: Nav2Scenario.FRAGMENTS,
- )
- R.id.nav2_share_sheet ->
- Nav2Destination.ShareSheet(
- productId = arguments?.getString(Nav2Args.PRODUCT_ID).orEmpty(),
- scenario =
- arguments?.getString(Nav2Args.SCENARIO).orEmpty().toNav2Scenario()
- ?: Nav2Scenario.FRAGMENTS,
- )
- else -> null
- }
-
-internal fun NavDestination.routeName(): String = routeNameOrNull() ?: Nav2RouteNames.HOME
-
-private fun NavDestination.routeNameOrNull(): String? = route ?: label?.toString()?.replace(" ", "")
-
-private fun String.toNav2Scenario(): Nav2Scenario? =
- Nav2Scenario.entries.firstOrNull { scenario -> scenario.name == this }
-
-internal fun recordManualChildSpan(routeName: String) {
- val span =
- Sentry.getSpan()
- ?.startChild(
- "test.navigation.manual_span",
- "Nav2 /$routeName manual span",
- )
- span?.setData("sample.manual_span", true)
- span?.finish()
-}
-
-internal fun tagCurrentNav2Scenario(scenario: Nav2Scenario) {
- Sentry.getSpan()?.setTag(NAV2_SCENARIO_TAG, scenario.label)
-}
-
-internal fun SentryTransaction.nav2ScenarioLabel(): String =
- getTag(NAV2_SCENARIO_TAG) ?: UNKNOWN_NAV2_SCENARIO_LABEL
-
-internal suspend fun recordSimulatedBackgroundSpan(routeName: String) {
- val parentSpan = Sentry.getSpan()
- suspendCancellableCoroutine { continuation ->
- val worker = Thread {
- val span =
- parentSpan?.startChild(
- "test.navigation.background_work",
- "Nav2 /$routeName background work",
- )
- span?.setData("sample.background_work", true)
- try {
- Thread.sleep(BACKGROUND_WORK_MILLIS)
- } catch (e: InterruptedException) {
- Thread.currentThread().interrupt()
- } finally {
- span?.finish()
- if (continuation.isActive) {
- continuation.resume(Unit)
- }
- }
- }
- continuation.invokeOnCancellation { worker.interrupt() }
- worker.start()
- }
-}
-
-internal fun tagNav2SampleAction(action: String, route: String) {
- val span = Sentry.getSpan() ?: return
- span.setTag("sample_action", "nav2_$action")
- span.setTag("sample_nav2_route", route)
-}
-
-internal fun MutableList.popTrackedBackStack(popBackStack: () -> Boolean): Boolean {
- val popped = popBackStack()
- if (popped && size > 1) {
- removeAt(lastIndex)
- }
- return popped
-}
-
-internal const val SENTRY_FLUSH_TIMEOUT_MILLIS = 5000L
-internal const val BACKGROUND_WORK_MILLIS = 1000L
-internal const val NAV2_SCENARIO_TAG = "sample_nav2_scenario"
-internal const val UNKNOWN_NAV2_SCENARIO_LABEL = "Unknown"
-
-internal const val MATCH_PARENT = ViewGroup.LayoutParams.MATCH_PARENT
-internal const val WRAP_CONTENT = ViewGroup.LayoutParams.WRAP_CONTENT
diff --git a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/Nav2SetupActivity.kt b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/Nav2SetupActivity.kt
deleted file mode 100644
index cdd0c35f40e..00000000000
--- a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/Nav2SetupActivity.kt
+++ /dev/null
@@ -1,297 +0,0 @@
-package io.sentry.samples.android.navigation
-
-import android.os.Bundle
-import androidx.activity.compose.setContent
-import androidx.appcompat.app.AppCompatActivity
-import androidx.compose.animation.animateColorAsState
-import androidx.compose.animation.core.tween
-import androidx.compose.foundation.background
-import androidx.compose.foundation.clickable
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.defaultMinSize
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.safeDrawingPadding
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.rememberScrollState
-import androidx.compose.foundation.selection.toggleable
-import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.foundation.verticalScroll
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.automirrored.outlined.HelpOutline
-import androidx.compose.material3.Button
-import androidx.compose.material3.Card
-import androidx.compose.material3.CardDefaults
-import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.Icon
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.PlainTooltip
-import androidx.compose.material3.Switch
-import androidx.compose.material3.SwitchDefaults
-import androidx.compose.material3.Text
-import androidx.compose.material3.TooltipAnchorPosition
-import androidx.compose.material3.TooltipBox
-import androidx.compose.material3.TooltipDefaults
-import androidx.compose.material3.rememberTooltipState
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.rememberCoroutineScope
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.clip
-import androidx.compose.ui.draw.scale
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.Shape
-import androidx.compose.ui.semantics.Role
-import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.sp
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.launch
-
-/**
- * Activity for configuring the developer's experience in the [Nav2Activity], in particular which
- * transaction types should be active and which nav data the SDK should emit.
- */
-class Nav2SetupActivity : AppCompatActivity() {
-
- private var configuration by mutableStateOf(Nav2SampleConfig())
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- configuration = savedInstanceState?.nav2SampleConfiguration() ?: configuration
- setContent {
- Nav2SampleTheme {
- Nav2SetupScreen(
- configuration = configuration,
- onConfigurationChanged = { updatedConfiguration ->
- configuration = updatedConfiguration
- },
- onLaunch = {
- val previousOptions = currentNav2SampleConfigSnapshot()
- configuration.applyToCurrentOptions()
- startActivity(nav2LaunchIntent(configuration, previousOptions))
- },
- )
- }
- }
- }
-
- override fun onSaveInstanceState(outState: Bundle) {
- super.onSaveInstanceState(outState)
- outState.putNav2SampleConfiguration(configuration)
- }
-}
-
-@Composable
-private fun Nav2SetupScreen(
- configuration: Nav2SampleConfig,
- onConfigurationChanged: (Nav2SampleConfig) -> Unit,
- onLaunch: () -> Unit,
-) {
- Column(
- modifier =
- Modifier.fillMaxSize()
- .background(MaterialTheme.colorScheme.background)
- .safeDrawingPadding()
- .verticalScroll(rememberScrollState())
- .padding(24.dp),
- verticalArrangement = Arrangement.spacedBy(16.dp),
- ) {
- Text(
- text = "Navigation 2 Setup",
- style = MaterialTheme.typography.headlineMedium,
- fontWeight = FontWeight.Bold,
- color = MaterialTheme.colorScheme.onBackground,
- )
- Text(
- text =
- "Choose which auto-instrumentation features should be active before the Nav2 sample launches.",
- style = MaterialTheme.typography.bodyMedium,
- )
- Nav2SetupSection(title = "Navigation") {
- Nav2SetupCheckboxRow(
- label = "Navigation transactions",
- checked = configuration.enableNavigationTransactions,
- ) {
- onConfigurationChanged(configuration.copy(enableNavigationTransactions = it))
- }
- Nav2SetupCheckboxRow(
- label = "Navigation breadcrumbs",
- checked = configuration.enableNavigationBreadcrumbs,
- ) {
- onConfigurationChanged(configuration.copy(enableNavigationBreadcrumbs = it))
- }
- Nav2SetupCheckboxRow(
- label = "Screen tracking",
- checked = configuration.enableScreenTracking,
- ) {
- onConfigurationChanged(configuration.copy(enableScreenTracking = it))
- }
- }
- Nav2SetupSection(title = "Other auto-transactions") {
- Nav2SetupCheckboxRow(
- label = "Activity ui.load transaction",
- checked = configuration.enableActivityUiLoadTransaction,
- helpText = ACTIVITY_UI_LOAD_HELP_TEXT,
- ) {
- onConfigurationChanged(configuration.copy(enableActivityUiLoadTransaction = it))
- }
- Nav2SetupCheckboxRow(
- label = "User interaction transactions",
- checked = configuration.enableUserInteractionTransactions,
- ) {
- onConfigurationChanged(configuration.copy(enableUserInteractionTransactions = it))
- }
- }
- Nav2SetupSection(title = "Other breadcrumbs") {
- Nav2SetupCheckboxRow(
- label = "User interaction breadcrumbs",
- checked = configuration.enableUserInteractionBreadcrumbs,
- ) {
- onConfigurationChanged(configuration.copy(enableUserInteractionBreadcrumbs = it))
- }
- }
- Button(onClick = onLaunch, modifier = Modifier.fillMaxWidth()) {
- Text(
- "Launch Nav2 Sample",
- fontSize = 18.sp,
- )
- }
- }
-}
-
-@Composable
-private fun Nav2SetupSection(title: String, content: @Composable () -> Unit) {
- Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
- Text(
- text = title,
- style = MaterialTheme.typography.headlineSmall,
- fontWeight = FontWeight.Bold,
- color = MaterialTheme.colorScheme.onBackground,
- )
- Card(
- colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant),
- modifier = Modifier.fillMaxWidth(),
- ) {
- Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
- content()
- }
- }
- }
-}
-
-@Composable
-private fun Nav2SetupCheckboxRow(
- label: String,
- checked: Boolean,
- helpText: String? = null,
- onCheckedChange: (Boolean) -> Unit,
-) {
- val sentryPink = Color(0xFFC85B9C)
- val rowShape: Shape = RoundedCornerShape(12.dp)
- val rowBackground by
- animateColorAsState(
- targetValue = if (checked) sentryPink.copy(alpha = 0.14f) else Color.Transparent,
- animationSpec = tween(durationMillis = 220),
- label = "nav2-setup-toggle-background",
- )
- val switchColors =
- SwitchDefaults.colors(
- checkedTrackColor = sentryPink,
- checkedBorderColor = sentryPink,
- checkedThumbColor = Color.White,
- )
- Row(
- modifier =
- Modifier.fillMaxWidth()
- .clip(rowShape)
- .background(rowBackground, rowShape)
- .toggleable(value = checked, role = Role.Switch, onValueChange = onCheckedChange)
- .defaultMinSize(minHeight = 52.dp)
- .padding(horizontal = 12.dp, vertical = 6.dp),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.spacedBy(12.dp),
- ) {
- Switch(
- checked = checked,
- onCheckedChange = null,
- colors = switchColors,
- modifier = Modifier.scale(0.8f),
- )
- Row(
- modifier = Modifier.weight(1f),
- verticalAlignment = Alignment.CenterVertically,
- ) {
- Text(text = label, style = MaterialTheme.typography.titleMedium)
- if (helpText != null) {
- Nav2SetupHelpTooltip(helpText)
- }
- }
- }
-}
-
-@OptIn(ExperimentalMaterial3Api::class)
-@Composable
-private fun Nav2SetupHelpTooltip(text: String) {
- val tooltipState = rememberTooltipState(isPersistent = true)
- val scope = rememberCoroutineScope()
-
- LaunchedEffect(tooltipState.isVisible) {
- if (tooltipState.isVisible) {
- delay(4000)
- tooltipState.dismiss()
- }
- }
-
- TooltipBox(
- positionProvider = TooltipDefaults.rememberTooltipPositionProvider(TooltipAnchorPosition.Above),
- tooltip = { PlainTooltip { Text(text) } },
- state = tooltipState,
- ) {
- Icon(
- imageVector = Icons.AutoMirrored.Outlined.HelpOutline,
- contentDescription = "Activity ui.load transaction help",
- tint = MaterialTheme.colorScheme.onSurfaceVariant,
- modifier =
- Modifier.padding(start = 6.dp).size(20.dp).clickable {
- scope.launch { tooltipState.show() }
- },
- )
- }
-}
-
-private fun Bundle.putNav2SampleConfiguration(configuration: Nav2SampleConfig) {
- putBoolean("enable_navigation_transactions", configuration.enableNavigationTransactions)
- putBoolean("enable_navigation_breadcrumbs", configuration.enableNavigationBreadcrumbs)
- putBoolean("enable_screen_tracking", configuration.enableScreenTracking)
- putBoolean("enable_activity_ui_load_transaction", configuration.enableActivityUiLoadTransaction)
- putBoolean(
- "enable_user_interaction_transactions",
- configuration.enableUserInteractionTransactions,
- )
- putBoolean(
- "enable_user_interaction_breadcrumbs",
- configuration.enableUserInteractionBreadcrumbs,
- )
-}
-
-private fun Bundle.nav2SampleConfiguration(): Nav2SampleConfig =
- Nav2SampleConfig(
- enableNavigationTransactions = getBoolean("enable_navigation_transactions", true),
- enableNavigationBreadcrumbs = getBoolean("enable_navigation_breadcrumbs", true),
- enableScreenTracking = getBoolean("enable_screen_tracking", true),
- enableActivityUiLoadTransaction = getBoolean("enable_activity_ui_load_transaction", false),
- enableUserInteractionTransactions = getBoolean("enable_user_interaction_transactions", false),
- enableUserInteractionBreadcrumbs = getBoolean("enable_user_interaction_breadcrumbs", false),
- )
-
-private const val ACTIVITY_UI_LOAD_HELP_TEXT =
- "The sample simulates disabling ui.load transactions, as actual activity lifecycle " +
- "tracing is fixed when the SDK initializes."
diff --git a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/common/NavigationRouteDisplay.kt b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/common/NavigationRouteDisplay.kt
new file mode 100644
index 00000000000..ce5aa3110d7
--- /dev/null
+++ b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/common/NavigationRouteDisplay.kt
@@ -0,0 +1,32 @@
+package io.sentry.samples.android.navigation.common
+
+import android.os.Bundle
+
+internal fun RouteSpec.displayArguments(arguments: Bundle?): List> =
+ displayedArguments.mapNotNull { argument ->
+ argument.toDisplayPair(arguments?.getString(argument.key))
+ }
+
+internal fun RouteSpec.displayArguments(arguments: Map): List> =
+ displayedArguments.mapNotNull { argument ->
+ argument.toDisplayPair(arguments[argument.key]?.toString())
+ }
+
+internal fun RouteSpec.displayRoute(arguments: Bundle?): String =
+ displayRoute(displayArguments(arguments))
+
+internal fun RouteSpec.displayRoute(arguments: Map): String =
+ displayRoute(displayArguments(arguments))
+
+internal fun List>.toDisplayString(): String =
+ joinToString(", ") { (label, value) -> "$label=$value" }
+
+private fun RouteSpec.displayRoute(displayArguments: List>): String =
+ if (displayArguments.isEmpty()) {
+ "/$routeName"
+ } else {
+ "/$routeName { ${displayArguments.toDisplayString()} }"
+ }
+
+private fun DisplayedArgument.toDisplayPair(value: String?): Pair? =
+ value?.takeIf { it.isNotEmpty() }?.let { label to it }
diff --git a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/common/NavigationRouteSpec.kt b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/common/NavigationRouteSpec.kt
new file mode 100644
index 00000000000..46d22f08bbf
--- /dev/null
+++ b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/common/NavigationRouteSpec.kt
@@ -0,0 +1,145 @@
+package io.sentry.samples.android.navigation.common
+
+internal object RouteNames {
+
+ const val LANDING = "Landing"
+ const val HOME = "Home"
+ const val CUSTOM = "Custom"
+ const val PRODUCT_LIST = "ProductList"
+ const val DEEP_LINK = "DeepLink"
+ const val PRODUCT_DETAIL = "ProductDetail"
+ const val CHECKOUT = "Checkout"
+ const val CONFIRMATION = "Confirmation"
+ const val PROMO_DIALOG = "PromoDialog"
+ const val SHARE_SHEET = "ShareSheet"
+}
+
+internal object NavArgs {
+
+ const val ROUTE_NAME = "route_name"
+ const val PRODUCT_ID = "product_id"
+ const val SOURCE = "source"
+ const val CAMPAIGN = "campaign"
+ const val ORDER_ID = "order_id"
+ const val PROMO_ID = "promo_id"
+ const val SCENARIO = "scenario"
+}
+
+internal data class DisplayedArgument(val key: String, val label: String = key)
+
+internal data class RouteSpec(
+ val routeName: String,
+ val title: String,
+ val description: String? = null,
+ val displayedArguments: List = emptyList(),
+)
+
+internal object RouteSpecs {
+ val landing =
+ RouteSpec(
+ routeName = RouteNames.LANDING,
+ title = "Landing",
+ description =
+ "Activity ui.load transactions are configured when the Sentry SDK initializes, so this " +
+ "sample cannot truly disable them at launch time. Instead, we cancel and clear the " +
+ "current ui.load transaction when you land here.",
+ )
+
+ val home =
+ RouteSpec(
+ routeName = RouteNames.HOME,
+ title = "Home",
+ description =
+ "Start a product flow, then use the Sentry UI to inspect route " +
+ "transactions, breadcrumbs, and screen tracking.",
+ )
+
+ val custom =
+ home.copy(
+ routeName = RouteNames.CUSTOM,
+ title = "Custom Transactions",
+ description =
+ "Reuses the single-stack shopping flow while simulating a power user who starts their " +
+ "own manual transactions.",
+ )
+
+ val deepLink =
+ RouteSpec(
+ routeName = RouteNames.DEEP_LINK,
+ title = "Deep Link (Fragments)",
+ description =
+ "Simulates opening a fragment deep link that builds a synthetic backstack before landing " +
+ "on a detail destination.",
+ )
+
+ val productList =
+ RouteSpec(
+ routeName = RouteNames.PRODUCT_LIST,
+ title = "Product List",
+ description = "This route starts the product journey.",
+ )
+
+ val productDetail =
+ RouteSpec(
+ routeName = RouteNames.PRODUCT_DETAIL,
+ title = "Product Detail",
+ description = "",
+ displayedArguments =
+ listOf(
+ DisplayedArgument(NavArgs.PRODUCT_ID, "productId"),
+ DisplayedArgument(NavArgs.SOURCE),
+ DisplayedArgument(NavArgs.CAMPAIGN),
+ ),
+ )
+
+ val checkout =
+ RouteSpec(
+ routeName = RouteNames.CHECKOUT,
+ title = "Checkout",
+ description = "",
+ displayedArguments = listOf(DisplayedArgument(NavArgs.PRODUCT_ID, "productId")),
+ )
+
+ val confirmation =
+ RouteSpec(
+ routeName = RouteNames.CONFIRMATION,
+ title = "Confirmation",
+ description = "End of the product flow.",
+ displayedArguments = listOf(DisplayedArgument(NavArgs.ORDER_ID, "orderId")),
+ )
+
+ val promoDialog =
+ RouteSpec(
+ routeName = RouteNames.PROMO_DIALOG,
+ title = "Promo Dialog",
+ description =
+ "This modal is a real Nav destination, so its breadcrumbs and route transaction should " +
+ "stand on their own.",
+ displayedArguments = listOf(DisplayedArgument(NavArgs.PROMO_ID, "promoId")),
+ )
+
+ val shareSheet =
+ RouteSpec(
+ routeName = RouteNames.SHARE_SHEET,
+ title = "Share Sheet",
+ description =
+ "This sheet stays attached to the current route so you can compare an overlay against a " +
+ "real destination.",
+ displayedArguments = listOf(DisplayedArgument(NavArgs.PRODUCT_ID, "productId")),
+ )
+
+ fun get(routeName: String): RouteSpec =
+ when (routeName) {
+ RouteNames.LANDING -> landing
+ RouteNames.HOME -> home
+ RouteNames.CUSTOM -> custom
+ RouteNames.DEEP_LINK -> deepLink
+ RouteNames.PRODUCT_LIST -> productList
+ RouteNames.PRODUCT_DETAIL -> productDetail
+ RouteNames.CHECKOUT -> checkout
+ RouteNames.CONFIRMATION -> confirmation
+ RouteNames.PROMO_DIALOG -> promoDialog
+ RouteNames.SHARE_SHEET -> shareSheet
+ else -> RouteSpec(routeName = routeName, title = routeName)
+ }
+}
diff --git a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/Nav2RouteWorkDialog.kt b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/common/NavigationRouteWorkDialog.kt
similarity index 88%
rename from sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/Nav2RouteWorkDialog.kt
rename to sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/common/NavigationRouteWorkDialog.kt
index 31b7e39f77b..bfddc3129e1 100644
--- a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/Nav2RouteWorkDialog.kt
+++ b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/common/NavigationRouteWorkDialog.kt
@@ -1,5 +1,6 @@
-package io.sentry.samples.android.navigation
+package io.sentry.samples.android.navigation.common
+import android.R
import android.content.Context
import android.graphics.Typeface
import android.view.View
@@ -39,14 +40,14 @@ private fun routeWorkDialogTitle(context: Context): View =
text = "Route work"
textSize = 20f
setTypeface(null, Typeface.BOLD)
- setTextColor(context.themeColor(android.R.attr.textColorPrimary))
+ setTextColor(context.themeColor(R.attr.textColorPrimary))
}
)
addView(
TextView(context).apply {
text = "Enable/disable the generation of spans by navigation destinations."
textSize = 14f
- setTextColor(context.themeColor(android.R.attr.textColorSecondary))
+ setTextColor(context.themeColor(R.attr.textColorSecondary))
setPadding(0, context.dp(8), 0, 0)
}
)
diff --git a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/common/NavigationSampleConfig.kt b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/common/NavigationSampleConfig.kt
new file mode 100644
index 00000000000..61bde68b2a3
--- /dev/null
+++ b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/common/NavigationSampleConfig.kt
@@ -0,0 +1,63 @@
+package io.sentry.samples.android.navigation.common
+
+import io.sentry.Sentry
+
+/** Configuration shared by nav sample apps. */
+internal data class NavigationSampleConfig(
+ val enableNavigationTransactions: Boolean = true,
+ val enableNavigationBreadcrumbs: Boolean = true,
+ val enableScreenTracking: Boolean = true,
+ val enableActivityUiLoadTransaction: Boolean = false,
+ val enableUserInteractionTransactions: Boolean = false,
+ val enableUserInteractionBreadcrumbs: Boolean = false,
+ val captureBackStack: Boolean = true,
+ val maxCapturedBackStackEntries: Int = 10,
+)
+
+internal val NavigationSampleConfig.hasOnlyActivityUiLoadTransactions: Boolean
+ get() =
+ enableActivityUiLoadTransaction &&
+ !enableNavigationTransactions &&
+ !enableUserInteractionTransactions
+
+internal data class NavigationSampleConfigSnapshot(
+ val enableScreenTracking: Boolean,
+ val enableUserInteractionTransactions: Boolean,
+ val enableUserInteractionBreadcrumbs: Boolean,
+)
+
+internal fun NavigationSampleConfig.applyToCurrentOptions() {
+ applyNavigationSampleOptions(
+ enableScreenTracking = enableScreenTracking,
+ enableUserInteractionTransactions = enableUserInteractionTransactions,
+ enableUserInteractionBreadcrumbs = enableUserInteractionBreadcrumbs,
+ )
+}
+
+internal fun NavigationSampleConfigSnapshot.applyToCurrentOptions() {
+ applyNavigationSampleOptions(
+ enableScreenTracking = enableScreenTracking,
+ enableUserInteractionTransactions = enableUserInteractionTransactions,
+ enableUserInteractionBreadcrumbs = enableUserInteractionBreadcrumbs,
+ )
+}
+
+private fun applyNavigationSampleOptions(
+ enableScreenTracking: Boolean,
+ enableUserInteractionTransactions: Boolean,
+ enableUserInteractionBreadcrumbs: Boolean,
+) {
+ val options = Sentry.getCurrentScopes().options
+ options.setEnableScreenTracking(enableScreenTracking)
+ options.setEnableUserInteractionTracing(enableUserInteractionTransactions)
+ options.setEnableUserInteractionBreadcrumbs(enableUserInteractionBreadcrumbs)
+}
+
+internal fun currentNavigationSampleConfigSnapshot(): NavigationSampleConfigSnapshot {
+ val options = Sentry.getCurrentScopes().options
+ return NavigationSampleConfigSnapshot(
+ enableScreenTracking = options.isEnableScreenTracking,
+ enableUserInteractionTransactions = options.isEnableUserInteractionTracing,
+ enableUserInteractionBreadcrumbs = options.isEnableUserInteractionBreadcrumbs,
+ )
+}
diff --git a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/common/NavigationSetupScreen.kt b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/common/NavigationSetupScreen.kt
new file mode 100644
index 00000000000..bef3414ab4f
--- /dev/null
+++ b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/common/NavigationSetupScreen.kt
@@ -0,0 +1,393 @@
+package io.sentry.samples.android.navigation.common
+
+import androidx.compose.animation.AnimatedContent
+import androidx.compose.animation.SizeTransform
+import androidx.compose.animation.animateColorAsState
+import androidx.compose.animation.core.spring
+import androidx.compose.animation.core.tween
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.animation.slideInVertically
+import androidx.compose.animation.slideOutVertically
+import androidx.compose.animation.togetherWith
+import androidx.compose.foundation.BorderStroke
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.defaultMinSize
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.safeDrawingPadding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.selection.toggleable
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.outlined.HelpOutline
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.Card
+import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.PlainTooltip
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Switch
+import androidx.compose.material3.SwitchDefaults
+import androidx.compose.material3.Text
+import androidx.compose.material3.TooltipAnchorPosition
+import androidx.compose.material3.TooltipBox
+import androidx.compose.material3.TooltipDefaults
+import androidx.compose.material3.rememberTooltipState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+
+@Composable
+internal fun NavigationSetupScreen(
+ navName: String,
+ navVersion: String,
+ configuration: NavigationSampleConfig,
+ showBackStackControls: Boolean = false,
+ onConfigurationChanged: (NavigationSampleConfig) -> Unit,
+ onLaunch: () -> Unit,
+) {
+ Column(
+ modifier =
+ Modifier.fillMaxSize()
+ .background(MaterialTheme.colorScheme.background)
+ .safeDrawingPadding()
+ .verticalScroll(rememberScrollState())
+ .padding(24.dp),
+ verticalArrangement = Arrangement.spacedBy(16.dp),
+ ) {
+ Text(
+ text = "Navigation $navVersion Setup",
+ style = MaterialTheme.typography.headlineMedium,
+ fontWeight = FontWeight.Bold,
+ color = MaterialTheme.colorScheme.onBackground,
+ )
+
+ Text(
+ text =
+ "Choose which auto-instrumentation features should be active before the $navName sample launches.",
+ style = MaterialTheme.typography.bodyMedium,
+ color = MaterialTheme.colorScheme.onBackground,
+ )
+
+ NavigationSetupSection(title = "Navigation") {
+ NavigationSetupCheckboxRow(
+ label = "Navigation transactions",
+ checked = configuration.enableNavigationTransactions,
+ ) {
+ onConfigurationChanged(configuration.copy(enableNavigationTransactions = it))
+ }
+ NavigationSetupCheckboxRow(
+ label = "Navigation breadcrumbs",
+ checked = configuration.enableNavigationBreadcrumbs,
+ ) {
+ onConfigurationChanged(configuration.copy(enableNavigationBreadcrumbs = it))
+ }
+ NavigationSetupCheckboxRow(
+ label = "Screen tracking",
+ checked = configuration.enableScreenTracking,
+ ) {
+ onConfigurationChanged(configuration.copy(enableScreenTracking = it))
+ }
+ if (showBackStackControls) {
+ NavigationSetupCheckboxRow(
+ label = "Capture back stack",
+ checked = configuration.captureBackStack,
+ ) {
+ onConfigurationChanged(configuration.copy(captureBackStack = it))
+ }
+ NavigationSetupCounterRow(
+ label = "Max captured back stack entries",
+ value = configuration.maxCapturedBackStackEntries,
+ enabled = configuration.captureBackStack,
+ ) {
+ onConfigurationChanged(configuration.copy(maxCapturedBackStackEntries = it))
+ }
+ }
+ }
+
+ NavigationSetupSection(title = "Other auto-transactions") {
+ NavigationSetupCheckboxRow(
+ label = "Activity ui.load transaction",
+ checked = configuration.enableActivityUiLoadTransaction,
+ helpText = ACTIVITY_UI_LOAD_HELP_TEXT,
+ ) {
+ onConfigurationChanged(configuration.copy(enableActivityUiLoadTransaction = it))
+ }
+ NavigationSetupCheckboxRow(
+ label = "User interaction transactions",
+ checked = configuration.enableUserInteractionTransactions,
+ ) {
+ onConfigurationChanged(configuration.copy(enableUserInteractionTransactions = it))
+ }
+ }
+
+ NavigationSetupSection(title = "Other breadcrumbs") {
+ NavigationSetupCheckboxRow(
+ label = "User interaction breadcrumbs",
+ checked = configuration.enableUserInteractionBreadcrumbs,
+ ) {
+ onConfigurationChanged(configuration.copy(enableUserInteractionBreadcrumbs = it))
+ }
+ }
+
+ Button(onClick = onLaunch, modifier = Modifier.fillMaxWidth()) {
+ Text("Launch $navName Sample", fontSize = 18.sp)
+ }
+ }
+}
+
+@Composable
+private fun NavigationSetupSection(title: String, content: @Composable () -> Unit) {
+ Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
+ Text(
+ text = title,
+ style = MaterialTheme.typography.headlineSmall,
+ fontWeight = FontWeight.Bold,
+ color = MaterialTheme.colorScheme.onBackground,
+ )
+ Card(
+ colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant),
+ modifier = Modifier.fillMaxWidth(),
+ ) {
+ Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
+ content()
+ }
+ }
+ }
+}
+
+@Composable
+private fun NavigationSetupCheckboxRow(
+ label: String,
+ checked: Boolean,
+ helpText: String? = null,
+ onCheckedChange: (Boolean) -> Unit,
+) {
+ val sentryPink = Color(0xFFC85B9C)
+ val rowShape: Shape = RoundedCornerShape(12.dp)
+ val rowBackground by
+ animateColorAsState(
+ targetValue =
+ if (checked) sentryPink.copy(alpha = 0.12f) else MaterialTheme.colorScheme.surface,
+ animationSpec = spring(stiffness = 550f, dampingRatio = 0.9f),
+ label = "navigation-setup-toggle-background",
+ )
+ val rowBorderColor by
+ animateColorAsState(
+ targetValue =
+ if (checked) sentryPink.copy(alpha = 0.35f)
+ else MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.7f),
+ animationSpec = tween(durationMillis = 180),
+ label = "navigation-setup-toggle-border",
+ )
+ val switchColors =
+ SwitchDefaults.colors(
+ checkedTrackColor = sentryPink,
+ checkedBorderColor = sentryPink,
+ checkedThumbColor = Color.White,
+ )
+
+ Surface(
+ shape = rowShape,
+ color = rowBackground,
+ border = BorderStroke(1.dp, rowBorderColor),
+ tonalElevation = if (checked) 1.dp else 0.dp,
+ modifier = Modifier.fillMaxWidth(),
+ ) {
+ Row(
+ modifier =
+ Modifier.fillMaxWidth()
+ .toggleable(value = checked, role = Role.Switch, onValueChange = onCheckedChange)
+ .defaultMinSize(minHeight = 56.dp)
+ .padding(horizontal = 14.dp, vertical = 8.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(14.dp),
+ ) {
+ Switch(
+ checked = checked,
+ onCheckedChange = null,
+ colors = switchColors,
+ )
+ Row(modifier = Modifier.weight(1f), verticalAlignment = Alignment.CenterVertically) {
+ Text(text = label, style = MaterialTheme.typography.titleMedium)
+ if (helpText != null) {
+ NavigationSetupHelpTooltip(helpText)
+ }
+ }
+ }
+ }
+}
+
+@Composable
+private fun NavigationSetupCounterRow(
+ label: String,
+ value: Int,
+ enabled: Boolean,
+ onValueChange: (Int) -> Unit,
+) {
+ val sentryPurple = Color(0xFF6C55B2)
+ val containerColor by
+ animateColorAsState(
+ targetValue =
+ if (enabled) MaterialTheme.colorScheme.surface
+ else MaterialTheme.colorScheme.surfaceVariant,
+ animationSpec = tween(durationMillis = 180),
+ label = "navigation-setup-counter-background",
+ )
+ val borderColor by
+ animateColorAsState(
+ targetValue =
+ if (enabled) sentryPurple.copy(alpha = 0.28f)
+ else MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f),
+ animationSpec = tween(durationMillis = 180),
+ label = "navigation-setup-counter-border",
+ )
+
+ Surface(
+ shape = RoundedCornerShape(12.dp),
+ color = containerColor,
+ border = BorderStroke(1.dp, borderColor),
+ tonalElevation = if (enabled) 1.dp else 0.dp,
+ modifier = Modifier.fillMaxWidth(),
+ ) {
+ Row(
+ modifier =
+ Modifier.fillMaxWidth()
+ .defaultMinSize(minHeight = 56.dp)
+ .padding(horizontal = 14.dp, vertical = 8.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(12.dp),
+ ) {
+ Text(
+ text = label,
+ style = MaterialTheme.typography.titleMedium,
+ modifier = Modifier.weight(1f),
+ )
+
+ NavigationSetupCounterButton(
+ label = "-",
+ enabled = enabled && value > 1,
+ onClick = { onValueChange((value - 1).coerceAtLeast(1)) },
+ color = sentryPurple,
+ )
+
+ Box(modifier = Modifier.size(44.dp), contentAlignment = Alignment.Center) {
+ AnimatedContent(
+ targetState = value,
+ transitionSpec = {
+ (slideInVertically { fullHeight -> fullHeight / 3 } + fadeIn()) togetherWith
+ (slideOutVertically { fullHeight -> -fullHeight / 3 } + fadeOut()) using
+ SizeTransform(clip = false)
+ },
+ label = "navigation-setup-counter-value",
+ ) { animatedValue ->
+ Text(
+ text = animatedValue.toString(),
+ style = MaterialTheme.typography.titleMedium,
+ fontWeight = FontWeight.Bold,
+ textAlign = TextAlign.Center,
+ )
+ }
+ }
+
+ NavigationSetupCounterButton(
+ label = "+",
+ enabled = enabled,
+ onClick = { onValueChange(value + 1) },
+ color = sentryPurple,
+ )
+ }
+ }
+}
+
+@Composable
+private fun NavigationSetupCounterButton(
+ label: String,
+ enabled: Boolean,
+ color: Color,
+ onClick: () -> Unit,
+) {
+ Button(
+ onClick = onClick,
+ enabled = enabled,
+ shape = CircleShape,
+ contentPadding = ButtonDefaults.ContentPadding,
+ colors =
+ ButtonDefaults.buttonColors(
+ containerColor = color,
+ contentColor = Color.White,
+ disabledContainerColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f),
+ disabledContentColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f),
+ ),
+ elevation =
+ ButtonDefaults.buttonElevation(
+ defaultElevation = 0.dp,
+ pressedElevation = 2.dp,
+ focusedElevation = 1.dp,
+ hoveredElevation = 1.dp,
+ disabledElevation = 0.dp,
+ ),
+ modifier = Modifier.size(48.dp),
+ ) {
+ Text(label, style = MaterialTheme.typography.titleLarge, textAlign = TextAlign.Center)
+ }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+private fun NavigationSetupHelpTooltip(text: String) {
+ val tooltipState = rememberTooltipState(isPersistent = true)
+ val scope = rememberCoroutineScope()
+
+ LaunchedEffect(tooltipState.isVisible) {
+ if (tooltipState.isVisible) {
+ delay(4000)
+ tooltipState.dismiss()
+ }
+ }
+
+ TooltipBox(
+ positionProvider = TooltipDefaults.rememberTooltipPositionProvider(TooltipAnchorPosition.Above),
+ tooltip = { PlainTooltip { Text(text) } },
+ state = tooltipState,
+ ) {
+ Icon(
+ imageVector = Icons.AutoMirrored.Outlined.HelpOutline,
+ contentDescription = "Activity ui.load transaction help",
+ tint = MaterialTheme.colorScheme.onSurfaceVariant,
+ modifier =
+ Modifier.padding(start = 6.dp).size(20.dp).clickable {
+ scope.launch { tooltipState.show() }
+ },
+ )
+ }
+}
+
+private const val ACTIVITY_UI_LOAD_HELP_TEXT =
+ "The sample simulates disabling ui.load transactions, as actual activity lifecycle " +
+ "tracing is fixed when the SDK initializes."
diff --git a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/Nav2ThemeColors.kt b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/common/NavigationThemeColors.kt
similarity index 71%
rename from sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/Nav2ThemeColors.kt
rename to sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/common/NavigationThemeColors.kt
index 6f5d99b5363..a92e107c281 100644
--- a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/Nav2ThemeColors.kt
+++ b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/common/NavigationThemeColors.kt
@@ -1,8 +1,9 @@
-package io.sentry.samples.android.navigation
+package io.sentry.samples.android.navigation.common
import android.content.Context
import androidx.annotation.AttrRes
+/** Resolves a theme color for navigation sample views. */
internal fun Context.themeColor(@AttrRes attrId: Int): Int {
val attributes = obtainStyledAttributes(intArrayOf(attrId))
return try {
diff --git a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/common/RouteWorkApi.kt b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/common/RouteWorkApi.kt
new file mode 100644
index 00000000000..8b12ee51fa3
--- /dev/null
+++ b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/common/RouteWorkApi.kt
@@ -0,0 +1,67 @@
+package io.sentry.samples.android.navigation.common
+
+import io.sentry.HttpStatusCodeRange
+import io.sentry.okhttp.SentryOkHttpEventListener
+import io.sentry.okhttp.SentryOkHttpInterceptor
+import okhttp3.OkHttpClient
+import okhttp3.ResponseBody
+import retrofit2.Call
+import retrofit2.Retrofit
+import retrofit2.converter.gson.GsonConverterFactory
+import retrofit2.http.GET
+import retrofit2.http.Url
+
+internal object RouteWorkApi {
+ private val client =
+ OkHttpClient.Builder()
+ .eventListener(SentryOkHttpEventListener())
+ .addInterceptor(
+ SentryOkHttpInterceptor(
+ captureFailedRequests = true,
+ failedRequestStatusCodes = listOf(HttpStatusCodeRange(400, 599)),
+ )
+ )
+ .build()
+
+ private val retrofit =
+ Retrofit.Builder()
+ .baseUrl("https://httpbin.org/")
+ .addConverterFactory(GsonConverterFactory.create())
+ .client(client)
+ .build()
+
+ private val service: RouteWorkService = retrofit.create(RouteWorkService::class.java)
+
+ fun enqueueRequest(onSuccess: () -> Unit, onFailure: (Throwable) -> Unit) {
+ service
+ .request(ROUTE_WORK_URL)
+ .enqueue(
+ object : retrofit2.Callback {
+ override fun onResponse(
+ call: retrofit2.Call,
+ response: retrofit2.Response,
+ ) {
+ response.body()?.close()
+ response.errorBody()?.close()
+ onSuccess()
+ }
+
+ override fun onFailure(call: retrofit2.Call, t: Throwable) {
+ onFailure(t)
+ }
+ }
+ )
+ }
+
+ suspend fun runRequest() {
+ service.requestAsync(ROUTE_WORK_URL).use {}
+ }
+}
+
+internal interface RouteWorkService {
+ @GET fun request(@Url url: String): Call
+
+ @GET suspend fun requestAsync(@Url url: String): ResponseBody
+}
+
+private const val ROUTE_WORK_URL = "https://httpbin.org/get?sentry_sample=navigation_route_work"
diff --git a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/common/RouteWorkOption.kt b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/common/RouteWorkOption.kt
new file mode 100644
index 00000000000..1c8371c3e55
--- /dev/null
+++ b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/common/RouteWorkOption.kt
@@ -0,0 +1,7 @@
+package io.sentry.samples.android.navigation.common
+
+/** Optional work navigation sample routes can perform after they become active. */
+internal enum class RouteWorkOption(val label: String, val tagName: String) {
+ HTTP_REQUEST("HTTP request", "http_request"),
+ MANUAL_CHILD_SPAN("Manual child span", "manual_child_span"),
+}
diff --git a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/Nav2Activity.kt b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/nav2/Nav2Activity.kt
similarity index 86%
rename from sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/Nav2Activity.kt
rename to sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/nav2/Nav2Activity.kt
index 70fb0fe0f3b..0cbd4ac14b3 100644
--- a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/Nav2Activity.kt
+++ b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/nav2/Nav2Activity.kt
@@ -1,4 +1,4 @@
-package io.sentry.samples.android.navigation
+package io.sentry.samples.android.navigation.nav2
import android.content.ClipData
import android.content.ClipboardManager
@@ -33,14 +33,18 @@ import androidx.navigation.fragment.NavHostFragment
import io.sentry.Sentry
import io.sentry.SpanStatus
import io.sentry.android.navigation.SentryNavigationListener
-import io.sentry.samples.android.GithubAPI
import io.sentry.samples.android.R
-import io.sentry.samples.android.Repo
-import io.sentry.samples.android.navigation.Nav2Destination.Home
-import io.sentry.samples.android.navigation.Nav2Destination.Landing
-import retrofit2.Call
-import retrofit2.Callback
-import retrofit2.Response
+import io.sentry.samples.android.navigation.common.NavigationSampleConfigSnapshot
+import io.sentry.samples.android.navigation.common.RouteNames
+import io.sentry.samples.android.navigation.common.RouteSpecs
+import io.sentry.samples.android.navigation.common.RouteWorkApi
+import io.sentry.samples.android.navigation.common.RouteWorkOption
+import io.sentry.samples.android.navigation.common.applyToCurrentOptions
+import io.sentry.samples.android.navigation.common.displayRoute
+import io.sentry.samples.android.navigation.common.hasOnlyActivityUiLoadTransactions
+import io.sentry.samples.android.navigation.common.showRouteWorkDialog
+import io.sentry.samples.android.navigation.nav2.Nav2Destination.Home
+import io.sentry.samples.android.navigation.nav2.Nav2Destination.Landing
/**
* Sample activity for testing Sentry's
@@ -66,17 +70,17 @@ class Nav2Activity : AppCompatActivity() {
private val enableNavigationBreadcrumbs = mutableStateOf(true)
private val enableNavigationTransactions = mutableStateOf(true)
- private lateinit var previousConfig: Nav2SampleConfigSnapshot
+ private lateinit var previousConfig: NavigationSampleConfigSnapshot
// Top bar config
- private val routeWorkOptions =
- mutableStateOf(setOf(RouteWorkOption.HTTP_REQUEST, RouteWorkOption.MANUAL_CHILD_SPAN))
+ private val routeWorkOptions = mutableStateOf(setOf(RouteWorkOption.MANUAL_CHILD_SPAN))
private lateinit var topBar: Nav2TopBar
private var activeScenario = Nav2Scenario.COMPOSE
+ private val composeScenario = mutableStateOf(Nav2Scenario.COMPOSE)
// Main content
private lateinit var contentHosts: Nav2ContentHosts
- private val performanceState = NavigationPerformanceState()
+ private val performanceState = Nav2PerformanceState()
// Transaction history bottom sheet
private var isTransactionHistoryActive = false
@@ -203,9 +207,11 @@ class Nav2Activity : AppCompatActivity() {
routeWorkOptions = routeWorkOptions.value,
onCaptureException = { captureSampleException("Nav2") },
onCrashApp = { showCrashConfirmation("Nav2") },
- onRouteChanged = { _, currentRoute, backStack ->
- updateComposeNavigationUi(currentRoute, backStack)
+ selectedScenario = composeScenario.value,
+ onRouteChanged = { scenario, _, currentRoute, backStack ->
+ updateComposeNavigationUi(scenario, currentRoute, backStack)
},
+ onExitRoot = { finish() },
)
}
}
@@ -258,15 +264,15 @@ class Nav2Activity : AppCompatActivity() {
setContent {
Nav2SampleTheme {
- NavigationPerformancePanel(
+ Nav2PerformancePanel(
title = "Performance",
description =
"Stress the Nav2 listener path with deep fragment back stacks, destination changes, " +
"and unrelated Compose recompositions. Use Perfetto sections prefixed with " +
"Nav2Stress to inspect hot paths.",
- currentRoute = "/${backStack.lastOrNull()?.routeName ?: Nav2RouteNames.HOME}",
+ currentRoute = "/${backStack.lastOrNull()?.routeName ?: RouteNames.HOME}",
backStack =
- navigationPerformanceBackStackPreview(
+ nav2PerformanceBackStackPreview(
backStack.map { destination -> "/${destination.routeName}" }
),
state = performanceState,
@@ -324,6 +330,7 @@ class Nav2Activity : AppCompatActivity() {
private fun openScenario(scenario: Nav2Scenario) {
activeScenario = scenario
+ composeScenario.value = scenario
topBar.select(activeScenario)
performanceState.stopAutomaticWork()
when (scenario) {
@@ -334,7 +341,22 @@ class Nav2Activity : AppCompatActivity() {
navController.currentBackStackEntry?.arguments,
)
}
- Nav2Scenario.COMPOSE -> contentHosts.showCompose()
+ Nav2Scenario.COMPOSE -> {
+ contentHosts.showCompose()
+ updateComposeNavigationUi(
+ scenario = Nav2Scenario.COMPOSE,
+ currentRoute = "/${RouteNames.HOME}",
+ backStack = "/${RouteNames.HOME}",
+ )
+ }
+ Nav2Scenario.CUSTOM -> {
+ contentHosts.showCompose()
+ updateComposeNavigationUi(
+ scenario = Nav2Scenario.CUSTOM,
+ currentRoute = "/${RouteNames.CUSTOM}",
+ backStack = "/${RouteNames.CUSTOM}",
+ )
+ }
Nav2Scenario.FRAGMENTS -> {
contentHosts.showFragments()
resetToHome()
@@ -369,20 +391,13 @@ class Nav2Activity : AppCompatActivity() {
when (option) {
RouteWorkOption.HTTP_REQUEST -> {
- GithubAPI.service
- .listRepos("getsentry")
- .enqueue(
- object : Callback> {
- override fun onResponse(call: Call>, response: Response>) {
- Thread { Sentry.flush(SENTRY_FLUSH_TIMEOUT_MILLIS) }.start()
- }
-
- override fun onFailure(call: Call>, t: Throwable) {
- Sentry.captureException(t)
- Thread { Sentry.flush(SENTRY_FLUSH_TIMEOUT_MILLIS) }.start()
- }
- }
- )
+ RouteWorkApi.enqueueRequest(
+ onSuccess = { Thread { Sentry.flush(SENTRY_FLUSH_TIMEOUT_MILLIS) }.start() },
+ onFailure = { t ->
+ Sentry.captureException(t)
+ Thread { Sentry.flush(SENTRY_FLUSH_TIMEOUT_MILLIS) }.start()
+ },
+ )
}
RouteWorkOption.MANUAL_CHILD_SPAN -> recordManualChildSpan(routeName)
@@ -393,16 +408,16 @@ class Nav2Activity : AppCompatActivity() {
private fun updatePerformanceTopBar() {
topBar.update(
scenario = Nav2Scenario.PERFORMANCE,
- currentRoute = "/${backStack.lastOrNull()?.routeName ?: Nav2RouteNames.HOME}",
+ currentRoute = "/${backStack.lastOrNull()?.routeName ?: RouteNames.HOME}",
backStack =
- navigationPerformanceBackStackPreview(
+ nav2PerformanceBackStackPreview(
backStack.map { destination -> "/${destination.routeName}" }
),
)
}
private fun buildNav2PerformanceStack() {
- traceNavigationPerformanceSection("Nav2Stress.buildStack") {
+ traceNav2PerformanceSection("Nav2Stress.buildStack") {
val generation = performanceState.nextGeneration()
resetToHome()
for (index in 1 until performanceState.stackDepth.coerceAtLeast(1)) {
@@ -413,7 +428,7 @@ class Nav2Activity : AppCompatActivity() {
}
private fun replaceNav2PerformanceTop() {
- traceNavigationPerformanceSection("Nav2Stress.replaceTop") {
+ traceNav2PerformanceSection("Nav2Stress.replaceTop") {
val generation = performanceState.nextGeneration()
if (backStack.size > 1) {
navigateBack()
@@ -456,9 +471,9 @@ class Nav2Activity : AppCompatActivity() {
syncTrackedBackStack(destination, arguments)
val trackedDestination = backStack.lastOrNull()
- val routeName = trackedDestination?.routeName ?: destination?.routeName() ?: Nav2RouteNames.HOME
+ val routeName = trackedDestination?.routeName ?: destination?.routeName() ?: RouteNames.HOME
val currentRoute =
- trackedDestination?.displayRoute() ?: Nav2RouteSpecs.get(routeName).displayRoute(arguments)
+ trackedDestination?.displayRoute() ?: RouteSpecs.get(routeName).displayRoute(arguments)
val scenario = fragmentTopBarScenario(trackedDestination)
topBar.update(
@@ -472,9 +487,13 @@ class Nav2Activity : AppCompatActivity() {
}
}
- private fun updateComposeNavigationUi(currentRoute: String, backStack: String) {
+ private fun updateComposeNavigationUi(
+ scenario: Nav2Scenario,
+ currentRoute: String,
+ backStack: String,
+ ) {
topBar.update(
- scenario = Nav2Scenario.COMPOSE,
+ scenario = scenario,
currentRoute = currentRoute,
backStack = backStack,
)
@@ -605,7 +624,7 @@ class Nav2Activity : AppCompatActivity() {
}
internal fun captureSampleException(navName: String) {
- Sentry.captureException(RuntimeException("$navName sample exception button"))
+ Sentry.captureException(RuntimeException("$navName sample capture exception button"))
Thread { Sentry.flush(SENTRY_FLUSH_TIMEOUT_MILLIS) }.start()
}
@@ -619,7 +638,7 @@ class Nav2Activity : AppCompatActivity() {
}
private fun crashSampleApp(navName: String): Nothing {
- throw RuntimeException("Fatal $navName sample crash button")
+ throw RuntimeException("Fatal $navName sample crash app button")
}
private val Int.dp: Int
diff --git a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/Nav2ComposeRoutes.kt b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/nav2/Nav2ComposeRoutes.kt
similarity index 57%
rename from sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/Nav2ComposeRoutes.kt
rename to sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/nav2/Nav2ComposeRoutes.kt
index ba61a6bb2ee..9d083ef7431 100644
--- a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/Nav2ComposeRoutes.kt
+++ b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/nav2/Nav2ComposeRoutes.kt
@@ -1,7 +1,10 @@
-package io.sentry.samples.android.navigation
+package io.sentry.samples.android.navigation.nav2
import android.os.Bundle
import androidx.activity.compose.BackHandler
+import androidx.compose.animation.core.tween
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.BoxScope
@@ -9,6 +12,7 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
@@ -23,6 +27,9 @@ import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
+import androidx.compose.material3.SegmentedButton
+import androidx.compose.material3.SegmentedButtonDefaults
+import androidx.compose.material3.SingleChoiceSegmentedButtonRow
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
@@ -31,6 +38,7 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.saveable.listSaver
import androidx.compose.runtime.saveable.rememberSaveable
@@ -39,7 +47,9 @@ import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.colorResource
import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.navigation.NavType
@@ -53,15 +63,28 @@ import io.sentry.android.navigation.SentryNavigationListener
import io.sentry.compose.SentryModifier.sentryTag
import io.sentry.compose.SentryTraced
import io.sentry.compose.withSentryObservableEffect
-import io.sentry.samples.android.GithubAPI
-import io.sentry.samples.android.navigation.Nav2ComposeDestination.Checkout
-import io.sentry.samples.android.navigation.Nav2ComposeDestination.Confirmation
-import io.sentry.samples.android.navigation.Nav2ComposeDestination.Home
-import io.sentry.samples.android.navigation.Nav2ComposeDestination.ProductDetail
-import io.sentry.samples.android.navigation.Nav2ComposeDestination.ProductList
-import io.sentry.samples.android.navigation.Nav2ComposeDestination.PromoDialog
+import io.sentry.samples.android.R
+import io.sentry.samples.android.navigation.common.NavArgs
+import io.sentry.samples.android.navigation.common.RouteNames
+import io.sentry.samples.android.navigation.common.RouteSpec
+import io.sentry.samples.android.navigation.common.RouteSpecs
+import io.sentry.samples.android.navigation.common.RouteWorkApi
+import io.sentry.samples.android.navigation.common.RouteWorkOption
+import io.sentry.samples.android.navigation.common.displayArguments
+import io.sentry.samples.android.navigation.common.displayRoute
+import io.sentry.samples.android.navigation.common.toDisplayString
+import io.sentry.samples.android.navigation.nav2.Nav2ComposeDestination.Checkout
+import io.sentry.samples.android.navigation.nav2.Nav2ComposeDestination.Confirmation
+import io.sentry.samples.android.navigation.nav2.Nav2ComposeDestination.Custom
+import io.sentry.samples.android.navigation.nav2.Nav2ComposeDestination.Home
+import io.sentry.samples.android.navigation.nav2.Nav2ComposeDestination.ProductDetail
+import io.sentry.samples.android.navigation.nav2.Nav2ComposeDestination.ProductList
+import io.sentry.samples.android.navigation.nav2.Nav2ComposeDestination.PromoDialog
import java.io.IOException
import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import retrofit2.HttpException
@@ -71,13 +94,24 @@ internal fun Nav2ComposeApp(
routeWorkOptions: Set,
onCaptureException: () -> Unit,
onCrashApp: () -> Unit,
- onRouteChanged: (routeName: String, currentRoute: String, backStack: String) -> Unit,
+ selectedScenario: Nav2Scenario,
+ onRouteChanged:
+ (scenario: Nav2Scenario, routeName: String, currentRoute: String, backStack: String) -> Unit,
+ onExitRoot: () -> Unit,
) {
val navController = rememberNavController().withSentryObservableEffect(navListener = navListener)
val backStack = rememberSaveableNav2ComposeBackStack()
val shareSheetProductId = rememberSaveable { mutableStateOf(null) }
val currentDestination = backStack.lastOrNull() ?: Home
+ var customTransactionMode by rememberSaveable {
+ mutableStateOf(Nav2CustomTransactionMode.PER_SCREEN)
+ }
+ var asyncBrowseProductsJob by remember { mutableStateOf(null) }
+ var isAsyncBrowseProductsRunning by rememberSaveable { mutableStateOf(false) }
+ val customTransactionsScope = androidx.compose.runtime.rememberCoroutineScope()
+ val customTransactionController =
+ androidx.compose.runtime.remember { Nav2CustomTransactionController() }
fun navigateTo(destination: Nav2ComposeDestination) {
backStack.add(destination)
@@ -99,20 +133,53 @@ internal fun Nav2ComposeApp(
shareSheetProductId.value = null
}
- fun resetToHome() {
- backStack.resetTo(Home)
+ fun resetToRoot() {
+ val root = if (selectedScenario == Nav2Scenario.CUSTOM) Custom else Home
+ backStack.resetTo(root)
shareSheetProductId.value = null
- navController.navigate(Home.route) {
+ navController.navigate(root.route) {
popUpTo(Home.route) { inclusive = false }
launchSingleTop = true
}
}
- BackHandler(enabled = shareSheetProductId.value != null) { dismissShareSheet() }
- BackHandler(enabled = shareSheetProductId.value == null && backStack.size > 1) { navigateBack() }
+ LaunchedEffect(selectedScenario) {
+ when (selectedScenario) {
+ Nav2Scenario.COMPOSE -> {
+ customTransactionController.cleanup()
+ backStack.resetTo(Home)
+ shareSheetProductId.value = null
+ navController.navigate(Home.route) {
+ popUpTo(Home.route) { inclusive = false }
+ launchSingleTop = true
+ }
+ }
+ Nav2Scenario.CUSTOM -> {
+ backStack.resetTo(Custom)
+ shareSheetProductId.value = null
+ navController.navigate(Custom.route) {
+ popUpTo(Home.route) { inclusive = false }
+ launchSingleTop = true
+ }
+ }
+ else -> Unit
+ }
+ }
+
+ LaunchedEffect(currentDestination, customTransactionMode) {
+ if (
+ currentDestination != Custom &&
+ customTransactionMode == Nav2CustomTransactionMode.ASYNC_FROM_USER_ACTION
+ ) {
+ asyncBrowseProductsJob?.cancel()
+ asyncBrowseProductsJob = null
+ isAsyncBrowseProductsRunning = false
+ }
+ }
LaunchedEffect(currentDestination, backStack.size) {
onRouteChanged(
+ selectedScenario,
currentDestination.routeName,
currentDestination.displayRoute(),
backStack.toComposeBackStackText(),
@@ -124,23 +191,70 @@ internal fun Nav2ComposeApp(
routeWorkOptions = routeWorkOptions,
)
+ Nav2CustomTransactionEffect(
+ selectedDestination = currentDestination,
+ mode = customTransactionMode,
+ controller = customTransactionController,
+ )
+
Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) {
Column(modifier = Modifier.fillMaxSize()) {
NavHost(
navController = navController,
startDestination = Home.route,
modifier = Modifier.weight(1f),
+ enterTransition = { fadeIn(animationSpec = tween(COMPOSE_ROUTE_TRANSITION_MILLIS)) },
+ exitTransition = { fadeOut(animationSpec = tween(COMPOSE_ROUTE_TRANSITION_MILLIS)) },
+ popEnterTransition = { fadeIn(animationSpec = tween(COMPOSE_ROUTE_TRANSITION_MILLIS)) },
+ popExitTransition = { fadeOut(animationSpec = tween(COMPOSE_ROUTE_TRANSITION_MILLIS)) },
) {
composable(Home.route) {
- TracedNav2ComposeRoute(Home.routeName) {
- Nav2ComposeHomeRoute(routeSpec = Nav2RouteSpecs.home) { navigateTo(ProductList) }
+ TracedNav2ComposeRoute(selectedScenario, Home.routeName) {
+ Nav2ComposeHomeRoute(routeSpec = RouteSpecs.home) { navigateTo(ProductList) }
+ }
+ }
+
+ composable(Custom.route) {
+ TracedNav2ComposeRoute(selectedScenario, Custom.routeName) {
+ Nav2ComposeCustomRoute(
+ routeSpec = RouteSpecs.custom,
+ mode = customTransactionMode,
+ onModeSelected = { customTransactionMode = it },
+ isAsyncBrowseProductsRunning = isAsyncBrowseProductsRunning,
+ onBrowseProducts = {
+ if (customTransactionMode == Nav2CustomTransactionMode.ASYNC_FROM_USER_ACTION) {
+ if (!isAsyncBrowseProductsRunning) {
+ customTransactionController.startAsyncBrowseProductsTransaction()
+ isAsyncBrowseProductsRunning = true
+ asyncBrowseProductsJob = customTransactionsScope.launch {
+ val span =
+ Sentry.getSpan()
+ ?.startChild(
+ "test.navigation.async_browse_products",
+ "Nav2 Custom async browse products",
+ )
+ try {
+ delay(250)
+ navigateTo(ProductList)
+ } finally {
+ span?.finish()
+ isAsyncBrowseProductsRunning = false
+ asyncBrowseProductsJob = null
+ }
+ }
+ }
+ } else {
+ navigateTo(ProductList)
+ }
+ },
+ )
}
}
composable(ProductList.route) {
- TracedNav2ComposeRoute(ProductList.routeName) {
+ TracedNav2ComposeRoute(selectedScenario, ProductList.routeName) {
Nav2ComposeProductListRoute(
- routeSpec = Nav2RouteSpecs.productList,
+ routeSpec = RouteSpecs.productList,
onOpenProduct42 = {
navigateTo(
ProductDetail(
@@ -161,20 +275,20 @@ internal fun Nav2ComposeApp(
route = Nav2ComposeDestination.PRODUCT_DETAIL_ROUTE,
arguments =
listOf(
- navArgument(Nav2Args.PRODUCT_ID) { type = NavType.StringType },
- navArgument(Nav2Args.SOURCE) { type = NavType.StringType },
- navArgument(Nav2Args.CAMPAIGN) {
+ navArgument(NavArgs.PRODUCT_ID) { type = NavType.StringType },
+ navArgument(NavArgs.SOURCE) { type = NavType.StringType },
+ navArgument(NavArgs.CAMPAIGN) {
type = NavType.StringType
defaultValue = ""
},
),
) { entry ->
- val productId = entry.arguments?.getString(Nav2Args.PRODUCT_ID).orEmpty()
- val source = entry.arguments?.getString(Nav2Args.SOURCE).orEmpty()
- val campaign = entry.arguments?.getString(Nav2Args.CAMPAIGN).orEmpty()
- TracedNav2ComposeRoute(Nav2RouteNames.PRODUCT_DETAIL) {
+ val productId = entry.arguments?.getString(NavArgs.PRODUCT_ID).orEmpty()
+ val source = entry.arguments?.getString(NavArgs.SOURCE).orEmpty()
+ val campaign = entry.arguments?.getString(NavArgs.CAMPAIGN).orEmpty()
+ TracedNav2ComposeRoute(selectedScenario, RouteNames.PRODUCT_DETAIL) {
Nav2ComposeProductDetailRoute(
- routeSpec = Nav2RouteSpecs.productDetail,
+ routeSpec = RouteSpecs.productDetail,
productId = productId,
source = source,
campaign = campaign,
@@ -189,12 +303,12 @@ internal fun Nav2ComposeApp(
composable(
route = Nav2ComposeDestination.CHECKOUT_ROUTE,
- arguments = listOf(navArgument(Nav2Args.PRODUCT_ID) { type = NavType.StringType }),
+ arguments = listOf(navArgument(NavArgs.PRODUCT_ID) { type = NavType.StringType }),
) { entry ->
- val productId = entry.arguments?.getString(Nav2Args.PRODUCT_ID).orEmpty()
- TracedNav2ComposeRoute(Nav2RouteNames.CHECKOUT) {
+ val productId = entry.arguments?.getString(NavArgs.PRODUCT_ID).orEmpty()
+ TracedNav2ComposeRoute(selectedScenario, RouteNames.CHECKOUT) {
Nav2ComposeCheckoutRoute(
- routeSpec = Nav2RouteSpecs.checkout,
+ routeSpec = RouteSpecs.checkout,
productId = productId,
onCompleteOrder = {
navigateTo(Confirmation(orderId = "order-$productId"))
@@ -205,28 +319,28 @@ internal fun Nav2ComposeApp(
composable(
route = Nav2ComposeDestination.CONFIRMATION_ROUTE,
- arguments = listOf(navArgument(Nav2Args.ORDER_ID) { type = NavType.StringType }),
+ arguments = listOf(navArgument(NavArgs.ORDER_ID) { type = NavType.StringType }),
) { entry ->
- TracedNav2ComposeRoute(Nav2RouteNames.CONFIRMATION) {
+ TracedNav2ComposeRoute(selectedScenario, RouteNames.CONFIRMATION) {
Nav2ComposeConfirmationRoute(
- routeSpec = Nav2RouteSpecs.confirmation,
- orderId = entry.arguments?.getString(Nav2Args.ORDER_ID).orEmpty(),
- onResetBackStack = { resetToHome() },
+ routeSpec = RouteSpecs.confirmation,
+ orderId = entry.arguments?.getString(NavArgs.ORDER_ID).orEmpty(),
+ onResetBackStack = { resetToRoot() },
)
}
}
dialog(
route = Nav2ComposeDestination.PROMO_DIALOG_ROUTE,
- arguments = listOf(navArgument(Nav2Args.PROMO_ID) { type = NavType.StringType }),
+ arguments = listOf(navArgument(NavArgs.PROMO_ID) { type = NavType.StringType }),
) { entry ->
// This dialog is a real Nav destination, so it participates in Nav2 the same way as the
// rest of the route graph. Compare it with the share sheet overlay below when inspecting
// Sentry's Nav2 breadcrumbs, destination arguments, and route transactions.
- TracedNav2ComposeRoute(Nav2RouteNames.PROMO_DIALOG) {
+ TracedNav2ComposeRoute(selectedScenario, RouteNames.PROMO_DIALOG) {
Nav2ComposePromoDialogRoute(
- routeSpec = Nav2RouteSpecs.promoDialog,
- promoId = entry.arguments?.getString(Nav2Args.PROMO_ID).orEmpty(),
+ routeSpec = RouteSpecs.promoDialog,
+ promoId = entry.arguments?.getString(NavArgs.PROMO_ID).orEmpty(),
onCaptureException = onCaptureException,
onCrashApp = onCrashApp,
onDismiss = { navigateBack() },
@@ -235,12 +349,27 @@ internal fun Nav2ComposeApp(
}
}
+ // These BackHandlers are intentionally declared AFTER NavHost. NavHost installs its own
+ // internal BackHandler that pops the real NavController; if ours ran second it would let
+ // NavHost silently pop the controller while this sample's tracked back stack (which drives
+ // the header and the root-exit decision) went stale. Composing ours last gives it priority in
+ // the OnBackPressedDispatcher, so the tracked list and the NavController are only ever moved
+ // together, and backing out of the root reliably exits the activity.
+ BackHandler(enabled = shareSheetProductId.value != null) { dismissShareSheet() }
+ BackHandler(enabled = shareSheetProductId.value == null) {
+ if (backStack.size > 1) {
+ navigateBack()
+ } else {
+ onExitRoot()
+ }
+ }
+
shareSheetProductId.value?.let { productId ->
// This share sheet is intentionally just a screen overlay, not a Nav destination. It lets
// the sample compare how Sentry's Nav2 integration behaves for proper Nav destinations vs.
// UI layered on top of the current route.
Nav2ComposeShareSheetRoute(
- routeSpec = Nav2RouteSpecs.shareSheet,
+ routeSpec = RouteSpecs.shareSheet,
productId = productId,
onCaptureException = onCaptureException,
onCrashApp = onCrashApp,
@@ -253,8 +382,12 @@ internal fun Nav2ComposeApp(
@OptIn(ExperimentalComposeUiApi::class)
@Composable
-private fun TracedNav2ComposeRoute(routeName: String, content: @Composable BoxScope.() -> Unit) {
- tagCurrentNav2Scenario(Nav2Scenario.COMPOSE)
+private fun TracedNav2ComposeRoute(
+ scenario: Nav2Scenario,
+ routeName: String,
+ content: @Composable BoxScope.() -> Unit,
+) {
+ tagCurrentNav2Scenario(scenario)
SentryTraced(
tag = "Nav2 /$routeName",
// Keep interaction tagging off here so route wrappers do not turn every Compose click into a
@@ -299,7 +432,7 @@ private suspend fun runRouteWork(
when (option) {
RouteWorkOption.HTTP_REQUEST -> {
try {
- GithubAPI.service.listReposAsync("getsentry", 5)
+ RouteWorkApi.runRequest()
} catch (e: IOException) {
Sentry.captureException(e)
} catch (e: HttpException) {
@@ -314,14 +447,85 @@ private suspend fun runRouteWork(
}
@Composable
-private fun Nav2ComposeHomeRoute(routeSpec: Nav2RouteSpec, onBrowseProducts: () -> Unit) {
+private fun Nav2ComposeHomeRoute(routeSpec: RouteSpec, onBrowseProducts: () -> Unit) {
Nav2ComposeActionRoute(routeSpec, buttons = listOf("Browse Products" to onBrowseProducts))
}
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+private fun Nav2ComposeCustomRoute(
+ routeSpec: RouteSpec,
+ mode: Nav2CustomTransactionMode,
+ onModeSelected: (Nav2CustomTransactionMode) -> Unit,
+ isAsyncBrowseProductsRunning: Boolean,
+ onBrowseProducts: () -> Unit,
+) {
+ val helperText =
+ when (mode) {
+ Nav2CustomTransactionMode.PER_SCREEN ->
+ "Starts a custom transaction for every destination so route work runs under app-owned screen-level transactions."
+ Nav2CustomTransactionMode.WHOLE_FLOW ->
+ "Keeps one custom transaction open for the whole shopping journey until the flow returns to the Custom home screen."
+ Nav2CustomTransactionMode.ASYNC_FROM_USER_ACTION ->
+ "This mode starts a manual transaction from the button tap, waits for async work, and then pushes Product List."
+ Nav2CustomTransactionMode.LINGERING ->
+ "The lingering transaction stays active until you leave the Custom tab."
+ }
+ val sentryPink = colorResource(R.color.colorAccent)
+
+ Nav2ComposeRouteScaffold(
+ routeSpec = routeSpec,
+ cardContent = {
+ Text("Mode", style = MaterialTheme.typography.titleSmall)
+ SingleChoiceSegmentedButtonRow(modifier = Modifier.fillMaxWidth()) {
+ Nav2CustomTransactionMode.entries.forEachIndexed { index, entry ->
+ SegmentedButton(
+ modifier = Modifier.weight(1f).defaultMinSize(minHeight = 72.dp),
+ shape =
+ SegmentedButtonDefaults.itemShape(
+ index = index,
+ count = Nav2CustomTransactionMode.entries.size,
+ ),
+ onClick = { onModeSelected(entry) },
+ selected = mode == entry,
+ colors =
+ SegmentedButtonDefaults.colors(
+ activeContainerColor = sentryPink,
+ activeContentColor = Color.White,
+ ),
+ icon = {},
+ label = {
+ Text(
+ text = entry.label,
+ style = MaterialTheme.typography.labelSmall,
+ textAlign = TextAlign.Center,
+ maxLines = 2,
+ )
+ },
+ )
+ }
+ }
+ Text(mode.description, style = MaterialTheme.typography.bodyMedium)
+ Nav2ComposeRouteButton(
+ label =
+ if (
+ mode == Nav2CustomTransactionMode.ASYNC_FROM_USER_ACTION && isAsyncBrowseProductsRunning
+ ) {
+ "Starting async custom transaction..."
+ } else {
+ "Browse Products"
+ },
+ onClick = onBrowseProducts,
+ )
+ },
+ content = { Text(helperText, style = MaterialTheme.typography.bodyMedium) },
+ )
+}
+
@OptIn(ExperimentalComposeUiApi::class)
@Composable
private fun Nav2ComposeProductListRoute(
- routeSpec: Nav2RouteSpec,
+ routeSpec: RouteSpec,
onOpenProduct42: () -> Unit,
onOpenProduct7: () -> Unit,
) {
@@ -363,7 +567,7 @@ private fun Nav2ComposeProductListRoute(
@Composable
private fun Nav2ComposeProductDetailRoute(
- routeSpec: Nav2RouteSpec,
+ routeSpec: RouteSpec,
productId: String,
source: String,
campaign: String,
@@ -372,16 +576,16 @@ private fun Nav2ComposeProductDetailRoute(
onCheckout: () -> Unit,
) {
LaunchedEffect(productId, source, campaign) {
- recordSimulatedBackgroundSpan(Nav2RouteNames.PRODUCT_DETAIL)
+ recordSimulatedBackgroundSpan(RouteNames.PRODUCT_DETAIL)
}
Nav2ComposeActionRoute(
routeSpec,
arguments =
mapOf(
- Nav2Args.PRODUCT_ID to productId,
- Nav2Args.SOURCE to source,
- Nav2Args.CAMPAIGN to campaign,
+ NavArgs.PRODUCT_ID to productId,
+ NavArgs.SOURCE to source,
+ NavArgs.CAMPAIGN to campaign,
),
buttons =
listOf(
@@ -394,33 +598,33 @@ private fun Nav2ComposeProductDetailRoute(
@Composable
private fun Nav2ComposeCheckoutRoute(
- routeSpec: Nav2RouteSpec,
+ routeSpec: RouteSpec,
productId: String,
onCompleteOrder: () -> Unit,
) {
Nav2ComposeActionRoute(
routeSpec,
- arguments = mapOf(Nav2Args.PRODUCT_ID to productId),
+ arguments = mapOf(NavArgs.PRODUCT_ID to productId),
buttons = listOf("Complete Order" to onCompleteOrder),
)
}
@Composable
private fun Nav2ComposeConfirmationRoute(
- routeSpec: Nav2RouteSpec,
+ routeSpec: RouteSpec,
orderId: String,
onResetBackStack: () -> Unit,
) {
Nav2ComposeActionRoute(
routeSpec,
- arguments = mapOf(Nav2Args.ORDER_ID to orderId),
+ arguments = mapOf(NavArgs.ORDER_ID to orderId),
buttons = listOf("Reset Backstack" to onResetBackStack),
)
}
@Composable
private fun Nav2ComposeActionRoute(
- routeSpec: Nav2RouteSpec,
+ routeSpec: RouteSpec,
arguments: Map = emptyMap(),
buttons: List Unit>>,
) {
@@ -437,7 +641,7 @@ private fun Nav2ComposeActionRoute(
@Composable
private fun Nav2ComposePromoDialogRoute(
- routeSpec: Nav2RouteSpec,
+ routeSpec: RouteSpec,
promoId: String,
onCaptureException: () -> Unit,
onCrashApp: () -> Unit,
@@ -448,7 +652,7 @@ private fun Nav2ComposePromoDialogRoute(
title = { Text(routeSpec.title) },
text = {
val argumentText =
- routeSpec.displayArguments(mapOf(Nav2Args.PROMO_ID to promoId)).toDisplayString()
+ routeSpec.displayArguments(mapOf(NavArgs.PROMO_ID to promoId)).toDisplayString()
Text(
listOfNotNull(routeSpec.description, argumentText.takeIf { it.isNotEmpty() })
.joinToString("\n\n")
@@ -483,7 +687,7 @@ private fun Nav2ComposePromoDialogRoute(
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun Nav2ComposeShareSheetRoute(
- routeSpec: Nav2RouteSpec,
+ routeSpec: RouteSpec,
productId: String,
onCaptureException: () -> Unit,
onCrashApp: () -> Unit,
@@ -496,8 +700,7 @@ private fun Nav2ComposeShareSheetRoute(
) {
Text(routeSpec.title, style = MaterialTheme.typography.headlineSmall)
routeSpec.description?.let { Text(it) }
- routeSpec.displayArguments(mapOf(Nav2Args.PRODUCT_ID to productId)).forEach { (label, value)
- ->
+ routeSpec.displayArguments(mapOf(NavArgs.PRODUCT_ID to productId)).forEach { (label, value) ->
Text("$label=$value")
}
Button(
@@ -527,7 +730,7 @@ private fun Nav2ComposeShareSheetRoute(
@Composable
private fun Nav2ComposeRouteScaffold(
- routeSpec: Nav2RouteSpec,
+ routeSpec: RouteSpec,
cardContent: (@Composable ColumnScope.() -> Unit)? = null,
content: (@Composable ColumnScope.() -> Unit)? = null,
) {
@@ -602,6 +805,7 @@ private fun Nav2ComposeRouteButton(
private fun nav2ComposeInteractionTag(label: String): String = "Nav2 Compose $label"
private const val PRODUCT_LIST_ITEM_COUNT = 20
+private const val COMPOSE_ROUTE_TRANSITION_MILLIS = 350
@Composable
private fun Nav2ComposeRouteInfo(label: String, value: String) {
@@ -651,16 +855,17 @@ private fun nav2ComposeBackStackSaver() =
private fun List.toComposeBackStackText(): String =
joinToString(" -> ") { destination -> destination.backStackRoute() }
-private sealed class Nav2ComposeDestination(
+internal sealed class Nav2ComposeDestination(
val routeName: String,
val route: String,
val arguments: Map = emptyMap(),
) {
- data object Home : Nav2ComposeDestination(Nav2RouteNames.HOME, Nav2RouteNames.HOME)
+ data object Home : Nav2ComposeDestination(RouteNames.HOME, RouteNames.HOME)
+
+ data object Custom : Nav2ComposeDestination(RouteNames.CUSTOM, RouteNames.CUSTOM)
- data object ProductList :
- Nav2ComposeDestination(Nav2RouteNames.PRODUCT_LIST, Nav2RouteNames.PRODUCT_LIST)
+ data object ProductList : Nav2ComposeDestination(RouteNames.PRODUCT_LIST, RouteNames.PRODUCT_LIST)
data class ProductDetail(
val productId: String,
@@ -668,42 +873,42 @@ private sealed class Nav2ComposeDestination(
val campaign: String = "",
) :
Nav2ComposeDestination(
- routeName = Nav2RouteNames.PRODUCT_DETAIL,
+ routeName = RouteNames.PRODUCT_DETAIL,
route =
- "${Nav2RouteNames.PRODUCT_DETAIL}/$productId/$source" +
- if (campaign.isNotEmpty()) "?${Nav2Args.CAMPAIGN}=$campaign" else "",
+ "${RouteNames.PRODUCT_DETAIL}/$productId/$source" +
+ if (campaign.isNotEmpty()) "?${NavArgs.CAMPAIGN}=$campaign" else "",
arguments =
mapOf(
- Nav2Args.PRODUCT_ID to productId,
- Nav2Args.SOURCE to source,
- Nav2Args.CAMPAIGN to campaign,
+ NavArgs.PRODUCT_ID to productId,
+ NavArgs.SOURCE to source,
+ NavArgs.CAMPAIGN to campaign,
)
.filterValues { value -> value.isNotEmpty() },
)
data class Checkout(val productId: String) :
Nav2ComposeDestination(
- routeName = Nav2RouteNames.CHECKOUT,
- route = "${Nav2RouteNames.CHECKOUT}/$productId",
- arguments = mapOf(Nav2Args.PRODUCT_ID to productId),
+ routeName = RouteNames.CHECKOUT,
+ route = "${RouteNames.CHECKOUT}/$productId",
+ arguments = mapOf(NavArgs.PRODUCT_ID to productId),
)
data class Confirmation(val orderId: String) :
Nav2ComposeDestination(
- routeName = Nav2RouteNames.CONFIRMATION,
- route = "${Nav2RouteNames.CONFIRMATION}/$orderId",
- arguments = mapOf(Nav2Args.ORDER_ID to orderId),
+ routeName = RouteNames.CONFIRMATION,
+ route = "${RouteNames.CONFIRMATION}/$orderId",
+ arguments = mapOf(NavArgs.ORDER_ID to orderId),
)
data class PromoDialog(val promoId: String) :
Nav2ComposeDestination(
- routeName = Nav2RouteNames.PROMO_DIALOG,
- route = "${Nav2RouteNames.PROMO_DIALOG}/$promoId",
- arguments = mapOf(Nav2Args.PROMO_ID to promoId),
+ routeName = RouteNames.PROMO_DIALOG,
+ route = "${RouteNames.PROMO_DIALOG}/$promoId",
+ arguments = mapOf(NavArgs.PROMO_ID to promoId),
)
fun displayRoute(): String {
- return Nav2RouteSpecs.get(routeName).displayRoute(arguments)
+ return RouteSpecs.get(routeName).displayRoute(arguments)
}
fun backStackRoute(): String = "/$routeName"
@@ -712,59 +917,61 @@ private sealed class Nav2ComposeDestination(
Bundle().apply {
when (this@Nav2ComposeDestination) {
Home -> putString("type", "home")
+ Custom -> putString("type", "custom")
ProductList -> putString("type", "product_list")
is ProductDetail -> {
putString("type", "product_detail")
- putString(Nav2Args.PRODUCT_ID, productId)
- putString(Nav2Args.SOURCE, source)
- putString(Nav2Args.CAMPAIGN, campaign)
+ putString(NavArgs.PRODUCT_ID, productId)
+ putString(NavArgs.SOURCE, source)
+ putString(NavArgs.CAMPAIGN, campaign)
}
is Checkout -> {
putString("type", "checkout")
- putString(Nav2Args.PRODUCT_ID, productId)
+ putString(NavArgs.PRODUCT_ID, productId)
}
is Confirmation -> {
putString("type", "confirmation")
- putString(Nav2Args.ORDER_ID, orderId)
+ putString(NavArgs.ORDER_ID, orderId)
}
is PromoDialog -> {
putString("type", "promo_dialog")
- putString(Nav2Args.PROMO_ID, promoId)
+ putString(NavArgs.PROMO_ID, promoId)
}
}
}
companion object {
const val PRODUCT_DETAIL_ROUTE =
- Nav2RouteNames.PRODUCT_DETAIL +
+ RouteNames.PRODUCT_DETAIL +
"/{" +
- Nav2Args.PRODUCT_ID +
+ NavArgs.PRODUCT_ID +
"}/{" +
- Nav2Args.SOURCE +
+ NavArgs.SOURCE +
"}?" +
- Nav2Args.CAMPAIGN +
+ NavArgs.CAMPAIGN +
"={" +
- Nav2Args.CAMPAIGN +
+ NavArgs.CAMPAIGN +
"}"
- const val CHECKOUT_ROUTE = Nav2RouteNames.CHECKOUT + "/{" + Nav2Args.PRODUCT_ID + "}"
- const val CONFIRMATION_ROUTE = Nav2RouteNames.CONFIRMATION + "/{" + Nav2Args.ORDER_ID + "}"
- const val PROMO_DIALOG_ROUTE = Nav2RouteNames.PROMO_DIALOG + "/{" + Nav2Args.PROMO_ID + "}"
+ const val CHECKOUT_ROUTE = RouteNames.CHECKOUT + "/{" + NavArgs.PRODUCT_ID + "}"
+ const val CONFIRMATION_ROUTE = RouteNames.CONFIRMATION + "/{" + NavArgs.ORDER_ID + "}"
+ const val PROMO_DIALOG_ROUTE = RouteNames.PROMO_DIALOG + "/{" + NavArgs.PROMO_ID + "}"
}
}
private fun Bundle.toNav2ComposeDestination(): Nav2ComposeDestination {
return when (getString("type")) {
"home" -> Home
+ "custom" -> Custom
"product_list" -> ProductList
"product_detail" ->
ProductDetail(
- productId = requireNotNull(getString(Nav2Args.PRODUCT_ID)),
- source = requireNotNull(getString(Nav2Args.SOURCE)),
- campaign = getString(Nav2Args.CAMPAIGN).orEmpty(),
+ productId = requireNotNull(getString(NavArgs.PRODUCT_ID)),
+ source = requireNotNull(getString(NavArgs.SOURCE)),
+ campaign = getString(NavArgs.CAMPAIGN).orEmpty(),
)
- "checkout" -> Checkout(productId = requireNotNull(getString(Nav2Args.PRODUCT_ID)))
- "confirmation" -> Confirmation(orderId = requireNotNull(getString(Nav2Args.ORDER_ID)))
- "promo_dialog" -> PromoDialog(promoId = requireNotNull(getString(Nav2Args.PROMO_ID)))
+ "checkout" -> Checkout(productId = requireNotNull(getString(NavArgs.PRODUCT_ID)))
+ "confirmation" -> Confirmation(orderId = requireNotNull(getString(NavArgs.ORDER_ID)))
+ "promo_dialog" -> PromoDialog(promoId = requireNotNull(getString(NavArgs.PROMO_ID)))
else -> Home
}
}
diff --git a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/Nav2ContentHosts.kt b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/nav2/Nav2ContentHosts.kt
similarity index 97%
rename from sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/Nav2ContentHosts.kt
rename to sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/nav2/Nav2ContentHosts.kt
index 38a85b77a82..4176bcac294 100644
--- a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/Nav2ContentHosts.kt
+++ b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/nav2/Nav2ContentHosts.kt
@@ -1,4 +1,4 @@
-package io.sentry.samples.android.navigation
+package io.sentry.samples.android.navigation.nav2
import android.content.Context
import android.view.View
diff --git a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/nav2/Nav2CustomTransactions.kt b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/nav2/Nav2CustomTransactions.kt
new file mode 100644
index 00000000000..d4d5348bc46
--- /dev/null
+++ b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/nav2/Nav2CustomTransactions.kt
@@ -0,0 +1,205 @@
+package io.sentry.samples.android.navigation.nav2
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import io.sentry.ITransaction
+import io.sentry.Sentry
+import io.sentry.SpanStatus
+import io.sentry.TransactionOptions
+import io.sentry.samples.android.navigation.common.RouteNames
+
+internal enum class Nav2CustomTransactionMode(
+ val label: String,
+ val description: String,
+) {
+ PER_SCREEN(
+ label = "Per Screen",
+ description =
+ "Starts a custom transaction for every destination so Nav2 route work runs under app-owned screen-level transactions.",
+ ),
+ WHOLE_FLOW(
+ label = "Whole Flow",
+ description =
+ "Keeps one custom transaction open for the whole shopping journey until the flow returns to the Custom home screen.",
+ ),
+ LINGERING(
+ label = "Lingering",
+ description =
+ "Starts one custom transaction and intentionally leaves it active across later routes to simulate a stale power-user transaction.",
+ ),
+ ASYNC_FROM_USER_ACTION(
+ label = "Async From User Action",
+ description =
+ "Starts a custom transaction from the Browse Products button, waits for async work, then navigates to Product List while the manual transaction stays active.",
+ ),
+}
+
+@Composable
+internal fun Nav2CustomTransactionEffect(
+ selectedDestination: Any,
+ mode: Nav2CustomTransactionMode,
+ controller: Nav2CustomTransactionController,
+) {
+ DisposableEffect(selectedDestination, mode) {
+ controller.onDestinationChanged(selectedDestination, mode)
+ onDispose {}
+ }
+
+ DisposableEffect(controller) { onDispose { controller.cleanup() } }
+}
+
+internal class Nav2CustomTransactionController {
+
+ private var activeTransaction: ITransaction? = null
+ private var activeMode: Nav2CustomTransactionMode? = null
+ private var activeRouteName: String? = null
+
+ fun onDestinationChanged(destination: Any, mode: Nav2CustomTransactionMode) {
+ val currentRouteName =
+ when (destination) {
+ is Nav2ComposeDestination -> destination.routeName
+ else -> return cleanup()
+ }
+
+ val isCustomScenario = destination is Nav2ComposeDestination.Custom || activeMode != null
+ if (!isCustomScenario) {
+ cleanup()
+ return
+ }
+
+ when (mode) {
+ Nav2CustomTransactionMode.PER_SCREEN -> handlePerScreen(currentRouteName)
+ Nav2CustomTransactionMode.WHOLE_FLOW -> handleWholeFlow(currentRouteName)
+ Nav2CustomTransactionMode.LINGERING -> handleLingering(currentRouteName)
+ Nav2CustomTransactionMode.ASYNC_FROM_USER_ACTION ->
+ handleAsyncFromUserAction(currentRouteName)
+ }
+ }
+
+ fun startAsyncBrowseProductsTransaction() {
+ finishActiveTransaction()
+ activeTransaction =
+ startCustomTransaction(
+ name = "custom.tap_to_browse_products",
+ operation = "ui.action",
+ mode = Nav2CustomTransactionMode.ASYNC_FROM_USER_ACTION,
+ routeName = RouteNames.CUSTOM,
+ )
+ activeMode = Nav2CustomTransactionMode.ASYNC_FROM_USER_ACTION
+ activeRouteName = RouteNames.CUSTOM
+ activeTransaction?.setData("sample.async_trigger", "browse_products")
+ }
+
+ fun cleanup() {
+ finishActiveTransaction()
+ }
+
+ private fun handlePerScreen(currentRoute: String) {
+ if (activeMode == Nav2CustomTransactionMode.PER_SCREEN && activeRouteName == currentRoute) {
+ return
+ }
+
+ finishActiveTransaction()
+ activeTransaction =
+ startCustomTransaction(
+ name = "custom.${currentRoute.lowercase()}_screen",
+ operation = "ui.screen.manual",
+ mode = Nav2CustomTransactionMode.PER_SCREEN,
+ routeName = currentRoute,
+ )
+ activeMode = Nav2CustomTransactionMode.PER_SCREEN
+ activeRouteName = currentRoute
+ }
+
+ private fun handleWholeFlow(currentRoute: String) {
+ if (currentRoute == RouteNames.CUSTOM) {
+ finishActiveTransaction()
+ activeMode = Nav2CustomTransactionMode.WHOLE_FLOW
+ activeRouteName = currentRoute
+ return
+ }
+
+ if (activeMode != Nav2CustomTransactionMode.WHOLE_FLOW || activeTransaction == null) {
+ finishActiveTransaction()
+ activeTransaction =
+ startCustomTransaction(
+ name = "custom.checkout_flow",
+ operation = "ui.flow.manual",
+ mode = Nav2CustomTransactionMode.WHOLE_FLOW,
+ routeName = currentRoute,
+ )
+ activeMode = Nav2CustomTransactionMode.WHOLE_FLOW
+ }
+
+ activeRouteName = currentRoute
+ activeTransaction?.setData("sample.current_route", currentRoute)
+ }
+
+ private fun handleLingering(currentRoute: String) {
+ if (activeMode == Nav2CustomTransactionMode.LINGERING && activeTransaction != null) {
+ activeTransaction?.setData("sample.current_route", currentRoute)
+ return
+ }
+
+ finishActiveTransaction()
+ activeTransaction =
+ startCustomTransaction(
+ name = "custom.lingering_navigation_transaction",
+ operation = "ui.flow.manual",
+ mode = Nav2CustomTransactionMode.LINGERING,
+ routeName = currentRoute,
+ )
+ activeMode = Nav2CustomTransactionMode.LINGERING
+ activeRouteName = currentRoute
+ }
+
+ private fun handleAsyncFromUserAction(currentRoute: String) {
+ if (activeMode != Nav2CustomTransactionMode.ASYNC_FROM_USER_ACTION) {
+ finishActiveTransaction()
+ return
+ }
+
+ val transaction = activeTransaction ?: return
+ activeRouteName = currentRoute
+ transaction.setData("sample.current_route", currentRoute)
+
+ if (currentRoute != RouteNames.CUSTOM && currentRoute != RouteNames.PRODUCT_LIST) {
+ finishActiveTransaction()
+ }
+ }
+
+ private fun startCustomTransaction(
+ name: String,
+ operation: String,
+ mode: Nav2CustomTransactionMode,
+ routeName: String,
+ ): ITransaction {
+ val options =
+ TransactionOptions().also {
+ it.isBindToScope = true
+ it.isWaitForChildren = true
+ it.idleTimeout = null
+ }
+ return Sentry.startTransaction(name, operation, options).apply {
+ setTag("sample_nav2_scenario", Nav2Scenario.CUSTOM.label)
+ setTag("sample_nav2_custom_mode", mode.label)
+ setData("sample.custom_transaction", true)
+ setData("sample.current_route", routeName)
+ }
+ }
+
+ private fun finishActiveTransaction() {
+ val transaction = activeTransaction ?: return
+ transaction.finish(transaction.status ?: SpanStatus.OK)
+ Sentry.configureScope { scope ->
+ scope.withTransaction { current ->
+ if (current == transaction) {
+ scope.clearTransaction()
+ }
+ }
+ }
+ activeTransaction = null
+ activeMode = null
+ activeRouteName = null
+ }
+}
diff --git a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/Nav2ModalFragments.kt b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/nav2/Nav2ModalFragments.kt
similarity index 91%
rename from sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/Nav2ModalFragments.kt
rename to sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/nav2/Nav2ModalFragments.kt
index a06df9ae62f..dc1e593a68b 100644
--- a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/Nav2ModalFragments.kt
+++ b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/nav2/Nav2ModalFragments.kt
@@ -1,4 +1,4 @@
-package io.sentry.samples.android.navigation
+package io.sentry.samples.android.navigation.nav2
import android.app.Dialog
import android.content.Context
@@ -16,13 +16,17 @@ import android.widget.TextView
import androidx.core.view.setPadding
import androidx.fragment.app.DialogFragment
import io.sentry.samples.android.R
+import io.sentry.samples.android.navigation.common.NavArgs
+import io.sentry.samples.android.navigation.common.RouteSpecs
+import io.sentry.samples.android.navigation.common.displayArguments
+import io.sentry.samples.android.navigation.common.themeColor
class Nav2PromoDialogFragment : DialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val activity = requireActivity() as Nav2Activity
activity.tagCurrentScenarioOnTransaction()
- val promoId = requireArguments().getString(Nav2Args.PROMO_ID).orEmpty()
+ val promoId = requireArguments().getString(NavArgs.PROMO_ID).orEmpty()
return Dialog(requireContext()).apply {
window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
@@ -43,14 +47,14 @@ class Nav2PromoDialogFragment : DialogFragment() {
private fun promoDialogContent(activity: Nav2Activity, promoId: String): View =
LinearLayout(requireContext()).apply {
- val routeSpec = Nav2RouteSpecs.promoDialog
+ val routeSpec = RouteSpecs.promoDialog
orientation = LinearLayout.VERTICAL
background = roundedSurface(context, topCornersOnly = false)
setPadding(24.dp(context))
addView(sectionLabel(context, "Navigation destination"))
addView(titleText(context, routeSpec.title))
routeSpec.description?.let { addView(bodyText(context, it)) }
- routeSpec.displayArguments(mapOf(Nav2Args.PROMO_ID to promoId)).firstOrNull()?.let {
+ routeSpec.displayArguments(mapOf(NavArgs.PROMO_ID to promoId)).firstOrNull()?.let {
(label, value) ->
addView(argumentPill(context, "$label=$value"))
}
@@ -85,7 +89,7 @@ class Nav2ShareSheetFragment : DialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val activity = requireActivity() as Nav2Activity
activity.tagCurrentScenarioOnTransaction()
- val productId = requireArguments().getString(Nav2Args.PRODUCT_ID).orEmpty()
+ val productId = requireArguments().getString(NavArgs.PRODUCT_ID).orEmpty()
return Dialog(requireContext()).apply {
window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
@@ -109,14 +113,14 @@ class Nav2ShareSheetFragment : DialogFragment() {
private fun shareSheetContent(activity: Nav2Activity, productId: String): View =
LinearLayout(requireContext()).apply {
- val routeSpec = Nav2RouteSpecs.shareSheet
+ val routeSpec = RouteSpecs.shareSheet
orientation = LinearLayout.VERTICAL
setPadding(24.dp(context))
background = roundedSurface(context, topCornersOnly = true)
addView(sectionLabel(context, "Overlay surface"))
addView(titleText(context, routeSpec.title))
routeSpec.description?.let { addView(bodyText(context, it)) }
- routeSpec.displayArguments(mapOf(Nav2Args.PRODUCT_ID to productId)).firstOrNull()?.let {
+ routeSpec.displayArguments(mapOf(NavArgs.PRODUCT_ID to productId)).firstOrNull()?.let {
(label, value) ->
addView(argumentPill(context, "$label=$value"))
}
diff --git a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/NavigationPerformanceControls.kt b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/nav2/Nav2PerformanceControls.kt
similarity index 94%
rename from sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/NavigationPerformanceControls.kt
rename to sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/nav2/Nav2PerformanceControls.kt
index 54ae586566d..2dc9a4517e4 100644
--- a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/NavigationPerformanceControls.kt
+++ b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/nav2/Nav2PerformanceControls.kt
@@ -1,4 +1,4 @@
-package io.sentry.samples.android.navigation
+package io.sentry.samples.android.navigation.nav2
import android.os.Trace
import androidx.compose.foundation.layout.Arrangement
@@ -34,7 +34,7 @@ import androidx.compose.ui.unit.dp
import kotlinx.coroutines.delay
/** State holder backing the [Nav2Scenario.PERFORMANCE] tab. */
-internal class NavigationPerformanceState {
+internal class Nav2PerformanceState {
var stackDepth by mutableIntStateOf(30)
var autoRecompose by mutableStateOf(false)
@@ -83,12 +83,12 @@ internal class NavigationPerformanceState {
/** "Performance" tab content. */
@Composable
-internal fun NavigationPerformancePanel(
+internal fun Nav2PerformancePanel(
title: String,
description: String,
currentRoute: String,
backStack: String,
- state: NavigationPerformanceState,
+ state: Nav2PerformanceState,
onBuildStack: () -> Unit,
onReplaceTop: () -> Unit,
) {
@@ -98,7 +98,7 @@ internal fun NavigationPerformancePanel(
if (!state.autoRecompose) {
break
}
- traceNavigationPerformanceSection("Nav2Stress.autoRecompose") {
+ traceNav2PerformanceSection("Nav2Stress.autoRecompose") {
state.markRecompositionRequest()
}
}
@@ -110,7 +110,7 @@ internal fun NavigationPerformancePanel(
if (!state.autoNavigate) {
break
}
- traceNavigationPerformanceSection("Nav2Stress.autoNavigate") { onReplaceTop() }
+ traceNav2PerformanceSection("Nav2Stress.autoNavigate") { onReplaceTop() }
}
}
@@ -264,7 +264,7 @@ private fun PerfToggleButton(
}
}
-internal fun traceNavigationPerformanceSection(sectionName: String, block: () -> Unit) {
+internal fun traceNav2PerformanceSection(sectionName: String, block: () -> Unit) {
Trace.beginSection(sectionName)
try {
block()
@@ -273,7 +273,7 @@ internal fun traceNavigationPerformanceSection(sectionName: String, block: () ->
}
}
-internal fun navigationPerformanceBackStackPreview(entries: List): String {
+internal fun nav2PerformanceBackStackPreview(entries: List): String {
if (entries.size <= 8) {
return entries.joinToString(" -> ")
}
diff --git a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/Nav2RouteFragment.kt b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/nav2/Nav2RouteFragment.kt
similarity index 86%
rename from sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/Nav2RouteFragment.kt
rename to sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/nav2/Nav2RouteFragment.kt
index 5fb4ef1bdd6..8a76d3eb301 100644
--- a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/Nav2RouteFragment.kt
+++ b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/nav2/Nav2RouteFragment.kt
@@ -1,4 +1,4 @@
-package io.sentry.samples.android.navigation
+package io.sentry.samples.android.navigation.nav2
import android.graphics.Typeface
import android.os.Bundle
@@ -16,6 +16,12 @@ import androidx.lifecycle.lifecycleScope
import io.sentry.ISpan
import io.sentry.Sentry
import io.sentry.samples.android.R
+import io.sentry.samples.android.navigation.common.NavArgs
+import io.sentry.samples.android.navigation.common.RouteNames
+import io.sentry.samples.android.navigation.common.RouteSpec
+import io.sentry.samples.android.navigation.common.RouteSpecs
+import io.sentry.samples.android.navigation.common.displayArguments
+import io.sentry.samples.android.navigation.common.themeColor
import kotlinx.coroutines.launch
class Nav2RouteFragment : Fragment() {
@@ -26,13 +32,13 @@ class Nav2RouteFragment : Fragment() {
savedInstanceState: Bundle?,
): View {
val activity = requireActivity() as Nav2Activity
- val routeName = requireArguments().getString(Nav2Args.ROUTE_NAME).orEmpty()
- val routeSpec = Nav2RouteSpecs.get(routeName)
+ val routeName = requireArguments().getString(NavArgs.ROUTE_NAME).orEmpty()
+ val routeSpec = RouteSpecs.get(routeName)
return when (routeName) {
- Nav2RouteNames.LANDING -> routeLayout(routeSpec)
+ RouteNames.LANDING -> routeLayout(routeSpec)
- Nav2RouteNames.HOME ->
+ RouteNames.HOME ->
routeLayout(
routeSpec,
buttons =
@@ -43,7 +49,7 @@ class Nav2RouteFragment : Fragment() {
),
)
- Nav2RouteNames.DEEP_LINK ->
+ RouteNames.DEEP_LINK ->
routeLayout(
routeSpec,
buttons =
@@ -54,7 +60,7 @@ class Nav2RouteFragment : Fragment() {
),
)
- Nav2RouteNames.PRODUCT_LIST ->
+ RouteNames.PRODUCT_LIST ->
routeLayout(
routeSpec,
buttons =
@@ -71,15 +77,15 @@ class Nav2RouteFragment : Fragment() {
trailingContent = { addProductListItemsToggle() },
)
- Nav2RouteNames.PRODUCT_DETAIL -> productDetailLayout(activity)
+ RouteNames.PRODUCT_DETAIL -> productDetailLayout(activity)
- Nav2RouteNames.CHECKOUT -> checkoutLayout(activity)
+ RouteNames.CHECKOUT -> checkoutLayout(activity)
- Nav2RouteNames.CONFIRMATION -> confirmationLayout(activity)
+ RouteNames.CONFIRMATION -> confirmationLayout(activity)
else ->
routeLayout(
- Nav2RouteSpec(routeName = routeName, title = "Unknown Route", description = routeName)
+ RouteSpec(routeName = routeName, title = "Unknown Route", description = routeName)
)
}
}
@@ -87,16 +93,16 @@ class Nav2RouteFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val activity = requireActivity() as Nav2Activity
- val routeName = requireArguments().getString(Nav2Args.ROUTE_NAME).orEmpty()
+ val routeName = requireArguments().getString(NavArgs.ROUTE_NAME).orEmpty()
activity.tagCurrentScenarioOnTransaction()
- if (routeName == Nav2RouteNames.LANDING) {
+ if (routeName == RouteNames.LANDING) {
view.post { activity.cancelCurrentUiLoadTransaction() }
return
}
- if (routeName == Nav2RouteNames.PRODUCT_DETAIL) {
+ if (routeName == RouteNames.PRODUCT_DETAIL) {
viewLifecycleOwner.lifecycleScope.launch {
recordSimulatedBackgroundSpan(routeName)
}
@@ -107,8 +113,8 @@ class Nav2RouteFragment : Fragment() {
private fun productDetailLayout(activity: Nav2Activity): View {
val arguments = requireArguments()
- val productId = arguments.getString(Nav2Args.PRODUCT_ID).orEmpty()
- val source = arguments.getString(Nav2Args.SOURCE).orEmpty()
+ val productId = arguments.getString(NavArgs.PRODUCT_ID).orEmpty()
+ val source = arguments.getString(NavArgs.SOURCE).orEmpty()
val scenario =
if (source == "deep-link") {
Nav2Scenario.DEEP_LINK
@@ -116,7 +122,7 @@ class Nav2RouteFragment : Fragment() {
Nav2Scenario.FRAGMENTS
}
- val routeSpec = Nav2RouteSpecs.productDetail
+ val routeSpec = RouteSpecs.productDetail
return routeLayout(
routeSpec,
arguments = arguments,
@@ -136,8 +142,8 @@ class Nav2RouteFragment : Fragment() {
}
private fun checkoutLayout(activity: Nav2Activity): View {
- val productId = requireArguments().getString(Nav2Args.PRODUCT_ID).orEmpty()
- val routeSpec = Nav2RouteSpecs.checkout
+ val productId = requireArguments().getString(NavArgs.PRODUCT_ID).orEmpty()
+ val routeSpec = RouteSpecs.checkout
return routeLayout(
routeSpec,
arguments = requireArguments(),
@@ -151,7 +157,7 @@ class Nav2RouteFragment : Fragment() {
}
private fun confirmationLayout(activity: Nav2Activity): View {
- val routeSpec = Nav2RouteSpecs.confirmation
+ val routeSpec = RouteSpecs.confirmation
return routeLayout(
routeSpec,
arguments = requireArguments(),
@@ -165,7 +171,7 @@ class Nav2RouteFragment : Fragment() {
}
private fun routeLayout(
- routeSpec: Nav2RouteSpec,
+ routeSpec: RouteSpec,
arguments: Bundle? = null,
buttons: List = emptyList(),
trailingContent: (LinearLayout.() -> Unit)? = null,
diff --git a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/nav2/Nav2Routes.kt b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/nav2/Nav2Routes.kt
new file mode 100644
index 00000000000..7cf3532a09d
--- /dev/null
+++ b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/nav2/Nav2Routes.kt
@@ -0,0 +1,217 @@
+package io.sentry.samples.android.navigation.nav2
+
+import android.os.Bundle
+import android.view.ViewGroup
+import androidx.core.os.bundleOf
+import androidx.navigation.NavDestination
+import io.sentry.Sentry
+import io.sentry.protocol.SentryTransaction
+import io.sentry.samples.android.R
+import io.sentry.samples.android.navigation.common.NavArgs
+import io.sentry.samples.android.navigation.common.RouteNames
+import io.sentry.samples.android.navigation.common.RouteSpec
+import io.sentry.samples.android.navigation.common.RouteSpecs
+import io.sentry.samples.android.navigation.common.displayRoute
+import kotlin.coroutines.resume
+import kotlinx.coroutines.suspendCancellableCoroutine
+
+internal sealed class Nav2Destination(
+ val id: Int,
+ val routeName: String,
+ val arguments: Bundle = Bundle.EMPTY,
+) {
+
+ data object Landing : Nav2Destination(R.id.nav2_landing, RouteNames.LANDING)
+
+ data object Home : Nav2Destination(R.id.nav2_home, RouteNames.HOME)
+
+ data object ProductList : Nav2Destination(R.id.nav2_product_list, RouteNames.PRODUCT_LIST)
+
+ data object DeepLink : Nav2Destination(R.id.nav2_deep_link, RouteNames.DEEP_LINK)
+
+ data class ProductDetail(
+ val productId: String,
+ val source: String,
+ val campaign: String = "",
+ ) :
+ Nav2Destination(
+ R.id.nav2_product_detail,
+ RouteNames.PRODUCT_DETAIL,
+ bundleOf(
+ NavArgs.PRODUCT_ID to productId,
+ NavArgs.SOURCE to source,
+ NavArgs.CAMPAIGN to campaign,
+ ),
+ )
+
+ data class Checkout(val productId: String) :
+ Nav2Destination(
+ R.id.nav2_checkout,
+ RouteNames.CHECKOUT,
+ bundleOf(NavArgs.PRODUCT_ID to productId),
+ )
+
+ data class Confirmation(val orderId: String) :
+ Nav2Destination(
+ R.id.nav2_confirmation,
+ RouteNames.CONFIRMATION,
+ bundleOf(NavArgs.ORDER_ID to orderId),
+ )
+
+ data class PromoDialog(val promoId: String, val scenario: Nav2Scenario) :
+ Nav2Destination(
+ R.id.nav2_promo_dialog,
+ RouteNames.PROMO_DIALOG,
+ bundleOf(NavArgs.PROMO_ID to promoId, NavArgs.SCENARIO to scenario.name),
+ )
+
+ data class ShareSheet(val productId: String, val scenario: Nav2Scenario) :
+ Nav2Destination(
+ R.id.nav2_share_sheet,
+ RouteNames.SHARE_SHEET,
+ bundleOf(NavArgs.PRODUCT_ID to productId, NavArgs.SCENARIO to scenario.name),
+ )
+}
+
+internal enum class Nav2Scenario(val label: String, val showTab: Boolean = true) {
+ LANDING(RouteNames.LANDING, showTab = false),
+ COMPOSE("Compose"),
+ FRAGMENTS("Fragments"),
+ DEEP_LINK("Deep Link (Fragments)"),
+ CUSTOM("Custom"),
+ PERFORMANCE("Performance"),
+}
+
+internal fun Nav2Destination.routeSpec(): RouteSpec = RouteSpecs.get(routeName)
+
+internal fun Nav2Destination.displayRoute(): String = routeSpec().displayRoute(arguments)
+
+internal fun MutableList.resetTo(destination: Nav2Destination) {
+ clear()
+ add(destination)
+}
+
+internal fun Nav2Destination.matches(destination: NavDestination, arguments: Bundle?): Boolean =
+ id == destination.id && argumentsMatch(arguments)
+
+private fun Nav2Destination.argumentsMatch(arguments: Bundle?): Boolean =
+ when (this) {
+ Nav2Destination.Home,
+ Nav2Destination.Landing,
+ Nav2Destination.ProductList,
+ Nav2Destination.DeepLink -> true
+ is Nav2Destination.ProductDetail ->
+ arguments?.getString(NavArgs.PRODUCT_ID) == productId &&
+ arguments.getString(NavArgs.SOURCE) == source &&
+ arguments.getString(NavArgs.CAMPAIGN).orEmpty() == campaign
+ is Nav2Destination.Checkout -> arguments?.getString(NavArgs.PRODUCT_ID) == productId
+ is Nav2Destination.Confirmation -> arguments?.getString(NavArgs.ORDER_ID) == orderId
+ is Nav2Destination.PromoDialog -> arguments?.getString(NavArgs.PROMO_ID) == promoId
+ is Nav2Destination.ShareSheet -> arguments?.getString(NavArgs.PRODUCT_ID) == productId
+ }
+
+internal fun NavDestination.toNav2Destination(arguments: Bundle?): Nav2Destination? =
+ when (id) {
+ R.id.nav2_landing -> Nav2Destination.Landing
+ R.id.nav2_home -> Nav2Destination.Home
+ R.id.nav2_product_list -> Nav2Destination.ProductList
+ R.id.nav2_deep_link -> Nav2Destination.DeepLink
+ R.id.nav2_product_detail ->
+ Nav2Destination.ProductDetail(
+ productId = arguments?.getString(NavArgs.PRODUCT_ID).orEmpty(),
+ source = arguments?.getString(NavArgs.SOURCE).orEmpty(),
+ campaign = arguments?.getString(NavArgs.CAMPAIGN).orEmpty(),
+ )
+ R.id.nav2_checkout ->
+ Nav2Destination.Checkout(arguments?.getString(NavArgs.PRODUCT_ID).orEmpty())
+ R.id.nav2_confirmation ->
+ Nav2Destination.Confirmation(arguments?.getString(NavArgs.ORDER_ID).orEmpty())
+ R.id.nav2_promo_dialog ->
+ Nav2Destination.PromoDialog(
+ promoId = arguments?.getString(NavArgs.PROMO_ID).orEmpty(),
+ scenario =
+ arguments?.getString(NavArgs.SCENARIO).orEmpty().toNav2Scenario()
+ ?: Nav2Scenario.FRAGMENTS,
+ )
+ R.id.nav2_share_sheet ->
+ Nav2Destination.ShareSheet(
+ productId = arguments?.getString(NavArgs.PRODUCT_ID).orEmpty(),
+ scenario =
+ arguments?.getString(NavArgs.SCENARIO).orEmpty().toNav2Scenario()
+ ?: Nav2Scenario.FRAGMENTS,
+ )
+ else -> null
+ }
+
+internal fun NavDestination.routeName(): String = routeNameOrNull() ?: RouteNames.HOME
+
+private fun NavDestination.routeNameOrNull(): String? = route ?: label?.toString()?.replace(" ", "")
+
+private fun String.toNav2Scenario(): Nav2Scenario? =
+ Nav2Scenario.entries.firstOrNull { scenario -> scenario.name == this }
+
+internal fun recordManualChildSpan(routeName: String) {
+ val span =
+ Sentry.getSpan()
+ ?.startChild(
+ "test.navigation.manual_span",
+ "Nav2 /$routeName manual span",
+ )
+ span?.setData("sample.manual_span", true)
+ span?.finish()
+}
+
+internal fun tagCurrentNav2Scenario(scenario: Nav2Scenario) {
+ Sentry.getSpan()?.setTag(NAV2_SCENARIO_TAG, scenario.label)
+}
+
+internal fun SentryTransaction.nav2ScenarioLabel(): String =
+ getTag(NAV2_SCENARIO_TAG) ?: UNKNOWN_NAV2_SCENARIO_LABEL
+
+internal suspend fun recordSimulatedBackgroundSpan(routeName: String) {
+ val parentSpan = Sentry.getSpan()
+ suspendCancellableCoroutine { continuation ->
+ val worker = Thread {
+ val span =
+ parentSpan?.startChild(
+ "test.navigation.background_work",
+ "Nav2 /$routeName background work",
+ )
+ span?.setData("sample.background_work", true)
+ try {
+ Thread.sleep(BACKGROUND_WORK_MILLIS)
+ } catch (e: InterruptedException) {
+ Thread.currentThread().interrupt()
+ } finally {
+ span?.finish()
+ if (continuation.isActive) {
+ continuation.resume(Unit)
+ }
+ }
+ }
+ continuation.invokeOnCancellation { worker.interrupt() }
+ worker.start()
+ }
+}
+
+internal fun tagNav2SampleAction(action: String, route: String) {
+ val span = Sentry.getSpan() ?: return
+ span.setTag("sample_action", "nav2_$action")
+ span.setTag("sample_nav2_route", route)
+}
+
+internal fun MutableList.popTrackedBackStack(popBackStack: () -> Boolean): Boolean {
+ val popped = popBackStack()
+ if (popped && size > 1) {
+ removeAt(lastIndex)
+ }
+ return popped
+}
+
+internal const val SENTRY_FLUSH_TIMEOUT_MILLIS = 5000L
+internal const val BACKGROUND_WORK_MILLIS = 1000L
+internal const val NAV2_SCENARIO_TAG = "sample_nav2_scenario"
+internal const val UNKNOWN_NAV2_SCENARIO_LABEL = "Unknown"
+
+internal const val MATCH_PARENT = ViewGroup.LayoutParams.MATCH_PARENT
+internal const val WRAP_CONTENT = ViewGroup.LayoutParams.WRAP_CONTENT
diff --git a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/Nav2SampleConfig.kt b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/nav2/Nav2SampleConfigAdapter.kt
similarity index 57%
rename from sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/Nav2SampleConfig.kt
rename to sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/nav2/Nav2SampleConfigAdapter.kt
index 064deeb0895..13b97107e3f 100644
--- a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/Nav2SampleConfig.kt
+++ b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/nav2/Nav2SampleConfigAdapter.kt
@@ -1,70 +1,19 @@
-package io.sentry.samples.android.navigation
+package io.sentry.samples.android.navigation.nav2
import android.content.Context
import android.content.Intent
-import io.sentry.Sentry
+import android.os.Bundle
+import io.sentry.samples.android.navigation.common.NavigationSampleConfig
+import io.sentry.samples.android.navigation.common.NavigationSampleConfigSnapshot
+import io.sentry.samples.android.navigation.common.currentNavigationSampleConfigSnapshot
-internal data class Nav2SampleConfig(
- val enableNavigationTransactions: Boolean = true,
- val enableNavigationBreadcrumbs: Boolean = true,
- val enableScreenTracking: Boolean = true,
- val enableActivityUiLoadTransaction: Boolean = false,
- val enableUserInteractionTransactions: Boolean = false,
- val enableUserInteractionBreadcrumbs: Boolean = false,
-)
-
-internal val Nav2SampleConfig.hasOnlyActivityUiLoadTransactions: Boolean
- get() =
- enableActivityUiLoadTransaction &&
- !enableNavigationTransactions &&
- !enableUserInteractionTransactions
-
-internal data class Nav2SampleConfigSnapshot(
- val enableScreenTracking: Boolean,
- val enableUserInteractionTransactions: Boolean,
- val enableUserInteractionBreadcrumbs: Boolean,
-)
-
-internal fun Nav2SampleConfig.applyToCurrentOptions() {
- applyNav2SampleOptions(
- enableScreenTracking = enableScreenTracking,
- enableUserInteractionTransactions = enableUserInteractionTransactions,
- enableUserInteractionBreadcrumbs = enableUserInteractionBreadcrumbs,
- )
-}
-
-internal fun Nav2SampleConfigSnapshot.applyToCurrentOptions() {
- applyNav2SampleOptions(
- enableScreenTracking = enableScreenTracking,
- enableUserInteractionTransactions = enableUserInteractionTransactions,
- enableUserInteractionBreadcrumbs = enableUserInteractionBreadcrumbs,
- )
-}
-
-private fun applyNav2SampleOptions(
- enableScreenTracking: Boolean,
- enableUserInteractionTransactions: Boolean,
- enableUserInteractionBreadcrumbs: Boolean,
-) {
- val options = Sentry.getCurrentScopes().options
- options.setEnableScreenTracking(enableScreenTracking)
- options.setEnableUserInteractionTracing(enableUserInteractionTransactions)
- options.setEnableUserInteractionBreadcrumbs(enableUserInteractionBreadcrumbs)
-}
-
-internal fun currentNav2SampleConfigSnapshot(): Nav2SampleConfigSnapshot {
- val options = Sentry.getCurrentScopes().options
- return Nav2SampleConfigSnapshot(
- enableScreenTracking = options.isEnableScreenTracking,
- enableUserInteractionTransactions = options.isEnableUserInteractionTracing,
- enableUserInteractionBreadcrumbs = options.isEnableUserInteractionBreadcrumbs,
- )
-}
+internal fun currentNav2SampleConfigSnapshot(): NavigationSampleConfigSnapshot =
+ currentNavigationSampleConfigSnapshot()
internal fun Intent.previousNav2SampleConfigSnapshot(
- fallback: Nav2SampleConfigSnapshot
-): Nav2SampleConfigSnapshot =
- Nav2SampleConfigSnapshot(
+ fallback: NavigationSampleConfigSnapshot
+): NavigationSampleConfigSnapshot =
+ NavigationSampleConfigSnapshot(
enableScreenTracking =
getBooleanExtra(EXTRA_PREVIOUS_ENABLE_SCREEN_TRACKING, fallback.enableScreenTracking),
enableUserInteractionTransactions =
@@ -79,8 +28,8 @@ internal fun Intent.previousNav2SampleConfigSnapshot(
),
)
-internal fun Intent.nav2SampleConfig(): Nav2SampleConfig =
- Nav2SampleConfig(
+internal fun Intent.nav2SampleConfig(): NavigationSampleConfig =
+ NavigationSampleConfig(
enableNavigationTransactions = getBooleanExtra(EXTRA_ENABLE_NAVIGATION_TRANSACTIONS, true),
enableNavigationBreadcrumbs = getBooleanExtra(EXTRA_ENABLE_NAVIGATION_BREADCRUMBS, true),
enableScreenTracking = getBooleanExtra(EXTRA_ENABLE_SCREEN_TRACKING, true),
@@ -93,8 +42,8 @@ internal fun Intent.nav2SampleConfig(): Nav2SampleConfig =
)
internal fun Context.nav2LaunchIntent(
- configuration: Nav2SampleConfig,
- previousOptions: Nav2SampleConfigSnapshot,
+ configuration: NavigationSampleConfig,
+ previousOptions: NavigationSampleConfigSnapshot,
): Intent =
Intent(this, Nav2Activity::class.java)
.putExtra(EXTRA_ENABLE_NAVIGATION_TRANSACTIONS, configuration.enableNavigationTransactions)
@@ -122,6 +71,35 @@ internal fun Context.nav2LaunchIntent(
previousOptions.enableUserInteractionBreadcrumbs,
)
+internal fun Bundle.putNav2SampleConfiguration(configuration: NavigationSampleConfig) {
+ putBoolean(STATE_ENABLE_NAVIGATION_TRANSACTIONS, configuration.enableNavigationTransactions)
+ putBoolean(STATE_ENABLE_NAVIGATION_BREADCRUMBS, configuration.enableNavigationBreadcrumbs)
+ putBoolean(STATE_ENABLE_SCREEN_TRACKING, configuration.enableScreenTracking)
+ putBoolean(
+ STATE_ENABLE_ACTIVITY_UI_LOAD_TRANSACTION,
+ configuration.enableActivityUiLoadTransaction,
+ )
+ putBoolean(
+ STATE_ENABLE_USER_INTERACTION_TRANSACTIONS,
+ configuration.enableUserInteractionTransactions,
+ )
+ putBoolean(
+ STATE_ENABLE_USER_INTERACTION_BREADCRUMBS,
+ configuration.enableUserInteractionBreadcrumbs,
+ )
+}
+
+internal fun Bundle.nav2SampleConfiguration(): NavigationSampleConfig =
+ NavigationSampleConfig(
+ enableNavigationTransactions = getBoolean(STATE_ENABLE_NAVIGATION_TRANSACTIONS, true),
+ enableNavigationBreadcrumbs = getBoolean(STATE_ENABLE_NAVIGATION_BREADCRUMBS, true),
+ enableScreenTracking = getBoolean(STATE_ENABLE_SCREEN_TRACKING, true),
+ enableActivityUiLoadTransaction = getBoolean(STATE_ENABLE_ACTIVITY_UI_LOAD_TRANSACTION, false),
+ enableUserInteractionTransactions =
+ getBoolean(STATE_ENABLE_USER_INTERACTION_TRANSACTIONS, false),
+ enableUserInteractionBreadcrumbs = getBoolean(STATE_ENABLE_USER_INTERACTION_BREADCRUMBS, false),
+ )
+
private const val EXTRA_ENABLE_NAVIGATION_TRANSACTIONS =
"io.sentry.samples.android.navigation.enable_navigation_transactions"
private const val EXTRA_ENABLE_NAVIGATION_BREADCRUMBS =
@@ -140,3 +118,10 @@ private const val EXTRA_PREVIOUS_ENABLE_USER_INTERACTION_TRANSACTIONS =
"io.sentry.samples.android.navigation.previous_enable_user_interaction_transactions"
private const val EXTRA_PREVIOUS_ENABLE_USER_INTERACTION_BREADCRUMBS =
"io.sentry.samples.android.navigation.previous_enable_user_interaction_breadcrumbs"
+private const val STATE_ENABLE_NAVIGATION_TRANSACTIONS = "enable_navigation_transactions"
+private const val STATE_ENABLE_NAVIGATION_BREADCRUMBS = "enable_navigation_breadcrumbs"
+private const val STATE_ENABLE_SCREEN_TRACKING = "enable_screen_tracking"
+private const val STATE_ENABLE_ACTIVITY_UI_LOAD_TRANSACTION = "enable_activity_ui_load_transaction"
+private const val STATE_ENABLE_USER_INTERACTION_TRANSACTIONS =
+ "enable_user_interaction_transactions"
+private const val STATE_ENABLE_USER_INTERACTION_BREADCRUMBS = "enable_user_interaction_breadcrumbs"
diff --git a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/Nav2SampleTheme.kt b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/nav2/Nav2SampleTheme.kt
similarity index 95%
rename from sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/Nav2SampleTheme.kt
rename to sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/nav2/Nav2SampleTheme.kt
index f6d18767343..b174476deb9 100644
--- a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/Nav2SampleTheme.kt
+++ b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/nav2/Nav2SampleTheme.kt
@@ -1,4 +1,4 @@
-package io.sentry.samples.android.navigation
+package io.sentry.samples.android.navigation.nav2
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
diff --git a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/nav2/Nav2SetupActivity.kt b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/nav2/Nav2SetupActivity.kt
new file mode 100644
index 00000000000..d982786d49c
--- /dev/null
+++ b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/nav2/Nav2SetupActivity.kt
@@ -0,0 +1,44 @@
+package io.sentry.samples.android.navigation.nav2
+
+import android.os.Bundle
+import androidx.activity.compose.setContent
+import androidx.appcompat.app.AppCompatActivity
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import io.sentry.samples.android.navigation.common.NavigationSampleConfig
+import io.sentry.samples.android.navigation.common.NavigationSetupScreen
+import io.sentry.samples.android.navigation.common.applyToCurrentOptions
+
+/** Activity for configuring the developer's experience in the [Nav2Activity]. */
+class Nav2SetupActivity : AppCompatActivity() {
+
+ private var configuration by mutableStateOf(NavigationSampleConfig())
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ configuration = savedInstanceState?.nav2SampleConfiguration() ?: configuration
+ setContent {
+ Nav2SampleTheme {
+ NavigationSetupScreen(
+ navName = "Nav2",
+ navVersion = "2",
+ configuration = configuration,
+ onConfigurationChanged = { updatedConfiguration ->
+ configuration = updatedConfiguration
+ },
+ onLaunch = {
+ val previousOptions = currentNav2SampleConfigSnapshot()
+ configuration.applyToCurrentOptions()
+ startActivity(nav2LaunchIntent(configuration, previousOptions))
+ },
+ )
+ }
+ }
+ }
+
+ override fun onSaveInstanceState(outState: Bundle) {
+ super.onSaveInstanceState(outState)
+ outState.putNav2SampleConfiguration(configuration)
+ }
+}
diff --git a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/Nav2TopBar.kt b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/nav2/Nav2TopBar.kt
similarity index 89%
rename from sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/Nav2TopBar.kt
rename to sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/nav2/Nav2TopBar.kt
index c21cebf6b39..6cacd2959ab 100644
--- a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/Nav2TopBar.kt
+++ b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/nav2/Nav2TopBar.kt
@@ -1,4 +1,4 @@
-package io.sentry.samples.android.navigation
+package io.sentry.samples.android.navigation.nav2
import android.content.Context
import android.graphics.Typeface
@@ -19,6 +19,9 @@ import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.core.view.setPadding
import io.sentry.samples.android.R
+import io.sentry.samples.android.navigation.common.RouteNames
+import io.sentry.samples.android.navigation.common.RouteWorkOption
+import io.sentry.samples.android.navigation.common.themeColor
/**
* A top bar consisting of nav info above tabs for selecting among a variety of [Nav2Scenario]s.
@@ -37,28 +40,33 @@ internal class Nav2TopBar(
mutableMapOf(
Nav2Scenario.LANDING to
Nav2TopBarState(
- currentRoute = "/${Nav2RouteNames.LANDING}",
- backStack = "/${Nav2RouteNames.LANDING}",
+ currentRoute = "/${RouteNames.LANDING}",
+ backStack = "/${RouteNames.LANDING}",
),
Nav2Scenario.COMPOSE to
Nav2TopBarState(
- currentRoute = "/${Nav2RouteNames.HOME}",
- backStack = "/${Nav2RouteNames.HOME}",
+ currentRoute = "/${RouteNames.HOME}",
+ backStack = "/${RouteNames.HOME}",
),
Nav2Scenario.FRAGMENTS to
Nav2TopBarState(
- currentRoute = "/${Nav2RouteNames.HOME}",
- backStack = "/${Nav2RouteNames.HOME}",
+ currentRoute = "/${RouteNames.HOME}",
+ backStack = "/${RouteNames.HOME}",
),
Nav2Scenario.DEEP_LINK to
Nav2TopBarState(
- currentRoute = "/${Nav2RouteNames.DEEP_LINK}",
- backStack = "/${Nav2RouteNames.DEEP_LINK}",
+ currentRoute = "/${RouteNames.DEEP_LINK}",
+ backStack = "/${RouteNames.DEEP_LINK}",
+ ),
+ Nav2Scenario.CUSTOM to
+ Nav2TopBarState(
+ currentRoute = "/${RouteNames.CUSTOM}",
+ backStack = "/${RouteNames.CUSTOM}",
),
Nav2Scenario.PERFORMANCE to
Nav2TopBarState(
- currentRoute = "/${Nav2RouteNames.HOME}",
- backStack = "/${Nav2RouteNames.HOME}",
+ currentRoute = "/${RouteNames.HOME}",
+ backStack = "/${RouteNames.HOME}",
),
)
private val tabViews = mutableMapOf()
@@ -183,6 +191,7 @@ internal class Nav2TopBar(
Nav2Scenario.COMPOSE -> R.id.nav2_tab_compose
Nav2Scenario.FRAGMENTS -> R.id.nav2_tab_fragments
Nav2Scenario.DEEP_LINK -> R.id.nav2_tab_deep_link
+ Nav2Scenario.CUSTOM -> R.id.nav2_tab_custom
Nav2Scenario.PERFORMANCE -> R.id.nav2_tab_performance
}
diff --git a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/Nav2TransactionHistory.kt b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/nav2/Nav2TransactionHistory.kt
similarity index 99%
rename from sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/Nav2TransactionHistory.kt
rename to sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/nav2/Nav2TransactionHistory.kt
index 1a911a35285..a846e51b8e9 100644
--- a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/Nav2TransactionHistory.kt
+++ b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/nav2/Nav2TransactionHistory.kt
@@ -1,4 +1,4 @@
-package io.sentry.samples.android.navigation
+package io.sentry.samples.android.navigation.nav2
import android.os.Handler
import android.os.Looper
diff --git a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/Nav2TransactionHistorySheet.kt b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/nav2/Nav2TransactionHistorySheet.kt
similarity index 99%
rename from sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/Nav2TransactionHistorySheet.kt
rename to sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/nav2/Nav2TransactionHistorySheet.kt
index 657b52dd31f..878f7b5f6b5 100644
--- a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/Nav2TransactionHistorySheet.kt
+++ b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/navigation/nav2/Nav2TransactionHistorySheet.kt
@@ -1,4 +1,4 @@
-package io.sentry.samples.android.navigation
+package io.sentry.samples.android.navigation.nav2
import androidx.compose.foundation.background
import androidx.compose.foundation.horizontalScroll
diff --git a/sentry-samples/sentry-samples-android/src/main/res/navigation/nav2_sample.xml b/sentry-samples/sentry-samples-android/src/main/res/navigation/nav2_sample.xml
index 142f7e97a46..8470d68c18c 100644
--- a/sentry-samples/sentry-samples-android/src/main/res/navigation/nav2_sample.xml
+++ b/sentry-samples/sentry-samples-android/src/main/res/navigation/nav2_sample.xml
@@ -8,7 +8,7 @@
+