From 41a83ff608341176e7cc1a119e61a11d92cabec1 Mon Sep 17 00:00:00 2001 From: Pritam Acharya Date: Thu, 10 Sep 2026 23:41:02 +0530 Subject: [PATCH 1/3] fix(graphql): support specification error types --- .../graphql/SentryGraphqlInstrumentation.java | 29 +++- .../SentryGraphqlInstrumentationTest.kt | 150 ++++++++++++++++++ 2 files changed, 172 insertions(+), 7 deletions(-) create mode 100644 sentry-graphql-core/src/test/kotlin/io/sentry/graphql/SentryGraphqlInstrumentationTest.kt diff --git a/sentry-graphql-core/src/main/java/io/sentry/graphql/SentryGraphqlInstrumentation.java b/sentry-graphql-core/src/main/java/io/sentry/graphql/SentryGraphqlInstrumentation.java index b1f2892500a..30e3f9144d1 100644 --- a/sentry-graphql-core/src/main/java/io/sentry/graphql/SentryGraphqlInstrumentation.java +++ b/sentry-graphql-core/src/main/java/io/sentry/graphql/SentryGraphqlInstrumentation.java @@ -127,8 +127,7 @@ public void instrumentExecutionResultComplete( final @NotNull List errors = result.getErrors(); if (errors != null) { for (GraphQLError error : errors) { - String errorType = getErrorType(error); - if (!isIgnored(errorType)) { + if (!isIgnored(error)) { exceptionReporter.captureThrowable( new RuntimeException(error.getMessage()), new ExceptionReporter.ExceptionDetails( @@ -158,19 +157,35 @@ private boolean isIgnored(final @Nullable String errorType) { || ignoredErrorTypes.contains(errorType); } - private @Nullable String getErrorType(final @Nullable GraphQLError error) { + private boolean isIgnored(final @Nullable GraphQLError error) { if (error == null) { - return null; + return false; } final @Nullable ErrorClassification errorType = error.getErrorType(); if (errorType != null) { - return errorType.toString(); + if (isIgnored(errorType.toString())) { + return true; + } + try { + final @Nullable Object specification = errorType.toSpecification(error); + if (specification instanceof String && isIgnored((String) specification)) { + return true; + } + if (specification instanceof Map) { + final @Nullable Object specificationType = ((Map) specification).get("type"); + return specificationType instanceof String && isIgnored((String) specificationType); + } + } catch (Throwable throwable) { + ExceptionUtils.rethrowIfFatal(throwable); + // Fall back to capturing the error if a custom classification cannot be serialized. + } + return false; } final @Nullable Map extensions = error.getExtensions(); if (extensions != null) { - return StringUtils.toString(extensions.get("errorType")); + return isIgnored(StringUtils.toString(extensions.get("errorType"))); } - return null; + return false; } public void beginExecuteOperation( diff --git a/sentry-graphql-core/src/test/kotlin/io/sentry/graphql/SentryGraphqlInstrumentationTest.kt b/sentry-graphql-core/src/test/kotlin/io/sentry/graphql/SentryGraphqlInstrumentationTest.kt new file mode 100644 index 00000000000..be27d267d42 --- /dev/null +++ b/sentry-graphql-core/src/test/kotlin/io/sentry/graphql/SentryGraphqlInstrumentationTest.kt @@ -0,0 +1,150 @@ +package io.sentry.graphql + +import graphql.ErrorClassification +import graphql.ExecutionResultImpl +import graphql.GraphQLContext +import graphql.GraphQLError +import graphql.GraphqlErrorException +import graphql.execution.instrumentation.parameters.InstrumentationExecutionParameters +import io.sentry.IScopes +import kotlin.test.Test +import org.mockito.kotlin.any +import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever + +class SentryGraphqlInstrumentationTest { + private val scopes = mock() + private val exceptionReporter = mock() + private val parameters = mock() + + init { + whenever(parameters.graphQLContext) + .thenReturn( + GraphQLContext.newContext() + .of(SentryGraphqlInstrumentation.SENTRY_SCOPES_CONTEXT_KEY, scopes) + .build() + ) + } + + @Test + fun `ignores error by classification string`() { + captureError( + error(CustomErrorClassification("SOME_ERROR", mapOf("type" to "OTHER_ERROR"))), + ignoredErrorTypes = listOf("SOME_ERROR"), + ) + + verify(exceptionReporter, never()).captureThrowable(any(), any(), any()) + } + + @Test + fun `ignores error by classification specification type`() { + captureError( + error( + CustomErrorClassification( + "graphql.validation.interpolation.ResourceBundleMessageInterpolator\$ValidationErrorType@1", + mapOf("type" to "ExtendedValidationError"), + ) + ), + ignoredErrorTypes = listOf("ExtendedValidationError"), + ) + + verify(exceptionReporter, never()).captureThrowable(any(), any(), any()) + } + + @Test + fun `ignores error by string classification specification`() { + captureError( + error(CustomErrorClassification("SOME_ERROR", "SPECIFICATION_ERROR")), + ignoredErrorTypes = listOf("SPECIFICATION_ERROR"), + ) + + verify(exceptionReporter, never()).captureThrowable(any(), any(), any()) + } + + @Test + fun `captures error when classification specification has no type`() { + captureError(error(CustomErrorClassification("SOME_ERROR", mapOf("constraint" to "@Size")))) + + verify(exceptionReporter).captureThrowable(any(), any(), any()) + } + + @Test + fun `captures error when classification specification is null`() { + captureError(error(CustomErrorClassification("SOME_ERROR", null))) + + verify(exceptionReporter).captureThrowable(any(), any(), any()) + } + + @Test + fun `captures error when classification specification type is unexpected`() { + captureError(error(CustomErrorClassification("SOME_ERROR", mapOf("type" to 42)))) + + verify(exceptionReporter).captureThrowable(any(), any(), any()) + } + + @Test + fun `captures error when classification specification throws`() { + captureError(error(ThrowingErrorClassification)) + + verify(exceptionReporter).captureThrowable(any(), any(), any()) + } + + @Test + fun `ignores error by extensions when classification is null`() { + val error = mock() + whenever(error.errorType).thenReturn(null) + whenever(error.extensions).thenReturn(mapOf("errorType" to "EXTENSION_ERROR")) + + captureError(error, ignoredErrorTypes = listOf("EXTENSION_ERROR")) + + verify(exceptionReporter, never()).captureThrowable(any(), any(), any()) + } + + @Test + fun `captures non-ignored error`() { + captureError( + error(CustomErrorClassification("SOME_ERROR", mapOf("type" to "SPECIFICATION_ERROR"))), + ignoredErrorTypes = listOf("OTHER_ERROR"), + ) + + verify(exceptionReporter).captureThrowable(any(), any(), any()) + } + + private fun captureError(error: GraphQLError, ignoredErrorTypes: List = emptyList()) { + val instrumentation = + SentryGraphqlInstrumentation( + null, + NoOpSubscriptionHandler.getInstance(), + exceptionReporter, + ignoredErrorTypes, + "manual.test", + ) + val result = ExecutionResultImpl.newExecutionResult().addError(error).build() + + instrumentation.instrumentExecutionResultComplete(parameters, result, null) + } + + private fun error(errorClassification: ErrorClassification): GraphQLError = + GraphqlErrorException.newErrorException() + .message("exception message") + .errorClassification(errorClassification) + .build() + + private class CustomErrorClassification( + private val stringValue: String, + private val specification: Any?, + ) : ErrorClassification { + override fun toSpecification(error: GraphQLError): Any? = specification + + override fun toString(): String = stringValue + } + + private object ThrowingErrorClassification : ErrorClassification { + override fun toSpecification(error: GraphQLError): Any = + throw AssertionError("failed to create specification") + + override fun toString(): String = "SOME_ERROR" + } +} From 2fd2ccb7aa412f55462494c502af611af8c5fc4e Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Fri, 6 Nov 2026 12:47:14 +0100 Subject: [PATCH 2/3] fix(graphql): Make specification filtering opt-in Add an explicit option before matching ignored GraphQL error types against classification specifications. Keep the existing behavior unchanged by default to avoid suppressing errors based on custom specification output. Expose the option through the Spring Boot GraphQL configuration. Co-Authored-By: Claude --- CHANGELOG.md | 1 + sentry-graphql-22/api/sentry-graphql-22.api | 2 + .../graphql22/SentryInstrumentation.java | 43 ++++++++++++- .../api/sentry-graphql-core.api | 2 + .../graphql/SentryGraphqlInstrumentation.java | 43 +++++++++++++ .../SentryGraphqlInstrumentationTest.kt | 60 +++++++++++++++++-- sentry-graphql/api/sentry-graphql.api | 2 + .../sentry/graphql/SentryInstrumentation.java | 43 ++++++++++++- .../api/sentry-spring-boot-4.api | 2 + .../sentry/spring/boot4/SentryProperties.java | 12 ++++ .../SentryGraphql22AutoConfiguration.java | 3 +- .../SentryGraphqlAutoConfiguration.java | 3 +- .../boot4/SentryAutoConfigurationTest.kt | 27 +++++++++ .../api/sentry-spring-boot-jakarta.api | 2 + .../spring/boot/jakarta/SentryProperties.java | 12 ++++ .../SentryGraphql22AutoConfiguration.java | 3 +- .../SentryGraphqlAutoConfiguration.java | 3 +- .../jakarta/SentryAutoConfigurationTest.kt | 27 +++++++++ sentry-spring-boot/api/sentry-spring-boot.api | 2 + .../sentry/spring/boot/SentryProperties.java | 12 ++++ .../SentryGraphqlAutoConfiguration.java | 3 +- .../boot/SentryAutoConfigurationTest.kt | 27 +++++++++ 22 files changed, 319 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9fd49952d1d..1acdfbd456c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -249,6 +249,7 @@ ### Fixes +- Add opt-in support for matching GraphQL ignored error types against classification specifications via `sentry.graphql.enable-specification-error-type-filtering` ([#6092](https://github.com/getsentry/sentry-java/pull/6092)) - Update `SentryTraced` so that it now honors `options.setIgnoredSpanOrigins` ([#6058](https://github.com/getsentry/sentry-java/pull/6058)) - `SentryTraced` now checks for its owning transaction dynamically rather than once per app process. The latter caused `SentryTraced` spans to be dropped process-wide once the original transaction finished ([#6057](https://github.com/getsentry/sentry-java/pull/6057)) - Fix typos in Spring GraphQL integration names (`GrahQL` to `GraphQL`) ([#6061](https://github.com/getsentry/sentry-java/pull/6061)) diff --git a/sentry-graphql-22/api/sentry-graphql-22.api b/sentry-graphql-22/api/sentry-graphql-22.api index 2bee0cf765d..b1e5b00adf2 100644 --- a/sentry-graphql-22/api/sentry-graphql-22.api +++ b/sentry-graphql-22/api/sentry-graphql-22.api @@ -7,8 +7,10 @@ public final class io/sentry/graphql22/SentryInstrumentation : graphql/execution public static final field SENTRY_EXCEPTIONS_CONTEXT_KEY Ljava/lang/String; public static final field SENTRY_SCOPES_CONTEXT_KEY Ljava/lang/String; public fun (Lio/sentry/graphql/SentryGraphqlInstrumentation$BeforeSpanCallback;Lio/sentry/graphql/SentrySubscriptionHandler;Lio/sentry/graphql/ExceptionReporter;Ljava/util/List;)V + public fun (Lio/sentry/graphql/SentryGraphqlInstrumentation$BeforeSpanCallback;Lio/sentry/graphql/SentrySubscriptionHandler;Lio/sentry/graphql/ExceptionReporter;Ljava/util/List;Z)V public fun (Lio/sentry/graphql/SentryGraphqlInstrumentation$BeforeSpanCallback;Lio/sentry/graphql/SentrySubscriptionHandler;Z)V public fun (Lio/sentry/graphql/SentryGraphqlInstrumentation$BeforeSpanCallback;Lio/sentry/graphql/SentrySubscriptionHandler;ZLjava/util/List;)V + public fun (Lio/sentry/graphql/SentryGraphqlInstrumentation$BeforeSpanCallback;Lio/sentry/graphql/SentrySubscriptionHandler;ZLjava/util/List;Z)V public fun (Lio/sentry/graphql/SentrySubscriptionHandler;Z)V public fun beginExecuteOperation (Lgraphql/execution/instrumentation/parameters/InstrumentationExecuteOperationParameters;Lgraphql/execution/instrumentation/InstrumentationState;)Lgraphql/execution/instrumentation/InstrumentationContext; public fun beginExecution (Lgraphql/execution/instrumentation/parameters/InstrumentationExecutionParameters;Lgraphql/execution/instrumentation/InstrumentationState;)Lgraphql/execution/instrumentation/InstrumentationContext; diff --git a/sentry-graphql-22/src/main/java/io/sentry/graphql22/SentryInstrumentation.java b/sentry-graphql-22/src/main/java/io/sentry/graphql22/SentryInstrumentation.java index 264c0a99da2..d4833cafee3 100644 --- a/sentry-graphql-22/src/main/java/io/sentry/graphql22/SentryInstrumentation.java +++ b/sentry-graphql-22/src/main/java/io/sentry/graphql22/SentryInstrumentation.java @@ -77,11 +77,35 @@ public SentryInstrumentation( final @NotNull SentrySubscriptionHandler subscriptionHandler, final boolean captureRequestBodyForNonSubscriptions, final @NotNull List ignoredErrorTypes) { + this( + beforeSpan, + subscriptionHandler, + captureRequestBodyForNonSubscriptions, + ignoredErrorTypes, + false); + } + + /** + * @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 + * @param ignoredErrorTypes list of error types that should not be captured and sent to Sentry + * @param enableSpecificationErrorTypeFiltering whether ignored error types should also be matched + * against {@link graphql.ErrorClassification#toSpecification(graphql.GraphQLError)} + */ + public SentryInstrumentation( + final @Nullable SentryGraphqlInstrumentation.BeforeSpanCallback beforeSpan, + final @NotNull SentrySubscriptionHandler subscriptionHandler, + final boolean captureRequestBodyForNonSubscriptions, + final @NotNull List ignoredErrorTypes, + final boolean enableSpecificationErrorTypeFiltering) { this( beforeSpan, subscriptionHandler, new ExceptionReporter(captureRequestBodyForNonSubscriptions), - ignoredErrorTypes); + ignoredErrorTypes, + enableSpecificationErrorTypeFiltering); } @TestOnly @@ -90,9 +114,24 @@ public SentryInstrumentation( final @NotNull SentrySubscriptionHandler subscriptionHandler, final @NotNull ExceptionReporter exceptionReporter, final @NotNull List ignoredErrorTypes) { + this(beforeSpan, subscriptionHandler, exceptionReporter, ignoredErrorTypes, false); + } + + @TestOnly + public SentryInstrumentation( + final @Nullable SentryGraphqlInstrumentation.BeforeSpanCallback beforeSpan, + final @NotNull SentrySubscriptionHandler subscriptionHandler, + final @NotNull ExceptionReporter exceptionReporter, + final @NotNull List ignoredErrorTypes, + final boolean enableSpecificationErrorTypeFiltering) { this.instrumentation = new SentryGraphqlInstrumentation( - beforeSpan, subscriptionHandler, exceptionReporter, ignoredErrorTypes, TRACE_ORIGIN); + beforeSpan, + subscriptionHandler, + exceptionReporter, + ignoredErrorTypes, + enableSpecificationErrorTypeFiltering, + TRACE_ORIGIN); SentryIntegrationPackageStorage.getInstance().addIntegration("GraphQL-v22"); } diff --git a/sentry-graphql-core/api/sentry-graphql-core.api b/sentry-graphql-core/api/sentry-graphql-core.api index 7b63e2270d4..7ab6b58db2c 100644 --- a/sentry-graphql-core/api/sentry-graphql-core.api +++ b/sentry-graphql-core/api/sentry-graphql-core.api @@ -46,7 +46,9 @@ public final class io/sentry/graphql/SentryGraphqlInstrumentation { public static final field SENTRY_EXCEPTIONS_CONTEXT_KEY Ljava/lang/String; public static final field SENTRY_SCOPES_CONTEXT_KEY Ljava/lang/String; public fun (Lio/sentry/graphql/SentryGraphqlInstrumentation$BeforeSpanCallback;Lio/sentry/graphql/SentrySubscriptionHandler;Lio/sentry/graphql/ExceptionReporter;Ljava/util/List;Ljava/lang/String;)V + public fun (Lio/sentry/graphql/SentryGraphqlInstrumentation$BeforeSpanCallback;Lio/sentry/graphql/SentrySubscriptionHandler;Lio/sentry/graphql/ExceptionReporter;Ljava/util/List;ZLjava/lang/String;)V public fun (Lio/sentry/graphql/SentryGraphqlInstrumentation$BeforeSpanCallback;Lio/sentry/graphql/SentrySubscriptionHandler;ZLjava/util/List;Ljava/lang/String;)V + public fun (Lio/sentry/graphql/SentryGraphqlInstrumentation$BeforeSpanCallback;Lio/sentry/graphql/SentrySubscriptionHandler;ZLjava/util/List;ZLjava/lang/String;)V public fun beginExecuteOperation (Lgraphql/execution/instrumentation/parameters/InstrumentationExecuteOperationParameters;)V public fun beginExecution (Lgraphql/execution/instrumentation/parameters/InstrumentationExecutionParameters;Lio/sentry/graphql/SentryGraphqlInstrumentation$TracingState;)V public fun createState ()Lgraphql/execution/instrumentation/InstrumentationState; diff --git a/sentry-graphql-core/src/main/java/io/sentry/graphql/SentryGraphqlInstrumentation.java b/sentry-graphql-core/src/main/java/io/sentry/graphql/SentryGraphqlInstrumentation.java index 30e3f9144d1..cb1757e30d4 100644 --- a/sentry-graphql-core/src/main/java/io/sentry/graphql/SentryGraphqlInstrumentation.java +++ b/sentry-graphql-core/src/main/java/io/sentry/graphql/SentryGraphqlInstrumentation.java @@ -55,6 +55,7 @@ public final class SentryGraphqlInstrumentation { private final @NotNull SentrySubscriptionHandler subscriptionHandler; private final @NotNull ExceptionReporter exceptionReporter; private final @NotNull List ignoredErrorTypes; + private final boolean enableSpecificationErrorTypeFiltering; private final @NotNull String traceOrigin; /** @@ -72,11 +73,38 @@ public SentryGraphqlInstrumentation( final boolean captureRequestBodyForNonSubscriptions, final @NotNull List ignoredErrorTypes, final @NotNull String traceOrigin) { + this( + beforeSpan, + subscriptionHandler, + captureRequestBodyForNonSubscriptions, + ignoredErrorTypes, + false, + traceOrigin); + } + + /** + * @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 + * @param ignoredErrorTypes list of error types that should not be captured and sent to Sentry + * @param enableSpecificationErrorTypeFiltering whether ignored error types should also be matched + * against {@link ErrorClassification#toSpecification(GraphQLError)} + * @param traceOrigin origin used for spans created by this integration + */ + public SentryGraphqlInstrumentation( + final @Nullable BeforeSpanCallback beforeSpan, + final @NotNull SentrySubscriptionHandler subscriptionHandler, + final boolean captureRequestBodyForNonSubscriptions, + final @NotNull List ignoredErrorTypes, + final boolean enableSpecificationErrorTypeFiltering, + final @NotNull String traceOrigin) { this( beforeSpan, subscriptionHandler, new ExceptionReporter(captureRequestBodyForNonSubscriptions), ignoredErrorTypes, + enableSpecificationErrorTypeFiltering, traceOrigin); } @@ -87,10 +115,22 @@ public SentryGraphqlInstrumentation( final @NotNull ExceptionReporter exceptionReporter, final @NotNull List ignoredErrorTypes, final @NotNull String traceOrigin) { + this(beforeSpan, subscriptionHandler, exceptionReporter, ignoredErrorTypes, false, traceOrigin); + } + + @TestOnly + public SentryGraphqlInstrumentation( + final @Nullable BeforeSpanCallback beforeSpan, + final @NotNull SentrySubscriptionHandler subscriptionHandler, + final @NotNull ExceptionReporter exceptionReporter, + final @NotNull List ignoredErrorTypes, + final boolean enableSpecificationErrorTypeFiltering, + final @NotNull String traceOrigin) { this.beforeSpan = beforeSpan; this.subscriptionHandler = subscriptionHandler; this.exceptionReporter = exceptionReporter; this.ignoredErrorTypes = ignoredErrorTypes; + this.enableSpecificationErrorTypeFiltering = enableSpecificationErrorTypeFiltering; this.traceOrigin = traceOrigin; } @@ -166,6 +206,9 @@ private boolean isIgnored(final @Nullable GraphQLError error) { if (isIgnored(errorType.toString())) { return true; } + if (!enableSpecificationErrorTypeFiltering) { + return false; + } try { final @Nullable Object specification = errorType.toSpecification(error); if (specification instanceof String && isIgnored((String) specification)) { diff --git a/sentry-graphql-core/src/test/kotlin/io/sentry/graphql/SentryGraphqlInstrumentationTest.kt b/sentry-graphql-core/src/test/kotlin/io/sentry/graphql/SentryGraphqlInstrumentationTest.kt index be27d267d42..09843710bea 100644 --- a/sentry-graphql-core/src/test/kotlin/io/sentry/graphql/SentryGraphqlInstrumentationTest.kt +++ b/sentry-graphql-core/src/test/kotlin/io/sentry/graphql/SentryGraphqlInstrumentationTest.kt @@ -39,7 +39,7 @@ class SentryGraphqlInstrumentationTest { } @Test - fun `ignores error by classification specification type`() { + fun `captures error by classification specification type when filtering is disabled`() { captureError( error( CustomErrorClassification( @@ -50,6 +50,35 @@ class SentryGraphqlInstrumentationTest { ignoredErrorTypes = listOf("ExtendedValidationError"), ) + verify(exceptionReporter).captureThrowable(any(), any(), any()) + } + + @Test + fun `does not inspect classification specification when filtering is disabled`() { + val classification = mock() + whenever(classification.toString()).thenReturn("SOME_ERROR") + + captureError( + error(classification), + ignoredErrorTypes = listOf("SPECIFICATION_ERROR"), + ) + + verify(classification, never()).toSpecification(any()) + } + + @Test + fun `ignores error by classification specification type when filtering is enabled`() { + captureError( + error( + CustomErrorClassification( + "graphql.validation.interpolation.ResourceBundleMessageInterpolator\$ValidationErrorType@1", + mapOf("type" to "ExtendedValidationError"), + ) + ), + ignoredErrorTypes = listOf("ExtendedValidationError"), + enableSpecificationErrorTypeFiltering = true, + ) + verify(exceptionReporter, never()).captureThrowable(any(), any(), any()) } @@ -58,6 +87,7 @@ class SentryGraphqlInstrumentationTest { captureError( error(CustomErrorClassification("SOME_ERROR", "SPECIFICATION_ERROR")), ignoredErrorTypes = listOf("SPECIFICATION_ERROR"), + enableSpecificationErrorTypeFiltering = true, ) verify(exceptionReporter, never()).captureThrowable(any(), any(), any()) @@ -65,28 +95,40 @@ class SentryGraphqlInstrumentationTest { @Test fun `captures error when classification specification has no type`() { - captureError(error(CustomErrorClassification("SOME_ERROR", mapOf("constraint" to "@Size")))) + captureError( + error(CustomErrorClassification("SOME_ERROR", mapOf("constraint" to "@Size"))), + enableSpecificationErrorTypeFiltering = true, + ) verify(exceptionReporter).captureThrowable(any(), any(), any()) } @Test fun `captures error when classification specification is null`() { - captureError(error(CustomErrorClassification("SOME_ERROR", null))) + captureError( + error(CustomErrorClassification("SOME_ERROR", null)), + enableSpecificationErrorTypeFiltering = true, + ) verify(exceptionReporter).captureThrowable(any(), any(), any()) } @Test fun `captures error when classification specification type is unexpected`() { - captureError(error(CustomErrorClassification("SOME_ERROR", mapOf("type" to 42)))) + captureError( + error(CustomErrorClassification("SOME_ERROR", mapOf("type" to 42))), + enableSpecificationErrorTypeFiltering = true, + ) verify(exceptionReporter).captureThrowable(any(), any(), any()) } @Test fun `captures error when classification specification throws`() { - captureError(error(ThrowingErrorClassification)) + captureError( + error(ThrowingErrorClassification), + enableSpecificationErrorTypeFiltering = true, + ) verify(exceptionReporter).captureThrowable(any(), any(), any()) } @@ -107,18 +149,24 @@ class SentryGraphqlInstrumentationTest { captureError( error(CustomErrorClassification("SOME_ERROR", mapOf("type" to "SPECIFICATION_ERROR"))), ignoredErrorTypes = listOf("OTHER_ERROR"), + enableSpecificationErrorTypeFiltering = true, ) verify(exceptionReporter).captureThrowable(any(), any(), any()) } - private fun captureError(error: GraphQLError, ignoredErrorTypes: List = emptyList()) { + private fun captureError( + error: GraphQLError, + ignoredErrorTypes: List = emptyList(), + enableSpecificationErrorTypeFiltering: Boolean = false, + ) { val instrumentation = SentryGraphqlInstrumentation( null, NoOpSubscriptionHandler.getInstance(), exceptionReporter, ignoredErrorTypes, + enableSpecificationErrorTypeFiltering, "manual.test", ) val result = ExecutionResultImpl.newExecutionResult().addError(error).build() diff --git a/sentry-graphql/api/sentry-graphql.api b/sentry-graphql/api/sentry-graphql.api index 2e5f81ae760..4f58e5ee912 100644 --- a/sentry-graphql/api/sentry-graphql.api +++ b/sentry-graphql/api/sentry-graphql.api @@ -7,8 +7,10 @@ public final class io/sentry/graphql/SentryInstrumentation : graphql/execution/i public static final field SENTRY_EXCEPTIONS_CONTEXT_KEY Ljava/lang/String; public static final field SENTRY_SCOPES_CONTEXT_KEY Ljava/lang/String; public fun (Lio/sentry/graphql/SentryGraphqlInstrumentation$BeforeSpanCallback;Lio/sentry/graphql/SentrySubscriptionHandler;Lio/sentry/graphql/ExceptionReporter;Ljava/util/List;)V + public fun (Lio/sentry/graphql/SentryGraphqlInstrumentation$BeforeSpanCallback;Lio/sentry/graphql/SentrySubscriptionHandler;Lio/sentry/graphql/ExceptionReporter;Ljava/util/List;Z)V public fun (Lio/sentry/graphql/SentryGraphqlInstrumentation$BeforeSpanCallback;Lio/sentry/graphql/SentrySubscriptionHandler;Z)V public fun (Lio/sentry/graphql/SentryGraphqlInstrumentation$BeforeSpanCallback;Lio/sentry/graphql/SentrySubscriptionHandler;ZLjava/util/List;)V + public fun (Lio/sentry/graphql/SentryGraphqlInstrumentation$BeforeSpanCallback;Lio/sentry/graphql/SentrySubscriptionHandler;ZLjava/util/List;Z)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 b8d35697f1f..0d0736f8b70 100644 --- a/sentry-graphql/src/main/java/io/sentry/graphql/SentryInstrumentation.java +++ b/sentry-graphql/src/main/java/io/sentry/graphql/SentryInstrumentation.java @@ -74,11 +74,35 @@ public SentryInstrumentation( final @NotNull SentrySubscriptionHandler subscriptionHandler, final boolean captureRequestBodyForNonSubscriptions, final @NotNull List ignoredErrorTypes) { + this( + beforeSpan, + subscriptionHandler, + captureRequestBodyForNonSubscriptions, + ignoredErrorTypes, + false); + } + + /** + * @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 + * @param ignoredErrorTypes list of error types that should not be captured and sent to Sentry + * @param enableSpecificationErrorTypeFiltering whether ignored error types should also be matched + * against {@link graphql.ErrorClassification#toSpecification(graphql.GraphQLError)} + */ + public SentryInstrumentation( + final @Nullable SentryGraphqlInstrumentation.BeforeSpanCallback beforeSpan, + final @NotNull SentrySubscriptionHandler subscriptionHandler, + final boolean captureRequestBodyForNonSubscriptions, + final @NotNull List ignoredErrorTypes, + final boolean enableSpecificationErrorTypeFiltering) { this( beforeSpan, subscriptionHandler, new ExceptionReporter(captureRequestBodyForNonSubscriptions), - ignoredErrorTypes); + ignoredErrorTypes, + enableSpecificationErrorTypeFiltering); } @TestOnly @@ -87,9 +111,24 @@ public SentryInstrumentation( final @NotNull SentrySubscriptionHandler subscriptionHandler, final @NotNull ExceptionReporter exceptionReporter, final @NotNull List ignoredErrorTypes) { + this(beforeSpan, subscriptionHandler, exceptionReporter, ignoredErrorTypes, false); + } + + @TestOnly + public SentryInstrumentation( + final @Nullable SentryGraphqlInstrumentation.BeforeSpanCallback beforeSpan, + final @NotNull SentrySubscriptionHandler subscriptionHandler, + final @NotNull ExceptionReporter exceptionReporter, + final @NotNull List ignoredErrorTypes, + final boolean enableSpecificationErrorTypeFiltering) { this.instrumentation = new SentryGraphqlInstrumentation( - beforeSpan, subscriptionHandler, exceptionReporter, ignoredErrorTypes, TRACE_ORIGIN); + beforeSpan, + subscriptionHandler, + exceptionReporter, + ignoredErrorTypes, + enableSpecificationErrorTypeFiltering, + TRACE_ORIGIN); SentryIntegrationPackageStorage.getInstance().addIntegration("GraphQL"); } diff --git a/sentry-spring-boot-4/api/sentry-spring-boot-4.api b/sentry-spring-boot-4/api/sentry-spring-boot-4.api index 080bb6d6f6a..432ed580bd5 100644 --- a/sentry-spring-boot-4/api/sentry-spring-boot-4.api +++ b/sentry-spring-boot-4/api/sentry-spring-boot-4.api @@ -74,6 +74,8 @@ public class io/sentry/spring/boot4/SentryProperties$Clients { public class io/sentry/spring/boot4/SentryProperties$Graphql { public fun ()V public fun getIgnoredErrorTypes ()Ljava/util/List; + public fun isEnableSpecificationErrorTypeFiltering ()Z + public fun setEnableSpecificationErrorTypeFiltering (Z)V public fun setIgnoredErrorTypes (Ljava/util/List;)V } diff --git a/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot4/SentryProperties.java b/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot4/SentryProperties.java index b99450644fc..3a30d465737 100644 --- a/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot4/SentryProperties.java +++ b/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot4/SentryProperties.java @@ -242,6 +242,9 @@ public static class Graphql { /** List of error types the Sentry Graphql integration should ignore. */ private @NotNull List ignoredErrorTypes = new ArrayList<>(); + /** Whether ignored error types should be matched against classification specifications. */ + private boolean enableSpecificationErrorTypeFiltering = false; + @NotNull public List getIgnoredErrorTypes() { return ignoredErrorTypes; @@ -250,6 +253,15 @@ public List getIgnoredErrorTypes() { public void setIgnoredErrorTypes(final @NotNull List ignoredErrorTypes) { this.ignoredErrorTypes = ignoredErrorTypes; } + + public boolean isEnableSpecificationErrorTypeFiltering() { + return enableSpecificationErrorTypeFiltering; + } + + public void setEnableSpecificationErrorTypeFiltering( + final boolean enableSpecificationErrorTypeFiltering) { + this.enableSpecificationErrorTypeFiltering = enableSpecificationErrorTypeFiltering; + } } @Open diff --git a/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot4/graphql/SentryGraphql22AutoConfiguration.java b/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot4/graphql/SentryGraphql22AutoConfiguration.java index e83b6241847..f1a9c74ebb0 100644 --- a/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot4/graphql/SentryGraphql22AutoConfiguration.java +++ b/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot4/graphql/SentryGraphql22AutoConfiguration.java @@ -56,7 +56,8 @@ private SentryInstrumentation createInstrumentation( beforeSpanCallback.getIfAvailable(), new SentrySpringSubscriptionHandler(), captureRequestBody, - sentryProperties.getGraphql().getIgnoredErrorTypes()); + sentryProperties.getGraphql().getIgnoredErrorTypes(), + sentryProperties.getGraphql().isEnableSpecificationErrorTypeFiltering()); } @Bean diff --git a/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot4/graphql/SentryGraphqlAutoConfiguration.java b/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot4/graphql/SentryGraphqlAutoConfiguration.java index 7bcef3c7b4d..7565b48a25c 100644 --- a/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot4/graphql/SentryGraphqlAutoConfiguration.java +++ b/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot4/graphql/SentryGraphqlAutoConfiguration.java @@ -56,7 +56,8 @@ private SentryInstrumentation createInstrumentation( beforeSpanCallback.getIfAvailable(), new SentrySpringSubscriptionHandler(), captureRequestBody, - sentryProperties.getGraphql().getIgnoredErrorTypes()); + sentryProperties.getGraphql().getIgnoredErrorTypes(), + sentryProperties.getGraphql().isEnableSpecificationErrorTypeFiltering()); } @Bean diff --git a/sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot4/SentryAutoConfigurationTest.kt b/sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot4/SentryAutoConfigurationTest.kt index 6cd61dae0de..334153d4e86 100644 --- a/sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot4/SentryAutoConfigurationTest.kt +++ b/sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot4/SentryAutoConfigurationTest.kt @@ -203,6 +203,33 @@ class SentryAutoConfigurationTest { } } + @Test + fun `GraphQL specification error type filtering is disabled by default`() { + dsnEnabledRunner.run { + assertThat( + it.getBean(SentryProperties::class.java).graphql.isEnableSpecificationErrorTypeFiltering + ) + .isFalse() + } + } + + @Test + fun `GraphQL specification error type filtering property is applied`() { + listOf(false, true).forEach { enabled -> + dsnEnabledRunner + .withPropertyValues("sentry.graphql.enable-specification-error-type-filtering=$enabled") + .run { + assertThat( + it + .getBean(SentryProperties::class.java) + .graphql + .isEnableSpecificationErrorTypeFiltering + ) + .isEqualTo(enabled) + } + } + } + @Test fun `properties are applied to SentryOptions`() { contextRunner 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 9b9c46158bc..da23ccf6ca3 100644 --- a/sentry-spring-boot-jakarta/api/sentry-spring-boot-jakarta.api +++ b/sentry-spring-boot-jakarta/api/sentry-spring-boot-jakarta.api @@ -74,6 +74,8 @@ public class io/sentry/spring/boot/jakarta/SentryProperties$Clients { public class io/sentry/spring/boot/jakarta/SentryProperties$Graphql { public fun ()V public fun getIgnoredErrorTypes ()Ljava/util/List; + public fun isEnableSpecificationErrorTypeFiltering ()Z + public fun setEnableSpecificationErrorTypeFiltering (Z)V public fun setIgnoredErrorTypes (Ljava/util/List;)V } 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 c7241a9466e..f3457d17f12 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 @@ -256,6 +256,9 @@ public static class Graphql { /** List of error types the Sentry Graphql integration should ignore. */ private @NotNull List ignoredErrorTypes = new ArrayList<>(); + /** Whether ignored error types should be matched against classification specifications. */ + private boolean enableSpecificationErrorTypeFiltering = false; + @NotNull public List getIgnoredErrorTypes() { return ignoredErrorTypes; @@ -264,5 +267,14 @@ public List getIgnoredErrorTypes() { public void setIgnoredErrorTypes(final @NotNull List ignoredErrorTypes) { this.ignoredErrorTypes = ignoredErrorTypes; } + + public boolean isEnableSpecificationErrorTypeFiltering() { + return enableSpecificationErrorTypeFiltering; + } + + public void setEnableSpecificationErrorTypeFiltering( + final boolean enableSpecificationErrorTypeFiltering) { + this.enableSpecificationErrorTypeFiltering = enableSpecificationErrorTypeFiltering; + } } } diff --git a/sentry-spring-boot-jakarta/src/main/java/io/sentry/spring/boot/jakarta/graphql/SentryGraphql22AutoConfiguration.java b/sentry-spring-boot-jakarta/src/main/java/io/sentry/spring/boot/jakarta/graphql/SentryGraphql22AutoConfiguration.java index aa741892848..defbc59f5ae 100644 --- a/sentry-spring-boot-jakarta/src/main/java/io/sentry/spring/boot/jakarta/graphql/SentryGraphql22AutoConfiguration.java +++ b/sentry-spring-boot-jakarta/src/main/java/io/sentry/spring/boot/jakarta/graphql/SentryGraphql22AutoConfiguration.java @@ -56,7 +56,8 @@ private SentryInstrumentation createInstrumentation( beforeSpanCallback.getIfAvailable(), new SentrySpringSubscriptionHandler(), captureRequestBody, - sentryProperties.getGraphql().getIgnoredErrorTypes()); + sentryProperties.getGraphql().getIgnoredErrorTypes(), + sentryProperties.getGraphql().isEnableSpecificationErrorTypeFiltering()); } @Bean 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 index e5c1da2df76..7c6c14f8d3f 100644 --- 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 @@ -56,7 +56,8 @@ private SentryInstrumentation createInstrumentation( beforeSpanCallback.getIfAvailable(), new SentrySpringSubscriptionHandler(), captureRequestBody, - sentryProperties.getGraphql().getIgnoredErrorTypes()); + sentryProperties.getGraphql().getIgnoredErrorTypes(), + sentryProperties.getGraphql().isEnableSpecificationErrorTypeFiltering()); } @Bean diff --git a/sentry-spring-boot-jakarta/src/test/kotlin/io/sentry/spring/boot/jakarta/SentryAutoConfigurationTest.kt b/sentry-spring-boot-jakarta/src/test/kotlin/io/sentry/spring/boot/jakarta/SentryAutoConfigurationTest.kt index 1217bbfea40..4f602db34f4 100644 --- a/sentry-spring-boot-jakarta/src/test/kotlin/io/sentry/spring/boot/jakarta/SentryAutoConfigurationTest.kt +++ b/sentry-spring-boot-jakarta/src/test/kotlin/io/sentry/spring/boot/jakarta/SentryAutoConfigurationTest.kt @@ -206,6 +206,33 @@ class SentryAutoConfigurationTest { } } + @Test + fun `GraphQL specification error type filtering is disabled by default`() { + dsnEnabledRunner.run { + assertThat( + it.getBean(SentryProperties::class.java).graphql.isEnableSpecificationErrorTypeFiltering + ) + .isFalse() + } + } + + @Test + fun `GraphQL specification error type filtering property is applied`() { + listOf(false, true).forEach { enabled -> + dsnEnabledRunner + .withPropertyValues("sentry.graphql.enable-specification-error-type-filtering=$enabled") + .run { + assertThat( + it + .getBean(SentryProperties::class.java) + .graphql + .isEnableSpecificationErrorTypeFiltering + ) + .isEqualTo(enabled) + } + } + } + @Test fun `properties are applied to SentryOptions`() { contextRunner diff --git a/sentry-spring-boot/api/sentry-spring-boot.api b/sentry-spring-boot/api/sentry-spring-boot.api index 4fafe9d695b..5be7b5295d4 100644 --- a/sentry-spring-boot/api/sentry-spring-boot.api +++ b/sentry-spring-boot/api/sentry-spring-boot.api @@ -59,6 +59,8 @@ public class io/sentry/spring/boot/SentryProperties$Clients { public class io/sentry/spring/boot/SentryProperties$Graphql { public fun ()V public fun getIgnoredErrorTypes ()Ljava/util/List; + public fun isEnableSpecificationErrorTypeFiltering ()Z + public fun setEnableSpecificationErrorTypeFiltering (Z)V public fun setIgnoredErrorTypes (Ljava/util/List;)V } 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 cf8c6934c50..fcca4fe7fc9 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 @@ -213,6 +213,9 @@ public static class Graphql { /** List of error types the Sentry Graphql integration should ignore. */ private @NotNull List ignoredErrorTypes = new ArrayList<>(); + /** Whether ignored error types should be matched against classification specifications. */ + private boolean enableSpecificationErrorTypeFiltering = false; + @NotNull public List getIgnoredErrorTypes() { return ignoredErrorTypes; @@ -221,5 +224,14 @@ public List getIgnoredErrorTypes() { public void setIgnoredErrorTypes(final @NotNull List ignoredErrorTypes) { this.ignoredErrorTypes = ignoredErrorTypes; } + + public boolean isEnableSpecificationErrorTypeFiltering() { + return enableSpecificationErrorTypeFiltering; + } + + public void setEnableSpecificationErrorTypeFiltering( + final boolean enableSpecificationErrorTypeFiltering) { + this.enableSpecificationErrorTypeFiltering = enableSpecificationErrorTypeFiltering; + } } } 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 index ebebe1f1499..40f6fcb2eb7 100644 --- 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 @@ -56,7 +56,8 @@ private SentryInstrumentation createInstrumentation( beforeSpanCallback.getIfAvailable(), new SentrySpringSubscriptionHandler(), captureRequestBody, - sentryProperties.getGraphql().getIgnoredErrorTypes()); + sentryProperties.getGraphql().getIgnoredErrorTypes(), + sentryProperties.getGraphql().isEnableSpecificationErrorTypeFiltering()); } @Bean diff --git a/sentry-spring-boot/src/test/kotlin/io/sentry/spring/boot/SentryAutoConfigurationTest.kt b/sentry-spring-boot/src/test/kotlin/io/sentry/spring/boot/SentryAutoConfigurationTest.kt index 20215ee145f..f4f14c6a85e 100644 --- a/sentry-spring-boot/src/test/kotlin/io/sentry/spring/boot/SentryAutoConfigurationTest.kt +++ b/sentry-spring-boot/src/test/kotlin/io/sentry/spring/boot/SentryAutoConfigurationTest.kt @@ -204,6 +204,33 @@ class SentryAutoConfigurationTest { } } + @Test + fun `GraphQL specification error type filtering is disabled by default`() { + dsnEnabledRunner.run { + assertThat( + it.getBean(SentryProperties::class.java).graphql.isEnableSpecificationErrorTypeFiltering + ) + .isFalse() + } + } + + @Test + fun `GraphQL specification error type filtering property is applied`() { + listOf(false, true).forEach { enabled -> + dsnEnabledRunner + .withPropertyValues("sentry.graphql.enable-specification-error-type-filtering=$enabled") + .run { + assertThat( + it + .getBean(SentryProperties::class.java) + .graphql + .isEnableSpecificationErrorTypeFiltering + ) + .isEqualTo(enabled) + } + } + } + @Test fun `properties are applied to SentryOptions`() { contextRunner From 4a823e5d3d91e72dda5bbd99c073b8977fce69de Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Fri, 6 Nov 2026 16:10:41 +0100 Subject: [PATCH 3/3] docs(graphql): Move changelog entry to Unreleased Keep the GraphQL specification filtering change in the current release notes instead of modifying the already released 8.56.0 section. Refs GH-6092 Co-Authored-By: Claude --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1acdfbd456c..9bf59e7c5ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ ### Fixes +- Add opt-in support for matching GraphQL ignored error types against classification specifications via `sentry.graphql.enable-specification-error-type-filtering` ([#6092](https://github.com/getsentry/sentry-java/pull/6092)) - Session Replay: Fix `SentryOkHttpInterceptor` hanging forever on responses whose body has no known length, such as Server-Sent Events or a gzipped response, when network details are enabled for the URL ([#6231](https://github.com/getsentry/sentry-java/pull/6231)) - Stop marking an OkHttp call span as failed when a connection attempt to one of the host's addresses fails but OkHttp connects on another ([#6472](https://github.com/getsentry/sentry-java/pull/6472)) @@ -249,7 +250,6 @@ ### Fixes -- Add opt-in support for matching GraphQL ignored error types against classification specifications via `sentry.graphql.enable-specification-error-type-filtering` ([#6092](https://github.com/getsentry/sentry-java/pull/6092)) - Update `SentryTraced` so that it now honors `options.setIgnoredSpanOrigins` ([#6058](https://github.com/getsentry/sentry-java/pull/6058)) - `SentryTraced` now checks for its owning transaction dynamically rather than once per app process. The latter caused `SentryTraced` spans to be dropped process-wide once the original transaction finished ([#6057](https://github.com/getsentry/sentry-java/pull/6057)) - Fix typos in Spring GraphQL integration names (`GrahQL` to `GraphQL`) ([#6061](https://github.com/getsentry/sentry-java/pull/6061))