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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
- Add `@SentryCaptureExceptionParameter` annotation which captures exceptions passed into an annotated method ([#2764](https://github.com/getsentry/sentry-java/pull/2764))
- This can be used to replace `Sentry.captureException` calls in `@ExceptionHandler` of a `@ControllerAdvice`
- Add `ServerWebExchange` to `Hint` for WebFlux as `WEBFLUX_EXCEPTION_HANDLER_EXCHANGE` ([#2977](https://github.com/getsentry/sentry-java/pull/2977))
- Allow filtering GraphQL errors ([#2967](https://github.com/getsentry/sentry-java/pull/2967))
- This list can be set directly when calling the constructor of `SentryInstrumentation`
- For Spring Boot it can also be set in `application.properties` as `sentry.graphql.ignored-error-types=SOME_ERROR,ANOTHER_ERROR`

### Dependencies

Expand Down
3 changes: 2 additions & 1 deletion sentry-graphql/api/sentry-graphql.api
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,9 @@ public final class io/sentry/graphql/SentryInstrumentation : graphql/execution/i
public fun <init> (Lio/sentry/IHub;)V
public fun <init> (Lio/sentry/IHub;Lio/sentry/graphql/SentryInstrumentation$BeforeSpanCallback;)V
public fun <init> (Lio/sentry/graphql/SentryInstrumentation$BeforeSpanCallback;)V
public fun <init> (Lio/sentry/graphql/SentryInstrumentation$BeforeSpanCallback;Lio/sentry/graphql/SentrySubscriptionHandler;Lio/sentry/graphql/ExceptionReporter;)V
public fun <init> (Lio/sentry/graphql/SentryInstrumentation$BeforeSpanCallback;Lio/sentry/graphql/SentrySubscriptionHandler;Lio/sentry/graphql/ExceptionReporter;Ljava/util/List;)V
public fun <init> (Lio/sentry/graphql/SentryInstrumentation$BeforeSpanCallback;Lio/sentry/graphql/SentrySubscriptionHandler;Z)V
public fun <init> (Lio/sentry/graphql/SentryInstrumentation$BeforeSpanCallback;Lio/sentry/graphql/SentrySubscriptionHandler;ZLjava/util/List;)V
public fun <init> (Lio/sentry/graphql/SentrySubscriptionHandler;Z)V
public fun beginExecuteOperation (Lgraphql/execution/instrumentation/parameters/InstrumentationExecuteOperationParameters;)Lgraphql/execution/instrumentation/InstrumentationContext;
public fun beginExecution (Lgraphql/execution/instrumentation/parameters/InstrumentationExecutionParameters;)Lgraphql/execution/instrumentation/InstrumentationContext;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import io.sentry.SentryIntegrationPackageStorage;
import io.sentry.SpanStatus;
import io.sentry.util.StringUtils;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
Expand All @@ -52,6 +53,8 @@ public final class SentryInstrumentation extends SimpleInstrumentation {

private final @NotNull ExceptionReporter exceptionReporter;

private final @NotNull List<String> ignoredErrorTypes;

/**
* @deprecated please use a constructor that takes a {@link SentrySubscriptionHandler} instead.
*/
Expand Down Expand Up @@ -104,17 +107,41 @@ public SentryInstrumentation(
this(
beforeSpan,
subscriptionHandler,
new ExceptionReporter(captureRequestBodyForNonSubscriptions));
new ExceptionReporter(captureRequestBodyForNonSubscriptions),
new ArrayList<>());
}

/**
* @param beforeSpan callback when a span is created
* @param subscriptionHandler can report subscription errors
* @param captureRequestBodyForNonSubscriptions false if request bodies should not be captured by
* this integration for query and mutation operations. This can be used to prevent unnecessary
* work by not adding the request body when another integration will add it anyways, as is the
* case with our spring integration for WebMVC.
* @param ignoredErrorTypes list of error types that should not be captured and sent to Sentry
*/
public SentryInstrumentation(
final @Nullable BeforeSpanCallback beforeSpan,
final @NotNull SentrySubscriptionHandler subscriptionHandler,
final boolean captureRequestBodyForNonSubscriptions,
final @NotNull List<String> ignoredErrorTypes) {
this(
beforeSpan,
subscriptionHandler,
new ExceptionReporter(captureRequestBodyForNonSubscriptions),
ignoredErrorTypes);
}

@TestOnly
public SentryInstrumentation(
final @Nullable BeforeSpanCallback beforeSpan,
final @NotNull SentrySubscriptionHandler subscriptionHandler,
final @NotNull ExceptionReporter exceptionReporter) {
final @NotNull ExceptionReporter exceptionReporter,
final @NotNull List<String> ignoredErrorTypes) {
this.beforeSpan = beforeSpan;
this.subscriptionHandler = subscriptionHandler;
this.exceptionReporter = exceptionReporter;
this.ignoredErrorTypes = ignoredErrorTypes;
SentryIntegrationPackageStorage.getInstance().addIntegration("GraphQL");
SentryIntegrationPackageStorage.getInstance()
.addPackage("maven:io.sentry:sentry-graphql", BuildConfig.VERSION_NAME);
Expand Down Expand Up @@ -171,11 +198,8 @@ public CompletableFuture<ExecutionResult> instrumentExecutionResult(
final @NotNull List<GraphQLError> errors = result.getErrors();
if (errors != null) {
for (GraphQLError error : errors) {
// not capturing INTERNAL_ERRORS as they should be reported via graphQlContext
// above
String errorType = getErrorType(error);
if (errorType == null
|| !ERROR_TYPES_HANDLED_BY_DATA_FETCHERS.contains(errorType)) {
if (!isIgnored(errorType)) {
exceptionReporter.captureThrowable(
new RuntimeException(error.getMessage()),
new ExceptionReporter.ExceptionDetails(
Expand All @@ -195,6 +219,17 @@ public CompletableFuture<ExecutionResult> instrumentExecutionResult(
});
}

private boolean isIgnored(final @Nullable String errorType) {
if (errorType == null) {
return false;
}

// not capturing INTERNAL_ERRORS as they should be reported via graphQlContext above
// also not capturing error types explicitly ignored by users
return ERROR_TYPES_HANDLED_BY_DATA_FETCHERS.contains(errorType)
|| ignoredErrorTypes.contains(errorType);
}

private @Nullable String getErrorType(final @Nullable GraphQLError error) {
if (error == null) {
return null;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.sentry.graphql

import graphql.ErrorClassification
import graphql.ErrorType
import graphql.ExecutionInput
import graphql.ExecutionResultImpl
Expand Down Expand Up @@ -68,7 +69,7 @@ class SentryInstrumentationAnotherTest {
val query = """query greeting(name: "somename")"""
val variables = mapOf("variableA" to "value a")

fun getSut(isTransactionActive: Boolean = true, operation: OperationDefinition.Operation = OperationDefinition.Operation.QUERY, graphQLContextParam: Map<Any?, Any?>? = null, addTransactionToTracingState: Boolean = true): SentryInstrumentation {
fun getSut(isTransactionActive: Boolean = true, operation: OperationDefinition.Operation = OperationDefinition.Operation.QUERY, graphQLContextParam: Map<Any?, Any?>? = null, addTransactionToTracingState: Boolean = true, ignoredErrors: List<String> = emptyList()): SentryInstrumentation {
whenever(hub.options).thenReturn(SentryOptions())
activeSpan = SentryTracer(TransactionContext("name", "op"), hub)

Expand All @@ -86,7 +87,7 @@ class SentryInstrumentationAnotherTest {
exceptionReporter = mock<ExceptionReporter>()
subscriptionHandler = mock<SentrySubscriptionHandler>()
whenever(subscriptionHandler.onSubscriptionResult(any(), any(), any(), any())).thenReturn("result modified by subscription handler")
val instrumentation = SentryInstrumentation(null, subscriptionHandler, exceptionReporter)
val instrumentation = SentryInstrumentation(null, subscriptionHandler, exceptionReporter, ignoredErrors)
dataFetcher = mock<DataFetcher<Any?>>()
whenever(dataFetcher.get(any())).thenReturn("raw result")
graphQLContext = GraphQLContext.newContext()
Expand Down Expand Up @@ -325,6 +326,19 @@ class SentryInstrumentationAnotherTest {
assertSame(executionResult, result)
}

@Test
fun `does not invoke exceptionReporter for ignored errors`() {
val instrumentation = fixture.getSut(ignoredErrors = listOf("SOME_ERROR"))
val executionResult = ExecutionResultImpl.newExecutionResult()
.data("raw result")
.addError(GraphqlErrorException.newErrorException().message("exception message").errorClassification(SomeErrorClassification.SOME_ERROR).build())
.build()
val resultFuture = instrumentation.instrumentExecutionResult(executionResult, fixture.instrumentationExecutionParameters)
verify(fixture.exceptionReporter, never()).captureThrowable(any(), any(), any())
val result = resultFuture.get()
assertSame(executionResult, result)
}

@Test
fun `never invokes exceptionReporter if no errors`() {
val instrumentation = fixture.getSut()
Expand All @@ -343,4 +357,8 @@ class SentryInstrumentationAnotherTest {
}

data class Show(val id: Int)

enum class SomeErrorClassification : ErrorClassification {
SOME_ERROR;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ class SentryInstrumentationTest {
val subscriptionHandler = mock<SentrySubscriptionHandler>()
whenever(subscriptionHandler.onSubscriptionResult(any(), any(), any(), any())).thenReturn("result modified by subscription handler")
val operation = OperationDefinition.Operation.SUBSCRIPTION
val instrumentation = SentryInstrumentation(null, subscriptionHandler, exceptionReporter)
val instrumentation = SentryInstrumentation(null, subscriptionHandler, exceptionReporter, emptyList())
val dataFetcher = mock<DataFetcher<Any?>>()
whenever(dataFetcher.get(any())).thenReturn("raw result")
val graphQLContext = GraphQLContext.newContext().build()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ sentry.traces-sample-rate=1.0
sentry.enable-tracing=true
sentry.ignored-checkins=ignored_monitor_slug_1,ignored_monitor_slug_2
sentry.debug=true
sentry.graphql.ignored-error-types=SOME_ERROR,ANOTHER_ERROR
in-app-includes="io.sentry.samples"

# Uncomment and set to true to enable aot compatibility
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ sentry.traces-sample-rate=1.0
sentry.enable-tracing=true
sentry.ignored-checkins=ignored_monitor_slug_1,ignored_monitor_slug_2
sentry.debug=true
sentry.graphql.ignored-error-types=SOME_ERROR,ANOTHER_ERROR
in-app-includes="io.sentry.samples"

# Database configuration
Expand Down
16 changes: 16 additions & 0 deletions sentry-spring-boot-jakarta/api/sentry-spring-boot-jakarta.api
Original file line number Diff line number Diff line change
Expand Up @@ -21,19 +21,27 @@ public class io/sentry/spring/boot/jakarta/SentryLogbackAppenderAutoConfiguratio
public class io/sentry/spring/boot/jakarta/SentryProperties : io/sentry/SentryOptions {
public fun <init> ()V
public fun getExceptionResolverOrder ()I
public fun getGraphql ()Lio/sentry/spring/boot/jakarta/SentryProperties$Graphql;
public fun getLogging ()Lio/sentry/spring/boot/jakarta/SentryProperties$Logging;
public fun getReactive ()Lio/sentry/spring/boot/jakarta/SentryProperties$Reactive;
public fun getUserFilterOrder ()Ljava/lang/Integer;
public fun isEnableAotCompatibility ()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 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
public fun setUserFilterOrder (Ljava/lang/Integer;)V
}

public class io/sentry/spring/boot/jakarta/SentryProperties$Graphql {
public fun <init> ()V
public fun getIgnoredErrorTypes ()Ljava/util/List;
public fun setIgnoredErrorTypes (Ljava/util/List;)V
}

public class io/sentry/spring/boot/jakarta/SentryProperties$Logging {
public fun <init> ()V
public fun getLoggers ()Ljava/util/List;
Expand All @@ -57,3 +65,11 @@ public class io/sentry/spring/boot/jakarta/SentryWebfluxAutoConfiguration {
public fun sentryWebExceptionHandler (Lio/sentry/IHub;)Lio/sentry/spring/jakarta/webflux/SentryWebExceptionHandler;
}

public class io/sentry/spring/boot/jakarta/graphql/SentryGraphqlAutoConfiguration {
public fun <init> ()V
public fun exceptionResolverAdapter ()Lio/sentry/spring/jakarta/graphql/SentryDataFetcherExceptionResolverAdapter;
public fun graphqlBeanPostProcessor ()Lio/sentry/spring/jakarta/graphql/SentryGraphqlBeanPostProcessor;
public fun sourceBuilderCustomizerWebflux (Lio/sentry/spring/boot/jakarta/SentryProperties;)Lorg/springframework/boot/autoconfigure/graphql/GraphQlSourceBuilderCustomizer;
public fun sourceBuilderCustomizerWebmvc (Lio/sentry/spring/boot/jakarta/SentryProperties;)Lorg/springframework/boot/autoconfigure/graphql/GraphQlSourceBuilderCustomizer;
}

Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import io.sentry.opentelemetry.OpenTelemetryLinkErrorEventProcessor;
import io.sentry.protocol.SdkVersion;
import io.sentry.quartz.SentryJobListener;
import io.sentry.spring.boot.jakarta.graphql.SentryGraphqlAutoConfiguration;
import io.sentry.spring.jakarta.ContextTagsEventProcessor;
import io.sentry.spring.jakarta.SentryExceptionResolver;
import io.sentry.spring.jakarta.SentryRequestResolver;
Expand All @@ -27,7 +28,6 @@
import io.sentry.spring.jakarta.checkin.SentryQuartzConfiguration;
import io.sentry.spring.jakarta.exception.SentryCaptureExceptionParameterPointcutConfiguration;
import io.sentry.spring.jakarta.exception.SentryExceptionParameterAdviceConfiguration;
import io.sentry.spring.jakarta.graphql.SentryGraphqlConfiguration;
import io.sentry.spring.jakarta.tracing.SentryAdviceConfiguration;
import io.sentry.spring.jakarta.tracing.SentrySpanPointcutConfiguration;
import io.sentry.spring.jakarta.tracing.SentryTracingFilter;
Expand Down Expand Up @@ -166,7 +166,7 @@ static class OpenTelemetryLinkErrorEventProcessorConfiguration {
}

@Configuration(proxyBeanMethods = false)
@Import(SentryGraphqlConfiguration.class)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can the SentryGraphqlConfiguration class be deleted, or is it still of use?

Copy link
Member

Choose a reason for hiding this comment

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

What's the reason why we didn't update the SentryGraphqlConfiguration class directly?
What does it change for someone manually using the SentryGraphqlConfiguration?
genuinely asking as i'm not so much into backend

Copy link
Member Author

Choose a reason for hiding this comment

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

I kept sentry-spring/SentryGraphqlConfiguration for people using Spring (not Boot) and I wasn't able to update because SentryProperties lives in sentry-spring-boot so I decided to simply duplicate the config and use the one in sentry-spring-boot for auto config of Spring Boot. LMK if you can think of a simpler way to achieve what we want.

@Import(SentryGraphqlAutoConfiguration.class)
@Open
@ConditionalOnClass({
SentryGraphqlExceptionHandler.class,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.jakewharton.nopen.annotation.Open;
import io.sentry.SentryOptions;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.jetbrains.annotations.NotNull;
Expand Down Expand Up @@ -40,6 +41,9 @@ public class SentryProperties extends SentryOptions {
*/
private boolean enableAotCompatibility = false;

/** Graphql integration properties. */
private @NotNull Graphql graphql = new Graphql();

public boolean isUseGitCommitIdAsRelease() {
return useGitCommitIdAsRelease;
}
Expand Down Expand Up @@ -100,6 +104,14 @@ public void setEnableAotCompatibility(boolean enableAotCompatibility) {
this.enableAotCompatibility = enableAotCompatibility;
}

public @NotNull Graphql getGraphql() {
return graphql;
}

public void setGraphql(@NotNull Graphql graphql) {
this.graphql = graphql;
}

@Open
public static class Logging {
/** Enable/Disable logging auto-configuration. */
Expand Down Expand Up @@ -163,4 +175,20 @@ public void setThreadLocalAccessorEnabled(boolean threadLocalAccessorEnabled) {
this.threadLocalAccessorEnabled = threadLocalAccessorEnabled;
}
}

@Open
public static class Graphql {

/** List of error types the Sentry Graphql integration should ignore. */
private @NotNull List<String> ignoredErrorTypes = new ArrayList<>();

@NotNull
public List<String> getIgnoredErrorTypes() {
return ignoredErrorTypes;
}

public void setIgnoredErrorTypes(final @NotNull List<String> ignoredErrorTypes) {
this.ignoredErrorTypes = ignoredErrorTypes;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package io.sentry.spring.boot.jakarta.graphql;

import com.jakewharton.nopen.annotation.Open;
import io.sentry.SentryIntegrationPackageStorage;
import io.sentry.graphql.SentryInstrumentation;
import io.sentry.spring.boot.jakarta.SentryProperties;
import io.sentry.spring.jakarta.graphql.SentryDataFetcherExceptionResolverAdapter;
import io.sentry.spring.jakarta.graphql.SentryGraphqlBeanPostProcessor;
import io.sentry.spring.jakarta.graphql.SentrySpringSubscriptionHandler;
import org.jetbrains.annotations.NotNull;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.autoconfigure.graphql.GraphQlSourceBuilderCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;

@Configuration(proxyBeanMethods = false)
@Open
public class SentryGraphqlAutoConfiguration {

@Bean
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
public GraphQlSourceBuilderCustomizer sourceBuilderCustomizerWebmvc(
final @NotNull SentryProperties sentryProperties) {
SentryIntegrationPackageStorage.getInstance().addIntegration("Spring6GrahQLWebMVC");
return sourceBuilderCustomizer(sentryProperties, false);
}

@Bean
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE)
public GraphQlSourceBuilderCustomizer sourceBuilderCustomizerWebflux(
final @NotNull SentryProperties sentryProperties) {
SentryIntegrationPackageStorage.getInstance().addIntegration("Spring6GrahQLWebFlux");
return sourceBuilderCustomizer(sentryProperties, true);
}

/**
* We're not setting defaultDataFetcherExceptionHandler here on purpose and instead use the
* resolver adapter below. This way Springs handler can still forward to other resolver adapters.
*/
private GraphQlSourceBuilderCustomizer sourceBuilderCustomizer(
final @NotNull SentryProperties sentryProperties, final boolean captureRequestBody) {
return (builder) ->
builder.configureGraphQl(
graphQlBuilder ->
graphQlBuilder.instrumentation(
new SentryInstrumentation(
null,
new SentrySpringSubscriptionHandler(),
captureRequestBody,
sentryProperties.getGraphql().getIgnoredErrorTypes())));
}

@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SentryDataFetcherExceptionResolverAdapter exceptionResolverAdapter() {
return new SentryDataFetcherExceptionResolverAdapter();
}

@Bean
public SentryGraphqlBeanPostProcessor graphqlBeanPostProcessor() {
return new SentryGraphqlBeanPostProcessor();
}
}
Loading