diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d32c4bb196..1b1c950a19a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ * Feat: Spring WebClient integration (#1621) * Feat: OpenFeign integration (#1632) +* Feat: Add support for async methods in Spring MVC () ## 5.1.0-beta.9 diff --git a/sentry-spring/api/sentry-spring.api b/sentry-spring/api/sentry-spring.api index 3d823ce3a91..3022d59de9b 100644 --- a/sentry-spring/api/sentry-spring.api +++ b/sentry-spring/api/sentry-spring.api @@ -63,6 +63,11 @@ public class io/sentry/spring/SentrySpringServletContainerInitializer : javax/se public fun onStartup (Ljava/util/Set;Ljavax/servlet/ServletContext;)V } +public final class io/sentry/spring/SentryTaskDecorator : org/springframework/core/task/TaskDecorator { + public fun ()V + public fun decorate (Ljava/lang/Runnable;)Ljava/lang/Runnable; +} + public class io/sentry/spring/SentryUserFilter : javax/servlet/Filter { public fun (Lio/sentry/IHub;Ljava/util/List;)V public fun doFilter (Ljavax/servlet/ServletRequest;Ljavax/servlet/ServletResponse;Ljavax/servlet/FilterChain;)V diff --git a/sentry-spring/src/main/java/io/sentry/spring/SentryTaskDecorator.java b/sentry-spring/src/main/java/io/sentry/spring/SentryTaskDecorator.java new file mode 100644 index 00000000000..f5a484536ad --- /dev/null +++ b/sentry-spring/src/main/java/io/sentry/spring/SentryTaskDecorator.java @@ -0,0 +1,29 @@ +package io.sentry.spring; + +import io.sentry.IHub; +import io.sentry.Sentry; +import java.util.concurrent.Callable; +import org.jetbrains.annotations.NotNull; +import org.springframework.core.task.TaskDecorator; +import org.springframework.scheduling.annotation.Async; + +/** + * Sets a current hub on a thread running a {@link Runnable} given by parameter. Used to propagate + * the current {@link IHub} on the thread executing async task - like MVC controller methods + * returning a {@link Callable} or Spring beans methods annotated with {@link Async}. + */ +public final class SentryTaskDecorator implements TaskDecorator { + @Override + public @NotNull Runnable decorate(final @NotNull Runnable runnable) { + final IHub oldState = Sentry.getCurrentHub(); + final IHub newHub = Sentry.getCurrentHub().clone(); + return () -> { + Sentry.setCurrentHub(newHub); + try { + runnable.run(); + } finally { + Sentry.setCurrentHub(oldState); + } + }; + } +} diff --git a/sentry-spring/src/test/kotlin/io/sentry/spring/mvc/SentrySpringIntegrationTest.kt b/sentry-spring/src/test/kotlin/io/sentry/spring/mvc/SentrySpringIntegrationTest.kt index f1e021ba981..5ee4b3a88d1 100644 --- a/sentry-spring/src/test/kotlin/io/sentry/spring/mvc/SentrySpringIntegrationTest.kt +++ b/sentry-spring/src/test/kotlin/io/sentry/spring/mvc/SentrySpringIntegrationTest.kt @@ -15,6 +15,7 @@ import io.sentry.SpanStatus import io.sentry.spring.EnableSentry import io.sentry.spring.SentryExceptionResolver import io.sentry.spring.SentrySpringFilter +import io.sentry.spring.SentryTaskDecorator import io.sentry.spring.SentryUserFilter import io.sentry.spring.SentryUserProvider import io.sentry.spring.tracing.SentryTracingConfiguration @@ -23,8 +24,8 @@ import io.sentry.spring.tracing.SentryTransaction import io.sentry.test.checkEvent import io.sentry.test.checkTransaction import io.sentry.transport.ITransport -import java.lang.Exception import java.time.Duration +import java.util.concurrent.Callable import org.assertj.core.api.Assertions.assertThat import org.awaitility.kotlin.await import org.junit.Before @@ -237,6 +238,21 @@ class SentrySpringIntegrationTest { }, anyOrNull()) } } + + @Test + fun `scope is applied to events triggered in async methods`() { + val restTemplate = TestRestTemplate().withBasicAuth("user", "password") + + restTemplate.getForEntity("http://localhost:$port/callable", String::class.java) + + await.untilAsserted { + verify(transport).send(checkEvent { event -> + assertThat(event.message!!.formatted).isEqualTo("this message should be in the scope of the request") + assertThat(event.request).isNotNull() + assertThat(event.request!!.url).isEqualTo("http://localhost:$port/callable") + }, anyOrNull()) + } + } } @SpringBootApplication @@ -278,6 +294,9 @@ open class App { this.filter = SentryTracingFilter(hub) this.order = Ordered.HIGHEST_PRECEDENCE + 1 // must run after SentrySpringFilter } + + @Bean + open fun sentryTaskDecorator() = SentryTaskDecorator() } @Service @@ -330,6 +349,14 @@ class HelloController { fun throwsHandled() { throw CustomException("handled exception") } + + @GetMapping("/callable") + fun callable(): Callable { + return Callable { + Sentry.captureMessage("this message should be in the scope of the request") + "from callable" + } + } } class CustomException(message: String) : RuntimeException(message)