[1.475.*] Pre-release merge (#1068)

This commit is contained in:
tramline-github[bot]
2026-02-10 14:30:10 +00:00
committed by GitHub
9 changed files with 251 additions and 14 deletions
@@ -32,6 +32,9 @@ runs:
lfs: true
persist-credentials: ${{ inputs.persist-credentials }}
- name: Validate Gradle wrapper
uses: gradle/actions/wrapper-validation@f29f5a9d7b09a7c6b29859002d29d24e1674c884 # v5
- name: Set up JDK
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with:
@@ -0,0 +1,241 @@
/** Taken from https://github.com/getsentry/sentry-java/pull/5023 */
@file:Suppress("UnstableApiUsage") // Sentry internal APIs are used by this module
package dev.msfjarvis.claw.android.ui.navigation
import android.annotation.SuppressLint
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.snapshotFlow
import io.sentry.Breadcrumb
import io.sentry.BuildConfig
import io.sentry.Hint
import io.sentry.IScopes
import io.sentry.ITransaction
import io.sentry.ScopesAdapter
import io.sentry.SentryIntegrationPackageStorage
import io.sentry.SentryLevel
import io.sentry.SentryOptions
import io.sentry.SpanStatus
import io.sentry.TransactionContext
import io.sentry.TransactionOptions
import io.sentry.TypeCheckHint
import io.sentry.protocol.TransactionNameSource
import io.sentry.util.IntegrationUtils.addIntegrationToSdkVersion
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
private const val TRACE_ORIGIN = "auto.navigation.navigation3"
private const val NAVIGATION_OP = "navigation"
/** Holder for the active navigation transaction in a composition scope. */
internal class NavigationTransactionHolder {
internal var activeTransaction: ITransaction? = null
}
/**
* A Composable that observes a back stack [List] and captures a [Breadcrumb] and starts an
* [ITransaction] for each navigation event, sending them to Sentry.
*
* This integration is designed for Android Navigation 3 which uses a back stack-based approach with
* Compose state observation instead of traditional listeners.
*
* Each invocation of this composable maintains independent transaction state, allowing multiple
* navigation instances (e.g., in split-screen or multi-pane layouts) to coexist without
* interference.
*
* @param T The type of keys in the back stack
* @param backStack The current back stack to observe for navigation changes
* @param enableNavigationBreadcrumbs Whether the integration should capture breadcrumbs for
* navigation events.
* @param enableNavigationTracing Whether the integration should start a new idle [ITransaction]
* with [SentryOptions.idleTimeout] for navigation events.
* @param keyToRoute A function to extract a route name from a back stack key. Defaults to
* [Any.toString].
* @param scopes The [IScopes] instance to use for capturing events. Defaults to the singleton
* instance.
*/
@Composable
fun <T> SentryNavigation3Traced(
@SuppressLint("ComposeUnstableCollections") backStack: List<T>,
enableNavigationBreadcrumbs: Boolean = true,
enableNavigationTracing: Boolean = true,
keyToRoute: (T) -> String? = { it.toString() },
scopes: IScopes = ScopesAdapter.getInstance(),
) {
val backStackSnapshot by rememberUpdatedState(backStack)
val enableBreadcrumbsSnapshot by rememberUpdatedState(enableNavigationBreadcrumbs)
val enableTracingSnapshot by rememberUpdatedState(enableNavigationTracing)
val keyToRouteSnapshot by rememberUpdatedState(keyToRoute)
val scopesSnapshot by rememberUpdatedState(scopes)
val transactionHolder = remember { NavigationTransactionHolder() }
DisposableEffect(Unit) {
addIntegrationToSdkVersion("Navigation3")
val observer =
SentryBackStackObserver(
enableNavigationBreadcrumbs = enableBreadcrumbsSnapshot,
enableNavigationTracing = enableTracingSnapshot,
keyToRoute = keyToRouteSnapshot,
scopes = scopesSnapshot,
transactionHolder = transactionHolder,
)
@SuppressLint("RawDispatchersUse")
val scope =
kotlinx.coroutines.CoroutineScope(
kotlinx.coroutines.Dispatchers.Main + kotlinx.coroutines.SupervisorJob()
)
@SuppressLint("DenyListedApi")
val job =
snapshotFlow { backStackSnapshot }
.drop(1) // Skip initial state
.onEach { currentStack ->
val currentKey = currentStack.lastOrNull()
val previousKey = observer.previousKey
if (currentKey != null && currentKey != previousKey) {
observer.handleNavigation(currentKey, currentStack)
observer.previousKey = currentKey
}
}
.launchIn(scope)
onDispose { job.cancel() }
}
}
/** Internal observer that monitors back stack changes and creates Sentry events. */
internal class SentryBackStackObserver<T>(
private val enableNavigationBreadcrumbs: Boolean,
private val enableNavigationTracing: Boolean,
private val keyToRoute: (T) -> String?,
private val scopes: IScopes,
private val transactionHolder: NavigationTransactionHolder,
) {
internal var previousKey: T? = null
private val isPerformanceEnabled: Boolean
get() = scopes.options.isTracingEnabled && enableNavigationTracing
init {
SentryIntegrationPackageStorage.getInstance()
.addPackage("maven:io.sentry:sentry-android-navigation3", BuildConfig.VERSION_NAME)
}
internal fun handleNavigation(currentKey: T, currentStack: List<T>) {
val currentRoute = keyToRoute(currentKey)
if (currentRoute != null) {
addBreadcrumb(currentRoute, currentStack)
if (scopes.options.isEnableScreenTracking) {
scopes.configureScope { it.screen = currentRoute }
}
startTracing(currentRoute, currentStack)
}
}
private fun addBreadcrumb(toRoute: String, currentStack: List<T>) {
if (!enableNavigationBreadcrumbs) {
return
}
val breadcrumb =
Breadcrumb().apply {
type = NAVIGATION_OP
category = NAVIGATION_OP
val fromKey = previousKey
if (fromKey != null) {
val fromRoute = keyToRoute(fromKey)
if (fromRoute != null) {
data["from"] = fromRoute
}
}
data["to"] = toRoute
// Capture back stack keys as a list
val backStackKeys = currentStack.mapNotNull { keyToRoute(it) }
if (backStackKeys.isNotEmpty()) {
data["back_stack"] = backStackKeys
}
level = SentryLevel.INFO
}
val hint = Hint()
hint.set(TypeCheckHint.ANDROID_NAV_DESTINATION, toRoute)
scopes.addBreadcrumb(breadcrumb, hint)
}
private fun startTracing(routeName: String, currentStack: List<T>) {
if (!isPerformanceEnabled) {
io.sentry.util.TracingUtils.startNewTrace(scopes)
return
}
// Finish previous transaction in this navigation scope
if (transactionHolder.activeTransaction != null) {
stopTracing()
}
val transactionOptions =
TransactionOptions().also {
it.isWaitForChildren = true
it.idleTimeout = scopes.options.idleTimeout
// Set deadline timeout based on configured option
val deadlineTimeoutMillis = scopes.options.deadlineTimeout
// No deadline when zero or negative value is set
it.deadlineTimeout = if (deadlineTimeoutMillis <= 0) null else deadlineTimeoutMillis
it.isTrimEnd = true
}
val transaction =
scopes.startTransaction(
TransactionContext(routeName, TransactionNameSource.ROUTE, NAVIGATION_OP),
transactionOptions,
)
transaction.spanContext.origin = TRACE_ORIGIN
// Capture back stack keys as data
val backStackKeys = currentStack.mapNotNull { keyToRoute(it) }
if (backStackKeys.isNotEmpty()) {
transaction.setData("back_stack", backStackKeys)
}
scopes.configureScope { scope ->
scope.withTransaction { tx ->
if (tx == null) {
scope.transaction = transaction
}
}
}
transactionHolder.activeTransaction = transaction
}
private fun stopTracing() {
val status = transactionHolder.activeTransaction?.status ?: SpanStatus.OK
transactionHolder.activeTransaction?.finish(status)
// clear transaction from scope so others can bind to it
scopes.configureScope { scope ->
scope.withTransaction { tx ->
if (tx == transactionHolder.activeTransaction) {
scope.clearTransaction()
}
}
}
transactionHolder.activeTransaction = null
}
}
@@ -65,6 +65,7 @@ import dev.msfjarvis.claw.android.ui.navigation.Newest
import dev.msfjarvis.claw.android.ui.navigation.NonStackable
import dev.msfjarvis.claw.android.ui.navigation.Saved
import dev.msfjarvis.claw.android.ui.navigation.Search
import dev.msfjarvis.claw.android.ui.navigation.SentryNavigation3Traced
import dev.msfjarvis.claw.android.ui.navigation.Settings
import dev.msfjarvis.claw.android.ui.navigation.TagFiltering
import dev.msfjarvis.claw.android.ui.navigation.TopLevelDestination
@@ -166,6 +167,7 @@ fun LobstersPostsScreen(
snackbarHost = { SnackbarHost(snackbarHostState) },
modifier = Modifier.semantics { testTagsAsResourceId = true },
) { contentPadding ->
SentryNavigation3Traced(backStack = backStack)
Row {
AnimatedVisibility(visible = navigationType == ClawNavigationType.NAVIGATION_RAIL) {
val currentDestination = backStack.lastOrNull()
@@ -20,7 +20,6 @@ import org.gradle.kotlin.dsl.configure
class SentryPlugin : Plugin<Project> {
override fun apply(project: Project) {
var enableSizeAnalysis = false
val enableSentry = project.providers.gradleProperty(SENTRY_ENABLE_GRADLE_PROPERTY).isPresent
val libs = project.extensions.getByName("libs") as LibrariesForLibs
project.extensions.configure<ApplicationAndroidComponentsExtension> {
@@ -32,9 +31,6 @@ class SentryPlugin : Plugin<Project> {
"sentryEnvironment",
if (isRelease) "production" else "dev",
)
if (!enableSizeAnalysis) {
enableSizeAnalysis = isRelease
}
}
}
if (!enableSentry) return
@@ -71,11 +67,6 @@ class SentryPlugin : Plugin<Project> {
url.set(null as String?)
telemetry.set(false)
telemetryDsn.set(null as String?)
sizeAnalysis {
enabled.set(
enableSizeAnalysis && project.providers.environmentVariable("GITHUB_ACTIONS").isPresent
)
}
vcsInfo {
headSha.set(project.providers.environmentVariable("SENTRY_VCS_HEAD_SHA"))
baseSha.set(project.providers.environmentVariable("SENTRY_VCS_BASE_SHA"))
@@ -26,7 +26,7 @@ class SpotlessPlugin : Plugin<Project> {
kotlin {
ktfmt(KTFMT_VERSION).googleStyle()
target("**/*.kt")
targetExclude("**/build/", "/spotless/")
targetExclude("**/build/", "/spotless/", "**/SentryNavigation3Integration.kt")
licenseHeaderFile(project.file("spotless/license.kt"))
}
kotlinGradle {
+1 -1
View File
@@ -29,7 +29,7 @@ navigation3-material = "1.3.0-alpha07"
paging = "3.4.0"
retrofit = "3.0.0"
runtimeSaveable = "1.11.0-alpha04"
sentry = "8.31.0"
sentry = "8.32.0"
serialization = "1.10.0"
sqldelight = "2.2.1"
sqlite = "2.6.2"
Binary file not shown.
+2 -2
View File
@@ -1,7 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionSha256Sum=b266d5ff6b90eada6dc3b20cb090e3731302e553a27c5d3e4df1f0d76beaff06
distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip
distributionSha256Sum=2341e5f62ce4ce9d1f51395b47428a6d53e972bc0a3f9d2bf2da5a294610c703
distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.0-rc-1-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
Vendored
+1 -1
View File
@@ -57,7 +57,7 @@
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# https://github.com/gradle/gradle/blob/a2da7f311fe4699328dbcef381bc459c2f757e3e/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.