[1.475.*] Pre-release merge (#1068)
This commit is contained in:
@@ -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:
|
||||
|
||||
+241
@@ -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 {
|
||||
|
||||
@@ -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"
|
||||
|
||||
Vendored
BIN
Binary file not shown.
+2
-2
@@ -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
|
||||
|
||||
@@ -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/.
|
||||
|
||||
Reference in New Issue
Block a user