Skip to content

Refactor ReactorUtils into its own sentry-reactor module #4155

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’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 14 commits into from
Feb 17, 2025
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 .github/ISSUE_TEMPLATE/bug_report_java.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ body:
- sentry-openfeign
- sentry-apache-http-client-5
- sentry-okhttp
- sentry-reactor
- other
validations:
required: true
Expand Down
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# Changelog

## Unreleased

### Behavioural Changes

- The class `io.sentry.spring.jakarta.webflux.ReactorUtils` is now deprecated, please use `io.sentry.reactor.SentryReactorUtils` in the new `sentry-reactor` module instead ([#4155](https://github.com/getsentry/sentry-java/pull/4155))
- The new module will be exposed as an `api` dependency when using `sentry-spring-boot-jakarta` (Spring Boot 3) or `sentry-spring-jakarta` (Spring 6).
Therefore, if you're using one of those modules, changing your imports will suffice.

## 8.2.0

### Breaking Changes
Expand Down
1 change: 1 addition & 0 deletions buildSrc/src/main/java/Config.kt
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,7 @@ object Config {
val SENTRY_SERVLET_JAKARTA_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.servlet.jakarta"
val SENTRY_COMPOSE_HELPER_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.compose.helper"
val SENTRY_OKHTTP_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.okhttp"
val SENTRY_REACTOR_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.reactor"
val group = "io.sentry"
val description = "SDK for sentry.io"
val versionNameProp = "versionName"
Expand Down
68 changes: 68 additions & 0 deletions sentry-reactor/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# sentry-reactor

This module provides a set of utilities to use Sentry with [Reactor](https://projectreactor.io/).

## Setup

Please refer to the documentation on how to set up our [Java SDK](https://docs.sentry.io/platforms/java/),
or our [Spring](https://docs.sentry.io/platforms/java/guides/spring/)
or [Spring Boot](https://docs.sentry.io/platforms/java/guides/spring-boot/) integrations if you're using Spring WebFlux.

If you're using our Spring Boot SDK with Spring Boot (`sentry-spring-boot` or `sentry-spring-boot-jakarta`), this module will be available and used under the hood to automatically instrument WebFlux.
If you're using our Spring SDK (`sentry-spring` or `sentry-spring-jakarta`), you need to configure WebFlux as we do in [SentryWebFluxAutoConfiguration](https://github.com/getsentry/sentry-java/blob/a5098280b52aec28c71c150e286b5c937767634d/sentry-spring-boot-jakarta/src/main/java/io/sentry/spring/boot/jakarta/SentryWebfluxAutoConfiguration.java) for Spring Boot.

Otherwise, read on to find out how to set up and use the integration.

Add the latest version of `io.sentry.reactor` as a dependency.
Make sure you're using `io.micrometer:context-propagation:1.0.2` or later, and `io.projectreactor:reactor-core:3.5.3` or later.

Then, enable automatic context propagation:
```java
import reactor.core.publisher.Hooks;
// ...
Hooks.enableAutomaticContextPropagation();
```

## Usage

You can use the utilities provided by this module to wrap `Mono` and `Flux` objects to enable correct errors, breadcrumbs and tracing in your application.

For normal use cases, you should wrap your operations on `Mono` or `Flux` objects using the `withSentry` function.
This will fork the *current scopes* and use them throughout the stream's execution context.

For example:
```java
import reactor.core.publisher.Mono;
import io.sentry.Sentry;
import io.sentry.ISpan;
import io.sentry.ITransaction;
import io.sentry.TransactionOptions;

TransactionOptions txOptions = new TransactionOptions();
txOptions.setBindToScope(true);
ITransaction tx = Sentry.startTransaction("Transaction", "op", txOptions);
ISpan child = tx.startChild("Outside Mono", "op")
Sentry.captureMessage("Message outside Mono")
child.finish()
String result = SentryReactorUtils.withSentry(
Mono.just("hello")
.map({ (it) ->
ISpan span = Sentry.getCurrentScopes().transaction.startChild("Inside Mono", "map");
Sentry.captureMessage("Message inside Mono");
span.finish();
return it;
})
).block();
System.out.println(result);
tx.finish();
```

For more complex use cases, you can also use `withSentryForkedRoots` to fork the root scopes or `withSentryScopes` to wrap the operation in arbitrary scopes.

For more information on scopes and scope forking, please consult our [scopes documentation](https://docs.sentry.io/platforms/java/enriching-events/scopes).

Examples of usage of this module (with Spring WebFlux) are provided in
[sentry-samples-spring-boot-webflux](https://github.com/getsentry/sentry-java/tree/main/sentry-samples/sentry-samples-spring-boot-webflux)
and
[sentry-samples-spring-boot-webflux-jakarta](https://github.com/getsentry/sentry-java/tree/main/sentry-samples/sentry-samples-spring-boot-webflux-jakarta)
.
26 changes: 26 additions & 0 deletions sentry-reactor/api/sentry-reactor.api
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
public final class io/sentry/reactor/BuildConfig {
public static final field SENTRY_REACTOR_SDK_NAME Ljava/lang/String;
public static final field VERSION_NAME Ljava/lang/String;
}

public final class io/sentry/reactor/SentryReactorThreadLocalAccessor : io/micrometer/context/ThreadLocalAccessor {
public static final field KEY Ljava/lang/String;
public fun <init> ()V
public fun getValue ()Lio/sentry/IScopes;
public synthetic fun getValue ()Ljava/lang/Object;
public fun key ()Ljava/lang/Object;
public fun reset ()V
public fun setValue (Lio/sentry/IScopes;)V
public synthetic fun setValue (Ljava/lang/Object;)V
}

public class io/sentry/reactor/SentryReactorUtils {
public fun <init> ()V
public static fun withSentry (Lreactor/core/publisher/Flux;)Lreactor/core/publisher/Flux;
public static fun withSentry (Lreactor/core/publisher/Mono;)Lreactor/core/publisher/Mono;
public static fun withSentryForkedRoots (Lreactor/core/publisher/Flux;)Lreactor/core/publisher/Flux;
public static fun withSentryForkedRoots (Lreactor/core/publisher/Mono;)Lreactor/core/publisher/Mono;
public static fun withSentryScopes (Lreactor/core/publisher/Flux;Lio/sentry/IScopes;)Lreactor/core/publisher/Flux;
public static fun withSentryScopes (Lreactor/core/publisher/Mono;Lio/sentry/IScopes;)Lreactor/core/publisher/Mono;
}

94 changes: 94 additions & 0 deletions sentry-reactor/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import net.ltgt.gradle.errorprone.errorprone
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
`java-library`
kotlin("jvm")
jacoco
id(Config.QualityPlugins.errorProne)
id(Config.QualityPlugins.gradleVersions)
id(Config.BuildPlugins.buildConfig) version Config.BuildPlugins.buildConfigVersion
}

configure<JavaPluginExtension> {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}

tasks.withType<KotlinCompile>().configureEach {
kotlinOptions.jvmTarget = JavaVersion.VERSION_17.toString()
kotlinOptions.languageVersion = Config.kotlinCompatibleLanguageVersion
}

dependencies {
api(projects.sentry)
compileOnly(Config.Libs.reactorCore)
compileOnly(Config.Libs.contextPropagation)

compileOnly(Config.CompileOnly.nopen)
errorprone(Config.CompileOnly.nopenChecker)
errorprone(Config.CompileOnly.errorprone)
errorprone(Config.CompileOnly.errorProneNullAway)
compileOnly(Config.CompileOnly.jetbrainsAnnotations)

// tests
testImplementation(projects.sentryTestSupport)
testImplementation(kotlin(Config.kotlinStdLib))
testImplementation(Config.TestLibs.kotlinTestJunit)
testImplementation(Config.TestLibs.mockitoKotlin)

testImplementation(Config.Libs.reactorCore)
testImplementation(Config.Libs.contextPropagation)

testImplementation(platform("org.junit:junit-bom:5.10.0"))
testImplementation("org.junit.jupiter:junit-jupiter")
}

configure<SourceSetContainer> {
test {
java.srcDir("src/test/java")
}
}

jacoco {
toolVersion = Config.QualityPlugins.Jacoco.version
}

tasks.jacocoTestReport {
reports {
xml.required.set(true)
html.required.set(false)
}
}

tasks {
jacocoTestCoverageVerification {
violationRules {
rule { limit { minimum = Config.QualityPlugins.Jacoco.minimumCoverage } }
}
}
check {
dependsOn(jacocoTestCoverageVerification)
dependsOn(jacocoTestReport)
}
}

buildConfig {
useJavaOutput()
packageName("io.sentry.reactor")
buildConfigField("String", "SENTRY_REACTOR_SDK_NAME", "\"${Config.Sentry.SENTRY_REACTOR_SDK_NAME}\"")
buildConfigField("String", "VERSION_NAME", "\"${project.version}\"")
}

val generateBuildConfig by tasks
tasks.withType<JavaCompile>().configureEach {
dependsOn(generateBuildConfig)
options.errorprone {
check("NullAway", net.ltgt.gradle.errorprone.CheckSeverity.ERROR)
option("NullAway:AnnotatedPackages", "io.sentry")
}
}

repositories {
mavenCentral()
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
package io.sentry.spring.jakarta.webflux;
package io.sentry.reactor;

import io.micrometer.context.ThreadLocalAccessor;
import io.sentry.IScopes;
import io.sentry.NoOpScopes;
import io.sentry.Sentry;
import org.jetbrains.annotations.ApiStatus;

@ApiStatus.Experimental
public final class SentryReactorThreadLocalAccessor implements ThreadLocalAccessor<IScopes> {

public static final String KEY = "sentry-scopes";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
package io.sentry.spring.jakarta.webflux;
package io.sentry.reactor;

import com.jakewharton.nopen.annotation.Open;
import io.sentry.IScopes;
import io.sentry.Sentry;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.util.context.Context;

@ApiStatus.Experimental
public final class ReactorUtils {
@Open
public class SentryReactorUtils {

/**
* Writes the current Sentry {@link IScopes} to the {@link Context} and uses {@link
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
io.sentry.reactor.SentryReactorThreadLocalAccessor
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package io.sentry.spring.jakarta.webflux
package io.sentry.reactor

import io.sentry.IScopes
import io.sentry.NoOpScopes
Expand All @@ -18,11 +18,12 @@ import kotlin.test.assertEquals
import kotlin.test.assertNotSame
import kotlin.test.assertSame

class ReactorUtilsTest {
class SentryReactorUtilsTest {

@BeforeTest
fun setup() {
Hooks.enableAutomaticContextPropagation()
Sentry.init("https://key@sentry.io/proj")
}

@AfterTest
Expand All @@ -34,7 +35,7 @@ class ReactorUtilsTest {
fun `propagates scopes inside mono`() {
val scopesToUse = mock<IScopes>()
var scopesInside: IScopes? = null
val mono = ReactorUtils.withSentryScopes(
val mono = SentryReactorUtils.withSentryScopes(
Mono.just("hello")
.publishOn(Schedulers.boundedElastic())
.map { it ->
Expand All @@ -52,7 +53,7 @@ class ReactorUtilsTest {
fun `propagates scopes inside flux`() {
val scopesToUse = mock<IScopes>()
var scopesInside: IScopes? = null
val flux = ReactorUtils.withSentryScopes(
val flux = SentryReactorUtils.withSentryScopes(
Flux.just("hello")
.publishOn(Schedulers.boundedElastic())
.map { it ->
Expand Down Expand Up @@ -101,7 +102,7 @@ class ReactorUtilsTest {
val mockScopes = mock<IScopes>()
whenever(mockScopes.forkedCurrentScope(any())).thenReturn(mock<IScopes>())
Sentry.setCurrentScopes(mockScopes)
ReactorUtils.withSentry(Mono.just("hello")).block()
SentryReactorUtils.withSentry(Mono.just("hello")).block()

verify(mockScopes).forkedCurrentScope(any())
}
Expand All @@ -111,7 +112,7 @@ class ReactorUtilsTest {
val mockScopes = mock<IScopes>()
whenever(mockScopes.forkedCurrentScope(any())).thenReturn(mock<IScopes>())
Sentry.setCurrentScopes(mockScopes)
ReactorUtils.withSentry(Flux.just("hello")).blockFirst()
SentryReactorUtils.withSentry(Flux.just("hello")).blockFirst()

verify(mockScopes).forkedCurrentScope(any())
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import io.opentelemetry.context.Scope;
import io.sentry.ISpan;
import io.sentry.Sentry;
import io.sentry.spring.jakarta.webflux.ReactorUtils;
import io.sentry.reactor.SentryReactorUtils;
import org.jetbrains.annotations.NotNull;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
Expand Down Expand Up @@ -51,7 +51,7 @@ Todo todo(@PathVariable Long id) {
@GetMapping("/todo-webclient/{id}")
Todo todoWebClient(@PathVariable Long id) {
Hooks.enableAutomaticContextPropagation();
return ReactorUtils.withSentry(
return SentryReactorUtils.withSentry(
Mono.just(true)
.publishOn(Schedulers.boundedElastic())
.flatMap(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import io.opentelemetry.context.Scope;
import io.sentry.ISpan;
import io.sentry.Sentry;
import io.sentry.spring.jakarta.webflux.ReactorUtils;
import io.sentry.reactor.SentryReactorUtils;
import org.jetbrains.annotations.NotNull;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
Expand Down Expand Up @@ -51,7 +51,7 @@ Todo todo(@PathVariable Long id) {
@GetMapping("/todo-webclient/{id}")
Todo todoWebClient(@PathVariable Long id) {
Hooks.enableAutomaticContextPropagation();
return ReactorUtils.withSentry(
return SentryReactorUtils.withSentry(
Mono.just(true)
.publishOn(Schedulers.boundedElastic())
.flatMap(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package io.sentry.samples.spring.boot.jakarta;

import io.sentry.spring.jakarta.webflux.ReactorUtils;
import io.sentry.reactor.SentryReactorUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
Expand Down Expand Up @@ -32,7 +32,7 @@ Todo todo(@PathVariable Long id) {
@GetMapping("/todo-webclient/{id}")
Todo todoWebClient(@PathVariable Long id) {
Hooks.enableAutomaticContextPropagation();
return ReactorUtils.withSentry(
return SentryReactorUtils.withSentry(
Mono.just(true)
.publishOn(Schedulers.boundedElastic())
.flatMap(
Expand Down
2 changes: 2 additions & 0 deletions sentry-spring-boot-jakarta/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ dependencies {
compileOnly(Config.Libs.OpenTelemetry.otelSdk)
compileOnly(projects.sentryOpentelemetry.sentryOpentelemetryCore)
compileOnly(projects.sentryOpentelemetry.sentryOpentelemetryAgentcustomization)
api(projects.sentryReactor)

annotationProcessor(platform(SpringBootPlugin.BOM_COORDINATES))
annotationProcessor(Config.AnnotationProcessors.springBootAutoConfigure)
Expand Down Expand Up @@ -85,6 +86,7 @@ dependencies {
testImplementation(projects.sentryOpentelemetry.sentryOpentelemetryAgent)
testImplementation(projects.sentryOpentelemetry.sentryOpentelemetryAgentcustomization)
testImplementation(projects.sentryOpentelemetry.sentryOpentelemetryBootstrap)
testImplementation(projects.sentryReactor)
}

configure<SourceSetContainer> {
Expand Down
Loading
Loading