From fa1b8b7a3be4912bd3c344040c6b585f27f4bef7 Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto <5731772+marandaneto@users.noreply.github.com> Date: Thu, 18 Mar 2021 12:01:14 +0100 Subject: [PATCH] Feat: Add OkHttp client application interceptor (#1330) Co-authored-by: Maciej Walkowiak --- CHANGELOG.md | 1 + buildSrc/src/main/java/Config.kt | 4 + .../api/sentry-android-okhttp.api | 15 +++ sentry-android-okhttp/build.gradle.kts | 94 ++++++++++++++ sentry-android-okhttp/proguard-rules.pro | 23 ++++ .../src/main/AndroidManifest.xml | 2 + .../android/okhttp/SentryOkHttpInterceptor.kt | 56 +++++++++ .../okhttp/SentryOkHttpInterceptorTest.kt | 118 ++++++++++++++++++ .../org.mockito.plugins.MockMaker | 1 + .../api/sentry-samples-android.api | 6 +- .../sentry-samples-android/build.gradle.kts | 3 +- .../sentry/samples/android/MainActivity.java | 11 ++ .../samples/android/NetworkInterceptor.kt | 31 ----- .../sentry/samples/android/SecondActivity.kt | 3 +- sentry/api/sentry.api | 3 +- .../src/main/java/io/sentry/Breadcrumb.java | 18 +++ .../src/main/java/io/sentry/SpanStatus.java | 5 +- .../src/test/java/io/sentry/BreadcrumbTest.kt | 21 ++++ .../src/test/java/io/sentry/SpanStatusTest.kt | 5 + settings.gradle.kts | 9 +- 20 files changed, 384 insertions(+), 45 deletions(-) create mode 100644 sentry-android-okhttp/api/sentry-android-okhttp.api create mode 100644 sentry-android-okhttp/build.gradle.kts create mode 100644 sentry-android-okhttp/proguard-rules.pro create mode 100644 sentry-android-okhttp/src/main/AndroidManifest.xml create mode 100644 sentry-android-okhttp/src/main/java/io/sentry/android/okhttp/SentryOkHttpInterceptor.kt create mode 100644 sentry-android-okhttp/src/test/java/io/sentry/android/okhttp/SentryOkHttpInterceptorTest.kt create mode 100644 sentry-android-okhttp/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker delete mode 100644 sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/NetworkInterceptor.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index c10ffc40c3..b6fe5ba3f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ * Feat: Set SDK version on Transactions (#1307) * Fix: Use logger set on SentryOptions in GsonSerializer (#1308) * Fix: Use the bindToScope correctly +* Feat: Add OkHttp client application interceptor (#1330) # 4.3.0 diff --git a/buildSrc/src/main/java/Config.kt b/buildSrc/src/main/java/Config.kt index 20cac1e927..2fc37f5c2a 100644 --- a/buildSrc/src/main/java/Config.kt +++ b/buildSrc/src/main/java/Config.kt @@ -24,6 +24,7 @@ object Config { private val sdkVersion = 30 val minSdkVersion = 14 + val minSdkVersionOkHttp = 21 val minSdkVersionNdk = 16 val targetSdkVersion = sdkVersion val compileSdkVersion = sdkVersion @@ -34,6 +35,8 @@ object Config { object Libs { val appCompat = "androidx.appcompat:appcompat:1.2.0" val timber = "com.jakewharton.timber:timber:4.7.1" + val okhttpBom = "com.squareup.okhttp3:okhttp-bom:4.9.0" + val okhttp = "com.squareup.okhttp3:okhttp" // only bump gson if https://github.com/google/gson/issues/1597 is fixed val gson = "com.google.code.gson:gson:2.8.5" val leakCanary = "com.squareup.leakcanary:leakcanary-android:2.5" @@ -88,6 +91,7 @@ object Config { val mockitoKotlin = "com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0" val mockitoInline = "org.mockito:mockito-inline:3.6.0" val awaitility = "org.awaitility:awaitility-kotlin:4.0.3" + val mockWebserver = "com.squareup.okhttp3:mockwebserver:4.9.0" } object QualityPlugins { diff --git a/sentry-android-okhttp/api/sentry-android-okhttp.api b/sentry-android-okhttp/api/sentry-android-okhttp.api new file mode 100644 index 0000000000..1c0f599635 --- /dev/null +++ b/sentry-android-okhttp/api/sentry-android-okhttp.api @@ -0,0 +1,15 @@ +public final class io/sentry/android/okhttp/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 ()V +} + +public final class io/sentry/android/okhttp/SentryOkHttpInterceptor : okhttp3/Interceptor { + public fun ()V + public fun (Lio/sentry/IHub;)V + public synthetic fun (Lio/sentry/IHub;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun intercept (Lokhttp3/Interceptor$Chain;)Lokhttp3/Response; +} + diff --git a/sentry-android-okhttp/build.gradle.kts b/sentry-android-okhttp/build.gradle.kts new file mode 100644 index 0000000000..c636fb8c4a --- /dev/null +++ b/sentry-android-okhttp/build.gradle.kts @@ -0,0 +1,94 @@ +import io.gitlab.arturbosch.detekt.Detekt +import io.gitlab.arturbosch.detekt.extensions.DetektExtension +import org.jetbrains.kotlin.config.KotlinCompilerVersion + +plugins { + id("com.android.library") + kotlin("android") + jacoco + id(Config.QualityPlugins.gradleVersions) + id(Config.QualityPlugins.detektPlugin) +} + +android { + compileSdkVersion(Config.Android.compileSdkVersion) + + defaultConfig { + targetSdkVersion(Config.Android.targetSdkVersion) + minSdkVersion(Config.Android.minSdkVersionOkHttp) + + versionName = project.version.toString() + versionCode = project.properties[Config.Sentry.buildVersionCodeProp].toString().toInt() + + // for AGP 4.1 + buildConfigField("String", "VERSION_NAME", "\"$versionName\"") + } + + buildTypes { + getByName("debug") + getByName("release") { + consumerProguardFiles("proguard-rules.pro") + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_1_8.toString() + } + + testOptions { + animationsDisabled = true + unitTests.apply { + isReturnDefaultValues = true + isIncludeAndroidResources = true + } + } + + lintOptions { + isWarningsAsErrors = true + isCheckDependencies = true + + // We run a full lint analysis as build part in CI, so skip vital checks for assemble tasks. + isCheckReleaseBuilds = false + } +} + +tasks.withType { + configure { + isIncludeNoLocationClasses = false + } +} + +kotlin { + explicitApi() +} + +dependencies { + api(project(":sentry")) + + implementation(Config.Libs.okhttpBom) + implementation(Config.Libs.okhttp) + + implementation(kotlin(Config.kotlinStdLib, KotlinCompilerVersion.VERSION)) + + // tests + testImplementation(Config.TestLibs.kotlinTestJunit) + testImplementation(Config.TestLibs.androidxJunit) + testImplementation(Config.TestLibs.mockitoKotlin) + testImplementation(Config.TestLibs.mockitoInline) + testImplementation(Config.TestLibs.mockWebserver) +} + +tasks.withType { + // Target version of the generated JVM bytecode. It is used for type resolution. + jvmTarget = JavaVersion.VERSION_1_8.toString() +} + +configure { + failFast = true + buildUponDefaultConfig = true +} diff --git a/sentry-android-okhttp/proguard-rules.pro b/sentry-android-okhttp/proguard-rules.pro new file mode 100644 index 0000000000..2c038252f1 --- /dev/null +++ b/sentry-android-okhttp/proguard-rules.pro @@ -0,0 +1,23 @@ +##---------------Begin: proguard configuration for OkHttp ---------- + +-keep class io.sentry.android.okhttp.** { *; } + +# To ensure that stack traces is unambiguous +# https://developer.android.com/studio/build/shrink-code#decode-stack-trace +-keepattributes LineNumberTable,SourceFile + +# https://square.github.io/okhttp/r8_proguard/ +# JSR 305 annotations are for embedding nullability information. +-dontwarn javax.annotation.** + +# A resource is loaded with a relative path so the package of this class must be preserved. +-keepnames class okhttp3.internal.publicsuffix.PublicSuffixDatabase + +# Animal Sniffer compileOnly dependency to ensure APIs are compatible with older versions of Java. +-dontwarn org.codehaus.mojo.animal_sniffer.* + +# OkHttp platform used only on JVM and when Conscrypt dependency is available. +-dontwarn okhttp3.internal.platform.ConscryptPlatform +-dontwarn org.conscrypt.ConscryptHostnameVerifier + +##---------------End: proguard configuration for OkHttp ---------- diff --git a/sentry-android-okhttp/src/main/AndroidManifest.xml b/sentry-android-okhttp/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..b5f4a22f4a --- /dev/null +++ b/sentry-android-okhttp/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/sentry-android-okhttp/src/main/java/io/sentry/android/okhttp/SentryOkHttpInterceptor.kt b/sentry-android-okhttp/src/main/java/io/sentry/android/okhttp/SentryOkHttpInterceptor.kt new file mode 100644 index 0000000000..fe33aaac19 --- /dev/null +++ b/sentry-android-okhttp/src/main/java/io/sentry/android/okhttp/SentryOkHttpInterceptor.kt @@ -0,0 +1,56 @@ +package io.sentry.android.okhttp + +import io.sentry.Breadcrumb +import io.sentry.HubAdapter +import io.sentry.IHub +import io.sentry.SpanStatus +import java.io.IOException +import okhttp3.Interceptor +import okhttp3.Response + +class SentryOkHttpInterceptor( + private val hub: IHub = HubAdapter.getInstance() +) : Interceptor { + + override fun intercept(chain: Interceptor.Chain): Response { + var request = chain.request() + + val url = request.url.toString() + val method = request.method + + // read transaction from the bound scope + val span = hub.span?.startChild("http.client", "$method $url") + + var response: Response? = null + + var code: Int? = null + try { + span?.toSentryTrace()?.let { + request = request.newBuilder().addHeader(it.name, it.value).build() + } + response = chain.proceed(request) + code = response.code + return response + } catch (e: IOException) { + span?.throwable = e + throw e + } finally { + span?.finish(SpanStatus.fromHttpStatusCode(code, SpanStatus.INTERNAL_ERROR)) + + val breadcrumb = Breadcrumb.http(request.url.toString(), request.method, code) + request.body?.contentLength().ifHasValidLength { + breadcrumb.setData("requestBodySize", it) + } + response?.body?.contentLength().ifHasValidLength { + breadcrumb.setData("responseBodySize", it) + } + hub.addBreadcrumb(breadcrumb) + } + } + + private fun Long?.ifHasValidLength(fn: (Long) -> Unit) { + if (this != null && this != -1L) { + fn.invoke(this) + } + } +} diff --git a/sentry-android-okhttp/src/test/java/io/sentry/android/okhttp/SentryOkHttpInterceptorTest.kt b/sentry-android-okhttp/src/test/java/io/sentry/android/okhttp/SentryOkHttpInterceptorTest.kt new file mode 100644 index 0000000000..35c09393fc --- /dev/null +++ b/sentry-android-okhttp/src/test/java/io/sentry/android/okhttp/SentryOkHttpInterceptorTest.kt @@ -0,0 +1,118 @@ +package io.sentry.android.okhttp + +import com.nhaarman.mockitokotlin2.any +import com.nhaarman.mockitokotlin2.check +import com.nhaarman.mockitokotlin2.mock +import com.nhaarman.mockitokotlin2.verify +import com.nhaarman.mockitokotlin2.whenever +import io.sentry.Breadcrumb +import io.sentry.IHub +import io.sentry.SentryTraceHeader +import io.sentry.SentryTracer +import io.sentry.SpanStatus +import io.sentry.TransactionContext +import java.io.IOException +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull +import kotlin.test.fail +import okhttp3.Interceptor +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.RequestBody.Companion.toRequestBody +import okhttp3.mockwebserver.MockResponse +import okhttp3.mockwebserver.MockWebServer + +class SentryOkHttpInterceptorTest { + + class Fixture { + val hub = mock() + val interceptor = SentryOkHttpInterceptor(hub) + val server = MockWebServer() + val sentryTracer = SentryTracer(TransactionContext("name", "op"), hub) + + fun getSut(isSpanActive: Boolean = true, httpStatusCode: Int = 201, responseBody: String = "success"): OkHttpClient { + if (isSpanActive) { + whenever(hub.span).thenReturn(sentryTracer) + } + server.enqueue(MockResponse().setBody(responseBody).setResponseCode(httpStatusCode)) + server.start() + return OkHttpClient.Builder().addInterceptor(interceptor).build() + } + } + + val fixture = Fixture() + + @Test + fun `when there is an active span, adds sentry trace header to the request`() { + val sut = fixture.getSut() + sut.newCall(Request.Builder().get().url(fixture.server.url("/hello")).build()).execute() + val recorderRequest = fixture.server.takeRequest() + assertNotNull(recorderRequest.headers[SentryTraceHeader.SENTRY_TRACE_HEADER]) + } + + @Test + fun `when there is no active span, does not add sentry trace header to the request`() { + val sut = fixture.getSut(isSpanActive = false) + sut.newCall(Request.Builder().get().url(fixture.server.url("/hello")).build()).execute() + val recorderRequest = fixture.server.takeRequest() + assertNull(recorderRequest.headers[SentryTraceHeader.SENTRY_TRACE_HEADER]) + } + + @Test + fun `does not overwrite response body`() { + val sut = fixture.getSut() + val response = sut.newCall(Request.Builder().get().url(fixture.server.url("/hello")).build()).execute() + assertEquals("success", response.body?.string()) + } + + @Test + fun `creates a span around the request`() { + val sut = fixture.getSut() + val url = fixture.server.url("/hello") + sut.newCall(Request.Builder().get().url(url).build()).execute() + assertEquals(1, fixture.sentryTracer.children.size) + val httpClientSpan = fixture.sentryTracer.children.first() + assertEquals("http.client", httpClientSpan.operation) + assertEquals("GET $url", httpClientSpan.description) + assertEquals(SpanStatus.OK, httpClientSpan.status) + } + + @Test + fun `maps http status code to SpanStatus`() { + val sut = fixture.getSut(httpStatusCode = 400) + val url = fixture.server.url("/hello") + sut.newCall(Request.Builder().get().url(url).build()).execute() + val httpClientSpan = fixture.sentryTracer.children.first() + assertEquals(SpanStatus.INVALID_ARGUMENT, httpClientSpan.status) + } + + @Test + fun `adds breadcrumb when http calls succeeds`() { + val sut = fixture.getSut(responseBody = "response body") + sut.newCall(Request.Builder().post("request-body".toRequestBody("text/plain".toMediaType())).url(fixture.server.url("/hello")).build()).execute() + verify(fixture.hub).addBreadcrumb(check { + assertEquals("http", it.type) + assertEquals(13L, it.data["responseBodySize"]) + assertEquals(12L, it.data["requestBodySize"]) + }) + } + + @Test + fun `adds breadcrumb when http calls results in exception`() { + val chain = mock() + whenever(chain.proceed(any())).thenThrow(IOException()) + whenever(chain.request()).thenReturn(Request.Builder().get().url(fixture.server.url("/hello")).build()) + + try { + fixture.interceptor.intercept(chain) + fail() + } catch (e: IOException) {} + + verify(fixture.hub).addBreadcrumb(check { + assertEquals("http", it.type) + }) + } +} diff --git a/sentry-android-okhttp/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/sentry-android-okhttp/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker new file mode 100644 index 0000000000..1f0955d450 --- /dev/null +++ b/sentry-android-okhttp/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker @@ -0,0 +1 @@ +mock-maker-inline diff --git a/sentry-samples/sentry-samples-android/api/sentry-samples-android.api b/sentry-samples/sentry-samples-android/api/sentry-samples-android.api index 29da099bd4..6be7374fb0 100644 --- a/sentry-samples/sentry-samples-android/api/sentry-samples-android.api +++ b/sentry-samples/sentry-samples-android/api/sentry-samples-android.api @@ -14,6 +14,7 @@ public abstract interface class io/sentry/samples/android/GitHubService { public class io/sentry/samples/android/MainActivity : androidx/appcompat/app/AppCompatActivity { public fun ()V protected fun onCreate (Landroid/os/Bundle;)V + protected fun onResume ()V } public class io/sentry/samples/android/MyApplication : android/app/Application { @@ -27,11 +28,6 @@ public class io/sentry/samples/android/NativeSample { public static fun message ()V } -public final class io/sentry/samples/android/NetworkInterceptor : okhttp3/Interceptor { - public fun ()V - public fun intercept (Lokhttp3/Interceptor$Chain;)Lokhttp3/Response; -} - public final class io/sentry/samples/android/Repo { public fun ()V } diff --git a/sentry-samples/sentry-samples-android/build.gradle.kts b/sentry-samples/sentry-samples-android/build.gradle.kts index c12a912522..d1c3e107ca 100644 --- a/sentry-samples/sentry-samples-android/build.gradle.kts +++ b/sentry-samples/sentry-samples-android/build.gradle.kts @@ -8,7 +8,7 @@ android { defaultConfig { applicationId = "io.sentry.samples.android" - minSdkVersion(Config.Android.minSdkVersionNdk) + minSdkVersion(Config.Android.minSdkVersionOkHttp) targetSdkVersion(Config.Android.targetSdkVersion) versionCode = 2 versionName = "1.1.0" @@ -96,6 +96,7 @@ dependencies { implementation(kotlin(Config.kotlinStdLib, org.jetbrains.kotlin.config.KotlinCompilerVersion.VERSION)) implementation(project(":sentry-android")) + implementation(project(":sentry-android-okhttp")) // how to exclude androidx if release health feature is disabled // implementation(project(":sentry-android")) { diff --git a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MainActivity.java b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MainActivity.java index d920daeb0f..b73530f8e1 100644 --- a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MainActivity.java +++ b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MainActivity.java @@ -4,7 +4,9 @@ import android.os.Bundle; import androidx.appcompat.app.AppCompatActivity; import io.sentry.Attachment; +import io.sentry.ISpan; import io.sentry.Sentry; +import io.sentry.SpanStatus; import io.sentry.UserFeedback; import io.sentry.protocol.SentryId; import io.sentry.protocol.User; @@ -156,4 +158,13 @@ protected void onCreate(Bundle savedInstanceState) { setContentView(binding.getRoot()); } + + @Override + protected void onResume() { + super.onResume(); + final ISpan span = Sentry.getSpan(); + if (span != null) { + span.finish(SpanStatus.OK); + } + } } diff --git a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/NetworkInterceptor.kt b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/NetworkInterceptor.kt deleted file mode 100644 index 5293ae9acc..0000000000 --- a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/NetworkInterceptor.kt +++ /dev/null @@ -1,31 +0,0 @@ -package io.sentry.samples.android - -import io.sentry.Sentry -import io.sentry.SpanStatus -import java.io.IOException -import okhttp3.Interceptor -import okhttp3.Response - -class NetworkInterceptor : Interceptor { - override fun intercept(chain: Interceptor.Chain): Response { - - val request = chain.request() - - // read transaction from the bound scope - val span = Sentry.getSpan()?.startChild("http.client", request.url().toString()) - - val response: Response - var code = 500 - try { - response = chain.proceed(request) - code = response.code() - } catch (e: IOException) { - span?.throwable = e - throw e - } finally { - span?.finish(SpanStatus.fromHttpStatusCode(code, SpanStatus.INTERNAL_ERROR)) - } - - return response - } -} 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 3b6c55f923..3959b0ecf6 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 @@ -6,6 +6,7 @@ import android.view.View import androidx.appcompat.app.AppCompatActivity import io.sentry.Sentry import io.sentry.SpanStatus +import io.sentry.android.okhttp.SentryOkHttpInterceptor import io.sentry.samples.android.databinding.ActivitySecondBinding import okhttp3.OkHttpClient import retrofit2.Call @@ -18,7 +19,7 @@ class SecondActivity : AppCompatActivity() { private lateinit var repos: List - private val client = OkHttpClient.Builder().addInterceptor(NetworkInterceptor()).build() + private val client = OkHttpClient.Builder().addInterceptor(SentryOkHttpInterceptor()).build() private lateinit var binding: ActivitySecondBinding diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 256406805c..f62e0e3314 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -32,6 +32,7 @@ public final class io/sentry/Breadcrumb : io/sentry/IUnknownPropertiesConsumer, public fun getTimestamp ()Ljava/util/Date; public fun getType ()Ljava/lang/String; public static fun http (Ljava/lang/String;Ljava/lang/String;)Lio/sentry/Breadcrumb; + public static fun http (Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;)Lio/sentry/Breadcrumb; public fun removeData (Ljava/lang/String;)V public fun setCategory (Ljava/lang/String;)V public fun setData (Ljava/lang/String;Ljava/lang/Object;)V @@ -1018,7 +1019,7 @@ public final class io/sentry/SpanStatus : java/lang/Enum { public static final field UNKNOWN Lio/sentry/SpanStatus; public static final field UNKNOWN_ERROR Lio/sentry/SpanStatus; public static fun fromHttpStatusCode (I)Lio/sentry/SpanStatus; - public static fun fromHttpStatusCode (ILio/sentry/SpanStatus;)Lio/sentry/SpanStatus; + public static fun fromHttpStatusCode (Ljava/lang/Integer;Lio/sentry/SpanStatus;)Lio/sentry/SpanStatus; public static fun valueOf (Ljava/lang/String;)Lio/sentry/SpanStatus; public static fun values ()[Lio/sentry/SpanStatus; } diff --git a/sentry/src/main/java/io/sentry/Breadcrumb.java b/sentry/src/main/java/io/sentry/Breadcrumb.java index 1e53e73ad0..67653bc87e 100644 --- a/sentry/src/main/java/io/sentry/Breadcrumb.java +++ b/sentry/src/main/java/io/sentry/Breadcrumb.java @@ -59,6 +59,24 @@ public Breadcrumb(final @Nullable Date timestamp) { return breadcrumb; } + /** + * Creates HTTP breadcrumb. + * + * @param url - the request URL + * @param method - the request method + * @param code - the code result. Code can be null when http request did not finish or ended with + * network error + * @return the breadcrumb + */ + public static @NotNull Breadcrumb http( + final @NotNull String url, final @NotNull String method, final @Nullable Integer code) { + final Breadcrumb breadcrumb = http(url, method); + if (code != null) { + breadcrumb.setData("status_code", code); + } + return breadcrumb; + } + /** Breadcrumb ctor */ public Breadcrumb() { this(DateUtils.getCurrentDateTime()); diff --git a/sentry/src/main/java/io/sentry/SpanStatus.java b/sentry/src/main/java/io/sentry/SpanStatus.java index 13e24ec26a..907c0ea3ff 100644 --- a/sentry/src/main/java/io/sentry/SpanStatus.java +++ b/sentry/src/main/java/io/sentry/SpanStatus.java @@ -91,8 +91,9 @@ public enum SpanStatus { * @return span status equivalent of http status code or defaultStatus if not found */ public static @NotNull SpanStatus fromHttpStatusCode( - final int httpStatusCode, final @NotNull SpanStatus defaultStatus) { - final SpanStatus spanStatus = fromHttpStatusCode(httpStatusCode); + final @Nullable Integer httpStatusCode, final @NotNull SpanStatus defaultStatus) { + final SpanStatus spanStatus = + httpStatusCode != null ? fromHttpStatusCode(httpStatusCode) : defaultStatus; return spanStatus != null ? spanStatus : defaultStatus; } diff --git a/sentry/src/test/java/io/sentry/BreadcrumbTest.kt b/sentry/src/test/java/io/sentry/BreadcrumbTest.kt index a1971bb729..877a6a993a 100644 --- a/sentry/src/test/java/io/sentry/BreadcrumbTest.kt +++ b/sentry/src/test/java/io/sentry/BreadcrumbTest.kt @@ -2,6 +2,7 @@ package io.sentry import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertFalse import kotlin.test.assertNotNull import kotlin.test.assertNotSame @@ -107,4 +108,24 @@ class BreadcrumbTest { assertEquals("http", breadcrumb.type) assertEquals("http", breadcrumb.category) } + + @Test + fun `creates HTTP breadcrumb with http status when status code is provided`() { + val breadcrumb = Breadcrumb.http("http://example.com", "POST", 400) + assertEquals("http://example.com", breadcrumb.data["url"]) + assertEquals("POST", breadcrumb.data["method"]) + assertEquals("http", breadcrumb.type) + assertEquals("http", breadcrumb.category) + assertEquals(400, breadcrumb.data["status_code"]) + } + + @Test + fun `creates HTTP breadcrumb without http status when code is null`() { + val breadcrumb = Breadcrumb.http("http://example.com", "POST", null) + assertEquals("http://example.com", breadcrumb.data["url"]) + assertEquals("POST", breadcrumb.data["method"]) + assertEquals("http", breadcrumb.type) + assertEquals("http", breadcrumb.category) + assertFalse(breadcrumb.data.containsKey("status_code")) + } } diff --git a/sentry/src/test/java/io/sentry/SpanStatusTest.kt b/sentry/src/test/java/io/sentry/SpanStatusTest.kt index 80263e5680..37cf76882a 100644 --- a/sentry/src/test/java/io/sentry/SpanStatusTest.kt +++ b/sentry/src/test/java/io/sentry/SpanStatusTest.kt @@ -30,4 +30,9 @@ class SpanStatusTest { fun `returns default value when no SpanStatus matches specific code`() { assertEquals(SpanStatus.UNKNOWN_ERROR, SpanStatus.fromHttpStatusCode(302, SpanStatus.UNKNOWN_ERROR)) } + + @Test + fun `returns default value when http code is null`() { + assertEquals(SpanStatus.UNKNOWN_ERROR, SpanStatus.fromHttpStatusCode(null, SpanStatus.UNKNOWN_ERROR)) + } } diff --git a/settings.gradle.kts b/settings.gradle.kts index 3a819a9880..b9eeab38d9 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -2,10 +2,12 @@ rootProject.name = "sentry-root" rootProject.buildFileName = "build.gradle.kts" include( - "sentry-android", - "sentry-android-ndk", - "sentry-android-core", "sentry", + "sentry-android-core", + "sentry-android-ndk", + "sentry-android", + "sentry-android-timber", + "sentry-android-okhttp", "sentry-test-support", "sentry-log4j2", "sentry-logback", @@ -14,7 +16,6 @@ include( "sentry-apache-http-client-5", "sentry-spring", "sentry-spring-boot-starter", - "sentry-android-timber", "sentry-samples:sentry-samples-android", "sentry-samples:sentry-samples-console", "sentry-samples:sentry-samples-jul",