From 4fac45da4ca55a72121135b0b641c209da0a4661 Mon Sep 17 00:00:00 2001 From: Lukas Kusik Date: Wed, 12 Feb 2025 13:14:33 +0100 Subject: [PATCH 1/8] Apollo 4 initial setup --- buildSrc/src/main/java/Config.kt | 2 + sentry-apollo-4/api/sentry-apollo-4.api | 51 ++ sentry-apollo-4/build.gradle.kts | 87 ++++ .../apollo4/SentryApollo4ClientException.kt | 11 + .../apollo4/SentryApollo4HttpInterceptor.kt | 458 ++++++++++++++++++ .../apollo4/SentryApollo4Interceptor.kt | 44 ++ .../apollo4/SentryApolloBuilderExtensions.kt | 39 ++ .../io/sentry/apollo4/LaunchDetailsQuery.kt | 89 ++++ .../SentryApollo4InterceptorClientErrors.kt | 390 +++++++++++++++ .../apollo4/SentryApollo4InterceptorTest.kt | 397 +++++++++++++++ ...ntryApollo4InterceptorWithVariablesTest.kt | 193 ++++++++ .../LaunchDetailsQuery_ResponseAdapter.kt | 166 +++++++ .../LaunchDetailsQuery_VariablesAdapter.kt | 27 ++ .../LaunchDetailsQuerySelections.kt | 78 +++ .../io/sentry/apollo4/type/GraphQLBoolean.kt | 17 + .../java/io/sentry/apollo4/type/GraphQLID.kt | 20 + .../io/sentry/apollo4/type/GraphQLString.kt | 18 + .../java/io/sentry/apollo4/type/Launch.kt | 14 + .../java/io/sentry/apollo4/type/Mission.kt | 14 + .../test/java/io/sentry/apollo4/type/Query.kt | 14 + .../java/io/sentry/apollo4/type/Rocket.kt | 14 + .../util/Apollo4PlatformTestManipulator.kt | 8 + .../org.mockito.plugins.MockMaker | 1 + sentry-compose/build.gradle.kts | 2 +- settings.gradle.kts | 1 + 25 files changed, 2154 insertions(+), 1 deletion(-) create mode 100644 sentry-apollo-4/api/sentry-apollo-4.api create mode 100644 sentry-apollo-4/build.gradle.kts create mode 100644 sentry-apollo-4/src/main/java/io/sentry/apollo4/SentryApollo4ClientException.kt create mode 100644 sentry-apollo-4/src/main/java/io/sentry/apollo4/SentryApollo4HttpInterceptor.kt create mode 100644 sentry-apollo-4/src/main/java/io/sentry/apollo4/SentryApollo4Interceptor.kt create mode 100644 sentry-apollo-4/src/main/java/io/sentry/apollo4/SentryApolloBuilderExtensions.kt create mode 100644 sentry-apollo-4/src/test/java/io/sentry/apollo4/LaunchDetailsQuery.kt create mode 100644 sentry-apollo-4/src/test/java/io/sentry/apollo4/SentryApollo4InterceptorClientErrors.kt create mode 100644 sentry-apollo-4/src/test/java/io/sentry/apollo4/SentryApollo4InterceptorTest.kt create mode 100644 sentry-apollo-4/src/test/java/io/sentry/apollo4/SentryApollo4InterceptorWithVariablesTest.kt create mode 100644 sentry-apollo-4/src/test/java/io/sentry/apollo4/adapter/LaunchDetailsQuery_ResponseAdapter.kt create mode 100644 sentry-apollo-4/src/test/java/io/sentry/apollo4/adapter/LaunchDetailsQuery_VariablesAdapter.kt create mode 100644 sentry-apollo-4/src/test/java/io/sentry/apollo4/selections/LaunchDetailsQuerySelections.kt create mode 100644 sentry-apollo-4/src/test/java/io/sentry/apollo4/type/GraphQLBoolean.kt create mode 100644 sentry-apollo-4/src/test/java/io/sentry/apollo4/type/GraphQLID.kt create mode 100644 sentry-apollo-4/src/test/java/io/sentry/apollo4/type/GraphQLString.kt create mode 100644 sentry-apollo-4/src/test/java/io/sentry/apollo4/type/Launch.kt create mode 100644 sentry-apollo-4/src/test/java/io/sentry/apollo4/type/Mission.kt create mode 100644 sentry-apollo-4/src/test/java/io/sentry/apollo4/type/Query.kt create mode 100644 sentry-apollo-4/src/test/java/io/sentry/apollo4/type/Rocket.kt create mode 100644 sentry-apollo-4/src/test/java/io/sentry/util/Apollo4PlatformTestManipulator.kt create mode 100644 sentry-apollo-4/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker diff --git a/buildSrc/src/main/java/Config.kt b/buildSrc/src/main/java/Config.kt index 0db77e349e..b065914d21 100644 --- a/buildSrc/src/main/java/Config.kt +++ b/buildSrc/src/main/java/Config.kt @@ -157,6 +157,7 @@ object Config { val composeCoil = "io.coil-kt:coil-compose:2.6.0" val apolloKotlin = "com.apollographql.apollo3:apollo-runtime:3.8.2" + val apolloKotlin4 = "com.apollographql.apollo:apollo-runtime:4.1.1" val sentryNativeNdk = "io.sentry:sentry-native-ndk:0.7.20" @@ -250,6 +251,7 @@ object Config { val SENTRY_SPRING_BOOT_JAKARTA_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.spring-boot.jakarta" val SENTRY_OPENTELEMETRY_AGENT_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.opentelemetry.agent" val SENTRY_APOLLO3_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.apollo3" + val SENTRY_APOLLO4_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.apollo4" val SENTRY_APOLLO_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.apollo" val SENTRY_GRAPHQL_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.graphql" val SENTRY_GRAPHQL22_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.graphql22" diff --git a/sentry-apollo-4/api/sentry-apollo-4.api b/sentry-apollo-4/api/sentry-apollo-4.api new file mode 100644 index 0000000000..0081d49c65 --- /dev/null +++ b/sentry-apollo-4/api/sentry-apollo-4.api @@ -0,0 +1,51 @@ +public final class io/sentry/apollo4/BuildConfig { + public static final field SENTRY_APOLLO4_SDK_NAME Ljava/lang/String; + public static final field VERSION_NAME Ljava/lang/String; +} + +public final class io/sentry/apollo4/SentryApollo4ClientException : java/lang/Exception { + public static final field Companion Lio/sentry/apollo4/SentryApollo4ClientException$Companion; + public fun (Ljava/lang/String;)V +} + +public final class io/sentry/apollo4/SentryApollo4ClientException$Companion { +} + +public final class io/sentry/apollo4/SentryApollo4HttpInterceptor : com/apollographql/apollo4/network/http/HttpInterceptor { + public static final field Companion Lio/sentry/apollo4/SentryApollo4HttpInterceptor$Companion; + public static final field DEFAULT_CAPTURE_FAILED_REQUESTS Z + public static final field SENTRY_APOLLO_3_OPERATION_TYPE Ljava/lang/String; + public static final field SENTRY_APOLLO_3_VARIABLES Ljava/lang/String; + public fun ()V + public fun (Lio/sentry/IScopes;)V + public fun (Lio/sentry/IScopes;Lio/sentry/apollo4/SentryApollo4HttpInterceptor$BeforeSpanCallback;)V + public fun (Lio/sentry/IScopes;Lio/sentry/apollo4/SentryApollo4HttpInterceptor$BeforeSpanCallback;Z)V + public fun (Lio/sentry/IScopes;Lio/sentry/apollo4/SentryApollo4HttpInterceptor$BeforeSpanCallback;ZLjava/util/List;)V + public synthetic fun (Lio/sentry/IScopes;Lio/sentry/apollo4/SentryApollo4HttpInterceptor$BeforeSpanCallback;ZLjava/util/List;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun dispose ()V + public fun intercept (Lcom/apollographql/apollo4/api/http/HttpRequest;Lcom/apollographql/apollo4/network/http/HttpInterceptorChain;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + +public abstract interface class io/sentry/apollo4/SentryApollo4HttpInterceptor$BeforeSpanCallback { + public abstract fun execute (Lio/sentry/ISpan;Lcom/apollographql/apollo4/api/http/HttpRequest;Lcom/apollographql/apollo4/api/http/HttpResponse;)Lio/sentry/ISpan; +} + +public final class io/sentry/apollo4/SentryApollo4HttpInterceptor$Companion { +} + +public final class io/sentry/apollo4/SentryApollo4Interceptor : com/apollographql/apollo4/interceptor/ApolloInterceptor { + public fun ()V + public fun intercept (Lcom/apollographql/apollo4/api/ApolloRequest;Lcom/apollographql/apollo4/interceptor/ApolloInterceptorChain;)Lkotlinx/coroutines/flow/Flow; +} + +public final class io/sentry/apollo4/SentryApolloBuilderExtensionsKt { + public static final fun sentryTracing (Lcom/apollographql/apollo4/ApolloClient$Builder;)Lcom/apollographql/apollo4/ApolloClient$Builder; + public static final fun sentryTracing (Lcom/apollographql/apollo4/ApolloClient$Builder;Lio/sentry/IScopes;)Lcom/apollographql/apollo4/ApolloClient$Builder; + public static final fun sentryTracing (Lcom/apollographql/apollo4/ApolloClient$Builder;Lio/sentry/IScopes;Z)Lcom/apollographql/apollo4/ApolloClient$Builder; + public static final fun sentryTracing (Lcom/apollographql/apollo4/ApolloClient$Builder;Lio/sentry/IScopes;ZLjava/util/List;)Lcom/apollographql/apollo4/ApolloClient$Builder; + public static final fun sentryTracing (Lcom/apollographql/apollo4/ApolloClient$Builder;Lio/sentry/IScopes;ZLjava/util/List;Lio/sentry/apollo4/SentryApollo4HttpInterceptor$BeforeSpanCallback;)Lcom/apollographql/apollo4/ApolloClient$Builder; + public static final fun sentryTracing (Lcom/apollographql/apollo4/ApolloClient$Builder;ZLjava/util/List;Lio/sentry/apollo4/SentryApollo4HttpInterceptor$BeforeSpanCallback;)Lcom/apollographql/apollo4/ApolloClient$Builder; + public static synthetic fun sentryTracing$default (Lcom/apollographql/apollo4/ApolloClient$Builder;Lio/sentry/IScopes;ZLjava/util/List;Lio/sentry/apollo4/SentryApollo4HttpInterceptor$BeforeSpanCallback;ILjava/lang/Object;)Lcom/apollographql/apollo4/ApolloClient$Builder; + public static synthetic fun sentryTracing$default (Lcom/apollographql/apollo4/ApolloClient$Builder;ZLjava/util/List;Lio/sentry/apollo4/SentryApollo4HttpInterceptor$BeforeSpanCallback;ILjava/lang/Object;)Lcom/apollographql/apollo4/ApolloClient$Builder; +} + diff --git a/sentry-apollo-4/build.gradle.kts b/sentry-apollo-4/build.gradle.kts new file mode 100644 index 0000000000..95882248ac --- /dev/null +++ b/sentry-apollo-4/build.gradle.kts @@ -0,0 +1,87 @@ +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.sentryKotlinExtensions) + + compileOnly(Config.Libs.apolloKotlin4) + + compileOnly(Config.CompileOnly.nopen) + errorprone(Config.CompileOnly.nopenChecker) + errorprone(Config.CompileOnly.errorprone) + errorprone(Config.CompileOnly.errorProneNullAway) + compileOnly(Config.CompileOnly.jetbrainsAnnotations) + + // tests + testImplementation(projects.sentryTestSupport) + testImplementation(Config.Libs.coroutinesCore) + testImplementation(kotlin(Config.kotlinStdLib)) + testImplementation(Config.TestLibs.kotlinTestJunit) + testImplementation(Config.TestLibs.mockitoKotlin) + testImplementation(Config.TestLibs.mockitoInline) + testImplementation(Config.TestLibs.mockWebserver) + testImplementation(Config.Libs.apolloKotlin4) +} + +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.apollo4") + buildConfigField("String", "SENTRY_APOLLO4_SDK_NAME", "\"${Config.Sentry.SENTRY_APOLLO4_SDK_NAME}\"") + buildConfigField("String", "VERSION_NAME", "\"${project.version}\"") +} diff --git a/sentry-apollo-4/src/main/java/io/sentry/apollo4/SentryApollo4ClientException.kt b/sentry-apollo-4/src/main/java/io/sentry/apollo4/SentryApollo4ClientException.kt new file mode 100644 index 0000000000..f363c61763 --- /dev/null +++ b/sentry-apollo-4/src/main/java/io/sentry/apollo4/SentryApollo4ClientException.kt @@ -0,0 +1,11 @@ +package io.sentry.apollo4 + +/** + * Used for holding an Apollo4 client error, for example. An integration that does not throw when API + * returns 4xx, 5xx or the `errors` field. + */ +class SentryApollo4ClientException(message: String?) : Exception(message) { + companion object { + private const val serialVersionUID = 4312120066430858144L + } +} diff --git a/sentry-apollo-4/src/main/java/io/sentry/apollo4/SentryApollo4HttpInterceptor.kt b/sentry-apollo-4/src/main/java/io/sentry/apollo4/SentryApollo4HttpInterceptor.kt new file mode 100644 index 0000000000..4c8f35ae51 --- /dev/null +++ b/sentry-apollo-4/src/main/java/io/sentry/apollo4/SentryApollo4HttpInterceptor.kt @@ -0,0 +1,458 @@ +package io.sentry.apollo4 + +import com.apollographql.apollo.api.http.DefaultHttpRequestComposer.Companion.HEADER_APOLLO_OPERATION_ID +import com.apollographql.apollo.api.http.DefaultHttpRequestComposer.Companion.HEADER_APOLLO_OPERATION_NAME +import com.apollographql.apollo.api.http.HttpHeader +import com.apollographql.apollo.api.http.HttpRequest +import com.apollographql.apollo.api.http.HttpResponse +import com.apollographql.apollo.exception.ApolloHttpException +import com.apollographql.apollo.network.http.HttpInterceptor +import com.apollographql.apollo.network.http.HttpInterceptorChain +import io.sentry.BaggageHeader +import io.sentry.Breadcrumb +import io.sentry.Hint +import io.sentry.IScopes +import io.sentry.ISpan +import io.sentry.ScopesAdapter +import io.sentry.SentryEvent +import io.sentry.SentryIntegrationPackageStorage +import io.sentry.SentryLevel +import io.sentry.SentryOptions.DEFAULT_PROPAGATION_TARGETS +import io.sentry.SpanDataConvention +import io.sentry.SpanDataConvention.HTTP_METHOD_KEY +import io.sentry.SpanStatus +import io.sentry.TypeCheckHint.APOLLO_REQUEST +import io.sentry.TypeCheckHint.APOLLO_RESPONSE +import io.sentry.exception.ExceptionMechanismException +import io.sentry.protocol.Mechanism +import io.sentry.protocol.Request +import io.sentry.protocol.Response +import io.sentry.util.HttpUtils +import io.sentry.util.IntegrationUtils.addIntegrationToSdkVersion +import io.sentry.util.Platform +import io.sentry.util.PropagationTargetsUtils +import io.sentry.util.SpanUtils +import io.sentry.util.TracingUtils +import io.sentry.util.UrlUtils +import io.sentry.vendor.Base64 +import okio.Buffer +import org.jetbrains.annotations.ApiStatus +import java.util.Locale + +private const val TRACE_ORIGIN = "auto.graphql.apollo4" + +class SentryApollo4HttpInterceptor @JvmOverloads constructor( + @ApiStatus.Internal private val scopes: IScopes = ScopesAdapter.getInstance(), + private val beforeSpan: BeforeSpanCallback? = null, + private val captureFailedRequests: Boolean = DEFAULT_CAPTURE_FAILED_REQUESTS, + private val failedRequestTargets: List = listOf(DEFAULT_PROPAGATION_TARGETS) +) : HttpInterceptor { + + init { + addIntegrationToSdkVersion("Apollo4") + if (captureFailedRequests) { + SentryIntegrationPackageStorage.getInstance() + .addIntegration("Apollo4ClientError") + } + SentryIntegrationPackageStorage.getInstance() + .addPackage("maven:io.sentry:sentry-apollo-4", BuildConfig.VERSION_NAME) + } + + private val regex: Regex by lazy { + "(?i)\"errors\"\\s*:\\s*\\[".toRegex() + } + + override suspend fun intercept( + request: HttpRequest, + chain: HttpInterceptorChain + ): HttpResponse { + val activeSpan = if (Platform.isAndroid()) scopes.transaction else scopes.span + + val operationName = getHeader("X-APOLLO-OPERATION-NAME", request.headers) + val operationType = decodeHeaderValue(request, SENTRY_APOLLO_4_OPERATION_TYPE) + val operationId = getHeader("X-APOLLO-OPERATION-ID", request.headers) + + var span: ISpan? = null + + if (activeSpan != null) { + span = startChild(request, activeSpan, operationName, operationType, operationId) + } + + val modifiedRequest = maybeAddTracingHeaders(scopes, request, span) + var httpResponse: HttpResponse? = null + var statusCode: Int? = null + + try { + httpResponse = chain.proceed(modifiedRequest) + statusCode = httpResponse.statusCode + span?.setData(SpanDataConvention.HTTP_STATUS_CODE_KEY, statusCode) + span?.status = SpanStatus.fromHttpStatusCode(statusCode) + + captureEvent(modifiedRequest, httpResponse, operationName, operationType) + + return httpResponse + } catch (e: Throwable) { + // https://github.com/apollographql/apollo-kotlin/issues/4711 will change error handling in v4 + when (e) { + is ApolloHttpException -> { + statusCode = e.statusCode + span?.setData(SpanDataConvention.HTTP_STATUS_CODE_KEY, statusCode) + span?.status = + SpanStatus.fromHttpStatusCode(statusCode, SpanStatus.INTERNAL_ERROR) + } + + else -> span?.status = SpanStatus.INTERNAL_ERROR + } + span?.throwable = e + throw e + } finally { + finish( + span, + modifiedRequest, + httpResponse, + statusCode, + operationName, + operationType, + operationId + ) + } + } + + private fun maybeAddTracingHeaders(scopes: IScopes, request: HttpRequest, span: ISpan?): HttpRequest { + var cleanedHeaders = removeSentryInternalHeaders(request.headers).toMutableList() + + if (!isIgnored()) { + TracingUtils.traceIfAllowed(scopes, request.url, request.headers.filter { it.name == BaggageHeader.BAGGAGE_HEADER }.map { it.value }, span)?.let { + cleanedHeaders.add(HttpHeader(it.sentryTraceHeader.name, it.sentryTraceHeader.value)) + it.baggageHeader?.let { baggageHeader -> + cleanedHeaders = cleanedHeaders.filterNot { it.name == BaggageHeader.BAGGAGE_HEADER }.toMutableList().apply { + add(HttpHeader(baggageHeader.name, baggageHeader.value)) + } + } + } + } + + val requestBuilder = request.newBuilder().apply { + headers(cleanedHeaders) + } + + return requestBuilder.build() + } + + private fun isIgnored(): Boolean { + return SpanUtils.isIgnored(scopes.getOptions().getIgnoredSpanOrigins(), TRACE_ORIGIN) + } + + private fun removeSentryInternalHeaders(headers: List): List { + return headers.filterNot { + it.name.equals(SENTRY_APOLLO_4_VARIABLES, true) || + it.name.equals(SENTRY_APOLLO_4_OPERATION_TYPE, true) + } + } + + private fun startChild( + request: HttpRequest, + activeSpan: ISpan, + operationName: String?, + operationType: String?, + operationId: String? + ): ISpan { + val urlDetails = UrlUtils.parse(request.url) + val method = request.method.name + + val operation = if (operationType != null) "http.graphql.$operationType" else "http.graphql" + val variables = decodeHeaderValue(request, SENTRY_APOLLO_4_VARIABLES) + + val description = "${operationType ?: method} ${operationName ?: urlDetails.urlOrFallback}" + + return activeSpan.startChild(operation, description).apply { + urlDetails.applyToSpan(this) + + spanContext.origin = TRACE_ORIGIN + + operationId?.let { + setData("operationId", it) + } + + variables?.let { + setData("variables", it) + } + setData(HTTP_METHOD_KEY, method.toUpperCase(Locale.ROOT)) + } + } + + private fun decodeHeaderValue(request: HttpRequest, headerName: String): String? { + return getHeader(headerName, request.headers)?.let { + try { + String(Base64.decode(it, Base64.NO_WRAP)) + } catch (e: Throwable) { + scopes.options.logger.log( + SentryLevel.ERROR, + "Error decoding internal apolloHeader $headerName", + e + ) + return null + } + } + } + + private fun finish( + span: ISpan?, + request: HttpRequest, + response: HttpResponse?, + statusCode: Int?, + operationName: String?, + operationType: String?, + operationId: String? + ) { + var responseContentLength: Long? = null + response?.body?.buffer?.size?.ifHasValidLength { + responseContentLength = it + } + + if (span != null) { + statusCode?.let { + span.setData(SpanDataConvention.HTTP_STATUS_CODE_KEY, statusCode) + } + responseContentLength?.let { + span.setData(SpanDataConvention.HTTP_RESPONSE_CONTENT_LENGTH_KEY, it) + } + if (beforeSpan != null) { + try { + val result = beforeSpan.execute(span, request, response) + if (result == null) { + // Span is dropped + span.spanContext.sampled = false + } + } catch (e: Throwable) { + scopes.options.logger.log( + SentryLevel.ERROR, + "An error occurred while executing beforeSpan on ApolloInterceptor", + e + ) + } + } + span.finish() + } + + val breadcrumb = Breadcrumb.http(request.url, request.method.name, statusCode) + + request.body?.contentLength.ifHasValidLength { contentLength -> + breadcrumb.setData("request_body_size", contentLength) + } + + operationName?.let { + breadcrumb.setData("operation_name", it) + } + operationType?.let { + breadcrumb.setData("operation_type", it) + } + operationId?.let { + breadcrumb.setData("operation_id", it) + } + + val hint = Hint().also { + it.set(APOLLO_REQUEST, request) + } + + response?.let { httpResponse -> + responseContentLength?.let { + breadcrumb.setData("response_body_size", it) + } + + hint.set(APOLLO_RESPONSE, httpResponse) + } + + scopes.addBreadcrumb(breadcrumb, hint) + } + + // Extensions + + private fun Long?.ifHasValidLength(fn: (Long) -> Unit) { + if (this != null && this != -1L) { + fn.invoke(this) + } + } + + private fun getHeader(key: String, headers: List): String? { + return headers.firstOrNull { it.name.equals(key, true) }?.value + } + + private fun getHeaders(headers: List): MutableMap? { + // Headers are only sent if isSendDefaultPii is enabled due to PII + if (!scopes.options.isSendDefaultPii) { + return null + } + + val headersMap = mutableMapOf() + + for (item in headers) { + val name = item.name + + // header is only sent if isn't sensitive + if (HttpUtils.containsSensitiveHeader(name)) { + continue + } + + headersMap[name] = item.value + } + return headersMap.ifEmpty { null } + } + + private fun captureEvent( + request: HttpRequest, + response: HttpResponse, + operationName: String?, + operationType: String? + ) { + // return if the feature is disabled + if (!captureFailedRequests) { + return + } + + // wrap everything up in a try catch block so every exception is swallowed and degraded + // gracefully + try { + // we pay the price to read the response in the memory to check if there's any errors + // GraphQL does not throw status code 400+ for every type of error + val body = try { + response.body?.peek()?.readUtf8() ?: "" + } catch (e: Throwable) { + scopes.options.logger.log( + SentryLevel.ERROR, + "Error reading the response body.", + e + ) + // bail out because the response body has the most important information + return + } + + // if there response body does not have the errors field, do not raise an issue + if (body.isEmpty() || !regex.containsMatchIn(body)) { + return + } + + // not possible to get a parameterized url, but we remove at least the + // query string and the fragment. + // url example: https://api.github.com/users/getsentry/repos/#fragment?query=query + // url will be: https://api.github.com/users/getsentry/repos/ + // ideally we'd like a parameterized url: https://api.github.com/users/{user}/repos/ + // but that's not possible + val urlDetails = UrlUtils.parse(request.url) + + // return if its not a target match + if (!PropagationTargetsUtils.contain(failedRequestTargets, urlDetails.urlOrFallback)) { + return + } + + val mechanism = Mechanism().apply { + type = "SentryApollo4Interceptor" + } + + val fingerprints = mutableListOf() + + val builder = StringBuilder() + builder.append("GraphQL Request failed") + operationName?.let { + builder.append(", name: $it") + fingerprints.add(operationName) + } + operationType?.let { + builder.append(", type: $it") + fingerprints.add(operationType) + } + + val exception = SentryApollo4ClientException(builder.toString()) + val mechanismException = + ExceptionMechanismException(mechanism, exception, Thread.currentThread(), true) + val event = SentryEvent(mechanismException) + + val hint = Hint() + hint.set(APOLLO_REQUEST, request) + hint.set(APOLLO_RESPONSE, response) + + val sentryRequest = Request().apply { + urlDetails.applyToRequest(this) + // Cookie is only sent if isSendDefaultPii is enabled + cookies = + if (scopes.options.isSendDefaultPii) getHeader("Cookie", request.headers) else null + method = request.method.name + headers = getHeaders(request.headers) + apiTarget = "graphql" + + request.body?.let { + bodySize = it.contentLength + + val buffer = Buffer() + + try { + it.writeTo(buffer) + data = buffer.readUtf8() + } catch (e: Throwable) { + scopes.options.logger.log( + SentryLevel.ERROR, + "Error reading the request body.", + e + ) + // continue because the response body alone can already give some insights + } finally { + buffer.close() + } + } + } + + val sentryResponse = Response().apply { + // Set-Cookie is only sent if isSendDefaultPii is enabled due to PII + cookies = if (scopes.options.isSendDefaultPii) { + getHeader( + "Set-Cookie", + response.headers + ) + } else { + null + } + headers = getHeaders(response.headers) + statusCode = response.statusCode + + response.body?.buffer?.size?.ifHasValidLength { contentLength -> + bodySize = contentLength + } + data = body + } + + fingerprints.add(response.statusCode.toString()) + + event.request = sentryRequest + event.contexts.setResponse(sentryResponse) + event.fingerprints = fingerprints + + scopes.captureEvent(event, hint) + } catch (e: Throwable) { + scopes.options.logger.log( + SentryLevel.ERROR, + "Error capturing the GraphQL error.", + e + ) + } + } + + /** + * The BeforeSpan callback + */ + fun interface BeforeSpanCallback { + /** + * Mutates span before being added. + * + * @param span the span to mutate or drop + * @param request the Apollo request object + * @param response the Apollo response object + */ + fun execute(span: ISpan, request: HttpRequest, response: HttpResponse?): ISpan? + } + + companion object { + const val SENTRY_APOLLO_4_VARIABLES = "SENTRY-APOLLO-4-VARIABLES" + const val SENTRY_APOLLO_4_OPERATION_TYPE = "SENTRY-APOLLO-4-OPERATION-TYPE" + const val DEFAULT_CAPTURE_FAILED_REQUESTS = true + } +} diff --git a/sentry-apollo-4/src/main/java/io/sentry/apollo4/SentryApollo4Interceptor.kt b/sentry-apollo-4/src/main/java/io/sentry/apollo4/SentryApollo4Interceptor.kt new file mode 100644 index 0000000000..a8c5270fcd --- /dev/null +++ b/sentry-apollo-4/src/main/java/io/sentry/apollo4/SentryApollo4Interceptor.kt @@ -0,0 +1,44 @@ +package io.sentry.apollo4 + +import com.apollographql.apollo.api.ApolloRequest +import com.apollographql.apollo.api.ApolloResponse +import com.apollographql.apollo.api.CustomScalarAdapters +import com.apollographql.apollo.api.Mutation +import com.apollographql.apollo.api.Operation +import com.apollographql.apollo.api.Query +import com.apollographql.apollo.api.Subscription +import com.apollographql.apollo.api.variables +import com.apollographql.apollo.interceptor.ApolloInterceptor +import com.apollographql.apollo.interceptor.ApolloInterceptorChain +import io.sentry.apollo4.SentryApollo4HttpInterceptor.Companion.SENTRY_APOLLO_4_OPERATION_TYPE +import io.sentry.apollo4.SentryApollo4HttpInterceptor.Companion.SENTRY_APOLLO_4_VARIABLES +import io.sentry.vendor.Base64 +import kotlinx.coroutines.flow.Flow + +class SentryApollo4Interceptor : ApolloInterceptor { + + override fun intercept( + request: ApolloRequest, + chain: ApolloInterceptorChain + ): Flow> { + val builder = request.newBuilder() + .addHttpHeader(SENTRY_APOLLO_4_OPERATION_TYPE, Base64.encodeToString(operationType(request).toByteArray(), Base64.NO_WRAP)) + + request.scalarAdapters?.let { + builder.addHttpHeader(SENTRY_APOLLO_4_VARIABLES, Base64.encodeToString(request.operation.variables(it).valueMap.toString().toByteArray(), Base64.NO_WRAP)) + } + builder.addHttpHeader("X-APOLLO-OPERATION-NAME", request.operation.name()) + builder.addHttpHeader("X-APOLLO-OPERATION-ID", request.operation.id()) + return chain.proceed(builder.build()) + } +} + +private fun operationType(apolloRequest: ApolloRequest) = when (apolloRequest.operation) { + is Query -> "query" + is Mutation -> "mutation" + is Subscription -> "subscription" + else -> apolloRequest.operation.javaClass.simpleName +} + +private val ApolloRequest.scalarAdapters + get() = executionContext[CustomScalarAdapters] diff --git a/sentry-apollo-4/src/main/java/io/sentry/apollo4/SentryApolloBuilderExtensions.kt b/sentry-apollo-4/src/main/java/io/sentry/apollo4/SentryApolloBuilderExtensions.kt new file mode 100644 index 0000000000..a0e07225d1 --- /dev/null +++ b/sentry-apollo-4/src/main/java/io/sentry/apollo4/SentryApolloBuilderExtensions.kt @@ -0,0 +1,39 @@ +package io.sentry.apollo4 + +import com.apollographql.apollo.ApolloClient +import io.sentry.IScopes +import io.sentry.ScopesAdapter +import io.sentry.SentryOptions.DEFAULT_PROPAGATION_TARGETS +import io.sentry.apollo4.SentryApollo4HttpInterceptor.Companion.DEFAULT_CAPTURE_FAILED_REQUESTS + +@JvmOverloads +fun ApolloClient.Builder.sentryTracing( + scopes: IScopes = ScopesAdapter.getInstance(), + captureFailedRequests: Boolean = DEFAULT_CAPTURE_FAILED_REQUESTS, + failedRequestTargets: List = listOf(DEFAULT_PROPAGATION_TARGETS), + beforeSpan: SentryApollo4HttpInterceptor.BeforeSpanCallback? = null +): ApolloClient.Builder { + addInterceptor(SentryApollo4Interceptor()) + addHttpInterceptor( + SentryApollo4HttpInterceptor( + scopes = scopes, + captureFailedRequests = captureFailedRequests, + failedRequestTargets = failedRequestTargets, + beforeSpan = beforeSpan + ) + ) + return this +} + +fun ApolloClient.Builder.sentryTracing( + captureFailedRequests: Boolean = DEFAULT_CAPTURE_FAILED_REQUESTS, + failedRequestTargets: List = listOf(DEFAULT_PROPAGATION_TARGETS), + beforeSpan: SentryApollo4HttpInterceptor.BeforeSpanCallback? = null +): ApolloClient.Builder { + return sentryTracing( + scopes = ScopesAdapter.getInstance(), + captureFailedRequests = captureFailedRequests, + failedRequestTargets = failedRequestTargets, + beforeSpan = beforeSpan + ) +} diff --git a/sentry-apollo-4/src/test/java/io/sentry/apollo4/LaunchDetailsQuery.kt b/sentry-apollo-4/src/test/java/io/sentry/apollo4/LaunchDetailsQuery.kt new file mode 100644 index 0000000000..136cf21848 --- /dev/null +++ b/sentry-apollo-4/src/test/java/io/sentry/apollo4/LaunchDetailsQuery.kt @@ -0,0 +1,89 @@ +package io.sentry.apollo4 + +import com.apollographql.apollo.api.Adapter +import com.apollographql.apollo.api.CompiledField +import com.apollographql.apollo.api.CustomScalarAdapters +import com.apollographql.apollo.api.Query +import com.apollographql.apollo.api.json.JsonWriter +import com.apollographql.apollo.api.obj +import io.sentry.apollo4.adapter.LaunchDetailsQuery_ResponseAdapter +import io.sentry.apollo4.adapter.LaunchDetailsQuery_VariablesAdapter +import io.sentry.apollo4.selections.LaunchDetailsQuerySelections +import kotlin.String + +public data class LaunchDetailsQuery( + public val id: String +) : Query { + public override fun id(): String = OPERATION_ID + + public override fun document(): String = OPERATION_DOCUMENT + + public override fun name(): String = OPERATION_NAME + + public override fun serializeVariables( + writer: JsonWriter, + customScalarAdapters: CustomScalarAdapters, + withDefaultValues: Boolean + ) { + LaunchDetailsQuery_VariablesAdapter.toJson(writer, customScalarAdapters, this) + } + + public override fun adapter(): Adapter = LaunchDetailsQuery_ResponseAdapter.Data.obj() + + public override fun rootField(): CompiledField = CompiledField.Builder( + name = "data", + type = io.sentry.apollo4.type.Query.type + ) + .selections(selections = LaunchDetailsQuerySelections.root) + .build() + + public data class Data( + public val launch: Launch? + ) : Query.Data + + public data class Launch( + public val id: String, + public val site: String?, + public val mission: Mission?, + public val rocket: Rocket? + ) + + public data class Mission( + public val name: String?, + public val missionPatch: String? + ) + + public data class Rocket( + public val name: String?, + public val type: String? + ) + + public companion object { + public const val OPERATION_ID: String = + "1b3bda4a2dcb47a77aa30346e10339d4600e0cbe9fa686867e9226e463b7118d" + + /** + * The minimized GraphQL document being sent to the server to save a few bytes. + * The un-minimized version is: + * + * query LaunchDetails($id: ID!) { + * launch(id: $id) { + * id + * site + * mission { + * name + * missionPatch(size: LARGE) + * } + * rocket { + * name + * type + * } + * } + * } + */ + public const val OPERATION_DOCUMENT: String = + "query LaunchDetails(${'$'}id: ID!) { launch(id: ${'$'}id) { id site mission { name missionPatch(size: LARGE) } rocket { name type } } }" + + public const val OPERATION_NAME: String = "LaunchDetails" + } +} diff --git a/sentry-apollo-4/src/test/java/io/sentry/apollo4/SentryApollo4InterceptorClientErrors.kt b/sentry-apollo-4/src/test/java/io/sentry/apollo4/SentryApollo4InterceptorClientErrors.kt new file mode 100644 index 0000000000..64e9e885a8 --- /dev/null +++ b/sentry-apollo-4/src/test/java/io/sentry/apollo4/SentryApollo4InterceptorClientErrors.kt @@ -0,0 +1,390 @@ +package io.sentry.apollo4 + +import com.apollographql.apollo.ApolloClient +import com.apollographql.apollo.api.http.HttpRequest +import com.apollographql.apollo.api.http.HttpResponse +import com.apollographql.apollo.exception.ApolloException +import io.sentry.Hint +import io.sentry.IScopes +import io.sentry.SentryIntegrationPackageStorage +import io.sentry.SentryOptions +import io.sentry.SentryOptions.DEFAULT_PROPAGATION_TARGETS +import io.sentry.TypeCheckHint +import io.sentry.apollo4.SentryApollo4HttpInterceptor.Companion.DEFAULT_CAPTURE_FAILED_REQUESTS +import io.sentry.exception.ExceptionMechanismException +import io.sentry.protocol.SdkVersion +import io.sentry.protocol.SentryId +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import okhttp3.mockwebserver.MockResponse +import okhttp3.mockwebserver.MockWebServer +import okhttp3.mockwebserver.SocketPolicy +import org.mockito.kotlin.any +import org.mockito.kotlin.check +import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNotNull +import kotlin.test.assertNull +import kotlin.test.assertTrue + +class SentryApollo4InterceptorClientErrors { + class Fixture { + val server = MockWebServer() + lateinit var scopes: IScopes + + private val responseBodyOk = + """{ + "data": { + "launch": { + "__typename": "Launch", + "id": "83", + "site": "CCAFS SLC 40", + "mission": { + "__typename": "Mission", + "name": "Amos-17", + "missionPatch": "https://images2.imgbox.com/a0/ab/XUoByiuR_o.png" + } + } + } +}""" + + val responseBodyNotOk = + """{ + "errors": [ + { + "message": "Cannot query field \"mySite\" on type \"Launch\". Did you mean \"site\"?", + "extensions": { + "code": "GRAPHQL_VALIDATION_FAILED" + } + } + ] +}""" + + fun getSut( + captureFailedRequests: Boolean = DEFAULT_CAPTURE_FAILED_REQUESTS, + failedRequestTargets: List = listOf(DEFAULT_PROPAGATION_TARGETS), + httpStatusCode: Int = 200, + responseBody: String = responseBodyOk, + sendDefaultPii: Boolean = false, + socketPolicy: SocketPolicy = SocketPolicy.KEEP_OPEN + ): ApolloClient { + SentryIntegrationPackageStorage.getInstance().clearStorage() + + scopes = mock().apply { + whenever(options).thenReturn( + SentryOptions().apply { + dsn = "https://key@sentry.io/proj" + sdkVersion = SdkVersion("test", "1.2.3") + isSendDefaultPii = sendDefaultPii + } + ) + } + whenever(scopes.captureEvent(any(), any())).thenReturn(SentryId.EMPTY_ID) + + val response = MockResponse() + .setBody(responseBody) + .setSocketPolicy(socketPolicy) + .setResponseCode(httpStatusCode) + + if (sendDefaultPii) { + response.addHeader("Set-Cookie", "Test") + } + + server.enqueue( + response + ) + + val builder = ApolloClient.Builder() + .serverUrl(server.url("?myQuery=query#myFragment").toString()) + .sentryTracing( + scopes = scopes, + captureFailedRequests = captureFailedRequests, + failedRequestTargets = failedRequestTargets + ) + if (sendDefaultPii) { + builder.addHttpHeader("Cookie", "Test") + } + + return builder.build() + } + } + + private val fixture = Fixture() + + // region captureFailedRequests + + @Test + fun `does not capture errors if captureFailedRequests is disabled`() { + val sut = fixture.getSut(captureFailedRequests = false, responseBody = fixture.responseBodyNotOk) + executeQuery(sut) + + verify(fixture.scopes, never()).captureEvent(any(), any()) + } + + @Test + fun `capture errors if captureFailedRequests is enabled`() { + val sut = + fixture.getSut(responseBody = fixture.responseBodyNotOk) + executeQuery(sut) + + verify(fixture.scopes).captureEvent(any(), any()) + } + + // endregion + + // region Apollo4ClientError + + @Test + fun `does not add Apollo4ClientError integration if captureFailedRequests is disabled`() { + fixture.getSut(captureFailedRequests = false) + + assertFalse(SentryIntegrationPackageStorage.getInstance().integrations.contains("Apollo4ClientError")) + } + + @Test + fun `adds Apollo4ClientError integration if captureFailedRequests is enabled`() { + fixture.getSut() + + assertTrue(SentryIntegrationPackageStorage.getInstance().integrations.contains("Apollo4ClientError")) + } + + // endregion + + // region failedRequestTargets + + @Test + fun `does not capture errors if failedRequestTargets does not match`() { + val sut = fixture.getSut( + failedRequestTargets = listOf("nope.com"), + responseBody = fixture.responseBodyNotOk + ) + executeQuery(sut) + + verify(fixture.scopes, never()).captureEvent(any(), any()) + } + + @Test + fun `capture errors if failedRequestTargets matches`() { + val sut = + fixture.getSut(responseBody = fixture.responseBodyNotOk) + executeQuery(sut) + + verify(fixture.scopes).captureEvent(any(), any()) + } + + // endregion + + // region SentryEvent + + @Test + fun `capture errors with SentryApollo4Interceptor mechanism`() { + val sut = + fixture.getSut(responseBody = fixture.responseBodyNotOk) + executeQuery(sut) + + verify(fixture.scopes).captureEvent( + check { + val throwable = (it.throwableMechanism as ExceptionMechanismException) + assertEquals("SentryApollo4Interceptor", throwable.exceptionMechanism.type) + }, + any() + ) + } + + @Test + fun `capture errors with title`() { + val sut = + fixture.getSut(responseBody = fixture.responseBodyNotOk) + executeQuery(sut) + + verify(fixture.scopes).captureEvent( + check { + val throwable = (it.throwableMechanism as ExceptionMechanismException) + assertEquals("GraphQL Request failed, name: LaunchDetails, type: query", throwable.throwable.message) + }, + any() + ) + } + + @Test + fun `capture errors with snapshot flag set`() { + val sut = + fixture.getSut(responseBody = fixture.responseBodyNotOk) + executeQuery(sut) + + verify(fixture.scopes).captureEvent( + check { + val throwable = (it.throwableMechanism as ExceptionMechanismException) + assertTrue(throwable.isSnapshot) + }, + any() + ) + } + + private val escapeDolar = "\$id" + + @Test + fun `capture errors with request context`() { + val sut = + fixture.getSut(responseBody = fixture.responseBodyNotOk) + executeQuery(sut) + val body = + """ +{"operationName":"LaunchDetails","variables":{"id":"83"},"query":"query LaunchDetails($escapeDolar: ID!) { launch(id: $escapeDolar) { id site mission { name missionPatch(size: LARGE) } rocket { name type } } }"} + """.trimIndent() + + verify(fixture.scopes).captureEvent( + check { + val request = it.request!! + + assertEquals("http://localhost:${fixture.server.port}/", request.url) + assertEquals("myQuery=query", request.queryString) + assertEquals("myFragment", request.fragment) + assertEquals("Post", request.method) + assertEquals("graphql", request.apiTarget) + assertEquals(193L, request.bodySize) + assertEquals(body, request.data) + assertNull(request.cookies) + assertNull(request.headers) + }, + any() + ) + } + + @Test + fun `capture errors with more request context if sendDefaultPii is enabled`() { + val sut = + fixture.getSut(responseBody = fixture.responseBodyNotOk, sendDefaultPii = true) + executeQuery(sut) + + verify(fixture.scopes).captureEvent( + check { + val request = it.request!! + + assertEquals("Test", request.cookies) + assertNotNull(request.headers) + assertEquals("LaunchDetails", request.headers?.get("X-APOLLO-OPERATION-NAME")) + }, + any() + ) + } + + @Test + fun `capture errors with response context`() { + val sut = + fixture.getSut(responseBody = fixture.responseBodyNotOk) + executeQuery(sut) + + verify(fixture.scopes).captureEvent( + check { + val response = it.contexts.response!! + + assertEquals(200, response.statusCode) + assertEquals(200, response.bodySize) + assertEquals(fixture.responseBodyNotOk, response.data) + assertNull(response.cookies) + assertNull(response.headers) + }, + any() + ) + } + + @Test + fun `capture errors with more response context if sendDefaultPii is enabled`() { + val sut = + fixture.getSut(responseBody = fixture.responseBodyNotOk, sendDefaultPii = true) + executeQuery(sut) + + verify(fixture.scopes).captureEvent( + check { + val response = it.contexts.response!! + + assertEquals("Test", response.cookies) + assertNotNull(response.headers) + assertEquals(200, response.headers?.get("Content-Length")?.toInt()) + }, + any() + ) + } + + @Test + fun `capture errors with specific fingerprints`() { + val sut = + fixture.getSut(responseBody = fixture.responseBodyNotOk) + executeQuery(sut) + + verify(fixture.scopes).captureEvent( + check { + assertEquals(listOf("LaunchDetails", "query", "200"), it.fingerprints) + }, + any() + ) + } + + // endregion + + // region errors + + @Test + fun `capture errors if response code is equal or higher than 400`() { + val sut = + fixture.getSut(responseBody = fixture.responseBodyNotOk, httpStatusCode = 500) + executeQuery(sut) + + // HttpInterceptor does not throw for >= 400 + verify(fixture.scopes).captureEvent(any(), any()) + } + + @Test + fun `capture errors swallow any exception during the error transformation`() { + val sut = + fixture.getSut(responseBody = fixture.responseBodyNotOk) + + whenever(fixture.scopes.captureEvent(any(), any())).thenThrow(RuntimeException()) + + executeQuery(sut) + } + + // endregion + + // region hints + + @Test + fun `hints are set when capturing errors`() { + val sut = + fixture.getSut(responseBody = fixture.responseBodyNotOk) + executeQuery(sut) + + verify(fixture.scopes).captureEvent( + any(), + check { + val request = it.get(TypeCheckHint.APOLLO_REQUEST) + assertNotNull(request) + assertTrue(request is HttpRequest) + + val response = it.get(TypeCheckHint.APOLLO_RESPONSE) + assertNotNull(response) + assertTrue(response is HttpResponse) + } + ) + } + + // endregion + + private fun executeQuery(sut: ApolloClient, id: String = "83") = runBlocking { + val coroutine = launch { + try { + sut.query(LaunchDetailsQuery(id)).execute() + } catch (e: ApolloException) { + return@launch + } + } + + coroutine.join() + } +} diff --git a/sentry-apollo-4/src/test/java/io/sentry/apollo4/SentryApollo4InterceptorTest.kt b/sentry-apollo-4/src/test/java/io/sentry/apollo4/SentryApollo4InterceptorTest.kt new file mode 100644 index 0000000000..f7232a65fc --- /dev/null +++ b/sentry-apollo-4/src/test/java/io/sentry/apollo4/SentryApollo4InterceptorTest.kt @@ -0,0 +1,397 @@ +package io.sentry.apollo4 + +import com.apollographql.apollo.ApolloClient +import com.apollographql.apollo.api.ApolloRequest +import com.apollographql.apollo.api.ApolloResponse +import com.apollographql.apollo.api.Operation +import com.apollographql.apollo.api.http.HttpRequest +import com.apollographql.apollo.api.http.HttpResponse +import com.apollographql.apollo.exception.ApolloException +import com.apollographql.apollo.exception.ApolloHttpException +import com.apollographql.apollo.interceptor.ApolloInterceptor +import com.apollographql.apollo.interceptor.ApolloInterceptorChain +import com.apollographql.apollo.network.http.HttpInterceptor +import com.apollographql.apollo.network.http.HttpInterceptorChain +import io.sentry.BaggageHeader +import io.sentry.Breadcrumb +import io.sentry.IScopes +import io.sentry.ITransaction +import io.sentry.Scope +import io.sentry.ScopeCallback +import io.sentry.SentryOptions +import io.sentry.SentryOptions.DEFAULT_PROPAGATION_TARGETS +import io.sentry.SentryTraceHeader +import io.sentry.SentryTracer +import io.sentry.SpanDataConvention +import io.sentry.SpanDataConvention.HTTP_METHOD_KEY +import io.sentry.SpanStatus +import io.sentry.TraceContext +import io.sentry.TracesSamplingDecision +import io.sentry.TransactionContext +import io.sentry.apollo4.SentryApollo4HttpInterceptor.BeforeSpanCallback +import io.sentry.mockServerRequestTimeoutMillis +import io.sentry.protocol.SdkVersion +import io.sentry.protocol.SentryTransaction +import io.sentry.util.Apollo4PlatformTestManipulator +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import okhttp3.mockwebserver.MockResponse +import okhttp3.mockwebserver.MockWebServer +import okhttp3.mockwebserver.SocketPolicy +import org.junit.Before +import org.mockito.kotlin.any +import org.mockito.kotlin.anyOrNull +import org.mockito.kotlin.check +import org.mockito.kotlin.doAnswer +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import java.util.concurrent.TimeUnit +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull +import kotlin.test.assertTrue + +class SentryApollo4InterceptorTest { + + class Fixture { + val server = MockWebServer() + val options = + SentryOptions().apply { + dsn = "https://key@sentry.io/proj" + setTracePropagationTargets(listOf(DEFAULT_PROPAGATION_TARGETS)) + sdkVersion = SdkVersion("test", "1.2.3") + } + val scope = Scope(options) + val scopes = mock().also { + whenever(it.options).thenReturn(options) + doAnswer { (it.arguments[0] as ScopeCallback).run(scope) }.whenever(it).configureScope(any()) + } + private var httpInterceptor = SentryApollo4HttpInterceptor(scopes, captureFailedRequests = false) + + @SuppressWarnings("LongParameterList") + fun getSut( + httpStatusCode: Int = 200, + responseBody: String = """{ + "data": { + "launch": { + "__typename": "Launch", + "id": "83", + "site": "CCAFS SLC 40", + "mission": { + "__typename": "Mission", + "name": "Amos-17", + "missionPatch": "https://images2.imgbox.com/a0/ab/XUoByiuR_o.png" + } + } + } +}""", + socketPolicy: SocketPolicy = SocketPolicy.KEEP_OPEN, + interceptor: HttpInterceptor? = null, + addThirdPartyBaggageHeader: Boolean = false, + beforeSpan: BeforeSpanCallback? = null + ): ApolloClient { + server.enqueue( + MockResponse() + .setBody(responseBody) + .setSocketPolicy(socketPolicy) + .setResponseCode(httpStatusCode) + ) + + if (beforeSpan != null) { + httpInterceptor = SentryApollo4HttpInterceptor(scopes, beforeSpan, captureFailedRequests = false) + } + + val builder = ApolloClient.Builder() + .serverUrl(server.url("/").toString()) + .addHttpInterceptor(httpInterceptor) + .addInterceptor(object : ApolloInterceptor { // Re-enable legacy headers + override fun intercept(request: ApolloRequest, chain: ApolloInterceptorChain): Flow> { + return chain.proceed( + request.newBuilder() + .addHttpHeader("X-APOLLO-OPERATION-NAME", request.operation.name()) + .addHttpHeader("X-APOLLO-OPERATION-ID", request.operation.id()) + .build() + ) + } + }) + + interceptor?.let { + builder.addHttpInterceptor(interceptor) + } + + if (addThirdPartyBaggageHeader) { + builder.addHttpHeader("baggage", "thirdPartyBaggage=someValue") + .addHttpHeader("baggage", "secondThirdPartyBaggage=secondValue; property;propertyKey=propertyValue,anotherThirdPartyBaggage=anotherValue") + } + + return builder.build() + } + } + + private val fixture = Fixture() + + @Before + fun setup() { + Apollo4PlatformTestManipulator.pretendIsAndroid(false) + } + + @Test + fun `creates a span around the successful request`() { + executeQuery() + + verify(fixture.scopes).captureTransaction( + check { + assertTransactionDetails(it, httpStatusCode = 200) + assertEquals(SpanStatus.OK, it.spans.first().status) + }, + anyOrNull(), + anyOrNull(), + anyOrNull() + ) + } + + @Test + fun `creates a span around the failed request`() { + executeQuery(fixture.getSut(httpStatusCode = 403)) + + verify(fixture.scopes).captureTransaction( + check { + assertTransactionDetails(it, httpStatusCode = 403) + assertEquals(SpanStatus.PERMISSION_DENIED, it.spans.first().status) + }, + anyOrNull(), + anyOrNull(), + anyOrNull() + ) + } + + @Test + fun `get http status from ApolloHttpException in failed request`() { + val failingInterceptor = object : HttpInterceptor { + override suspend fun intercept(request: HttpRequest, chain: HttpInterceptorChain): HttpResponse { + throw ApolloHttpException(404, mock(), mock(), "") + } + } + executeQuery(fixture.getSut(interceptor = failingInterceptor)) + + verify(fixture.scopes).captureTransaction( + check { + assertTransactionDetails(it, httpStatusCode = 404, contentLength = null) + assertEquals("POST", it.spans.first().data?.get(SpanDataConvention.HTTP_METHOD_KEY)) + assertEquals(404, it.spans.first().data?.get(SpanDataConvention.HTTP_STATUS_CODE_KEY)) + assertEquals(SpanStatus.NOT_FOUND, it.spans.first().status) + }, + anyOrNull(), + anyOrNull(), + anyOrNull() + ) + } + + @Test + fun `creates a span around the request failing with network error`() { + executeQuery(fixture.getSut(socketPolicy = SocketPolicy.DISCONNECT_DURING_REQUEST_BODY)) + + verify(fixture.scopes).captureTransaction( + check { + assertTransactionDetails(it, httpStatusCode = null, contentLength = null) + assertEquals(SpanStatus.INTERNAL_ERROR, it.spans.first().status) + }, + anyOrNull(), + anyOrNull(), + anyOrNull() + ) + } + + @Test + fun `does not add sentry trace header to the request if host is disallowed`() { + fixture.options.setTracePropagationTargets(listOf("some-host-that-does-not-exist")) + executeQuery(isSpanActive = false) + + val recorderRequest = fixture.server.takeRequest(mockServerRequestTimeoutMillis, TimeUnit.MILLISECONDS)!! + assertNull(recorderRequest.headers[SentryTraceHeader.SENTRY_TRACE_HEADER]) + assertNull(recorderRequest.headers[BaggageHeader.BAGGAGE_HEADER]) + } + + @Test + fun `when there is no active span, does not add sentry trace header to the request`() { + executeQuery(isSpanActive = false) + + val recorderRequest = fixture.server.takeRequest(mockServerRequestTimeoutMillis, TimeUnit.MILLISECONDS)!! + assertNotNull(recorderRequest.headers[SentryTraceHeader.SENTRY_TRACE_HEADER]) + assertNotNull(recorderRequest.headers[BaggageHeader.BAGGAGE_HEADER]) + } + + @Test + fun `does not add sentry-trace header when span origin is ignored`() { + fixture.options.setIgnoredSpanOrigins(listOf("auto.graphql.apollo4")) + executeQuery(isSpanActive = false) + + val recorderRequest = fixture.server.takeRequest(mockServerRequestTimeoutMillis, TimeUnit.MILLISECONDS)!! + assertNull(recorderRequest.headers[SentryTraceHeader.SENTRY_TRACE_HEADER]) + assertNull(recorderRequest.headers[BaggageHeader.BAGGAGE_HEADER]) + } + + @Test + fun `when there is an active span, adds sentry trace headers to the request`() { + executeQuery() + val recorderRequest = fixture.server.takeRequest(mockServerRequestTimeoutMillis, TimeUnit.MILLISECONDS)!! + assertNotNull(recorderRequest.headers[SentryTraceHeader.SENTRY_TRACE_HEADER]) + assertNotNull(recorderRequest.headers[BaggageHeader.BAGGAGE_HEADER]) + } + + @Test + fun `when there is an active span, existing baggage headers are merged with sentry baggage into single header`() { + executeQuery(sut = fixture.getSut(addThirdPartyBaggageHeader = true)) + val recorderRequest = fixture.server.takeRequest(mockServerRequestTimeoutMillis, TimeUnit.MILLISECONDS)!! + assertNotNull(recorderRequest.headers[SentryTraceHeader.SENTRY_TRACE_HEADER]) + assertNotNull(recorderRequest.headers[BaggageHeader.BAGGAGE_HEADER]) + + val baggageHeaderValues = recorderRequest.headers.values(BaggageHeader.BAGGAGE_HEADER) + assertEquals(baggageHeaderValues.size, 1) + assertTrue(baggageHeaderValues[0].startsWith("thirdPartyBaggage=someValue,secondThirdPartyBaggage=secondValue; property;propertyKey=propertyValue,anotherThirdPartyBaggage=anotherValue")) + assertTrue(baggageHeaderValues[0].contains("sentry-public_key=key")) + assertTrue(baggageHeaderValues[0].contains("sentry-transaction=op")) + assertTrue(baggageHeaderValues[0].contains("sentry-trace_id")) + } + + @Test + fun `customizer modifies span`() { + executeQuery( + + fixture.getSut( + beforeSpan = { span, request, response -> + span.description = "overwritten description" + span + } + ) + ) + + verify(fixture.scopes).captureTransaction( + check { + assertEquals(1, it.spans.size) + val httpClientSpan = it.spans.first() + assertEquals("overwritten description", httpClientSpan.description) + }, + anyOrNull(), + anyOrNull(), + anyOrNull() + ) + } + + @Test + fun `returning null in beforeSpan callback drops span`() { + executeQuery( + fixture.getSut( + beforeSpan = { _, _, _ -> null } + ) + ) + + verify(fixture.scopes).captureTransaction( + check { + assertEquals(0, it.spans.size) + }, + anyOrNull(), + anyOrNull(), + anyOrNull() + ) + } + + @Test + fun `when customizer throws, exception is handled`() { + executeQuery( + fixture.getSut( + beforeSpan = { _, _, _ -> + throw RuntimeException() + } + ) + ) + + verify(fixture.scopes).captureTransaction( + check { + assertEquals(1, it.spans.size) + }, + anyOrNull(), + anyOrNull(), + anyOrNull() + ) + } + + @Test + fun `adds breadcrumb when http calls succeeds`() { + executeQuery(fixture.getSut()) + verify(fixture.scopes).addBreadcrumb( + check { + assertEquals("http", it.type) + // response_body_size is added but mock webserver returns 0 always + assertEquals(0L, it.data["response_body_size"]) + assertEquals(193L, it.data["request_body_size"]) + assertEquals("LaunchDetails", it.data["operation_name"]) + assertNotNull(it.data["operation_id"]) + }, + anyOrNull() + ) + } + + @Test + fun `sets SDKVersion Info`() { + assertNotNull(fixture.scopes.options.sdkVersion) + assert(fixture.scopes.options.sdkVersion!!.integrationSet.contains("Apollo4")) + val packageInfo = fixture.scopes.options.sdkVersion!!.packageSet.firstOrNull { pkg -> pkg.name == "maven:io.sentry:sentry-apollo-4" } + assertNotNull(packageInfo) + assert(packageInfo.version == BuildConfig.VERSION_NAME) + } + + @Test + fun `attaches to root transaction on Android`() { + Apollo4PlatformTestManipulator.pretendIsAndroid(true) + executeQuery(fixture.getSut()) + verify(fixture.scopes).transaction + } + + @Test + fun `attaches to child span on non-Android`() { + Apollo4PlatformTestManipulator.pretendIsAndroid(false) + executeQuery(fixture.getSut()) + verify(fixture.scopes).span + } + + private fun assertTransactionDetails(it: SentryTransaction, httpStatusCode: Int? = 200, contentLength: Long? = 0L) { + assertEquals(1, it.spans.size) + val httpClientSpan = it.spans.first() + assertEquals("http.graphql", httpClientSpan.op) + assertTrue { httpClientSpan.description?.startsWith("Post LaunchDetails") == true } + assertNotNull(httpClientSpan.data) { + assertNotNull(it["operationId"]) + assertEquals("POST", it[HTTP_METHOD_KEY]) + httpStatusCode?.let { code -> + assertEquals(code, it[SpanDataConvention.HTTP_STATUS_CODE_KEY]) + } + contentLength?.let { contentLength -> + assertEquals(contentLength, it[SpanDataConvention.HTTP_RESPONSE_CONTENT_LENGTH_KEY]) + } + } + } + + private fun executeQuery(sut: ApolloClient = fixture.getSut(), isSpanActive: Boolean = true, id: String = "83") = runBlocking { + var tx: ITransaction? = null + if (isSpanActive) { + tx = SentryTracer(TransactionContext("op", "desc", TracesSamplingDecision(true)), fixture.scopes) + whenever(fixture.scopes.transaction).thenReturn(tx) + whenever(fixture.scopes.span).thenReturn(tx) + } + + val coroutine = launch { + try { + sut.query(LaunchDetailsQuery(id)).execute() + } catch (e: ApolloException) { + return@launch + } + } + + coroutine.join() + tx?.finish() + } +} diff --git a/sentry-apollo-4/src/test/java/io/sentry/apollo4/SentryApollo4InterceptorWithVariablesTest.kt b/sentry-apollo-4/src/test/java/io/sentry/apollo4/SentryApollo4InterceptorWithVariablesTest.kt new file mode 100644 index 0000000000..c36d140972 --- /dev/null +++ b/sentry-apollo-4/src/test/java/io/sentry/apollo4/SentryApollo4InterceptorWithVariablesTest.kt @@ -0,0 +1,193 @@ +package io.sentry.apollo4 + +import com.apollographql.apollo.ApolloClient +import com.apollographql.apollo.exception.ApolloException +import io.sentry.Breadcrumb +import io.sentry.IScopes +import io.sentry.ITransaction +import io.sentry.SentryOptions +import io.sentry.SentryTracer +import io.sentry.SpanStatus +import io.sentry.TraceContext +import io.sentry.TracesSamplingDecision +import io.sentry.TransactionContext +import io.sentry.apollo4.SentryApollo4HttpInterceptor.BeforeSpanCallback +import io.sentry.mockServerRequestTimeoutMillis +import io.sentry.protocol.SentryTransaction +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import okhttp3.mockwebserver.MockResponse +import okhttp3.mockwebserver.MockWebServer +import okhttp3.mockwebserver.SocketPolicy +import org.mockito.kotlin.anyOrNull +import org.mockito.kotlin.check +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import java.util.concurrent.TimeUnit +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull + +class SentryApollo4InterceptorWithVariablesTest { + + class Fixture { + val server = MockWebServer() + val scopes = mock() + + @SuppressWarnings("LongParameterList") + fun getSut( + httpStatusCode: Int = 200, + responseBody: String = """{ + "data": { + "launch": { + "__typename": "Launch", + "id": "83", + "site": "CCAFS SLC 40", + "mission": { + "__typename": "Mission", + "name": "Amos-17", + "missionPatch": "https://images2.imgbox.com/a0/ab/XUoByiuR_o.png" + } + } + } +}""", + socketPolicy: SocketPolicy = SocketPolicy.KEEP_OPEN, + beforeSpan: BeforeSpanCallback? = null + ): ApolloClient { + whenever(scopes.options).thenReturn( + SentryOptions().apply { + dsn = "http://key@localhost/proj" + } + ) + + server.enqueue( + MockResponse() + .setBody(responseBody) + .setSocketPolicy(socketPolicy) + .setResponseCode(httpStatusCode) + ) + + return ApolloClient.Builder().serverUrl(server.url("/").toString()) + .sentryTracing(scopes = scopes, beforeSpan = beforeSpan, captureFailedRequests = false) + .build() + } + } + + private val fixture = Fixture() + + @Test + fun `creates a span around the successful request`() { + executeQuery() + + verify(fixture.scopes).captureTransaction( + check { + assertTransactionDetails(it) + assertEquals(SpanStatus.OK, it.spans.first().status) + }, + anyOrNull(), + anyOrNull(), + anyOrNull() + ) + } + + @Test + fun `creates a span around the failed request`() { + executeQuery(fixture.getSut(httpStatusCode = 403)) + + verify(fixture.scopes).captureTransaction( + check { + assertTransactionDetails(it) + assertEquals(SpanStatus.PERMISSION_DENIED, it.spans.first().status) + }, + anyOrNull(), + anyOrNull(), + anyOrNull() + ) + } + + @Test + fun `creates a span around the request failing with network error`() { + executeQuery(fixture.getSut(socketPolicy = SocketPolicy.DISCONNECT_DURING_REQUEST_BODY)) + + verify(fixture.scopes).captureTransaction( + check { + assertTransactionDetails(it) + assertEquals(SpanStatus.INTERNAL_ERROR, it.spans.first().status) + }, + anyOrNull(), + anyOrNull(), + anyOrNull() + ) + } + + @Test + fun `handles non-ascii header values correctly`() { + executeQuery(id = "á") + + verify(fixture.scopes).captureTransaction( + check { + assertTransactionDetails(it) + assertEquals(SpanStatus.OK, it.spans.first().status) + }, + anyOrNull(), + anyOrNull(), + anyOrNull() + ) + } + + @Test + fun `adds breadcrumb when http calls succeeds`() { + executeQuery(fixture.getSut()) + verify(fixture.scopes).addBreadcrumb( + check { + assertEquals("http", it.type) + // response_body_size is added but mock webserver returns 0 always + assertEquals(0L, it.data["response_body_size"]) + assertEquals(193L, it.data["request_body_size"]) + assertEquals("query", it.data["operation_type"]) + }, + anyOrNull() + ) + } + + @Test + fun `internal headers are not sent over the wire`() { + executeQuery(fixture.getSut()) + val recorderRequest = fixture.server.takeRequest(mockServerRequestTimeoutMillis, TimeUnit.MILLISECONDS)!! + assertNull(recorderRequest.headers[SentryApollo4HttpInterceptor.SENTRY_APOLLO_4_VARIABLES]) + assertNull(recorderRequest.headers[SentryApollo4HttpInterceptor.SENTRY_APOLLO_4_OPERATION_TYPE]) + } + + private fun assertTransactionDetails(it: SentryTransaction) { + assertEquals(1, it.spans.size) + val httpClientSpan = it.spans.first() + assertEquals("http.graphql.query", httpClientSpan.op) + assertEquals("query LaunchDetails", httpClientSpan.description) + assertEquals("auto.graphql.apollo4", httpClientSpan.origin) + assertNotNull(httpClientSpan.data) { + assertNotNull(it["operationId"]) + assertNotNull(it["variables"]) + } + } + + private fun executeQuery(sut: ApolloClient = fixture.getSut(), isSpanActive: Boolean = true, id: String = "83") = runBlocking { + var tx: ITransaction? = null + if (isSpanActive) { + tx = SentryTracer(TransactionContext("op", "desc", TracesSamplingDecision(true)), fixture.scopes) + whenever(fixture.scopes.span).thenReturn(tx) + } + + val coroutine = launch { + try { + sut.query(LaunchDetailsQuery(id)).execute() + } catch (e: ApolloException) { + return@launch + } + } + + coroutine.join() + tx?.finish() + } +} diff --git a/sentry-apollo-4/src/test/java/io/sentry/apollo4/adapter/LaunchDetailsQuery_ResponseAdapter.kt b/sentry-apollo-4/src/test/java/io/sentry/apollo4/adapter/LaunchDetailsQuery_ResponseAdapter.kt new file mode 100644 index 0000000000..e4f87980ec --- /dev/null +++ b/sentry-apollo-4/src/test/java/io/sentry/apollo4/adapter/LaunchDetailsQuery_ResponseAdapter.kt @@ -0,0 +1,166 @@ +// +// AUTO-GENERATED FILE. DO NOT MODIFY. +// +// This class was automatically generated by Apollo GraphQL version '3.3.0'. +// +package io.sentry.apollo4.adapter + +import com.apollographql.apollo.api.Adapter +import com.apollographql.apollo.api.CustomScalarAdapters +import com.apollographql.apollo.api.NullableStringAdapter +import com.apollographql.apollo.api.StringAdapter +import com.apollographql.apollo.api.json.JsonReader +import com.apollographql.apollo.api.json.JsonWriter +import com.apollographql.apollo.api.nullable +import com.apollographql.apollo.api.obj +import io.sentry.apollo4.LaunchDetailsQuery +import kotlin.String +import kotlin.collections.List + +public object LaunchDetailsQuery_ResponseAdapter { + public object Data : Adapter { + public val RESPONSE_NAMES: List = listOf("launch") + + public override fun fromJson(reader: JsonReader, customScalarAdapters: CustomScalarAdapters): + LaunchDetailsQuery.Data { + var launch: LaunchDetailsQuery.Launch? = null + + while (true) { + when (reader.selectName(RESPONSE_NAMES)) { + 0 -> launch = Launch.obj().nullable().fromJson(reader, customScalarAdapters) + else -> break + } + } + + return LaunchDetailsQuery.Data( + launch = launch + ) + } + + public override fun toJson( + writer: JsonWriter, + customScalarAdapters: CustomScalarAdapters, + `value`: LaunchDetailsQuery.Data + ) { + writer.name("launch") + Launch.obj().nullable().toJson(writer, customScalarAdapters, value.launch) + } + } + + public object Launch : Adapter { + public val RESPONSE_NAMES: List = listOf("id", "site", "mission", "rocket") + + public override fun fromJson(reader: JsonReader, customScalarAdapters: CustomScalarAdapters): + LaunchDetailsQuery.Launch { + var id: String? = null + var site: String? = null + var mission: LaunchDetailsQuery.Mission? = null + var rocket: LaunchDetailsQuery.Rocket? = null + + while (true) { + when (reader.selectName(RESPONSE_NAMES)) { + 0 -> id = StringAdapter.fromJson(reader, customScalarAdapters) + 1 -> site = NullableStringAdapter.fromJson(reader, customScalarAdapters) + 2 -> mission = Mission.obj().nullable().fromJson(reader, customScalarAdapters) + 3 -> rocket = Rocket.obj().nullable().fromJson(reader, customScalarAdapters) + else -> break + } + } + + return LaunchDetailsQuery.Launch( + id = id!!, + site = site, + mission = mission, + rocket = rocket + ) + } + + public override fun toJson( + writer: JsonWriter, + customScalarAdapters: CustomScalarAdapters, + `value`: LaunchDetailsQuery.Launch + ) { + writer.name("id") + StringAdapter.toJson(writer, customScalarAdapters, value.id) + + writer.name("site") + NullableStringAdapter.toJson(writer, customScalarAdapters, value.site) + + writer.name("mission") + Mission.obj().nullable().toJson(writer, customScalarAdapters, value.mission) + + writer.name("rocket") + Rocket.obj().nullable().toJson(writer, customScalarAdapters, value.rocket) + } + } + + public object Mission : Adapter { + public val RESPONSE_NAMES: List = listOf("name", "missionPatch") + + public override fun fromJson(reader: JsonReader, customScalarAdapters: CustomScalarAdapters): + LaunchDetailsQuery.Mission { + var name: String? = null + var missionPatch: String? = null + + while (true) { + when (reader.selectName(RESPONSE_NAMES)) { + 0 -> name = NullableStringAdapter.fromJson(reader, customScalarAdapters) + 1 -> missionPatch = NullableStringAdapter.fromJson(reader, customScalarAdapters) + else -> break + } + } + + return LaunchDetailsQuery.Mission( + name = name, + missionPatch = missionPatch + ) + } + + public override fun toJson( + writer: JsonWriter, + customScalarAdapters: CustomScalarAdapters, + `value`: LaunchDetailsQuery.Mission + ) { + writer.name("name") + NullableStringAdapter.toJson(writer, customScalarAdapters, value.name) + + writer.name("missionPatch") + NullableStringAdapter.toJson(writer, customScalarAdapters, value.missionPatch) + } + } + + public object Rocket : Adapter { + public val RESPONSE_NAMES: List = listOf("name", "type") + + public override fun fromJson(reader: JsonReader, customScalarAdapters: CustomScalarAdapters): + LaunchDetailsQuery.Rocket { + var name: String? = null + var type: String? = null + + while (true) { + when (reader.selectName(RESPONSE_NAMES)) { + 0 -> name = NullableStringAdapter.fromJson(reader, customScalarAdapters) + 1 -> type = NullableStringAdapter.fromJson(reader, customScalarAdapters) + else -> break + } + } + + return LaunchDetailsQuery.Rocket( + name = name, + type = type + ) + } + + public override fun toJson( + writer: JsonWriter, + customScalarAdapters: CustomScalarAdapters, + `value`: LaunchDetailsQuery.Rocket + ) { + writer.name("name") + NullableStringAdapter.toJson(writer, customScalarAdapters, value.name) + + writer.name("type") + NullableStringAdapter.toJson(writer, customScalarAdapters, value.type) + } + } +} diff --git a/sentry-apollo-4/src/test/java/io/sentry/apollo4/adapter/LaunchDetailsQuery_VariablesAdapter.kt b/sentry-apollo-4/src/test/java/io/sentry/apollo4/adapter/LaunchDetailsQuery_VariablesAdapter.kt new file mode 100644 index 0000000000..b0d6f498e0 --- /dev/null +++ b/sentry-apollo-4/src/test/java/io/sentry/apollo4/adapter/LaunchDetailsQuery_VariablesAdapter.kt @@ -0,0 +1,27 @@ +// +// AUTO-GENERATED FILE. DO NOT MODIFY. +// +// This class was automatically generated by Apollo GraphQL version '3.3.0'. +// +package io.sentry.apollo4.adapter + +import com.apollographql.apollo.api.Adapter +import com.apollographql.apollo.api.CustomScalarAdapters +import com.apollographql.apollo.api.StringAdapter +import com.apollographql.apollo.api.json.JsonReader +import com.apollographql.apollo.api.json.JsonWriter +import io.sentry.apollo4.LaunchDetailsQuery + +object LaunchDetailsQuery_VariablesAdapter : Adapter { + override fun fromJson(reader: JsonReader, customScalarAdapters: CustomScalarAdapters): + LaunchDetailsQuery = throw IllegalStateException("Input type used in output position") + + override fun toJson( + writer: JsonWriter, + customScalarAdapters: CustomScalarAdapters, + `value`: LaunchDetailsQuery + ) { + writer.name("id") + StringAdapter.toJson(writer, customScalarAdapters, value.id) + } +} diff --git a/sentry-apollo-4/src/test/java/io/sentry/apollo4/selections/LaunchDetailsQuerySelections.kt b/sentry-apollo-4/src/test/java/io/sentry/apollo4/selections/LaunchDetailsQuerySelections.kt new file mode 100644 index 0000000000..522f2b18e2 --- /dev/null +++ b/sentry-apollo-4/src/test/java/io/sentry/apollo4/selections/LaunchDetailsQuerySelections.kt @@ -0,0 +1,78 @@ +// +// AUTO-GENERATED FILE. DO NOT MODIFY. +// +// This class was automatically generated by Apollo GraphQL version '3.3.0'. +// +package io.sentry.apollo4.selections + +import com.apollographql.apollo.api.* +import io.sentry.apollo4.type.GraphQLID +import io.sentry.apollo4.type.GraphQLString +import io.sentry.apollo4.type.Launch +import io.sentry.apollo4.type.Mission +import io.sentry.apollo4.type.Query.Companion.type +import io.sentry.apollo4.type.Rocket +import kotlin.collections.List + +public object LaunchDetailsQuerySelections { + private val mission: List = listOf( + CompiledField.Builder( + name = "name", + type = GraphQLString.type + ).build(), + CompiledField.Builder( + name = "missionPatch", + type = GraphQLString.type + ).arguments( + listOf( + CompiledArgument.Builder(CompiledArgumentDefinition.Builder("size").build()).value("LARGE").build() + ) + ) + .build() + ) + + private val rocket: List = listOf( + CompiledField.Builder( + name = "name", + type = GraphQLString.type + ).build(), + CompiledField.Builder( + name = "type", + type = GraphQLString.type + ).build() + ) + + private val launch: List = listOf( + CompiledField.Builder( + name = "id", + type = GraphQLID.type.notNull() + ).build(), + CompiledField.Builder( + name = "site", + type = GraphQLString.type + ).build(), + CompiledField.Builder( + name = "mission", + type = Mission.type + ).selections(mission) + .build(), + CompiledField.Builder( + name = "rocket", + type = Rocket.type + ).selections(rocket) + .build() + ) + + public val root: List = listOf( + CompiledField.Builder( + name = "launch", + type = Launch.type + ).arguments( + listOf( + CompiledArgument.Builder(CompiledArgumentDefinition.Builder("id").build()).value("id").build() + ) + ) + .selections(launch) + .build() + ) +} diff --git a/sentry-apollo-4/src/test/java/io/sentry/apollo4/type/GraphQLBoolean.kt b/sentry-apollo-4/src/test/java/io/sentry/apollo4/type/GraphQLBoolean.kt new file mode 100644 index 0000000000..26b23bf04f --- /dev/null +++ b/sentry-apollo-4/src/test/java/io/sentry/apollo4/type/GraphQLBoolean.kt @@ -0,0 +1,17 @@ +// +// AUTO-GENERATED FILE. DO NOT MODIFY. +// +// This class was automatically generated by Apollo GraphQL version '3.3.0'. +// +package io.sentry.apollo4.type + +import com.apollographql.apollo.api.CustomScalarType + +/** + * The `Boolean` scalar type represents `true` or `false`. + */ +public class GraphQLBoolean { + public companion object { + public val type: CustomScalarType = CustomScalarType("Boolean", "kotlin.Boolean") + } +} diff --git a/sentry-apollo-4/src/test/java/io/sentry/apollo4/type/GraphQLID.kt b/sentry-apollo-4/src/test/java/io/sentry/apollo4/type/GraphQLID.kt new file mode 100644 index 0000000000..89a617047d --- /dev/null +++ b/sentry-apollo-4/src/test/java/io/sentry/apollo4/type/GraphQLID.kt @@ -0,0 +1,20 @@ +// +// AUTO-GENERATED FILE. DO NOT MODIFY. +// +// This class was automatically generated by Apollo GraphQL version '3.3.0'. +// +package io.sentry.apollo4.type + +import com.apollographql.apollo.api.CustomScalarType + +/** + * The `ID` scalar type represents a unique identifier, often used to refetch an object or as key + * for a cache. The ID type appears in a JSON response as a String; however, it is not intended to be + * human-readable. When expected as an input type, any string (such as `"4"`) or integer (such as `4`) + * input value will be accepted as an ID. + */ +public class GraphQLID { + public companion object { + public val type: CustomScalarType = CustomScalarType("ID", "kotlin.String") + } +} diff --git a/sentry-apollo-4/src/test/java/io/sentry/apollo4/type/GraphQLString.kt b/sentry-apollo-4/src/test/java/io/sentry/apollo4/type/GraphQLString.kt new file mode 100644 index 0000000000..b1a057ef3f --- /dev/null +++ b/sentry-apollo-4/src/test/java/io/sentry/apollo4/type/GraphQLString.kt @@ -0,0 +1,18 @@ +// +// AUTO-GENERATED FILE. DO NOT MODIFY. +// +// This class was automatically generated by Apollo GraphQL version '3.3.0'. +// +package io.sentry.apollo4.type + +import com.apollographql.apollo.api.CustomScalarType + +/** + * The `String` scalar type represents textual data, represented as UTF-8 character sequences. The + * String type is most often used by GraphQL to represent free-form human-readable text. + */ +public class GraphQLString { + public companion object { + public val type: CustomScalarType = CustomScalarType("String", "kotlin.String") + } +} diff --git a/sentry-apollo-4/src/test/java/io/sentry/apollo4/type/Launch.kt b/sentry-apollo-4/src/test/java/io/sentry/apollo4/type/Launch.kt new file mode 100644 index 0000000000..7290414098 --- /dev/null +++ b/sentry-apollo-4/src/test/java/io/sentry/apollo4/type/Launch.kt @@ -0,0 +1,14 @@ +// +// AUTO-GENERATED FILE. DO NOT MODIFY. +// +// This class was automatically generated by Apollo GraphQL version '3.3.0'. +// +package io.sentry.apollo4.type + +import com.apollographql.apollo.api.ObjectType + +public class Launch { + public companion object { + public val type: ObjectType = ObjectType.Builder(name = "Launch").build() + } +} diff --git a/sentry-apollo-4/src/test/java/io/sentry/apollo4/type/Mission.kt b/sentry-apollo-4/src/test/java/io/sentry/apollo4/type/Mission.kt new file mode 100644 index 0000000000..1432fb99b9 --- /dev/null +++ b/sentry-apollo-4/src/test/java/io/sentry/apollo4/type/Mission.kt @@ -0,0 +1,14 @@ +// +// AUTO-GENERATED FILE. DO NOT MODIFY. +// +// This class was automatically generated by Apollo GraphQL version '3.3.0'. +// +package io.sentry.apollo4.type + +import com.apollographql.apollo.api.ObjectType + +public class Mission { + public companion object { + public val type: ObjectType = ObjectType.Builder(name = "Mission").build() + } +} diff --git a/sentry-apollo-4/src/test/java/io/sentry/apollo4/type/Query.kt b/sentry-apollo-4/src/test/java/io/sentry/apollo4/type/Query.kt new file mode 100644 index 0000000000..4c15a31039 --- /dev/null +++ b/sentry-apollo-4/src/test/java/io/sentry/apollo4/type/Query.kt @@ -0,0 +1,14 @@ +// +// AUTO-GENERATED FILE. DO NOT MODIFY. +// +// This class was automatically generated by Apollo GraphQL version '3.3.0'. +// +package io.sentry.apollo4.type + +import com.apollographql.apollo.api.ObjectType + +public class Query { + public companion object { + public val type: ObjectType = ObjectType.Builder(name = "Query").build() + } +} diff --git a/sentry-apollo-4/src/test/java/io/sentry/apollo4/type/Rocket.kt b/sentry-apollo-4/src/test/java/io/sentry/apollo4/type/Rocket.kt new file mode 100644 index 0000000000..5153ff8cda --- /dev/null +++ b/sentry-apollo-4/src/test/java/io/sentry/apollo4/type/Rocket.kt @@ -0,0 +1,14 @@ +// +// AUTO-GENERATED FILE. DO NOT MODIFY. +// +// This class was automatically generated by Apollo GraphQL version '3.3.0'. +// +package io.sentry.apollo4.type + +import com.apollographql.apollo.api.ObjectType + +public class Rocket { + public companion object { + public val type: ObjectType = ObjectType.Builder(name = "Rocket").build() + } +} diff --git a/sentry-apollo-4/src/test/java/io/sentry/util/Apollo4PlatformTestManipulator.kt b/sentry-apollo-4/src/test/java/io/sentry/util/Apollo4PlatformTestManipulator.kt new file mode 100644 index 0000000000..f47438550e --- /dev/null +++ b/sentry-apollo-4/src/test/java/io/sentry/util/Apollo4PlatformTestManipulator.kt @@ -0,0 +1,8 @@ +package io.sentry.util + +object Apollo4PlatformTestManipulator { + + fun pretendIsAndroid(isAndroid: Boolean) { + Platform.isAndroid = isAndroid + } +} diff --git a/sentry-apollo-4/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/sentry-apollo-4/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker new file mode 100644 index 0000000000..1f0955d450 --- /dev/null +++ b/sentry-apollo-4/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker @@ -0,0 +1 @@ +mock-maker-inline diff --git a/sentry-compose/build.gradle.kts b/sentry-compose/build.gradle.kts index df8c4dae20..f699c7d71c 100644 --- a/sentry-compose/build.gradle.kts +++ b/sentry-compose/build.gradle.kts @@ -17,7 +17,7 @@ plugins { kotlin { explicitApi() - android { + androidTarget { publishLibraryVariants("release") compilations.all { kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8.toString() diff --git a/settings.gradle.kts b/settings.gradle.kts index d99f0f0e0a..022f77e827 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -25,6 +25,7 @@ include( "sentry-compose-helper", "sentry-apollo", "sentry-apollo-3", + "sentry-apollo-4", "sentry-test-support", "sentry-log4j2", "sentry-logback", From 18d3d8d6e7c40551cacfd5a5498925e6e596d150 Mon Sep 17 00:00:00 2001 From: lcian Date: Fri, 14 Feb 2025 10:45:48 +0100 Subject: [PATCH 2/8] rename some stuff, change and add a few tests --- sentry-apollo-4/api/sentry-apollo-4.api | 37 ++++++------ sentry-apollo-4/build.gradle.kts | 1 + .../java/io/sentry/apollo4/SentryApollo4.kt | 17 ++++++ ...s.kt => SentryApollo4BuilderExtensions.kt} | 0 .../apollo4/SentryApollo4ClientException.kt | 2 +- .../apollo4/SentryApollo4HttpInterceptor.kt | 27 ++++----- .../apollo4/SentryApollo4Interceptor.kt | 26 +++++--- ...ollo4BuilderExtensionsClientErrorsTest.kt} | 4 +- ... => SentryApollo4BuilderExtensionsTest.kt} | 60 ++++++++++++------- ...kt => SentryApollo4HttpInterceptorTest.kt} | 24 +------- .../{ => generated}/LaunchDetailsQuery.kt | 10 ++-- .../LaunchDetailsQuery_ResponseAdapter.kt | 4 +- .../LaunchDetailsQuery_VariablesAdapter.kt | 4 +- .../LaunchDetailsQuerySelections.kt | 20 ++++--- .../{ => generated}/type/GraphQLBoolean.kt | 2 +- .../apollo4/{ => generated}/type/GraphQLID.kt | 2 +- .../{ => generated}/type/GraphQLString.kt | 2 +- .../apollo4/{ => generated}/type/Launch.kt | 2 +- .../apollo4/{ => generated}/type/Mission.kt | 2 +- .../apollo4/{ => generated}/type/Query.kt | 2 +- .../apollo4/{ => generated}/type/Rocket.kt | 2 +- sentry-compose/build.gradle.kts | 2 +- 22 files changed, 140 insertions(+), 112 deletions(-) create mode 100644 sentry-apollo-4/src/main/java/io/sentry/apollo4/SentryApollo4.kt rename sentry-apollo-4/src/main/java/io/sentry/apollo4/{SentryApolloBuilderExtensions.kt => SentryApollo4BuilderExtensions.kt} (100%) rename sentry-apollo-4/src/test/java/io/sentry/apollo4/{SentryApollo4InterceptorClientErrors.kt => SentryApollo4BuilderExtensionsClientErrorsTest.kt} (98%) rename sentry-apollo-4/src/test/java/io/sentry/apollo4/{SentryApollo4InterceptorWithVariablesTest.kt => SentryApollo4BuilderExtensionsTest.kt} (82%) rename sentry-apollo-4/src/test/java/io/sentry/apollo4/{SentryApollo4InterceptorTest.kt => SentryApollo4HttpInterceptorTest.kt} (91%) rename sentry-apollo-4/src/test/java/io/sentry/apollo4/{ => generated}/LaunchDetailsQuery.kt (88%) rename sentry-apollo-4/src/test/java/io/sentry/apollo4/{ => generated}/adapter/LaunchDetailsQuery_ResponseAdapter.kt (98%) rename sentry-apollo-4/src/test/java/io/sentry/apollo4/{ => generated}/adapter/LaunchDetailsQuery_VariablesAdapter.kt (90%) rename sentry-apollo-4/src/test/java/io/sentry/apollo4/{ => generated}/selections/LaunchDetailsQuerySelections.kt (75%) rename sentry-apollo-4/src/test/java/io/sentry/apollo4/{ => generated}/type/GraphQLBoolean.kt (90%) rename sentry-apollo-4/src/test/java/io/sentry/apollo4/{ => generated}/type/GraphQLID.kt (94%) rename sentry-apollo-4/src/test/java/io/sentry/apollo4/{ => generated}/type/GraphQLString.kt (92%) rename sentry-apollo-4/src/test/java/io/sentry/apollo4/{ => generated}/type/Launch.kt (88%) rename sentry-apollo-4/src/test/java/io/sentry/apollo4/{ => generated}/type/Mission.kt (88%) rename sentry-apollo-4/src/test/java/io/sentry/apollo4/{ => generated}/type/Query.kt (88%) rename sentry-apollo-4/src/test/java/io/sentry/apollo4/{ => generated}/type/Rocket.kt (88%) diff --git a/sentry-apollo-4/api/sentry-apollo-4.api b/sentry-apollo-4/api/sentry-apollo-4.api index 0081d49c65..1704f6a918 100644 --- a/sentry-apollo-4/api/sentry-apollo-4.api +++ b/sentry-apollo-4/api/sentry-apollo-4.api @@ -3,6 +3,17 @@ public final class io/sentry/apollo4/BuildConfig { public static final field VERSION_NAME Ljava/lang/String; } +public final class io/sentry/apollo4/SentryApollo4BuilderExtensionsKt { + public static final fun sentryTracing (Lcom/apollographql/apollo/ApolloClient$Builder;)Lcom/apollographql/apollo/ApolloClient$Builder; + public static final fun sentryTracing (Lcom/apollographql/apollo/ApolloClient$Builder;Lio/sentry/IScopes;)Lcom/apollographql/apollo/ApolloClient$Builder; + public static final fun sentryTracing (Lcom/apollographql/apollo/ApolloClient$Builder;Lio/sentry/IScopes;Z)Lcom/apollographql/apollo/ApolloClient$Builder; + public static final fun sentryTracing (Lcom/apollographql/apollo/ApolloClient$Builder;Lio/sentry/IScopes;ZLjava/util/List;)Lcom/apollographql/apollo/ApolloClient$Builder; + public static final fun sentryTracing (Lcom/apollographql/apollo/ApolloClient$Builder;Lio/sentry/IScopes;ZLjava/util/List;Lio/sentry/apollo4/SentryApollo4HttpInterceptor$BeforeSpanCallback;)Lcom/apollographql/apollo/ApolloClient$Builder; + public static final fun sentryTracing (Lcom/apollographql/apollo/ApolloClient$Builder;ZLjava/util/List;Lio/sentry/apollo4/SentryApollo4HttpInterceptor$BeforeSpanCallback;)Lcom/apollographql/apollo/ApolloClient$Builder; + public static synthetic fun sentryTracing$default (Lcom/apollographql/apollo/ApolloClient$Builder;Lio/sentry/IScopes;ZLjava/util/List;Lio/sentry/apollo4/SentryApollo4HttpInterceptor$BeforeSpanCallback;ILjava/lang/Object;)Lcom/apollographql/apollo/ApolloClient$Builder; + public static synthetic fun sentryTracing$default (Lcom/apollographql/apollo/ApolloClient$Builder;ZLjava/util/List;Lio/sentry/apollo4/SentryApollo4HttpInterceptor$BeforeSpanCallback;ILjava/lang/Object;)Lcom/apollographql/apollo/ApolloClient$Builder; +} + public final class io/sentry/apollo4/SentryApollo4ClientException : java/lang/Exception { public static final field Companion Lio/sentry/apollo4/SentryApollo4ClientException$Companion; public fun (Ljava/lang/String;)V @@ -11,41 +22,29 @@ public final class io/sentry/apollo4/SentryApollo4ClientException : java/lang/Ex public final class io/sentry/apollo4/SentryApollo4ClientException$Companion { } -public final class io/sentry/apollo4/SentryApollo4HttpInterceptor : com/apollographql/apollo4/network/http/HttpInterceptor { +public final class io/sentry/apollo4/SentryApollo4HttpInterceptor : com/apollographql/apollo/network/http/HttpInterceptor { public static final field Companion Lio/sentry/apollo4/SentryApollo4HttpInterceptor$Companion; public static final field DEFAULT_CAPTURE_FAILED_REQUESTS Z - public static final field SENTRY_APOLLO_3_OPERATION_TYPE Ljava/lang/String; - public static final field SENTRY_APOLLO_3_VARIABLES Ljava/lang/String; public fun ()V public fun (Lio/sentry/IScopes;)V public fun (Lio/sentry/IScopes;Lio/sentry/apollo4/SentryApollo4HttpInterceptor$BeforeSpanCallback;)V public fun (Lio/sentry/IScopes;Lio/sentry/apollo4/SentryApollo4HttpInterceptor$BeforeSpanCallback;Z)V public fun (Lio/sentry/IScopes;Lio/sentry/apollo4/SentryApollo4HttpInterceptor$BeforeSpanCallback;ZLjava/util/List;)V public synthetic fun (Lio/sentry/IScopes;Lio/sentry/apollo4/SentryApollo4HttpInterceptor$BeforeSpanCallback;ZLjava/util/List;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public fun dispose ()V - public fun intercept (Lcom/apollographql/apollo4/api/http/HttpRequest;Lcom/apollographql/apollo4/network/http/HttpInterceptorChain;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun intercept (Lcom/apollographql/apollo/api/http/HttpRequest;Lcom/apollographql/apollo/network/http/HttpInterceptorChain;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; } public abstract interface class io/sentry/apollo4/SentryApollo4HttpInterceptor$BeforeSpanCallback { - public abstract fun execute (Lio/sentry/ISpan;Lcom/apollographql/apollo4/api/http/HttpRequest;Lcom/apollographql/apollo4/api/http/HttpResponse;)Lio/sentry/ISpan; + public abstract fun execute (Lio/sentry/ISpan;Lcom/apollographql/apollo/api/http/HttpRequest;Lcom/apollographql/apollo/api/http/HttpResponse;)Lio/sentry/ISpan; } public final class io/sentry/apollo4/SentryApollo4HttpInterceptor$Companion { } -public final class io/sentry/apollo4/SentryApollo4Interceptor : com/apollographql/apollo4/interceptor/ApolloInterceptor { +public final class io/sentry/apollo4/SentryApollo4Interceptor : com/apollographql/apollo/interceptor/ApolloInterceptor { public fun ()V - public fun intercept (Lcom/apollographql/apollo4/api/ApolloRequest;Lcom/apollographql/apollo4/interceptor/ApolloInterceptorChain;)Lkotlinx/coroutines/flow/Flow; -} - -public final class io/sentry/apollo4/SentryApolloBuilderExtensionsKt { - public static final fun sentryTracing (Lcom/apollographql/apollo4/ApolloClient$Builder;)Lcom/apollographql/apollo4/ApolloClient$Builder; - public static final fun sentryTracing (Lcom/apollographql/apollo4/ApolloClient$Builder;Lio/sentry/IScopes;)Lcom/apollographql/apollo4/ApolloClient$Builder; - public static final fun sentryTracing (Lcom/apollographql/apollo4/ApolloClient$Builder;Lio/sentry/IScopes;Z)Lcom/apollographql/apollo4/ApolloClient$Builder; - public static final fun sentryTracing (Lcom/apollographql/apollo4/ApolloClient$Builder;Lio/sentry/IScopes;ZLjava/util/List;)Lcom/apollographql/apollo4/ApolloClient$Builder; - public static final fun sentryTracing (Lcom/apollographql/apollo4/ApolloClient$Builder;Lio/sentry/IScopes;ZLjava/util/List;Lio/sentry/apollo4/SentryApollo4HttpInterceptor$BeforeSpanCallback;)Lcom/apollographql/apollo4/ApolloClient$Builder; - public static final fun sentryTracing (Lcom/apollographql/apollo4/ApolloClient$Builder;ZLjava/util/List;Lio/sentry/apollo4/SentryApollo4HttpInterceptor$BeforeSpanCallback;)Lcom/apollographql/apollo4/ApolloClient$Builder; - public static synthetic fun sentryTracing$default (Lcom/apollographql/apollo4/ApolloClient$Builder;Lio/sentry/IScopes;ZLjava/util/List;Lio/sentry/apollo4/SentryApollo4HttpInterceptor$BeforeSpanCallback;ILjava/lang/Object;)Lcom/apollographql/apollo4/ApolloClient$Builder; - public static synthetic fun sentryTracing$default (Lcom/apollographql/apollo4/ApolloClient$Builder;ZLjava/util/List;Lio/sentry/apollo4/SentryApollo4HttpInterceptor$BeforeSpanCallback;ILjava/lang/Object;)Lcom/apollographql/apollo4/ApolloClient$Builder; + public fun (Lio/sentry/IScopes;)V + public synthetic fun (Lio/sentry/IScopes;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun intercept (Lcom/apollographql/apollo/api/ApolloRequest;Lcom/apollographql/apollo/interceptor/ApolloInterceptorChain;)Lkotlinx/coroutines/flow/Flow; } diff --git a/sentry-apollo-4/build.gradle.kts b/sentry-apollo-4/build.gradle.kts index 95882248ac..aadbe4551c 100644 --- a/sentry-apollo-4/build.gradle.kts +++ b/sentry-apollo-4/build.gradle.kts @@ -41,6 +41,7 @@ dependencies { testImplementation(Config.TestLibs.mockitoInline) testImplementation(Config.TestLibs.mockWebserver) testImplementation(Config.Libs.apolloKotlin4) + testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3") } configure { diff --git a/sentry-apollo-4/src/main/java/io/sentry/apollo4/SentryApollo4.kt b/sentry-apollo-4/src/main/java/io/sentry/apollo4/SentryApollo4.kt new file mode 100644 index 0000000000..dd599c5e6e --- /dev/null +++ b/sentry-apollo-4/src/main/java/io/sentry/apollo4/SentryApollo4.kt @@ -0,0 +1,17 @@ +package io.sentry.apollo4 + +/** + * Common constants used across the module + */ +internal const val OPERATION_ID_HEADER_NAME = "SENTRY-APOLLO-4-OPERATION-ID" +internal const val OPERATION_NAME_HEADER_NAME = "SENTRY-APOLLO-4-OPERATION-NAME" +internal const val OPERATION_TYPE_HEADER_NAME = "SENTRY-APOLLO-4-OPERATION-TYPE" +internal const val VARIABLES_HEADER_NAME = "SENTRY-APOLLO-4-VARIABLES" +internal val INTERNAL_HEADER_NAMES by lazy { + listOf( + OPERATION_ID_HEADER_NAME, + OPERATION_NAME_HEADER_NAME, + OPERATION_TYPE_HEADER_NAME, + VARIABLES_HEADER_NAME + ) +} diff --git a/sentry-apollo-4/src/main/java/io/sentry/apollo4/SentryApolloBuilderExtensions.kt b/sentry-apollo-4/src/main/java/io/sentry/apollo4/SentryApollo4BuilderExtensions.kt similarity index 100% rename from sentry-apollo-4/src/main/java/io/sentry/apollo4/SentryApolloBuilderExtensions.kt rename to sentry-apollo-4/src/main/java/io/sentry/apollo4/SentryApollo4BuilderExtensions.kt diff --git a/sentry-apollo-4/src/main/java/io/sentry/apollo4/SentryApollo4ClientException.kt b/sentry-apollo-4/src/main/java/io/sentry/apollo4/SentryApollo4ClientException.kt index f363c61763..11f6440dc8 100644 --- a/sentry-apollo-4/src/main/java/io/sentry/apollo4/SentryApollo4ClientException.kt +++ b/sentry-apollo-4/src/main/java/io/sentry/apollo4/SentryApollo4ClientException.kt @@ -6,6 +6,6 @@ package io.sentry.apollo4 */ class SentryApollo4ClientException(message: String?) : Exception(message) { companion object { - private const val serialVersionUID = 4312120066430858144L + private const val serialVersionUID = 4312160066430858144L } } diff --git a/sentry-apollo-4/src/main/java/io/sentry/apollo4/SentryApollo4HttpInterceptor.kt b/sentry-apollo-4/src/main/java/io/sentry/apollo4/SentryApollo4HttpInterceptor.kt index 4c8f35ae51..ec510255a0 100644 --- a/sentry-apollo-4/src/main/java/io/sentry/apollo4/SentryApollo4HttpInterceptor.kt +++ b/sentry-apollo-4/src/main/java/io/sentry/apollo4/SentryApollo4HttpInterceptor.kt @@ -1,7 +1,5 @@ package io.sentry.apollo4 -import com.apollographql.apollo.api.http.DefaultHttpRequestComposer.Companion.HEADER_APOLLO_OPERATION_ID -import com.apollographql.apollo.api.http.DefaultHttpRequestComposer.Companion.HEADER_APOLLO_OPERATION_NAME import com.apollographql.apollo.api.http.HttpHeader import com.apollographql.apollo.api.http.HttpRequest import com.apollographql.apollo.api.http.HttpResponse @@ -68,9 +66,9 @@ class SentryApollo4HttpInterceptor @JvmOverloads constructor( ): HttpResponse { val activeSpan = if (Platform.isAndroid()) scopes.transaction else scopes.span - val operationName = getHeader("X-APOLLO-OPERATION-NAME", request.headers) - val operationType = decodeHeaderValue(request, SENTRY_APOLLO_4_OPERATION_TYPE) - val operationId = getHeader("X-APOLLO-OPERATION-ID", request.headers) + val operationId = decodeHeaderValue(request, OPERATION_ID_HEADER_NAME) + val operationName = decodeHeaderValue(request, OPERATION_NAME_HEADER_NAME) + val operationType = decodeHeaderValue(request, OPERATION_TYPE_HEADER_NAME) var span: ISpan? = null @@ -140,13 +138,12 @@ class SentryApollo4HttpInterceptor @JvmOverloads constructor( } private fun isIgnored(): Boolean { - return SpanUtils.isIgnored(scopes.getOptions().getIgnoredSpanOrigins(), TRACE_ORIGIN) + return SpanUtils.isIgnored(scopes.getOptions().ignoredSpanOrigins, TRACE_ORIGIN) } private fun removeSentryInternalHeaders(headers: List): List { - return headers.filterNot { - it.name.equals(SENTRY_APOLLO_4_VARIABLES, true) || - it.name.equals(SENTRY_APOLLO_4_OPERATION_TYPE, true) + return headers.filterNot { header -> + INTERNAL_HEADER_NAMES.any { internalHeader -> header.name.equals(internalHeader, true) } } } @@ -161,7 +158,7 @@ class SentryApollo4HttpInterceptor @JvmOverloads constructor( val method = request.method.name val operation = if (operationType != null) "http.graphql.$operationType" else "http.graphql" - val variables = decodeHeaderValue(request, SENTRY_APOLLO_4_VARIABLES) + val variables = decodeHeaderValue(request, VARIABLES_HEADER_NAME) val description = "${operationType ?: method} ${operationName ?: urlDetails.urlOrFallback}" @@ -177,7 +174,7 @@ class SentryApollo4HttpInterceptor @JvmOverloads constructor( variables?.let { setData("variables", it) } - setData(HTTP_METHOD_KEY, method.toUpperCase(Locale.ROOT)) + setData(HTTP_METHOD_KEY, method.uppercase(Locale.ROOT)) } } @@ -227,7 +224,7 @@ class SentryApollo4HttpInterceptor @JvmOverloads constructor( } catch (e: Throwable) { scopes.options.logger.log( SentryLevel.ERROR, - "An error occurred while executing beforeSpan on ApolloInterceptor", + "An error occurred while executing beforeSpan in ApolloInterceptor", e ) } @@ -327,7 +324,7 @@ class SentryApollo4HttpInterceptor @JvmOverloads constructor( return } - // if there response body does not have the errors field, do not raise an issue + // if the response body does not have the errors field, do not raise an issue if (body.isEmpty() || !regex.containsMatchIn(body)) { return } @@ -340,7 +337,7 @@ class SentryApollo4HttpInterceptor @JvmOverloads constructor( // but that's not possible val urlDetails = UrlUtils.parse(request.url) - // return if its not a target match + // return if it's not a target match if (!PropagationTargetsUtils.contain(failedRequestTargets, urlDetails.urlOrFallback)) { return } @@ -451,8 +448,6 @@ class SentryApollo4HttpInterceptor @JvmOverloads constructor( } companion object { - const val SENTRY_APOLLO_4_VARIABLES = "SENTRY-APOLLO-4-VARIABLES" - const val SENTRY_APOLLO_4_OPERATION_TYPE = "SENTRY-APOLLO-4-OPERATION-TYPE" const val DEFAULT_CAPTURE_FAILED_REQUESTS = true } } diff --git a/sentry-apollo-4/src/main/java/io/sentry/apollo4/SentryApollo4Interceptor.kt b/sentry-apollo-4/src/main/java/io/sentry/apollo4/SentryApollo4Interceptor.kt index a8c5270fcd..5a57eccefc 100644 --- a/sentry-apollo-4/src/main/java/io/sentry/apollo4/SentryApollo4Interceptor.kt +++ b/sentry-apollo-4/src/main/java/io/sentry/apollo4/SentryApollo4Interceptor.kt @@ -10,29 +10,41 @@ import com.apollographql.apollo.api.Subscription import com.apollographql.apollo.api.variables import com.apollographql.apollo.interceptor.ApolloInterceptor import com.apollographql.apollo.interceptor.ApolloInterceptorChain -import io.sentry.apollo4.SentryApollo4HttpInterceptor.Companion.SENTRY_APOLLO_4_OPERATION_TYPE -import io.sentry.apollo4.SentryApollo4HttpInterceptor.Companion.SENTRY_APOLLO_4_VARIABLES +import io.sentry.IScopes +import io.sentry.ScopesAdapter import io.sentry.vendor.Base64 import kotlinx.coroutines.flow.Flow +import org.jetbrains.annotations.ApiStatus -class SentryApollo4Interceptor : ApolloInterceptor { +/** + * Interceptor that adds the GraphQL request information to the outgoing HTTP request's headers so that + * the information can be accessed by {@link SentryApollo4HttpInterceptor} + */ +class SentryApollo4Interceptor @JvmOverloads constructor( + @ApiStatus.Internal private val scopes: IScopes = ScopesAdapter.getInstance() +) : ApolloInterceptor { override fun intercept( request: ApolloRequest, chain: ApolloInterceptorChain ): Flow> { val builder = request.newBuilder() - .addHttpHeader(SENTRY_APOLLO_4_OPERATION_TYPE, Base64.encodeToString(operationType(request).toByteArray(), Base64.NO_WRAP)) + .addHttpHeader(OPERATION_ID_HEADER_NAME, encodeHeaderValue(request.operation.id())) + .addHttpHeader(OPERATION_NAME_HEADER_NAME, encodeHeaderValue(request.operation.name())) + .addHttpHeader(OPERATION_TYPE_HEADER_NAME, encodeHeaderValue(operationType(request))) request.scalarAdapters?.let { - builder.addHttpHeader(SENTRY_APOLLO_4_VARIABLES, Base64.encodeToString(request.operation.variables(it).valueMap.toString().toByteArray(), Base64.NO_WRAP)) + builder.addHttpHeader(VARIABLES_HEADER_NAME, encodeHeaderValue(request.operation.variables(it).valueMap.toString())) } - builder.addHttpHeader("X-APOLLO-OPERATION-NAME", request.operation.name()) - builder.addHttpHeader("X-APOLLO-OPERATION-ID", request.operation.id()) + return chain.proceed(builder.build()) } } +private fun encodeHeaderValue(value: String): String { + return Base64.encodeToString(value.toByteArray(), Base64.NO_WRAP) +} + private fun operationType(apolloRequest: ApolloRequest) = when (apolloRequest.operation) { is Query -> "query" is Mutation -> "mutation" diff --git a/sentry-apollo-4/src/test/java/io/sentry/apollo4/SentryApollo4InterceptorClientErrors.kt b/sentry-apollo-4/src/test/java/io/sentry/apollo4/SentryApollo4BuilderExtensionsClientErrorsTest.kt similarity index 98% rename from sentry-apollo-4/src/test/java/io/sentry/apollo4/SentryApollo4InterceptorClientErrors.kt rename to sentry-apollo-4/src/test/java/io/sentry/apollo4/SentryApollo4BuilderExtensionsClientErrorsTest.kt index 64e9e885a8..b84872cf1d 100644 --- a/sentry-apollo-4/src/test/java/io/sentry/apollo4/SentryApollo4InterceptorClientErrors.kt +++ b/sentry-apollo-4/src/test/java/io/sentry/apollo4/SentryApollo4BuilderExtensionsClientErrorsTest.kt @@ -11,6 +11,7 @@ import io.sentry.SentryOptions import io.sentry.SentryOptions.DEFAULT_PROPAGATION_TARGETS import io.sentry.TypeCheckHint import io.sentry.apollo4.SentryApollo4HttpInterceptor.Companion.DEFAULT_CAPTURE_FAILED_REQUESTS +import io.sentry.apollo4.generated.LaunchDetailsQuery import io.sentry.exception.ExceptionMechanismException import io.sentry.protocol.SdkVersion import io.sentry.protocol.SentryId @@ -32,7 +33,7 @@ import kotlin.test.assertNotNull import kotlin.test.assertNull import kotlin.test.assertTrue -class SentryApollo4InterceptorClientErrors { +class SentryApollo4BuilderExtensionsClientErrorsTest { class Fixture { val server = MockWebServer() lateinit var scopes: IScopes @@ -268,7 +269,6 @@ class SentryApollo4InterceptorClientErrors { assertEquals("Test", request.cookies) assertNotNull(request.headers) - assertEquals("LaunchDetails", request.headers?.get("X-APOLLO-OPERATION-NAME")) }, any() ) diff --git a/sentry-apollo-4/src/test/java/io/sentry/apollo4/SentryApollo4InterceptorWithVariablesTest.kt b/sentry-apollo-4/src/test/java/io/sentry/apollo4/SentryApollo4BuilderExtensionsTest.kt similarity index 82% rename from sentry-apollo-4/src/test/java/io/sentry/apollo4/SentryApollo4InterceptorWithVariablesTest.kt rename to sentry-apollo-4/src/test/java/io/sentry/apollo4/SentryApollo4BuilderExtensionsTest.kt index c36d140972..1cc5cc8466 100644 --- a/sentry-apollo-4/src/test/java/io/sentry/apollo4/SentryApollo4InterceptorWithVariablesTest.kt +++ b/sentry-apollo-4/src/test/java/io/sentry/apollo4/SentryApollo4BuilderExtensionsTest.kt @@ -12,6 +12,7 @@ import io.sentry.TraceContext import io.sentry.TracesSamplingDecision import io.sentry.TransactionContext import io.sentry.apollo4.SentryApollo4HttpInterceptor.BeforeSpanCallback +import io.sentry.apollo4.generated.LaunchDetailsQuery import io.sentry.mockServerRequestTimeoutMillis import io.sentry.protocol.SentryTransaction import kotlinx.coroutines.launch @@ -28,9 +29,9 @@ import java.util.concurrent.TimeUnit import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertNotNull -import kotlin.test.assertNull +import kotlin.test.assertTrue -class SentryApollo4InterceptorWithVariablesTest { +class SentryApollo4BuilderExtensionsTest { class Fixture { val server = MockWebServer() @@ -78,7 +79,7 @@ class SentryApollo4InterceptorWithVariablesTest { private val fixture = Fixture() @Test - fun `creates a span around the successful request`() { + fun `creates span around successful request`() { executeQuery() verify(fixture.scopes).captureTransaction( @@ -93,7 +94,7 @@ class SentryApollo4InterceptorWithVariablesTest { } @Test - fun `creates a span around the failed request`() { + fun `creates span around failed request`() { executeQuery(fixture.getSut(httpStatusCode = 403)) verify(fixture.scopes).captureTransaction( @@ -108,7 +109,7 @@ class SentryApollo4InterceptorWithVariablesTest { } @Test - fun `creates a span around the request failing with network error`() { + fun `creates span around request failing with network error`() { executeQuery(fixture.getSut(socketPolicy = SocketPolicy.DISCONNECT_DURING_REQUEST_BODY)) verify(fixture.scopes).captureTransaction( @@ -123,28 +124,29 @@ class SentryApollo4InterceptorWithVariablesTest { } @Test - fun `handles non-ascii header values correctly`() { - executeQuery(id = "á") + fun `adds breadcrumb when http call succeeds`() { + executeQuery(fixture.getSut()) - verify(fixture.scopes).captureTransaction( - check { - assertTransactionDetails(it) - assertEquals(SpanStatus.OK, it.spans.first().status) + verify(fixture.scopes).addBreadcrumb( + check { + assertEquals("http", it.type) + assertEquals(200, it.data["status_code"]) + // response_body_size is added but mock webserver returns 0 always + assertEquals(0L, it.data["response_body_size"]) + assertEquals(193L, it.data["request_body_size"]) + assertEquals("query", it.data["operation_type"]) }, - anyOrNull(), - anyOrNull(), anyOrNull() ) } @Test - fun `adds breadcrumb when http calls succeeds`() { - executeQuery(fixture.getSut()) + fun `adds breadcrumb when http call fails`() { + executeQuery(fixture.getSut(socketPolicy = SocketPolicy.DISCONNECT_DURING_REQUEST_BODY)) + verify(fixture.scopes).addBreadcrumb( check { assertEquals("http", it.type) - // response_body_size is added but mock webserver returns 0 always - assertEquals(0L, it.data["response_body_size"]) assertEquals(193L, it.data["request_body_size"]) assertEquals("query", it.data["operation_type"]) }, @@ -153,11 +155,27 @@ class SentryApollo4InterceptorWithVariablesTest { } @Test - fun `internal headers are not sent over the wire`() { + fun `handles non-ascii header values correctly`() { + executeQuery(id = "á") + + verify(fixture.scopes).captureTransaction( + check { + assertTransactionDetails(it) + assertEquals(SpanStatus.OK, it.spans.first().status) + }, + anyOrNull(), + anyOrNull(), + anyOrNull() + ) + } + + @Test + fun `does not send internal headers over the wire`() { executeQuery(fixture.getSut()) - val recorderRequest = fixture.server.takeRequest(mockServerRequestTimeoutMillis, TimeUnit.MILLISECONDS)!! - assertNull(recorderRequest.headers[SentryApollo4HttpInterceptor.SENTRY_APOLLO_4_VARIABLES]) - assertNull(recorderRequest.headers[SentryApollo4HttpInterceptor.SENTRY_APOLLO_4_OPERATION_TYPE]) + val recordedRequest = fixture.server.takeRequest(mockServerRequestTimeoutMillis, TimeUnit.MILLISECONDS)!! + for (sentryHeader in INTERNAL_HEADER_NAMES) { + assertTrue(recordedRequest.headers.none { header -> header.first.equals(sentryHeader, true) }) + } } private fun assertTransactionDetails(it: SentryTransaction) { diff --git a/sentry-apollo-4/src/test/java/io/sentry/apollo4/SentryApollo4InterceptorTest.kt b/sentry-apollo-4/src/test/java/io/sentry/apollo4/SentryApollo4HttpInterceptorTest.kt similarity index 91% rename from sentry-apollo-4/src/test/java/io/sentry/apollo4/SentryApollo4InterceptorTest.kt rename to sentry-apollo-4/src/test/java/io/sentry/apollo4/SentryApollo4HttpInterceptorTest.kt index f7232a65fc..4d1fe46590 100644 --- a/sentry-apollo-4/src/test/java/io/sentry/apollo4/SentryApollo4InterceptorTest.kt +++ b/sentry-apollo-4/src/test/java/io/sentry/apollo4/SentryApollo4HttpInterceptorTest.kt @@ -1,15 +1,10 @@ package io.sentry.apollo4 import com.apollographql.apollo.ApolloClient -import com.apollographql.apollo.api.ApolloRequest -import com.apollographql.apollo.api.ApolloResponse -import com.apollographql.apollo.api.Operation import com.apollographql.apollo.api.http.HttpRequest import com.apollographql.apollo.api.http.HttpResponse import com.apollographql.apollo.exception.ApolloException import com.apollographql.apollo.exception.ApolloHttpException -import com.apollographql.apollo.interceptor.ApolloInterceptor -import com.apollographql.apollo.interceptor.ApolloInterceptorChain import com.apollographql.apollo.network.http.HttpInterceptor import com.apollographql.apollo.network.http.HttpInterceptorChain import io.sentry.BaggageHeader @@ -29,11 +24,11 @@ import io.sentry.TraceContext import io.sentry.TracesSamplingDecision import io.sentry.TransactionContext import io.sentry.apollo4.SentryApollo4HttpInterceptor.BeforeSpanCallback +import io.sentry.apollo4.generated.LaunchDetailsQuery import io.sentry.mockServerRequestTimeoutMillis import io.sentry.protocol.SdkVersion import io.sentry.protocol.SentryTransaction import io.sentry.util.Apollo4PlatformTestManipulator -import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import okhttp3.mockwebserver.MockResponse @@ -54,7 +49,7 @@ import kotlin.test.assertNotNull import kotlin.test.assertNull import kotlin.test.assertTrue -class SentryApollo4InterceptorTest { +class SentryApollo4HttpInterceptorTest { class Fixture { val server = MockWebServer() @@ -107,16 +102,6 @@ class SentryApollo4InterceptorTest { val builder = ApolloClient.Builder() .serverUrl(server.url("/").toString()) .addHttpInterceptor(httpInterceptor) - .addInterceptor(object : ApolloInterceptor { // Re-enable legacy headers - override fun intercept(request: ApolloRequest, chain: ApolloInterceptorChain): Flow> { - return chain.proceed( - request.newBuilder() - .addHttpHeader("X-APOLLO-OPERATION-NAME", request.operation.name()) - .addHttpHeader("X-APOLLO-OPERATION-ID", request.operation.id()) - .build() - ) - } - }) interceptor?.let { builder.addHttpInterceptor(interceptor) @@ -328,8 +313,6 @@ class SentryApollo4InterceptorTest { // response_body_size is added but mock webserver returns 0 always assertEquals(0L, it.data["response_body_size"]) assertEquals(193L, it.data["request_body_size"]) - assertEquals("LaunchDetails", it.data["operation_name"]) - assertNotNull(it.data["operation_id"]) }, anyOrNull() ) @@ -362,9 +345,8 @@ class SentryApollo4InterceptorTest { assertEquals(1, it.spans.size) val httpClientSpan = it.spans.first() assertEquals("http.graphql", httpClientSpan.op) - assertTrue { httpClientSpan.description?.startsWith("Post LaunchDetails") == true } + assertEquals("Post http://${fixture.server.hostName}:${fixture.server.port}/", httpClientSpan.description) assertNotNull(httpClientSpan.data) { - assertNotNull(it["operationId"]) assertEquals("POST", it[HTTP_METHOD_KEY]) httpStatusCode?.let { code -> assertEquals(code, it[SpanDataConvention.HTTP_STATUS_CODE_KEY]) diff --git a/sentry-apollo-4/src/test/java/io/sentry/apollo4/LaunchDetailsQuery.kt b/sentry-apollo-4/src/test/java/io/sentry/apollo4/generated/LaunchDetailsQuery.kt similarity index 88% rename from sentry-apollo-4/src/test/java/io/sentry/apollo4/LaunchDetailsQuery.kt rename to sentry-apollo-4/src/test/java/io/sentry/apollo4/generated/LaunchDetailsQuery.kt index 136cf21848..5fe0b05021 100644 --- a/sentry-apollo-4/src/test/java/io/sentry/apollo4/LaunchDetailsQuery.kt +++ b/sentry-apollo-4/src/test/java/io/sentry/apollo4/generated/LaunchDetailsQuery.kt @@ -1,4 +1,4 @@ -package io.sentry.apollo4 +package io.sentry.apollo4.generated import com.apollographql.apollo.api.Adapter import com.apollographql.apollo.api.CompiledField @@ -6,9 +6,9 @@ import com.apollographql.apollo.api.CustomScalarAdapters import com.apollographql.apollo.api.Query import com.apollographql.apollo.api.json.JsonWriter import com.apollographql.apollo.api.obj -import io.sentry.apollo4.adapter.LaunchDetailsQuery_ResponseAdapter -import io.sentry.apollo4.adapter.LaunchDetailsQuery_VariablesAdapter -import io.sentry.apollo4.selections.LaunchDetailsQuerySelections +import io.sentry.apollo4.generated.adapter.LaunchDetailsQuery_ResponseAdapter +import io.sentry.apollo4.generated.adapter.LaunchDetailsQuery_VariablesAdapter +import io.sentry.apollo4.generated.selections.LaunchDetailsQuerySelections import kotlin.String public data class LaunchDetailsQuery( @@ -32,7 +32,7 @@ public data class LaunchDetailsQuery( public override fun rootField(): CompiledField = CompiledField.Builder( name = "data", - type = io.sentry.apollo4.type.Query.type + type = io.sentry.apollo4.generated.type.Query.type ) .selections(selections = LaunchDetailsQuerySelections.root) .build() diff --git a/sentry-apollo-4/src/test/java/io/sentry/apollo4/adapter/LaunchDetailsQuery_ResponseAdapter.kt b/sentry-apollo-4/src/test/java/io/sentry/apollo4/generated/adapter/LaunchDetailsQuery_ResponseAdapter.kt similarity index 98% rename from sentry-apollo-4/src/test/java/io/sentry/apollo4/adapter/LaunchDetailsQuery_ResponseAdapter.kt rename to sentry-apollo-4/src/test/java/io/sentry/apollo4/generated/adapter/LaunchDetailsQuery_ResponseAdapter.kt index e4f87980ec..8926f7d4f1 100644 --- a/sentry-apollo-4/src/test/java/io/sentry/apollo4/adapter/LaunchDetailsQuery_ResponseAdapter.kt +++ b/sentry-apollo-4/src/test/java/io/sentry/apollo4/generated/adapter/LaunchDetailsQuery_ResponseAdapter.kt @@ -3,7 +3,7 @@ // // This class was automatically generated by Apollo GraphQL version '3.3.0'. // -package io.sentry.apollo4.adapter +package io.sentry.apollo4.generated.adapter import com.apollographql.apollo.api.Adapter import com.apollographql.apollo.api.CustomScalarAdapters @@ -13,7 +13,7 @@ import com.apollographql.apollo.api.json.JsonReader import com.apollographql.apollo.api.json.JsonWriter import com.apollographql.apollo.api.nullable import com.apollographql.apollo.api.obj -import io.sentry.apollo4.LaunchDetailsQuery +import io.sentry.apollo4.generated.LaunchDetailsQuery import kotlin.String import kotlin.collections.List diff --git a/sentry-apollo-4/src/test/java/io/sentry/apollo4/adapter/LaunchDetailsQuery_VariablesAdapter.kt b/sentry-apollo-4/src/test/java/io/sentry/apollo4/generated/adapter/LaunchDetailsQuery_VariablesAdapter.kt similarity index 90% rename from sentry-apollo-4/src/test/java/io/sentry/apollo4/adapter/LaunchDetailsQuery_VariablesAdapter.kt rename to sentry-apollo-4/src/test/java/io/sentry/apollo4/generated/adapter/LaunchDetailsQuery_VariablesAdapter.kt index b0d6f498e0..8e2e96c92a 100644 --- a/sentry-apollo-4/src/test/java/io/sentry/apollo4/adapter/LaunchDetailsQuery_VariablesAdapter.kt +++ b/sentry-apollo-4/src/test/java/io/sentry/apollo4/generated/adapter/LaunchDetailsQuery_VariablesAdapter.kt @@ -3,14 +3,14 @@ // // This class was automatically generated by Apollo GraphQL version '3.3.0'. // -package io.sentry.apollo4.adapter +package io.sentry.apollo4.generated.adapter import com.apollographql.apollo.api.Adapter import com.apollographql.apollo.api.CustomScalarAdapters import com.apollographql.apollo.api.StringAdapter import com.apollographql.apollo.api.json.JsonReader import com.apollographql.apollo.api.json.JsonWriter -import io.sentry.apollo4.LaunchDetailsQuery +import io.sentry.apollo4.generated.LaunchDetailsQuery object LaunchDetailsQuery_VariablesAdapter : Adapter { override fun fromJson(reader: JsonReader, customScalarAdapters: CustomScalarAdapters): diff --git a/sentry-apollo-4/src/test/java/io/sentry/apollo4/selections/LaunchDetailsQuerySelections.kt b/sentry-apollo-4/src/test/java/io/sentry/apollo4/generated/selections/LaunchDetailsQuerySelections.kt similarity index 75% rename from sentry-apollo-4/src/test/java/io/sentry/apollo4/selections/LaunchDetailsQuerySelections.kt rename to sentry-apollo-4/src/test/java/io/sentry/apollo4/generated/selections/LaunchDetailsQuerySelections.kt index 522f2b18e2..de8836c330 100644 --- a/sentry-apollo-4/src/test/java/io/sentry/apollo4/selections/LaunchDetailsQuerySelections.kt +++ b/sentry-apollo-4/src/test/java/io/sentry/apollo4/generated/selections/LaunchDetailsQuerySelections.kt @@ -3,15 +3,19 @@ // // This class was automatically generated by Apollo GraphQL version '3.3.0'. // -package io.sentry.apollo4.selections +package io.sentry.apollo4.generated.selections -import com.apollographql.apollo.api.* -import io.sentry.apollo4.type.GraphQLID -import io.sentry.apollo4.type.GraphQLString -import io.sentry.apollo4.type.Launch -import io.sentry.apollo4.type.Mission -import io.sentry.apollo4.type.Query.Companion.type -import io.sentry.apollo4.type.Rocket +import com.apollographql.apollo.api.CompiledArgument +import com.apollographql.apollo.api.CompiledArgumentDefinition +import com.apollographql.apollo.api.CompiledField +import com.apollographql.apollo.api.CompiledSelection +import com.apollographql.apollo.api.notNull +import io.sentry.apollo4.generated.type.GraphQLID +import io.sentry.apollo4.generated.type.GraphQLString +import io.sentry.apollo4.generated.type.Launch +import io.sentry.apollo4.generated.type.Mission +import io.sentry.apollo4.generated.type.Query.Companion.type +import io.sentry.apollo4.generated.type.Rocket import kotlin.collections.List public object LaunchDetailsQuerySelections { diff --git a/sentry-apollo-4/src/test/java/io/sentry/apollo4/type/GraphQLBoolean.kt b/sentry-apollo-4/src/test/java/io/sentry/apollo4/generated/type/GraphQLBoolean.kt similarity index 90% rename from sentry-apollo-4/src/test/java/io/sentry/apollo4/type/GraphQLBoolean.kt rename to sentry-apollo-4/src/test/java/io/sentry/apollo4/generated/type/GraphQLBoolean.kt index 26b23bf04f..939d391e3f 100644 --- a/sentry-apollo-4/src/test/java/io/sentry/apollo4/type/GraphQLBoolean.kt +++ b/sentry-apollo-4/src/test/java/io/sentry/apollo4/generated/type/GraphQLBoolean.kt @@ -3,7 +3,7 @@ // // This class was automatically generated by Apollo GraphQL version '3.3.0'. // -package io.sentry.apollo4.type +package io.sentry.apollo4.generated.type import com.apollographql.apollo.api.CustomScalarType diff --git a/sentry-apollo-4/src/test/java/io/sentry/apollo4/type/GraphQLID.kt b/sentry-apollo-4/src/test/java/io/sentry/apollo4/generated/type/GraphQLID.kt similarity index 94% rename from sentry-apollo-4/src/test/java/io/sentry/apollo4/type/GraphQLID.kt rename to sentry-apollo-4/src/test/java/io/sentry/apollo4/generated/type/GraphQLID.kt index 89a617047d..4aea4184a1 100644 --- a/sentry-apollo-4/src/test/java/io/sentry/apollo4/type/GraphQLID.kt +++ b/sentry-apollo-4/src/test/java/io/sentry/apollo4/generated/type/GraphQLID.kt @@ -3,7 +3,7 @@ // // This class was automatically generated by Apollo GraphQL version '3.3.0'. // -package io.sentry.apollo4.type +package io.sentry.apollo4.generated.type import com.apollographql.apollo.api.CustomScalarType diff --git a/sentry-apollo-4/src/test/java/io/sentry/apollo4/type/GraphQLString.kt b/sentry-apollo-4/src/test/java/io/sentry/apollo4/generated/type/GraphQLString.kt similarity index 92% rename from sentry-apollo-4/src/test/java/io/sentry/apollo4/type/GraphQLString.kt rename to sentry-apollo-4/src/test/java/io/sentry/apollo4/generated/type/GraphQLString.kt index b1a057ef3f..96394bfe4d 100644 --- a/sentry-apollo-4/src/test/java/io/sentry/apollo4/type/GraphQLString.kt +++ b/sentry-apollo-4/src/test/java/io/sentry/apollo4/generated/type/GraphQLString.kt @@ -3,7 +3,7 @@ // // This class was automatically generated by Apollo GraphQL version '3.3.0'. // -package io.sentry.apollo4.type +package io.sentry.apollo4.generated.type import com.apollographql.apollo.api.CustomScalarType diff --git a/sentry-apollo-4/src/test/java/io/sentry/apollo4/type/Launch.kt b/sentry-apollo-4/src/test/java/io/sentry/apollo4/generated/type/Launch.kt similarity index 88% rename from sentry-apollo-4/src/test/java/io/sentry/apollo4/type/Launch.kt rename to sentry-apollo-4/src/test/java/io/sentry/apollo4/generated/type/Launch.kt index 7290414098..066c5a323d 100644 --- a/sentry-apollo-4/src/test/java/io/sentry/apollo4/type/Launch.kt +++ b/sentry-apollo-4/src/test/java/io/sentry/apollo4/generated/type/Launch.kt @@ -3,7 +3,7 @@ // // This class was automatically generated by Apollo GraphQL version '3.3.0'. // -package io.sentry.apollo4.type +package io.sentry.apollo4.generated.type import com.apollographql.apollo.api.ObjectType diff --git a/sentry-apollo-4/src/test/java/io/sentry/apollo4/type/Mission.kt b/sentry-apollo-4/src/test/java/io/sentry/apollo4/generated/type/Mission.kt similarity index 88% rename from sentry-apollo-4/src/test/java/io/sentry/apollo4/type/Mission.kt rename to sentry-apollo-4/src/test/java/io/sentry/apollo4/generated/type/Mission.kt index 1432fb99b9..070fa9258f 100644 --- a/sentry-apollo-4/src/test/java/io/sentry/apollo4/type/Mission.kt +++ b/sentry-apollo-4/src/test/java/io/sentry/apollo4/generated/type/Mission.kt @@ -3,7 +3,7 @@ // // This class was automatically generated by Apollo GraphQL version '3.3.0'. // -package io.sentry.apollo4.type +package io.sentry.apollo4.generated.type import com.apollographql.apollo.api.ObjectType diff --git a/sentry-apollo-4/src/test/java/io/sentry/apollo4/type/Query.kt b/sentry-apollo-4/src/test/java/io/sentry/apollo4/generated/type/Query.kt similarity index 88% rename from sentry-apollo-4/src/test/java/io/sentry/apollo4/type/Query.kt rename to sentry-apollo-4/src/test/java/io/sentry/apollo4/generated/type/Query.kt index 4c15a31039..ca72e33147 100644 --- a/sentry-apollo-4/src/test/java/io/sentry/apollo4/type/Query.kt +++ b/sentry-apollo-4/src/test/java/io/sentry/apollo4/generated/type/Query.kt @@ -3,7 +3,7 @@ // // This class was automatically generated by Apollo GraphQL version '3.3.0'. // -package io.sentry.apollo4.type +package io.sentry.apollo4.generated.type import com.apollographql.apollo.api.ObjectType diff --git a/sentry-apollo-4/src/test/java/io/sentry/apollo4/type/Rocket.kt b/sentry-apollo-4/src/test/java/io/sentry/apollo4/generated/type/Rocket.kt similarity index 88% rename from sentry-apollo-4/src/test/java/io/sentry/apollo4/type/Rocket.kt rename to sentry-apollo-4/src/test/java/io/sentry/apollo4/generated/type/Rocket.kt index 5153ff8cda..3d43df676f 100644 --- a/sentry-apollo-4/src/test/java/io/sentry/apollo4/type/Rocket.kt +++ b/sentry-apollo-4/src/test/java/io/sentry/apollo4/generated/type/Rocket.kt @@ -3,7 +3,7 @@ // // This class was automatically generated by Apollo GraphQL version '3.3.0'. // -package io.sentry.apollo4.type +package io.sentry.apollo4.generated.type import com.apollographql.apollo.api.ObjectType diff --git a/sentry-compose/build.gradle.kts b/sentry-compose/build.gradle.kts index f699c7d71c..df8c4dae20 100644 --- a/sentry-compose/build.gradle.kts +++ b/sentry-compose/build.gradle.kts @@ -17,7 +17,7 @@ plugins { kotlin { explicitApi() - androidTarget { + android { publishLibraryVariants("release") compilations.all { kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8.toString() From 47fbe9ec2797dec30212d1c938451c8ce065818f Mon Sep 17 00:00:00 2001 From: lcian Date: Fri, 14 Feb 2025 12:00:35 +0100 Subject: [PATCH 3/8] parametrize tests to run with both v3 and v4 implementations of `ApolloCall::execute` --- sentry-apollo-4/build.gradle.kts | 1 + ...entryApollo4BuilderExtensionsClientErrorsTest.kt | 13 +++++++++++-- .../apollo4/SentryApollo4BuilderExtensionsTest.kt | 13 +++++++++++-- .../apollo4/SentryApollo4HttpInterceptorTest.kt | 13 +++++++++++-- 4 files changed, 34 insertions(+), 6 deletions(-) diff --git a/sentry-apollo-4/build.gradle.kts b/sentry-apollo-4/build.gradle.kts index aadbe4551c..6e8c292966 100644 --- a/sentry-apollo-4/build.gradle.kts +++ b/sentry-apollo-4/build.gradle.kts @@ -42,6 +42,7 @@ dependencies { testImplementation(Config.TestLibs.mockWebserver) testImplementation(Config.Libs.apolloKotlin4) testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3") + testImplementation("org.jetbrains.kotlin:kotlin-reflect:2.0.0") } configure { diff --git a/sentry-apollo-4/src/test/java/io/sentry/apollo4/SentryApollo4BuilderExtensionsClientErrorsTest.kt b/sentry-apollo-4/src/test/java/io/sentry/apollo4/SentryApollo4BuilderExtensionsClientErrorsTest.kt index b84872cf1d..d7df80cb03 100644 --- a/sentry-apollo-4/src/test/java/io/sentry/apollo4/SentryApollo4BuilderExtensionsClientErrorsTest.kt +++ b/sentry-apollo-4/src/test/java/io/sentry/apollo4/SentryApollo4BuilderExtensionsClientErrorsTest.kt @@ -1,6 +1,9 @@ package io.sentry.apollo4 +import com.apollographql.apollo.ApolloCall import com.apollographql.apollo.ApolloClient +import com.apollographql.apollo.api.ApolloResponse +import com.apollographql.apollo.api.Operation import com.apollographql.apollo.api.http.HttpRequest import com.apollographql.apollo.api.http.HttpResponse import com.apollographql.apollo.exception.ApolloException @@ -26,6 +29,7 @@ import org.mockito.kotlin.mock import org.mockito.kotlin.never import org.mockito.kotlin.verify import org.mockito.kotlin.whenever +import kotlin.reflect.KSuspendFunction1 import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFalse @@ -33,7 +37,12 @@ import kotlin.test.assertNotNull import kotlin.test.assertNull import kotlin.test.assertTrue -class SentryApollo4BuilderExtensionsClientErrorsTest { +class SentryApollo4BuilderExtensionsClientErrorsTestWithV4Implementation : SentryApollo4BuilderExtensionsClientErrorsTest(ApolloCall<*>::execute) +class SentryApollo4BuilderExtensionsClientErrorsTestWithV3Implementation : SentryApollo4BuilderExtensionsClientErrorsTest(ApolloCall<*>::executeV3) + +abstract class SentryApollo4BuilderExtensionsClientErrorsTest( + private val executeQueryImplementation: KSuspendFunction1, ApolloResponse> +) { class Fixture { val server = MockWebServer() lateinit var scopes: IScopes @@ -379,7 +388,7 @@ class SentryApollo4BuilderExtensionsClientErrorsTest { private fun executeQuery(sut: ApolloClient, id: String = "83") = runBlocking { val coroutine = launch { try { - sut.query(LaunchDetailsQuery(id)).execute() + executeQueryImplementation(sut.query(LaunchDetailsQuery(id))) } catch (e: ApolloException) { return@launch } diff --git a/sentry-apollo-4/src/test/java/io/sentry/apollo4/SentryApollo4BuilderExtensionsTest.kt b/sentry-apollo-4/src/test/java/io/sentry/apollo4/SentryApollo4BuilderExtensionsTest.kt index 1cc5cc8466..5098e241af 100644 --- a/sentry-apollo-4/src/test/java/io/sentry/apollo4/SentryApollo4BuilderExtensionsTest.kt +++ b/sentry-apollo-4/src/test/java/io/sentry/apollo4/SentryApollo4BuilderExtensionsTest.kt @@ -1,6 +1,9 @@ package io.sentry.apollo4 +import com.apollographql.apollo.ApolloCall import com.apollographql.apollo.ApolloClient +import com.apollographql.apollo.api.ApolloResponse +import com.apollographql.apollo.api.Operation import com.apollographql.apollo.exception.ApolloException import io.sentry.Breadcrumb import io.sentry.IScopes @@ -26,12 +29,18 @@ import org.mockito.kotlin.mock import org.mockito.kotlin.verify import org.mockito.kotlin.whenever import java.util.concurrent.TimeUnit +import kotlin.reflect.KSuspendFunction1 import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertNotNull import kotlin.test.assertTrue -class SentryApollo4BuilderExtensionsTest { +class SentryApollo4BuilderExtensionsTestWithV4Implementation : SentryApollo4BuilderExtensionsTest(ApolloCall<*>::execute) +class SentryApollo4BuilderExtensionsTestWithV3Implementation : SentryApollo4BuilderExtensionsTest(ApolloCall<*>::executeV3) + +abstract class SentryApollo4BuilderExtensionsTest( + private val executeQueryImplementation: KSuspendFunction1, ApolloResponse> +) { class Fixture { val server = MockWebServer() @@ -199,7 +208,7 @@ class SentryApollo4BuilderExtensionsTest { val coroutine = launch { try { - sut.query(LaunchDetailsQuery(id)).execute() + executeQueryImplementation(sut.query(LaunchDetailsQuery(id))) } catch (e: ApolloException) { return@launch } diff --git a/sentry-apollo-4/src/test/java/io/sentry/apollo4/SentryApollo4HttpInterceptorTest.kt b/sentry-apollo-4/src/test/java/io/sentry/apollo4/SentryApollo4HttpInterceptorTest.kt index 4d1fe46590..f0344f62ec 100644 --- a/sentry-apollo-4/src/test/java/io/sentry/apollo4/SentryApollo4HttpInterceptorTest.kt +++ b/sentry-apollo-4/src/test/java/io/sentry/apollo4/SentryApollo4HttpInterceptorTest.kt @@ -1,6 +1,9 @@ package io.sentry.apollo4 +import com.apollographql.apollo.ApolloCall import com.apollographql.apollo.ApolloClient +import com.apollographql.apollo.api.ApolloResponse +import com.apollographql.apollo.api.Operation import com.apollographql.apollo.api.http.HttpRequest import com.apollographql.apollo.api.http.HttpResponse import com.apollographql.apollo.exception.ApolloException @@ -43,13 +46,19 @@ import org.mockito.kotlin.mock import org.mockito.kotlin.verify import org.mockito.kotlin.whenever import java.util.concurrent.TimeUnit +import kotlin.reflect.KSuspendFunction1 import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertNotNull import kotlin.test.assertNull import kotlin.test.assertTrue -class SentryApollo4HttpInterceptorTest { +class SentryApollo4HttpInterceptorTestWithV4Implementation : SentryApollo4HttpInterceptorTest(ApolloCall<*>::execute) +class SentryApollo4HttpInterceptorTestWithV3Implementation : SentryApollo4HttpInterceptorTest(ApolloCall<*>::executeV3) + +abstract class SentryApollo4HttpInterceptorTest( + private val executeQueryImplementation: KSuspendFunction1, ApolloResponse> +) { class Fixture { val server = MockWebServer() @@ -367,7 +376,7 @@ class SentryApollo4HttpInterceptorTest { val coroutine = launch { try { - sut.query(LaunchDetailsQuery(id)).execute() + executeQueryImplementation(sut.query(LaunchDetailsQuery(id))) } catch (e: ApolloException) { return@launch } From 10b690292f2788db0f308d7e424361b3de9bdb8d Mon Sep 17 00:00:00 2001 From: lcian Date: Fri, 14 Feb 2025 12:37:57 +0100 Subject: [PATCH 4/8] changelog --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d6f2529ff4..f352def642 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +### Features + +- Add Apollo 4 integration ([#4166](https://github.com/getsentry/sentry-java/pull/4166)) + ## 8.2.0 ### Breaking Changes From 59977875b26d512fa23f365827ec5f1b8077e367 Mon Sep 17 00:00:00 2001 From: lcian Date: Mon, 17 Feb 2025 10:00:52 +0100 Subject: [PATCH 5/8] rename SentryApollo4BuilderExtensions back to SentryApolloBuilderExtensions to make it easier to migrate --- ...ollo4BuilderExtensions.kt => SentryApolloBuilderExtensions.kt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename sentry-apollo-4/src/main/java/io/sentry/apollo4/{SentryApollo4BuilderExtensions.kt => SentryApolloBuilderExtensions.kt} (100%) diff --git a/sentry-apollo-4/src/main/java/io/sentry/apollo4/SentryApollo4BuilderExtensions.kt b/sentry-apollo-4/src/main/java/io/sentry/apollo4/SentryApolloBuilderExtensions.kt similarity index 100% rename from sentry-apollo-4/src/main/java/io/sentry/apollo4/SentryApollo4BuilderExtensions.kt rename to sentry-apollo-4/src/main/java/io/sentry/apollo4/SentryApolloBuilderExtensions.kt From 621d2a4ceddf509293ed4d85e6e75a0d03335f20 Mon Sep 17 00:00:00 2001 From: lcian Date: Mon, 24 Feb 2025 12:21:56 +0100 Subject: [PATCH 6/8] make api --- sentry-apollo-4/api/sentry-apollo-4.api | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/sentry-apollo-4/api/sentry-apollo-4.api b/sentry-apollo-4/api/sentry-apollo-4.api index 1704f6a918..ec7f6ff051 100644 --- a/sentry-apollo-4/api/sentry-apollo-4.api +++ b/sentry-apollo-4/api/sentry-apollo-4.api @@ -3,17 +3,6 @@ public final class io/sentry/apollo4/BuildConfig { public static final field VERSION_NAME Ljava/lang/String; } -public final class io/sentry/apollo4/SentryApollo4BuilderExtensionsKt { - public static final fun sentryTracing (Lcom/apollographql/apollo/ApolloClient$Builder;)Lcom/apollographql/apollo/ApolloClient$Builder; - public static final fun sentryTracing (Lcom/apollographql/apollo/ApolloClient$Builder;Lio/sentry/IScopes;)Lcom/apollographql/apollo/ApolloClient$Builder; - public static final fun sentryTracing (Lcom/apollographql/apollo/ApolloClient$Builder;Lio/sentry/IScopes;Z)Lcom/apollographql/apollo/ApolloClient$Builder; - public static final fun sentryTracing (Lcom/apollographql/apollo/ApolloClient$Builder;Lio/sentry/IScopes;ZLjava/util/List;)Lcom/apollographql/apollo/ApolloClient$Builder; - public static final fun sentryTracing (Lcom/apollographql/apollo/ApolloClient$Builder;Lio/sentry/IScopes;ZLjava/util/List;Lio/sentry/apollo4/SentryApollo4HttpInterceptor$BeforeSpanCallback;)Lcom/apollographql/apollo/ApolloClient$Builder; - public static final fun sentryTracing (Lcom/apollographql/apollo/ApolloClient$Builder;ZLjava/util/List;Lio/sentry/apollo4/SentryApollo4HttpInterceptor$BeforeSpanCallback;)Lcom/apollographql/apollo/ApolloClient$Builder; - public static synthetic fun sentryTracing$default (Lcom/apollographql/apollo/ApolloClient$Builder;Lio/sentry/IScopes;ZLjava/util/List;Lio/sentry/apollo4/SentryApollo4HttpInterceptor$BeforeSpanCallback;ILjava/lang/Object;)Lcom/apollographql/apollo/ApolloClient$Builder; - public static synthetic fun sentryTracing$default (Lcom/apollographql/apollo/ApolloClient$Builder;ZLjava/util/List;Lio/sentry/apollo4/SentryApollo4HttpInterceptor$BeforeSpanCallback;ILjava/lang/Object;)Lcom/apollographql/apollo/ApolloClient$Builder; -} - public final class io/sentry/apollo4/SentryApollo4ClientException : java/lang/Exception { public static final field Companion Lio/sentry/apollo4/SentryApollo4ClientException$Companion; public fun (Ljava/lang/String;)V @@ -48,3 +37,14 @@ public final class io/sentry/apollo4/SentryApollo4Interceptor : com/apollographq public fun intercept (Lcom/apollographql/apollo/api/ApolloRequest;Lcom/apollographql/apollo/interceptor/ApolloInterceptorChain;)Lkotlinx/coroutines/flow/Flow; } +public final class io/sentry/apollo4/SentryApolloBuilderExtensionsKt { + public static final fun sentryTracing (Lcom/apollographql/apollo/ApolloClient$Builder;)Lcom/apollographql/apollo/ApolloClient$Builder; + public static final fun sentryTracing (Lcom/apollographql/apollo/ApolloClient$Builder;Lio/sentry/IScopes;)Lcom/apollographql/apollo/ApolloClient$Builder; + public static final fun sentryTracing (Lcom/apollographql/apollo/ApolloClient$Builder;Lio/sentry/IScopes;Z)Lcom/apollographql/apollo/ApolloClient$Builder; + public static final fun sentryTracing (Lcom/apollographql/apollo/ApolloClient$Builder;Lio/sentry/IScopes;ZLjava/util/List;)Lcom/apollographql/apollo/ApolloClient$Builder; + public static final fun sentryTracing (Lcom/apollographql/apollo/ApolloClient$Builder;Lio/sentry/IScopes;ZLjava/util/List;Lio/sentry/apollo4/SentryApollo4HttpInterceptor$BeforeSpanCallback;)Lcom/apollographql/apollo/ApolloClient$Builder; + public static final fun sentryTracing (Lcom/apollographql/apollo/ApolloClient$Builder;ZLjava/util/List;Lio/sentry/apollo4/SentryApollo4HttpInterceptor$BeforeSpanCallback;)Lcom/apollographql/apollo/ApolloClient$Builder; + public static synthetic fun sentryTracing$default (Lcom/apollographql/apollo/ApolloClient$Builder;Lio/sentry/IScopes;ZLjava/util/List;Lio/sentry/apollo4/SentryApollo4HttpInterceptor$BeforeSpanCallback;ILjava/lang/Object;)Lcom/apollographql/apollo/ApolloClient$Builder; + public static synthetic fun sentryTracing$default (Lcom/apollographql/apollo/ApolloClient$Builder;ZLjava/util/List;Lio/sentry/apollo4/SentryApollo4HttpInterceptor$BeforeSpanCallback;ILjava/lang/Object;)Lcom/apollographql/apollo/ApolloClient$Builder; +} + From de7eeb9853cd5e13bbaca53ca41fbf7f3549a432 Mon Sep 17 00:00:00 2001 From: lcian Date: Mon, 24 Feb 2025 12:33:12 +0100 Subject: [PATCH 7/8] add README.md --- sentry-apollo-4/README.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 sentry-apollo-4/README.md diff --git a/sentry-apollo-4/README.md b/sentry-apollo-4/README.md new file mode 100644 index 0000000000..e9b4ad2efe --- /dev/null +++ b/sentry-apollo-4/README.md @@ -0,0 +1,5 @@ +# sentry-apollo-4 + +This module provides an integration for [Apollo Kotlin 4](https://www.apollographql.com/docs/kotlin/v4). + +Please consult the documentation on how to install and use this integration in the Sentry Docs for [Android](https://docs.sentry.io/platforms/android/integrations/apollo4/) or [Java](https://docs.sentry.io/platforms/java/tracing/instrumentation/apollo4/). From 0b526c77e36ba052cdce973f69c0ae11225b67c2 Mon Sep 17 00:00:00 2001 From: lcian Date: Mon, 24 Feb 2025 12:43:44 +0100 Subject: [PATCH 8/8] update comment --- .../main/java/io/sentry/apollo4/SentryApollo4HttpInterceptor.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry-apollo-4/src/main/java/io/sentry/apollo4/SentryApollo4HttpInterceptor.kt b/sentry-apollo-4/src/main/java/io/sentry/apollo4/SentryApollo4HttpInterceptor.kt index ec510255a0..a4e31431bb 100644 --- a/sentry-apollo-4/src/main/java/io/sentry/apollo4/SentryApollo4HttpInterceptor.kt +++ b/sentry-apollo-4/src/main/java/io/sentry/apollo4/SentryApollo4HttpInterceptor.kt @@ -90,7 +90,7 @@ class SentryApollo4HttpInterceptor @JvmOverloads constructor( return httpResponse } catch (e: Throwable) { - // https://github.com/apollographql/apollo-kotlin/issues/4711 will change error handling in v4 + // client errors don't throw anymore in v4, but we should still be able to detect all of them by looking at the status code and/or errors in the response body when (e) { is ApolloHttpException -> { statusCode = e.statusCode