From 770b0c9a7ad48808fa58fbb616dd0260ce5208be Mon Sep 17 00:00:00 2001 From: Lukas Bloder Date: Wed, 8 Jun 2022 00:24:43 +0200 Subject: [PATCH 01/12] add Interceptors for apollo3, add issue templates, adapt original test for apollo3 --- .github/ISSUE_TEMPLATE/bug_report_android.yml | 1 + .github/ISSUE_TEMPLATE/bug_report_java.yml | 1 + buildSrc/src/main/java/Config.kt | 3 + sentry-apollo-3/api/sentry-apollo-3.api | 28 ++ sentry-apollo-3/build.gradle.kts | 79 ++++++ .../main/java/io/sentry/apollo3/Extensions.kt | 26 ++ .../apollo3/SentryApollo3HttpInterceptor.kt | 46 ++++ .../apollo3/SentryApollo3Interceptor.kt | 237 +++++++++++++++++ .../io/sentry/apollo3/LaunchDetailsQuery.kt | 88 +++++++ .../apollo3/SentryApollo3InterceptorTest.kt | 247 ++++++++++++++++++ .../LaunchDetailsQuery_ResponseAdapter.kt | 166 ++++++++++++ .../LaunchDetailsQuery_VariablesAdapter.kt | 28 ++ .../LaunchDetailsQuerySelections.kt | 82 ++++++ .../io/sentry/apollo3/type/GraphQLBoolean.kt | 17 ++ .../java/io/sentry/apollo3/type/GraphQLID.kt | 20 ++ .../io/sentry/apollo3/type/GraphQLString.kt | 18 ++ .../java/io/sentry/apollo3/type/Launch.kt | 14 + .../java/io/sentry/apollo3/type/Mission.kt | 14 + .../test/java/io/sentry/apollo3/type/Query.kt | 14 + .../java/io/sentry/apollo3/type/Rocket.kt | 14 + .../org.mockito.plugins.MockMaker | 1 + settings.gradle.kts | 1 + 22 files changed, 1145 insertions(+) create mode 100644 sentry-apollo-3/api/sentry-apollo-3.api create mode 100644 sentry-apollo-3/build.gradle.kts create mode 100644 sentry-apollo-3/src/main/java/io/sentry/apollo3/Extensions.kt create mode 100644 sentry-apollo-3/src/main/java/io/sentry/apollo3/SentryApollo3HttpInterceptor.kt create mode 100644 sentry-apollo-3/src/main/java/io/sentry/apollo3/SentryApollo3Interceptor.kt create mode 100644 sentry-apollo-3/src/test/java/io/sentry/apollo3/LaunchDetailsQuery.kt create mode 100644 sentry-apollo-3/src/test/java/io/sentry/apollo3/SentryApollo3InterceptorTest.kt create mode 100644 sentry-apollo-3/src/test/java/io/sentry/apollo3/adapter/LaunchDetailsQuery_ResponseAdapter.kt create mode 100644 sentry-apollo-3/src/test/java/io/sentry/apollo3/adapter/LaunchDetailsQuery_VariablesAdapter.kt create mode 100644 sentry-apollo-3/src/test/java/io/sentry/apollo3/selections/LaunchDetailsQuerySelections.kt create mode 100644 sentry-apollo-3/src/test/java/io/sentry/apollo3/type/GraphQLBoolean.kt create mode 100644 sentry-apollo-3/src/test/java/io/sentry/apollo3/type/GraphQLID.kt create mode 100644 sentry-apollo-3/src/test/java/io/sentry/apollo3/type/GraphQLString.kt create mode 100644 sentry-apollo-3/src/test/java/io/sentry/apollo3/type/Launch.kt create mode 100644 sentry-apollo-3/src/test/java/io/sentry/apollo3/type/Mission.kt create mode 100644 sentry-apollo-3/src/test/java/io/sentry/apollo3/type/Query.kt create mode 100644 sentry-apollo-3/src/test/java/io/sentry/apollo3/type/Rocket.kt create mode 100644 sentry-apollo-3/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker 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/buildSrc/src/main/java/Config.kt b/buildSrc/src/main/java/Config.kt index e7e99bddd8..9fa8519c65 100644 --- a/buildSrc/src/main/java/Config.kt +++ b/buildSrc/src/main/java/Config.kt @@ -103,6 +103,8 @@ object Config { val graphQlJava = "com.graphql-java:graphql-java:17.3" val kotlinReflect = "org.jetbrains.kotlin:kotlin-reflect" + + val apollo3 = "com.apollographql.apollo3:apollo-runtime:3.3.0" } object AnnotationProcessors { @@ -124,6 +126,7 @@ object Config { 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" } 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..b3569cce3f --- /dev/null +++ b/sentry-apollo-3/api/sentry-apollo-3.api @@ -0,0 +1,28 @@ +public final class io/sentry/apollo3/ExtensionsKt { + public static final fun getHttpInfo (Lcom/apollographql/apollo3/api/ApolloResponse;)Lcom/apollographql/apollo3/network/http/HttpInfo; + public static final fun getScalarAdapters (Lcom/apollographql/apollo3/api/ApolloRequest;)Lcom/apollographql/apollo3/api/CustomScalarAdapters; + public static final fun headersContentLength (Lcom/apollographql/apollo3/api/http/HttpResponse;)J + public static final fun toLongOrDefault (Ljava/lang/String;J)J +} + +public final class io/sentry/apollo3/SentryApollo3HttpInterceptor : com/apollographql/apollo3/network/http/HttpInterceptor { + public fun ()V + public fun (Lio/sentry/IHub;)V + public synthetic fun (Lio/sentry/IHub;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 final class io/sentry/apollo3/SentryApollo3Interceptor : com/apollographql/apollo3/interceptor/ApolloInterceptor { + public fun ()V + public fun (Lio/sentry/IHub;)V + public fun (Lio/sentry/IHub;Lio/sentry/apollo3/SentryApollo3Interceptor$BeforeSpanCallback;)V + public synthetic fun (Lio/sentry/IHub;Lio/sentry/apollo3/SentryApollo3Interceptor$BeforeSpanCallback;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Lio/sentry/apollo3/SentryApollo3Interceptor$BeforeSpanCallback;)V + public fun intercept (Lcom/apollographql/apollo3/api/ApolloRequest;Lcom/apollographql/apollo3/interceptor/ApolloInterceptorChain;)Lkotlinx/coroutines/flow/Flow; +} + +public abstract interface class io/sentry/apollo3/SentryApollo3Interceptor$BeforeSpanCallback { + public abstract fun execute (Lio/sentry/ISpan;Lcom/apollographql/apollo3/api/ApolloRequest;Lcom/apollographql/apollo3/api/ApolloResponse;)Lio/sentry/ISpan; +} + diff --git a/sentry-apollo-3/build.gradle.kts b/sentry-apollo-3/build.gradle.kts new file mode 100644 index 0000000000..0215b3c51e --- /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.apollo3) + + 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/Extensions.kt b/sentry-apollo-3/src/main/java/io/sentry/apollo3/Extensions.kt new file mode 100644 index 0000000000..212beb1880 --- /dev/null +++ b/sentry-apollo-3/src/main/java/io/sentry/apollo3/Extensions.kt @@ -0,0 +1,26 @@ +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.Operation +import com.apollographql.apollo3.api.http.HttpResponse +import com.apollographql.apollo3.network.http.HttpInfo + +val ApolloResponse.httpInfo + get() = executionContext[HttpInfo] + +val ApolloRequest.scalarAdapters + get() = executionContext[CustomScalarAdapters] + +fun HttpResponse.headersContentLength(): Long { + return headers.firstOrNull { it.name == "Content-Length" }?.value?.toLongOrDefault(-1L) ?: -1L +} + +fun String.toLongOrDefault(defaultValue: Long): Long { + return try { + toLong() + } catch (_: NumberFormatException) { + defaultValue + } +} 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..240946305e --- /dev/null +++ b/sentry-apollo-3/src/main/java/io/sentry/apollo3/SentryApollo3HttpInterceptor.kt @@ -0,0 +1,46 @@ +package io.sentry.apollo3 + +import com.apollographql.apollo3.api.http.HttpRequest +import com.apollographql.apollo3.api.http.HttpResponse +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.TypeCheckHint + +class SentryApollo3HttpInterceptor(private val hub: IHub = HubAdapter.getInstance()) : + HttpInterceptor { + override suspend fun intercept( + request: HttpRequest, + chain: HttpInterceptorChain + ): HttpResponse { + val httpResponse = chain.proceed(request) + + val breadcrumb = Breadcrumb.http(request.url, request.method.name, httpResponse.statusCode) + + request.body?.contentLength.ifHasValidLength { contentLength -> + breadcrumb.setData("request_body_size", contentLength) + } + + httpResponse.headersContentLength().ifHasValidLength { contentLength -> + breadcrumb.setData("response_body_size", contentLength) + } + + val hint = Hint().also { + it.set(TypeCheckHint.APOLLO_REQUEST, request) + it.set(TypeCheckHint.APOLLO_RESPONSE, httpResponse) + } + + hub.addBreadcrumb(breadcrumb, hint) + + return httpResponse + } + + private fun Long?.ifHasValidLength(fn: (Long) -> Unit) { + if (this != null && this != -1L) { + fn.invoke(this) + } + } +} 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..ae615c64ed --- /dev/null +++ b/sentry-apollo-3/src/main/java/io/sentry/apollo3/SentryApollo3Interceptor.kt @@ -0,0 +1,237 @@ +package io.sentry.apollo3 + +import com.apollographql.apollo3.api.ApolloRequest +import com.apollographql.apollo3.api.ApolloResponse +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.exception.ApolloHttpException +import com.apollographql.apollo3.interceptor.ApolloInterceptor +import com.apollographql.apollo3.interceptor.ApolloInterceptorChain +import io.sentry.HubAdapter +import io.sentry.IHub +import io.sentry.ISpan +import io.sentry.SentryLevel +import io.sentry.SpanStatus +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.onEach + +class SentryApollo3Interceptor( + private val hub: IHub = HubAdapter.getInstance(), + private val beforeSpan: BeforeSpanCallback? = null +) : ApolloInterceptor { + + constructor(hub: IHub) : this(hub, null) + constructor(beforeSpan: BeforeSpanCallback) : this( + HubAdapter.getInstance(), + beforeSpan + ) + + override fun intercept( + request: ApolloRequest, + chain: ApolloInterceptorChain + ): Flow> { + val activeSpan = hub.span + if (activeSpan == null) { + return chain.proceed(request) + } else { + val span = startChild(request, activeSpan) + val sentryTraceHeader = span.toSentryTrace() + + val modifiedRequest = + request.newBuilder().addHttpHeader(sentryTraceHeader.name, sentryTraceHeader.value) + .build() + span.setData("operationId", modifiedRequest.operation.id()) + modifiedRequest.scalarAdapters?.let { + span.setData( + "variables", + modifiedRequest.operation.variables(it).valueMap.toString() + ) + } + + return chain.proceed(modifiedRequest).onEach { + span.status = + SpanStatus.fromHttpStatusCode(it.httpInfo?.statusCode, SpanStatus.UNKNOWN) + finish(span, request, it) + }.catch { throwable -> + span.apply { + status = + if (throwable is ApolloHttpException) SpanStatus.fromHttpStatusCode( + throwable.statusCode, + SpanStatus.INTERNAL_ERROR + ) else SpanStatus.INTERNAL_ERROR + } + finish(span, request) + throw throwable + } + } + } + + private fun startChild( + request: ApolloRequest, + activeSpan: ISpan + ): ISpan { + val operation = request.operation.name() + val operationType = when (request.operation) { + is Query -> "query" + is Mutation -> "mutation" + is Subscription -> "subscription" + else -> request.operation.javaClass.simpleName + } + val description = "$operationType $operation" + return activeSpan.startChild(operation, description) + } + + private fun finish(span: ISpan, request: ApolloRequest, response: ApolloResponse? = null) { + var newSpan: ISpan = span + if (beforeSpan != null) { + try { + newSpan = beforeSpan.execute(span, request, response) + } catch (e: Exception) { + hub.options.logger.log(SentryLevel.ERROR, "An error occurred while executing beforeSpan on ApolloInterceptor", e) + } + } + newSpan.finish() + } + + /** + * The BeforeSpan callback + */ + 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: ApolloRequest, response: ApolloResponse?): ISpan + } +} +// +// class SentryApolloInterceptor( +// private val hub: IHub = HubAdapter.getInstance(), +// private val beforeSpan: BeforeSpanCallback? = null +// ) : ApolloInterceptor { +// +// constructor(hub: IHub) : this(hub, null) +// constructor(beforeSpan: BeforeSpanCallback) : this(HubAdapter.getInstance(), beforeSpan) +// +// override fun interceptAsync(request: InterceptorRequest, chain: ApolloInterceptorChain, dispatcher: Executor, callBack: CallBack) { +// val activeSpan = hub.span +// if (activeSpan == null) { +// chain.proceedAsync(request, dispatcher, callBack) +// } else { +// val span = startChild(request, activeSpan) +// val sentryTraceHeader = span.toSentryTrace() +// +// // we have no access to URI, no way to verify tracing origins +// val headers = request.requestHeaders.toBuilder().addHeader(sentryTraceHeader.name, sentryTraceHeader.value).build() +// val requestWithHeader = request.toBuilder().requestHeaders(headers).build() +// span.setData("operationId", requestWithHeader.operation.operationId()) +// span.setData("variables", requestWithHeader.operation.variables().valueMap().toString()) +// +// chain.proceedAsync( +// requestWithHeader, dispatcher, +// object : CallBack { +// override fun onResponse(response: InterceptorResponse) { +// // onResponse is called only for statuses 2xx +// span.status = response.httpResponse.map { SpanStatus.fromHttpStatusCode(it.code(), SpanStatus.UNKNOWN) } +// .or(SpanStatus.UNKNOWN) +// +// finish(span, requestWithHeader, response) +// callBack.onResponse(response) +// } +// +// override fun onFetch(sourceType: FetchSourceType) { +// callBack.onFetch(sourceType) +// } +// +// override fun onFailure(e: ApolloException) { +// span.apply { +// status = if (e is ApolloHttpException) SpanStatus.fromHttpStatusCode(e.code(), SpanStatus.INTERNAL_ERROR) else SpanStatus.INTERNAL_ERROR +// throwable = e +// } +// finish(span, requestWithHeader) +// callBack.onFailure(e) +// } +// +// override fun onCompleted() { +// callBack.onCompleted() +// } +// } +// ) +// } +// } +// +// override fun dispose() {} +// +// private fun startChild(request: InterceptorRequest, activeSpan: ISpan): ISpan { +// val operation = request.operation.name().name() +// val operationType = when (request.operation) { +// is Query -> "query" +// is Mutation -> "mutation" +// is Subscription -> "subscription" +// else -> request.operation.javaClass.simpleName +// } +// val description = "$operationType $operation" +// return activeSpan.startChild(operation, description) +// } +// +// private fun finish(span: ISpan, request: InterceptorRequest, response: InterceptorResponse? = null) { +// var newSpan: ISpan = span +// if (beforeSpan != null) { +// try { +// newSpan = beforeSpan.execute(span, request, response) +// } catch (e: Exception) { +// hub.options.logger.log(SentryLevel.ERROR, "An error occurred while executing beforeSpan on ApolloInterceptor", e) +// } +// } +// newSpan.finish() +// +// response?.let { +// if (it.httpResponse.isPresent) { +// val httpResponse = it.httpResponse.get() +// val httpRequest = httpResponse.request() +// +// val breadcrumb = Breadcrumb.http(httpRequest.url().toString(), httpRequest.method(), httpResponse.code()) +// +// httpRequest.body()?.contentLength().ifHasValidLength { contentLength -> +// breadcrumb.setData("request_body_size", contentLength) +// } +// httpResponse.body()?.contentLength().ifHasValidLength { contentLength -> +// breadcrumb.setData("response_body_size", contentLength) +// } +// +// val hint = Hint().also { +// it.set(APOLLO_REQUEST, httpRequest) +// it.set(APOLLO_RESPONSE, httpResponse) +// } +// hub.addBreadcrumb(breadcrumb, hint) +// } +// } +// } +// +// 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 HTTP request executed by okHttp +// * @param response the HTTP response received by okHttp +// */ +// fun execute(span: ISpan, request: InterceptorRequest, response: InterceptorResponse?): ISpan +// } +// } 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..300fc64d0d --- /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 com.example.rocketreserver.adapter.LaunchDetailsQuery_ResponseAdapter +import com.example.rocketreserver.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..dbb4c6e8d7 --- /dev/null +++ b/sentry-apollo-3/src/test/java/io/sentry/apollo3/SentryApollo3InterceptorTest.kt @@ -0,0 +1,247 @@ +package io.sentry.apollo3 + +import com.apollographql.apollo3.ApolloClient +import com.apollographql.apollo3.api.ApolloRequest +import com.apollographql.apollo3.api.ApolloResponse +import com.apollographql.apollo3.api.Operation +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.ISpan +import io.sentry.ITransaction +import io.sentry.SentryOptions +import io.sentry.SentryTraceHeader +import io.sentry.SentryTracer +import io.sentry.SpanStatus +import io.sentry.TraceState +import io.sentry.TransactionContext +import io.sentry.apollo3.SentryApollo3Interceptor.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 SentryApollo3InterceptorTest { + + class Fixture { + val server = MockWebServer() + val hub = mock() + private var interceptor = SentryApollo3Interceptor(hub) + 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, + useHttpInterceptor: Boolean = false, + beforeSpan: BeforeSpanCallback? = null + ): ApolloClient { + whenever(hub.options).thenReturn(SentryOptions()) + + server.enqueue( + MockResponse() + .setBody(responseBody) + .setSocketPolicy(socketPolicy) + .setResponseCode(httpStatusCode) + ) + + if (beforeSpan != null) { + interceptor = SentryApollo3Interceptor(hub, beforeSpan) + } + val builder = ApolloClient.builder() + .serverUrl(server.url("/").toString()) + .addInterceptor(interceptor) + + if (useHttpInterceptor) { + builder.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]) + } + + @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]) + } + + @Test + fun `customizer modifies span`() { + executeQuery( + + fixture.getSut( + beforeSpan = object : BeforeSpanCallback { + override fun execute( + span: ISpan, + request: ApolloRequest, + response: ApolloResponse? + ): ISpan { + span.description = "overwritten description" + return 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 `when customizer throws, exception is handled`() { + executeQuery( + fixture.getSut( + beforeSpan = object : BeforeSpanCallback { + override fun execute( + span: ISpan, + request: ApolloRequest, + response: ApolloResponse? + ): ISpan { + 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(useHttpInterceptor = true)) + 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) + assertEquals("query LaunchDetails", httpClientSpan.description) + assertNotNull(httpClientSpan.data) { + assertNotNull(it["operationId"]) + assertEquals("{id=83}", it["variables"]) + } + } + + private fun executeQuery(sut: ApolloClient = fixture.getSut(), isSpanActive: Boolean = true) = runBlocking { + var tx: ITransaction? = null + if (isSpanActive) { + tx = SentryTracer(TransactionContext("op", "desc", 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..ffa210160c --- /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 com.example.rocketreserver.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..ecfff40762 --- /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 com.example.rocketreserver.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 + +public object LaunchDetailsQuery_VariablesAdapter : Adapter { + public override fun fromJson(reader: JsonReader, customScalarAdapters: CustomScalarAdapters): + LaunchDetailsQuery = throw IllegalStateException("Input type used in output position") + + public 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/settings.gradle.kts b/settings.gradle.kts index a017e9bd69..69431225c8 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -13,6 +13,7 @@ include( "sentry-android-okhttp", "sentry-android-fragment", "sentry-apollo", + "sentry-apollo-3", "sentry-test-support", "sentry-log4j2", "sentry-logback", From 2418613465b1a04bd0f17819c05c6cefc979b64a Mon Sep 17 00:00:00 2001 From: Lukas Bloder Date: Fri, 17 Jun 2022 01:41:37 +0200 Subject: [PATCH 02/12] add http interceptor for apollo3, extend ISerializer to allow for deserialization with custom Deserlializer, support batched apollo requests --- buildSrc/src/main/java/Config.kt | 1 + sentry-apollo-3/api/sentry-apollo-3.api | 67 ++++- sentry-apollo-3/build.gradle.kts | 1 + .../apollo3/ApolloRequestBodyContent.kt | 57 +++++ .../main/java/io/sentry/apollo3/Extensions.kt | 11 - .../apollo3/SentryApollo3HttpInterceptor.kt | 166 ++++++++++-- .../apollo3/SentryApollo3Interceptor.kt | 237 ------------------ .../io/sentry/apollo3/LaunchDetailsQuery.kt | 4 +- .../apollo3/SentryApollo3InterceptorTest.kt | 53 ++-- .../LaunchDetailsQuery_ResponseAdapter.kt | 2 +- .../LaunchDetailsQuery_VariablesAdapter.kt | 8 +- sentry/api/sentry.api | 2 + .../src/main/java/io/sentry/ISerializer.java | 3 + .../main/java/io/sentry/JsonSerializer.java | 22 +- .../main/java/io/sentry/NoOpSerializer.java | 6 + 15 files changed, 315 insertions(+), 325 deletions(-) create mode 100644 sentry-apollo-3/src/main/java/io/sentry/apollo3/ApolloRequestBodyContent.kt delete mode 100644 sentry-apollo-3/src/main/java/io/sentry/apollo3/SentryApollo3Interceptor.kt diff --git a/buildSrc/src/main/java/Config.kt b/buildSrc/src/main/java/Config.kt index a55056b9e3..ade28d7681 100644 --- a/buildSrc/src/main/java/Config.kt +++ b/buildSrc/src/main/java/Config.kt @@ -106,6 +106,7 @@ object Config { val kotlinReflect = "org.jetbrains.kotlin:kotlin-reflect" val apollo3 = "com.apollographql.apollo3:apollo-runtime:3.3.0" + val apollo3cache = "com.apollographql.apollo3:apollo-normalized-cache:3.3.0" } object AnnotationProcessors { diff --git a/sentry-apollo-3/api/sentry-apollo-3.api b/sentry-apollo-3/api/sentry-apollo-3.api index b3569cce3f..464b7cd278 100644 --- a/sentry-apollo-3/api/sentry-apollo-3.api +++ b/sentry-apollo-3/api/sentry-apollo-3.api @@ -1,28 +1,69 @@ +public final class io/sentry/apollo3/ApolloRequestBodyContent { + public fun ()V + public fun (Ljava/lang/String;Ljava/util/Map;)V + public synthetic fun (Ljava/lang/String;Ljava/util/Map;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Ljava/util/Map; + public final fun copy (Ljava/lang/String;Ljava/util/Map;)Lio/sentry/apollo3/ApolloRequestBodyContent; + public static synthetic fun copy$default (Lio/sentry/apollo3/ApolloRequestBodyContent;Ljava/lang/String;Ljava/util/Map;ILjava/lang/Object;)Lio/sentry/apollo3/ApolloRequestBodyContent; + public fun equals (Ljava/lang/Object;)Z + public final fun getQuery ()Ljava/lang/String; + public final fun getVariables ()Ljava/util/Map; + public fun hashCode ()I + public final fun setQuery (Ljava/lang/String;)V + public final fun setVariables (Ljava/util/Map;)V + public fun toString ()Ljava/lang/String; +} + +public final class io/sentry/apollo3/ApolloRequestBodyContent$Deserializer : io/sentry/JsonDeserializer { + public static final field INSTANCE Lio/sentry/apollo3/ApolloRequestBodyContent$Deserializer; + public fun deserialize (Lio/sentry/JsonObjectReader;Lio/sentry/ILogger;)Lio/sentry/apollo3/ApolloRequestBodyContent; + public synthetic fun deserialize (Lio/sentry/JsonObjectReader;Lio/sentry/ILogger;)Ljava/lang/Object; +} + +public final class io/sentry/apollo3/ApolloRequestBodyContent$JsonKeys { + public static final field INSTANCE Lio/sentry/apollo3/ApolloRequestBodyContent$JsonKeys; + public static final field QUERY Ljava/lang/String; + public static final field VARIABLES Ljava/lang/String; +} + +public final class io/sentry/apollo3/ApolloRequestBodyListContent { + public fun (Ljava/util/List;)V + public final fun component1 ()Ljava/util/List; + public final fun copy (Ljava/util/List;)Lio/sentry/apollo3/ApolloRequestBodyListContent; + public static synthetic fun copy$default (Lio/sentry/apollo3/ApolloRequestBodyListContent;Ljava/util/List;ILjava/lang/Object;)Lio/sentry/apollo3/ApolloRequestBodyListContent; + public fun equals (Ljava/lang/Object;)Z + public final fun getQueries ()Ljava/util/List; + public fun hashCode ()I + public final fun setQueries (Ljava/util/List;)V + public fun toString ()Ljava/lang/String; +} + +public final class io/sentry/apollo3/ApolloRequestBodyListContent$Deserializer : io/sentry/JsonDeserializer { + public static final field INSTANCE Lio/sentry/apollo3/ApolloRequestBodyListContent$Deserializer; + public fun deserialize (Lio/sentry/JsonObjectReader;Lio/sentry/ILogger;)Lio/sentry/apollo3/ApolloRequestBodyListContent; + public synthetic fun deserialize (Lio/sentry/JsonObjectReader;Lio/sentry/ILogger;)Ljava/lang/Object; +} + public final class io/sentry/apollo3/ExtensionsKt { - public static final fun getHttpInfo (Lcom/apollographql/apollo3/api/ApolloResponse;)Lcom/apollographql/apollo3/network/http/HttpInfo; - public static final fun getScalarAdapters (Lcom/apollographql/apollo3/api/ApolloRequest;)Lcom/apollographql/apollo3/api/CustomScalarAdapters; public static final fun headersContentLength (Lcom/apollographql/apollo3/api/http/HttpResponse;)J public static final fun toLongOrDefault (Ljava/lang/String;J)J } public final class io/sentry/apollo3/SentryApollo3HttpInterceptor : com/apollographql/apollo3/network/http/HttpInterceptor { + public static final field Companion Lio/sentry/apollo3/SentryApollo3HttpInterceptor$Companion; public fun ()V - public fun (Lio/sentry/IHub;)V - public synthetic fun (Lio/sentry/IHub;ILkotlin/jvm/internal/DefaultConstructorMarker;)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 final class io/sentry/apollo3/SentryApollo3Interceptor : com/apollographql/apollo3/interceptor/ApolloInterceptor { - public fun ()V - public fun (Lio/sentry/IHub;)V - public fun (Lio/sentry/IHub;Lio/sentry/apollo3/SentryApollo3Interceptor$BeforeSpanCallback;)V - public synthetic fun (Lio/sentry/IHub;Lio/sentry/apollo3/SentryApollo3Interceptor$BeforeSpanCallback;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public fun (Lio/sentry/apollo3/SentryApollo3Interceptor$BeforeSpanCallback;)V - public fun intercept (Lcom/apollographql/apollo3/api/ApolloRequest;Lcom/apollographql/apollo3/interceptor/ApolloInterceptorChain;)Lkotlinx/coroutines/flow/Flow; +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 abstract interface class io/sentry/apollo3/SentryApollo3Interceptor$BeforeSpanCallback { - public abstract fun execute (Lio/sentry/ISpan;Lcom/apollographql/apollo3/api/ApolloRequest;Lcom/apollographql/apollo3/api/ApolloResponse;)Lio/sentry/ISpan; +public final class io/sentry/apollo3/SentryApollo3HttpInterceptor$Companion { + public final fun getKNOWN_OPERATIONS ()Ljava/util/List; } diff --git a/sentry-apollo-3/build.gradle.kts b/sentry-apollo-3/build.gradle.kts index 0215b3c51e..dedcb9cd6f 100644 --- a/sentry-apollo-3/build.gradle.kts +++ b/sentry-apollo-3/build.gradle.kts @@ -25,6 +25,7 @@ dependencies { api(projects.sentryKotlinExtensions) implementation(Config.Libs.apollo3) + implementation(Config.Libs.apollo3cache) compileOnly(Config.CompileOnly.nopen) errorprone(Config.CompileOnly.nopenChecker) diff --git a/sentry-apollo-3/src/main/java/io/sentry/apollo3/ApolloRequestBodyContent.kt b/sentry-apollo-3/src/main/java/io/sentry/apollo3/ApolloRequestBodyContent.kt new file mode 100644 index 0000000000..3b65f8def7 --- /dev/null +++ b/sentry-apollo-3/src/main/java/io/sentry/apollo3/ApolloRequestBodyContent.kt @@ -0,0 +1,57 @@ +package io.sentry.apollo3 + +import io.sentry.ILogger +import io.sentry.JsonDeserializer +import io.sentry.JsonObjectReader +import io.sentry.vendor.gson.stream.JsonToken + +data class ApolloRequestBodyListContent( + var queries: List? +) { + object Deserializer : JsonDeserializer { + override fun deserialize( + reader: JsonObjectReader, + logger: ILogger + ): ApolloRequestBodyListContent { + reader.beginArray() + val requestBodyListContent = ApolloRequestBodyListContent( + reader.nextList( + logger, + ApolloRequestBodyContent.Deserializer + ) + ) + reader.endArray() + + return requestBodyListContent + } + } +} + +data class ApolloRequestBodyContent( + var query: String = "", + var variables: Map? = null +) { + // JsonSerializable + object JsonKeys { + const val QUERY = "query" + const val VARIABLES = "variables" + } + + object Deserializer : JsonDeserializer { + override fun deserialize(reader: JsonObjectReader, logger: ILogger): ApolloRequestBodyContent { + val content = ApolloRequestBodyContent() + reader.beginObject() + while (reader.peek() == JsonToken.NAME) { + when (val nextName = reader.nextName()) { + JsonKeys.VARIABLES -> content.variables = reader.nextObjectOrNull() as? Map + JsonKeys.QUERY -> content.query = reader.nextString() + else -> { + reader.nextUnknown(logger, mutableMapOf(), nextName) + } + } + } + reader.endObject() + return content + } + } +} diff --git a/sentry-apollo-3/src/main/java/io/sentry/apollo3/Extensions.kt b/sentry-apollo-3/src/main/java/io/sentry/apollo3/Extensions.kt index 212beb1880..7d5899cc1a 100644 --- a/sentry-apollo-3/src/main/java/io/sentry/apollo3/Extensions.kt +++ b/sentry-apollo-3/src/main/java/io/sentry/apollo3/Extensions.kt @@ -1,17 +1,6 @@ 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.Operation import com.apollographql.apollo3.api.http.HttpResponse -import com.apollographql.apollo3.network.http.HttpInfo - -val ApolloResponse.httpInfo - get() = executionContext[HttpInfo] - -val ApolloRequest.scalarAdapters - get() = executionContext[CustomScalarAdapters] fun HttpResponse.headersContentLength(): Long { return headers.firstOrNull { it.name == "Content-Length" }?.value?.toLongOrDefault(-1L) ?: -1L 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 index 240946305e..208f83b065 100644 --- a/sentry-apollo-3/src/main/java/io/sentry/apollo3/SentryApollo3HttpInterceptor.kt +++ b/sentry-apollo-3/src/main/java/io/sentry/apollo3/SentryApollo3HttpInterceptor.kt @@ -2,40 +2,63 @@ package io.sentry.apollo3 import com.apollographql.apollo3.api.http.HttpRequest import com.apollographql.apollo3.api.http.HttpResponse +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 +import okio.Buffer +import java.io.InputStreamReader -class SentryApollo3HttpInterceptor(private val hub: IHub = HubAdapter.getInstance()) : +class SentryApollo3HttpInterceptor(private val hub: IHub = HubAdapter.getInstance(), private val beforeSpan: BeforeSpanCallback? = null) : HttpInterceptor { + override suspend fun intercept( request: HttpRequest, chain: HttpInterceptorChain ): HttpResponse { - val httpResponse = chain.proceed(request) + val activeSpan = hub.span + return if (activeSpan == null) { + chain.proceed(request) + } else { + val span = startChild(request, activeSpan) - val breadcrumb = Breadcrumb.http(request.url, request.method.name, httpResponse.statusCode) + val requestBuilder = request.newBuilder() - request.body?.contentLength.ifHasValidLength { contentLength -> - breadcrumb.setData("request_body_size", contentLength) - } + if (TracingOrigins.contain(hub.options.tracingOrigins, request.url)) { + val sentryTraceHeader = span.toSentryTrace() + val baggageHeader = span.toBaggageHeader() + requestBuilder.addHeader(sentryTraceHeader.name, sentryTraceHeader.value) - httpResponse.headersContentLength().ifHasValidLength { contentLength -> - breadcrumb.setData("response_body_size", contentLength) - } + baggageHeader?.let { + requestBuilder.addHeader(it.name, it.value) + } + } - val hint = Hint().also { - it.set(TypeCheckHint.APOLLO_REQUEST, request) - it.set(TypeCheckHint.APOLLO_RESPONSE, httpResponse) - } + val modifiedRequest = requestBuilder.build() - hub.addBreadcrumb(breadcrumb, hint) + return try { + val httpResponse = chain.proceed(modifiedRequest) + span.status = SpanStatus.fromHttpStatusCode(httpResponse.statusCode, SpanStatus.UNKNOWN) + finish(span, modifiedRequest, httpResponse) - return httpResponse + httpResponse + } catch (e: Exception) { + when (e) { + is ApolloNetworkException -> span.status = SpanStatus.INTERNAL_ERROR + else -> SpanStatus.UNKNOWN + } + finish(span, modifiedRequest) + throw e + } + } } private fun Long?.ifHasValidLength(fn: (Long) -> Unit) { @@ -43,4 +66,117 @@ class SentryApollo3HttpInterceptor(private val hub: IHub = HubAdapter.getInstanc fn.invoke(this) } } + + private fun startChild(request: HttpRequest, activeSpan: ISpan): ISpan { + val buffer = Buffer() + request.body?.writeTo(buffer) + + val reader = InputStreamReader(buffer.inputStream()) + + val content = try { + hub.options.serializer.deserialize( + reader, + ApolloRequestBodyContent::class.java, + ApolloRequestBodyContent.Deserializer + ) + } catch (e: Exception) { + null + } ?: try { + hub.options.serializer.deserialize( + reader, + ApolloRequestBodyListContent::class.java, + ApolloRequestBodyListContent.Deserializer + ) + } catch (e: Exception) { + null + } + + val span = when (content) { + is ApolloRequestBodyContent -> createSpanFromBodyContent(activeSpan, request, content) + is ApolloRequestBodyListContent -> createSpanFromBodyListContent(activeSpan, content) + else -> activeSpan.startChild("unkown") + } + + val operationId = request.headers.firstOrNull { it.name == "X-APOLLO-OPERATION-ID" }?.value ?: "unknown" + + return span.apply { + setData("operationId", operationId) + } + } + + private fun createSpanFromBodyListContent(activeSpan: ISpan, content: ApolloRequestBodyListContent): ISpan { + return activeSpan.startChild("Batched Operation").apply { + setData("isBatched", true) + content.queries?.forEachIndexed { index, item -> + setData("variables $index", item.variables.toString()) + setData("variables $index", item.variables.toString()) + } + } + } + + private fun createSpanFromBodyContent(activeSpan: ISpan, request: HttpRequest, content: ApolloRequestBodyContent): ISpan { + val operationType = parseOperationType(content) + val operationName = request.headers.firstOrNull { it.name == "X-APOLLO-OPERATION-NAME" }?.value ?: "unknown" + val description = "$operationType $operationName" + + return activeSpan.startChild(operationName, description).apply { + setData("isBatched", false) + setData("variables", content.variables.toString()) + } + } + + private fun finish(span: ISpan, request: HttpRequest, response: HttpResponse? = null) { + var newSpan: ISpan = span + if (beforeSpan != null) { + try { + newSpan = beforeSpan.execute(span, request, response) + } catch (e: Exception) { + hub.options.logger.log(SentryLevel.ERROR, "An error occurred while executing beforeSpan on ApolloInterceptor", e) + } + } + newSpan.finish() + + response?.let { httpResponse -> + val breadcrumb = + Breadcrumb.http(request.url, request.method.name, httpResponse.statusCode) + + request.body?.contentLength.ifHasValidLength { contentLength -> + breadcrumb.setData("request_body_size", contentLength) + } + + httpResponse.headersContentLength().ifHasValidLength { contentLength -> + breadcrumb.setData("response_body_size", contentLength) + } + + val hint = Hint().also { + it.set(TypeCheckHint.APOLLO_REQUEST, request) + it.set(TypeCheckHint.APOLLO_RESPONSE, httpResponse) + } + + hub.addBreadcrumb(breadcrumb, hint) + } + } + + private fun parseOperationType(content: ApolloRequestBodyContent?): String { + val operationPart = content?.query?.takeWhile { !it.isWhitespace() } + return KNOWN_OPERATIONS.firstOrNull { it == operationPart } ?: "unknown" + } + + companion object { + val KNOWN_OPERATIONS = listOf("query", "mutation", "subscription") + } + + /** + * 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 + } } 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 deleted file mode 100644 index ae615c64ed..0000000000 --- a/sentry-apollo-3/src/main/java/io/sentry/apollo3/SentryApollo3Interceptor.kt +++ /dev/null @@ -1,237 +0,0 @@ -package io.sentry.apollo3 - -import com.apollographql.apollo3.api.ApolloRequest -import com.apollographql.apollo3.api.ApolloResponse -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.exception.ApolloHttpException -import com.apollographql.apollo3.interceptor.ApolloInterceptor -import com.apollographql.apollo3.interceptor.ApolloInterceptorChain -import io.sentry.HubAdapter -import io.sentry.IHub -import io.sentry.ISpan -import io.sentry.SentryLevel -import io.sentry.SpanStatus -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.catch -import kotlinx.coroutines.flow.onEach - -class SentryApollo3Interceptor( - private val hub: IHub = HubAdapter.getInstance(), - private val beforeSpan: BeforeSpanCallback? = null -) : ApolloInterceptor { - - constructor(hub: IHub) : this(hub, null) - constructor(beforeSpan: BeforeSpanCallback) : this( - HubAdapter.getInstance(), - beforeSpan - ) - - override fun intercept( - request: ApolloRequest, - chain: ApolloInterceptorChain - ): Flow> { - val activeSpan = hub.span - if (activeSpan == null) { - return chain.proceed(request) - } else { - val span = startChild(request, activeSpan) - val sentryTraceHeader = span.toSentryTrace() - - val modifiedRequest = - request.newBuilder().addHttpHeader(sentryTraceHeader.name, sentryTraceHeader.value) - .build() - span.setData("operationId", modifiedRequest.operation.id()) - modifiedRequest.scalarAdapters?.let { - span.setData( - "variables", - modifiedRequest.operation.variables(it).valueMap.toString() - ) - } - - return chain.proceed(modifiedRequest).onEach { - span.status = - SpanStatus.fromHttpStatusCode(it.httpInfo?.statusCode, SpanStatus.UNKNOWN) - finish(span, request, it) - }.catch { throwable -> - span.apply { - status = - if (throwable is ApolloHttpException) SpanStatus.fromHttpStatusCode( - throwable.statusCode, - SpanStatus.INTERNAL_ERROR - ) else SpanStatus.INTERNAL_ERROR - } - finish(span, request) - throw throwable - } - } - } - - private fun startChild( - request: ApolloRequest, - activeSpan: ISpan - ): ISpan { - val operation = request.operation.name() - val operationType = when (request.operation) { - is Query -> "query" - is Mutation -> "mutation" - is Subscription -> "subscription" - else -> request.operation.javaClass.simpleName - } - val description = "$operationType $operation" - return activeSpan.startChild(operation, description) - } - - private fun finish(span: ISpan, request: ApolloRequest, response: ApolloResponse? = null) { - var newSpan: ISpan = span - if (beforeSpan != null) { - try { - newSpan = beforeSpan.execute(span, request, response) - } catch (e: Exception) { - hub.options.logger.log(SentryLevel.ERROR, "An error occurred while executing beforeSpan on ApolloInterceptor", e) - } - } - newSpan.finish() - } - - /** - * The BeforeSpan callback - */ - 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: ApolloRequest, response: ApolloResponse?): ISpan - } -} -// -// class SentryApolloInterceptor( -// private val hub: IHub = HubAdapter.getInstance(), -// private val beforeSpan: BeforeSpanCallback? = null -// ) : ApolloInterceptor { -// -// constructor(hub: IHub) : this(hub, null) -// constructor(beforeSpan: BeforeSpanCallback) : this(HubAdapter.getInstance(), beforeSpan) -// -// override fun interceptAsync(request: InterceptorRequest, chain: ApolloInterceptorChain, dispatcher: Executor, callBack: CallBack) { -// val activeSpan = hub.span -// if (activeSpan == null) { -// chain.proceedAsync(request, dispatcher, callBack) -// } else { -// val span = startChild(request, activeSpan) -// val sentryTraceHeader = span.toSentryTrace() -// -// // we have no access to URI, no way to verify tracing origins -// val headers = request.requestHeaders.toBuilder().addHeader(sentryTraceHeader.name, sentryTraceHeader.value).build() -// val requestWithHeader = request.toBuilder().requestHeaders(headers).build() -// span.setData("operationId", requestWithHeader.operation.operationId()) -// span.setData("variables", requestWithHeader.operation.variables().valueMap().toString()) -// -// chain.proceedAsync( -// requestWithHeader, dispatcher, -// object : CallBack { -// override fun onResponse(response: InterceptorResponse) { -// // onResponse is called only for statuses 2xx -// span.status = response.httpResponse.map { SpanStatus.fromHttpStatusCode(it.code(), SpanStatus.UNKNOWN) } -// .or(SpanStatus.UNKNOWN) -// -// finish(span, requestWithHeader, response) -// callBack.onResponse(response) -// } -// -// override fun onFetch(sourceType: FetchSourceType) { -// callBack.onFetch(sourceType) -// } -// -// override fun onFailure(e: ApolloException) { -// span.apply { -// status = if (e is ApolloHttpException) SpanStatus.fromHttpStatusCode(e.code(), SpanStatus.INTERNAL_ERROR) else SpanStatus.INTERNAL_ERROR -// throwable = e -// } -// finish(span, requestWithHeader) -// callBack.onFailure(e) -// } -// -// override fun onCompleted() { -// callBack.onCompleted() -// } -// } -// ) -// } -// } -// -// override fun dispose() {} -// -// private fun startChild(request: InterceptorRequest, activeSpan: ISpan): ISpan { -// val operation = request.operation.name().name() -// val operationType = when (request.operation) { -// is Query -> "query" -// is Mutation -> "mutation" -// is Subscription -> "subscription" -// else -> request.operation.javaClass.simpleName -// } -// val description = "$operationType $operation" -// return activeSpan.startChild(operation, description) -// } -// -// private fun finish(span: ISpan, request: InterceptorRequest, response: InterceptorResponse? = null) { -// var newSpan: ISpan = span -// if (beforeSpan != null) { -// try { -// newSpan = beforeSpan.execute(span, request, response) -// } catch (e: Exception) { -// hub.options.logger.log(SentryLevel.ERROR, "An error occurred while executing beforeSpan on ApolloInterceptor", e) -// } -// } -// newSpan.finish() -// -// response?.let { -// if (it.httpResponse.isPresent) { -// val httpResponse = it.httpResponse.get() -// val httpRequest = httpResponse.request() -// -// val breadcrumb = Breadcrumb.http(httpRequest.url().toString(), httpRequest.method(), httpResponse.code()) -// -// httpRequest.body()?.contentLength().ifHasValidLength { contentLength -> -// breadcrumb.setData("request_body_size", contentLength) -// } -// httpResponse.body()?.contentLength().ifHasValidLength { contentLength -> -// breadcrumb.setData("response_body_size", contentLength) -// } -// -// val hint = Hint().also { -// it.set(APOLLO_REQUEST, httpRequest) -// it.set(APOLLO_RESPONSE, httpResponse) -// } -// hub.addBreadcrumb(breadcrumb, hint) -// } -// } -// } -// -// 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 HTTP request executed by okHttp -// * @param response the HTTP response received by okHttp -// */ -// fun execute(span: ISpan, request: InterceptorRequest, response: InterceptorResponse?): ISpan -// } -// } 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 index 300fc64d0d..5e6653edc8 100644 --- a/sentry-apollo-3/src/test/java/io/sentry/apollo3/LaunchDetailsQuery.kt +++ b/sentry-apollo-3/src/test/java/io/sentry/apollo3/LaunchDetailsQuery.kt @@ -6,8 +6,8 @@ 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 com.example.rocketreserver.adapter.LaunchDetailsQuery_ResponseAdapter -import com.example.rocketreserver.adapter.LaunchDetailsQuery_VariablesAdapter +import io.sentry.apollo3.adapter.LaunchDetailsQuery_ResponseAdapter +import io.sentry.apollo3.adapter.LaunchDetailsQuery_VariablesAdapter import io.sentry.apollo3.selections.LaunchDetailsQuerySelections import kotlin.String 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 index dbb4c6e8d7..85a02ad0b6 100644 --- a/sentry-apollo-3/src/test/java/io/sentry/apollo3/SentryApollo3InterceptorTest.kt +++ b/sentry-apollo-3/src/test/java/io/sentry/apollo3/SentryApollo3InterceptorTest.kt @@ -1,9 +1,6 @@ package io.sentry.apollo3 import com.apollographql.apollo3.ApolloClient -import com.apollographql.apollo3.api.ApolloRequest -import com.apollographql.apollo3.api.ApolloResponse -import com.apollographql.apollo3.api.Operation import com.apollographql.apollo3.exception.ApolloException import com.nhaarman.mockitokotlin2.anyOrNull import com.nhaarman.mockitokotlin2.check @@ -12,15 +9,14 @@ import com.nhaarman.mockitokotlin2.verify import com.nhaarman.mockitokotlin2.whenever import io.sentry.Breadcrumb import io.sentry.IHub -import io.sentry.ISpan import io.sentry.ITransaction import io.sentry.SentryOptions import io.sentry.SentryTraceHeader import io.sentry.SentryTracer import io.sentry.SpanStatus -import io.sentry.TraceState +import io.sentry.TraceContext import io.sentry.TransactionContext -import io.sentry.apollo3.SentryApollo3Interceptor.BeforeSpanCallback +import io.sentry.apollo3.SentryApollo3HttpInterceptor.BeforeSpanCallback import io.sentry.protocol.SentryTransaction import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking @@ -37,7 +33,6 @@ class SentryApollo3InterceptorTest { class Fixture { val server = MockWebServer() val hub = mock() - private var interceptor = SentryApollo3Interceptor(hub) private var httpInterceptor = SentryApollo3HttpInterceptor(hub) @SuppressWarnings("LongParameterList") @@ -71,15 +66,17 @@ class SentryApollo3InterceptorTest { ) if (beforeSpan != null) { - interceptor = SentryApollo3Interceptor(hub, beforeSpan) + httpInterceptor = SentryApollo3HttpInterceptor(hub, beforeSpan) } val builder = ApolloClient.builder() .serverUrl(server.url("/").toString()) - .addInterceptor(interceptor) + .addHttpInterceptor(httpInterceptor) +// .addInterceptor(interceptor) +// .addInterceptor(CacheAndNetworkInterceptor) - if (useHttpInterceptor) { - builder.addHttpInterceptor(httpInterceptor) - } +// if (useHttpInterceptor) { +// builder.addHttpInterceptor(httpInterceptor) +// } return builder.build() } @@ -96,7 +93,7 @@ class SentryApollo3InterceptorTest { assertTransactionDetails(it) assertEquals(SpanStatus.OK, it.spans.first().status) }, - anyOrNull(), + anyOrNull(), anyOrNull(), anyOrNull() ) @@ -111,7 +108,7 @@ class SentryApollo3InterceptorTest { assertTransactionDetails(it) assertEquals(SpanStatus.PERMISSION_DENIED, it.spans.first().status) }, - anyOrNull(), + anyOrNull(), anyOrNull(), anyOrNull() ) @@ -126,7 +123,7 @@ class SentryApollo3InterceptorTest { assertTransactionDetails(it) assertEquals(SpanStatus.INTERNAL_ERROR, it.spans.first().status) }, - anyOrNull(), + anyOrNull(), anyOrNull(), anyOrNull() ) @@ -152,15 +149,9 @@ class SentryApollo3InterceptorTest { executeQuery( fixture.getSut( - beforeSpan = object : BeforeSpanCallback { - override fun execute( - span: ISpan, - request: ApolloRequest, - response: ApolloResponse? - ): ISpan { - span.description = "overwritten description" - return span - } + beforeSpan = { span, request, response -> + span.description = "overwritten description" + span } ) ) @@ -171,7 +162,7 @@ class SentryApollo3InterceptorTest { val httpClientSpan = it.spans.first() assertEquals("overwritten description", httpClientSpan.description) }, - anyOrNull(), + anyOrNull(), anyOrNull(), anyOrNull() ) @@ -181,14 +172,8 @@ class SentryApollo3InterceptorTest { fun `when customizer throws, exception is handled`() { executeQuery( fixture.getSut( - beforeSpan = object : BeforeSpanCallback { - override fun execute( - span: ISpan, - request: ApolloRequest, - response: ApolloResponse? - ): ISpan { - throw RuntimeException() - } + beforeSpan = { _, _, _ -> + throw RuntimeException() } ) ) @@ -197,7 +182,7 @@ class SentryApollo3InterceptorTest { check { assertEquals(1, it.spans.size) }, - anyOrNull(), + anyOrNull(), anyOrNull(), anyOrNull() ) 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 index ffa210160c..dd8f57d142 100644 --- 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 @@ -3,7 +3,7 @@ // // This class was automatically generated by Apollo GraphQL version '3.3.0'. // -package com.example.rocketreserver.adapter +package io.sentry.apollo3.adapter import com.apollographql.apollo3.api.Adapter import com.apollographql.apollo3.api.CustomScalarAdapters 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 index ecfff40762..81048e2bbe 100644 --- 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 @@ -3,7 +3,7 @@ // // This class was automatically generated by Apollo GraphQL version '3.3.0'. // -package com.example.rocketreserver.adapter +package io.sentry.apollo3.adapter import com.apollographql.apollo3.api.Adapter import com.apollographql.apollo3.api.CustomScalarAdapters @@ -13,11 +13,11 @@ import com.apollographql.apollo3.api.json.JsonWriter import io.sentry.apollo3.LaunchDetailsQuery import kotlin.IllegalStateException -public object LaunchDetailsQuery_VariablesAdapter : Adapter { - public override fun fromJson(reader: JsonReader, customScalarAdapters: CustomScalarAdapters): +object LaunchDetailsQuery_VariablesAdapter : Adapter { + override fun fromJson(reader: JsonReader, customScalarAdapters: CustomScalarAdapters): LaunchDetailsQuery = throw IllegalStateException("Input type used in output position") - public override fun toJson( + override fun toJson( writer: JsonWriter, customScalarAdapters: CustomScalarAdapters, `value`: LaunchDetailsQuery, diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 8752c93f08..c5afb0ad2a 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -438,6 +438,7 @@ public abstract interface class io/sentry/ISentryExecutorService { public abstract interface class io/sentry/ISerializer { public abstract fun deserialize (Ljava/io/Reader;Ljava/lang/Class;)Ljava/lang/Object; + public abstract fun deserialize (Ljava/io/Reader;Ljava/lang/Class;Lio/sentry/JsonDeserializer;)Ljava/lang/Object; public abstract fun deserializeEnvelope (Ljava/io/InputStream;)Lio/sentry/SentryEnvelope; public abstract fun serialize (Lio/sentry/SentryEnvelope;Ljava/io/OutputStream;)V public abstract fun serialize (Ljava/lang/Object;Ljava/io/Writer;)V @@ -548,6 +549,7 @@ public abstract interface class io/sentry/JsonSerializable { public final class io/sentry/JsonSerializer : io/sentry/ISerializer { public fun (Lio/sentry/SentryOptions;)V public fun deserialize (Ljava/io/Reader;Ljava/lang/Class;)Ljava/lang/Object; + public fun deserialize (Ljava/io/Reader;Ljava/lang/Class;Lio/sentry/JsonDeserializer;)Ljava/lang/Object; public fun deserializeEnvelope (Ljava/io/InputStream;)Lio/sentry/SentryEnvelope; public fun serialize (Lio/sentry/SentryEnvelope;Ljava/io/OutputStream;)V public fun serialize (Ljava/lang/Object;Ljava/io/Writer;)V diff --git a/sentry/src/main/java/io/sentry/ISerializer.java b/sentry/src/main/java/io/sentry/ISerializer.java index dd44d108cc..921530da7b 100644 --- a/sentry/src/main/java/io/sentry/ISerializer.java +++ b/sentry/src/main/java/io/sentry/ISerializer.java @@ -12,6 +12,9 @@ public interface ISerializer { @Nullable T deserialize(@NotNull Reader reader, @NotNull Class clazz); + @Nullable T deserialize( + @NotNull Reader reader, @NotNull Class clazz, @NotNull JsonDeserializer deserializer); + @Nullable SentryEnvelope deserializeEnvelope(@NotNull InputStream inputStream); diff --git a/sentry/src/main/java/io/sentry/JsonSerializer.java b/sentry/src/main/java/io/sentry/JsonSerializer.java index 52c5d2e2da..887526c9e9 100644 --- a/sentry/src/main/java/io/sentry/JsonSerializer.java +++ b/sentry/src/main/java/io/sentry/JsonSerializer.java @@ -106,22 +106,28 @@ public JsonSerializer(@NotNull SentryOptions options) { // Deserialize @Override - public @Nullable T deserialize(@NotNull Reader reader, @NotNull Class clazz) { + public @Nullable T deserialize( + @NotNull Reader reader, @NotNull Class clazz, @NotNull JsonDeserializer deserializer) { try { JsonObjectReader jsonObjectReader = new JsonObjectReader(reader); - JsonDeserializer deserializer = deserializersByClass.get(clazz); - if (deserializer != null) { - Object object = deserializer.deserialize(jsonObjectReader, options.getLogger()); - return clazz.cast(object); - } else { - return null; // No way to deserialize objects we don't know about. - } + Object object = deserializer.deserialize(jsonObjectReader, options.getLogger()); + return clazz.cast(object); } catch (Exception e) { options.getLogger().log(SentryLevel.ERROR, "Error when deserializing", e); return null; } } + @Override + public @Nullable T deserialize(@NotNull Reader reader, @NotNull Class clazz) { + JsonDeserializer deserializer = deserializersByClass.get(clazz); + if (deserializer != null) { + return deserialize(reader, clazz, deserializer); + } else { + return null; // No way to deserialize objects we don't know about. + } + } + @Override public @Nullable SentryEnvelope deserializeEnvelope(@NotNull InputStream inputStream) { Objects.requireNonNull(inputStream, "The InputStream object is required."); diff --git a/sentry/src/main/java/io/sentry/NoOpSerializer.java b/sentry/src/main/java/io/sentry/NoOpSerializer.java index ad517e2e5f..a15eba2a0d 100644 --- a/sentry/src/main/java/io/sentry/NoOpSerializer.java +++ b/sentry/src/main/java/io/sentry/NoOpSerializer.java @@ -25,6 +25,12 @@ private NoOpSerializer() {} return null; } + @Override + public @Nullable T deserialize( + @NotNull Reader reader, @NotNull Class clazz, @NotNull JsonDeserializer deserializer) { + return null; + } + @Override public @Nullable SentryEnvelope deserializeEnvelope(@NotNull InputStream inputStream) { return null; From e70c24af25938fe849884374143176d19de3b8cd Mon Sep 17 00:00:00 2001 From: Lukas Bloder Date: Mon, 20 Jun 2022 00:04:14 +0200 Subject: [PATCH 03/12] fix how response body size is calculated, refactor and simplify interceptor --- CHANGELOG.md | 2 ++ buildSrc/src/main/java/Config.kt | 1 - sentry-apollo-3/api/sentry-apollo-3.api | 35 +++++-------------- sentry-apollo-3/build.gradle.kts | 1 - .../apollo3/ApolloRequestBodyContent.kt | 27 +++----------- .../main/java/io/sentry/apollo3/Extensions.kt | 15 -------- .../apollo3/SentryApollo3HttpInterceptor.kt | 28 +++------------ .../apollo3/SentryApollo3InterceptorTest.kt | 10 ++---- 8 files changed, 20 insertions(+), 99 deletions(-) delete mode 100644 sentry-apollo-3/src/main/java/io/sentry/apollo3/Extensions.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index 69e1558f76..d9e40805aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +- Add integration for Apollo-Kotlin 3 + ### Features - Replace `tracestate` header with `baggage` header ([#2078](https://github.com/getsentry/sentry-java/pull/2078)) diff --git a/buildSrc/src/main/java/Config.kt b/buildSrc/src/main/java/Config.kt index ade28d7681..a55056b9e3 100644 --- a/buildSrc/src/main/java/Config.kt +++ b/buildSrc/src/main/java/Config.kt @@ -106,7 +106,6 @@ object Config { val kotlinReflect = "org.jetbrains.kotlin:kotlin-reflect" val apollo3 = "com.apollographql.apollo3:apollo-runtime:3.3.0" - val apollo3cache = "com.apollographql.apollo3:apollo-normalized-cache:3.3.0" } object AnnotationProcessors { diff --git a/sentry-apollo-3/api/sentry-apollo-3.api b/sentry-apollo-3/api/sentry-apollo-3.api index 464b7cd278..5453178c26 100644 --- a/sentry-apollo-3/api/sentry-apollo-3.api +++ b/sentry-apollo-3/api/sentry-apollo-3.api @@ -1,15 +1,18 @@ public final class io/sentry/apollo3/ApolloRequestBodyContent { public fun ()V - public fun (Ljava/lang/String;Ljava/util/Map;)V - public synthetic fun (Ljava/lang/String;Ljava/util/Map;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Ljava/lang/String;Ljava/util/Map;Ljava/lang/String;)V + public synthetic fun (Ljava/lang/String;Ljava/util/Map;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1 ()Ljava/lang/String; public final fun component2 ()Ljava/util/Map; - public final fun copy (Ljava/lang/String;Ljava/util/Map;)Lio/sentry/apollo3/ApolloRequestBodyContent; - public static synthetic fun copy$default (Lio/sentry/apollo3/ApolloRequestBodyContent;Ljava/lang/String;Ljava/util/Map;ILjava/lang/Object;)Lio/sentry/apollo3/ApolloRequestBodyContent; + public final fun component3 ()Ljava/lang/String; + public final fun copy (Ljava/lang/String;Ljava/util/Map;Ljava/lang/String;)Lio/sentry/apollo3/ApolloRequestBodyContent; + public static synthetic fun copy$default (Lio/sentry/apollo3/ApolloRequestBodyContent;Ljava/lang/String;Ljava/util/Map;Ljava/lang/String;ILjava/lang/Object;)Lio/sentry/apollo3/ApolloRequestBodyContent; public fun equals (Ljava/lang/Object;)Z + public final fun getOperationName ()Ljava/lang/String; public final fun getQuery ()Ljava/lang/String; public final fun getVariables ()Ljava/util/Map; public fun hashCode ()I + public final fun setOperationName (Ljava/lang/String;)V public final fun setQuery (Ljava/lang/String;)V public final fun setVariables (Ljava/util/Map;)V public fun toString ()Ljava/lang/String; @@ -23,33 +26,11 @@ public final class io/sentry/apollo3/ApolloRequestBodyContent$Deserializer : io/ public final class io/sentry/apollo3/ApolloRequestBodyContent$JsonKeys { public static final field INSTANCE Lio/sentry/apollo3/ApolloRequestBodyContent$JsonKeys; + public static final field OPERATION_NAME Ljava/lang/String; public static final field QUERY Ljava/lang/String; public static final field VARIABLES Ljava/lang/String; } -public final class io/sentry/apollo3/ApolloRequestBodyListContent { - public fun (Ljava/util/List;)V - public final fun component1 ()Ljava/util/List; - public final fun copy (Ljava/util/List;)Lio/sentry/apollo3/ApolloRequestBodyListContent; - public static synthetic fun copy$default (Lio/sentry/apollo3/ApolloRequestBodyListContent;Ljava/util/List;ILjava/lang/Object;)Lio/sentry/apollo3/ApolloRequestBodyListContent; - public fun equals (Ljava/lang/Object;)Z - public final fun getQueries ()Ljava/util/List; - public fun hashCode ()I - public final fun setQueries (Ljava/util/List;)V - public fun toString ()Ljava/lang/String; -} - -public final class io/sentry/apollo3/ApolloRequestBodyListContent$Deserializer : io/sentry/JsonDeserializer { - public static final field INSTANCE Lio/sentry/apollo3/ApolloRequestBodyListContent$Deserializer; - public fun deserialize (Lio/sentry/JsonObjectReader;Lio/sentry/ILogger;)Lio/sentry/apollo3/ApolloRequestBodyListContent; - public synthetic fun deserialize (Lio/sentry/JsonObjectReader;Lio/sentry/ILogger;)Ljava/lang/Object; -} - -public final class io/sentry/apollo3/ExtensionsKt { - public static final fun headersContentLength (Lcom/apollographql/apollo3/api/http/HttpResponse;)J - public static final fun toLongOrDefault (Ljava/lang/String;J)J -} - public final class io/sentry/apollo3/SentryApollo3HttpInterceptor : com/apollographql/apollo3/network/http/HttpInterceptor { public static final field Companion Lio/sentry/apollo3/SentryApollo3HttpInterceptor$Companion; public fun ()V diff --git a/sentry-apollo-3/build.gradle.kts b/sentry-apollo-3/build.gradle.kts index dedcb9cd6f..0215b3c51e 100644 --- a/sentry-apollo-3/build.gradle.kts +++ b/sentry-apollo-3/build.gradle.kts @@ -25,7 +25,6 @@ dependencies { api(projects.sentryKotlinExtensions) implementation(Config.Libs.apollo3) - implementation(Config.Libs.apollo3cache) compileOnly(Config.CompileOnly.nopen) errorprone(Config.CompileOnly.nopenChecker) diff --git a/sentry-apollo-3/src/main/java/io/sentry/apollo3/ApolloRequestBodyContent.kt b/sentry-apollo-3/src/main/java/io/sentry/apollo3/ApolloRequestBodyContent.kt index 3b65f8def7..9c36ab6215 100644 --- a/sentry-apollo-3/src/main/java/io/sentry/apollo3/ApolloRequestBodyContent.kt +++ b/sentry-apollo-3/src/main/java/io/sentry/apollo3/ApolloRequestBodyContent.kt @@ -5,36 +5,16 @@ import io.sentry.JsonDeserializer import io.sentry.JsonObjectReader import io.sentry.vendor.gson.stream.JsonToken -data class ApolloRequestBodyListContent( - var queries: List? -) { - object Deserializer : JsonDeserializer { - override fun deserialize( - reader: JsonObjectReader, - logger: ILogger - ): ApolloRequestBodyListContent { - reader.beginArray() - val requestBodyListContent = ApolloRequestBodyListContent( - reader.nextList( - logger, - ApolloRequestBodyContent.Deserializer - ) - ) - reader.endArray() - - return requestBodyListContent - } - } -} - data class ApolloRequestBodyContent( var query: String = "", - var variables: Map? = null + var variables: Map? = null, + var operationName: String? = null ) { // JsonSerializable object JsonKeys { const val QUERY = "query" const val VARIABLES = "variables" + const val OPERATION_NAME = "operationName" } object Deserializer : JsonDeserializer { @@ -45,6 +25,7 @@ data class ApolloRequestBodyContent( when (val nextName = reader.nextName()) { JsonKeys.VARIABLES -> content.variables = reader.nextObjectOrNull() as? Map JsonKeys.QUERY -> content.query = reader.nextString() + JsonKeys.OPERATION_NAME -> content.operationName = reader.nextString() else -> { reader.nextUnknown(logger, mutableMapOf(), nextName) } diff --git a/sentry-apollo-3/src/main/java/io/sentry/apollo3/Extensions.kt b/sentry-apollo-3/src/main/java/io/sentry/apollo3/Extensions.kt deleted file mode 100644 index 7d5899cc1a..0000000000 --- a/sentry-apollo-3/src/main/java/io/sentry/apollo3/Extensions.kt +++ /dev/null @@ -1,15 +0,0 @@ -package io.sentry.apollo3 - -import com.apollographql.apollo3.api.http.HttpResponse - -fun HttpResponse.headersContentLength(): Long { - return headers.firstOrNull { it.name == "Content-Length" }?.value?.toLongOrDefault(-1L) ?: -1L -} - -fun String.toLongOrDefault(defaultValue: Long): Long { - return try { - toLong() - } catch (_: NumberFormatException) { - defaultValue - } -} 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 index 208f83b065..e44b013dac 100644 --- a/sentry-apollo-3/src/main/java/io/sentry/apollo3/SentryApollo3HttpInterceptor.kt +++ b/sentry-apollo-3/src/main/java/io/sentry/apollo3/SentryApollo3HttpInterceptor.kt @@ -80,21 +80,12 @@ class SentryApollo3HttpInterceptor(private val hub: IHub = HubAdapter.getInstanc ApolloRequestBodyContent.Deserializer ) } catch (e: Exception) { - null - } ?: try { - hub.options.serializer.deserialize( - reader, - ApolloRequestBodyListContent::class.java, - ApolloRequestBodyListContent.Deserializer - ) - } catch (e: Exception) { - null + hub.options.logger.log(SentryLevel.ERROR, "Error deserializing Apollo Request Body.", e) } val span = when (content) { is ApolloRequestBodyContent -> createSpanFromBodyContent(activeSpan, request, content) - is ApolloRequestBodyListContent -> createSpanFromBodyListContent(activeSpan, content) - else -> activeSpan.startChild("unkown") + else -> activeSpan.startChild("apollo.request.unknown") } val operationId = request.headers.firstOrNull { it.name == "X-APOLLO-OPERATION-ID" }?.value ?: "unknown" @@ -104,23 +95,12 @@ class SentryApollo3HttpInterceptor(private val hub: IHub = HubAdapter.getInstanc } } - private fun createSpanFromBodyListContent(activeSpan: ISpan, content: ApolloRequestBodyListContent): ISpan { - return activeSpan.startChild("Batched Operation").apply { - setData("isBatched", true) - content.queries?.forEachIndexed { index, item -> - setData("variables $index", item.variables.toString()) - setData("variables $index", item.variables.toString()) - } - } - } - private fun createSpanFromBodyContent(activeSpan: ISpan, request: HttpRequest, content: ApolloRequestBodyContent): ISpan { val operationType = parseOperationType(content) - val operationName = request.headers.firstOrNull { it.name == "X-APOLLO-OPERATION-NAME" }?.value ?: "unknown" + val operationName = content.operationName ?: request.headers.firstOrNull { it.name == "X-APOLLO-OPERATION-NAME" }?.value ?: "unknown" val description = "$operationType $operationName" return activeSpan.startChild(operationName, description).apply { - setData("isBatched", false) setData("variables", content.variables.toString()) } } @@ -144,7 +124,7 @@ class SentryApollo3HttpInterceptor(private val hub: IHub = HubAdapter.getInstanc breadcrumb.setData("request_body_size", contentLength) } - httpResponse.headersContentLength().ifHasValidLength { contentLength -> + httpResponse.body?.peek()?.readByteArray()?.size?.toLong().ifHasValidLength { contentLength -> breadcrumb.setData("response_body_size", contentLength) } 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 index 85a02ad0b6..2efe47f658 100644 --- a/sentry-apollo-3/src/test/java/io/sentry/apollo3/SentryApollo3InterceptorTest.kt +++ b/sentry-apollo-3/src/test/java/io/sentry/apollo3/SentryApollo3InterceptorTest.kt @@ -53,7 +53,6 @@ class SentryApollo3InterceptorTest { } }""", socketPolicy: SocketPolicy = SocketPolicy.KEEP_OPEN, - useHttpInterceptor: Boolean = false, beforeSpan: BeforeSpanCallback? = null ): ApolloClient { whenever(hub.options).thenReturn(SentryOptions()) @@ -71,12 +70,6 @@ class SentryApollo3InterceptorTest { val builder = ApolloClient.builder() .serverUrl(server.url("/").toString()) .addHttpInterceptor(httpInterceptor) -// .addInterceptor(interceptor) -// .addInterceptor(CacheAndNetworkInterceptor) - -// if (useHttpInterceptor) { -// builder.addHttpInterceptor(httpInterceptor) -// } return builder.build() } @@ -190,7 +183,7 @@ class SentryApollo3InterceptorTest { @Test fun `adds breadcrumb when http calls succeeds`() { - executeQuery(fixture.getSut(useHttpInterceptor = true)) + executeQuery(fixture.getSut()) verify(fixture.hub).addBreadcrumb( check { assertEquals("http", it.type) @@ -226,6 +219,7 @@ class SentryApollo3InterceptorTest { return@launch } } + coroutine.join() tx?.finish() } From 71dabc076fb67e50e60836971613e3bbf7a3cefc Mon Sep 17 00:00:00 2001 From: Lukas Bloder Date: Mon, 20 Jun 2022 00:07:44 +0200 Subject: [PATCH 04/12] update changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d0c3e376e1..3aeec927fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ## Unreleased -- Add integration for Apollo-Kotlin 3 +- Add integration for Apollo-Kotlin 3 ([#2109](https://github.com/getsentry/sentry-java/pull/2109)) ### Features From ea95a8cc68075bd0590fc02d5711041953220134 Mon Sep 17 00:00:00 2001 From: Lukas Bloder Date: Tue, 21 Jun 2022 08:01:28 +0200 Subject: [PATCH 05/12] use throwable, add constructors for java compatibility, use new apollo Builder api, update mockserver dependency --- buildSrc/src/main/java/Config.kt | 3 +-- sentry-apollo-3/api/sentry-apollo-3.api | 2 ++ sentry-apollo-3/build.gradle.kts | 2 +- .../sentry/apollo3/SentryApollo3HttpInterceptor.kt | 13 ++++++++----- .../sentry/apollo3/SentryApollo3InterceptorTest.kt | 2 +- sentry-apollo/build.gradle.kts | 2 +- 6 files changed, 14 insertions(+), 10 deletions(-) diff --git a/buildSrc/src/main/java/Config.kt b/buildSrc/src/main/java/Config.kt index a55056b9e3..f8ba660d6e 100644 --- a/buildSrc/src/main/java/Config.kt +++ b/buildSrc/src/main/java/Config.kt @@ -105,7 +105,7 @@ object Config { val kotlinReflect = "org.jetbrains.kotlin:kotlin-reflect" - val apollo3 = "com.apollographql.apollo3:apollo-runtime:3.3.0" + val apolloKotlin = "com.apollographql.apollo3:apollo-runtime:3.3.0" } object AnnotationProcessors { @@ -133,7 +133,6 @@ 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" diff --git a/sentry-apollo-3/api/sentry-apollo-3.api b/sentry-apollo-3/api/sentry-apollo-3.api index 5453178c26..3d900ad8ef 100644 --- a/sentry-apollo-3/api/sentry-apollo-3.api +++ b/sentry-apollo-3/api/sentry-apollo-3.api @@ -34,8 +34,10 @@ public final class io/sentry/apollo3/ApolloRequestBodyContent$JsonKeys { public final class io/sentry/apollo3/SentryApollo3HttpInterceptor : com/apollographql/apollo3/network/http/HttpInterceptor { public static final field Companion Lio/sentry/apollo3/SentryApollo3HttpInterceptor$Companion; 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 (Lio/sentry/apollo3/SentryApollo3HttpInterceptor$BeforeSpanCallback;)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; } diff --git a/sentry-apollo-3/build.gradle.kts b/sentry-apollo-3/build.gradle.kts index 0215b3c51e..a04a9bd91f 100644 --- a/sentry-apollo-3/build.gradle.kts +++ b/sentry-apollo-3/build.gradle.kts @@ -24,7 +24,7 @@ dependencies { api(projects.sentry) api(projects.sentryKotlinExtensions) - implementation(Config.Libs.apollo3) + implementation(Config.Libs.apolloKotlin) compileOnly(Config.CompileOnly.nopen) errorprone(Config.CompileOnly.nopenChecker) 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 index e44b013dac..decc45d996 100644 --- a/sentry-apollo-3/src/main/java/io/sentry/apollo3/SentryApollo3HttpInterceptor.kt +++ b/sentry-apollo-3/src/main/java/io/sentry/apollo3/SentryApollo3HttpInterceptor.kt @@ -20,6 +20,9 @@ import java.io.InputStreamReader class SentryApollo3HttpInterceptor(private val hub: IHub = HubAdapter.getInstance(), private val beforeSpan: BeforeSpanCallback? = null) : HttpInterceptor { + constructor(hub: IHub) : this(hub, null) + constructor(beforeSpan: BeforeSpanCallback) : this(HubAdapter.getInstance(), beforeSpan) + override suspend fun intercept( request: HttpRequest, chain: HttpInterceptorChain @@ -50,7 +53,7 @@ class SentryApollo3HttpInterceptor(private val hub: IHub = HubAdapter.getInstanc finish(span, modifiedRequest, httpResponse) httpResponse - } catch (e: Exception) { + } catch (e: Throwable) { when (e) { is ApolloNetworkException -> span.status = SpanStatus.INTERNAL_ERROR else -> SpanStatus.UNKNOWN @@ -79,7 +82,7 @@ class SentryApollo3HttpInterceptor(private val hub: IHub = HubAdapter.getInstanc ApolloRequestBodyContent::class.java, ApolloRequestBodyContent.Deserializer ) - } catch (e: Exception) { + } catch (e: Throwable) { hub.options.logger.log(SentryLevel.ERROR, "Error deserializing Apollo Request Body.", e) } @@ -110,7 +113,7 @@ class SentryApollo3HttpInterceptor(private val hub: IHub = HubAdapter.getInstanc if (beforeSpan != null) { try { newSpan = beforeSpan.execute(span, request, response) - } catch (e: Exception) { + } catch (e: Throwable) { hub.options.logger.log(SentryLevel.ERROR, "An error occurred while executing beforeSpan on ApolloInterceptor", e) } } @@ -137,8 +140,8 @@ class SentryApollo3HttpInterceptor(private val hub: IHub = HubAdapter.getInstanc } } - private fun parseOperationType(content: ApolloRequestBodyContent?): String { - val operationPart = content?.query?.takeWhile { !it.isWhitespace() } + private fun parseOperationType(content: ApolloRequestBodyContent): String { + val operationPart = content.query.takeWhile { !it.isWhitespace() } return KNOWN_OPERATIONS.firstOrNull { it == operationPart } ?: "unknown" } 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 index 2efe47f658..41a89e27f2 100644 --- a/sentry-apollo-3/src/test/java/io/sentry/apollo3/SentryApollo3InterceptorTest.kt +++ b/sentry-apollo-3/src/test/java/io/sentry/apollo3/SentryApollo3InterceptorTest.kt @@ -67,7 +67,7 @@ class SentryApollo3InterceptorTest { if (beforeSpan != null) { httpInterceptor = SentryApollo3HttpInterceptor(hub, beforeSpan) } - val builder = ApolloClient.builder() + val builder = ApolloClient.Builder() .serverUrl(server.url("/").toString()) .addHttpInterceptor(httpInterceptor) 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) } From 683d3749465f3af36296f100b15bdc294d9fe600 Mon Sep 17 00:00:00 2001 From: Lukas Bloder Date: Mon, 27 Jun 2022 08:53:28 +0200 Subject: [PATCH 06/12] add custom request composer to capture more info about the apollo call --- sentry-apollo-3/api/sentry-apollo-3.api | 43 +--- .../apollo3/ApolloRequestBodyContent.kt | 38 --- .../apollo3/SentryApollo3HttpInterceptor.kt | 146 ++++++----- .../apollo3/SentryApollo3RequestComposer.kt | 39 +++ .../apollo3/SentryApollo3InterceptorTest.kt | 7 +- ...entryApollo3InterceptorWithComposerTest.kt | 232 ++++++++++++++++++ 6 files changed, 362 insertions(+), 143 deletions(-) delete mode 100644 sentry-apollo-3/src/main/java/io/sentry/apollo3/ApolloRequestBodyContent.kt create mode 100644 sentry-apollo-3/src/main/java/io/sentry/apollo3/SentryApollo3RequestComposer.kt create mode 100644 sentry-apollo-3/src/test/java/io/sentry/apollo3/SentryApollo3InterceptorWithComposerTest.kt diff --git a/sentry-apollo-3/api/sentry-apollo-3.api b/sentry-apollo-3/api/sentry-apollo-3.api index 3d900ad8ef..96fb0aa3a2 100644 --- a/sentry-apollo-3/api/sentry-apollo-3.api +++ b/sentry-apollo-3/api/sentry-apollo-3.api @@ -1,43 +1,12 @@ -public final class io/sentry/apollo3/ApolloRequestBodyContent { - public fun ()V - public fun (Ljava/lang/String;Ljava/util/Map;Ljava/lang/String;)V - public synthetic fun (Ljava/lang/String;Ljava/util/Map;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun component1 ()Ljava/lang/String; - public final fun component2 ()Ljava/util/Map; - public final fun component3 ()Ljava/lang/String; - public final fun copy (Ljava/lang/String;Ljava/util/Map;Ljava/lang/String;)Lio/sentry/apollo3/ApolloRequestBodyContent; - public static synthetic fun copy$default (Lio/sentry/apollo3/ApolloRequestBodyContent;Ljava/lang/String;Ljava/util/Map;Ljava/lang/String;ILjava/lang/Object;)Lio/sentry/apollo3/ApolloRequestBodyContent; - public fun equals (Ljava/lang/Object;)Z - public final fun getOperationName ()Ljava/lang/String; - public final fun getQuery ()Ljava/lang/String; - public final fun getVariables ()Ljava/util/Map; - public fun hashCode ()I - public final fun setOperationName (Ljava/lang/String;)V - public final fun setQuery (Ljava/lang/String;)V - public final fun setVariables (Ljava/util/Map;)V - public fun toString ()Ljava/lang/String; -} - -public final class io/sentry/apollo3/ApolloRequestBodyContent$Deserializer : io/sentry/JsonDeserializer { - public static final field INSTANCE Lio/sentry/apollo3/ApolloRequestBodyContent$Deserializer; - public fun deserialize (Lio/sentry/JsonObjectReader;Lio/sentry/ILogger;)Lio/sentry/apollo3/ApolloRequestBodyContent; - public synthetic fun deserialize (Lio/sentry/JsonObjectReader;Lio/sentry/ILogger;)Ljava/lang/Object; -} - -public final class io/sentry/apollo3/ApolloRequestBodyContent$JsonKeys { - public static final field INSTANCE Lio/sentry/apollo3/ApolloRequestBodyContent$JsonKeys; - public static final field OPERATION_NAME Ljava/lang/String; - public static final field QUERY Ljava/lang/String; - public static final field VARIABLES Ljava/lang/String; -} - 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 (Lio/sentry/apollo3/SentryApollo3HttpInterceptor$BeforeSpanCallback;)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; } @@ -47,6 +16,10 @@ public abstract interface class io/sentry/apollo3/SentryApollo3HttpInterceptor$B } public final class io/sentry/apollo3/SentryApollo3HttpInterceptor$Companion { - public final fun getKNOWN_OPERATIONS ()Ljava/util/List; +} + +public final class io/sentry/apollo3/SentryApollo3RequestComposer : com/apollographql/apollo3/api/http/HttpRequestComposer { + public fun (Ljava/lang/String;)V + public fun compose (Lcom/apollographql/apollo3/api/ApolloRequest;)Lcom/apollographql/apollo3/api/http/HttpRequest; } diff --git a/sentry-apollo-3/src/main/java/io/sentry/apollo3/ApolloRequestBodyContent.kt b/sentry-apollo-3/src/main/java/io/sentry/apollo3/ApolloRequestBodyContent.kt deleted file mode 100644 index 9c36ab6215..0000000000 --- a/sentry-apollo-3/src/main/java/io/sentry/apollo3/ApolloRequestBodyContent.kt +++ /dev/null @@ -1,38 +0,0 @@ -package io.sentry.apollo3 - -import io.sentry.ILogger -import io.sentry.JsonDeserializer -import io.sentry.JsonObjectReader -import io.sentry.vendor.gson.stream.JsonToken - -data class ApolloRequestBodyContent( - var query: String = "", - var variables: Map? = null, - var operationName: String? = null -) { - // JsonSerializable - object JsonKeys { - const val QUERY = "query" - const val VARIABLES = "variables" - const val OPERATION_NAME = "operationName" - } - - object Deserializer : JsonDeserializer { - override fun deserialize(reader: JsonObjectReader, logger: ILogger): ApolloRequestBodyContent { - val content = ApolloRequestBodyContent() - reader.beginObject() - while (reader.peek() == JsonToken.NAME) { - when (val nextName = reader.nextName()) { - JsonKeys.VARIABLES -> content.variables = reader.nextObjectOrNull() as? Map - JsonKeys.QUERY -> content.query = reader.nextString() - JsonKeys.OPERATION_NAME -> content.operationName = reader.nextString() - else -> { - reader.nextUnknown(logger, mutableMapOf(), nextName) - } - } - } - reader.endObject() - return content - } - } -} 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 index decc45d996..16ded438c9 100644 --- a/sentry-apollo-3/src/main/java/io/sentry/apollo3/SentryApollo3HttpInterceptor.kt +++ b/sentry-apollo-3/src/main/java/io/sentry/apollo3/SentryApollo3HttpInterceptor.kt @@ -1,7 +1,9 @@ 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 @@ -14,15 +16,10 @@ import io.sentry.SentryLevel import io.sentry.SpanStatus import io.sentry.TracingOrigins import io.sentry.TypeCheckHint -import okio.Buffer -import java.io.InputStreamReader -class SentryApollo3HttpInterceptor(private val hub: IHub = HubAdapter.getInstance(), private val beforeSpan: BeforeSpanCallback? = null) : +class SentryApollo3HttpInterceptor @JvmOverloads constructor(private val hub: IHub = HubAdapter.getInstance(), private val beforeSpan: BeforeSpanCallback? = null) : HttpInterceptor { - constructor(hub: IHub) : this(hub, null) - constructor(beforeSpan: BeforeSpanCallback) : this(HubAdapter.getInstance(), beforeSpan) - override suspend fun intercept( request: HttpRequest, chain: HttpInterceptorChain @@ -33,7 +30,11 @@ class SentryApollo3HttpInterceptor(private val hub: IHub = HubAdapter.getInstanc } else { val span = startChild(request, activeSpan) - val requestBuilder = request.newBuilder() + val cleanedHeaders = removeSentryInternalHeaders(request.headers) + + val requestBuilder = request.newBuilder().apply { + headers(cleanedHeaders) + } if (TracingOrigins.contain(hub.options.tracingOrigins, request.url)) { val sentryTraceHeader = span.toSentryTrace() @@ -46,69 +47,64 @@ class SentryApollo3HttpInterceptor(private val hub: IHub = HubAdapter.getInstanc } val modifiedRequest = requestBuilder.build() + var httpResponse: HttpResponse? = null + var statusCode: Int? = null - return try { - val httpResponse = chain.proceed(modifiedRequest) - span.status = SpanStatus.fromHttpStatusCode(httpResponse.statusCode, SpanStatus.UNKNOWN) - finish(span, modifiedRequest, httpResponse) - - httpResponse + 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.UNKNOWN + else -> SpanStatus.INTERNAL_ERROR } - finish(span, modifiedRequest) + span.throwable = e throw e + } finally { + finish(span, modifiedRequest, httpResponse, statusCode) } } } - private fun Long?.ifHasValidLength(fn: (Long) -> Unit) { - if (this != null && this != -1L) { - fn.invoke(this) - } + 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 buffer = Buffer() - request.body?.writeTo(buffer) - - val reader = InputStreamReader(buffer.inputStream()) - - val content = try { - hub.options.serializer.deserialize( - reader, - ApolloRequestBodyContent::class.java, - ApolloRequestBodyContent.Deserializer - ) - } catch (e: Throwable) { - hub.options.logger.log(SentryLevel.ERROR, "Error deserializing Apollo Request Body.", e) - } - - val span = when (content) { - is ApolloRequestBodyContent -> createSpanFromBodyContent(activeSpan, request, content) - else -> activeSpan.startChild("apollo.request.unknown") - } - - val operationId = request.headers.firstOrNull { it.name == "X-APOLLO-OPERATION-ID" }?.value ?: "unknown" + 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) + } - return span.apply { - setData("operationId", operationId) + variables?.let { + setData("variables", it) + } } } - private fun createSpanFromBodyContent(activeSpan: ISpan, request: HttpRequest, content: ApolloRequestBodyContent): ISpan { - val operationType = parseOperationType(content) - val operationName = content.operationName ?: request.headers.firstOrNull { it.name == "X-APOLLO-OPERATION-NAME" }?.value ?: "unknown" - val description = "$operationType $operationName" - - return activeSpan.startChild(operationName, description).apply { - setData("variables", content.variables.toString()) - } + private fun operationNameFromHeaders(request: HttpRequest): String? { + return request.valueForHeader(SENTRY_APOLLO_3_OPERATION_NAME) ?: request.valueForHeader("X-APOLLO-OPERATION-NAME") } - private fun finish(span: ISpan, request: HttpRequest, response: HttpResponse? = null) { + private fun HttpRequest.valueForHeader(key: String) = headers.firstOrNull { it.name == key }?.value + + private fun finish(span: ISpan, request: HttpRequest, response: HttpResponse? = null, statusCode: Int?) { var newSpan: ISpan = span if (beforeSpan != null) { try { @@ -119,34 +115,44 @@ class SentryApollo3HttpInterceptor(private val hub: IHub = HubAdapter.getInstanc } newSpan.finish() - response?.let { httpResponse -> - val breadcrumb = - Breadcrumb.http(request.url, request.method.name, httpResponse.statusCode) + val breadcrumb = + Breadcrumb.http(request.url, request.method.name, statusCode) - request.body?.contentLength.ifHasValidLength { contentLength -> - breadcrumb.setData("request_body_size", contentLength) - } + request.body?.contentLength.ifHasValidLength { contentLength -> + breadcrumb.setData("request_body_size", contentLength) + } + + val hint = Hint().also { + it.set(TypeCheckHint.APOLLO_REQUEST, request) + } - httpResponse.body?.peek()?.readByteArray()?.size?.toLong().ifHasValidLength { contentLength -> + response?.let { httpResponse -> + httpResponse.headersContentLength().ifHasValidLength { contentLength -> breadcrumb.setData("response_body_size", contentLength) } - val hint = Hint().also { - it.set(TypeCheckHint.APOLLO_REQUEST, request) - it.set(TypeCheckHint.APOLLO_RESPONSE, httpResponse) + if (!breadcrumb.data.containsKey("response_body_size")) { + httpResponse.body?.buffer?.size?.ifHasValidLength { contentLength -> + breadcrumb.setData("response_body_size", contentLength) + } } - hub.addBreadcrumb(breadcrumb, hint) + hint.set(TypeCheckHint.APOLLO_RESPONSE, httpResponse) } + + hub.addBreadcrumb(breadcrumb, hint) } - private fun parseOperationType(content: ApolloRequestBodyContent): String { - val operationPart = content.query.takeWhile { !it.isWhitespace() } - return KNOWN_OPERATIONS.firstOrNull { it == operationPart } ?: "unknown" + // Extensions + + private fun HttpResponse.headersContentLength(): Long { + return headers.firstOrNull { it.name == "Content-Length" }?.value?.toLongOrNull() ?: -1L } - companion object { - val KNOWN_OPERATIONS = listOf("query", "mutation", "subscription") + private fun Long?.ifHasValidLength(fn: (Long) -> Unit) { + if (this != null && this != -1L) { + fn.invoke(this) + } } /** @@ -162,4 +168,10 @@ class SentryApollo3HttpInterceptor(private val hub: IHub = HubAdapter.getInstanc */ 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/SentryApollo3RequestComposer.kt b/sentry-apollo-3/src/main/java/io/sentry/apollo3/SentryApollo3RequestComposer.kt new file mode 100644 index 0000000000..673772d397 --- /dev/null +++ b/sentry-apollo-3/src/main/java/io/sentry/apollo3/SentryApollo3RequestComposer.kt @@ -0,0 +1,39 @@ +package io.sentry.apollo3 + +import com.apollographql.apollo3.api.ApolloRequest +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.http.DefaultHttpRequestComposer +import com.apollographql.apollo3.api.http.HttpRequest +import com.apollographql.apollo3.api.http.HttpRequestComposer +import com.apollographql.apollo3.api.variables + +class SentryApollo3RequestComposer(url: String) : HttpRequestComposer { + private val defaultHttpRequestComposer = DefaultHttpRequestComposer(url) + + override fun compose(apolloRequest: ApolloRequest): HttpRequest { + val httpRequest = defaultHttpRequestComposer.compose(apolloRequest) + val builder = httpRequest.newBuilder() + .addHeader(SentryApollo3HttpInterceptor.SENTRY_APOLLO_3_OPERATION_TYPE, operationType(apolloRequest)) + .addHeader(SentryApollo3HttpInterceptor.SENTRY_APOLLO_3_OPERATION_NAME, apolloRequest.operation.name()) + + apolloRequest.scalarAdapters?.let { + builder.addHeader(SentryApollo3HttpInterceptor.SENTRY_APOLLO_3_VARIABLES, apolloRequest.operation.variables(it).valueMap.toString()) + } + + return 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/test/java/io/sentry/apollo3/SentryApollo3InterceptorTest.kt b/sentry-apollo-3/src/test/java/io/sentry/apollo3/SentryApollo3InterceptorTest.kt index 41a89e27f2..8f4fe2538d 100644 --- a/sentry-apollo-3/src/test/java/io/sentry/apollo3/SentryApollo3InterceptorTest.kt +++ b/sentry-apollo-3/src/test/java/io/sentry/apollo3/SentryApollo3InterceptorTest.kt @@ -27,6 +27,7 @@ import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertNotNull import kotlin.test.assertNull +import kotlin.test.assertTrue class SentryApollo3InterceptorTest { @@ -53,7 +54,7 @@ class SentryApollo3InterceptorTest { } }""", socketPolicy: SocketPolicy = SocketPolicy.KEEP_OPEN, - beforeSpan: BeforeSpanCallback? = null + beforeSpan: BeforeSpanCallback? = null, ): ApolloClient { whenever(hub.options).thenReturn(SentryOptions()) @@ -67,6 +68,7 @@ class SentryApollo3InterceptorTest { if (beforeSpan != null) { httpInterceptor = SentryApollo3HttpInterceptor(hub, beforeSpan) } + val builder = ApolloClient.Builder() .serverUrl(server.url("/").toString()) .addHttpInterceptor(httpInterceptor) @@ -198,10 +200,9 @@ class SentryApollo3InterceptorTest { assertEquals(1, it.spans.size) val httpClientSpan = it.spans.first() assertEquals("LaunchDetails", httpClientSpan.op) - assertEquals("query LaunchDetails", httpClientSpan.description) + assertTrue { httpClientSpan.description?.startsWith("Post LaunchDetails") == true } assertNotNull(httpClientSpan.data) { assertNotNull(it["operationId"]) - assertEquals("{id=83}", it["variables"]) } } diff --git a/sentry-apollo-3/src/test/java/io/sentry/apollo3/SentryApollo3InterceptorWithComposerTest.kt b/sentry-apollo-3/src/test/java/io/sentry/apollo3/SentryApollo3InterceptorWithComposerTest.kt new file mode 100644 index 0000000000..79df5a1140 --- /dev/null +++ b/sentry-apollo-3/src/test/java/io/sentry/apollo3/SentryApollo3InterceptorWithComposerTest.kt @@ -0,0 +1,232 @@ +package io.sentry.apollo3 + +import com.apollographql.apollo3.ApolloClient +import com.apollographql.apollo3.exception.ApolloException +import com.apollographql.apollo3.network.http.HttpNetworkTransport +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.SentryTraceHeader +import io.sentry.SentryTracer +import io.sentry.SpanStatus +import io.sentry.TraceContext +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 SentryApollo3InterceptorWithComposerTest { + + 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()) + + server.enqueue( + MockResponse() + .setBody(responseBody) + .setSocketPolicy(socketPolicy) + .setResponseCode(httpStatusCode) + ) + + if (beforeSpan != null) { + httpInterceptor = SentryApollo3HttpInterceptor(hub, beforeSpan) + } + + val builder = ApolloClient.builder() + .networkTransport( + HttpNetworkTransport.Builder() + .httpRequestComposer(SentryApollo3RequestComposer(server.url("/").toString())) + .addInterceptor(httpInterceptor) + .build() + ) + + 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]) + } + + @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]) + } + + @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 `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) + 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", 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() + } +} From 6f2d6be2974a3e4a5a48086208c751b0e7bdad6d Mon Sep 17 00:00:00 2001 From: Lukas Bloder Date: Thu, 30 Jun 2022 09:22:12 +0200 Subject: [PATCH 07/12] remove unnecessary testcases in SentryApollo3InterceptorWithComposeTest, make return for beforeSpan nullable, add testcase for dropping spans, add baggage Headers to test --- .../apollo3/SentryApollo3HttpInterceptor.kt | 12 ++-- .../apollo3/SentryApollo3InterceptorTest.kt | 28 +++++++- ...entryApollo3InterceptorWithComposerTest.kt | 69 +++---------------- 3 files changed, 44 insertions(+), 65 deletions(-) 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 index 16ded438c9..26af28c2bc 100644 --- a/sentry-apollo-3/src/main/java/io/sentry/apollo3/SentryApollo3HttpInterceptor.kt +++ b/sentry-apollo-3/src/main/java/io/sentry/apollo3/SentryApollo3HttpInterceptor.kt @@ -105,15 +105,18 @@ class SentryApollo3HttpInterceptor @JvmOverloads constructor(private val hub: IH private fun HttpRequest.valueForHeader(key: String) = headers.firstOrNull { it.name == key }?.value private fun finish(span: ISpan, request: HttpRequest, response: HttpResponse? = null, statusCode: Int?) { - var newSpan: ISpan = span if (beforeSpan != null) { try { - newSpan = beforeSpan.execute(span, request, response) + 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) } } - newSpan.finish() + span.finish() val breadcrumb = Breadcrumb.http(request.url, request.method.name, statusCode) @@ -127,6 +130,7 @@ class SentryApollo3HttpInterceptor @JvmOverloads constructor(private val hub: IH } response?.let { httpResponse -> + // Content-Length header is not present on batched operations httpResponse.headersContentLength().ifHasValidLength { contentLength -> breadcrumb.setData("response_body_size", contentLength) } @@ -166,7 +170,7 @@ class SentryApollo3HttpInterceptor @JvmOverloads constructor(private val hub: IH * @param request the Apollo request object * @param response the Apollo response object */ - fun execute(span: ISpan, request: HttpRequest, response: HttpResponse?): ISpan + fun execute(span: ISpan, request: HttpRequest, response: HttpResponse?): ISpan? } companion object { 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 index 8f4fe2538d..38702971f8 100644 --- a/sentry-apollo-3/src/test/java/io/sentry/apollo3/SentryApollo3InterceptorTest.kt +++ b/sentry-apollo-3/src/test/java/io/sentry/apollo3/SentryApollo3InterceptorTest.kt @@ -7,6 +7,7 @@ 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 @@ -56,7 +57,12 @@ class SentryApollo3InterceptorTest { socketPolicy: SocketPolicy = SocketPolicy.KEEP_OPEN, beforeSpan: BeforeSpanCallback? = null, ): ApolloClient { - whenever(hub.options).thenReturn(SentryOptions()) + whenever(hub.options).thenReturn( + SentryOptions().apply { + dsn = "https://key@sentry.io/proj" + isTraceSampling = true + } + ) server.enqueue( MockResponse() @@ -130,6 +136,7 @@ class SentryApollo3InterceptorTest { val recorderRequest = fixture.server.takeRequest() assertNull(recorderRequest.headers[SentryTraceHeader.SENTRY_TRACE_HEADER]) + assertNull(recorderRequest.headers[BaggageHeader.BAGGAGE_HEADER]) } @Test @@ -137,6 +144,7 @@ class SentryApollo3InterceptorTest { executeQuery() val recorderRequest = fixture.server.takeRequest() assertNotNull(recorderRequest.headers[SentryTraceHeader.SENTRY_TRACE_HEADER]) + assertNotNull(recorderRequest.headers[BaggageHeader.BAGGAGE_HEADER]) } @Test @@ -163,6 +171,24 @@ class SentryApollo3InterceptorTest { ) } + @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( diff --git a/sentry-apollo-3/src/test/java/io/sentry/apollo3/SentryApollo3InterceptorWithComposerTest.kt b/sentry-apollo-3/src/test/java/io/sentry/apollo3/SentryApollo3InterceptorWithComposerTest.kt index 79df5a1140..3c8c710dfd 100644 --- a/sentry-apollo-3/src/test/java/io/sentry/apollo3/SentryApollo3InterceptorWithComposerTest.kt +++ b/sentry-apollo-3/src/test/java/io/sentry/apollo3/SentryApollo3InterceptorWithComposerTest.kt @@ -12,7 +12,6 @@ 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 @@ -128,65 +127,6 @@ class SentryApollo3InterceptorWithComposerTest { ) } - @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]) - } - - @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]) - } - - @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 `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()) @@ -200,6 +140,15 @@ class SentryApollo3InterceptorWithComposerTest { ) } + @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() From ab17056545d0aa24cac444b53b344062fdd312de Mon Sep 17 00:00:00 2001 From: Lukas Bloder Date: Thu, 30 Jun 2022 09:23:25 +0200 Subject: [PATCH 08/12] fix changelog after merge --- CHANGELOG.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 397b365ab0..21b57980ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Features + +- Add integration for Apollo-Kotlin 3 ([#2109](https://github.com/getsentry/sentry-java/pull/2109)) + ### Fixes - Filter out app starts with more than 60s ([#2127](https://github.com/getsentry/sentry-java/pull/2127)) @@ -21,8 +25,6 @@ ## 6.1.1 -- Add integration for Apollo-Kotlin 3 ([#2109](https://github.com/getsentry/sentry-java/pull/2109)) - ### Features - Replace `tracestate` header with `baggage` header ([#2078](https://github.com/getsentry/sentry-java/pull/2078)) From 6164c809997759de0b2d67bf5400f0dca57b045e Mon Sep 17 00:00:00 2001 From: Lukas Bloder Date: Sun, 3 Jul 2022 17:17:52 +0200 Subject: [PATCH 09/12] rollback changes to deserializer, move away from custom request composer and use interceptor, add extension function to apply both interceptors at once --- sentry-apollo-3/api/sentry-apollo-3.api | 13 ++++-- .../apollo3/SentryApollo3Interceptor.kt | 40 +++++++++++++++++++ .../apollo3/SentryApollo3RequestComposer.kt | 39 ------------------ .../sentry/apollo3/apolloBuilderExtensions.kt | 12 ++++++ ...tryApollo3InterceptorWithVariablesTest.kt} | 20 ++-------- sentry/api/sentry.api | 2 - .../src/main/java/io/sentry/ISerializer.java | 3 -- .../main/java/io/sentry/JsonSerializer.java | 22 ++++------ .../main/java/io/sentry/NoOpSerializer.java | 6 --- 9 files changed, 74 insertions(+), 83 deletions(-) create mode 100644 sentry-apollo-3/src/main/java/io/sentry/apollo3/SentryApollo3Interceptor.kt delete mode 100644 sentry-apollo-3/src/main/java/io/sentry/apollo3/SentryApollo3RequestComposer.kt create mode 100644 sentry-apollo-3/src/main/java/io/sentry/apollo3/apolloBuilderExtensions.kt rename sentry-apollo-3/src/test/java/io/sentry/apollo3/{SentryApollo3InterceptorWithComposerTest.kt => SentryApollo3InterceptorWithVariablesTest.kt} (88%) diff --git a/sentry-apollo-3/api/sentry-apollo-3.api b/sentry-apollo-3/api/sentry-apollo-3.api index 96fb0aa3a2..76f3a7595a 100644 --- a/sentry-apollo-3/api/sentry-apollo-3.api +++ b/sentry-apollo-3/api/sentry-apollo-3.api @@ -1,3 +1,10 @@ +public final class io/sentry/apollo3/ApolloBuilderExtensionsKt { + 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 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 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; @@ -18,8 +25,8 @@ public abstract interface class io/sentry/apollo3/SentryApollo3HttpInterceptor$B public final class io/sentry/apollo3/SentryApollo3HttpInterceptor$Companion { } -public final class io/sentry/apollo3/SentryApollo3RequestComposer : com/apollographql/apollo3/api/http/HttpRequestComposer { - public fun (Ljava/lang/String;)V - public fun compose (Lcom/apollographql/apollo3/api/ApolloRequest;)Lcom/apollographql/apollo3/api/http/HttpRequest; +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; } 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/SentryApollo3RequestComposer.kt b/sentry-apollo-3/src/main/java/io/sentry/apollo3/SentryApollo3RequestComposer.kt deleted file mode 100644 index 673772d397..0000000000 --- a/sentry-apollo-3/src/main/java/io/sentry/apollo3/SentryApollo3RequestComposer.kt +++ /dev/null @@ -1,39 +0,0 @@ -package io.sentry.apollo3 - -import com.apollographql.apollo3.api.ApolloRequest -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.http.DefaultHttpRequestComposer -import com.apollographql.apollo3.api.http.HttpRequest -import com.apollographql.apollo3.api.http.HttpRequestComposer -import com.apollographql.apollo3.api.variables - -class SentryApollo3RequestComposer(url: String) : HttpRequestComposer { - private val defaultHttpRequestComposer = DefaultHttpRequestComposer(url) - - override fun compose(apolloRequest: ApolloRequest): HttpRequest { - val httpRequest = defaultHttpRequestComposer.compose(apolloRequest) - val builder = httpRequest.newBuilder() - .addHeader(SentryApollo3HttpInterceptor.SENTRY_APOLLO_3_OPERATION_TYPE, operationType(apolloRequest)) - .addHeader(SentryApollo3HttpInterceptor.SENTRY_APOLLO_3_OPERATION_NAME, apolloRequest.operation.name()) - - apolloRequest.scalarAdapters?.let { - builder.addHeader(SentryApollo3HttpInterceptor.SENTRY_APOLLO_3_VARIABLES, apolloRequest.operation.variables(it).valueMap.toString()) - } - - return 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/apolloBuilderExtensions.kt b/sentry-apollo-3/src/main/java/io/sentry/apollo3/apolloBuilderExtensions.kt new file mode 100644 index 0000000000..584d02d78c --- /dev/null +++ b/sentry-apollo-3/src/main/java/io/sentry/apollo3/apolloBuilderExtensions.kt @@ -0,0 +1,12 @@ +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 +} diff --git a/sentry-apollo-3/src/test/java/io/sentry/apollo3/SentryApollo3InterceptorWithComposerTest.kt b/sentry-apollo-3/src/test/java/io/sentry/apollo3/SentryApollo3InterceptorWithVariablesTest.kt similarity index 88% rename from sentry-apollo-3/src/test/java/io/sentry/apollo3/SentryApollo3InterceptorWithComposerTest.kt rename to sentry-apollo-3/src/test/java/io/sentry/apollo3/SentryApollo3InterceptorWithVariablesTest.kt index 3c8c710dfd..ccc2f5e7bb 100644 --- a/sentry-apollo-3/src/test/java/io/sentry/apollo3/SentryApollo3InterceptorWithComposerTest.kt +++ b/sentry-apollo-3/src/test/java/io/sentry/apollo3/SentryApollo3InterceptorWithVariablesTest.kt @@ -2,7 +2,6 @@ package io.sentry.apollo3 import com.apollographql.apollo3.ApolloClient import com.apollographql.apollo3.exception.ApolloException -import com.apollographql.apollo3.network.http.HttpNetworkTransport import com.nhaarman.mockitokotlin2.anyOrNull import com.nhaarman.mockitokotlin2.check import com.nhaarman.mockitokotlin2.mock @@ -28,12 +27,11 @@ import kotlin.test.assertEquals import kotlin.test.assertNotNull import kotlin.test.assertNull -class SentryApollo3InterceptorWithComposerTest { +class SentryApollo3InterceptorWithVariablesTest { class Fixture { val server = MockWebServer() val hub = mock() - private var httpInterceptor = SentryApollo3HttpInterceptor(hub) @SuppressWarnings("LongParameterList") fun getSut( @@ -64,19 +62,9 @@ class SentryApollo3InterceptorWithComposerTest { .setResponseCode(httpStatusCode) ) - if (beforeSpan != null) { - httpInterceptor = SentryApollo3HttpInterceptor(hub, beforeSpan) - } - - val builder = ApolloClient.builder() - .networkTransport( - HttpNetworkTransport.Builder() - .httpRequestComposer(SentryApollo3RequestComposer(server.url("/").toString())) - .addInterceptor(httpInterceptor) - .build() - ) - - return builder.build() + return ApolloClient.Builder().serverUrl(server.url("/").toString()) + .sentryTracing(hub, beforeSpan) + .build() } } diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index c0ef1685b9..a55aab4471 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -438,7 +438,6 @@ public abstract interface class io/sentry/ISentryExecutorService { public abstract interface class io/sentry/ISerializer { public abstract fun deserialize (Ljava/io/Reader;Ljava/lang/Class;)Ljava/lang/Object; - public abstract fun deserialize (Ljava/io/Reader;Ljava/lang/Class;Lio/sentry/JsonDeserializer;)Ljava/lang/Object; public abstract fun deserializeEnvelope (Ljava/io/InputStream;)Lio/sentry/SentryEnvelope; public abstract fun serialize (Lio/sentry/SentryEnvelope;Ljava/io/OutputStream;)V public abstract fun serialize (Ljava/lang/Object;Ljava/io/Writer;)V @@ -549,7 +548,6 @@ public abstract interface class io/sentry/JsonSerializable { public final class io/sentry/JsonSerializer : io/sentry/ISerializer { public fun (Lio/sentry/SentryOptions;)V public fun deserialize (Ljava/io/Reader;Ljava/lang/Class;)Ljava/lang/Object; - public fun deserialize (Ljava/io/Reader;Ljava/lang/Class;Lio/sentry/JsonDeserializer;)Ljava/lang/Object; public fun deserializeEnvelope (Ljava/io/InputStream;)Lio/sentry/SentryEnvelope; public fun serialize (Lio/sentry/SentryEnvelope;Ljava/io/OutputStream;)V public fun serialize (Ljava/lang/Object;Ljava/io/Writer;)V diff --git a/sentry/src/main/java/io/sentry/ISerializer.java b/sentry/src/main/java/io/sentry/ISerializer.java index 921530da7b..dd44d108cc 100644 --- a/sentry/src/main/java/io/sentry/ISerializer.java +++ b/sentry/src/main/java/io/sentry/ISerializer.java @@ -12,9 +12,6 @@ public interface ISerializer { @Nullable T deserialize(@NotNull Reader reader, @NotNull Class clazz); - @Nullable T deserialize( - @NotNull Reader reader, @NotNull Class clazz, @NotNull JsonDeserializer deserializer); - @Nullable SentryEnvelope deserializeEnvelope(@NotNull InputStream inputStream); diff --git a/sentry/src/main/java/io/sentry/JsonSerializer.java b/sentry/src/main/java/io/sentry/JsonSerializer.java index 887526c9e9..52c5d2e2da 100644 --- a/sentry/src/main/java/io/sentry/JsonSerializer.java +++ b/sentry/src/main/java/io/sentry/JsonSerializer.java @@ -106,28 +106,22 @@ public JsonSerializer(@NotNull SentryOptions options) { // Deserialize @Override - public @Nullable T deserialize( - @NotNull Reader reader, @NotNull Class clazz, @NotNull JsonDeserializer deserializer) { + public @Nullable T deserialize(@NotNull Reader reader, @NotNull Class clazz) { try { JsonObjectReader jsonObjectReader = new JsonObjectReader(reader); - Object object = deserializer.deserialize(jsonObjectReader, options.getLogger()); - return clazz.cast(object); + JsonDeserializer deserializer = deserializersByClass.get(clazz); + if (deserializer != null) { + Object object = deserializer.deserialize(jsonObjectReader, options.getLogger()); + return clazz.cast(object); + } else { + return null; // No way to deserialize objects we don't know about. + } } catch (Exception e) { options.getLogger().log(SentryLevel.ERROR, "Error when deserializing", e); return null; } } - @Override - public @Nullable T deserialize(@NotNull Reader reader, @NotNull Class clazz) { - JsonDeserializer deserializer = deserializersByClass.get(clazz); - if (deserializer != null) { - return deserialize(reader, clazz, deserializer); - } else { - return null; // No way to deserialize objects we don't know about. - } - } - @Override public @Nullable SentryEnvelope deserializeEnvelope(@NotNull InputStream inputStream) { Objects.requireNonNull(inputStream, "The InputStream object is required."); diff --git a/sentry/src/main/java/io/sentry/NoOpSerializer.java b/sentry/src/main/java/io/sentry/NoOpSerializer.java index a15eba2a0d..ad517e2e5f 100644 --- a/sentry/src/main/java/io/sentry/NoOpSerializer.java +++ b/sentry/src/main/java/io/sentry/NoOpSerializer.java @@ -25,12 +25,6 @@ private NoOpSerializer() {} return null; } - @Override - public @Nullable T deserialize( - @NotNull Reader reader, @NotNull Class clazz, @NotNull JsonDeserializer deserializer) { - return null; - } - @Override public @Nullable SentryEnvelope deserializeEnvelope(@NotNull InputStream inputStream) { return null; From d58842ac3f86294b176c6d48d88443f347692775 Mon Sep 17 00:00:00 2001 From: Lukas Bloder Date: Sun, 3 Jul 2022 23:39:03 +0200 Subject: [PATCH 10/12] update tests to use TraceSamplingDecision --- .../java/io/sentry/apollo3/SentryApollo3InterceptorTest.kt | 3 ++- .../apollo3/SentryApollo3InterceptorWithVariablesTest.kt | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) 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 index 38702971f8..cff354aecd 100644 --- a/sentry-apollo-3/src/test/java/io/sentry/apollo3/SentryApollo3InterceptorTest.kt +++ b/sentry-apollo-3/src/test/java/io/sentry/apollo3/SentryApollo3InterceptorTest.kt @@ -16,6 +16,7 @@ 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 @@ -235,7 +236,7 @@ class SentryApollo3InterceptorTest { private fun executeQuery(sut: ApolloClient = fixture.getSut(), isSpanActive: Boolean = true) = runBlocking { var tx: ITransaction? = null if (isSpanActive) { - tx = SentryTracer(TransactionContext("op", "desc", true), fixture.hub) + tx = SentryTracer(TransactionContext("op", "desc", TracesSamplingDecision(true)), fixture.hub) whenever(fixture.hub.span).thenReturn(tx) } 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 index ccc2f5e7bb..1079dc0d0b 100644 --- a/sentry-apollo-3/src/test/java/io/sentry/apollo3/SentryApollo3InterceptorWithVariablesTest.kt +++ b/sentry-apollo-3/src/test/java/io/sentry/apollo3/SentryApollo3InterceptorWithVariablesTest.kt @@ -14,6 +14,7 @@ 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 @@ -151,7 +152,7 @@ class SentryApollo3InterceptorWithVariablesTest { private fun executeQuery(sut: ApolloClient = fixture.getSut(), isSpanActive: Boolean = true) = runBlocking { var tx: ITransaction? = null if (isSpanActive) { - tx = SentryTracer(TransactionContext("op", "desc", true), fixture.hub) + tx = SentryTracer(TransactionContext("op", "desc", TracesSamplingDecision(true)), fixture.hub) whenever(fixture.hub.span).thenReturn(tx) } From 1eba47c2df1f35e10d4bed58d8075be6b0561002 Mon Sep 17 00:00:00 2001 From: Lukas Bloder Date: Mon, 11 Jul 2022 08:30:58 +0200 Subject: [PATCH 11/12] add secondary extension for java compatibility, add apollo to craft.yml --- .craft.yml | 1 + .../main/java/io/sentry/apollo3/apolloBuilderExtensions.kt | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/.craft.yml b/.craft.yml index 7b791bc567..3a865bd69c 100644 --- a/.craft.yml +++ b/.craft.yml @@ -43,3 +43,4 @@ targets: maven:io.sentry:sentry-graphql: maven:io.sentry:sentry-android-navigation: maven:io.sentry:sentry-compose: + maven:io.sentry:sentry-apollo3: diff --git a/sentry-apollo-3/src/main/java/io/sentry/apollo3/apolloBuilderExtensions.kt b/sentry-apollo-3/src/main/java/io/sentry/apollo3/apolloBuilderExtensions.kt index 584d02d78c..e4b18622c8 100644 --- a/sentry-apollo-3/src/main/java/io/sentry/apollo3/apolloBuilderExtensions.kt +++ b/sentry-apollo-3/src/main/java/io/sentry/apollo3/apolloBuilderExtensions.kt @@ -10,3 +10,7 @@ fun ApolloClient.Builder.sentryTracing(hub: IHub = HubAdapter.getInstance(), bef addHttpInterceptor(SentryApollo3HttpInterceptor(hub, beforeSpan)) return this } + +fun ApolloClient.Builder.sentryTracing(beforeSpan: SentryApollo3HttpInterceptor.BeforeSpanCallback? = null): ApolloClient.Builder { + return sentryTracing(HubAdapter.getInstance(), beforeSpan) +} From 5f906ab8d5dcaa22fb86e2025ca02c28e648c43a Mon Sep 17 00:00:00 2001 From: Lukas Bloder Date: Mon, 11 Jul 2022 12:08:39 +0200 Subject: [PATCH 12/12] update api --- sentry-apollo-3/api/sentry-apollo-3.api | 16 +++++++++------- ...sions.kt => sentryApolloBuilderExtensions.kt} | 0 2 files changed, 9 insertions(+), 7 deletions(-) rename sentry-apollo-3/src/main/java/io/sentry/apollo3/{apolloBuilderExtensions.kt => sentryApolloBuilderExtensions.kt} (100%) diff --git a/sentry-apollo-3/api/sentry-apollo-3.api b/sentry-apollo-3/api/sentry-apollo-3.api index 76f3a7595a..a41facc18a 100644 --- a/sentry-apollo-3/api/sentry-apollo-3.api +++ b/sentry-apollo-3/api/sentry-apollo-3.api @@ -1,10 +1,3 @@ -public final class io/sentry/apollo3/ApolloBuilderExtensionsKt { - 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 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 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; @@ -30,3 +23,12 @@ public final class io/sentry/apollo3/SentryApollo3Interceptor : com/apollographq 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/src/main/java/io/sentry/apollo3/apolloBuilderExtensions.kt b/sentry-apollo-3/src/main/java/io/sentry/apollo3/sentryApolloBuilderExtensions.kt similarity index 100% rename from sentry-apollo-3/src/main/java/io/sentry/apollo3/apolloBuilderExtensions.kt rename to sentry-apollo-3/src/main/java/io/sentry/apollo3/sentryApolloBuilderExtensions.kt