From 7e479406ffa687210944a6ee4a7686bc3fe2e0c9 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Wed, 11 Oct 2023 08:13:49 +0200 Subject: [PATCH] Allow filtering graphql errors (#2967) --- CHANGELOG.md | 3 + sentry-graphql/api/sentry-graphql.api | 3 +- .../sentry/graphql/SentryInstrumentation.java | 47 ++++++++++++-- .../SentryInstrumentationAnotherTest.kt | 22 ++++++- .../graphql/SentryInstrumentationTest.kt | 2 +- .../src/main/resources/application.properties | 1 + .../src/main/resources/application.properties | 1 + .../api/sentry-spring-boot-jakarta.api | 16 +++++ .../boot/jakarta/SentryAutoConfiguration.java | 4 +- .../spring/boot/jakarta/SentryProperties.java | 28 ++++++++ .../SentryGraphqlAutoConfiguration.java | 65 +++++++++++++++++++ sentry-spring-boot/api/sentry-spring-boot.api | 16 +++++ .../spring/boot/SentryAutoConfiguration.java | 4 +- .../sentry/spring/boot/SentryProperties.java | 28 ++++++++ .../SentryGraphqlAutoConfiguration.java | 65 +++++++++++++++++++ 15 files changed, 291 insertions(+), 14 deletions(-) create mode 100644 sentry-spring-boot-jakarta/src/main/java/io/sentry/spring/boot/jakarta/graphql/SentryGraphqlAutoConfiguration.java create mode 100644 sentry-spring-boot/src/main/java/io/sentry/spring/boot/graphql/SentryGraphqlAutoConfiguration.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 153909bab7..e9053ef532 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/sentry-graphql/api/sentry-graphql.api b/sentry-graphql/api/sentry-graphql.api index 5d4b907f14..58e864d0b4 100644 --- a/sentry-graphql/api/sentry-graphql.api +++ b/sentry-graphql/api/sentry-graphql.api @@ -53,8 +53,9 @@ public final class io/sentry/graphql/SentryInstrumentation : graphql/execution/i public fun (Lio/sentry/IHub;)V public fun (Lio/sentry/IHub;Lio/sentry/graphql/SentryInstrumentation$BeforeSpanCallback;)V public fun (Lio/sentry/graphql/SentryInstrumentation$BeforeSpanCallback;)V - public fun (Lio/sentry/graphql/SentryInstrumentation$BeforeSpanCallback;Lio/sentry/graphql/SentrySubscriptionHandler;Lio/sentry/graphql/ExceptionReporter;)V + public fun (Lio/sentry/graphql/SentryInstrumentation$BeforeSpanCallback;Lio/sentry/graphql/SentrySubscriptionHandler;Lio/sentry/graphql/ExceptionReporter;Ljava/util/List;)V public fun (Lio/sentry/graphql/SentryInstrumentation$BeforeSpanCallback;Lio/sentry/graphql/SentrySubscriptionHandler;Z)V + public fun (Lio/sentry/graphql/SentryInstrumentation$BeforeSpanCallback;Lio/sentry/graphql/SentrySubscriptionHandler;ZLjava/util/List;)V public fun (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; diff --git a/sentry-graphql/src/main/java/io/sentry/graphql/SentryInstrumentation.java b/sentry-graphql/src/main/java/io/sentry/graphql/SentryInstrumentation.java index c5f36d7228..bd53bb57f2 100644 --- a/sentry-graphql/src/main/java/io/sentry/graphql/SentryInstrumentation.java +++ b/sentry-graphql/src/main/java/io/sentry/graphql/SentryInstrumentation.java @@ -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; @@ -52,6 +53,8 @@ public final class SentryInstrumentation extends SimpleInstrumentation { private final @NotNull ExceptionReporter exceptionReporter; + private final @NotNull List ignoredErrorTypes; + /** * @deprecated please use a constructor that takes a {@link SentrySubscriptionHandler} instead. */ @@ -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 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 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); @@ -171,11 +198,8 @@ public CompletableFuture instrumentExecutionResult( final @NotNull List 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( @@ -195,6 +219,17 @@ public CompletableFuture 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; diff --git a/sentry-graphql/src/test/kotlin/io/sentry/graphql/SentryInstrumentationAnotherTest.kt b/sentry-graphql/src/test/kotlin/io/sentry/graphql/SentryInstrumentationAnotherTest.kt index 2b78036ef3..b8bc76388c 100644 --- a/sentry-graphql/src/test/kotlin/io/sentry/graphql/SentryInstrumentationAnotherTest.kt +++ b/sentry-graphql/src/test/kotlin/io/sentry/graphql/SentryInstrumentationAnotherTest.kt @@ -1,5 +1,6 @@ package io.sentry.graphql +import graphql.ErrorClassification import graphql.ErrorType import graphql.ExecutionInput import graphql.ExecutionResultImpl @@ -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? = null, addTransactionToTracingState: Boolean = true): SentryInstrumentation { + fun getSut(isTransactionActive: Boolean = true, operation: OperationDefinition.Operation = OperationDefinition.Operation.QUERY, graphQLContextParam: Map? = null, addTransactionToTracingState: Boolean = true, ignoredErrors: List = emptyList()): SentryInstrumentation { whenever(hub.options).thenReturn(SentryOptions()) activeSpan = SentryTracer(TransactionContext("name", "op"), hub) @@ -86,7 +87,7 @@ class SentryInstrumentationAnotherTest { exceptionReporter = mock() subscriptionHandler = mock() 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>() whenever(dataFetcher.get(any())).thenReturn("raw result") graphQLContext = GraphQLContext.newContext() @@ -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() @@ -343,4 +357,8 @@ class SentryInstrumentationAnotherTest { } data class Show(val id: Int) + + enum class SomeErrorClassification : ErrorClassification { + SOME_ERROR; + } } diff --git a/sentry-graphql/src/test/kotlin/io/sentry/graphql/SentryInstrumentationTest.kt b/sentry-graphql/src/test/kotlin/io/sentry/graphql/SentryInstrumentationTest.kt index 19b7b3732b..2294a6aa9c 100644 --- a/sentry-graphql/src/test/kotlin/io/sentry/graphql/SentryInstrumentationTest.kt +++ b/sentry-graphql/src/test/kotlin/io/sentry/graphql/SentryInstrumentationTest.kt @@ -170,7 +170,7 @@ class SentryInstrumentationTest { val subscriptionHandler = mock() 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>() whenever(dataFetcher.get(any())).thenReturn("raw result") val graphQLContext = GraphQLContext.newContext().build() diff --git a/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/resources/application.properties b/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/resources/application.properties index d06f7e0878..d677399f7e 100644 --- a/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/resources/application.properties +++ b/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/resources/application.properties @@ -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 diff --git a/sentry-samples/sentry-samples-spring-boot/src/main/resources/application.properties b/sentry-samples/sentry-samples-spring-boot/src/main/resources/application.properties index 31989d2dfe..75461046db 100644 --- a/sentry-samples/sentry-samples-spring-boot/src/main/resources/application.properties +++ b/sentry-samples/sentry-samples-spring-boot/src/main/resources/application.properties @@ -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 diff --git a/sentry-spring-boot-jakarta/api/sentry-spring-boot-jakarta.api b/sentry-spring-boot-jakarta/api/sentry-spring-boot-jakarta.api index 8611bbf0a2..566ef2b30e 100644 --- a/sentry-spring-boot-jakarta/api/sentry-spring-boot-jakarta.api +++ b/sentry-spring-boot-jakarta/api/sentry-spring-boot-jakarta.api @@ -21,6 +21,7 @@ public class io/sentry/spring/boot/jakarta/SentryLogbackAppenderAutoConfiguratio public class io/sentry/spring/boot/jakarta/SentryProperties : io/sentry/SentryOptions { public fun ()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; @@ -28,12 +29,19 @@ public class io/sentry/spring/boot/jakarta/SentryProperties : io/sentry/SentryOp 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 ()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 ()V public fun getLoggers ()Ljava/util/List; @@ -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 ()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; +} + diff --git a/sentry-spring-boot-jakarta/src/main/java/io/sentry/spring/boot/jakarta/SentryAutoConfiguration.java b/sentry-spring-boot-jakarta/src/main/java/io/sentry/spring/boot/jakarta/SentryAutoConfiguration.java index 69cb159359..20cbeeeeda 100644 --- a/sentry-spring-boot-jakarta/src/main/java/io/sentry/spring/boot/jakarta/SentryAutoConfiguration.java +++ b/sentry-spring-boot-jakarta/src/main/java/io/sentry/spring/boot/jakarta/SentryAutoConfiguration.java @@ -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; @@ -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; @@ -166,7 +166,7 @@ static class OpenTelemetryLinkErrorEventProcessorConfiguration { } @Configuration(proxyBeanMethods = false) - @Import(SentryGraphqlConfiguration.class) + @Import(SentryGraphqlAutoConfiguration.class) @Open @ConditionalOnClass({ SentryGraphqlExceptionHandler.class, diff --git a/sentry-spring-boot-jakarta/src/main/java/io/sentry/spring/boot/jakarta/SentryProperties.java b/sentry-spring-boot-jakarta/src/main/java/io/sentry/spring/boot/jakarta/SentryProperties.java index f5c485b402..7b3469d7f1 100644 --- a/sentry-spring-boot-jakarta/src/main/java/io/sentry/spring/boot/jakarta/SentryProperties.java +++ b/sentry-spring-boot-jakarta/src/main/java/io/sentry/spring/boot/jakarta/SentryProperties.java @@ -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; @@ -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; } @@ -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. */ @@ -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 ignoredErrorTypes = new ArrayList<>(); + + @NotNull + public List getIgnoredErrorTypes() { + return ignoredErrorTypes; + } + + public void setIgnoredErrorTypes(final @NotNull List ignoredErrorTypes) { + this.ignoredErrorTypes = ignoredErrorTypes; + } + } } diff --git a/sentry-spring-boot-jakarta/src/main/java/io/sentry/spring/boot/jakarta/graphql/SentryGraphqlAutoConfiguration.java b/sentry-spring-boot-jakarta/src/main/java/io/sentry/spring/boot/jakarta/graphql/SentryGraphqlAutoConfiguration.java new file mode 100644 index 0000000000..0fcbdec072 --- /dev/null +++ b/sentry-spring-boot-jakarta/src/main/java/io/sentry/spring/boot/jakarta/graphql/SentryGraphqlAutoConfiguration.java @@ -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(); + } +} diff --git a/sentry-spring-boot/api/sentry-spring-boot.api b/sentry-spring-boot/api/sentry-spring-boot.api index 642f550e6b..91266f9d98 100644 --- a/sentry-spring-boot/api/sentry-spring-boot.api +++ b/sentry-spring-boot/api/sentry-spring-boot.api @@ -21,15 +21,23 @@ public class io/sentry/spring/boot/SentryLogbackAppenderAutoConfiguration { public class io/sentry/spring/boot/SentryProperties : io/sentry/SentryOptions { public fun ()V public fun getExceptionResolverOrder ()I + 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 isUseGitCommitIdAsRelease ()Z public fun setExceptionResolverOrder (I)V + public fun setGraphql (Lio/sentry/spring/boot/SentryProperties$Graphql;)V public fun setLogging (Lio/sentry/spring/boot/SentryProperties$Logging;)V public fun setUseGitCommitIdAsRelease (Z)V public fun setUserFilterOrder (Ljava/lang/Integer;)V } +public class io/sentry/spring/boot/SentryProperties$Graphql { + public fun ()V + public fun getIgnoredErrorTypes ()Ljava/util/List; + public fun setIgnoredErrorTypes (Ljava/util/List;)V +} + public class io/sentry/spring/boot/SentryProperties$Logging { public fun ()V public fun getLoggers ()Ljava/util/List; @@ -49,3 +57,11 @@ public class io/sentry/spring/boot/SentryWebfluxAutoConfiguration { public fun sentryWebFilter (Lio/sentry/IHub;)Lio/sentry/spring/webflux/SentryWebFilter; } +public class io/sentry/spring/boot/graphql/SentryGraphqlAutoConfiguration { + public fun ()V + public fun exceptionResolverAdapter ()Lio/sentry/spring/graphql/SentryDataFetcherExceptionResolverAdapter; + public fun graphqlBeanPostProcessor ()Lio/sentry/spring/graphql/SentryGraphqlBeanPostProcessor; + public fun sourceBuilderCustomizerWebflux (Lio/sentry/spring/boot/SentryProperties;)Lorg/springframework/boot/autoconfigure/graphql/GraphQlSourceBuilderCustomizer; + public fun sourceBuilderCustomizerWebmvc (Lio/sentry/spring/boot/SentryProperties;)Lorg/springframework/boot/autoconfigure/graphql/GraphQlSourceBuilderCustomizer; +} + diff --git a/sentry-spring-boot/src/main/java/io/sentry/spring/boot/SentryAutoConfiguration.java b/sentry-spring-boot/src/main/java/io/sentry/spring/boot/SentryAutoConfiguration.java index 7198eecbfd..edbfee271d 100644 --- a/sentry-spring-boot/src/main/java/io/sentry/spring/boot/SentryAutoConfiguration.java +++ b/sentry-spring-boot/src/main/java/io/sentry/spring/boot/SentryAutoConfiguration.java @@ -22,12 +22,12 @@ import io.sentry.spring.SentryUserProvider; import io.sentry.spring.SentryWebConfiguration; import io.sentry.spring.SpringSecuritySentryUserProvider; +import io.sentry.spring.boot.graphql.SentryGraphqlAutoConfiguration; import io.sentry.spring.checkin.SentryCheckInAdviceConfiguration; import io.sentry.spring.checkin.SentryCheckInPointcutConfiguration; import io.sentry.spring.checkin.SentryQuartzConfiguration; import io.sentry.spring.exception.SentryCaptureExceptionParameterPointcutConfiguration; import io.sentry.spring.exception.SentryExceptionParameterAdviceConfiguration; -import io.sentry.spring.graphql.SentryGraphqlConfiguration; import io.sentry.spring.tracing.SentryAdviceConfiguration; import io.sentry.spring.tracing.SentrySpanPointcutConfiguration; import io.sentry.spring.tracing.SentryTracingFilter; @@ -166,7 +166,7 @@ static class OpenTelemetryLinkErrorEventProcessorConfiguration { } @Configuration(proxyBeanMethods = false) - @Import(SentryGraphqlConfiguration.class) + @Import(SentryGraphqlAutoConfiguration.class) @Open @ConditionalOnClass({ SentryGraphqlExceptionHandler.class, diff --git a/sentry-spring-boot/src/main/java/io/sentry/spring/boot/SentryProperties.java b/sentry-spring-boot/src/main/java/io/sentry/spring/boot/SentryProperties.java index 6172966a74..334e36f402 100644 --- a/sentry-spring-boot/src/main/java/io/sentry/spring/boot/SentryProperties.java +++ b/sentry-spring-boot/src/main/java/io/sentry/spring/boot/SentryProperties.java @@ -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; @@ -30,6 +31,9 @@ public class SentryProperties extends SentryOptions { /** Logging framework integration properties. */ private @NotNull Logging logging = new Logging(); + /** Graphql integration properties. */ + private @NotNull Graphql graphql = new Graphql(); + public boolean isUseGitCommitIdAsRelease() { return useGitCommitIdAsRelease; } @@ -74,6 +78,14 @@ public void setLogging(@NotNull Logging logging) { this.logging = logging; } + 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. */ @@ -121,4 +133,20 @@ public void setLoggers(final @NotNull List loggers) { this.loggers = loggers; } } + + @Open + public static class Graphql { + + /** List of error types the Sentry Graphql integration should ignore. */ + private @NotNull List ignoredErrorTypes = new ArrayList<>(); + + @NotNull + public List getIgnoredErrorTypes() { + return ignoredErrorTypes; + } + + public void setIgnoredErrorTypes(final @NotNull List ignoredErrorTypes) { + this.ignoredErrorTypes = ignoredErrorTypes; + } + } } diff --git a/sentry-spring-boot/src/main/java/io/sentry/spring/boot/graphql/SentryGraphqlAutoConfiguration.java b/sentry-spring-boot/src/main/java/io/sentry/spring/boot/graphql/SentryGraphqlAutoConfiguration.java new file mode 100644 index 0000000000..5c92d4bd6a --- /dev/null +++ b/sentry-spring-boot/src/main/java/io/sentry/spring/boot/graphql/SentryGraphqlAutoConfiguration.java @@ -0,0 +1,65 @@ +package io.sentry.spring.boot.graphql; + +import com.jakewharton.nopen.annotation.Open; +import io.sentry.SentryIntegrationPackageStorage; +import io.sentry.graphql.SentryInstrumentation; +import io.sentry.spring.boot.SentryProperties; +import io.sentry.spring.graphql.SentryDataFetcherExceptionResolverAdapter; +import io.sentry.spring.graphql.SentryGraphqlBeanPostProcessor; +import io.sentry.spring.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("Spring5GrahQLWebMVC"); + return sourceBuilderCustomizer(sentryProperties, false); + } + + @Bean + @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE) + public GraphQlSourceBuilderCustomizer sourceBuilderCustomizerWebflux( + final @NotNull SentryProperties sentryProperties) { + SentryIntegrationPackageStorage.getInstance().addIntegration("Spring5GrahQLWebFlux"); + 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(); + } +}