diff --git a/.craft.yml b/.craft.yml index 9bcb332c81..f674e0faa0 100644 --- a/.craft.yml +++ b/.craft.yml @@ -45,3 +45,4 @@ targets: maven:io.sentry:sentry-compose: maven:io.sentry:sentry-compose-android: maven:io.sentry:sentry-compose-desktop: + maven:io.sentry:sentry-apollo-3: diff --git a/.github/ISSUE_TEMPLATE/bug_report_android.yml b/.github/ISSUE_TEMPLATE/bug_report_android.yml index b9251afc5e..52bfa433d7 100644 --- a/.github/ISSUE_TEMPLATE/bug_report_android.yml +++ b/.github/ISSUE_TEMPLATE/bug_report_android.yml @@ -14,6 +14,7 @@ body: - sentry-android-timber - sentry-android-fragment - sentry-apollo + - sentry-apollo-3 - other validations: required: true diff --git a/.github/ISSUE_TEMPLATE/bug_report_java.yml b/.github/ISSUE_TEMPLATE/bug_report_java.yml index 8d56959802..36e41102bb 100644 --- a/.github/ISSUE_TEMPLATE/bug_report_java.yml +++ b/.github/ISSUE_TEMPLATE/bug_report_java.yml @@ -12,6 +12,7 @@ body: - sentry-jul - sentry-jdbc - sentry-apollo + - sentry-apollo-3 - sentry-kotlin-extensions - sentry-servlet - sentry-servlet-jakarta diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e80ab7eba..879c8fac3d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ ### Features +- Add integration for Apollo-Kotlin 3 ([#2109](https://github.com/getsentry/sentry-java/pull/2109)) - New package `sentry-android-navigation` for AndroidX Navigation support ([#2136](https://github.com/getsentry/sentry-java/pull/2136)) - New package `sentry-compose` for Jetpack Compose support (Navigation) ([#2136](https://github.com/getsentry/sentry-java/pull/2136)) - Add sample rate to baggage as well as trace in envelope header and flatten user ([#2135](https://github.com/getsentry/sentry-java/pull/2135)) diff --git a/buildSrc/src/main/java/Config.kt b/buildSrc/src/main/java/Config.kt index 1c6f5c7ea6..bf0744c5f0 100644 --- a/buildSrc/src/main/java/Config.kt +++ b/buildSrc/src/main/java/Config.kt @@ -119,6 +119,8 @@ object Config { val composeFoundation = "androidx.compose.foundation:foundation:$composeVersion" val composeFoundationLayout = "androidx.compose.foundation:foundation-layout:$composeVersion" val composeMaterial = "androidx.compose.material3:material3:1.0.0-alpha13" + + val apolloKotlin = "com.apollographql.apollo3:apollo-runtime:3.3.0" } object AnnotationProcessors { @@ -146,7 +148,7 @@ object Config { val mockitoInline = "org.mockito:mockito-inline:4.3.1" val awaitility = "org.awaitility:awaitility-kotlin:4.1.1" val mockWebserver = "com.squareup.okhttp3:mockwebserver:${Libs.okHttpVersion}" - val mockWebserver3 = "com.squareup.okhttp3:mockwebserver:3.14.9" + val mockWebserver4 = "com.squareup.okhttp3:mockwebserver:4.9.3" val jsonUnit = "net.javacrumbs.json-unit:json-unit:2.32.0" val hsqldb = "org.hsqldb:hsqldb:2.6.1" val javaFaker = "com.github.javafaker:javafaker:1.0.2" diff --git a/sentry-apollo-3/api/sentry-apollo-3.api b/sentry-apollo-3/api/sentry-apollo-3.api new file mode 100644 index 0000000000..a41facc18a --- /dev/null +++ b/sentry-apollo-3/api/sentry-apollo-3.api @@ -0,0 +1,34 @@ +public final class io/sentry/apollo3/SentryApollo3HttpInterceptor : com/apollographql/apollo3/network/http/HttpInterceptor { + public static final field Companion Lio/sentry/apollo3/SentryApollo3HttpInterceptor$Companion; + public static final field SENTRY_APOLLO_3_OPERATION_NAME Ljava/lang/String; + 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/IHub;)V + public fun (Lio/sentry/IHub;Lio/sentry/apollo3/SentryApollo3HttpInterceptor$BeforeSpanCallback;)V + public synthetic fun (Lio/sentry/IHub;Lio/sentry/apollo3/SentryApollo3HttpInterceptor$BeforeSpanCallback;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun dispose ()V + public fun intercept (Lcom/apollographql/apollo3/api/http/HttpRequest;Lcom/apollographql/apollo3/network/http/HttpInterceptorChain;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + +public abstract interface class io/sentry/apollo3/SentryApollo3HttpInterceptor$BeforeSpanCallback { + public abstract fun execute (Lio/sentry/ISpan;Lcom/apollographql/apollo3/api/http/HttpRequest;Lcom/apollographql/apollo3/api/http/HttpResponse;)Lio/sentry/ISpan; +} + +public final class io/sentry/apollo3/SentryApollo3HttpInterceptor$Companion { +} + +public final class io/sentry/apollo3/SentryApollo3Interceptor : com/apollographql/apollo3/interceptor/ApolloInterceptor { + public fun ()V + public fun intercept (Lcom/apollographql/apollo3/api/ApolloRequest;Lcom/apollographql/apollo3/interceptor/ApolloInterceptorChain;)Lkotlinx/coroutines/flow/Flow; +} + +public final class io/sentry/apollo3/SentryApolloBuilderExtensionsKt { + public static final fun sentryTracing (Lcom/apollographql/apollo3/ApolloClient$Builder;)Lcom/apollographql/apollo3/ApolloClient$Builder; + public static final fun sentryTracing (Lcom/apollographql/apollo3/ApolloClient$Builder;Lio/sentry/IHub;)Lcom/apollographql/apollo3/ApolloClient$Builder; + public static final fun sentryTracing (Lcom/apollographql/apollo3/ApolloClient$Builder;Lio/sentry/IHub;Lio/sentry/apollo3/SentryApollo3HttpInterceptor$BeforeSpanCallback;)Lcom/apollographql/apollo3/ApolloClient$Builder; + public static final fun sentryTracing (Lcom/apollographql/apollo3/ApolloClient$Builder;Lio/sentry/apollo3/SentryApollo3HttpInterceptor$BeforeSpanCallback;)Lcom/apollographql/apollo3/ApolloClient$Builder; + public static synthetic fun sentryTracing$default (Lcom/apollographql/apollo3/ApolloClient$Builder;Lio/sentry/IHub;Lio/sentry/apollo3/SentryApollo3HttpInterceptor$BeforeSpanCallback;ILjava/lang/Object;)Lcom/apollographql/apollo3/ApolloClient$Builder; + public static synthetic fun sentryTracing$default (Lcom/apollographql/apollo3/ApolloClient$Builder;Lio/sentry/apollo3/SentryApollo3HttpInterceptor$BeforeSpanCallback;ILjava/lang/Object;)Lcom/apollographql/apollo3/ApolloClient$Builder; +} + diff --git a/sentry-apollo-3/build.gradle.kts b/sentry-apollo-3/build.gradle.kts new file mode 100644 index 0000000000..a04a9bd91f --- /dev/null +++ b/sentry-apollo-3/build.gradle.kts @@ -0,0 +1,79 @@ +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) + + implementation(Config.Libs.apolloKotlin) + + 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.mockWebserver4) +} + +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") + } +} diff --git a/sentry-apollo-3/src/main/java/io/sentry/apollo3/SentryApollo3HttpInterceptor.kt b/sentry-apollo-3/src/main/java/io/sentry/apollo3/SentryApollo3HttpInterceptor.kt new file mode 100644 index 0000000000..26af28c2bc --- /dev/null +++ b/sentry-apollo-3/src/main/java/io/sentry/apollo3/SentryApollo3HttpInterceptor.kt @@ -0,0 +1,181 @@ +package io.sentry.apollo3 + +import com.apollographql.apollo3.api.http.HttpHeader +import com.apollographql.apollo3.api.http.HttpRequest +import com.apollographql.apollo3.api.http.HttpResponse +import com.apollographql.apollo3.exception.ApolloHttpException +import com.apollographql.apollo3.exception.ApolloNetworkException +import com.apollographql.apollo3.network.http.HttpInterceptor +import com.apollographql.apollo3.network.http.HttpInterceptorChain +import io.sentry.Breadcrumb +import io.sentry.Hint +import io.sentry.HubAdapter +import io.sentry.IHub +import io.sentry.ISpan +import io.sentry.SentryLevel +import io.sentry.SpanStatus +import io.sentry.TracingOrigins +import io.sentry.TypeCheckHint + +class SentryApollo3HttpInterceptor @JvmOverloads constructor(private val hub: IHub = HubAdapter.getInstance(), private val beforeSpan: BeforeSpanCallback? = null) : + HttpInterceptor { + + override suspend fun intercept( + request: HttpRequest, + chain: HttpInterceptorChain + ): HttpResponse { + val activeSpan = hub.span + return if (activeSpan == null) { + chain.proceed(request) + } else { + val span = startChild(request, activeSpan) + + val cleanedHeaders = removeSentryInternalHeaders(request.headers) + + val requestBuilder = request.newBuilder().apply { + headers(cleanedHeaders) + } + + if (TracingOrigins.contain(hub.options.tracingOrigins, request.url)) { + val sentryTraceHeader = span.toSentryTrace() + val baggageHeader = span.toBaggageHeader() + requestBuilder.addHeader(sentryTraceHeader.name, sentryTraceHeader.value) + + baggageHeader?.let { + requestBuilder.addHeader(it.name, it.value) + } + } + + val modifiedRequest = requestBuilder.build() + var httpResponse: HttpResponse? = null + var statusCode: Int? = null + + try { + httpResponse = chain.proceed(modifiedRequest) + statusCode = httpResponse.statusCode + span.status = SpanStatus.fromHttpStatusCode(statusCode, SpanStatus.UNKNOWN) + return httpResponse + } catch (e: Throwable) { + when (e) { + is ApolloHttpException -> { + statusCode = e.statusCode + span.status = SpanStatus.fromHttpStatusCode(statusCode, SpanStatus.INTERNAL_ERROR) + } + is ApolloNetworkException -> span.status = SpanStatus.INTERNAL_ERROR + else -> SpanStatus.INTERNAL_ERROR + } + span.throwable = e + throw e + } finally { + finish(span, modifiedRequest, httpResponse, statusCode) + } + } + } + + private fun removeSentryInternalHeaders(headers: List): List { + return headers.filterNot { it.name == SENTRY_APOLLO_3_VARIABLES || it.name == SENTRY_APOLLO_3_OPERATION_NAME || it.name == SENTRY_APOLLO_3_OPERATION_TYPE } + } + + private fun startChild(request: HttpRequest, activeSpan: ISpan): ISpan { + val url = request.url + val method = request.method + + val operationName = operationNameFromHeaders(request) + val operation = operationName ?: "apollo.client" + val operationType = request.valueForHeader(SENTRY_APOLLO_3_OPERATION_TYPE) ?: method + val operationId = request.valueForHeader("X-APOLLO-OPERATION-ID") + val variables = request.valueForHeader(SENTRY_APOLLO_3_VARIABLES) + val description = "$operationType ${operationName ?: url}" + + return activeSpan.startChild(operation, description).apply { + operationId?.let { + setData("operationId", it) + } + + variables?.let { + setData("variables", it) + } + } + } + + private fun operationNameFromHeaders(request: HttpRequest): String? { + return request.valueForHeader(SENTRY_APOLLO_3_OPERATION_NAME) ?: request.valueForHeader("X-APOLLO-OPERATION-NAME") + } + + private fun HttpRequest.valueForHeader(key: String) = headers.firstOrNull { it.name == key }?.value + + private fun finish(span: ISpan, request: HttpRequest, response: HttpResponse? = null, statusCode: Int?) { + if (beforeSpan != null) { + try { + val result = beforeSpan.execute(span, request, response) + if (result == null) { + // Span is dropped + span.spanContext.sampled = false + } + } catch (e: Throwable) { + hub.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) + } + + val hint = Hint().also { + it.set(TypeCheckHint.APOLLO_REQUEST, request) + } + + response?.let { httpResponse -> + // Content-Length header is not present on batched operations + httpResponse.headersContentLength().ifHasValidLength { contentLength -> + breadcrumb.setData("response_body_size", contentLength) + } + + if (!breadcrumb.data.containsKey("response_body_size")) { + httpResponse.body?.buffer?.size?.ifHasValidLength { contentLength -> + breadcrumb.setData("response_body_size", contentLength) + } + } + + hint.set(TypeCheckHint.APOLLO_RESPONSE, httpResponse) + } + + hub.addBreadcrumb(breadcrumb, hint) + } + + // Extensions + + private fun HttpResponse.headersContentLength(): Long { + return headers.firstOrNull { it.name == "Content-Length" }?.value?.toLongOrNull() ?: -1L + } + + private fun Long?.ifHasValidLength(fn: (Long) -> Unit) { + if (this != null && this != -1L) { + fn.invoke(this) + } + } + + /** + * 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_3_VARIABLES = "SENTRY-APOLLO-3-VARIABLES" + const val SENTRY_APOLLO_3_OPERATION_NAME = "SENTRY-APOLLO-3-OPERATION-NAME" + const val SENTRY_APOLLO_3_OPERATION_TYPE = "SENTRY-APOLLO-3-OPERATION-TYPE" + } +} diff --git a/sentry-apollo-3/src/main/java/io/sentry/apollo3/SentryApollo3Interceptor.kt b/sentry-apollo-3/src/main/java/io/sentry/apollo3/SentryApollo3Interceptor.kt new file mode 100644 index 0000000000..c0e0448e81 --- /dev/null +++ b/sentry-apollo-3/src/main/java/io/sentry/apollo3/SentryApollo3Interceptor.kt @@ -0,0 +1,40 @@ +package io.sentry.apollo3 + +import com.apollographql.apollo3.api.ApolloRequest +import com.apollographql.apollo3.api.ApolloResponse +import com.apollographql.apollo3.api.CustomScalarAdapters +import com.apollographql.apollo3.api.Mutation +import com.apollographql.apollo3.api.Operation +import com.apollographql.apollo3.api.Query +import com.apollographql.apollo3.api.Subscription +import com.apollographql.apollo3.api.variables +import com.apollographql.apollo3.interceptor.ApolloInterceptor +import com.apollographql.apollo3.interceptor.ApolloInterceptorChain +import kotlinx.coroutines.flow.Flow + +class SentryApollo3Interceptor : ApolloInterceptor { + + override fun intercept( + request: ApolloRequest, + chain: ApolloInterceptorChain + ): Flow> { + val builder = request.newBuilder() + .addHttpHeader(SentryApollo3HttpInterceptor.SENTRY_APOLLO_3_OPERATION_TYPE, operationType(request)) + .addHttpHeader(SentryApollo3HttpInterceptor.SENTRY_APOLLO_3_OPERATION_NAME, request.operation.name()) + + request.scalarAdapters?.let { + builder.addHttpHeader(SentryApollo3HttpInterceptor.SENTRY_APOLLO_3_VARIABLES, request.operation.variables(it).valueMap.toString()) + } + 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-3/src/main/java/io/sentry/apollo3/sentryApolloBuilderExtensions.kt b/sentry-apollo-3/src/main/java/io/sentry/apollo3/sentryApolloBuilderExtensions.kt new file mode 100644 index 0000000000..e4b18622c8 --- /dev/null +++ b/sentry-apollo-3/src/main/java/io/sentry/apollo3/sentryApolloBuilderExtensions.kt @@ -0,0 +1,16 @@ +package io.sentry.apollo3 + +import com.apollographql.apollo3.ApolloClient +import io.sentry.HubAdapter +import io.sentry.IHub + +@JvmOverloads +fun ApolloClient.Builder.sentryTracing(hub: IHub = HubAdapter.getInstance(), beforeSpan: SentryApollo3HttpInterceptor.BeforeSpanCallback? = null): ApolloClient.Builder { + addInterceptor(SentryApollo3Interceptor()) + addHttpInterceptor(SentryApollo3HttpInterceptor(hub, beforeSpan)) + return this +} + +fun ApolloClient.Builder.sentryTracing(beforeSpan: SentryApollo3HttpInterceptor.BeforeSpanCallback? = null): ApolloClient.Builder { + return sentryTracing(HubAdapter.getInstance(), beforeSpan) +} diff --git a/sentry-apollo-3/src/test/java/io/sentry/apollo3/LaunchDetailsQuery.kt b/sentry-apollo-3/src/test/java/io/sentry/apollo3/LaunchDetailsQuery.kt new file mode 100644 index 0000000000..5e6653edc8 --- /dev/null +++ b/sentry-apollo-3/src/test/java/io/sentry/apollo3/LaunchDetailsQuery.kt @@ -0,0 +1,88 @@ +package io.sentry.apollo3 + +import com.apollographql.apollo3.api.Adapter +import com.apollographql.apollo3.api.CompiledField +import com.apollographql.apollo3.api.CustomScalarAdapters +import com.apollographql.apollo3.api.Query +import com.apollographql.apollo3.api.json.JsonWriter +import com.apollographql.apollo3.api.obj +import io.sentry.apollo3.adapter.LaunchDetailsQuery_ResponseAdapter +import io.sentry.apollo3.adapter.LaunchDetailsQuery_VariablesAdapter +import io.sentry.apollo3.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 + ) { + 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.apollo3.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-3/src/test/java/io/sentry/apollo3/SentryApollo3InterceptorTest.kt b/sentry-apollo-3/src/test/java/io/sentry/apollo3/SentryApollo3InterceptorTest.kt new file mode 100644 index 0000000000..cff354aecd --- /dev/null +++ b/sentry-apollo-3/src/test/java/io/sentry/apollo3/SentryApollo3InterceptorTest.kt @@ -0,0 +1,254 @@ +package io.sentry.apollo3 + +import com.apollographql.apollo3.ApolloClient +import com.apollographql.apollo3.exception.ApolloException +import com.nhaarman.mockitokotlin2.anyOrNull +import com.nhaarman.mockitokotlin2.check +import com.nhaarman.mockitokotlin2.mock +import com.nhaarman.mockitokotlin2.verify +import com.nhaarman.mockitokotlin2.whenever +import io.sentry.BaggageHeader +import io.sentry.Breadcrumb +import io.sentry.IHub +import io.sentry.ITransaction +import io.sentry.SentryOptions +import io.sentry.SentryTraceHeader +import io.sentry.SentryTracer +import io.sentry.SpanStatus +import io.sentry.TraceContext +import io.sentry.TracesSamplingDecision +import io.sentry.TransactionContext +import io.sentry.apollo3.SentryApollo3HttpInterceptor.BeforeSpanCallback +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 kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull +import kotlin.test.assertTrue + +class SentryApollo3InterceptorTest { + + class Fixture { + val server = MockWebServer() + val hub = mock() + private var httpInterceptor = SentryApollo3HttpInterceptor(hub) + + @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(hub.options).thenReturn( + SentryOptions().apply { + dsn = "https://key@sentry.io/proj" + isTraceSampling = true + } + ) + + server.enqueue( + MockResponse() + .setBody(responseBody) + .setSocketPolicy(socketPolicy) + .setResponseCode(httpStatusCode) + ) + + if (beforeSpan != null) { + httpInterceptor = SentryApollo3HttpInterceptor(hub, beforeSpan) + } + + val builder = ApolloClient.Builder() + .serverUrl(server.url("/").toString()) + .addHttpInterceptor(httpInterceptor) + + return builder.build() + } + } + + private val fixture = Fixture() + + @Test + fun `creates a span around the successful request`() { + executeQuery() + + verify(fixture.hub).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.hub).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.hub).captureTransaction( + check { + assertTransactionDetails(it) + assertEquals(SpanStatus.INTERNAL_ERROR, it.spans.first().status) + }, + anyOrNull(), + anyOrNull(), + anyOrNull() + ) + } + + @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() + 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() + assertNotNull(recorderRequest.headers[SentryTraceHeader.SENTRY_TRACE_HEADER]) + assertNotNull(recorderRequest.headers[BaggageHeader.BAGGAGE_HEADER]) + } + + @Test + fun `customizer modifies span`() { + executeQuery( + + fixture.getSut( + beforeSpan = { span, request, response -> + span.description = "overwritten description" + span + } + ) + ) + + verify(fixture.hub).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.hub).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.hub).captureTransaction( + check { + assertEquals(1, it.spans.size) + }, + anyOrNull(), + anyOrNull(), + anyOrNull() + ) + } + + @Test + fun `adds breadcrumb when http calls succeeds`() { + executeQuery(fixture.getSut()) + verify(fixture.hub).addBreadcrumb( + check { + assertEquals("http", it.type) + assertEquals(280L, it.data["response_body_size"]) + assertEquals(193L, it.data["request_body_size"]) + }, + anyOrNull() + ) + } + + private fun assertTransactionDetails(it: SentryTransaction) { + assertEquals(1, it.spans.size) + val httpClientSpan = it.spans.first() + assertEquals("LaunchDetails", httpClientSpan.op) + assertTrue { httpClientSpan.description?.startsWith("Post LaunchDetails") == true } + assertNotNull(httpClientSpan.data) { + assertNotNull(it["operationId"]) + } + } + + private fun executeQuery(sut: ApolloClient = fixture.getSut(), isSpanActive: Boolean = true) = runBlocking { + var tx: ITransaction? = null + if (isSpanActive) { + tx = SentryTracer(TransactionContext("op", "desc", TracesSamplingDecision(true)), fixture.hub) + whenever(fixture.hub.span).thenReturn(tx) + } + + val coroutine = launch { + try { + sut.query(LaunchDetailsQuery("83")).execute() + } catch (e: ApolloException) { + return@launch + } + } + + coroutine.join() + tx?.finish() + } +} diff --git a/sentry-apollo-3/src/test/java/io/sentry/apollo3/SentryApollo3InterceptorWithVariablesTest.kt b/sentry-apollo-3/src/test/java/io/sentry/apollo3/SentryApollo3InterceptorWithVariablesTest.kt new file mode 100644 index 0000000000..1079dc0d0b --- /dev/null +++ b/sentry-apollo-3/src/test/java/io/sentry/apollo3/SentryApollo3InterceptorWithVariablesTest.kt @@ -0,0 +1,170 @@ +package io.sentry.apollo3 + +import com.apollographql.apollo3.ApolloClient +import com.apollographql.apollo3.exception.ApolloException +import com.nhaarman.mockitokotlin2.anyOrNull +import com.nhaarman.mockitokotlin2.check +import com.nhaarman.mockitokotlin2.mock +import com.nhaarman.mockitokotlin2.verify +import com.nhaarman.mockitokotlin2.whenever +import io.sentry.Breadcrumb +import io.sentry.IHub +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.apollo3.SentryApollo3HttpInterceptor.BeforeSpanCallback +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 kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull + +class SentryApollo3InterceptorWithVariablesTest { + + class Fixture { + val server = MockWebServer() + val hub = 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(hub.options).thenReturn(SentryOptions()) + + server.enqueue( + MockResponse() + .setBody(responseBody) + .setSocketPolicy(socketPolicy) + .setResponseCode(httpStatusCode) + ) + + return ApolloClient.Builder().serverUrl(server.url("/").toString()) + .sentryTracing(hub, beforeSpan) + .build() + } + } + + private val fixture = Fixture() + + @Test + fun `creates a span around the successful request`() { + executeQuery() + + verify(fixture.hub).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.hub).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.hub).captureTransaction( + check { + assertTransactionDetails(it) + assertEquals(SpanStatus.INTERNAL_ERROR, it.spans.first().status) + }, + anyOrNull(), + anyOrNull(), + anyOrNull() + ) + } + + @Test + fun `adds breadcrumb when http calls succeeds`() { + executeQuery(fixture.getSut()) + verify(fixture.hub).addBreadcrumb( + check { + assertEquals("http", it.type) + assertEquals(280L, it.data["response_body_size"]) + assertEquals(193L, it.data["request_body_size"]) + }, + anyOrNull() + ) + } + + @Test + fun `internal headers are not sent over the wire`() { + executeQuery(fixture.getSut()) + val recorderRequest = fixture.server.takeRequest() + assertNull(recorderRequest.headers[SentryApollo3HttpInterceptor.SENTRY_APOLLO_3_VARIABLES]) + assertNull(recorderRequest.headers[SentryApollo3HttpInterceptor.SENTRY_APOLLO_3_OPERATION_NAME]) + assertNull(recorderRequest.headers[SentryApollo3HttpInterceptor.SENTRY_APOLLO_3_VARIABLES]) + } + + private fun assertTransactionDetails(it: SentryTransaction) { + assertEquals(1, it.spans.size) + val httpClientSpan = it.spans.first() + assertEquals("LaunchDetails", httpClientSpan.op) + assertEquals("query LaunchDetails", httpClientSpan.description) + assertNotNull(httpClientSpan.data) { + assertNotNull(it["operationId"]) + assertNotNull(it["variables"]) + } + } + + private fun executeQuery(sut: ApolloClient = fixture.getSut(), isSpanActive: Boolean = true) = runBlocking { + var tx: ITransaction? = null + if (isSpanActive) { + tx = SentryTracer(TransactionContext("op", "desc", TracesSamplingDecision(true)), fixture.hub) + whenever(fixture.hub.span).thenReturn(tx) + } + + val coroutine = launch { + try { + sut.query(LaunchDetailsQuery("83")).execute() + } catch (e: ApolloException) { + return@launch + } + } + + coroutine.join() + tx?.finish() + } +} diff --git a/sentry-apollo-3/src/test/java/io/sentry/apollo3/adapter/LaunchDetailsQuery_ResponseAdapter.kt b/sentry-apollo-3/src/test/java/io/sentry/apollo3/adapter/LaunchDetailsQuery_ResponseAdapter.kt new file mode 100644 index 0000000000..dd8f57d142 --- /dev/null +++ b/sentry-apollo-3/src/test/java/io/sentry/apollo3/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.apollo3.adapter + +import com.apollographql.apollo3.api.Adapter +import com.apollographql.apollo3.api.CustomScalarAdapters +import com.apollographql.apollo3.api.NullableStringAdapter +import com.apollographql.apollo3.api.StringAdapter +import com.apollographql.apollo3.api.json.JsonReader +import com.apollographql.apollo3.api.json.JsonWriter +import com.apollographql.apollo3.api.nullable +import com.apollographql.apollo3.api.obj +import io.sentry.apollo3.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-3/src/test/java/io/sentry/apollo3/adapter/LaunchDetailsQuery_VariablesAdapter.kt b/sentry-apollo-3/src/test/java/io/sentry/apollo3/adapter/LaunchDetailsQuery_VariablesAdapter.kt new file mode 100644 index 0000000000..81048e2bbe --- /dev/null +++ b/sentry-apollo-3/src/test/java/io/sentry/apollo3/adapter/LaunchDetailsQuery_VariablesAdapter.kt @@ -0,0 +1,28 @@ +// +// AUTO-GENERATED FILE. DO NOT MODIFY. +// +// This class was automatically generated by Apollo GraphQL version '3.3.0'. +// +package io.sentry.apollo3.adapter + +import com.apollographql.apollo3.api.Adapter +import com.apollographql.apollo3.api.CustomScalarAdapters +import com.apollographql.apollo3.api.StringAdapter +import com.apollographql.apollo3.api.json.JsonReader +import com.apollographql.apollo3.api.json.JsonWriter +import io.sentry.apollo3.LaunchDetailsQuery +import kotlin.IllegalStateException + +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-3/src/test/java/io/sentry/apollo3/selections/LaunchDetailsQuerySelections.kt b/sentry-apollo-3/src/test/java/io/sentry/apollo3/selections/LaunchDetailsQuerySelections.kt new file mode 100644 index 0000000000..fc0f4fbc39 --- /dev/null +++ b/sentry-apollo-3/src/test/java/io/sentry/apollo3/selections/LaunchDetailsQuerySelections.kt @@ -0,0 +1,82 @@ +// +// AUTO-GENERATED FILE. DO NOT MODIFY. +// +// This class was automatically generated by Apollo GraphQL version '3.3.0'. +// +package io.sentry.apollo3.selections + +import com.apollographql.apollo3.api.CompiledArgument +import com.apollographql.apollo3.api.CompiledField +import com.apollographql.apollo3.api.CompiledSelection +import com.apollographql.apollo3.api.CompiledVariable +import com.apollographql.apollo3.api.notNull +import io.sentry.apollo3.type.GraphQLID +import io.sentry.apollo3.type.GraphQLString +import io.sentry.apollo3.type.Launch +import io.sentry.apollo3.type.Mission +import io.sentry.apollo3.type.Query.Companion.type +import io.sentry.apollo3.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("size", "LARGE") + ) + ) + .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("id", CompiledVariable("id")) + ) + ) + .selections(launch) + .build() + ) +} diff --git a/sentry-apollo-3/src/test/java/io/sentry/apollo3/type/GraphQLBoolean.kt b/sentry-apollo-3/src/test/java/io/sentry/apollo3/type/GraphQLBoolean.kt new file mode 100644 index 0000000000..164f4cfe53 --- /dev/null +++ b/sentry-apollo-3/src/test/java/io/sentry/apollo3/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.apollo3.type + +import com.apollographql.apollo3.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-3/src/test/java/io/sentry/apollo3/type/GraphQLID.kt b/sentry-apollo-3/src/test/java/io/sentry/apollo3/type/GraphQLID.kt new file mode 100644 index 0000000000..25967c23a4 --- /dev/null +++ b/sentry-apollo-3/src/test/java/io/sentry/apollo3/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.apollo3.type + +import com.apollographql.apollo3.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-3/src/test/java/io/sentry/apollo3/type/GraphQLString.kt b/sentry-apollo-3/src/test/java/io/sentry/apollo3/type/GraphQLString.kt new file mode 100644 index 0000000000..b5302e6f22 --- /dev/null +++ b/sentry-apollo-3/src/test/java/io/sentry/apollo3/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.apollo3.type + +import com.apollographql.apollo3.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-3/src/test/java/io/sentry/apollo3/type/Launch.kt b/sentry-apollo-3/src/test/java/io/sentry/apollo3/type/Launch.kt new file mode 100644 index 0000000000..7a1a3de37f --- /dev/null +++ b/sentry-apollo-3/src/test/java/io/sentry/apollo3/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.apollo3.type + +import com.apollographql.apollo3.api.ObjectType + +public class Launch { + public companion object { + public val type: ObjectType = ObjectType(name = "Launch") + } +} diff --git a/sentry-apollo-3/src/test/java/io/sentry/apollo3/type/Mission.kt b/sentry-apollo-3/src/test/java/io/sentry/apollo3/type/Mission.kt new file mode 100644 index 0000000000..d7268eea5b --- /dev/null +++ b/sentry-apollo-3/src/test/java/io/sentry/apollo3/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.apollo3.type + +import com.apollographql.apollo3.api.ObjectType + +public class Mission { + public companion object { + public val type: ObjectType = ObjectType(name = "Mission") + } +} diff --git a/sentry-apollo-3/src/test/java/io/sentry/apollo3/type/Query.kt b/sentry-apollo-3/src/test/java/io/sentry/apollo3/type/Query.kt new file mode 100644 index 0000000000..59ac614e6b --- /dev/null +++ b/sentry-apollo-3/src/test/java/io/sentry/apollo3/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.apollo3.type + +import com.apollographql.apollo3.api.ObjectType + +public class Query { + public companion object { + public val type: ObjectType = ObjectType(name = "Query") + } +} diff --git a/sentry-apollo-3/src/test/java/io/sentry/apollo3/type/Rocket.kt b/sentry-apollo-3/src/test/java/io/sentry/apollo3/type/Rocket.kt new file mode 100644 index 0000000000..6e32fbe2c3 --- /dev/null +++ b/sentry-apollo-3/src/test/java/io/sentry/apollo3/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.apollo3.type + +import com.apollographql.apollo3.api.ObjectType + +public class Rocket { + public companion object { + public val type: ObjectType = ObjectType(name = "Rocket") + } +} diff --git a/sentry-apollo-3/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/sentry-apollo-3/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker new file mode 100644 index 0000000000..1f0955d450 --- /dev/null +++ b/sentry-apollo-3/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker @@ -0,0 +1 @@ +mock-maker-inline diff --git a/sentry-apollo/build.gradle.kts b/sentry-apollo/build.gradle.kts index 516bd7bd1d..3304e431c7 100644 --- a/sentry-apollo/build.gradle.kts +++ b/sentry-apollo/build.gradle.kts @@ -39,7 +39,7 @@ dependencies { testImplementation(Config.TestLibs.kotlinTestJunit) testImplementation(Config.TestLibs.mockitoKotlin) testImplementation(Config.TestLibs.mockitoInline) - testImplementation(Config.TestLibs.mockWebserver3) + testImplementation(Config.TestLibs.mockWebserver4) testImplementation(Config.Libs.apolloCoroutines) } diff --git a/settings.gradle.kts b/settings.gradle.kts index cd8cf46de5..7f223f91a4 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -15,6 +15,7 @@ include( "sentry-android-navigation", "sentry-compose", "sentry-apollo", + "sentry-apollo-3", "sentry-test-support", "sentry-log4j2", "sentry-logback",