Skip to content

Commit f78fb8d

Browse files
authored
Merge f6c6a0a into dd03aa1
2 parents dd03aa1 + f6c6a0a commit f78fb8d

File tree

14 files changed

+585
-0
lines changed

14 files changed

+585
-0
lines changed

sentry-samples/sentry-samples-spring-boot-webflux-jakarta/src/main/resources/application.properties

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@ sentry.max-breadcrumbs=150
88
sentry.logging.minimum-event-level=info
99
sentry.logging.minimum-breadcrumb-level=debug
1010
sentry.reactive.thread-local-accessor-enabled=true
11+
sentry.enable-tracing=false

sentry-samples/sentry-samples-spring-boot-webflux/src/main/resources/application.properties

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ sentry.max-breadcrumbs=150
77
# Logback integration configuration options
88
sentry.logging.minimum-event-level=info
99
sentry.logging.minimum-breadcrumb-level=debug
10+
sentry.enable-tracing=true

sentry-spring-boot-starter-jakarta/api/sentry-spring-boot-starter-jakarta.api

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,5 +51,6 @@ public class io/sentry/spring/boot/jakarta/SentryProperties$Reactive {
5151
public class io/sentry/spring/boot/jakarta/SentryWebfluxAutoConfiguration {
5252
public fun <init> ()V
5353
public fun sentryWebExceptionHandler (Lio/sentry/IHub;)Lio/sentry/spring/jakarta/webflux/SentryWebExceptionHandler;
54+
public fun sentryWebTracingFilter ()Lio/sentry/spring/jakarta/webflux/SentryWebTracingFilter;
5455
}
5556

sentry-spring-boot-starter-jakarta/src/main/java/io/sentry/spring/boot/jakarta/SentryWebfluxAutoConfiguration.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,23 @@
66
import io.sentry.spring.jakarta.webflux.SentryWebExceptionHandler;
77
import io.sentry.spring.jakarta.webflux.SentryWebFilter;
88
import io.sentry.spring.jakarta.webflux.SentryWebFilterWithThreadLocalAccessor;
9+
import io.sentry.spring.jakarta.webflux.SentryWebTracingFilter;
910
import org.jetbrains.annotations.ApiStatus;
1011
import org.jetbrains.annotations.NotNull;
1112
import org.springframework.boot.ApplicationRunner;
1213
import org.springframework.boot.autoconfigure.condition.AllNestedConditions;
1314
import org.springframework.boot.autoconfigure.condition.AnyNestedCondition;
1415
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
1516
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
17+
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
1618
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass;
1719
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
1820
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
1921
import org.springframework.context.annotation.Bean;
2022
import org.springframework.context.annotation.Conditional;
2123
import org.springframework.context.annotation.Configuration;
24+
import org.springframework.core.Ordered;
25+
import org.springframework.core.annotation.Order;
2226
import reactor.core.publisher.Hooks;
2327
import reactor.core.scheduler.Schedulers;
2428

@@ -30,6 +34,7 @@
3034
@Open
3135
@ApiStatus.Experimental
3236
public class SentryWebfluxAutoConfiguration {
37+
private static final int SENTRY_SPRING_FILTER_PRECEDENCE = Ordered.HIGHEST_PRECEDENCE;
3338

3439
@Configuration(proxyBeanMethods = false)
3540
@Conditional(SentryThreadLocalAccessorCondition.class)
@@ -43,6 +48,7 @@ static class SentryWebfluxFilterThreadLocalAccessorConfiguration {
4348
* ThreadLocalAccessor to propagate the Sentry hub.
4449
*/
4550
@Bean
51+
@Order(SENTRY_SPRING_FILTER_PRECEDENCE)
4652
public @NotNull SentryWebFilterWithThreadLocalAccessor sentryWebFilterWithContextPropagation(
4753
final @NotNull IHub hub) {
4854
Hooks.enableAutomaticContextPropagation();
@@ -65,11 +71,20 @@ static class SentryWebfluxFilterConfiguration {
6571

6672
/** Configures a filter that sets up Sentry {@link io.sentry.Scope} for each request. */
6773
@Bean
74+
@Order(SENTRY_SPRING_FILTER_PRECEDENCE)
6875
public @NotNull SentryWebFilter sentryWebFilter(final @NotNull IHub hub) {
6976
return new SentryWebFilter(hub);
7077
}
7178
}
7279

80+
@Bean
81+
@Order(SENTRY_SPRING_FILTER_PRECEDENCE + 1)
82+
@Conditional(SentryAutoConfiguration.SentryTracingCondition.class)
83+
@ConditionalOnMissingBean(name = "sentryWebTracingFilter")
84+
public @NotNull SentryWebTracingFilter sentryWebTracingFilter() {
85+
return new SentryWebTracingFilter();
86+
}
87+
7388
/** Configures exception handler that handles unhandled exceptions and sends them to Sentry. */
7489
@Bean
7590
public @NotNull SentryWebExceptionHandler sentryWebExceptionHandler(final @NotNull IHub hub) {

sentry-spring-boot-starter/api/sentry-spring-boot-starter.api

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,5 +45,6 @@ public class io/sentry/spring/boot/SentryWebfluxAutoConfiguration {
4545
public fun sentryScheduleHookApplicationRunner ()Lorg/springframework/boot/ApplicationRunner;
4646
public fun sentryWebExceptionHandler (Lio/sentry/IHub;)Lio/sentry/spring/webflux/SentryWebExceptionHandler;
4747
public fun sentryWebFilter (Lio/sentry/IHub;)Lio/sentry/spring/webflux/SentryWebFilter;
48+
public fun sentryWebTracingFilter ()Lio/sentry/spring/webflux/SentryWebTracingFilter;
4849
}
4950

sentry-spring-boot-starter/src/main/java/io/sentry/spring/boot/SentryWebfluxAutoConfiguration.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,19 @@
55
import io.sentry.spring.webflux.SentryScheduleHook;
66
import io.sentry.spring.webflux.SentryWebExceptionHandler;
77
import io.sentry.spring.webflux.SentryWebFilter;
8+
import io.sentry.spring.webflux.SentryWebTracingFilter;
89
import org.jetbrains.annotations.ApiStatus;
910
import org.jetbrains.annotations.NotNull;
1011
import org.springframework.boot.ApplicationRunner;
1112
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
1213
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
14+
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
1315
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
1416
import org.springframework.context.annotation.Bean;
17+
import org.springframework.context.annotation.Conditional;
1518
import org.springframework.context.annotation.Configuration;
19+
import org.springframework.core.Ordered;
20+
import org.springframework.core.annotation.Order;
1621
import reactor.core.scheduler.Schedulers;
1722

1823
/** Configures Sentry integration for Spring Webflux and Project Reactor. */
@@ -23,6 +28,7 @@
2328
@Open
2429
@ApiStatus.Experimental
2530
public class SentryWebfluxAutoConfiguration {
31+
private static final int SENTRY_SPRING_FILTER_PRECEDENCE = Ordered.HIGHEST_PRECEDENCE;
2632

2733
/** Configures hook that sets correct hub on the executing thread. */
2834
@Bean
@@ -34,10 +40,19 @@ public class SentryWebfluxAutoConfiguration {
3440

3541
/** Configures a filter that sets up Sentry {@link io.sentry.Scope} for each request. */
3642
@Bean
43+
@Order(SENTRY_SPRING_FILTER_PRECEDENCE)
3744
public @NotNull SentryWebFilter sentryWebFilter(final @NotNull IHub hub) {
3845
return new SentryWebFilter(hub);
3946
}
4047

48+
@Bean
49+
@Order(SENTRY_SPRING_FILTER_PRECEDENCE + 1)
50+
@Conditional(SentryAutoConfiguration.SentryTracingCondition.class)
51+
@ConditionalOnMissingBean(name = "sentryWebTracingFilter")
52+
public @NotNull SentryWebTracingFilter sentryWebTracingFilter() {
53+
return new SentryWebTracingFilter();
54+
}
55+
4156
/** Configures exception handler that handles unhandled exceptions and sends them to Sentry. */
4257
@Bean
4358
public @NotNull SentryWebExceptionHandler sentryWebExceptionHandler(final @NotNull IHub hub) {

sentry-spring-jakarta/api/sentry-spring-jakarta.api

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,3 +214,8 @@ public final class io/sentry/spring/jakarta/webflux/SentryWebFilterWithThreadLoc
214214
public fun filter (Lorg/springframework/web/server/ServerWebExchange;Lorg/springframework/web/server/WebFilterChain;)Lreactor/core/publisher/Mono;
215215
}
216216

217+
public class io/sentry/spring/jakarta/webflux/SentryWebTracingFilter : org/springframework/web/server/WebFilter {
218+
public fun <init> ()V
219+
public fun filter (Lorg/springframework/web/server/ServerWebExchange;Lorg/springframework/web/server/WebFilterChain;)Lreactor/core/publisher/Mono;
220+
}
221+
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
package io.sentry.spring.jakarta.webflux;
2+
3+
import static io.sentry.spring.jakarta.webflux.AbstractSentryWebFilter.SENTRY_HUB_KEY;
4+
5+
import com.jakewharton.nopen.annotation.Open;
6+
import io.sentry.Baggage;
7+
import io.sentry.BaggageHeader;
8+
import io.sentry.CustomSamplingContext;
9+
import io.sentry.IHub;
10+
import io.sentry.ITransaction;
11+
import io.sentry.Sentry;
12+
import io.sentry.SentryLevel;
13+
import io.sentry.SentryTraceHeader;
14+
import io.sentry.SpanStatus;
15+
import io.sentry.TransactionContext;
16+
import io.sentry.TransactionOptions;
17+
import io.sentry.exception.InvalidSentryTraceHeaderException;
18+
import io.sentry.protocol.TransactionNameSource;
19+
import java.util.List;
20+
import org.jetbrains.annotations.ApiStatus;
21+
import org.jetbrains.annotations.NotNull;
22+
import org.jetbrains.annotations.Nullable;
23+
import org.springframework.http.HttpHeaders;
24+
import org.springframework.http.HttpMethod;
25+
import org.springframework.http.HttpStatusCode;
26+
import org.springframework.http.server.reactive.ServerHttpRequest;
27+
import org.springframework.http.server.reactive.ServerHttpResponse;
28+
import org.springframework.web.server.ServerWebExchange;
29+
import org.springframework.web.server.WebFilter;
30+
import org.springframework.web.server.WebFilterChain;
31+
import reactor.core.publisher.Mono;
32+
33+
@Open
34+
@ApiStatus.Experimental
35+
public class SentryWebTracingFilter implements WebFilter {
36+
37+
private static final String TRANSACTION_OP = "http.server";
38+
39+
@Override
40+
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
41+
final @Nullable Object hubObject = exchange.getAttributes().getOrDefault(SENTRY_HUB_KEY, null);
42+
final @NotNull IHub hub = hubObject == null ? Sentry.getCurrentHub() : (IHub) hubObject;
43+
final @NotNull ServerHttpRequest request = exchange.getRequest();
44+
45+
if (hub.isEnabled() && shouldTraceRequest(hub, request)) {
46+
final @NotNull ITransaction transaction = startTransaction(hub, request);
47+
48+
return chain
49+
.filter(exchange)
50+
.doFinally(
51+
__ -> {
52+
String transactionName = TransactionNameProvider.provideTransactionName(exchange);
53+
if (transactionName != null) {
54+
transaction.setName(transactionName, TransactionNameSource.ROUTE);
55+
transaction.setOperation(TRANSACTION_OP);
56+
}
57+
if (transaction.getStatus() == null) {
58+
final @Nullable ServerHttpResponse response = exchange.getResponse();
59+
if (response != null) {
60+
final @Nullable HttpStatusCode statusCode = response.getStatusCode();
61+
if (statusCode != null) {
62+
transaction.setStatus(SpanStatus.fromHttpStatusCode(statusCode.value()));
63+
}
64+
}
65+
}
66+
transaction.finish();
67+
})
68+
.doOnError(
69+
e -> {
70+
transaction.setStatus(SpanStatus.INTERNAL_ERROR);
71+
transaction.setThrowable(e);
72+
});
73+
} else {
74+
return chain.filter(exchange);
75+
}
76+
}
77+
78+
private boolean shouldTraceRequest(
79+
final @NotNull IHub hub, final @NotNull ServerHttpRequest request) {
80+
return hub.getOptions().isTraceOptionsRequests()
81+
|| !HttpMethod.OPTIONS.equals(request.getMethod());
82+
}
83+
84+
private @NotNull ITransaction startTransaction(
85+
final @NotNull IHub hub, final @NotNull ServerHttpRequest request) {
86+
final @NotNull HttpHeaders headers = request.getHeaders();
87+
final @Nullable List<String> sentryTraceHeaders =
88+
headers.get(SentryTraceHeader.SENTRY_TRACE_HEADER);
89+
final @Nullable List<String> baggageHeaders = headers.get(BaggageHeader.BAGGAGE_HEADER);
90+
final @NotNull String name = request.getMethod() + " " + request.getURI().getPath();
91+
final @NotNull CustomSamplingContext customSamplingContext = new CustomSamplingContext();
92+
customSamplingContext.set("request", request);
93+
94+
final TransactionOptions transactionOptions = new TransactionOptions();
95+
transactionOptions.setCustomSamplingContext(customSamplingContext);
96+
transactionOptions.setBindToScope(true);
97+
98+
if (sentryTraceHeaders != null && sentryTraceHeaders.size() > 0) {
99+
final @NotNull Baggage baggage =
100+
Baggage.fromHeader(baggageHeaders, hub.getOptions().getLogger());
101+
try {
102+
final @NotNull TransactionContext contexts =
103+
TransactionContext.fromSentryTrace(
104+
name,
105+
TransactionNameSource.URL,
106+
TRANSACTION_OP,
107+
new SentryTraceHeader(sentryTraceHeaders.get(0)),
108+
baggage,
109+
null);
110+
111+
return hub.startTransaction(contexts, transactionOptions);
112+
} catch (InvalidSentryTraceHeaderException e) {
113+
hub.getOptions()
114+
.getLogger()
115+
.log(SentryLevel.DEBUG, e, "Failed to parse Sentry trace header: %s", e.getMessage());
116+
}
117+
}
118+
119+
return hub.startTransaction(
120+
new TransactionContext(name, TransactionNameSource.URL, TRANSACTION_OP),
121+
transactionOptions);
122+
}
123+
}

sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/webflux/SentryWebfluxIntegrationTest.kt

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import io.sentry.IHub
55
import io.sentry.ITransportFactory
66
import io.sentry.Sentry
77
import io.sentry.checkEvent
8+
import io.sentry.checkTransaction
89
import io.sentry.transport.ITransport
910
import org.assertj.core.api.Assertions.assertThat
1011
import org.awaitility.kotlin.await
@@ -122,6 +123,22 @@ class SentryWebfluxIntegrationTest {
122123
)
123124
}
124125
}
126+
127+
@Test
128+
fun `sends transaction`() {
129+
testClient.get()
130+
.uri("http://localhost:$port/hello?param=value#top")
131+
.exchange()
132+
.expectStatus()
133+
.isOk
134+
135+
verify(transport).send(
136+
checkTransaction { event ->
137+
assertEquals("GET /hello", event.transaction)
138+
},
139+
anyOrNull()
140+
)
141+
}
125142
}
126143

127144
@SpringBootApplication(exclude = [ReactiveSecurityAutoConfiguration::class, SecurityAutoConfiguration::class])
@@ -148,6 +165,9 @@ open class App {
148165
@Bean
149166
open fun sentryWebExceptionHandler(hub: IHub) = SentryWebExceptionHandler(hub)
150167

168+
@Bean
169+
open fun sentryTracingFilter(hub: IHub) = SentryWebTracingFilter()
170+
151171
@Bean
152172
open fun sentryScheduleHookRegistrar() = ApplicationRunner {
153173
Schedulers.onScheduleHook("sentry", SentryScheduleHook())
@@ -159,6 +179,7 @@ open class App {
159179
it.dsn = "http://key@localhost/proj"
160180
it.setDebug(true)
161181
it.setTransportFactory(transportFactory)
182+
it.enableTracing = true
162183
}
163184
}
164185
}

sentry-spring/api/sentry-spring.api

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,13 @@ public final class io/sentry/spring/webflux/SentryWebExceptionHandler : org/spri
177177
}
178178

179179
public final class io/sentry/spring/webflux/SentryWebFilter : org/springframework/web/server/WebFilter {
180+
public static final field SENTRY_HUB_KEY Ljava/lang/String;
180181
public fun <init> (Lio/sentry/IHub;)V
181182
public fun filter (Lorg/springframework/web/server/ServerWebExchange;Lorg/springframework/web/server/WebFilterChain;)Lreactor/core/publisher/Mono;
182183
}
183184

185+
public class io/sentry/spring/webflux/SentryWebTracingFilter : org/springframework/web/server/WebFilter {
186+
public fun <init> ()V
187+
public fun filter (Lorg/springframework/web/server/ServerWebExchange;Lorg/springframework/web/server/WebFilterChain;)Lreactor/core/publisher/Mono;
188+
}
189+

0 commit comments

Comments
 (0)