From 02ed018bcab57301d8c44af411cac21019068781 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Wed, 2 Oct 2024 08:48:45 +0200 Subject: [PATCH 1/2] Add support for v22 of graphql-java, new modules sentry-graphql-22 and sentry-graphql-core --- .craft.yml | 2 + .github/ISSUE_TEMPLATE/bug_report_java.yml | 1 + README.md | 2 + buildSrc/src/main/java/Config.kt | 1 + sentry-graphql-22/api/sentry-graphql-22.api | 17 + sentry-graphql-22/build.gradle.kts | 89 ++++ .../graphql22/SentryInstrumentation.java | 142 +++++++ .../SentryInstrumentationAnotherTest.kt | 379 ++++++++++++++++++ .../graphql22/SentryInstrumentationTest.kt | 243 +++++++++++ .../api/sentry-graphql-core.api | 77 ++++ sentry-graphql-core/build.gradle.kts | 88 ++++ .../io/sentry/graphql/ExceptionReporter.java | 0 .../io/sentry/graphql/GraphqlStringUtils.java | 0 .../graphql/NoOpSubscriptionHandler.java | 0 .../SentryDataFetcherExceptionHandler.java | 0 ...tryGenericDataFetcherExceptionHandler.java | 0 .../SentryGraphqlExceptionHandler.java | 2 +- .../graphql/SentryGraphqlInstrumentation.java | 341 ++++++++++++++++ .../graphql/SentrySubscriptionHandler.java | 0 .../sentry/graphql/ExceptionReporterTest.kt | 2 +- .../sentry/graphql/GraphqlStringUtilsTest.kt | 0 .../SentryDataFetcherExceptionHandlerTest.kt | 0 ...yGenericDataFetcherExceptionHandlerTest.kt | 2 +- sentry-graphql/api/sentry-graphql.api | 67 +--- sentry-graphql/build.gradle.kts | 1 + .../sentry/graphql/SentryInstrumentation.java | 335 +--------------- .../SentryInstrumentationAnotherTest.kt | 14 +- .../graphql/SentryInstrumentationTest.kt | 8 +- .../build.gradle.kts | 2 +- sentry-spring-jakarta/build.gradle.kts | 2 +- .../graphql/SentryBatchLoaderRegistry.java | 2 +- .../graphql/SentryBatchLoaderRegistry.java | 2 +- settings.gradle.kts | 2 + 33 files changed, 1421 insertions(+), 402 deletions(-) create mode 100644 sentry-graphql-22/api/sentry-graphql-22.api create mode 100644 sentry-graphql-22/build.gradle.kts create mode 100644 sentry-graphql-22/src/main/java/io/sentry/graphql22/SentryInstrumentation.java create mode 100644 sentry-graphql-22/src/test/kotlin/io/sentry/graphql22/SentryInstrumentationAnotherTest.kt create mode 100644 sentry-graphql-22/src/test/kotlin/io/sentry/graphql22/SentryInstrumentationTest.kt create mode 100644 sentry-graphql-core/api/sentry-graphql-core.api create mode 100644 sentry-graphql-core/build.gradle.kts rename {sentry-graphql => sentry-graphql-core}/src/main/java/io/sentry/graphql/ExceptionReporter.java (100%) rename {sentry-graphql => sentry-graphql-core}/src/main/java/io/sentry/graphql/GraphqlStringUtils.java (100%) rename {sentry-graphql => sentry-graphql-core}/src/main/java/io/sentry/graphql/NoOpSubscriptionHandler.java (100%) rename {sentry-graphql => sentry-graphql-core}/src/main/java/io/sentry/graphql/SentryDataFetcherExceptionHandler.java (100%) rename {sentry-graphql => sentry-graphql-core}/src/main/java/io/sentry/graphql/SentryGenericDataFetcherExceptionHandler.java (100%) rename {sentry-graphql => sentry-graphql-core}/src/main/java/io/sentry/graphql/SentryGraphqlExceptionHandler.java (95%) create mode 100644 sentry-graphql-core/src/main/java/io/sentry/graphql/SentryGraphqlInstrumentation.java rename {sentry-graphql => sentry-graphql-core}/src/main/java/io/sentry/graphql/SentrySubscriptionHandler.java (100%) rename {sentry-graphql => sentry-graphql-core}/src/test/kotlin/io/sentry/graphql/ExceptionReporterTest.kt (99%) rename {sentry-graphql => sentry-graphql-core}/src/test/kotlin/io/sentry/graphql/GraphqlStringUtilsTest.kt (100%) rename {sentry-graphql => sentry-graphql-core}/src/test/kotlin/io/sentry/graphql/SentryDataFetcherExceptionHandlerTest.kt (100%) rename {sentry-graphql => sentry-graphql-core}/src/test/kotlin/io/sentry/graphql/SentryGenericDataFetcherExceptionHandlerTest.kt (94%) diff --git a/.craft.yml b/.craft.yml index 3a5a1fcef5..3d0a878a4c 100644 --- a/.craft.yml +++ b/.craft.yml @@ -47,6 +47,8 @@ targets: maven:io.sentry:sentry-apollo: maven:io.sentry:sentry-jdbc: maven:io.sentry:sentry-graphql: +# maven:io.sentry:sentry-graphql-core: +# maven:io.sentry:sentry-graphql-22: maven:io.sentry:sentry-quartz: maven:io.sentry:sentry-okhttp: maven:io.sentry:sentry-android-navigation: diff --git a/.github/ISSUE_TEMPLATE/bug_report_java.yml b/.github/ISSUE_TEMPLATE/bug_report_java.yml index f802c3a0cc..f95244f5b1 100644 --- a/.github/ISSUE_TEMPLATE/bug_report_java.yml +++ b/.github/ISSUE_TEMPLATE/bug_report_java.yml @@ -27,6 +27,7 @@ body: - sentry-logback - sentry-log4j2 - sentry-graphql + - sentry-graphql-22 - sentry-quartz - sentry-openfeign - sentry-apache-http-client-5 diff --git a/README.md b/README.md index b1c4cb5183..aaca8f78d5 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,8 @@ Sentry SDK for Java and Android | sentry-log4j2 | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-log4j2/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-log4j2) | | sentry-bom | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-bom/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-bom) | | sentry-graphql | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-graphql/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-graphql) | +| sentry-graphql-core | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-graphql-core/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-graphql-core) | +| sentry-graphql-22 | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-graphql-22/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-graphql-22) | | sentry-quartz | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-quartz/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-quartz) | | sentry-openfeign | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-openfeign/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-openfeign) | | sentry-opentelemetry-agent | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-opentelemetry-agent/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-opentelemetry-agent) | diff --git a/buildSrc/src/main/java/Config.kt b/buildSrc/src/main/java/Config.kt index 08dd42901f..a7a24473e3 100644 --- a/buildSrc/src/main/java/Config.kt +++ b/buildSrc/src/main/java/Config.kt @@ -133,6 +133,7 @@ object Config { val p6spy = "p6spy:p6spy:3.9.1" val graphQlJava = "com.graphql-java:graphql-java:17.3" + val graphQlJava22 = "com.graphql-java:graphql-java:22.1" val quartz = "org.quartz-scheduler:quartz:2.3.0" diff --git a/sentry-graphql-22/api/sentry-graphql-22.api b/sentry-graphql-22/api/sentry-graphql-22.api new file mode 100644 index 0000000000..88d55bdd7c --- /dev/null +++ b/sentry-graphql-22/api/sentry-graphql-22.api @@ -0,0 +1,17 @@ +public final class io/sentry/graphql22/BuildConfig { + public static final field SENTRY_GRAPHQL_SDK_NAME Ljava/lang/String; + public static final field VERSION_NAME Ljava/lang/String; +} + +public final class io/sentry/graphql22/SentryInstrumentation : graphql/execution/instrumentation/SimpleInstrumentation { + 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;Z)V + public fun (Lio/sentry/graphql/SentryGraphqlInstrumentation$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/InstrumentationState;)Lgraphql/execution/instrumentation/InstrumentationContext; + public fun beginExecution (Lgraphql/execution/instrumentation/parameters/InstrumentationExecutionParameters;Lgraphql/execution/instrumentation/InstrumentationState;)Lgraphql/execution/instrumentation/InstrumentationContext; + public fun createState (Lgraphql/execution/instrumentation/parameters/InstrumentationCreateStateParameters;)Lgraphql/execution/instrumentation/InstrumentationState; + public fun instrumentDataFetcher (Lgraphql/schema/DataFetcher;Lgraphql/execution/instrumentation/parameters/InstrumentationFieldFetchParameters;Lgraphql/execution/instrumentation/InstrumentationState;)Lgraphql/schema/DataFetcher; + public fun instrumentExecutionResult (Lgraphql/ExecutionResult;Lgraphql/execution/instrumentation/parameters/InstrumentationExecutionParameters;Lgraphql/execution/instrumentation/InstrumentationState;)Ljava/util/concurrent/CompletableFuture; +} + diff --git a/sentry-graphql-22/build.gradle.kts b/sentry-graphql-22/build.gradle.kts new file mode 100644 index 0000000000..36e7ae51c5 --- /dev/null +++ b/sentry-graphql-22/build.gradle.kts @@ -0,0 +1,89 @@ +import net.ltgt.gradle.errorprone.errorprone +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +plugins { + `java-library` + kotlin("jvm") + jacoco + id(Config.QualityPlugins.errorProne) + id(Config.QualityPlugins.gradleVersions) + id(Config.BuildPlugins.buildConfig) version Config.BuildPlugins.buildConfigVersion +} + +configure { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 +} + +tasks.withType().configureEach { + kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8.toString() + kotlinOptions.languageVersion = Config.kotlinCompatibleLanguageVersion +} + +dependencies { + api(projects.sentry) + api(projects.sentryGraphqlCore) + compileOnly(Config.Libs.graphQlJava22) + + compileOnly(Config.CompileOnly.nopen) + errorprone(Config.CompileOnly.nopenChecker) + errorprone(Config.CompileOnly.errorprone) + errorprone(Config.CompileOnly.errorProneNullAway) + compileOnly(Config.CompileOnly.jetbrainsAnnotations) + + // tests + testImplementation(projects.sentry) + testImplementation(projects.sentryTestSupport) + testImplementation(kotlin(Config.kotlinStdLib)) + testImplementation(Config.TestLibs.kotlinTestJunit) + testImplementation(Config.TestLibs.mockitoKotlin) + testImplementation(Config.TestLibs.mockitoInline) + testImplementation(Config.TestLibs.mockWebserver) + testImplementation(Config.Libs.okhttp) + testImplementation(Config.Libs.springBootStarterGraphql) + testImplementation("com.netflix.graphql.dgs:graphql-error-types:4.9.2") + testImplementation(Config.Libs.graphQlJava22) +} + +configure { + test { + java.srcDir("src/test/java") + } +} + +jacoco { + toolVersion = Config.QualityPlugins.Jacoco.version +} + +tasks.jacocoTestReport { + reports { + xml.required.set(true) + html.required.set(false) + } +} + +tasks { + jacocoTestCoverageVerification { + violationRules { + rule { limit { minimum = Config.QualityPlugins.Jacoco.minimumCoverage } } + } + } + check { + dependsOn(jacocoTestCoverageVerification) + dependsOn(jacocoTestReport) + } +} + +tasks.withType().configureEach { + options.errorprone { + check("NullAway", net.ltgt.gradle.errorprone.CheckSeverity.ERROR) + option("NullAway:AnnotatedPackages", "io.sentry") + } +} + +buildConfig { + useJavaOutput() + packageName("io.sentry.graphql22") + buildConfigField("String", "SENTRY_GRAPHQL_SDK_NAME", "\"${Config.Sentry.SENTRY_GRAPHQL_SDK_NAME}\"") + buildConfigField("String", "VERSION_NAME", "\"${project.version}\"") +} 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 new file mode 100644 index 0000000000..066b8d60b8 --- /dev/null +++ b/sentry-graphql-22/src/main/java/io/sentry/graphql22/SentryInstrumentation.java @@ -0,0 +1,142 @@ +package io.sentry.graphql22; + +import graphql.ExecutionResult; +import graphql.execution.instrumentation.InstrumentationContext; +import graphql.execution.instrumentation.InstrumentationState; +import graphql.execution.instrumentation.parameters.InstrumentationCreateStateParameters; +import graphql.execution.instrumentation.parameters.InstrumentationExecuteOperationParameters; +import graphql.execution.instrumentation.parameters.InstrumentationExecutionParameters; +import graphql.execution.instrumentation.parameters.InstrumentationFieldFetchParameters; +import graphql.schema.DataFetcher; +import io.sentry.SentryIntegrationPackageStorage; +import io.sentry.graphql.ExceptionReporter; +import io.sentry.graphql.SentryGraphqlInstrumentation; +import io.sentry.graphql.SentrySubscriptionHandler; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.TestOnly; + +@SuppressWarnings("deprecation") +public final class SentryInstrumentation + extends graphql.execution.instrumentation.SimpleInstrumentation { + + private static final String TRACE_ORIGIN = "auto.graphql.graphql22"; + private final @NotNull SentryGraphqlInstrumentation instrumentation; + + /** + * @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. + */ + public SentryInstrumentation( + final @Nullable SentryGraphqlInstrumentation.BeforeSpanCallback beforeSpan, + final @NotNull SentrySubscriptionHandler subscriptionHandler, + final boolean captureRequestBodyForNonSubscriptions) { + this( + beforeSpan, + subscriptionHandler, + 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 SentryGraphqlInstrumentation.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 SentryGraphqlInstrumentation.BeforeSpanCallback beforeSpan, + final @NotNull SentrySubscriptionHandler subscriptionHandler, + final @NotNull ExceptionReporter exceptionReporter, + final @NotNull List ignoredErrorTypes) { + this.instrumentation = + new SentryGraphqlInstrumentation( + beforeSpan, subscriptionHandler, exceptionReporter, ignoredErrorTypes, TRACE_ORIGIN); + SentryIntegrationPackageStorage.getInstance().addIntegration("GraphQL-v22"); + SentryIntegrationPackageStorage.getInstance() + .addPackage("maven:io.sentry:sentry-graphql-22", BuildConfig.VERSION_NAME); + } + + /** + * @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. + */ + public SentryInstrumentation( + final @NotNull SentrySubscriptionHandler subscriptionHandler, + final boolean captureRequestBodyForNonSubscriptions) { + this(null, subscriptionHandler, captureRequestBodyForNonSubscriptions); + } + + @Override + public @NotNull InstrumentationState createState( + final @NotNull InstrumentationCreateStateParameters parameters) { + return instrumentation.createState(); + } + + @Override + public @Nullable InstrumentationContext beginExecution( + final @NotNull InstrumentationExecutionParameters parameters, + final @NotNull InstrumentationState state) { + final SentryGraphqlInstrumentation.TracingState tracingState = + InstrumentationState.ofState(state); + instrumentation.beginExecution(parameters, tracingState); + return super.beginExecution(parameters, state); + } + + @Override + public CompletableFuture instrumentExecutionResult( + ExecutionResult executionResult, + InstrumentationExecutionParameters parameters, + final @NotNull InstrumentationState state) { + return super.instrumentExecutionResult(executionResult, parameters, state) + .whenComplete( + (result, exception) -> { + instrumentation.instrumentExecutionResultComplete(parameters, result, exception); + }); + } + + @Override + public @NotNull InstrumentationContext beginExecuteOperation( + final @NotNull InstrumentationExecuteOperationParameters parameters, + final @NotNull InstrumentationState state) { + instrumentation.beginExecuteOperation(parameters); + return super.beginExecuteOperation(parameters, state); + } + + @Override + @SuppressWarnings({"FutureReturnValueIgnored", "deprecation"}) + public @NotNull DataFetcher instrumentDataFetcher( + final @NotNull DataFetcher dataFetcher, + final @NotNull InstrumentationFieldFetchParameters parameters, + final @NotNull InstrumentationState state) { + final SentryGraphqlInstrumentation.TracingState tracingState = + InstrumentationState.ofState(state); + return instrumentation.instrumentDataFetcher(dataFetcher, parameters, tracingState); + } +} diff --git a/sentry-graphql-22/src/test/kotlin/io/sentry/graphql22/SentryInstrumentationAnotherTest.kt b/sentry-graphql-22/src/test/kotlin/io/sentry/graphql22/SentryInstrumentationAnotherTest.kt new file mode 100644 index 0000000000..628001bfeb --- /dev/null +++ b/sentry-graphql-22/src/test/kotlin/io/sentry/graphql22/SentryInstrumentationAnotherTest.kt @@ -0,0 +1,379 @@ +package io.sentry.graphql22 + +import graphql.ErrorClassification +import graphql.ErrorType +import graphql.ExecutionInput +import graphql.ExecutionResult +import graphql.GraphQLContext +import graphql.GraphqlErrorException +import graphql.execution.ExecutionContext +import graphql.execution.ExecutionContextBuilder +import graphql.execution.ExecutionId +import graphql.execution.ExecutionStepInfo +import graphql.execution.ExecutionStrategyParameters +import graphql.execution.MergedField +import graphql.execution.MergedSelectionSet +import graphql.execution.ResultPath +import graphql.execution.instrumentation.parameters.InstrumentationExecuteOperationParameters +import graphql.execution.instrumentation.parameters.InstrumentationExecutionParameters +import graphql.execution.instrumentation.parameters.InstrumentationFieldFetchParameters +import graphql.language.Field +import graphql.language.OperationDefinition +import graphql.scalar.GraphqlStringCoercing +import graphql.schema.DataFetcher +import graphql.schema.DataFetchingEnvironment +import graphql.schema.DataFetchingEnvironmentImpl +import graphql.schema.GraphQLFieldDefinition +import graphql.schema.GraphQLObjectType +import graphql.schema.GraphQLScalarType +import graphql.schema.GraphQLSchema +import io.sentry.Breadcrumb +import io.sentry.Hint +import io.sentry.IScopes +import io.sentry.Sentry +import io.sentry.SentryOptions +import io.sentry.SentryTracer +import io.sentry.TransactionContext +import io.sentry.TypeCheckHint +import io.sentry.graphql.ExceptionReporter +import io.sentry.graphql.ExceptionReporter.ExceptionDetails +import io.sentry.graphql.SentryGraphqlInstrumentation +import io.sentry.graphql.SentrySubscriptionHandler +import org.mockito.Mockito +import org.mockito.kotlin.any +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.same +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertSame + +class SentryInstrumentationAnotherTest { + + class Fixture { + val scopes = mock() + lateinit var activeSpan: SentryTracer + lateinit var dataFetcher: DataFetcher + lateinit var fieldFetchParameters: InstrumentationFieldFetchParameters + lateinit var instrumentationExecutionParameters: InstrumentationExecutionParameters + lateinit var environment: DataFetchingEnvironment + lateinit var executionContext: ExecutionContext + lateinit var executionStrategyParameters: ExecutionStrategyParameters + lateinit var executionStepInfo: ExecutionStepInfo + lateinit var graphQLContext: GraphQLContext + lateinit var subscriptionHandler: SentrySubscriptionHandler + lateinit var exceptionReporter: ExceptionReporter + internal lateinit var instrumentationState: SentryGraphqlInstrumentation.TracingState + lateinit var instrumentationExecuteOperationParameters: InstrumentationExecuteOperationParameters + 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, ignoredErrors: List = emptyList()): SentryInstrumentation { + whenever(scopes.options).thenReturn(SentryOptions()) + activeSpan = SentryTracer(TransactionContext("name", "op"), scopes) + + if (isTransactionActive) { + whenever(scopes.span).thenReturn(activeSpan) + } else { + whenever(scopes.span).thenReturn(null) + } + + val defaultGraphQLContext = mapOf( + SentryGraphqlInstrumentation.SENTRY_SCOPES_CONTEXT_KEY to scopes + ) + val mergedField = + MergedField.newMergedField().addField(Field.newField("myFieldName").build()).build() + exceptionReporter = mock() + subscriptionHandler = mock() + whenever(subscriptionHandler.onSubscriptionResult(any(), any(), any(), any())).thenReturn("result modified by subscription handler") + val instrumentation = SentryInstrumentation( + null, + subscriptionHandler, + exceptionReporter, + ignoredErrors + ) + dataFetcher = mock>() + whenever(dataFetcher.get(any())).thenReturn("raw result") + graphQLContext = GraphQLContext.newContext() + .of(graphQLContextParam ?: defaultGraphQLContext).build() + val scalarType = GraphQLScalarType.newScalar().name("MyResponseType").coercing( + GraphqlStringCoercing() + ).build() + val field = GraphQLFieldDefinition.newFieldDefinition() + .name("myQueryFieldName") + .type(scalarType) + .build() + val objectType = GraphQLObjectType.newObject().name("QUERY").field(field).build() + executionStepInfo = ExecutionStepInfo.newExecutionStepInfo() + .type(scalarType) + .fieldContainer(objectType) + .parentInfo(ExecutionStepInfo.newExecutionStepInfo().type(objectType).build()) + .path(ResultPath.rootPath().segment("child")) + .field(mergedField) + .build() + val operationDefinition = OperationDefinition.newOperationDefinition() + .operation(operation) + .name("operation name") + .build() + environment = DataFetchingEnvironmentImpl.newDataFetchingEnvironment() + .graphQLContext(graphQLContext) + .executionStepInfo(executionStepInfo) + .operationDefinition(operationDefinition) + .build() + executionContext = ExecutionContextBuilder.newExecutionContextBuilder() + .executionId(ExecutionId.generate()) + .graphQLContext(graphQLContext) + .operationDefinition(operationDefinition) + .build() + executionStrategyParameters = ExecutionStrategyParameters.newParameters() + .executionStepInfo(executionStepInfo) + .fields(MergedSelectionSet.newMergedSelectionSet().build()) + .field(mergedField) + .build() + instrumentationState = SentryGraphqlInstrumentation.TracingState().also { + if (isTransactionActive && addTransactionToTracingState) { + it.transaction = activeSpan + } + } + fieldFetchParameters = InstrumentationFieldFetchParameters( + executionContext, + { environment }, + executionStrategyParameters, + false + ) + val executionInput = ExecutionInput.newExecutionInput() + .query(query) + .graphQLContext(graphQLContextParam ?: defaultGraphQLContext) + .variables(variables) + .build() + val schema = GraphQLSchema.newSchema().query( + GraphQLObjectType.newObject().name("QueryType").field( + field + ).build() + ).build() + instrumentationExecutionParameters = InstrumentationExecutionParameters(executionInput, schema) + instrumentationExecuteOperationParameters = InstrumentationExecuteOperationParameters(executionContext) + + return instrumentation + } + } + + private val fixture = Fixture() + + @Test + fun `invokes subscription handler for subscription`() { + val instrumentation = fixture.getSut(isTransactionActive = false, operation = OperationDefinition.Operation.SUBSCRIPTION) + val instrumentedDataFetcher = instrumentation.instrumentDataFetcher(fixture.dataFetcher, fixture.fieldFetchParameters, fixture.instrumentationState) + val result = instrumentedDataFetcher.get(fixture.environment) + + assertEquals("result modified by subscription handler", result) + verify(fixture.subscriptionHandler).onSubscriptionResult(eq("raw result"), same(fixture.scopes), same(fixture.exceptionReporter), same(fixture.fieldFetchParameters)) + } + + @Test + fun `invokes subscription handler for subscription if transaction is active`() { + val instrumentation = fixture.getSut(isTransactionActive = true, operation = OperationDefinition.Operation.SUBSCRIPTION) + val instrumentedDataFetcher = instrumentation.instrumentDataFetcher(fixture.dataFetcher, fixture.fieldFetchParameters, fixture.instrumentationState) + val result = instrumentedDataFetcher.get(fixture.environment) + + assertEquals("result modified by subscription handler", result) + verify(fixture.subscriptionHandler).onSubscriptionResult(eq("raw result"), same(fixture.scopes), same(fixture.exceptionReporter), same(fixture.fieldFetchParameters)) + } + + @Test + fun `does not invoke subscription handler for query`() { + val instrumentation = fixture.getSut(isTransactionActive = false, operation = OperationDefinition.Operation.QUERY) + val instrumentedDataFetcher = instrumentation.instrumentDataFetcher(fixture.dataFetcher, fixture.fieldFetchParameters, fixture.instrumentationState) + val result = instrumentedDataFetcher.get(fixture.environment) + + assertEquals("raw result", result) + verify(fixture.subscriptionHandler, never()).onSubscriptionResult(any(), any(), any(), any()) + } + + @Test + fun `does not invoke subscription handler for query if transaction is active`() { + val instrumentation = fixture.getSut(isTransactionActive = false, operation = OperationDefinition.Operation.QUERY) + val instrumentedDataFetcher = instrumentation.instrumentDataFetcher(fixture.dataFetcher, fixture.fieldFetchParameters, fixture.instrumentationState) + val result = instrumentedDataFetcher.get(fixture.environment) + + assertEquals("raw result", result) + verify(fixture.subscriptionHandler, never()).onSubscriptionResult(any(), any(), any(), any()) + } + + @Test + fun `does not invoke subscription handler for mutation`() { + val instrumentation = fixture.getSut(isTransactionActive = false, operation = OperationDefinition.Operation.MUTATION) + val instrumentedDataFetcher = instrumentation.instrumentDataFetcher(fixture.dataFetcher, fixture.fieldFetchParameters, fixture.instrumentationState) + val result = instrumentedDataFetcher.get(fixture.environment) + + assertEquals("raw result", result) + verify(fixture.subscriptionHandler, never()).onSubscriptionResult(any(), any(), any(), any()) + } + + @Test + fun `does not invoke subscription handler for mutation if transaction is active`() { + val instrumentation = fixture.getSut(isTransactionActive = false, operation = OperationDefinition.Operation.MUTATION) + val instrumentedDataFetcher = instrumentation.instrumentDataFetcher(fixture.dataFetcher, fixture.fieldFetchParameters, fixture.instrumentationState) + val result = instrumentedDataFetcher.get(fixture.environment) + + assertEquals("raw result", result) + verify(fixture.subscriptionHandler, never()).onSubscriptionResult(any(), any(), any(), any()) + } + + @Test + fun `adds a breadcrumb for operation`() { + val instrumentation = fixture.getSut() + instrumentation.beginExecuteOperation(fixture.instrumentationExecuteOperationParameters, fixture.instrumentationState) + verify(fixture.scopes).addBreadcrumb( + org.mockito.kotlin.check { breadcrumb -> + assertEquals("graphql", breadcrumb.type) + assertEquals("query", breadcrumb.category) + assertEquals("operation name", breadcrumb.data["operation_name"]) + assertEquals("query", breadcrumb.data["operation_type"]) + assertEquals(fixture.executionContext.executionId.toString(), breadcrumb.data["operation_id"]) + } + ) + } + + @Test + fun `adds a breadcrumb for data fetcher`() { + val instrumentation = fixture.getSut() + instrumentation.instrumentDataFetcher(fixture.dataFetcher, fixture.fieldFetchParameters, fixture.instrumentationState).get(fixture.environment) + verify(fixture.scopes).addBreadcrumb( + org.mockito.kotlin.check { breadcrumb -> + assertEquals("graphql", breadcrumb.type) + assertEquals("graphql.fetcher", breadcrumb.category) + assertEquals("/child", breadcrumb.data["path"]) + assertEquals("myFieldName", breadcrumb.data["field"]) + assertEquals("MyResponseType", breadcrumb.data["type"]) + assertEquals("QUERY", breadcrumb.data["object_type"]) + }, + org.mockito.kotlin.check { hint -> + val environment = hint.getAs(TypeCheckHint.GRAPHQL_DATA_FETCHING_ENVIRONMENT, DataFetchingEnvironment::class.java) + assertNotNull(environment) + } + ) + } + + @Test + fun `stores scopes in context and adds transaction to state`() { + val instrumentation = fixture.getSut(isTransactionActive = true, operation = OperationDefinition.Operation.MUTATION, graphQLContextParam = emptyMap(), addTransactionToTracingState = false) + withMockScopes { + instrumentation.beginExecution(fixture.instrumentationExecutionParameters, fixture.instrumentationState) + assertSame(fixture.scopes, fixture.instrumentationExecutionParameters.graphQLContext.get(SentryGraphqlInstrumentation.SENTRY_SCOPES_CONTEXT_KEY)) + assertNotNull(fixture.instrumentationState.transaction) + } + } + + @Test + fun `invokes exceptionReporter for error`() { + val instrumentation = fixture.getSut() + val executionResult = ExecutionResult.newExecutionResult() + .data("raw result") + .addError( + GraphqlErrorException.newErrorException().message("exception message").errorClassification( + ErrorType.ValidationError + ).build() + ) + .build() + val resultFuture = instrumentation.instrumentExecutionResult(executionResult, fixture.instrumentationExecutionParameters, fixture.instrumentationState) + verify(fixture.exceptionReporter).captureThrowable( + org.mockito.kotlin.check { + assertEquals("exception message", it.message) + }, + org.mockito.kotlin.check { + assertSame(fixture.scopes, it.scopes) + assertSame(fixture.query, it.query) + assertEquals(false, it.isSubscription) + assertEquals(fixture.variables, it.variables) + }, + same(executionResult) + ) + val result = resultFuture.get() + assertSame(executionResult, result) + } + + @Test + fun `invokes exceptionReporter for exceptions in GraphQLContext`() { + val exception = IllegalStateException("some exception") + val instrumentation = fixture.getSut( + graphQLContextParam = mapOf( + SentryGraphqlInstrumentation.SENTRY_EXCEPTIONS_CONTEXT_KEY to listOf(exception), + SentryGraphqlInstrumentation.SENTRY_SCOPES_CONTEXT_KEY to fixture.scopes + ) + ) + val executionResult = ExecutionResult.newExecutionResult() + .data("raw result") + .build() + val resultFuture = instrumentation.instrumentExecutionResult(executionResult, fixture.instrumentationExecutionParameters, fixture.instrumentationState) + verify(fixture.exceptionReporter).captureThrowable( + org.mockito.kotlin.check { + assertSame(exception, it) + }, + org.mockito.kotlin.check { + assertSame(fixture.scopes, it.scopes) + assertSame(fixture.query, it.query) + assertEquals(false, it.isSubscription) + assertEquals(fixture.variables, it.variables) + }, + same(executionResult) + ) + val result = resultFuture.get() + assertSame(executionResult, result) + } + + @Test + fun `does not invoke exceptionReporter for certain errors that should be handled by SentryDataFetcherExceptionHandler`() { + val instrumentation = fixture.getSut() + val executionResult = ExecutionResult.newExecutionResult() + .data("raw result") + .addError(GraphqlErrorException.newErrorException().message("exception message").errorClassification(ErrorType.DataFetchingException).build()) + .addError(GraphqlErrorException.newErrorException().message("exception message").errorClassification(org.springframework.graphql.execution.ErrorType.INTERNAL_ERROR).build()) + .addError(GraphqlErrorException.newErrorException().message("exception message").errorClassification(com.netflix.graphql.types.errors.ErrorType.INTERNAL).build()) + .build() + val resultFuture = instrumentation.instrumentExecutionResult(executionResult, fixture.instrumentationExecutionParameters, fixture.instrumentationState) + verify(fixture.exceptionReporter, never()).captureThrowable(any(), any(), any()) + val result = resultFuture.get() + assertSame(executionResult, result) + } + + @Test + fun `does not invoke exceptionReporter for ignored errors`() { + val instrumentation = fixture.getSut(ignoredErrors = listOf("SOME_ERROR")) + val executionResult = ExecutionResult.newExecutionResult() + .data("raw result") + .addError(GraphqlErrorException.newErrorException().message("exception message").errorClassification(SomeErrorClassification.SOME_ERROR).build()) + .build() + val resultFuture = instrumentation.instrumentExecutionResult(executionResult, fixture.instrumentationExecutionParameters, fixture.instrumentationState) + 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() + val executionResult = ExecutionResult.newExecutionResult() + .data("raw result") + .build() + val resultFuture = instrumentation.instrumentExecutionResult(executionResult, fixture.instrumentationExecutionParameters, fixture.instrumentationState) + verify(fixture.exceptionReporter, never()).captureThrowable(any(), any(), any()) + val result = resultFuture.get() + assertSame(executionResult, result) + } + + fun withMockScopes(closure: () -> Unit) = Mockito.mockStatic(Sentry::class.java).use { + it.`when` { Sentry.getCurrentScopes() }.thenReturn(fixture.scopes) + closure.invoke() + } + + data class Show(val id: Int) + + enum class SomeErrorClassification : ErrorClassification { + SOME_ERROR; + } +} diff --git a/sentry-graphql-22/src/test/kotlin/io/sentry/graphql22/SentryInstrumentationTest.kt b/sentry-graphql-22/src/test/kotlin/io/sentry/graphql22/SentryInstrumentationTest.kt new file mode 100644 index 0000000000..bec8c209b1 --- /dev/null +++ b/sentry-graphql-22/src/test/kotlin/io/sentry/graphql22/SentryInstrumentationTest.kt @@ -0,0 +1,243 @@ +package io.sentry.graphql22 + +import graphql.GraphQL +import graphql.GraphQLContext +import graphql.execution.ExecutionContextBuilder +import graphql.execution.ExecutionId +import graphql.execution.ExecutionStepInfo +import graphql.execution.ExecutionStrategyParameters +import graphql.execution.MergedField +import graphql.execution.MergedSelectionSet +import graphql.execution.instrumentation.parameters.InstrumentationFieldFetchParameters +import graphql.language.Field +import graphql.language.OperationDefinition +import graphql.scalar.GraphqlStringCoercing +import graphql.schema.DataFetcher +import graphql.schema.DataFetchingEnvironmentImpl +import graphql.schema.GraphQLScalarType +import graphql.schema.idl.RuntimeWiring +import graphql.schema.idl.SchemaGenerator +import graphql.schema.idl.SchemaParser +import io.sentry.IScopes +import io.sentry.Sentry +import io.sentry.SentryOptions +import io.sentry.SentryTracer +import io.sentry.SpanStatus +import io.sentry.TransactionContext +import io.sentry.graphql.ExceptionReporter +import io.sentry.graphql.NoOpSubscriptionHandler +import io.sentry.graphql.SentryGraphqlInstrumentation +import io.sentry.graphql.SentrySubscriptionHandler +import org.mockito.Mockito +import org.mockito.kotlin.any +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever +import java.lang.RuntimeException +import kotlin.random.Random +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNotNull +import kotlin.test.assertTrue + +class SentryInstrumentationTest { + + class Fixture { + val scopes = mock() + lateinit var activeSpan: SentryTracer + + fun getSut(isTransactionActive: Boolean = true, dataFetcherThrows: Boolean = false, beforeSpan: SentryGraphqlInstrumentation.BeforeSpanCallback? = null): GraphQL { + whenever(scopes.options).thenReturn(SentryOptions()) + activeSpan = SentryTracer(TransactionContext("name", "op"), scopes) + val schema = """ + type Query { + shows: [Show] + } + + type Show { + id: Int + } + """.trimIndent() + + val graphQLSchema = SchemaGenerator().makeExecutableSchema(SchemaParser().parse(schema), buildRuntimeWiring(dataFetcherThrows)) + val graphQL = GraphQL.newGraphQL(graphQLSchema) + .instrumentation( + SentryInstrumentation( + beforeSpan, + NoOpSubscriptionHandler.getInstance(), + true + ) + ) + .build() + + if (isTransactionActive) { + whenever(scopes.span).thenReturn(activeSpan) + } else { + whenever(scopes.span).thenReturn(null) + } + + return graphQL + } + + private fun buildRuntimeWiring(dataFetcherThrows: Boolean) = RuntimeWiring.newRuntimeWiring() + .type("Query") { + it.dataFetcher("shows") { + if (dataFetcherThrows) { + throw RuntimeException("error") + } else { + listOf(Show(Random.nextInt()), Show(Random.nextInt())) + } + } + }.build() + } + + private val fixture = Fixture() + + @Test + fun `when transaction is active, creates inner spans`() { + val sut = fixture.getSut() + + withMockScopes { + val result = sut.execute("{ shows { id } }") + + assertTrue(result.errors.isEmpty()) + assertEquals(1, fixture.activeSpan.children.size) + val span = fixture.activeSpan.children.first() + assertEquals("graphql", span.operation) + assertEquals("Query.shows", span.description) + assertEquals("auto.graphql.graphql22", span.spanContext.origin) + assertTrue(span.isFinished) + assertEquals(SpanStatus.OK, span.status) + } + } + + @Test + fun `when transaction is active, and data fetcher throws, creates inner spans`() { + val sut = fixture.getSut(dataFetcherThrows = true) + + withMockScopes { + val result = sut.execute("{ shows { id } }") + + assertTrue(result.errors.isNotEmpty()) + assertEquals(1, fixture.activeSpan.children.size) + val span = fixture.activeSpan.children.first() + assertEquals("graphql", span.operation) + assertEquals("Query.shows", span.description) + assertTrue(span.isFinished) + assertEquals(SpanStatus.INTERNAL_ERROR, span.status) + } + } + + @Test + fun `when transaction is not active, does not create spans`() { + val sut = fixture.getSut(isTransactionActive = false) + + withMockScopes { + val result = sut.execute("{ shows { id } }") + + assertTrue(result.errors.isEmpty()) + assertTrue(fixture.activeSpan.children.isEmpty()) + } + } + + @Test + fun `beforeSpan can drop spans`() { + val sut = fixture.getSut(beforeSpan = SentryGraphqlInstrumentation.BeforeSpanCallback { _, _, _ -> null }) + + withMockScopes { + val result = sut.execute("{ shows { id } }") + + assertTrue(result.errors.isEmpty()) + assertEquals(1, fixture.activeSpan.children.size) + val span = fixture.activeSpan.children.first() + assertEquals("graphql", span.operation) + assertEquals("Query.shows", span.description) + assertNotNull(span.isSampled) { + assertFalse(it) + } + } + } + + @Test + fun `beforeSpan can modify spans`() { + val sut = fixture.getSut(beforeSpan = SentryGraphqlInstrumentation.BeforeSpanCallback { span, _, _ -> span.apply { description = "changed" } }) + + withMockScopes { + val result = sut.execute("{ shows { id } }") + + assertTrue(result.errors.isEmpty()) + assertEquals(1, fixture.activeSpan.children.size) + val span = fixture.activeSpan.children.first() + assertEquals("graphql", span.operation) + assertEquals("changed", span.description) + assertTrue(span.isFinished) + } + } + + @Test + fun `invokes subscription handler for subscription`() { + val exceptionReporter = mock() + 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, + emptyList() + ) + val dataFetcher = mock>() + whenever(dataFetcher.get(any())).thenReturn("raw result") + val graphQLContext = GraphQLContext.newContext().build() + val executionStepInfo = ExecutionStepInfo.newExecutionStepInfo().type( + GraphQLScalarType.newScalar().name("MyResponseType").coercing( + GraphqlStringCoercing() + ).build() + ).build() + val environment = DataFetchingEnvironmentImpl.newDataFetchingEnvironment() + .graphQLContext(graphQLContext) + .executionStepInfo(executionStepInfo) + .operationDefinition(OperationDefinition.newOperationDefinition().operation(operation).build()) + .build() + val executionContext = ExecutionContextBuilder.newExecutionContextBuilder() + .executionId(ExecutionId.generate()) + .graphQLContext(graphQLContext) + .build() + val executionStrategyParameters = ExecutionStrategyParameters.newParameters() + .executionStepInfo(executionStepInfo) + .fields(MergedSelectionSet.newMergedSelectionSet().build()) + .field(MergedField.newMergedField().addField(Field.newField("myFieldName").build()).build()) + .build() + val parameters = InstrumentationFieldFetchParameters( + executionContext, + { environment }, + executionStrategyParameters, + false + ) + val instrumentedDataFetcher = instrumentation.instrumentDataFetcher(dataFetcher, parameters, SentryGraphqlInstrumentation.TracingState()) + val result = instrumentedDataFetcher.get(environment) + + assertNotNull(result) + assertEquals("result modified by subscription handler", result) + } + + @Test + fun `Integration adds itself to integration and package list`() { + withMockScopes { + val sut = fixture.getSut() + assertNotNull(fixture.scopes.options.sdkVersion) + assert(fixture.scopes.options.sdkVersion!!.integrationSet.contains("GraphQL-v22")) + val packageInfo = + fixture.scopes.options.sdkVersion!!.packageSet.firstOrNull { pkg -> pkg.name == "maven:io.sentry:sentry-graphql-22" } + assertNotNull(packageInfo) + assert(packageInfo.version == BuildConfig.VERSION_NAME) + } + } + + fun withMockScopes(closure: () -> Unit) = Mockito.mockStatic(Sentry::class.java).use { + it.`when` { Sentry.getCurrentScopes() }.thenReturn(fixture.scopes) + closure.invoke() + } + + data class Show(val id: Int) +} diff --git a/sentry-graphql-core/api/sentry-graphql-core.api b/sentry-graphql-core/api/sentry-graphql-core.api new file mode 100644 index 0000000000..95dede49a2 --- /dev/null +++ b/sentry-graphql-core/api/sentry-graphql-core.api @@ -0,0 +1,77 @@ +public final class io/sentry/graphql/BuildConfig { + public static final field SENTRY_GRAPHQL_SDK_NAME Ljava/lang/String; + public static final field VERSION_NAME Ljava/lang/String; +} + +public final class io/sentry/graphql/ExceptionReporter { + public fun (Z)V + public fun captureThrowable (Ljava/lang/Throwable;Lio/sentry/graphql/ExceptionReporter$ExceptionDetails;Lgraphql/ExecutionResult;)V +} + +public final class io/sentry/graphql/ExceptionReporter$ExceptionDetails { + public fun (Lio/sentry/IScopes;Lgraphql/execution/instrumentation/parameters/InstrumentationExecutionParameters;Z)V + public fun (Lio/sentry/IScopes;Lgraphql/schema/DataFetchingEnvironment;Z)V + public fun getHub ()Lio/sentry/IScopes; + public fun getQuery ()Ljava/lang/String; + public fun getScopes ()Lio/sentry/IScopes; + public fun getVariables ()Ljava/util/Map; + public fun isSubscription ()Z +} + +public final class io/sentry/graphql/GraphqlStringUtils { + public fun ()V + public static fun fieldToString (Lgraphql/execution/MergedField;)Ljava/lang/String; + public static fun objectTypeToString (Lgraphql/schema/GraphQLObjectType;)Ljava/lang/String; + public static fun typeToString (Lgraphql/schema/GraphQLOutputType;)Ljava/lang/String; +} + +public final class io/sentry/graphql/NoOpSubscriptionHandler : io/sentry/graphql/SentrySubscriptionHandler { + public static fun getInstance ()Lio/sentry/graphql/NoOpSubscriptionHandler; + public fun onSubscriptionResult (Ljava/lang/Object;Lio/sentry/IScopes;Lio/sentry/graphql/ExceptionReporter;Lgraphql/execution/instrumentation/parameters/InstrumentationFieldFetchParameters;)Ljava/lang/Object; +} + +public final class io/sentry/graphql/SentryDataFetcherExceptionHandler : graphql/execution/DataFetcherExceptionHandler { + public fun (Lgraphql/execution/DataFetcherExceptionHandler;)V + public fun (Lio/sentry/IScopes;Lgraphql/execution/DataFetcherExceptionHandler;)V + public fun handleException (Lgraphql/execution/DataFetcherExceptionHandlerParameters;)Ljava/util/concurrent/CompletableFuture; + public fun onException (Lgraphql/execution/DataFetcherExceptionHandlerParameters;)Lgraphql/execution/DataFetcherExceptionHandlerResult; +} + +public final class io/sentry/graphql/SentryGenericDataFetcherExceptionHandler : graphql/execution/DataFetcherExceptionHandler { + public fun (Lgraphql/execution/DataFetcherExceptionHandler;)V + public fun (Lio/sentry/IScopes;Lgraphql/execution/DataFetcherExceptionHandler;)V + public fun handleException (Lgraphql/execution/DataFetcherExceptionHandlerParameters;)Ljava/util/concurrent/CompletableFuture; + public fun onException (Lgraphql/execution/DataFetcherExceptionHandlerParameters;)Lgraphql/execution/DataFetcherExceptionHandlerResult; +} + +public final class io/sentry/graphql/SentryGraphqlExceptionHandler { + public fun (Lgraphql/execution/DataFetcherExceptionHandler;)V + public fun handleException (Ljava/lang/Throwable;Lgraphql/schema/DataFetchingEnvironment;Lgraphql/execution/DataFetcherExceptionHandlerParameters;)Ljava/util/concurrent/CompletableFuture; +} + +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;ZLjava/util/List;Ljava/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; + public fun instrumentDataFetcher (Lgraphql/schema/DataFetcher;Lgraphql/execution/instrumentation/parameters/InstrumentationFieldFetchParameters;Lio/sentry/graphql/SentryGraphqlInstrumentation$TracingState;)Lgraphql/schema/DataFetcher; + public fun instrumentExecutionResultComplete (Lgraphql/execution/instrumentation/parameters/InstrumentationExecutionParameters;Lgraphql/ExecutionResult;Ljava/lang/Throwable;)V +} + +public abstract interface class io/sentry/graphql/SentryGraphqlInstrumentation$BeforeSpanCallback { + public abstract fun execute (Lio/sentry/ISpan;Lgraphql/schema/DataFetchingEnvironment;Ljava/lang/Object;)Lio/sentry/ISpan; +} + +public final class io/sentry/graphql/SentryGraphqlInstrumentation$TracingState : graphql/execution/instrumentation/InstrumentationState { + public fun ()V + public fun getTransaction ()Lio/sentry/ISpan; + public fun setTransaction (Lio/sentry/ISpan;)V +} + +public abstract interface class io/sentry/graphql/SentrySubscriptionHandler { + public abstract fun onSubscriptionResult (Ljava/lang/Object;Lio/sentry/IScopes;Lio/sentry/graphql/ExceptionReporter;Lgraphql/execution/instrumentation/parameters/InstrumentationFieldFetchParameters;)Ljava/lang/Object; +} + diff --git a/sentry-graphql-core/build.gradle.kts b/sentry-graphql-core/build.gradle.kts new file mode 100644 index 0000000000..ed1c197acd --- /dev/null +++ b/sentry-graphql-core/build.gradle.kts @@ -0,0 +1,88 @@ +import net.ltgt.gradle.errorprone.errorprone +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +plugins { + `java-library` + kotlin("jvm") + jacoco + id(Config.QualityPlugins.errorProne) + id(Config.QualityPlugins.gradleVersions) + id(Config.BuildPlugins.buildConfig) version Config.BuildPlugins.buildConfigVersion +} + +configure { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 +} + +tasks.withType().configureEach { + kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8.toString() + kotlinOptions.languageVersion = Config.kotlinCompatibleLanguageVersion +} + +dependencies { + api(projects.sentry) + compileOnly(Config.Libs.graphQlJava) + + compileOnly(Config.CompileOnly.nopen) + errorprone(Config.CompileOnly.nopenChecker) + errorprone(Config.CompileOnly.errorprone) + errorprone(Config.CompileOnly.errorProneNullAway) + compileOnly(Config.CompileOnly.jetbrainsAnnotations) + + // tests + testImplementation(projects.sentry) + testImplementation(projects.sentryTestSupport) + testImplementation(kotlin(Config.kotlinStdLib)) + testImplementation(Config.TestLibs.kotlinTestJunit) + testImplementation(Config.TestLibs.mockitoKotlin) + testImplementation(Config.TestLibs.mockitoInline) + testImplementation(Config.TestLibs.mockWebserver) + testImplementation(Config.Libs.okhttp) + testImplementation(Config.Libs.springBootStarterGraphql) + testImplementation("com.netflix.graphql.dgs:graphql-error-types:4.9.2") + testImplementation(Config.Libs.graphQlJava) +} + +configure { + test { + java.srcDir("src/test/java") + } +} + +jacoco { + toolVersion = Config.QualityPlugins.Jacoco.version +} + +tasks.jacocoTestReport { + reports { + xml.required.set(true) + html.required.set(false) + } +} + +tasks { + jacocoTestCoverageVerification { + violationRules { + rule { limit { minimum = Config.QualityPlugins.Jacoco.minimumCoverage } } + } + } + check { + dependsOn(jacocoTestCoverageVerification) + dependsOn(jacocoTestReport) + } +} + +tasks.withType().configureEach { + options.errorprone { + check("NullAway", net.ltgt.gradle.errorprone.CheckSeverity.ERROR) + option("NullAway:AnnotatedPackages", "io.sentry") + } +} + +buildConfig { + useJavaOutput() + packageName("io.sentry.graphql") + buildConfigField("String", "SENTRY_GRAPHQL_SDK_NAME", "\"${Config.Sentry.SENTRY_GRAPHQL_SDK_NAME}\"") + buildConfigField("String", "VERSION_NAME", "\"${project.version}\"") +} diff --git a/sentry-graphql/src/main/java/io/sentry/graphql/ExceptionReporter.java b/sentry-graphql-core/src/main/java/io/sentry/graphql/ExceptionReporter.java similarity index 100% rename from sentry-graphql/src/main/java/io/sentry/graphql/ExceptionReporter.java rename to sentry-graphql-core/src/main/java/io/sentry/graphql/ExceptionReporter.java diff --git a/sentry-graphql/src/main/java/io/sentry/graphql/GraphqlStringUtils.java b/sentry-graphql-core/src/main/java/io/sentry/graphql/GraphqlStringUtils.java similarity index 100% rename from sentry-graphql/src/main/java/io/sentry/graphql/GraphqlStringUtils.java rename to sentry-graphql-core/src/main/java/io/sentry/graphql/GraphqlStringUtils.java diff --git a/sentry-graphql/src/main/java/io/sentry/graphql/NoOpSubscriptionHandler.java b/sentry-graphql-core/src/main/java/io/sentry/graphql/NoOpSubscriptionHandler.java similarity index 100% rename from sentry-graphql/src/main/java/io/sentry/graphql/NoOpSubscriptionHandler.java rename to sentry-graphql-core/src/main/java/io/sentry/graphql/NoOpSubscriptionHandler.java diff --git a/sentry-graphql/src/main/java/io/sentry/graphql/SentryDataFetcherExceptionHandler.java b/sentry-graphql-core/src/main/java/io/sentry/graphql/SentryDataFetcherExceptionHandler.java similarity index 100% rename from sentry-graphql/src/main/java/io/sentry/graphql/SentryDataFetcherExceptionHandler.java rename to sentry-graphql-core/src/main/java/io/sentry/graphql/SentryDataFetcherExceptionHandler.java diff --git a/sentry-graphql/src/main/java/io/sentry/graphql/SentryGenericDataFetcherExceptionHandler.java b/sentry-graphql-core/src/main/java/io/sentry/graphql/SentryGenericDataFetcherExceptionHandler.java similarity index 100% rename from sentry-graphql/src/main/java/io/sentry/graphql/SentryGenericDataFetcherExceptionHandler.java rename to sentry-graphql-core/src/main/java/io/sentry/graphql/SentryGenericDataFetcherExceptionHandler.java diff --git a/sentry-graphql/src/main/java/io/sentry/graphql/SentryGraphqlExceptionHandler.java b/sentry-graphql-core/src/main/java/io/sentry/graphql/SentryGraphqlExceptionHandler.java similarity index 95% rename from sentry-graphql/src/main/java/io/sentry/graphql/SentryGraphqlExceptionHandler.java rename to sentry-graphql-core/src/main/java/io/sentry/graphql/SentryGraphqlExceptionHandler.java index 7da3dbfc91..4b178c498f 100644 --- a/sentry-graphql/src/main/java/io/sentry/graphql/SentryGraphqlExceptionHandler.java +++ b/sentry-graphql-core/src/main/java/io/sentry/graphql/SentryGraphqlExceptionHandler.java @@ -1,6 +1,6 @@ package io.sentry.graphql; -import static io.sentry.graphql.SentryInstrumentation.SENTRY_EXCEPTIONS_CONTEXT_KEY; +import static io.sentry.graphql.SentryGraphqlInstrumentation.SENTRY_EXCEPTIONS_CONTEXT_KEY; import graphql.GraphQLContext; import graphql.execution.DataFetcherExceptionHandler; 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 new file mode 100644 index 0000000000..2ba7cd36e9 --- /dev/null +++ b/sentry-graphql-core/src/main/java/io/sentry/graphql/SentryGraphqlInstrumentation.java @@ -0,0 +1,341 @@ +package io.sentry.graphql; + +import graphql.ErrorClassification; +import graphql.ExecutionResult; +import graphql.GraphQLContext; +import graphql.GraphQLError; +import graphql.execution.ExecutionContext; +import graphql.execution.ExecutionStepInfo; +import graphql.execution.instrumentation.InstrumentationState; +import graphql.execution.instrumentation.parameters.InstrumentationExecuteOperationParameters; +import graphql.execution.instrumentation.parameters.InstrumentationExecutionParameters; +import graphql.execution.instrumentation.parameters.InstrumentationFieldFetchParameters; +import graphql.language.OperationDefinition; +import graphql.schema.DataFetcher; +import graphql.schema.DataFetchingEnvironment; +import graphql.schema.GraphQLNonNull; +import graphql.schema.GraphQLObjectType; +import graphql.schema.GraphQLOutputType; +import io.sentry.Breadcrumb; +import io.sentry.Hint; +import io.sentry.IScopes; +import io.sentry.ISpan; +import io.sentry.NoOpScopes; +import io.sentry.Sentry; +import io.sentry.SpanOptions; +import io.sentry.SpanStatus; +import io.sentry.TypeCheckHint; +import io.sentry.util.StringUtils; +import java.util.Arrays; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CopyOnWriteArrayList; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.TestOnly; + +public final class SentryGraphqlInstrumentation { + + public static final @NotNull String SENTRY_SCOPES_CONTEXT_KEY = "sentry.scopes"; + public static final @NotNull String SENTRY_EXCEPTIONS_CONTEXT_KEY = "sentry.exceptions"; + + private static final @NotNull List ERROR_TYPES_HANDLED_BY_DATA_FETCHERS = + Arrays.asList( + "INTERNAL_ERROR", // spring-graphql + "INTERNAL", // Netflix DGS + "DataFetchingException" // raw graphql-java + ); + private final @Nullable BeforeSpanCallback beforeSpan; + private final @NotNull SentrySubscriptionHandler subscriptionHandler; + private final @NotNull ExceptionReporter exceptionReporter; + private final @NotNull List ignoredErrorTypes; + private final @NotNull String 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. 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 SentryGraphqlInstrumentation( + final @Nullable BeforeSpanCallback beforeSpan, + final @NotNull SentrySubscriptionHandler subscriptionHandler, + final boolean captureRequestBodyForNonSubscriptions, + final @NotNull List ignoredErrorTypes, + final @NotNull String traceOrigin) { + this( + beforeSpan, + subscriptionHandler, + new ExceptionReporter(captureRequestBodyForNonSubscriptions), + ignoredErrorTypes, + traceOrigin); + } + + @TestOnly + public SentryGraphqlInstrumentation( + final @Nullable BeforeSpanCallback beforeSpan, + final @NotNull SentrySubscriptionHandler subscriptionHandler, + final @NotNull ExceptionReporter exceptionReporter, + final @NotNull List ignoredErrorTypes, + final @NotNull String traceOrigin) { + this.beforeSpan = beforeSpan; + this.subscriptionHandler = subscriptionHandler; + this.exceptionReporter = exceptionReporter; + this.ignoredErrorTypes = ignoredErrorTypes; + this.traceOrigin = traceOrigin; + } + + public @NotNull InstrumentationState createState() { + return new TracingState(); + } + + public @NotNull void beginExecution( + final @NotNull InstrumentationExecutionParameters parameters, + final @NotNull TracingState tracingState) { + final @NotNull IScopes currentScopes = Sentry.getCurrentScopes(); + tracingState.setTransaction(currentScopes.getSpan()); + parameters.getGraphQLContext().put(SENTRY_SCOPES_CONTEXT_KEY, currentScopes); + } + + public void instrumentExecutionResultComplete( + final @NotNull InstrumentationExecutionParameters parameters, + final @Nullable ExecutionResult result, + final @Nullable Throwable exception) { + if (result != null) { + final @Nullable GraphQLContext graphQLContext = parameters.getGraphQLContext(); + if (graphQLContext != null) { + final @NotNull List exceptions = + graphQLContext.getOrDefault( + SENTRY_EXCEPTIONS_CONTEXT_KEY, new CopyOnWriteArrayList()); + for (Throwable throwable : exceptions) { + exceptionReporter.captureThrowable( + throwable, + new ExceptionReporter.ExceptionDetails( + scopesFromContext(graphQLContext), parameters, false), + result); + } + } + final @NotNull List errors = result.getErrors(); + if (errors != null) { + for (GraphQLError error : errors) { + String errorType = getErrorType(error); + if (!isIgnored(errorType)) { + exceptionReporter.captureThrowable( + new RuntimeException(error.getMessage()), + new ExceptionReporter.ExceptionDetails( + scopesFromContext(graphQLContext), parameters, false), + result); + } + } + } + } + if (exception != null) { + exceptionReporter.captureThrowable( + exception, + new ExceptionReporter.ExceptionDetails( + scopesFromContext(parameters.getGraphQLContext()), parameters, false), + null); + } + } + + 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; + } + final @Nullable ErrorClassification errorType = error.getErrorType(); + if (errorType != null) { + return errorType.toString(); + } + final @Nullable Map extensions = error.getExtensions(); + if (extensions != null) { + return StringUtils.toString(extensions.get("errorType")); + } + return null; + } + + public @NotNull void beginExecuteOperation( + final @NotNull InstrumentationExecuteOperationParameters parameters) { + final @Nullable ExecutionContext executionContext = parameters.getExecutionContext(); + if (executionContext != null) { + final @Nullable OperationDefinition operationDefinition = + executionContext.getOperationDefinition(); + if (operationDefinition != null) { + final @Nullable OperationDefinition.Operation operation = + operationDefinition.getOperation(); + final @Nullable String operationType = + operation == null ? null : operation.name().toLowerCase(Locale.ROOT); + scopesFromContext(parameters.getExecutionContext().getGraphQLContext()) + .addBreadcrumb( + Breadcrumb.graphqlOperation( + operationDefinition.getName(), + operationType, + StringUtils.toString(executionContext.getExecutionId()))); + } + } + } + + private @NotNull IScopes scopesFromContext(final @Nullable GraphQLContext context) { + if (context == null) { + return NoOpScopes.getInstance(); + } + return context.getOrDefault(SENTRY_SCOPES_CONTEXT_KEY, NoOpScopes.getInstance()); + } + + @SuppressWarnings({"FutureReturnValueIgnored", "deprecation"}) + public @NotNull DataFetcher instrumentDataFetcher( + final @NotNull DataFetcher dataFetcher, + final @NotNull InstrumentationFieldFetchParameters parameters, + final @NotNull TracingState tracingState) { + // We only care about user code + if (parameters.isTrivialDataFetcher()) { + return dataFetcher; + } + + return environment -> { + final @Nullable ExecutionStepInfo executionStepInfo = environment.getExecutionStepInfo(); + if (executionStepInfo != null) { + Hint hint = new Hint(); + hint.set(TypeCheckHint.GRAPHQL_DATA_FETCHING_ENVIRONMENT, environment); + scopesFromContext(parameters.getExecutionContext().getGraphQLContext()) + .addBreadcrumb( + Breadcrumb.graphqlDataFetcher( + StringUtils.toString(executionStepInfo.getPath()), + GraphqlStringUtils.fieldToString(executionStepInfo.getField()), + GraphqlStringUtils.typeToString(executionStepInfo.getType()), + GraphqlStringUtils.objectTypeToString(executionStepInfo.getObjectType())), + hint); + } + final ISpan transaction = tracingState.getTransaction(); + if (transaction != null) { + final ISpan span = createSpan(transaction, parameters); + try { + final @Nullable Object tmpResult = dataFetcher.get(environment); + final @Nullable Object result = + maybeCallSubscriptionHandler(parameters, environment, tmpResult); + if (result instanceof CompletableFuture) { + ((CompletableFuture) result) + .whenComplete( + (r, ex) -> { + if (ex != null) { + span.setThrowable(ex); + span.setStatus(SpanStatus.INTERNAL_ERROR); + } else { + span.setStatus(SpanStatus.OK); + } + finish(span, environment, r); + }); + } else { + span.setStatus(SpanStatus.OK); + finish(span, environment, result); + } + return result; + } catch (Throwable e) { + span.setThrowable(e); + span.setStatus(SpanStatus.INTERNAL_ERROR); + finish(span, environment); + throw e; + } + } else { + final Object result = dataFetcher.get(environment); + return maybeCallSubscriptionHandler(parameters, environment, result); + } + }; + } + + private @Nullable Object maybeCallSubscriptionHandler( + final @NotNull InstrumentationFieldFetchParameters parameters, + final @NotNull DataFetchingEnvironment environment, + final @Nullable Object tmpResult) { + if (tmpResult == null) { + return null; + } + + if (OperationDefinition.Operation.SUBSCRIPTION.equals( + environment.getOperationDefinition().getOperation())) { + return subscriptionHandler.onSubscriptionResult( + tmpResult, + scopesFromContext(environment.getGraphQlContext()), + exceptionReporter, + parameters); + } + + return tmpResult; + } + + private void finish( + final @NotNull ISpan span, + final @NotNull DataFetchingEnvironment environment, + final @Nullable Object result) { + if (beforeSpan != null) { + final ISpan newSpan = beforeSpan.execute(span, environment, result); + if (newSpan == null) { + // span is dropped + span.getSpanContext().setSampled(false); + } else { + newSpan.finish(); + } + } else { + span.finish(); + } + } + + private void finish( + final @NotNull ISpan span, final @NotNull DataFetchingEnvironment environment) { + finish(span, environment, null); + } + + private @NotNull ISpan createSpan( + @NotNull ISpan transaction, @NotNull InstrumentationFieldFetchParameters parameters) { + final GraphQLOutputType type = parameters.getExecutionStepInfo().getParent().getType(); + GraphQLObjectType parent; + if (type instanceof GraphQLNonNull) { + parent = (GraphQLObjectType) ((GraphQLNonNull) type).getWrappedType(); + } else { + parent = (GraphQLObjectType) type; + } + final @NotNull SpanOptions spanOptions = new SpanOptions(); + spanOptions.setOrigin(traceOrigin); + final @NotNull ISpan span = + transaction.startChild( + "graphql", + parent.getName() + "." + parameters.getExecutionStepInfo().getPath().getSegmentName(), + spanOptions); + + return span; + } + + public static final class TracingState implements InstrumentationState { + private @Nullable ISpan transaction; + + public @Nullable ISpan getTransaction() { + return transaction; + } + + public void setTransaction(final @Nullable ISpan transaction) { + this.transaction = transaction; + } + } + + @FunctionalInterface + public interface BeforeSpanCallback { + @Nullable + ISpan execute( + @NotNull ISpan span, @NotNull DataFetchingEnvironment environment, @Nullable Object result); + } +} diff --git a/sentry-graphql/src/main/java/io/sentry/graphql/SentrySubscriptionHandler.java b/sentry-graphql-core/src/main/java/io/sentry/graphql/SentrySubscriptionHandler.java similarity index 100% rename from sentry-graphql/src/main/java/io/sentry/graphql/SentrySubscriptionHandler.java rename to sentry-graphql-core/src/main/java/io/sentry/graphql/SentrySubscriptionHandler.java diff --git a/sentry-graphql/src/test/kotlin/io/sentry/graphql/ExceptionReporterTest.kt b/sentry-graphql-core/src/test/kotlin/io/sentry/graphql/ExceptionReporterTest.kt similarity index 99% rename from sentry-graphql/src/test/kotlin/io/sentry/graphql/ExceptionReporterTest.kt rename to sentry-graphql-core/src/test/kotlin/io/sentry/graphql/ExceptionReporterTest.kt index 3a798a2f86..df561f7169 100644 --- a/sentry-graphql/src/test/kotlin/io/sentry/graphql/ExceptionReporterTest.kt +++ b/sentry-graphql-core/src/test/kotlin/io/sentry/graphql/ExceptionReporterTest.kt @@ -75,7 +75,7 @@ class ExceptionReporterTest { field ).build() ).build() - val instrumentationState = SentryInstrumentation.TracingState() + val instrumentationState = SentryGraphqlInstrumentation.TracingState() instrumentationExecutionParameters = InstrumentationExecutionParameters(executionInput, schema, instrumentationState) doAnswer { (it.arguments[0] as ScopeCallback).run(scope) }.whenever(scopes).configureScope(any()) diff --git a/sentry-graphql/src/test/kotlin/io/sentry/graphql/GraphqlStringUtilsTest.kt b/sentry-graphql-core/src/test/kotlin/io/sentry/graphql/GraphqlStringUtilsTest.kt similarity index 100% rename from sentry-graphql/src/test/kotlin/io/sentry/graphql/GraphqlStringUtilsTest.kt rename to sentry-graphql-core/src/test/kotlin/io/sentry/graphql/GraphqlStringUtilsTest.kt diff --git a/sentry-graphql/src/test/kotlin/io/sentry/graphql/SentryDataFetcherExceptionHandlerTest.kt b/sentry-graphql-core/src/test/kotlin/io/sentry/graphql/SentryDataFetcherExceptionHandlerTest.kt similarity index 100% rename from sentry-graphql/src/test/kotlin/io/sentry/graphql/SentryDataFetcherExceptionHandlerTest.kt rename to sentry-graphql-core/src/test/kotlin/io/sentry/graphql/SentryDataFetcherExceptionHandlerTest.kt diff --git a/sentry-graphql/src/test/kotlin/io/sentry/graphql/SentryGenericDataFetcherExceptionHandlerTest.kt b/sentry-graphql-core/src/test/kotlin/io/sentry/graphql/SentryGenericDataFetcherExceptionHandlerTest.kt similarity index 94% rename from sentry-graphql/src/test/kotlin/io/sentry/graphql/SentryGenericDataFetcherExceptionHandlerTest.kt rename to sentry-graphql-core/src/test/kotlin/io/sentry/graphql/SentryGenericDataFetcherExceptionHandlerTest.kt index 88e2f5df55..ee8cf36d77 100644 --- a/sentry-graphql/src/test/kotlin/io/sentry/graphql/SentryGenericDataFetcherExceptionHandlerTest.kt +++ b/sentry-graphql-core/src/test/kotlin/io/sentry/graphql/SentryGenericDataFetcherExceptionHandlerTest.kt @@ -32,7 +32,7 @@ class SentryGenericDataFetcherExceptionHandlerTest { ).build() handler.onException(parameters) - val exceptions: List = parameters.dataFetchingEnvironment.graphQlContext[SentryInstrumentation.SENTRY_EXCEPTIONS_CONTEXT_KEY] + val exceptions: List = parameters.dataFetchingEnvironment.graphQlContext[SentryGraphqlInstrumentation.SENTRY_EXCEPTIONS_CONTEXT_KEY] assertNotNull(exceptions) assertEquals(1, exceptions.size) assertEquals(exception, exceptions.first()) diff --git a/sentry-graphql/api/sentry-graphql.api b/sentry-graphql/api/sentry-graphql.api index d119256010..7c0b278254 100644 --- a/sentry-graphql/api/sentry-graphql.api +++ b/sentry-graphql/api/sentry-graphql.api @@ -3,63 +3,10 @@ public final class io/sentry/graphql/BuildConfig { public static final field VERSION_NAME Ljava/lang/String; } -public final class io/sentry/graphql/ExceptionReporter { - public fun (Z)V - public fun captureThrowable (Ljava/lang/Throwable;Lio/sentry/graphql/ExceptionReporter$ExceptionDetails;Lgraphql/ExecutionResult;)V -} - -public final class io/sentry/graphql/ExceptionReporter$ExceptionDetails { - public fun (Lio/sentry/IScopes;Lgraphql/execution/instrumentation/parameters/InstrumentationExecutionParameters;Z)V - public fun (Lio/sentry/IScopes;Lgraphql/schema/DataFetchingEnvironment;Z)V - public fun getHub ()Lio/sentry/IScopes; - public fun getQuery ()Ljava/lang/String; - public fun getScopes ()Lio/sentry/IScopes; - public fun getVariables ()Ljava/util/Map; - public fun isSubscription ()Z -} - -public final class io/sentry/graphql/GraphqlStringUtils { - public fun ()V - public static fun fieldToString (Lgraphql/execution/MergedField;)Ljava/lang/String; - public static fun objectTypeToString (Lgraphql/schema/GraphQLObjectType;)Ljava/lang/String; - public static fun typeToString (Lgraphql/schema/GraphQLOutputType;)Ljava/lang/String; -} - -public final class io/sentry/graphql/NoOpSubscriptionHandler : io/sentry/graphql/SentrySubscriptionHandler { - public static fun getInstance ()Lio/sentry/graphql/NoOpSubscriptionHandler; - public fun onSubscriptionResult (Ljava/lang/Object;Lio/sentry/IScopes;Lio/sentry/graphql/ExceptionReporter;Lgraphql/execution/instrumentation/parameters/InstrumentationFieldFetchParameters;)Ljava/lang/Object; -} - -public final class io/sentry/graphql/SentryDataFetcherExceptionHandler : graphql/execution/DataFetcherExceptionHandler { - public fun (Lgraphql/execution/DataFetcherExceptionHandler;)V - public fun (Lio/sentry/IScopes;Lgraphql/execution/DataFetcherExceptionHandler;)V - public fun handleException (Lgraphql/execution/DataFetcherExceptionHandlerParameters;)Ljava/util/concurrent/CompletableFuture; - public fun onException (Lgraphql/execution/DataFetcherExceptionHandlerParameters;)Lgraphql/execution/DataFetcherExceptionHandlerResult; -} - -public final class io/sentry/graphql/SentryGenericDataFetcherExceptionHandler : graphql/execution/DataFetcherExceptionHandler { - public fun (Lgraphql/execution/DataFetcherExceptionHandler;)V - public fun (Lio/sentry/IScopes;Lgraphql/execution/DataFetcherExceptionHandler;)V - public fun handleException (Lgraphql/execution/DataFetcherExceptionHandlerParameters;)Ljava/util/concurrent/CompletableFuture; - public fun onException (Lgraphql/execution/DataFetcherExceptionHandlerParameters;)Lgraphql/execution/DataFetcherExceptionHandlerResult; -} - -public final class io/sentry/graphql/SentryGraphqlExceptionHandler { - public fun (Lgraphql/execution/DataFetcherExceptionHandler;)V - public fun handleException (Ljava/lang/Throwable;Lgraphql/schema/DataFetchingEnvironment;Lgraphql/execution/DataFetcherExceptionHandlerParameters;)Ljava/util/concurrent/CompletableFuture; -} - public final class io/sentry/graphql/SentryInstrumentation : graphql/execution/instrumentation/SimpleInstrumentation { - public static final field SENTRY_EXCEPTIONS_CONTEXT_KEY Ljava/lang/String; - public static final field SENTRY_HUB_CONTEXT_KEY Ljava/lang/String; - public static final field SENTRY_SCOPES_CONTEXT_KEY Ljava/lang/String; - public fun ()V - public fun (Lio/sentry/IScopes;)V - public fun (Lio/sentry/IScopes;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;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/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;Z)V + public fun (Lio/sentry/graphql/SentryGraphqlInstrumentation$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; @@ -68,11 +15,3 @@ public final class io/sentry/graphql/SentryInstrumentation : graphql/execution/i public fun instrumentExecutionResult (Lgraphql/ExecutionResult;Lgraphql/execution/instrumentation/parameters/InstrumentationExecutionParameters;)Ljava/util/concurrent/CompletableFuture; } -public abstract interface class io/sentry/graphql/SentryInstrumentation$BeforeSpanCallback { - public abstract fun execute (Lio/sentry/ISpan;Lgraphql/schema/DataFetchingEnvironment;Ljava/lang/Object;)Lio/sentry/ISpan; -} - -public abstract interface class io/sentry/graphql/SentrySubscriptionHandler { - public abstract fun onSubscriptionResult (Ljava/lang/Object;Lio/sentry/IScopes;Lio/sentry/graphql/ExceptionReporter;Lgraphql/execution/instrumentation/parameters/InstrumentationFieldFetchParameters;)Ljava/lang/Object; -} - diff --git a/sentry-graphql/build.gradle.kts b/sentry-graphql/build.gradle.kts index ed1c197acd..f0de17f288 100644 --- a/sentry-graphql/build.gradle.kts +++ b/sentry-graphql/build.gradle.kts @@ -22,6 +22,7 @@ tasks.withType().configureEach { dependencies { api(projects.sentry) + api(projects.sentryGraphqlCore) compileOnly(Config.Libs.graphQlJava) compileOnly(Config.CompileOnly.nopen) 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 9a853faf38..c4b5ef0f0e 100644 --- a/sentry-graphql/src/main/java/io/sentry/graphql/SentryInstrumentation.java +++ b/sentry-graphql/src/main/java/io/sentry/graphql/SentryInstrumentation.java @@ -1,40 +1,16 @@ package io.sentry.graphql; -import graphql.ErrorClassification; import graphql.ExecutionResult; -import graphql.GraphQLContext; -import graphql.GraphQLError; -import graphql.execution.ExecutionContext; -import graphql.execution.ExecutionStepInfo; import graphql.execution.instrumentation.InstrumentationContext; import graphql.execution.instrumentation.InstrumentationState; import graphql.execution.instrumentation.parameters.InstrumentationExecuteOperationParameters; import graphql.execution.instrumentation.parameters.InstrumentationExecutionParameters; import graphql.execution.instrumentation.parameters.InstrumentationFieldFetchParameters; -import graphql.language.OperationDefinition; import graphql.schema.DataFetcher; -import graphql.schema.DataFetchingEnvironment; -import graphql.schema.GraphQLNonNull; -import graphql.schema.GraphQLObjectType; -import graphql.schema.GraphQLOutputType; -import io.sentry.Breadcrumb; -import io.sentry.Hint; -import io.sentry.IScopes; -import io.sentry.ISpan; -import io.sentry.NoOpScopes; -import io.sentry.Sentry; import io.sentry.SentryIntegrationPackageStorage; -import io.sentry.SpanOptions; -import io.sentry.SpanStatus; -import io.sentry.TypeCheckHint; -import io.sentry.util.StringUtils; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; -import java.util.Locale; -import java.util.Map; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CopyOnWriteArrayList; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.TestOnly; @@ -43,65 +19,8 @@ public final class SentryInstrumentation extends graphql.execution.instrumentation.SimpleInstrumentation { - private static final @NotNull List ERROR_TYPES_HANDLED_BY_DATA_FETCHERS = - Arrays.asList( - "INTERNAL_ERROR", // spring-graphql - "INTERNAL", // Netflix DGS - "DataFetchingException" // raw graphql-java - ); - public static final @NotNull String SENTRY_SCOPES_CONTEXT_KEY = "sentry.scopes"; - - /** - * @deprecated please use {@link SentryInstrumentation#SENTRY_SCOPES_CONTEXT_KEY} instead. - */ - @Deprecated - public static final @NotNull String SENTRY_HUB_CONTEXT_KEY = SENTRY_SCOPES_CONTEXT_KEY; - - public static final @NotNull String SENTRY_EXCEPTIONS_CONTEXT_KEY = "sentry.exceptions"; private static final String TRACE_ORIGIN = "auto.graphql.graphql"; - private final @Nullable BeforeSpanCallback beforeSpan; - private final @NotNull SentrySubscriptionHandler subscriptionHandler; - - private final @NotNull ExceptionReporter exceptionReporter; - - private final @NotNull List ignoredErrorTypes; - - /** - * @deprecated please use a constructor that takes a {@link SentrySubscriptionHandler} instead. - */ - @Deprecated - @SuppressWarnings("InlineMeSuggester") - public SentryInstrumentation() { - this(null, NoOpSubscriptionHandler.getInstance(), true); - } - - /** - * @deprecated please use a constructor that takes a {@link SentrySubscriptionHandler} instead. - */ - @Deprecated - @SuppressWarnings("InlineMeSuggester") - public SentryInstrumentation(final @Nullable IScopes scopes) { - this(null, NoOpSubscriptionHandler.getInstance(), true); - } - - /** - * @deprecated please use a constructor that takes a {@link SentrySubscriptionHandler} instead. - */ - @Deprecated - @SuppressWarnings("InlineMeSuggester") - public SentryInstrumentation(final @Nullable BeforeSpanCallback beforeSpan) { - this(beforeSpan, NoOpSubscriptionHandler.getInstance(), true); - } - - /** - * @deprecated please use a constructor that takes a {@link SentrySubscriptionHandler} instead. - */ - @Deprecated - @SuppressWarnings("InlineMeSuggester") - public SentryInstrumentation( - final @Nullable IScopes scopes, final @Nullable BeforeSpanCallback beforeSpan) { - this(beforeSpan, NoOpSubscriptionHandler.getInstance(), true); - } + private final @NotNull SentryGraphqlInstrumentation instrumentation; /** * @param beforeSpan callback when a span is created @@ -112,7 +31,7 @@ public SentryInstrumentation( * case with our spring integration for WebMVC. */ public SentryInstrumentation( - final @Nullable BeforeSpanCallback beforeSpan, + final @Nullable SentryGraphqlInstrumentation.BeforeSpanCallback beforeSpan, final @NotNull SentrySubscriptionHandler subscriptionHandler, final boolean captureRequestBodyForNonSubscriptions) { this( @@ -132,7 +51,7 @@ public SentryInstrumentation( * @param ignoredErrorTypes list of error types that should not be captured and sent to Sentry */ public SentryInstrumentation( - final @Nullable BeforeSpanCallback beforeSpan, + final @Nullable SentryGraphqlInstrumentation.BeforeSpanCallback beforeSpan, final @NotNull SentrySubscriptionHandler subscriptionHandler, final boolean captureRequestBodyForNonSubscriptions, final @NotNull List ignoredErrorTypes) { @@ -145,14 +64,13 @@ public SentryInstrumentation( @TestOnly public SentryInstrumentation( - final @Nullable BeforeSpanCallback beforeSpan, + final @Nullable SentryGraphqlInstrumentation.BeforeSpanCallback beforeSpan, final @NotNull SentrySubscriptionHandler subscriptionHandler, final @NotNull ExceptionReporter exceptionReporter, final @NotNull List ignoredErrorTypes) { - this.beforeSpan = beforeSpan; - this.subscriptionHandler = subscriptionHandler; - this.exceptionReporter = exceptionReporter; - this.ignoredErrorTypes = ignoredErrorTypes; + this.instrumentation = + new SentryGraphqlInstrumentation( + beforeSpan, subscriptionHandler, exceptionReporter, ignoredErrorTypes, TRACE_ORIGIN); SentryIntegrationPackageStorage.getInstance().addIntegration("GraphQL"); SentryIntegrationPackageStorage.getInstance() .addPackage("maven:io.sentry:sentry-graphql", BuildConfig.VERSION_NAME); @@ -172,264 +90,43 @@ public SentryInstrumentation( } @Override - @SuppressWarnings("deprecation") public @NotNull InstrumentationState createState() { - return new TracingState(); + return instrumentation.createState(); } @Override - @SuppressWarnings("deprecation") public @NotNull InstrumentationContext beginExecution( final @NotNull InstrumentationExecutionParameters parameters) { - final TracingState tracingState = parameters.getInstrumentationState(); - final @NotNull IScopes currentScopes = Sentry.getCurrentScopes(); - tracingState.setTransaction(currentScopes.getSpan()); - parameters.getGraphQLContext().put(SENTRY_SCOPES_CONTEXT_KEY, currentScopes); + final SentryGraphqlInstrumentation.TracingState tracingState = + parameters.getInstrumentationState(); + instrumentation.beginExecution(parameters, tracingState); return super.beginExecution(parameters); } @Override - @SuppressWarnings("deprecation") public CompletableFuture instrumentExecutionResult( ExecutionResult executionResult, InstrumentationExecutionParameters parameters) { return super.instrumentExecutionResult(executionResult, parameters) .whenComplete( (result, exception) -> { - if (result != null) { - final @Nullable GraphQLContext graphQLContext = parameters.getGraphQLContext(); - if (graphQLContext != null) { - final @NotNull List exceptions = - graphQLContext.getOrDefault( - SENTRY_EXCEPTIONS_CONTEXT_KEY, new CopyOnWriteArrayList()); - for (Throwable throwable : exceptions) { - exceptionReporter.captureThrowable( - throwable, - new ExceptionReporter.ExceptionDetails( - scopesFromContext(graphQLContext), parameters, false), - result); - } - } - final @NotNull List errors = result.getErrors(); - if (errors != null) { - for (GraphQLError error : errors) { - String errorType = getErrorType(error); - if (!isIgnored(errorType)) { - exceptionReporter.captureThrowable( - new RuntimeException(error.getMessage()), - new ExceptionReporter.ExceptionDetails( - scopesFromContext(graphQLContext), parameters, false), - result); - } - } - } - } - if (exception != null) { - exceptionReporter.captureThrowable( - exception, - new ExceptionReporter.ExceptionDetails( - scopesFromContext(parameters.getGraphQLContext()), parameters, false), - null); - } + instrumentation.instrumentExecutionResultComplete(parameters, result, exception); }); } - 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; - } - final @Nullable ErrorClassification errorType = error.getErrorType(); - if (errorType != null) { - return errorType.toString(); - } - final @Nullable Map extensions = error.getExtensions(); - if (extensions != null) { - return StringUtils.toString(extensions.get("errorType")); - } - return null; - } - @Override - @SuppressWarnings("deprecation") public @NotNull InstrumentationContext beginExecuteOperation( final @NotNull InstrumentationExecuteOperationParameters parameters) { - final @Nullable ExecutionContext executionContext = parameters.getExecutionContext(); - if (executionContext != null) { - final @Nullable OperationDefinition operationDefinition = - executionContext.getOperationDefinition(); - if (operationDefinition != null) { - final @Nullable OperationDefinition.Operation operation = - operationDefinition.getOperation(); - final @Nullable String operationType = - operation == null ? null : operation.name().toLowerCase(Locale.ROOT); - scopesFromContext(parameters.getExecutionContext().getGraphQLContext()) - .addBreadcrumb( - Breadcrumb.graphqlOperation( - operationDefinition.getName(), - operationType, - StringUtils.toString(executionContext.getExecutionId()))); - } - } + instrumentation.beginExecuteOperation(parameters); return super.beginExecuteOperation(parameters); } - private @NotNull IScopes scopesFromContext(final @Nullable GraphQLContext context) { - if (context == null) { - return NoOpScopes.getInstance(); - } - return context.getOrDefault(SENTRY_SCOPES_CONTEXT_KEY, NoOpScopes.getInstance()); - } - @Override @SuppressWarnings({"FutureReturnValueIgnored", "deprecation"}) public @NotNull DataFetcher instrumentDataFetcher( final @NotNull DataFetcher dataFetcher, final @NotNull InstrumentationFieldFetchParameters parameters) { - // We only care about user code - if (parameters.isTrivialDataFetcher()) { - return dataFetcher; - } - - return environment -> { - final @Nullable ExecutionStepInfo executionStepInfo = environment.getExecutionStepInfo(); - if (executionStepInfo != null) { - Hint hint = new Hint(); - hint.set(TypeCheckHint.GRAPHQL_DATA_FETCHING_ENVIRONMENT, environment); - scopesFromContext(parameters.getExecutionContext().getGraphQLContext()) - .addBreadcrumb( - Breadcrumb.graphqlDataFetcher( - StringUtils.toString(executionStepInfo.getPath()), - GraphqlStringUtils.fieldToString(executionStepInfo.getField()), - GraphqlStringUtils.typeToString(executionStepInfo.getType()), - GraphqlStringUtils.objectTypeToString(executionStepInfo.getObjectType())), - hint); - } - final TracingState tracingState = parameters.getInstrumentationState(); - final ISpan transaction = tracingState.getTransaction(); - if (transaction != null) { - final ISpan span = createSpan(transaction, parameters); - try { - final @Nullable Object tmpResult = dataFetcher.get(environment); - final @Nullable Object result = - maybeCallSubscriptionHandler(parameters, environment, tmpResult); - if (result instanceof CompletableFuture) { - ((CompletableFuture) result) - .whenComplete( - (r, ex) -> { - if (ex != null) { - span.setThrowable(ex); - span.setStatus(SpanStatus.INTERNAL_ERROR); - } else { - span.setStatus(SpanStatus.OK); - } - finish(span, environment, r); - }); - } else { - span.setStatus(SpanStatus.OK); - finish(span, environment, result); - } - return result; - } catch (Throwable e) { - span.setThrowable(e); - span.setStatus(SpanStatus.INTERNAL_ERROR); - finish(span, environment); - throw e; - } - } else { - final Object result = dataFetcher.get(environment); - return maybeCallSubscriptionHandler(parameters, environment, result); - } - }; - } - - private @Nullable Object maybeCallSubscriptionHandler( - final @NotNull InstrumentationFieldFetchParameters parameters, - final @NotNull DataFetchingEnvironment environment, - final @Nullable Object tmpResult) { - if (tmpResult == null) { - return null; - } - - if (OperationDefinition.Operation.SUBSCRIPTION.equals( - environment.getOperationDefinition().getOperation())) { - return subscriptionHandler.onSubscriptionResult( - tmpResult, - scopesFromContext(environment.getGraphQlContext()), - exceptionReporter, - parameters); - } - - return tmpResult; - } - - private void finish( - final @NotNull ISpan span, - final @NotNull DataFetchingEnvironment environment, - final @Nullable Object result) { - if (beforeSpan != null) { - final ISpan newSpan = beforeSpan.execute(span, environment, result); - if (newSpan == null) { - // span is dropped - span.getSpanContext().setSampled(false); - } else { - newSpan.finish(); - } - } else { - span.finish(); - } - } - - private void finish( - final @NotNull ISpan span, final @NotNull DataFetchingEnvironment environment) { - finish(span, environment, null); - } - - private @NotNull ISpan createSpan( - @NotNull ISpan transaction, @NotNull InstrumentationFieldFetchParameters parameters) { - final GraphQLOutputType type = parameters.getExecutionStepInfo().getParent().getType(); - GraphQLObjectType parent; - if (type instanceof GraphQLNonNull) { - parent = (GraphQLObjectType) ((GraphQLNonNull) type).getWrappedType(); - } else { - parent = (GraphQLObjectType) type; - } - final @NotNull SpanOptions spanOptions = new SpanOptions(); - spanOptions.setOrigin(TRACE_ORIGIN); - final @NotNull ISpan span = - transaction.startChild( - "graphql", - parent.getName() + "." + parameters.getExecutionStepInfo().getPath().getSegmentName(), - spanOptions); - - return span; - } - - static final class TracingState implements InstrumentationState { - private @Nullable ISpan transaction; - - public @Nullable ISpan getTransaction() { - return transaction; - } - - public void setTransaction(final @Nullable ISpan transaction) { - this.transaction = transaction; - } - } - - @FunctionalInterface - public interface BeforeSpanCallback { - @Nullable - ISpan execute( - @NotNull ISpan span, @NotNull DataFetchingEnvironment environment, @Nullable Object result); + final SentryGraphqlInstrumentation.TracingState tracingState = + parameters.getInstrumentationState(); + return instrumentation.instrumentDataFetcher(dataFetcher, parameters, tracingState); } } 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 7b673a66a8..7324c59a79 100644 --- a/sentry-graphql/src/test/kotlin/io/sentry/graphql/SentryInstrumentationAnotherTest.kt +++ b/sentry-graphql/src/test/kotlin/io/sentry/graphql/SentryInstrumentationAnotherTest.kt @@ -36,8 +36,6 @@ import io.sentry.SentryTracer import io.sentry.TransactionContext import io.sentry.TypeCheckHint import io.sentry.graphql.ExceptionReporter.ExceptionDetails -import io.sentry.graphql.SentryInstrumentation.SENTRY_EXCEPTIONS_CONTEXT_KEY -import io.sentry.graphql.SentryInstrumentation.TracingState import org.mockito.Mockito import org.mockito.kotlin.any import org.mockito.kotlin.eq @@ -66,7 +64,7 @@ class SentryInstrumentationAnotherTest { lateinit var graphQLContext: GraphQLContext lateinit var subscriptionHandler: SentrySubscriptionHandler lateinit var exceptionReporter: ExceptionReporter - internal lateinit var instrumentationState: TracingState + internal lateinit var instrumentationState: SentryGraphqlInstrumentation.TracingState lateinit var instrumentationExecuteOperationParameters: InstrumentationExecuteOperationParameters val query = """query greeting(name: "somename")""" val variables = mapOf("variableA" to "value a") @@ -82,7 +80,7 @@ class SentryInstrumentationAnotherTest { } val defaultGraphQLContext = mapOf( - SentryInstrumentation.SENTRY_SCOPES_CONTEXT_KEY to scopes + SentryGraphqlInstrumentation.SENTRY_SCOPES_CONTEXT_KEY to scopes ) val mergedField = MergedField.newMergedField().addField(Field.newField("myFieldName").build()).build() @@ -128,7 +126,7 @@ class SentryInstrumentationAnotherTest { .fields(MergedSelectionSet.newMergedSelectionSet().build()) .field(mergedField) .build() - instrumentationState = SentryInstrumentation.TracingState().also { + instrumentationState = SentryGraphqlInstrumentation.TracingState().also { if (isTransactionActive && addTransactionToTracingState) { it.transaction = activeSpan } @@ -260,7 +258,7 @@ class SentryInstrumentationAnotherTest { val instrumentation = fixture.getSut(isTransactionActive = true, operation = OperationDefinition.Operation.MUTATION, graphQLContextParam = emptyMap(), addTransactionToTracingState = false) withMockScopes { instrumentation.beginExecution(fixture.instrumentationExecutionParameters) - assertSame(fixture.scopes, fixture.instrumentationExecutionParameters.graphQLContext.get(SentryInstrumentation.SENTRY_SCOPES_CONTEXT_KEY)) + assertSame(fixture.scopes, fixture.instrumentationExecutionParameters.graphQLContext.get(SentryGraphqlInstrumentation.SENTRY_SCOPES_CONTEXT_KEY)) assertNotNull(fixture.instrumentationState.transaction) } } @@ -298,8 +296,8 @@ class SentryInstrumentationAnotherTest { val exception = IllegalStateException("some exception") val instrumentation = fixture.getSut( graphQLContextParam = mapOf( - SENTRY_EXCEPTIONS_CONTEXT_KEY to listOf(exception), - SentryInstrumentation.SENTRY_SCOPES_CONTEXT_KEY to fixture.scopes + SentryGraphqlInstrumentation.SENTRY_EXCEPTIONS_CONTEXT_KEY to listOf(exception), + SentryGraphqlInstrumentation.SENTRY_SCOPES_CONTEXT_KEY to fixture.scopes ) ) val executionResult = ExecutionResultImpl.newExecutionResult() 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 7128b839e3..c8d63a1e98 100644 --- a/sentry-graphql/src/test/kotlin/io/sentry/graphql/SentryInstrumentationTest.kt +++ b/sentry-graphql/src/test/kotlin/io/sentry/graphql/SentryInstrumentationTest.kt @@ -42,7 +42,7 @@ class SentryInstrumentationTest { val scopes = mock() lateinit var activeSpan: SentryTracer - fun getSut(isTransactionActive: Boolean = true, dataFetcherThrows: Boolean = false, beforeSpan: SentryInstrumentation.BeforeSpanCallback? = null): GraphQL { + fun getSut(isTransactionActive: Boolean = true, dataFetcherThrows: Boolean = false, beforeSpan: SentryGraphqlInstrumentation.BeforeSpanCallback? = null): GraphQL { whenever(scopes.options).thenReturn(SentryOptions()) activeSpan = SentryTracer(TransactionContext("name", "op"), scopes) val schema = """ @@ -132,7 +132,7 @@ class SentryInstrumentationTest { @Test fun `beforeSpan can drop spans`() { - val sut = fixture.getSut(beforeSpan = SentryInstrumentation.BeforeSpanCallback { _, _, _ -> null }) + val sut = fixture.getSut(beforeSpan = SentryGraphqlInstrumentation.BeforeSpanCallback { _, _, _ -> null }) withMockScopes { val result = sut.execute("{ shows { id } }") @@ -150,7 +150,7 @@ class SentryInstrumentationTest { @Test fun `beforeSpan can modify spans`() { - val sut = fixture.getSut(beforeSpan = SentryInstrumentation.BeforeSpanCallback { span, _, _ -> span.apply { description = "changed" } }) + val sut = fixture.getSut(beforeSpan = SentryGraphqlInstrumentation.BeforeSpanCallback { span, _, _ -> span.apply { description = "changed" } }) withMockScopes { val result = sut.execute("{ shows { id } }") @@ -198,7 +198,7 @@ class SentryInstrumentationTest { environment, executionStrategyParameters, false - ).withNewState(SentryInstrumentation.TracingState()) + ).withNewState(SentryGraphqlInstrumentation.TracingState()) val instrumentedDataFetcher = instrumentation.instrumentDataFetcher(dataFetcher, parameters) val result = instrumentedDataFetcher.get(environment) diff --git a/sentry-samples/sentry-samples-spring-boot-jakarta/build.gradle.kts b/sentry-samples/sentry-samples-spring-boot-jakarta/build.gradle.kts index c5f16a2cd0..34a26d1f04 100644 --- a/sentry-samples/sentry-samples-spring-boot-jakarta/build.gradle.kts +++ b/sentry-samples/sentry-samples-spring-boot-jakarta/build.gradle.kts @@ -50,7 +50,7 @@ dependencies { implementation(kotlin(Config.kotlinStdLib, KotlinCompilerVersion.VERSION)) implementation(projects.sentrySpringBootStarterJakarta) implementation(projects.sentryLogback) - implementation(projects.sentryGraphql) + implementation(projects.sentryGraphql22) implementation(projects.sentryQuartz) // database query tracing diff --git a/sentry-spring-jakarta/build.gradle.kts b/sentry-spring-jakarta/build.gradle.kts index be3c00583e..ce14faef92 100644 --- a/sentry-spring-jakarta/build.gradle.kts +++ b/sentry-spring-jakarta/build.gradle.kts @@ -61,7 +61,7 @@ dependencies { testImplementation(Config.Libs.springBoot3StarterGraphql) testImplementation(Config.Libs.contextPropagation) testImplementation(Config.TestLibs.awaitility) - testImplementation(Config.Libs.graphQlJava) + testImplementation(Config.Libs.graphQlJava22) } tasks.withType { diff --git a/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/graphql/SentryBatchLoaderRegistry.java b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/graphql/SentryBatchLoaderRegistry.java index 3e2223b694..e31c6a725d 100644 --- a/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/graphql/SentryBatchLoaderRegistry.java +++ b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/graphql/SentryBatchLoaderRegistry.java @@ -1,6 +1,6 @@ package io.sentry.spring.jakarta.graphql; -import static io.sentry.graphql.SentryInstrumentation.SENTRY_SCOPES_CONTEXT_KEY; +import static io.sentry.graphql.SentryGraphqlInstrumentation.SENTRY_SCOPES_CONTEXT_KEY; import graphql.GraphQLContext; import io.sentry.Breadcrumb; diff --git a/sentry-spring/src/main/java/io/sentry/spring/graphql/SentryBatchLoaderRegistry.java b/sentry-spring/src/main/java/io/sentry/spring/graphql/SentryBatchLoaderRegistry.java index f1d8717598..5fe8eb265a 100644 --- a/sentry-spring/src/main/java/io/sentry/spring/graphql/SentryBatchLoaderRegistry.java +++ b/sentry-spring/src/main/java/io/sentry/spring/graphql/SentryBatchLoaderRegistry.java @@ -1,6 +1,6 @@ package io.sentry.spring.graphql; -import static io.sentry.graphql.SentryInstrumentation.SENTRY_SCOPES_CONTEXT_KEY; +import static io.sentry.graphql.SentryGraphqlInstrumentation.SENTRY_SCOPES_CONTEXT_KEY; import graphql.GraphQLContext; import io.sentry.Breadcrumb; diff --git a/settings.gradle.kts b/settings.gradle.kts index faa615d050..54f5b2f3ec 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -41,6 +41,8 @@ include( "sentry-bom", "sentry-openfeign", "sentry-graphql", + "sentry-graphql-22", + "sentry-graphql-core", "sentry-jdbc", "sentry-opentelemetry:sentry-opentelemetry-bootstrap", "sentry-opentelemetry:sentry-opentelemetry-core", From 3cc76b406dd99e26e026ed6e5f3776bc49ec2a65 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Wed, 2 Oct 2024 10:05:47 +0200 Subject: [PATCH 2/2] Add back callback interface and constants as deprecated --- sentry-graphql-22/api/sentry-graphql-22.api | 5 +++++ .../graphql22/SentryInstrumentation.java | 21 +++++++++++++++++++ sentry-graphql/api/sentry-graphql.api | 5 +++++ .../sentry/graphql/SentryInstrumentation.java | 21 +++++++++++++++++++ 4 files changed, 52 insertions(+) diff --git a/sentry-graphql-22/api/sentry-graphql-22.api b/sentry-graphql-22/api/sentry-graphql-22.api index 88d55bdd7c..8731cd1e07 100644 --- a/sentry-graphql-22/api/sentry-graphql-22.api +++ b/sentry-graphql-22/api/sentry-graphql-22.api @@ -4,6 +4,8 @@ public final class io/sentry/graphql22/BuildConfig { } public final class io/sentry/graphql22/SentryInstrumentation : graphql/execution/instrumentation/SimpleInstrumentation { + 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;Z)V public fun (Lio/sentry/graphql/SentryGraphqlInstrumentation$BeforeSpanCallback;Lio/sentry/graphql/SentrySubscriptionHandler;ZLjava/util/List;)V @@ -15,3 +17,6 @@ public final class io/sentry/graphql22/SentryInstrumentation : graphql/execution public fun instrumentExecutionResult (Lgraphql/ExecutionResult;Lgraphql/execution/instrumentation/parameters/InstrumentationExecutionParameters;Lgraphql/execution/instrumentation/InstrumentationState;)Ljava/util/concurrent/CompletableFuture; } +public abstract interface class io/sentry/graphql22/SentryInstrumentation$BeforeSpanCallback : io/sentry/graphql/SentryGraphqlInstrumentation$BeforeSpanCallback { +} + 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 066b8d60b8..1409113b72 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 @@ -23,6 +23,20 @@ public final class SentryInstrumentation extends graphql.execution.instrumentation.SimpleInstrumentation { + /** + * @deprecated please use {@link SentryGraphqlInstrumentation#SENTRY_SCOPES_CONTEXT_KEY} + */ + @Deprecated + public static final @NotNull String SENTRY_SCOPES_CONTEXT_KEY = + SentryGraphqlInstrumentation.SENTRY_SCOPES_CONTEXT_KEY; + + /** + * @deprecated please use {@link SentryGraphqlInstrumentation#SENTRY_EXCEPTIONS_CONTEXT_KEY} + */ + @Deprecated + public static final @NotNull String SENTRY_EXCEPTIONS_CONTEXT_KEY = + SentryGraphqlInstrumentation.SENTRY_EXCEPTIONS_CONTEXT_KEY; + private static final String TRACE_ORIGIN = "auto.graphql.graphql22"; private final @NotNull SentryGraphqlInstrumentation instrumentation; @@ -139,4 +153,11 @@ public CompletableFuture instrumentExecutionResult( InstrumentationState.ofState(state); return instrumentation.instrumentDataFetcher(dataFetcher, parameters, tracingState); } + + /** + * @deprecated please use {@link SentryGraphqlInstrumentation.BeforeSpanCallback} + */ + @Deprecated + @FunctionalInterface + public interface BeforeSpanCallback extends SentryGraphqlInstrumentation.BeforeSpanCallback {} } diff --git a/sentry-graphql/api/sentry-graphql.api b/sentry-graphql/api/sentry-graphql.api index 7c0b278254..2e5f81ae76 100644 --- a/sentry-graphql/api/sentry-graphql.api +++ b/sentry-graphql/api/sentry-graphql.api @@ -4,6 +4,8 @@ public final class io/sentry/graphql/BuildConfig { } public final class io/sentry/graphql/SentryInstrumentation : graphql/execution/instrumentation/SimpleInstrumentation { + 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;Z)V public fun (Lio/sentry/graphql/SentryGraphqlInstrumentation$BeforeSpanCallback;Lio/sentry/graphql/SentrySubscriptionHandler;ZLjava/util/List;)V @@ -15,3 +17,6 @@ public final class io/sentry/graphql/SentryInstrumentation : graphql/execution/i public fun instrumentExecutionResult (Lgraphql/ExecutionResult;Lgraphql/execution/instrumentation/parameters/InstrumentationExecutionParameters;)Ljava/util/concurrent/CompletableFuture; } +public abstract interface class io/sentry/graphql/SentryInstrumentation$BeforeSpanCallback : io/sentry/graphql/SentryGraphqlInstrumentation$BeforeSpanCallback { +} + 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 c4b5ef0f0e..db2982e080 100644 --- a/sentry-graphql/src/main/java/io/sentry/graphql/SentryInstrumentation.java +++ b/sentry-graphql/src/main/java/io/sentry/graphql/SentryInstrumentation.java @@ -19,6 +19,20 @@ public final class SentryInstrumentation extends graphql.execution.instrumentation.SimpleInstrumentation { + /** + * @deprecated please use {@link SentryGraphqlInstrumentation#SENTRY_SCOPES_CONTEXT_KEY} + */ + @Deprecated + public static final @NotNull String SENTRY_SCOPES_CONTEXT_KEY = + SentryGraphqlInstrumentation.SENTRY_SCOPES_CONTEXT_KEY; + + /** + * @deprecated please use {@link SentryGraphqlInstrumentation#SENTRY_EXCEPTIONS_CONTEXT_KEY} + */ + @Deprecated + public static final @NotNull String SENTRY_EXCEPTIONS_CONTEXT_KEY = + SentryGraphqlInstrumentation.SENTRY_EXCEPTIONS_CONTEXT_KEY; + private static final String TRACE_ORIGIN = "auto.graphql.graphql"; private final @NotNull SentryGraphqlInstrumentation instrumentation; @@ -129,4 +143,11 @@ public CompletableFuture instrumentExecutionResult( parameters.getInstrumentationState(); return instrumentation.instrumentDataFetcher(dataFetcher, parameters, tracingState); } + + /** + * @deprecated please use {@link SentryGraphqlInstrumentation.BeforeSpanCallback} + */ + @Deprecated + @FunctionalInterface + public interface BeforeSpanCallback extends SentryGraphqlInstrumentation.BeforeSpanCallback {} }