Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@

- Reduce excessive CPU usage when serializing breadcrumbs to disk for ANRs ([#4181](https://github.com/getsentry/sentry-java/pull/4181))
- Ensure app start type is set, even when ActivityLifecycleIntegration is not running ([#4250](https://github.com/getsentry/sentry-java/pull/4250))
- Use `SpringServletTransactionNameProvider` as fallback for Spring WebMVC ([#4263](https://github.com/getsentry/sentry-java/pull/4263))
- In certain cases the SDK was not able to provide a transaction name automatically and thus did not finish the transaction for the request.
- We now first try `SpringMvcTransactionNameProvider` which would provide the route as transaction name.
- If that does not return anything, we try `SpringServletTransactionNameProvider` next, which returns the URL of the request.

### Behavioral Changes

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
import io.sentry.spring.jakarta.exception.SentryExceptionParameterAdviceConfiguration;
import io.sentry.spring.jakarta.opentelemetry.SentryOpenTelemetryAgentWithoutAutoInitConfiguration;
import io.sentry.spring.jakarta.opentelemetry.SentryOpenTelemetryNoAgentConfiguration;
import io.sentry.spring.jakarta.tracing.CombinedTransactionNameProvider;
import io.sentry.spring.jakarta.tracing.SentryAdviceConfiguration;
import io.sentry.spring.jakarta.tracing.SentrySpanPointcutConfiguration;
import io.sentry.spring.jakarta.tracing.SentryTracingFilter;
Expand All @@ -42,6 +43,7 @@
import io.sentry.transport.ITransportGate;
import io.sentry.transport.apache.ApacheHttpClientTransportFactory;
import jakarta.servlet.http.HttpServletRequest;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import org.aspectj.lang.ProceedingJoinPoint;
Expand Down Expand Up @@ -342,7 +344,10 @@ static class SentryMvcModeConfig {
@Bean
@ConditionalOnMissingBean(TransactionNameProvider.class)
public @NotNull TransactionNameProvider transactionNameProvider() {
return new SpringMvcTransactionNameProvider();
return new CombinedTransactionNameProvider(
Arrays.asList(
new SpringMvcTransactionNameProvider(),
new SpringServletTransactionNameProvider()));
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
import io.sentry.spring.exception.SentryExceptionParameterAdviceConfiguration;
import io.sentry.spring.opentelemetry.SentryOpenTelemetryAgentWithoutAutoInitConfiguration;
import io.sentry.spring.opentelemetry.SentryOpenTelemetryNoAgentConfiguration;
import io.sentry.spring.tracing.CombinedTransactionNameProvider;
import io.sentry.spring.tracing.SentryAdviceConfiguration;
import io.sentry.spring.tracing.SentrySpanPointcutConfiguration;
import io.sentry.spring.tracing.SentryTracingFilter;
Expand All @@ -41,6 +42,7 @@
import io.sentry.spring.tracing.TransactionNameProvider;
import io.sentry.transport.ITransportGate;
import io.sentry.transport.apache.ApacheHttpClientTransportFactory;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import javax.servlet.http.HttpServletRequest;
Expand Down Expand Up @@ -327,7 +329,10 @@ static class SentryMvcModeConfig {
@Bean
@ConditionalOnMissingBean(TransactionNameProvider.class)
public @NotNull TransactionNameProvider transactionNameProvider() {
return new SpringMvcTransactionNameProvider();
return new CombinedTransactionNameProvider(
Arrays.asList(
new SpringMvcTransactionNameProvider(),
new SpringServletTransactionNameProvider()));
}
}

Expand Down
14 changes: 14 additions & 0 deletions sentry-spring-jakarta/api/sentry-spring-jakarta.api
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,13 @@ public class io/sentry/spring/jakarta/opentelemetry/SentryOpenTelemetryNoAgentCo
public fun sentryOpenTelemetryOptionsConfiguration ()Lio/sentry/Sentry$OptionsConfiguration;
}

public final class io/sentry/spring/jakarta/tracing/CombinedTransactionNameProvider : io/sentry/spring/jakarta/tracing/TransactionNameProvider {
public fun <init> (Ljava/util/List;)V
public fun provideTransactionName (Ljakarta/servlet/http/HttpServletRequest;)Ljava/lang/String;
public fun provideTransactionNameAndSource (Ljakarta/servlet/http/HttpServletRequest;)Lio/sentry/spring/jakarta/tracing/TransactionNameWithSource;
public fun provideTransactionSource ()Lio/sentry/protocol/TransactionNameSource;
}

public class io/sentry/spring/jakarta/tracing/SentryAdviceConfiguration {
public fun <init> ()V
public fun sentrySpanAdvice ()Lorg/aopalliance/aop/Advice;
Expand Down Expand Up @@ -300,9 +307,16 @@ public final class io/sentry/spring/jakarta/tracing/SpringServletTransactionName

public abstract interface class io/sentry/spring/jakarta/tracing/TransactionNameProvider {
public abstract fun provideTransactionName (Ljakarta/servlet/http/HttpServletRequest;)Ljava/lang/String;
public fun provideTransactionNameAndSource (Ljakarta/servlet/http/HttpServletRequest;)Lio/sentry/spring/jakarta/tracing/TransactionNameWithSource;
public fun provideTransactionSource ()Lio/sentry/protocol/TransactionNameSource;
}

public final class io/sentry/spring/jakarta/tracing/TransactionNameWithSource {
public fun <init> (Ljava/lang/String;Lio/sentry/protocol/TransactionNameSource;)V
public fun getTransactionName ()Ljava/lang/String;
public fun getTransactionNameSource ()Lio/sentry/protocol/TransactionNameSource;
}

public abstract class io/sentry/spring/jakarta/webflux/AbstractSentryWebFilter : org/springframework/web/server/WebFilter {
public static final field SENTRY_HUB_KEY Ljava/lang/String;
public static final field SENTRY_SCOPES_KEY Ljava/lang/String;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package io.sentry.spring.jakarta.tracing;

import io.sentry.protocol.TransactionNameSource;
import jakarta.servlet.http.HttpServletRequest;
import java.util.List;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

/**
* Resolves transaction name using {@link HttpServletRequest#getMethod()} and templated route that
* handled the request. To return correct transaction name, it must be used after request is
* processed by {@link org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping}
* where {@link org.springframework.web.servlet.HandlerMapping#BEST_MATCHING_PATTERN_ATTRIBUTE} is
* set.
*/
@ApiStatus.Internal
public final class CombinedTransactionNameProvider implements TransactionNameProvider {

private final @NotNull List<TransactionNameProvider> providers;

public CombinedTransactionNameProvider(final @NotNull List<TransactionNameProvider> providers) {
this.providers = providers;
}

@Override
public @Nullable String provideTransactionName(@NotNull HttpServletRequest request) {
for (TransactionNameProvider provider : providers) {
String transactionName = provider.provideTransactionName(request);
if (transactionName != null) {
return transactionName;
}
}

return null;
}

@Override
@ApiStatus.Internal
public @NotNull TransactionNameSource provideTransactionSource() {
return TransactionNameSource.CUSTOM;
}

@ApiStatus.Internal
@Override
public @NotNull TransactionNameWithSource provideTransactionNameAndSource(
@NotNull HttpServletRequest request) {
for (TransactionNameProvider provider : providers) {
String transactionName = provider.provideTransactionName(request);
if (transactionName != null) {
final @NotNull TransactionNameSource source = provider.provideTransactionSource();
return new TransactionNameWithSource(transactionName, source);
}
}

return new TransactionNameWithSource(null, TransactionNameSource.CUSTOM);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -145,9 +145,11 @@ private void doFilterWithTransaction(
} finally {
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();
final @NotNull TransactionNameWithSource transactionNameWithSource =
transactionNameProvider.provideTransactionNameAndSource(httpRequest);
final @Nullable String transactionName = transactionNameWithSource.getTransactionName();
final @NotNull TransactionNameSource transactionNameSource =
transactionNameWithSource.getTransactionNameSource();
// 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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,12 @@ public interface TransactionNameProvider {
default TransactionNameSource provideTransactionSource() {
return TransactionNameSource.CUSTOM;
}

@NotNull
@ApiStatus.Internal
default TransactionNameWithSource provideTransactionNameAndSource(
final @NotNull HttpServletRequest request) {
return new TransactionNameWithSource(
provideTransactionName(request), provideTransactionSource());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package io.sentry.spring.jakarta.tracing;

import io.sentry.protocol.TransactionNameSource;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

@ApiStatus.Internal
public final class TransactionNameWithSource {
private final @Nullable String transactionName;
private final @NotNull TransactionNameSource transactionNameSource;

public TransactionNameWithSource(
final @Nullable String transactionName,
final @NotNull TransactionNameSource transactionNameSource) {
this.transactionName = transactionName;
this.transactionNameSource = transactionNameSource;
}

public @Nullable String getTransactionName() {
return transactionName;
}

public @NotNull TransactionNameSource getTransactionNameSource() {
return transactionNameSource;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,12 @@ class SentryTracingFilterTest {
request.setAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE, "/product/{id}")
whenever(transactionNameProvider.provideTransactionName(request)).thenReturn("POST /product/{id}")
whenever(transactionNameProvider.provideTransactionSource()).thenReturn(TransactionNameSource.CUSTOM)
whenever(transactionNameProvider.provideTransactionNameAndSource(request)).thenReturn(
TransactionNameWithSource(
"POST /product/{id}",
TransactionNameSource.CUSTOM
)
)
if (sentryTraceHeader != null) {
request.addHeader("sentry-trace", sentryTraceHeader)
whenever(scopes.startTransaction(any(), check<TransactionOptions> { it.isBindToScope })).thenAnswer { SentryTracer(it.arguments[0] as TransactionContext, scopes) }
Expand Down
14 changes: 14 additions & 0 deletions sentry-spring/api/sentry-spring.api
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,13 @@ public class io/sentry/spring/opentelemetry/SentryOpenTelemetryNoAgentConfigurat
public fun sentryOpenTelemetryOptionsConfiguration ()Lio/sentry/Sentry$OptionsConfiguration;
}

public final class io/sentry/spring/tracing/CombinedTransactionNameProvider : io/sentry/spring/tracing/TransactionNameProvider {
public fun <init> (Ljava/util/List;)V
public fun provideTransactionName (Ljavax/servlet/http/HttpServletRequest;)Ljava/lang/String;
public fun provideTransactionNameAndSource (Ljavax/servlet/http/HttpServletRequest;)Lio/sentry/spring/tracing/TransactionNameWithSource;
public fun provideTransactionSource ()Lio/sentry/protocol/TransactionNameSource;
}

public class io/sentry/spring/tracing/SentryAdviceConfiguration {
public fun <init> ()V
public fun sentrySpanAdvice ()Lorg/aopalliance/aop/Advice;
Expand Down Expand Up @@ -291,9 +298,16 @@ public final class io/sentry/spring/tracing/SpringServletTransactionNameProvider

public abstract interface class io/sentry/spring/tracing/TransactionNameProvider {
public abstract fun provideTransactionName (Ljavax/servlet/http/HttpServletRequest;)Ljava/lang/String;
public fun provideTransactionNameAndSource (Ljavax/servlet/http/HttpServletRequest;)Lio/sentry/spring/tracing/TransactionNameWithSource;
public fun provideTransactionSource ()Lio/sentry/protocol/TransactionNameSource;
}

public final class io/sentry/spring/tracing/TransactionNameWithSource {
public fun <init> (Ljava/lang/String;Lio/sentry/protocol/TransactionNameSource;)V
public fun getTransactionName ()Ljava/lang/String;
public fun getTransactionNameSource ()Lio/sentry/protocol/TransactionNameSource;
}

public class io/sentry/spring/webflux/SentryRequestResolver {
public fun <init> (Lio/sentry/IScopes;)V
public fun resolveSentryRequest (Lorg/springframework/http/server/reactive/ServerHttpRequest;)Lio/sentry/protocol/Request;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package io.sentry.spring.tracing;

import io.sentry.protocol.TransactionNameSource;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

/**
* Resolves transaction name using {@link HttpServletRequest#getMethod()} and templated route that
* handled the request. To return correct transaction name, it must be used after request is
* processed by {@link org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping}
* where {@link org.springframework.web.servlet.HandlerMapping#BEST_MATCHING_PATTERN_ATTRIBUTE} is
* set.
*/
@ApiStatus.Internal
public final class CombinedTransactionNameProvider implements TransactionNameProvider {

private final @NotNull List<TransactionNameProvider> providers;
private @Nullable TransactionNameSource source = null;

public CombinedTransactionNameProvider(final @NotNull List<TransactionNameProvider> providers) {
this.providers = providers;
}

@Override
public @Nullable String provideTransactionName(final @NotNull HttpServletRequest request) {
for (TransactionNameProvider provider : providers) {
String transactionName = provider.provideTransactionName(request);
if (transactionName != null) {
source = provider.provideTransactionSource();
return transactionName;
}
}

return null;
}

@Override
@ApiStatus.Internal
public @NotNull TransactionNameSource provideTransactionSource() {
final @Nullable TransactionNameSource tmpSource = source;
return tmpSource == null ? TransactionNameSource.CUSTOM : tmpSource;
}

@ApiStatus.Internal
@Override
public @NotNull TransactionNameWithSource provideTransactionNameAndSource(
@NotNull HttpServletRequest request) {
for (TransactionNameProvider provider : providers) {
String transactionName = provider.provideTransactionName(request);
if (transactionName != null) {
final @NotNull TransactionNameSource source = provider.provideTransactionSource();
return new TransactionNameWithSource(transactionName, source);
}
}

return new TransactionNameWithSource(null, TransactionNameSource.CUSTOM);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -144,9 +144,11 @@ private void doFilterWithTransaction(
} finally {
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();
final @NotNull TransactionNameWithSource transactionNameWithSource =
transactionNameProvider.provideTransactionNameAndSource(httpRequest);
final @Nullable String transactionName = transactionNameWithSource.getTransactionName();
final @NotNull TransactionNameSource transactionNameSource =
transactionNameWithSource.getTransactionNameSource();
// 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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,12 @@ public interface TransactionNameProvider {
default TransactionNameSource provideTransactionSource() {
return TransactionNameSource.CUSTOM;
}

@NotNull
@ApiStatus.Internal
default TransactionNameWithSource provideTransactionNameAndSource(
final @NotNull HttpServletRequest request) {
return new TransactionNameWithSource(
provideTransactionName(request), provideTransactionSource());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package io.sentry.spring.tracing;

import io.sentry.protocol.TransactionNameSource;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

@ApiStatus.Internal
public final class TransactionNameWithSource {
private final @Nullable String transactionName;
private final @NotNull TransactionNameSource transactionNameSource;

public TransactionNameWithSource(
final @Nullable String transactionName,
final @NotNull TransactionNameSource transactionNameSource) {
this.transactionName = transactionName;
this.transactionNameSource = transactionNameSource;
}

public @Nullable String getTransactionName() {
return transactionName;
}

public @NotNull TransactionNameSource getTransactionNameSource() {
return transactionNameSource;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ class SentryTracingFilterTest {
request.setAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE, "/product/{id}")
whenever(transactionNameProvider.provideTransactionName(request)).thenReturn("POST /product/{id}")
whenever(transactionNameProvider.provideTransactionSource()).thenReturn(TransactionNameSource.CUSTOM)
whenever(transactionNameProvider.provideTransactionNameAndSource(request)).thenReturn(TransactionNameWithSource("POST /product/{id}", TransactionNameSource.CUSTOM))
if (sentryTraceHeader != null) {
request.addHeader("sentry-trace", sentryTraceHeader)
whenever(scopes.startTransaction(any(), check<TransactionOptions> { it.isBindToScope })).thenAnswer { SentryTracer(it.arguments[0] as TransactionContext, scopes) }
Expand Down
Loading