Skip to content

Commit

Permalink
Allow filtering graphql errors (#2967)
Browse files Browse the repository at this point in the history
  • Loading branch information
adinauer committed Oct 11, 2023
1 parent 86f0de8 commit 7e47940
Show file tree
Hide file tree
Showing 15 changed files with 291 additions and 14 deletions.
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)
@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

0 comments on commit 7e47940

Please sign in to comment.