Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/build
15 changes: 15 additions & 0 deletions sentry-android-navigation/api/sentry-android-navigation.api
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
}
}
}

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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="io.sentry.android.navigation"/>
Original file line number Diff line number Diff line change
@@ -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()
Comment thread
romtsn marked this conversation as resolved.
) : NavController.OnDestinationChangedListener {

private var previousDestinationRef: WeakReference<NavDestination>? = null
private var previousArgs: Bundle? = null
Comment thread
romtsn marked this conversation as resolved.

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"
Comment thread
philipphofmann marked this conversation as resolved.

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 }
Comment thread
romtsn marked this conversation as resolved.
arguments?.let { args ->
val toArguments = args.keySet().filter {
it != NavController.KEY_DEEP_LINK_INTENT // there's a lot of unrelated stuff
}.associateWith { args[it] }
if (toArguments.isNotEmpty()) {
data["to_arguments"] = toArguments
}
}

level = INFO
}
val hint = Hint()
hint.set(TypeCheckHint.ANDROID_NAV_DESTINATION, destination)
hub.addBreadcrumb(breadcrumb)
}
}
Original file line number Diff line number Diff line change
@@ -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 {
Comment thread
romtsn marked this conversation as 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()

@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
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/build
12 changes: 12 additions & 0 deletions sentry-compose/api/android/sentry-compose.api
Original file line number Diff line number Diff line change
@@ -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.
Loading