Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Jetpack Compose Navigation support #2136

Merged
merged 4 commits into from
Jun 29, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions build.gradle.kts
Expand Up @@ -34,6 +34,7 @@ buildscript {
// classpath("io.sentry:sentry-android-gradle-plugin:{version}")

classpath(Config.QualityPlugins.binaryCompatibilityValidatorPlugin)
classpath(Config.BuildPlugins.composeGradlePlugin)
}
}

Expand Down
18 changes: 16 additions & 2 deletions buildSrc/src/main/java/Config.kt
@@ -1,12 +1,14 @@
import java.math.BigDecimal

object Config {
val kotlinVersion = "1.5.31"
val kotlinVersion = "1.6.10"
val kotlinStdLib = "stdlib-jdk8"

val springBootVersion = "2.6.8"
val kotlinCompatibleLanguageVersion = "1.4"

val composeVersion = "1.1.1"

object BuildPlugins {
val androidGradle = "com.android.tools.build:gradle:7.2.0"
val kotlinGradlePlugin = "gradle-plugin"
Expand All @@ -19,14 +21,16 @@ object Config {
val grettyVersion = "4.0.0"
val gradleMavenPublishPlugin = "com.vanniktech:gradle-maven-publish-plugin:0.18.0"
val dokkaPlugin = "org.jetbrains.dokka:dokka-gradle-plugin:$kotlinVersion"
val composeGradlePlugin = "org.jetbrains.compose:compose-gradle-plugin:$composeVersion"
}

object Android {
private val sdkVersion = 31
private val sdkVersion = 32

val minSdkVersion = 14
val minSdkVersionOkHttp = 21
val minSdkVersionNdk = 16
val minSdkVersionCompose = 21
val targetSdkVersion = sdkVersion
val compileSdkVersion = sdkVersion

Expand Down Expand Up @@ -104,6 +108,16 @@ object Config {
val graphQlJava = "com.graphql-java:graphql-java:17.3"

val kotlinReflect = "org.jetbrains.kotlin:kotlin-reflect"
val kotlinStdLib = "org.jetbrains.kotlin:kotlin-stdlib"

private val navigationVersion = "2.4.2"
val navigationRuntime = "androidx.navigation:navigation-runtime:$navigationVersion"
// compose deps
val composeNavigation = "androidx.navigation:navigation-compose:$navigationVersion"
val composeActivity = "androidx.activity:activity-compose:1.4.0"
val composeFoundation = "androidx.compose.foundation:foundation:$composeVersion"
val composeFoundationLayout = "androidx.compose.foundation:foundation-layout:$composeVersion"
val composeMaterial = "androidx.compose.material3:material3:1.0.0-alpha13"
}

object AnnotationProcessors {
Expand Down
1 change: 1 addition & 0 deletions sentry-android-navigation/.gitignore
@@ -0,0 +1 @@
/build
15 changes: 15 additions & 0 deletions sentry-android-navigation/api/sentry-android-navigation.api
@@ -0,0 +1,15 @@
public final class io/sentry/android/navigation/BuildConfig {
public static final field BUILD_TYPE Ljava/lang/String;
public static final field DEBUG Z
public static final field LIBRARY_PACKAGE_NAME Ljava/lang/String;
public static final field VERSION_NAME Ljava/lang/String;
public fun <init> ()V
}

public final class io/sentry/android/navigation/SentryNavigationListener : androidx/navigation/NavController$OnDestinationChangedListener {
public fun <init> ()V
public fun <init> (Lio/sentry/IHub;)V
public synthetic fun <init> (Lio/sentry/IHub;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun onDestinationChanged (Landroidx/navigation/NavController;Landroidx/navigation/NavDestination;Landroid/os/Bundle;)V
}

95 changes: 95 additions & 0 deletions sentry-android-navigation/build.gradle.kts
@@ -0,0 +1,95 @@
import io.gitlab.arturbosch.detekt.Detekt
import io.gitlab.arturbosch.detekt.extensions.DetektExtension

plugins {
id("com.android.library")
kotlin("android")
jacoco
id(Config.QualityPlugins.gradleVersions)
id(Config.QualityPlugins.detektPlugin)
}

android {
compileSdk = Config.Android.compileSdkVersion

defaultConfig {
targetSdk = Config.Android.targetSdkVersion
minSdk = Config.Android.minSdkVersion

// for AGP 4.1
buildConfigField("String", "VERSION_NAME", "\"${project.version}\"")
}

buildTypes {
getByName("debug")
getByName("release") {
consumerProguardFiles("proguard-rules.pro")
}
}

kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8.toString()
kotlinOptions.languageVersion = Config.kotlinCompatibleLanguageVersion
}

testOptions {
animationsDisabled = true
unitTests.apply {
isReturnDefaultValues = true
isIncludeAndroidResources = true
}
}

lint {
warningsAsErrors = true
checkDependencies = true

// We run a full lint analysis as build part in CI, so skip vital checks for assemble tasks.
checkReleaseBuilds = false
}

variantFilter {
if (Config.Android.shouldSkipDebugVariant(buildType.name)) {
ignore = true
}
}
}
romtsn marked this conversation as resolved.
Show resolved Hide resolved

tasks.withType<Test> {
configure<JacocoTaskExtension> {
isIncludeNoLocationClasses = false
}
}

kotlin {
explicitApi()
}

dependencies {
api(projects.sentry)

compileOnly(Config.Libs.navigationRuntime)

// tests
testImplementation(Config.Libs.navigationRuntime)

testImplementation(Config.TestLibs.kotlinTestJunit)
testImplementation(Config.TestLibs.mockitoKotlin)
testImplementation(Config.TestLibs.mockitoInline)

testImplementation(Config.TestLibs.robolectric)
testImplementation(Config.TestLibs.androidxCore)
testImplementation(Config.TestLibs.androidxRunner)
testImplementation(Config.TestLibs.androidxJunit)
testImplementation(Config.TestLibs.androidxCoreKtx)
}

tasks.withType<Detekt> {
// Target version of the generated JVM bytecode. It is used for type resolution.
jvmTarget = JavaVersion.VERSION_1_8.toString()
}

configure<DetektExtension> {
buildUponDefaultConfig = true
allRules = true
}
7 changes: 7 additions & 0 deletions sentry-android-navigation/proguard-rules.pro
@@ -0,0 +1,7 @@
##---------------Begin: proguard configuration for Compose ----------

# To ensure that stack traces is unambiguous
# https://developer.android.com/studio/build/shrink-code#decode-stack-trace
-keepattributes LineNumberTable,SourceFile

##---------------End: proguard configuration for Compose ----------
2 changes: 2 additions & 0 deletions sentry-android-navigation/src/main/AndroidManifest.xml
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="io.sentry.android.navigation"/>
@@ -0,0 +1,64 @@
package io.sentry.android.navigation

import android.os.Bundle
import androidx.navigation.NavController
import androidx.navigation.NavDestination
import io.sentry.Breadcrumb
import io.sentry.Hint
import io.sentry.HubAdapter
import io.sentry.IHub
import io.sentry.SentryLevel.INFO
import io.sentry.TypeCheckHint
import java.lang.ref.WeakReference

class SentryNavigationListener @JvmOverloads constructor(
private val hub: IHub = HubAdapter.getInstance()
) : NavController.OnDestinationChangedListener {

private var previousDestinationRef: WeakReference<NavDestination>? = null
private var previousArgs: Bundle? = null

override fun onDestinationChanged(
controller: NavController,
destination: NavDestination,
arguments: Bundle?
) {
addBreadcrumb(destination, arguments)
previousDestinationRef = WeakReference(destination)
previousArgs = arguments
}

private fun addBreadcrumb(destination: NavDestination, arguments: Bundle?) {
val breadcrumb = Breadcrumb().apply {
type = "navigation"
category = "navigation"

val from = previousDestinationRef?.get()?.route
from?.let { data["from"] = it }
previousArgs?.let { args ->
val fromArguments = args.keySet().filter {
it != NavController.KEY_DEEP_LINK_INTENT // there's a lot of unrelated stuff
}.associateWith { args[it] }
if (fromArguments.isNotEmpty()) {
data["from_arguments"] = fromArguments
}
}

val to = destination.route
to?.let { data["to"] = it }
arguments?.let { args ->
val toArguments = args.keySet().filter {
it != NavController.KEY_DEEP_LINK_INTENT // there's a lot of unrelated stuff
romtsn marked this conversation as resolved.
Show resolved Hide resolved
}.associateWith { args[it] }
if (toArguments.isNotEmpty()) {
data["to_arguments"] = toArguments
}
}
romtsn marked this conversation as resolved.
Show resolved Hide resolved

level = INFO
}
val hint = Hint()
hint.set(TypeCheckHint.ANDROID_NAV_DESTINATION, destination)
hub.addBreadcrumb(breadcrumb)
}
}
@@ -0,0 +1,119 @@
package io.sentry.android.navigation

import androidx.core.os.bundleOf
import androidx.navigation.NavController
import androidx.navigation.NavDestination
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.nhaarman.mockitokotlin2.check
import com.nhaarman.mockitokotlin2.mock
import com.nhaarman.mockitokotlin2.reset
import com.nhaarman.mockitokotlin2.verify
import com.nhaarman.mockitokotlin2.whenever
import io.sentry.Breadcrumb
import io.sentry.IHub
import io.sentry.SentryLevel
import org.junit.runner.RunWith
import org.robolectric.annotation.Config
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNull

@RunWith(AndroidJUnit4::class)
@Config(sdk = [31])
class SentryNavigationListenerTest {
romtsn marked this conversation as resolved.
Show resolved Hide resolved

class Fixture {
val hub = mock<IHub>()
val destination = mock<NavDestination>()
val navController = mock<NavController>()

fun getSut(toRoute: String = "route"): SentryNavigationListener {
whenever(destination.route).thenReturn(toRoute)
return SentryNavigationListener(hub)
}
}

private val fixture = Fixture()
romtsn marked this conversation as resolved.
Show resolved Hide resolved

@Test
fun `onDestinationChanged captures a breadcrumb`() {
val sut = fixture.getSut()

sut.onDestinationChanged(fixture.navController, fixture.destination, null)

verify(fixture.hub).addBreadcrumb(
check<Breadcrumb> {
assertEquals("navigation", it.type)
assertEquals("navigation", it.category)
assertEquals("route", it.data["to"])
assertEquals(SentryLevel.INFO, it.level)
}
)
}

@Test
fun `onDestinationChanged captures a breadcrumb with arguments`() {
val sut = fixture.getSut()

sut.onDestinationChanged(
fixture.navController,
fixture.destination,
bundleOf("arg1" to "foo", "arg2" to "bar")
)

verify(fixture.hub).addBreadcrumb(
check<Breadcrumb> {
assertEquals("route", it.data["to"])
assertEquals(mapOf("arg1" to "foo", "arg2" to "bar"), it.data["to_arguments"])
}
)
}

@Test
fun `onDestinationChanged does not send empty args map`() {
val sut = fixture.getSut()

sut.onDestinationChanged(
fixture.navController,
fixture.destination,
bundleOf()
)

verify(fixture.hub).addBreadcrumb(
check<Breadcrumb> {
assertEquals("route", it.data["to"])
assertNull(it.data["to_arguments"])
}
)
}

@Test
fun `onDestinationChanged captures a breadcrumb with from and to destinations`() {
val sut = fixture.getSut(toRoute = "route_from")

sut.onDestinationChanged(
fixture.navController,
fixture.destination,
bundleOf("from_arg1" to "from_foo")
)
reset(fixture.hub)

val toDestination = mock<NavDestination> {
whenever(mock.route).thenReturn("route_to")
}
sut.onDestinationChanged(
fixture.navController,
toDestination,
bundleOf("to_arg1" to "to_foo")
)
verify(fixture.hub).addBreadcrumb(
check<Breadcrumb> {
assertEquals("route_from", it.data["from"])
assertEquals(mapOf("from_arg1" to "from_foo"), it.data["from_arguments"])

assertEquals("route_to", it.data["to"])
assertEquals(mapOf("to_arg1" to "to_foo"), it.data["to_arguments"])
}
)
}
}
1 change: 1 addition & 0 deletions sentry-compose/.gitignore
@@ -0,0 +1 @@
/build
12 changes: 12 additions & 0 deletions sentry-compose/api/android/sentry-compose.api
@@ -0,0 +1,12 @@
public final class io/sentry/compose/BuildConfig {
public static final field BUILD_TYPE Ljava/lang/String;
public static final field DEBUG Z
public static final field LIBRARY_PACKAGE_NAME Ljava/lang/String;
public static final field VERSION_NAME Ljava/lang/String;
public fun <init> ()V
}

public final class io/sentry/compose/SentryNavigationIntegrationKt {
public static final fun withSentryObservableEffect (Landroidx/navigation/NavHostController;Landroidx/compose/runtime/Composer;I)Landroidx/navigation/NavHostController;
}

Empty file.