Skip to content
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
- Added `enableTraceIdGeneration` to the AndroidOptions. This allows Hybrid SDKs to "freeze" and control the trace and connect errors on different layers of the application ([4188](https://github.com/getsentry/sentry-java/pull/4188))
- Move to a single NetworkCallback listener to reduce number of IPC calls on Android ([#4164](https://github.com/getsentry/sentry-java/pull/4164))
- Add GraphQL Apollo Kotlin 4 integration ([#4166](https://github.com/getsentry/sentry-java/pull/4166))
- Add support for async dispatch requests to Spring Boot 2 and 3 ([#3983](https://github.com/getsentry/sentry-java/pull/3983))
- To enable it, please set `sentry.keep-transactions-open-for-async-responses=true` in `application.properties` or `sentry.keepTransactionsOpenForAsyncResponses: true` in `application.yml`
- Add constructor to JUL `SentryHandler` for disabling external config ([#4208](https://github.com/getsentry/sentry-java/pull/4208))

### Fixes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,12 @@ public class io/sentry/spring/boot/jakarta/SentryProperties : io/sentry/SentryOp
public fun getReactive ()Lio/sentry/spring/boot/jakarta/SentryProperties$Reactive;
public fun getUserFilterOrder ()Ljava/lang/Integer;
public fun isEnableAotCompatibility ()Z
public fun isKeepTransactionsOpenForAsyncResponses ()Z
public fun isUseGitCommitIdAsRelease ()Z
public fun setEnableAotCompatibility (Z)V
public fun setExceptionResolverOrder (I)V
public fun setGraphql (Lio/sentry/spring/boot/jakarta/SentryProperties$Graphql;)V
public fun setKeepTransactionsOpenForAsyncResponses (Z)V
public fun setLogging (Lio/sentry/spring/boot/jakarta/SentryProperties$Logging;)V
public fun setReactive (Lio/sentry/spring/boot/jakarta/SentryProperties$Reactive;)V
public fun setUseGitCommitIdAsRelease (Z)V
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -312,9 +312,14 @@ static class SentrySecurityConfiguration {
@ConditionalOnMissingBean(name = "sentryTracingFilter")
public FilterRegistrationBean<SentryTracingFilter> sentryTracingFilter(
final @NotNull IScopes scopes,
final @NotNull TransactionNameProvider transactionNameProvider) {
final @NotNull TransactionNameProvider transactionNameProvider,
final @NotNull SentryProperties sentryProperties) {
FilterRegistrationBean<SentryTracingFilter> filter =
new FilterRegistrationBean<>(new SentryTracingFilter(scopes, transactionNameProvider));
new FilterRegistrationBean<>(
new SentryTracingFilter(
scopes,
transactionNameProvider,
sentryProperties.isKeepTransactionsOpenForAsyncResponses()));
filter.setOrder(SENTRY_SPRING_FILTER_PRECEDENCE + 1); // must run after SentrySpringFilter
return filter;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.slf4j.event.Level;
Expand All @@ -28,6 +29,8 @@ public class SentryProperties extends SentryOptions {
*/
private @Nullable Integer userFilterOrder;

@ApiStatus.Experimental private boolean keepTransactionsOpenForAsyncResponses = false;

/** Logging framework integration properties. */
private @NotNull Logging logging = new Logging();

Expand Down Expand Up @@ -104,6 +107,15 @@ public void setEnableAotCompatibility(boolean enableAotCompatibility) {
this.enableAotCompatibility = enableAotCompatibility;
}

public boolean isKeepTransactionsOpenForAsyncResponses() {
return keepTransactionsOpenForAsyncResponses;
}

public void setKeepTransactionsOpenForAsyncResponses(
boolean keepTransactionsOpenForAsyncResponses) {
this.keepTransactionsOpenForAsyncResponses = keepTransactionsOpenForAsyncResponses;
}

public @NotNull Graphql getGraphql() {
return graphql;
}
Expand Down
2 changes: 2 additions & 0 deletions sentry-spring-boot/api/sentry-spring-boot.api
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,11 @@ public class io/sentry/spring/boot/SentryProperties : io/sentry/SentryOptions {
public fun getGraphql ()Lio/sentry/spring/boot/SentryProperties$Graphql;
public fun getLogging ()Lio/sentry/spring/boot/SentryProperties$Logging;
public fun getUserFilterOrder ()Ljava/lang/Integer;
public fun isKeepTransactionsOpenForAsyncResponses ()Z
public fun isUseGitCommitIdAsRelease ()Z
public fun setExceptionResolverOrder (I)V
public fun setGraphql (Lio/sentry/spring/boot/SentryProperties$Graphql;)V
public fun setKeepTransactionsOpenForAsyncResponses (Z)V
public fun setLogging (Lio/sentry/spring/boot/SentryProperties$Logging;)V
public fun setUseGitCommitIdAsRelease (Z)V
public fun setUserFilterOrder (Ljava/lang/Integer;)V
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -297,9 +297,14 @@ static class SentrySecurityConfiguration {
@ConditionalOnMissingBean(name = "sentryTracingFilter")
public FilterRegistrationBean<SentryTracingFilter> sentryTracingFilter(
final @NotNull IScopes scopes,
final @NotNull TransactionNameProvider transactionNameProvider) {
final @NotNull TransactionNameProvider transactionNameProvider,
final @NotNull SentryProperties sentryProperties) {
FilterRegistrationBean<SentryTracingFilter> filter =
new FilterRegistrationBean<>(new SentryTracingFilter(scopes, transactionNameProvider));
new FilterRegistrationBean<>(
new SentryTracingFilter(
scopes,
transactionNameProvider,
sentryProperties.isKeepTransactionsOpenForAsyncResponses()));
filter.setOrder(SENTRY_SPRING_FILTER_PRECEDENCE + 1); // must run after SentrySpringFilter
return filter;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.slf4j.event.Level;
Expand All @@ -28,6 +29,8 @@ public class SentryProperties extends SentryOptions {
*/
private @Nullable Integer userFilterOrder;

@ApiStatus.Experimental private boolean keepTransactionsOpenForAsyncResponses = false;

/** Logging framework integration properties. */
private @NotNull Logging logging = new Logging();

Expand Down Expand Up @@ -70,6 +73,15 @@ public void setUserFilterOrder(final @Nullable Integer userFilterOrder) {
this.userFilterOrder = userFilterOrder;
}

public boolean isKeepTransactionsOpenForAsyncResponses() {
return keepTransactionsOpenForAsyncResponses;
}

public void setKeepTransactionsOpenForAsyncResponses(
boolean keepTransactionsOpenForAsyncResponses) {
this.keepTransactionsOpenForAsyncResponses = keepTransactionsOpenForAsyncResponses;
}

public @NotNull Logging getLogging() {
return logging;
}
Expand Down
2 changes: 2 additions & 0 deletions sentry-spring-jakarta/api/sentry-spring-jakarta.api
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,9 @@ public class io/sentry/spring/jakarta/tracing/SentryTracingFilter : org/springfr
public fun <init> ()V
public fun <init> (Lio/sentry/IScopes;)V
public fun <init> (Lio/sentry/IScopes;Lio/sentry/spring/jakarta/tracing/TransactionNameProvider;)V
public fun <init> (Lio/sentry/IScopes;Lio/sentry/spring/jakarta/tracing/TransactionNameProvider;Z)V
protected fun doFilterInternal (Ljakarta/servlet/http/HttpServletRequest;Ljakarta/servlet/http/HttpServletResponse;Ljakarta/servlet/FilterChain;)V
protected fun shouldNotFilterAsyncDispatch ()Z
}

public abstract interface annotation class io/sentry/spring/jakarta/tracing/SentryTransaction : java/lang/annotation/Annotation {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,11 @@ public class SentryTracingFilter extends OncePerRequestFilter {
private static final String TRANSACTION_OP = "http.server";

private static final String TRACE_ORIGIN = "auto.http.spring_jakarta.webmvc";
private static final String TRANSACTION_ATTR = "sentry.transaction";

private final @NotNull TransactionNameProvider transactionNameProvider;
private final @NotNull IScopes scopes;
private final boolean isAsyncSupportEnabled;

/**
* Creates filter that resolves transaction name using {@link SpringMvcTransactionNameProvider}.
Expand All @@ -63,28 +65,52 @@ public SentryTracingFilter() {
public SentryTracingFilter(
final @NotNull IScopes scopes,
final @NotNull TransactionNameProvider transactionNameProvider) {
this(scopes, transactionNameProvider, false);
}

/**
* Creates filter that resolves transaction name using transaction name provider given by
* parameter.
*
* @param scopes - the scopes
* @param transactionNameProvider - transaction name provider.
* @param isAsyncSupportEnabled - whether transactions should be kept open until async handling is
* done
*/
public SentryTracingFilter(
final @NotNull IScopes scopes,
final @NotNull TransactionNameProvider transactionNameProvider,
final boolean isAsyncSupportEnabled) {
this.scopes = Objects.requireNonNull(scopes, "Scopes are required");
this.transactionNameProvider =
Objects.requireNonNull(transactionNameProvider, "transactionNameProvider is required");
this.isAsyncSupportEnabled = isAsyncSupportEnabled;
}

public SentryTracingFilter(final @NotNull IScopes scopes) {
this(scopes, new SpringMvcTransactionNameProvider());
}

@Override
protected boolean shouldNotFilterAsyncDispatch() {
return !isAsyncSupportEnabled;
}

@Override
protected void doFilterInternal(
final @NotNull HttpServletRequest httpRequest,
final @NotNull HttpServletResponse httpResponse,
final @NotNull FilterChain filterChain)
throws ServletException, IOException {
if (scopes.isEnabled() && !isIgnored()) {
final @Nullable String sentryTraceHeader =
httpRequest.getHeader(SentryTraceHeader.SENTRY_TRACE_HEADER);
final @Nullable List<String> baggageHeader =
Collections.list(httpRequest.getHeaders(BaggageHeader.BAGGAGE_HEADER));
final @Nullable TransactionContext transactionContext =
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change would break existing functionality. We would no longer be parsing and propagating incoming tracing information or create a new trace in case there's no incoming info. This would break distributed tracing, which allows you to track execution from e.g. an App across multiple servers.

scopes.continueTrace(sentryTraceHeader, baggageHeader);
@Nullable TransactionContext transactionContext = null;
if (shouldContinueTrace(httpRequest)) {
final @Nullable String sentryTraceHeader =
httpRequest.getHeader(SentryTraceHeader.SENTRY_TRACE_HEADER);
final @Nullable List<String> baggageHeader =
Collections.list(httpRequest.getHeaders(BaggageHeader.BAGGAGE_HEADER));
transactionContext = scopes.continueTrace(sentryTraceHeader, baggageHeader);
}
if (scopes.getOptions().isTracingEnabled() && shouldTraceRequest(httpRequest)) {
doFilterWithTransaction(httpRequest, httpResponse, filterChain, transactionContext);
} else {
Expand All @@ -105,35 +131,85 @@ private void doFilterWithTransaction(
FilterChain filterChain,
final @Nullable TransactionContext transactionContext)
throws IOException, ServletException {
// at this stage we are not able to get real transaction name
final ITransaction transaction = startTransaction(httpRequest, transactionContext);
final @Nullable ITransaction transaction =
getOrStartTransaction(httpRequest, transactionContext);

try {
filterChain.doFilter(httpRequest, httpResponse);
} catch (Throwable e) {
// exceptions that are not handled by Spring
transaction.setStatus(SpanStatus.INTERNAL_ERROR);
if (transaction != null) {
// exceptions that are not handled by Spring
transaction.setStatus(SpanStatus.INTERNAL_ERROR);
}
throw e;
} finally {
// after all filters run, templated path pattern is available in request attribute
final String transactionName = transactionNameProvider.provideTransactionName(httpRequest);
final TransactionNameSource transactionNameSource =
transactionNameProvider.provideTransactionSource();
// if transaction name is not resolved, the request has not been processed by a controller
// and we should not report it to Sentry
if (transactionName != null) {
transaction.setName(transactionName, transactionNameSource);
transaction.setOperation(TRANSACTION_OP);
// if exception has been thrown, transaction status is already set to INTERNAL_ERROR, and
// httpResponse.getStatus() returns 200.
if (transaction.getStatus() == null) {
transaction.setStatus(SpanStatus.fromHttpStatusCode(httpResponse.getStatus()));
if (shouldFinishTransaction(httpRequest) && transaction != null) {
// after all filters run, templated path pattern is available in request attribute
final String transactionName = transactionNameProvider.provideTransactionName(httpRequest);
final TransactionNameSource transactionNameSource =
transactionNameProvider.provideTransactionSource();
// if transaction name is not resolved, the request has not been processed by a controller
// and we should not report it to Sentry
if (transactionName != null) {
transaction.setName(transactionName, transactionNameSource);
transaction.setOperation(TRANSACTION_OP);
// if exception has been thrown, transaction status is already set to INTERNAL_ERROR, and
// httpResponse.getStatus() returns 200.
if (transaction.getStatus() == null) {
transaction.setStatus(SpanStatus.fromHttpStatusCode(httpResponse.getStatus()));
}
transaction.finish();
}
transaction.finish();
}
}
}

private ITransaction getOrStartTransaction(
final @NotNull HttpServletRequest httpRequest,
final @Nullable TransactionContext transactionContext) {
if (isAsyncDispatch(httpRequest)) {
// second invocation of this filter for the same async request already has the transaction
// in the attributes
return (ITransaction) httpRequest.getAttribute(TRANSACTION_ATTR);
} else {
// at this stage we are not able to get real transaction name
final @NotNull ITransaction transaction = startTransaction(httpRequest, transactionContext);
if (shouldStoreTransactionForAsyncProcessing()) {
httpRequest.setAttribute(TRANSACTION_ATTR, transaction);
}
return transaction;
}
}

/**
* Returns false if an async request is being dispatched (second invocation of the filter for the
* same async request).
*
* <p>Returns true if not an async request or this is the first invocation of the filter for the
* same async request
*/
private boolean shouldContinueTrace(HttpServletRequest httpRequest) {
return !isAsyncSupportEnabled || !isAsyncDispatch(httpRequest);
}

private boolean shouldStoreTransactionForAsyncProcessing() {
return isAsyncSupportEnabled;
}

/**
* Returns false if async request handling has only been started but not yet finished (first
* invocation of this filter for the same async request).
*
* <p>Returns true if not an async request or async request handling has finished (second
* invocation of this filter for the same async request)
*
* <p>Note: isAsyncStarted changes its return value after filterChain.doFilter() of the first
* async invocation
*/
private boolean shouldFinishTransaction(HttpServletRequest httpRequest) {
return !isAsyncSupportEnabled || !isAsyncStarted(httpRequest);
}

private boolean shouldTraceRequest(final @NotNull HttpServletRequest request) {
return scopes.getOptions().isTraceOptionsRequests()
|| !HttpMethod.OPTIONS.name().equals(request.getMethod());
Expand Down
Loading
Loading