From fa5bc98875613fc18e77c836c31b021f0ab2eec8 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Fri, 17 Jul 2026 15:17:50 +0200 Subject: [PATCH] feat(graphql): Apply outgoing response body policy Use the Data Collection outgoing response body decision when attaching GraphQL execution results. Preserve the sendDefaultPii and maxRequestBodySize gate when Data Collection is absent. Co-Authored-By: Claude --- .../io/sentry/graphql/ExceptionReporter.java | 7 +- .../sentry/graphql/ExceptionReporterTest.kt | 119 ++++++++++++++++++ sentry/api/sentry.api | 1 + .../io/sentry/DataCollectionResolver.java | 4 + .../io/sentry/DataCollectionResolverTest.kt | 31 +++++ 5 files changed, 159 insertions(+), 3 deletions(-) diff --git a/sentry-graphql-core/src/main/java/io/sentry/graphql/ExceptionReporter.java b/sentry-graphql-core/src/main/java/io/sentry/graphql/ExceptionReporter.java index d53a6376e0..4330a49e22 100644 --- a/sentry-graphql-core/src/main/java/io/sentry/graphql/ExceptionReporter.java +++ b/sentry-graphql-core/src/main/java/io/sentry/graphql/ExceptionReporter.java @@ -62,9 +62,10 @@ private boolean isAllowedToAttachRequestBody(final @NotNull IScopes scopes) { } private boolean isAllowedToAttachResponseBody(final @NotNull IScopes scopes) { - final @NotNull SentryOptions options = scopes.getOptions(); - return options.isSendDefaultPii() - && !SentryOptions.RequestSize.NONE.equals(options.getMaxRequestBodySize()); + return scopes + .getOptions() + .getDataCollectionResolver() + .isOutgoingResponseBodyWithLegacyBodyGate(); } private void setRequestDetailsOnEvent( diff --git a/sentry-graphql-core/src/test/kotlin/io/sentry/graphql/ExceptionReporterTest.kt b/sentry-graphql-core/src/test/kotlin/io/sentry/graphql/ExceptionReporterTest.kt index 316edf53ff..3d367663a1 100644 --- a/sentry-graphql-core/src/test/kotlin/io/sentry/graphql/ExceptionReporterTest.kt +++ b/sentry-graphql-core/src/test/kotlin/io/sentry/graphql/ExceptionReporterTest.kt @@ -12,6 +12,7 @@ import graphql.schema.GraphQLObjectType import graphql.schema.GraphQLScalarType import graphql.schema.GraphQLSchema import io.sentry.Hint +import io.sentry.HttpBodyType import io.sentry.IScope import io.sentry.IScopes import io.sentry.KeyValueCollectionBehavior @@ -253,6 +254,124 @@ class ExceptionReporterTest { ) } + @Test + fun `legacy options can disable outgoing response data`() { + val options = SentryOptions().also { it.maxRequestBodySize = SentryOptions.RequestSize.ALWAYS } + val exceptionReporter = fixture.getSut(options) + + exceptionReporter.captureThrowable( + fixture.exception, + ExceptionReporter.ExceptionDetails( + fixture.scopes, + fixture.instrumentationExecutionParameters, + false, + ), + fixture.executionResult, + ) + + verify(fixture.scopes) + .captureEvent( + org.mockito.kotlin.check { assertNull(it.contexts.response) }, + any(), + ) + } + + @Test + fun `legacy request body size can disable outgoing response data`() { + val options = SentryOptions().also { it.isSendDefaultPii = true } + val exceptionReporter = fixture.getSut(options) + + exceptionReporter.captureThrowable( + fixture.exception, + ExceptionReporter.ExceptionDetails( + fixture.scopes, + fixture.instrumentationExecutionParameters, + false, + ), + fixture.executionResult, + ) + + verify(fixture.scopes) + .captureEvent( + org.mockito.kotlin.check { assertNull(it.contexts.response) }, + any(), + ) + } + + @Test + fun `data collection response ignores legacy request body options`() { + val options = + SentryOptions().also { + it.maxRequestBodySize = SentryOptions.RequestSize.NONE + it.dataCollection.httpBodies = setOf(HttpBodyType.OUTGOING_RESPONSE) + } + val exceptionReporter = fixture.getSut(options) + + exceptionReporter.captureThrowable( + fixture.exception, + ExceptionReporter.ExceptionDetails( + fixture.scopes, + fixture.instrumentationExecutionParameters, + false, + ), + fixture.executionResult, + ) + + verify(fixture.scopes) + .captureEvent( + org.mockito.kotlin.check { assertNotNull(it.contexts.response?.data) }, + any(), + ) + } + + @Test + fun `data collection can disable outgoing response data`() { + val options = fixture.defaultOptions.also { it.dataCollection.httpBodies = emptySet() } + val exceptionReporter = fixture.getSut(options) + + exceptionReporter.captureThrowable( + fixture.exception, + ExceptionReporter.ExceptionDetails( + fixture.scopes, + fixture.instrumentationExecutionParameters, + false, + ), + fixture.executionResult, + ) + + verify(fixture.scopes) + .captureEvent( + org.mockito.kotlin.check { assertNull(it.contexts.response) }, + any(), + ) + } + + @Test + fun `data collection namespace default enables outgoing response data`() { + val options = + SentryOptions().also { + it.maxRequestBodySize = SentryOptions.RequestSize.NONE + it.dataCollection.cookies = KeyValueCollectionBehavior.off() + } + val exceptionReporter = fixture.getSut(options) + + exceptionReporter.captureThrowable( + fixture.exception, + ExceptionReporter.ExceptionDetails( + fixture.scopes, + fixture.instrumentationExecutionParameters, + false, + ), + fixture.executionResult, + ) + + verify(fixture.scopes) + .captureEvent( + org.mockito.kotlin.check { assertNotNull(it.contexts.response?.data) }, + any(), + ) + } + @Test fun `does not attach query or variables if sendDefaultPii is false`() { val exceptionReporter = diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index db9547700d..7b65d3d86c 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -435,6 +435,7 @@ public final class io/sentry/DataCollectionResolver { public fun isIncomingResponseBody ()Z public fun isOutgoingRequestBody ()Z public fun isOutgoingResponseBody ()Z + public fun isOutgoingResponseBodyWithLegacyBodyGate ()Z public fun isUserInfo ()Z } diff --git a/sentry/src/main/java/io/sentry/DataCollectionResolver.java b/sentry/src/main/java/io/sentry/DataCollectionResolver.java index a293614eb2..16d27b6878 100644 --- a/sentry/src/main/java/io/sentry/DataCollectionResolver.java +++ b/sentry/src/main/java/io/sentry/DataCollectionResolver.java @@ -104,6 +104,10 @@ public boolean isOutgoingResponseBody() { return isHttpBodyEnabled(HttpBodyType.OUTGOING_RESPONSE, options.isSendDefaultPii()); } + public boolean isOutgoingResponseBodyWithLegacyBodyGate() { + return isHttpBodyEnabled(HttpBodyType.OUTGOING_RESPONSE, isLegacyGraphqlBodyEnabled()); + } + private boolean isLegacyGraphqlBodyEnabled() { return options.isSendDefaultPii() && !SentryOptions.RequestSize.NONE.equals(options.getMaxRequestBodySize()); diff --git a/sentry/src/test/java/io/sentry/DataCollectionResolverTest.kt b/sentry/src/test/java/io/sentry/DataCollectionResolverTest.kt index fb525ce43d..d84df7ae6a 100644 --- a/sentry/src/test/java/io/sentry/DataCollectionResolverTest.kt +++ b/sentry/src/test/java/io/sentry/DataCollectionResolverTest.kt @@ -128,6 +128,37 @@ class DataCollectionResolverTest { assertThat(options.dataCollectionResolver.isGraphqlVariablesWithLegacyBodyGate).isTrue() } + @Test + fun `outgoing response legacy body variant preserves the legacy size gate`() { + val options = + SentryOptions().apply { + isSendDefaultPii = true + maxRequestBodySize = SentryOptions.RequestSize.NONE + } + + assertThat(options.dataCollectionResolver.isOutgoingResponseBodyWithLegacyBodyGate).isFalse() + + options.maxRequestBodySize = SentryOptions.RequestSize.SMALL + + assertThat(options.dataCollectionResolver.isOutgoingResponseBodyWithLegacyBodyGate).isTrue() + } + + @Test + fun `outgoing response legacy body variant uses data collection when namespace is explicit`() { + val options = + SentryOptions().apply { + isSendDefaultPii = false + maxRequestBodySize = SentryOptions.RequestSize.NONE + dataCollection.graphql.setDocument(true) + } + + assertThat(options.dataCollectionResolver.isOutgoingResponseBodyWithLegacyBodyGate).isTrue() + + options.dataCollection.httpBodies = emptySet() + + assertThat(options.dataCollectionResolver.isOutgoingResponseBodyWithLegacyBodyGate).isFalse() + } + @Test fun `GraphQL legacy body variants ignore the size option when namespace is explicit`() { val options =