From 12cd936650a5eb72e38c2115c1a9f76ca2da0655 Mon Sep 17 00:00:00 2001 From: Lukas Bloder Date: Tue, 21 Jul 2026 10:02:09 +0200 Subject: [PATCH 1/6] add okhttp autoconfiguration for spring boot 4 --- .../api/sentry-spring-boot-4.api | 4 + sentry-spring-boot-4/build.gradle.kts | 4 + .../boot4/SentryOkHttpAutoConfiguration.java | 28 +++ .../SentryOkHttpClientBeanPostProcessor.java | 84 +++++++ ...ot.autoconfigure.AutoConfiguration.imports | 1 + .../SentryOkHttpAutoConfigurationTest.kt | 207 ++++++++++++++++++ 6 files changed, 328 insertions(+) create mode 100644 sentry-spring-boot-4/src/main/java/io/sentry/spring/boot4/SentryOkHttpAutoConfiguration.java create mode 100644 sentry-spring-boot-4/src/main/java/io/sentry/spring/boot4/SentryOkHttpClientBeanPostProcessor.java create mode 100644 sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot4/SentryOkHttpAutoConfigurationTest.kt diff --git a/sentry-spring-boot-4/api/sentry-spring-boot-4.api b/sentry-spring-boot-4/api/sentry-spring-boot-4.api index 4c8be990b85..723ef6abd14 100644 --- a/sentry-spring-boot-4/api/sentry-spring-boot-4.api +++ b/sentry-spring-boot-4/api/sentry-spring-boot-4.api @@ -24,6 +24,10 @@ public class io/sentry/spring/boot4/SentryLogbackInitializer : org/springframewo public fun supportsEventType (Lorg/springframework/core/ResolvableType;)Z } +public class io/sentry/spring/boot4/SentryOkHttpAutoConfiguration { + public fun ()V +} + public class io/sentry/spring/boot4/SentryProfilerAutoConfiguration { public fun ()V } diff --git a/sentry-spring-boot-4/build.gradle.kts b/sentry-spring-boot-4/build.gradle.kts index 2a6634b257f..888585c898e 100644 --- a/sentry-spring-boot-4/build.gradle.kts +++ b/sentry-spring-boot-4/build.gradle.kts @@ -30,6 +30,8 @@ dependencies { api(projects.sentry) api(projects.sentrySpring7) compileOnly(projects.sentryLogback) + compileOnly(projects.sentryOkhttp) + compileOnly(libs.okhttp) compileOnly(projects.sentryApacheHttpClient5) compileOnly(platform(SpringBootPlugin.BOM_COORDINATES)) compileOnly(projects.sentryGraphql) @@ -65,6 +67,7 @@ dependencies { // tests testImplementation(projects.sentryLogback) + testImplementation(projects.sentryOkhttp) testImplementation(projects.sentryApacheHttpClient5) testImplementation(projects.sentryGraphql) testImplementation(projects.sentryGraphql22) @@ -81,6 +84,7 @@ dependencies { testImplementation(platform(SpringBootPlugin.BOM_COORDINATES)) testImplementation(libs.context.propagation) testImplementation(libs.kotlin.test.junit) + testImplementation(libs.google.truth) testImplementation(libs.mockito.kotlin) testImplementation(libs.okhttp) testImplementation(libs.okhttp.mockwebserver) diff --git a/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot4/SentryOkHttpAutoConfiguration.java b/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot4/SentryOkHttpAutoConfiguration.java new file mode 100644 index 00000000000..7fdb73af9bb --- /dev/null +++ b/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot4/SentryOkHttpAutoConfiguration.java @@ -0,0 +1,28 @@ +package io.sentry.spring.boot4; + +import com.jakewharton.nopen.annotation.Open; +import io.sentry.okhttp.SentryOkHttpEventListener; +import io.sentry.okhttp.SentryOkHttpInterceptor; +import okhttp3.OkHttpClient; +import org.jetbrains.annotations.NotNull; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** Auto-configures Sentry instrumentation for Spring-managed {@link OkHttpClient} beans. */ +@Configuration(proxyBeanMethods = false) +@Open +@ConditionalOnClass({ + OkHttpClient.class, + SentryOkHttpInterceptor.class, + SentryOkHttpEventListener.class +}) +@ConditionalOnProperty(name = "sentry.dsn") +public class SentryOkHttpAutoConfiguration { + + @Bean + static @NotNull SentryOkHttpClientBeanPostProcessor sentryOkHttpClientBeanPostProcessor() { + return new SentryOkHttpClientBeanPostProcessor(); + } +} diff --git a/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot4/SentryOkHttpClientBeanPostProcessor.java b/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot4/SentryOkHttpClientBeanPostProcessor.java new file mode 100644 index 00000000000..9b0e465c906 --- /dev/null +++ b/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot4/SentryOkHttpClientBeanPostProcessor.java @@ -0,0 +1,84 @@ +package io.sentry.spring.boot4; + +import io.sentry.ScopesAdapter; +import io.sentry.SentryLevel; +import io.sentry.okhttp.SentryOkHttpEventListener; +import io.sentry.okhttp.SentryOkHttpInterceptor; +import okhttp3.Call; +import okhttp3.EventListener; +import okhttp3.Interceptor; +import okhttp3.OkHttpClient; +import org.jetbrains.annotations.NotNull; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.core.Ordered; +import org.springframework.core.PriorityOrdered; + +final class SentryOkHttpClientBeanPostProcessor implements BeanPostProcessor, PriorityOrdered { + + @Override + public @NotNull Object postProcessAfterInitialization( + final @NotNull Object bean, final @NotNull String beanName) throws BeansException { + if (!(bean instanceof OkHttpClient)) { + return bean; + } + + final @NotNull OkHttpClient client = (OkHttpClient) bean; + if (client.getClass() != OkHttpClient.class) { + ScopesAdapter.getInstance() + .getOptions() + .getLogger() + .log( + SentryLevel.WARNING, + "Sentry OkHttp auto-instrumentation skipped for bean '%s' (%s) because replacing " + + "an OkHttpClient subclass would not preserve its type. Configure Sentry " + + "instrumentation manually for this client.", + beanName, + client.getClass().getName()); + return client; + } + + final boolean addInterceptor = !hasSentryInterceptor(client); + final boolean wrapEventListener = + !(client.eventListenerFactory() instanceof SentryEventListenerFactory); + if (!addInterceptor && !wrapEventListener) { + return client; + } + + final @NotNull OkHttpClient.Builder builder = client.newBuilder(); + if (addInterceptor) { + builder.addInterceptor(new SentryOkHttpInterceptor()); + } + if (wrapEventListener) { + builder.eventListenerFactory(new SentryEventListenerFactory(client.eventListenerFactory())); + } + return builder.build(); + } + + private static boolean hasSentryInterceptor(final @NotNull OkHttpClient client) { + for (final @NotNull Interceptor interceptor : client.interceptors()) { + if (interceptor instanceof SentryOkHttpInterceptor) { + return true; + } + } + return false; + } + + @Override + public int getOrder() { + return Ordered.LOWEST_PRECEDENCE; + } + + private static final class SentryEventListenerFactory implements EventListener.Factory { + private final @NotNull EventListener.Factory delegate; + + private SentryEventListenerFactory(final @NotNull EventListener.Factory delegate) { + this.delegate = delegate; + } + + @Override + public @NotNull EventListener create(final @NotNull Call call) { + return new SentryOkHttpEventListener(ScopesAdapter.getInstance(), delegate); + } + } +} diff --git a/sentry-spring-boot-4/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/sentry-spring-boot-4/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index a108fa2ca10..0fe658c8a18 100644 --- a/sentry-spring-boot-4/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/sentry-spring-boot-4/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -1,4 +1,5 @@ io.sentry.spring.boot4.SentryAutoConfiguration io.sentry.spring.boot4.SentryProfilerAutoConfiguration io.sentry.spring.boot4.SentryLogbackAppenderAutoConfiguration +io.sentry.spring.boot4.SentryOkHttpAutoConfiguration io.sentry.spring.boot4.SentryWebfluxAutoConfiguration diff --git a/sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot4/SentryOkHttpAutoConfigurationTest.kt b/sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot4/SentryOkHttpAutoConfigurationTest.kt new file mode 100644 index 00000000000..536ce6dbe21 --- /dev/null +++ b/sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot4/SentryOkHttpAutoConfigurationTest.kt @@ -0,0 +1,207 @@ +package io.sentry.spring.boot4 + +import com.google.common.truth.Truth.assertThat +import io.sentry.ITransportFactory +import io.sentry.NoOpTransportFactory +import io.sentry.okhttp.SentryOkHttpEventListener +import io.sentry.okhttp.SentryOkHttpInterceptor +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicBoolean +import kotlin.test.Test +import okhttp3.Call +import okhttp3.EventListener +import okhttp3.Interceptor +import okhttp3.OkHttpClient +import okhttp3.Request +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever +import org.springframework.boot.autoconfigure.EnableAutoConfiguration +import org.springframework.boot.test.context.FilteredClassLoader +import org.springframework.boot.test.context.runner.ApplicationContextRunner +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration + +class SentryOkHttpAutoConfigurationTest { + + private val contextRunner = + ApplicationContextRunner() + .withUserConfiguration(TestApplication::class.java, NoOpTransportConfiguration::class.java) + .withPropertyValues( + "sentry.shutdownTimeoutMillis=0", + "sentry.sessionFlushTimeoutMillis=0", + "sentry.flushTimeoutMillis=0", + "sentry.send-modules=false", + "sentry.enable-backpressure-handling=false", + "sentry.enable-spotlight=false", + ) + + @Test + fun `instruments a Spring managed OkHttpClient`() { + contextRunner + .withPropertyValues("sentry.dsn=http://key@localhost/proj") + .withUserConfiguration(OkHttpClientConfiguration::class.java) + .run { context -> + val client = context.getBean(OkHttpClient::class.java) + val existingInterceptor = context.getBean("existingInterceptor", Interceptor::class.java) + + assertThat(client.connectTimeoutMillis).isEqualTo(1234) + assertThat(client.interceptors).contains(existingInterceptor) + assertThat(client.interceptors.filterIsInstance()).hasSize(1) + assertThat(client.interceptors.last()).isInstanceOf(SentryOkHttpInterceptor::class.java) + + val call = mock() + whenever(call.request()).thenReturn(Request.Builder().url("https://example.com").build()) + val listener = client.eventListenerFactory.create(call) + assertThat(listener).isInstanceOf(SentryOkHttpEventListener::class.java) + + listener.callStart(call) + assertThat(context.getBean(RecordingEventListener::class.java).callStarted.get()).isTrue() + listener.callEnd(call) + } + } + + @Test + fun `instruments every Spring managed OkHttpClient`() { + contextRunner + .withPropertyValues("sentry.dsn=http://key@localhost/proj") + .withUserConfiguration(MultipleOkHttpClientsConfiguration::class.java) + .run { context -> + val clients = context.getBeansOfType(OkHttpClient::class.java) + + assertThat(clients).hasSize(2) + clients.values.forEach { client -> + assertThat(client.interceptors.filterIsInstance()).hasSize(1) + assertThat(client.interceptors.last()).isInstanceOf(SentryOkHttpInterceptor::class.java) + } + } + } + + @Test + fun `does not duplicate an existing Sentry interceptor`() { + contextRunner + .withPropertyValues("sentry.dsn=http://key@localhost/proj") + .withUserConfiguration(ManuallyInstrumentedOkHttpClientConfiguration::class.java) + .run { context -> + val client = context.getBean(OkHttpClient::class.java) + + assertThat(client.interceptors.filterIsInstance()).hasSize(1) + assertThat(client.eventListenerFactory.create(mock())) + .isInstanceOf(SentryOkHttpEventListener::class.java) + } + } + + @Test + fun `post processor is idempotent`() { + val processor = SentryOkHttpClientBeanPostProcessor() + val firstResult = + processor.postProcessAfterInitialization(OkHttpClient(), "okHttpClient") as OkHttpClient + val secondResult = + processor.postProcessAfterInitialization(firstResult, "okHttpClient") as OkHttpClient + + assertThat(secondResult).isSameInstanceAs(firstResult) + assertThat(secondResult.interceptors.filterIsInstance()).hasSize(1) + } + + @Test + fun `does not replace unrelated beans`() { + val processor = SentryOkHttpClientBeanPostProcessor() + val bean = Any() + + assertThat(processor.postProcessAfterInitialization(bean, "bean")).isSameInstanceAs(bean) + } + + @Test + fun `does not replace OkHttpClient subclasses`() { + val processor = SentryOkHttpClientBeanPostProcessor() + val client = CustomOkHttpClient() + + assertThat(processor.postProcessAfterInitialization(client, "okHttpClient")) + .isSameInstanceAs(client) + assertThat(client.interceptors).isEmpty() + } + + @Test + fun `does not instrument OkHttpClient without a dsn`() { + contextRunner.withUserConfiguration(OkHttpClientConfiguration::class.java).run { context -> + val client = context.getBean(OkHttpClient::class.java) + + assertThat(client.interceptors.filterIsInstance()).isEmpty() + assertThat(client.eventListenerFactory.create(mock())) + .isNotInstanceOf(SentryOkHttpEventListener::class.java) + } + } + + @Test + fun `does not create a default OkHttpClient`() { + contextRunner.withPropertyValues("sentry.dsn=http://key@localhost/proj").run { context -> + assertThat(context.getBeansOfType(OkHttpClient::class.java)).isEmpty() + } + } + + @Test + fun `does not instrument when sentry-okhttp is not on the classpath`() { + contextRunner + .withClassLoader(FilteredClassLoader(SentryOkHttpInterceptor::class.java)) + .withPropertyValues("sentry.dsn=http://key@localhost/proj") + .withUserConfiguration(OkHttpClientConfiguration::class.java) + .run { context -> + val client = context.getBean(OkHttpClient::class.java) + assertThat(client.interceptors).hasSize(1) + assertThat(client.interceptors.first().javaClass.name) + .isEqualTo(OkHttpClientConfiguration.ExistingInterceptor::class.java.name) + } + } + + @Configuration(proxyBeanMethods = false) @EnableAutoConfiguration open class TestApplication + + @Configuration(proxyBeanMethods = false) + open class NoOpTransportConfiguration { + @Bean open fun noOpTransportFactory(): ITransportFactory = NoOpTransportFactory.getInstance() + } + + @Configuration(proxyBeanMethods = false) + open class OkHttpClientConfiguration { + @Bean open fun existingInterceptor(): Interceptor = ExistingInterceptor() + + @Bean open fun recordingEventListener(): RecordingEventListener = RecordingEventListener() + + @Bean + open fun okHttpClient( + existingInterceptor: Interceptor, + recordingEventListener: RecordingEventListener, + ): OkHttpClient = + OkHttpClient.Builder() + .connectTimeout(1234, TimeUnit.MILLISECONDS) + .addInterceptor(existingInterceptor) + .eventListener(recordingEventListener) + .build() + + class ExistingInterceptor : Interceptor { + override fun intercept(chain: Interceptor.Chain) = chain.proceed(chain.request()) + } + } + + @Configuration(proxyBeanMethods = false) + open class MultipleOkHttpClientsConfiguration { + @Bean open fun firstOkHttpClient(): OkHttpClient = OkHttpClient() + + @Bean open fun secondOkHttpClient(): OkHttpClient = OkHttpClient() + } + + @Configuration(proxyBeanMethods = false) + open class ManuallyInstrumentedOkHttpClientConfiguration { + @Bean + open fun okHttpClient(): OkHttpClient = + OkHttpClient.Builder().addInterceptor(SentryOkHttpInterceptor()).build() + } + + class CustomOkHttpClient : OkHttpClient() + + class RecordingEventListener : EventListener() { + val callStarted = AtomicBoolean(false) + + override fun callStart(call: Call) { + callStarted.set(true) + } + } +} From 679eaaa2f1a8c51f34a8d16b62eccc9951b40605 Mon Sep 17 00:00:00 2001 From: Lukas Bloder Date: Fri, 24 Jul 2026 16:00:05 +0200 Subject: [PATCH 2/6] add changelog entry --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a6600fda921..f42f81b82d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Features +- Add OkHttp autoconfiguration for Spring Boot 4 ([#5797](https://github.com/getsentry/sentry-java/pull/5797)) - Add `io.sentry:sentry-opentelemetry-bom` to align Sentry OpenTelemetry modules with tested OpenTelemetry dependencies ([#5629](https://github.com/getsentry/sentry-java/pull/5629)) - Spring Boot Gradle plugin: add the Sentry BOM to `dependencyManagement`; explicit imports are applied after Spring Boot's implicit BOM ```kotlin From acb0d7447de8e8845b39fcca563155bd84ca0507 Mon Sep 17 00:00:00 2001 From: Lukas Bloder Date: Mon, 21 Sep 2026 11:52:20 +0200 Subject: [PATCH 3/6] only auto-configure okhttp when otel is absent, add flag to enable okhttp autoconfig --- .../api/sentry-spring-boot-4.api | 8 ++ .../boot4/SentryOkHttpAutoConfiguration.java | 6 ++ .../sentry/spring/boot4/SentryProperties.java | 25 +++++ .../SentryOkHttpAutoConfigurationTest.kt | 100 ++++++++++++++++-- 4 files changed, 132 insertions(+), 7 deletions(-) diff --git a/sentry-spring-boot-4/api/sentry-spring-boot-4.api b/sentry-spring-boot-4/api/sentry-spring-boot-4.api index 723ef6abd14..d0c8785db9e 100644 --- a/sentry-spring-boot-4/api/sentry-spring-boot-4.api +++ b/sentry-spring-boot-4/api/sentry-spring-boot-4.api @@ -34,6 +34,7 @@ public class io/sentry/spring/boot4/SentryProfilerAutoConfiguration { public class io/sentry/spring/boot4/SentryProperties : io/sentry/SentryOptions { public fun ()V + public fun getClients ()Lio/sentry/spring/boot4/SentryProperties$Clients; public fun getExceptionResolverOrder ()I public fun getGraphql ()Lio/sentry/spring/boot4/SentryProperties$Graphql; public fun getLogging ()Lio/sentry/spring/boot4/SentryProperties$Logging; @@ -42,6 +43,7 @@ public class io/sentry/spring/boot4/SentryProperties : io/sentry/SentryOptions { public fun isEnableAotCompatibility ()Z public fun isKeepTransactionsOpenForAsyncResponses ()Z public fun isUseGitCommitIdAsRelease ()Z + public fun setClients (Lio/sentry/spring/boot4/SentryProperties$Clients;)V public fun setEnableAotCompatibility (Z)V public fun setExceptionResolverOrder (I)V public fun setGraphql (Lio/sentry/spring/boot4/SentryProperties$Graphql;)V @@ -52,6 +54,12 @@ public class io/sentry/spring/boot4/SentryProperties : io/sentry/SentryOptions { public fun setUserFilterOrder (Ljava/lang/Integer;)V } +public class io/sentry/spring/boot4/SentryProperties$Clients { + public fun ()V + public fun isOkHttpEnabled ()Z + public fun setOkHttpEnabled (Z)V +} + public class io/sentry/spring/boot4/SentryProperties$Graphql { public fun ()V public fun getIgnoredErrorTypes ()Ljava/util/List; diff --git a/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot4/SentryOkHttpAutoConfiguration.java b/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot4/SentryOkHttpAutoConfiguration.java index 7fdb73af9bb..627cd516a0e 100644 --- a/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot4/SentryOkHttpAutoConfiguration.java +++ b/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot4/SentryOkHttpAutoConfiguration.java @@ -6,6 +6,7 @@ import okhttp3.OkHttpClient; import org.jetbrains.annotations.NotNull; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -18,10 +19,15 @@ SentryOkHttpInterceptor.class, SentryOkHttpEventListener.class }) +@ConditionalOnMissingClass({ + "io.sentry.opentelemetry.SentryAutoConfigurationCustomizerProvider", + "io.sentry.opentelemetry.agent.AgentMarker" +}) @ConditionalOnProperty(name = "sentry.dsn") public class SentryOkHttpAutoConfiguration { @Bean + @ConditionalOnProperty(name = "sentry.clients.ok-http-enabled", havingValue = "true") static @NotNull SentryOkHttpClientBeanPostProcessor sentryOkHttpClientBeanPostProcessor() { return new SentryOkHttpClientBeanPostProcessor(); } diff --git a/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot4/SentryProperties.java b/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot4/SentryProperties.java index edb8d44cdd3..8773b4b64e3 100644 --- a/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot4/SentryProperties.java +++ b/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot4/SentryProperties.java @@ -47,6 +47,9 @@ public class SentryProperties extends SentryOptions { /** Graphql integration properties. */ private @NotNull Graphql graphql = new Graphql(); + /** Clients integration properties. */ + private @NotNull Clients clients = new Clients(); + public boolean isUseGitCommitIdAsRelease() { return useGitCommitIdAsRelease; } @@ -124,6 +127,14 @@ public void setGraphql(@NotNull Graphql graphql) { this.graphql = graphql; } + public @NotNull Clients getClients() { + return clients; + } + + public void setClients(@NotNull Clients clients) { + this.clients = clients; + } + @Open public static class Logging { /** Enable/Disable logging auto-configuration. */ @@ -215,4 +226,18 @@ public void setIgnoredErrorTypes(final @NotNull List ignoredErrorTypes) this.ignoredErrorTypes = ignoredErrorTypes; } } + + @Open + public static class Clients { + /** Enable automatic instrumentation of Spring-managed OkHttp clients. Disabled by default. */ + private boolean okHttpEnabled = false; + + public boolean isOkHttpEnabled() { + return okHttpEnabled; + } + + public void setOkHttpEnabled(boolean okHttpEnabled) { + this.okHttpEnabled = okHttpEnabled; + } + } } diff --git a/sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot4/SentryOkHttpAutoConfigurationTest.kt b/sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot4/SentryOkHttpAutoConfigurationTest.kt index 536ce6dbe21..ed016d53702 100644 --- a/sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot4/SentryOkHttpAutoConfigurationTest.kt +++ b/sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot4/SentryOkHttpAutoConfigurationTest.kt @@ -1,10 +1,13 @@ package io.sentry.spring.boot4 import com.google.common.truth.Truth.assertThat +import io.opentelemetry.api.OpenTelemetry import io.sentry.ITransportFactory import io.sentry.NoOpTransportFactory import io.sentry.okhttp.SentryOkHttpEventListener import io.sentry.okhttp.SentryOkHttpInterceptor +import io.sentry.opentelemetry.SentryAutoConfigurationCustomizerProvider +import io.sentry.opentelemetry.agent.AgentMarker import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicBoolean import kotlin.test.Test @@ -23,7 +26,7 @@ import org.springframework.context.annotation.Configuration class SentryOkHttpAutoConfigurationTest { - private val contextRunner = + private val baseContextRunner = ApplicationContextRunner() .withUserConfiguration(TestApplication::class.java, NoOpTransportConfiguration::class.java) .withPropertyValues( @@ -35,15 +38,54 @@ class SentryOkHttpAutoConfigurationTest { "sentry.enable-spotlight=false", ) + private val contextRunner = + baseContextRunner.withPropertyValues("sentry.clients.ok-http-enabled=true") + + private val noOtelClassLoader = + FilteredClassLoader( + SentryAutoConfigurationCustomizerProvider::class.java, + AgentMarker::class.java, + ) + + private val noOtelContextRunner = contextRunner.withClassLoader(noOtelClassLoader) + + @Test + fun `does not modify clients unless explicitly enabled`() { + for (properties in + listOf(emptyArray(), arrayOf("sentry.clients.ok-http-enabled=false"))) { + val plainClient = OkHttpClient() + val manualClient = + OkHttpClient.Builder() + .addInterceptor(SentryOkHttpInterceptor()) + .eventListener(SentryOkHttpEventListener()) + .build() + + baseContextRunner + .withClassLoader(noOtelClassLoader) + .withPropertyValues("sentry.dsn=http://key@localhost/proj", *properties) + .withBean("plainClient", OkHttpClient::class.java, { plainClient }) + .withBean("manualClient", OkHttpClient::class.java, { manualClient }) + .run { context -> + assertThat(context.getBean("plainClient")).isSameInstanceAs(plainClient) + assertThat(context.getBean("manualClient")).isSameInstanceAs(manualClient) + assertThat(context.getBeansOfType(SentryOkHttpClientBeanPostProcessor::class.java)) + .isEmpty() + assertThat(context.getBean(SentryProperties::class.java).clients.isOkHttpEnabled) + .isFalse() + } + } + } + @Test fun `instruments a Spring managed OkHttpClient`() { - contextRunner + noOtelContextRunner .withPropertyValues("sentry.dsn=http://key@localhost/proj") .withUserConfiguration(OkHttpClientConfiguration::class.java) .run { context -> val client = context.getBean(OkHttpClient::class.java) val existingInterceptor = context.getBean("existingInterceptor", Interceptor::class.java) + assertThat(context.getBean(SentryProperties::class.java).clients.isOkHttpEnabled).isTrue() assertThat(client.connectTimeoutMillis).isEqualTo(1234) assertThat(client.interceptors).contains(existingInterceptor) assertThat(client.interceptors.filterIsInstance()).hasSize(1) @@ -62,7 +104,7 @@ class SentryOkHttpAutoConfigurationTest { @Test fun `instruments every Spring managed OkHttpClient`() { - contextRunner + noOtelContextRunner .withPropertyValues("sentry.dsn=http://key@localhost/proj") .withUserConfiguration(MultipleOkHttpClientsConfiguration::class.java) .run { context -> @@ -78,7 +120,7 @@ class SentryOkHttpAutoConfigurationTest { @Test fun `does not duplicate an existing Sentry interceptor`() { - contextRunner + noOtelContextRunner .withPropertyValues("sentry.dsn=http://key@localhost/proj") .withUserConfiguration(ManuallyInstrumentedOkHttpClientConfiguration::class.java) .run { context -> @@ -120,9 +162,42 @@ class SentryOkHttpAutoConfigurationTest { assertThat(client.interceptors).isEmpty() } + @Test + fun `does not instrument when OpenTelemetry agent is present`() { + contextRunner + .withPropertyValues("sentry.dsn=http://key@localhost/proj") + .withUserConfiguration(OkHttpClientConfiguration::class.java) + .run { context -> + val client = context.getBean(OkHttpClient::class.java) + + assertThat(client.interceptors.filterIsInstance()).isEmpty() + assertThat(client.eventListenerFactory.create(mock())) + .isNotInstanceOf(SentryOkHttpEventListener::class.java) + } + } + + @Test + fun `does not instrument when Sentry OpenTelemetry integration is present`() { + contextRunner + .withClassLoader(FilteredClassLoader(AgentMarker::class.java)) + .withPropertyValues("sentry.dsn=http://key@localhost/proj") + .withUserConfiguration( + OkHttpClientConfiguration::class.java, + OpenTelemetryConfiguration::class.java, + ) + .run { context -> + val client = context.getBean(OkHttpClient::class.java) + + assertThat(client.interceptors.filterIsInstance()).isEmpty() + assertThat(client.eventListenerFactory.create(mock())) + .isNotInstanceOf(SentryOkHttpEventListener::class.java) + } + } + @Test fun `does not instrument OkHttpClient without a dsn`() { - contextRunner.withUserConfiguration(OkHttpClientConfiguration::class.java).run { context -> + noOtelContextRunner.withUserConfiguration(OkHttpClientConfiguration::class.java).run { context + -> val client = context.getBean(OkHttpClient::class.java) assertThat(client.interceptors.filterIsInstance()).isEmpty() @@ -133,7 +208,7 @@ class SentryOkHttpAutoConfigurationTest { @Test fun `does not create a default OkHttpClient`() { - contextRunner.withPropertyValues("sentry.dsn=http://key@localhost/proj").run { context -> + noOtelContextRunner.withPropertyValues("sentry.dsn=http://key@localhost/proj").run { context -> assertThat(context.getBeansOfType(OkHttpClient::class.java)).isEmpty() } } @@ -141,7 +216,13 @@ class SentryOkHttpAutoConfigurationTest { @Test fun `does not instrument when sentry-okhttp is not on the classpath`() { contextRunner - .withClassLoader(FilteredClassLoader(SentryOkHttpInterceptor::class.java)) + .withClassLoader( + FilteredClassLoader( + SentryOkHttpInterceptor::class.java, + SentryAutoConfigurationCustomizerProvider::class.java, + AgentMarker::class.java, + ) + ) .withPropertyValues("sentry.dsn=http://key@localhost/proj") .withUserConfiguration(OkHttpClientConfiguration::class.java) .run { context -> @@ -154,6 +235,11 @@ class SentryOkHttpAutoConfigurationTest { @Configuration(proxyBeanMethods = false) @EnableAutoConfiguration open class TestApplication + @Configuration(proxyBeanMethods = false) + open class OpenTelemetryConfiguration { + @Bean open fun openTelemetry(): OpenTelemetry = OpenTelemetry.noop() + } + @Configuration(proxyBeanMethods = false) open class NoOpTransportConfiguration { @Bean open fun noOpTransportFactory(): ITransportFactory = NoOpTransportFactory.getInstance() From 4e3cff35bc7eff89817a894c5eb7f9fc29bee5df Mon Sep 17 00:00:00 2001 From: Lukas Bloder Date: Mon, 21 Sep 2026 15:35:31 +0200 Subject: [PATCH 4/6] move OkHttp auto-config into SentryAutoConfiguration --- .../api/sentry-spring-boot-4.api | 4 --- .../spring/boot4/SentryAutoConfiguration.java | 22 ++++++++++++ .../boot4/SentryOkHttpAutoConfiguration.java | 34 ------------------- ...ot.autoconfigure.AutoConfiguration.imports | 1 - 4 files changed, 22 insertions(+), 39 deletions(-) delete mode 100644 sentry-spring-boot-4/src/main/java/io/sentry/spring/boot4/SentryOkHttpAutoConfiguration.java diff --git a/sentry-spring-boot-4/api/sentry-spring-boot-4.api b/sentry-spring-boot-4/api/sentry-spring-boot-4.api index c5273eae64c..f75c731c8b1 100644 --- a/sentry-spring-boot-4/api/sentry-spring-boot-4.api +++ b/sentry-spring-boot-4/api/sentry-spring-boot-4.api @@ -35,10 +35,6 @@ public class io/sentry/spring/boot4/SentryLogbackInitializer : org/springframewo public fun supportsEventType (Lorg/springframework/core/ResolvableType;)Z } -public class io/sentry/spring/boot4/SentryOkHttpAutoConfiguration { - public fun ()V -} - public class io/sentry/spring/boot4/SentryProfilerAutoConfiguration { public fun ()V } diff --git a/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot4/SentryAutoConfiguration.java b/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot4/SentryAutoConfiguration.java index 2429c1e7446..9f03af20ea8 100644 --- a/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot4/SentryAutoConfiguration.java +++ b/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot4/SentryAutoConfiguration.java @@ -274,6 +274,28 @@ static class SentryKafkaQueueConfiguration { } } + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass( + name = { + "okhttp3.OkHttpClient", + "io.sentry.okhttp.SentryOkHttpInterceptor", + "io.sentry.okhttp.SentryOkHttpEventListener" + }) + @ConditionalOnProperty(name = "sentry.clients.ok-http-enabled", havingValue = "true") + @ConditionalOnMissingClass({ + "io.sentry.opentelemetry.SentryAutoConfigurationCustomizerProvider", + "io.sentry.opentelemetry.agent.AgentMarker" + }) + @Open + static class SentryOkHttpConfiguration { + + @Bean + public static @NotNull SentryOkHttpClientBeanPostProcessor + sentryOkHttpClientBeanPostProcessor() { + return new SentryOkHttpClientBeanPostProcessor(); + } + } + @Configuration(proxyBeanMethods = false) @ConditionalOnClass(ProceedingJoinPoint.class) @ConditionalOnProperty( diff --git a/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot4/SentryOkHttpAutoConfiguration.java b/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot4/SentryOkHttpAutoConfiguration.java deleted file mode 100644 index 627cd516a0e..00000000000 --- a/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot4/SentryOkHttpAutoConfiguration.java +++ /dev/null @@ -1,34 +0,0 @@ -package io.sentry.spring.boot4; - -import com.jakewharton.nopen.annotation.Open; -import io.sentry.okhttp.SentryOkHttpEventListener; -import io.sentry.okhttp.SentryOkHttpInterceptor; -import okhttp3.OkHttpClient; -import org.jetbrains.annotations.NotNull; -import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -/** Auto-configures Sentry instrumentation for Spring-managed {@link OkHttpClient} beans. */ -@Configuration(proxyBeanMethods = false) -@Open -@ConditionalOnClass({ - OkHttpClient.class, - SentryOkHttpInterceptor.class, - SentryOkHttpEventListener.class -}) -@ConditionalOnMissingClass({ - "io.sentry.opentelemetry.SentryAutoConfigurationCustomizerProvider", - "io.sentry.opentelemetry.agent.AgentMarker" -}) -@ConditionalOnProperty(name = "sentry.dsn") -public class SentryOkHttpAutoConfiguration { - - @Bean - @ConditionalOnProperty(name = "sentry.clients.ok-http-enabled", havingValue = "true") - static @NotNull SentryOkHttpClientBeanPostProcessor sentryOkHttpClientBeanPostProcessor() { - return new SentryOkHttpClientBeanPostProcessor(); - } -} diff --git a/sentry-spring-boot-4/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/sentry-spring-boot-4/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index 64a7a12b551..e3e7c2e467b 100644 --- a/sentry-spring-boot-4/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/sentry-spring-boot-4/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -2,5 +2,4 @@ io.sentry.spring.boot4.SentryAutoConfiguration io.sentry.spring.boot4.SentryProfilerAutoConfiguration io.sentry.spring.boot4.SentryLogbackAppenderAutoConfiguration io.sentry.spring.boot4.SentryLog4j2AppenderAutoConfiguration -io.sentry.spring.boot4.SentryOkHttpAutoConfiguration io.sentry.spring.boot4.SentryWebfluxAutoConfiguration From 7074cdd38c44a9af8d5a5c6f55039b2ccfb02a19 Mon Sep 17 00:00:00 2001 From: Lukas Bloder Date: Mon, 21 Sep 2026 17:18:47 +0200 Subject: [PATCH 5/6] forward early cancel events and skip re-wrapping Sentry OkHttp listeners --- CHANGELOG.md | 2 +- .../SentryOkHttpClientBeanPostProcessor.java | 6 ++- .../SentryOkHttpAutoConfigurationTest.kt | 38 +++++++++++++++++++ 3 files changed, 44 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 84c2698941d..0080eb67f64 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ ### Features -- Add OkHttp autoconfiguration for Spring Boot 4 ([#5797](https://github.com/getsentry/sentry-java/pull/5797)) +- Add OkHttp autoconfiguration for Spring Boot ([#5797](https://github.com/getsentry/sentry-java/pull/5797)) - Add `LocalSentrySpan` to `sentry-compose` so apps can provide a parent `ISpan` to a composable subtree and have nested `SentryTraced` spans attach to it ([#6112]https://github.com/getsentry/sentry-java/pull/6112) - Add `dataCollection`, a fine-grained replacement for `sendDefaultPii`, for controlling data collected automatically by SDK integrations ([#5759](https://github.com/getsentry/sentry-java/pull/5759)) - `sendDefaultPii` remains supported for backwards compatibility. When `dataCollection` is not configured, the SDK preserves the existing `sendDefaultPii` behavior. diff --git a/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot4/SentryOkHttpClientBeanPostProcessor.java b/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot4/SentryOkHttpClientBeanPostProcessor.java index 9b0e465c906..5dd84ab413c 100644 --- a/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot4/SentryOkHttpClientBeanPostProcessor.java +++ b/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot4/SentryOkHttpClientBeanPostProcessor.java @@ -78,7 +78,11 @@ private SentryEventListenerFactory(final @NotNull EventListener.Factory delegate @Override public @NotNull EventListener create(final @NotNull Call call) { - return new SentryOkHttpEventListener(ScopesAdapter.getInstance(), delegate); + final @NotNull EventListener original = delegate.create(call); + if (original instanceof SentryOkHttpEventListener) { + return original; + } + return new SentryOkHttpEventListener(ScopesAdapter.getInstance(), original); } } } diff --git a/sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot4/SentryOkHttpAutoConfigurationTest.kt b/sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot4/SentryOkHttpAutoConfigurationTest.kt index ed016d53702..6bd9c4517b3 100644 --- a/sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot4/SentryOkHttpAutoConfigurationTest.kt +++ b/sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot4/SentryOkHttpAutoConfigurationTest.kt @@ -118,6 +118,39 @@ class SentryOkHttpAutoConfigurationTest { } } + @Test + fun `forwards canceled before callStart to the original event listener`() { + noOtelContextRunner + .withPropertyValues("sentry.dsn=http://key@localhost/proj") + .withUserConfiguration(OkHttpClientConfiguration::class.java) + .run { context -> + val client = context.getBean(OkHttpClient::class.java) + + // OkHttp creates the listener in the Call constructor and reports canceled() even if + // the call is never executed, i.e. before callStart() ever happens + client.newCall(Request.Builder().url("https://example.com").build()).cancel() + + assertThat(context.getBean(RecordingEventListener::class.java).callCanceled.get()).isTrue() + } + } + + @Test + fun `does not wrap a Sentry listener created by the original factory`() { + val sentryListener = SentryOkHttpEventListener() + val client = OkHttpClient.Builder().eventListenerFactory { sentryListener }.build() + + noOtelContextRunner + .withPropertyValues("sentry.dsn=http://key@localhost/proj") + .withBean("okHttpClient", OkHttpClient::class.java, { client }) + .run { context -> + val instrumented = context.getBean(OkHttpClient::class.java) + + assertThat(instrumented.eventListenerFactory.create(mock())) + .isSameInstanceAs(sentryListener) + assertThat(instrumented.interceptors.filterIsInstance()).hasSize(1) + } + } + @Test fun `does not duplicate an existing Sentry interceptor`() { noOtelContextRunner @@ -285,9 +318,14 @@ class SentryOkHttpAutoConfigurationTest { class RecordingEventListener : EventListener() { val callStarted = AtomicBoolean(false) + val callCanceled = AtomicBoolean(false) override fun callStart(call: Call) { callStarted.set(true) } + + override fun canceled(call: Call) { + callCanceled.set(true) + } } } From cddf3fad30db3254a8084ace0a9b362f1ef1e60f Mon Sep 17 00:00:00 2001 From: Lukas Bloder Date: Tue, 22 Sep 2026 14:09:39 +0200 Subject: [PATCH 6/6] add okhttp to spring boot sample, add e2e test --- .../build.gradle.kts | 4 +++ .../spring/boot4/SentryDemoApplication.java | 7 +++++ .../samples/spring/boot4/TodoController.java | 28 ++++++++++++++++++- .../src/main/resources/application.properties | 2 ++ .../io/sentry/systemtest/TodoSystemTest.kt | 16 +++++++++++ .../sentry/systemtest/util/RestTestClient.kt | 6 ++++ 6 files changed, 62 insertions(+), 1 deletion(-) diff --git a/sentry-samples/sentry-samples-spring-boot-4/build.gradle.kts b/sentry-samples/sentry-samples-spring-boot-4/build.gradle.kts index 17ec5b2a45f..2958ea17a8e 100644 --- a/sentry-samples/sentry-samples-spring-boot-4/build.gradle.kts +++ b/sentry-samples/sentry-samples-spring-boot-4/build.gradle.kts @@ -64,6 +64,10 @@ dependencies { implementation(libs.springboot4.starter.kafka) implementation(projects.sentryKafka) + // okhttp client instrumentation + implementation(projects.sentryOkhttp) + implementation(libs.okhttp) + // database query tracing implementation(projects.sentryJdbc) runtimeOnly(libs.hsqldb) diff --git a/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot4/SentryDemoApplication.java b/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot4/SentryDemoApplication.java index 13d97fa8442..03688f1a84c 100644 --- a/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot4/SentryDemoApplication.java +++ b/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot4/SentryDemoApplication.java @@ -4,6 +4,7 @@ import io.sentry.samples.spring.boot4.quartz.SampleJob; import java.util.Collections; +import okhttp3.OkHttpClient; import org.quartz.JobDetail; import org.quartz.SimpleTrigger; import org.springframework.boot.SpringApplication; @@ -42,6 +43,12 @@ RestClient restClient(RestClient.Builder builder) { return builder.build(); } + @Bean + OkHttpClient okHttpClient() { + // automatically instrumented by Sentry via sentry.clients.ok-http-enabled=true + return new OkHttpClient.Builder().build(); + } + @Bean public JobDetailFactoryBean jobDetail() { JobDetailFactoryBean jobDetailFactory = new JobDetailFactoryBean(); diff --git a/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot4/TodoController.java b/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot4/TodoController.java index 0f71cca0419..84ecd9e5ea7 100644 --- a/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot4/TodoController.java +++ b/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot4/TodoController.java @@ -1,6 +1,11 @@ package io.sentry.samples.spring.boot4; import io.sentry.reactor.SentryReactorUtils; +import java.io.IOException; +import java.io.UncheckedIOException; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RestController; @@ -10,17 +15,27 @@ import reactor.core.publisher.Hooks; import reactor.core.publisher.Mono; import reactor.core.scheduler.Schedulers; +import tools.jackson.databind.ObjectMapper; @RestController public class TodoController { private final RestTemplate restTemplate; private final WebClient webClient; private final RestClient restClient; + private final OkHttpClient okHttpClient; + private final ObjectMapper objectMapper; - public TodoController(RestTemplate restTemplate, WebClient webClient, RestClient restClient) { + public TodoController( + RestTemplate restTemplate, + WebClient webClient, + RestClient restClient, + OkHttpClient okHttpClient, + ObjectMapper objectMapper) { this.restTemplate = restTemplate; this.webClient = webClient; this.restClient = restClient; + this.okHttpClient = okHttpClient; + this.objectMapper = objectMapper; } @GetMapping("/todo/{id}") @@ -54,4 +69,15 @@ Todo todoRestClient(@PathVariable Long id) { .retrieve() .body(Todo.class); } + + @GetMapping("/todo-okhttp/{id}") + Todo todoOkHttp(@PathVariable Long id) { + final Request request = + new Request.Builder().url("https://jsonplaceholder.typicode.com/todos/" + id).build(); + try (Response response = okHttpClient.newCall(request).execute()) { + return objectMapper.readValue(response.body().byteStream(), Todo.class); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } } diff --git a/sentry-samples/sentry-samples-spring-boot-4/src/main/resources/application.properties b/sentry-samples/sentry-samples-spring-boot-4/src/main/resources/application.properties index 8198059343a..8fb97401ea6 100644 --- a/sentry-samples/sentry-samples-spring-boot-4/src/main/resources/application.properties +++ b/sentry-samples/sentry-samples-spring-boot-4/src/main/resources/application.properties @@ -21,6 +21,8 @@ sentry.profile-session-sample-rate=1.0 sentry.profiling-traces-dir-path=tmp/sentry/profiling-traces sentry.profile-lifecycle=TRACE sentry.enable-cache-tracing=true +# Automatically instrument Spring-managed OkHttpClient beans +sentry.clients.ok-http-enabled=true spring.cache.cache-names=todos spring.cache.caffeine.spec=maximumSize=500,expireAfterAccess=600s diff --git a/sentry-samples/sentry-samples-spring-boot-4/src/test/kotlin/io/sentry/systemtest/TodoSystemTest.kt b/sentry-samples/sentry-samples-spring-boot-4/src/test/kotlin/io/sentry/systemtest/TodoSystemTest.kt index d34485e1388..66600e9753d 100644 --- a/sentry-samples/sentry-samples-spring-boot-4/src/test/kotlin/io/sentry/systemtest/TodoSystemTest.kt +++ b/sentry-samples/sentry-samples-spring-boot-4/src/test/kotlin/io/sentry/systemtest/TodoSystemTest.kt @@ -58,4 +58,20 @@ class TodoSystemTest { ) } } + + @Test + fun `get todo okhttp works`() { + val restClient = testHelper.restClient + restClient.getTodoOkHttp(1L) + assertEquals(200, restClient.lastKnownStatusCode) + + testHelper.ensureTransactionReceived { transaction, envelopeHeader -> + transaction.transaction == "GET /todo-okhttp/{id}" && + testHelper.doesTransactionContainSpanWithOpAndDescription( + transaction, + "http.client", + "GET https://jsonplaceholder.typicode.com/todos/1", + ) + } + } } diff --git a/sentry-system-test-support/src/main/kotlin/io/sentry/systemtest/util/RestTestClient.kt b/sentry-system-test-support/src/main/kotlin/io/sentry/systemtest/util/RestTestClient.kt index b9dc0f3ccad..ac2c69f5ed2 100644 --- a/sentry-system-test-support/src/main/kotlin/io/sentry/systemtest/util/RestTestClient.kt +++ b/sentry-system-test-support/src/main/kotlin/io/sentry/systemtest/util/RestTestClient.kt @@ -50,6 +50,12 @@ class RestTestClient(private val backendBaseUrl: String) : LoggingInsecureRestCl return callTyped(request, true) } + fun getTodoOkHttp(id: Long): Todo? { + val request = Request.Builder().url("$backendBaseUrl/todo-okhttp/$id") + + return callTyped(request, true) + } + fun getCachedTodo(id: Long): Todo? { val request = Request.Builder().url("$backendBaseUrl/cache/$id")