Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add GraphQL Apollo Kotlin 4 integration #4166

Merged
merged 12 commits into from
Feb 24, 2025
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## Unreleased

### Features

- Add Apollo 4 integration ([#4166](https://github.com/getsentry/sentry-java/pull/4166))

## 8.2.0

### Breaking Changes
Expand Down
37 changes: 18 additions & 19 deletions sentry-apollo-4/api/sentry-apollo-4.api
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,17 @@ public final class io/sentry/apollo4/BuildConfig {
public static final field VERSION_NAME Ljava/lang/String;
}

public final class io/sentry/apollo4/SentryApollo4BuilderExtensionsKt {
public static final fun sentryTracing (Lcom/apollographql/apollo/ApolloClient$Builder;)Lcom/apollographql/apollo/ApolloClient$Builder;
public static final fun sentryTracing (Lcom/apollographql/apollo/ApolloClient$Builder;Lio/sentry/IScopes;)Lcom/apollographql/apollo/ApolloClient$Builder;
public static final fun sentryTracing (Lcom/apollographql/apollo/ApolloClient$Builder;Lio/sentry/IScopes;Z)Lcom/apollographql/apollo/ApolloClient$Builder;
public static final fun sentryTracing (Lcom/apollographql/apollo/ApolloClient$Builder;Lio/sentry/IScopes;ZLjava/util/List;)Lcom/apollographql/apollo/ApolloClient$Builder;
public static final fun sentryTracing (Lcom/apollographql/apollo/ApolloClient$Builder;Lio/sentry/IScopes;ZLjava/util/List;Lio/sentry/apollo4/SentryApollo4HttpInterceptor$BeforeSpanCallback;)Lcom/apollographql/apollo/ApolloClient$Builder;
public static final fun sentryTracing (Lcom/apollographql/apollo/ApolloClient$Builder;ZLjava/util/List;Lio/sentry/apollo4/SentryApollo4HttpInterceptor$BeforeSpanCallback;)Lcom/apollographql/apollo/ApolloClient$Builder;
public static synthetic fun sentryTracing$default (Lcom/apollographql/apollo/ApolloClient$Builder;Lio/sentry/IScopes;ZLjava/util/List;Lio/sentry/apollo4/SentryApollo4HttpInterceptor$BeforeSpanCallback;ILjava/lang/Object;)Lcom/apollographql/apollo/ApolloClient$Builder;
public static synthetic fun sentryTracing$default (Lcom/apollographql/apollo/ApolloClient$Builder;ZLjava/util/List;Lio/sentry/apollo4/SentryApollo4HttpInterceptor$BeforeSpanCallback;ILjava/lang/Object;)Lcom/apollographql/apollo/ApolloClient$Builder;
}

public final class io/sentry/apollo4/SentryApollo4ClientException : java/lang/Exception {
public static final field Companion Lio/sentry/apollo4/SentryApollo4ClientException$Companion;
public fun <init> (Ljava/lang/String;)V
Expand All @@ -11,41 +22,29 @@ public final class io/sentry/apollo4/SentryApollo4ClientException : java/lang/Ex
public final class io/sentry/apollo4/SentryApollo4ClientException$Companion {
}

public final class io/sentry/apollo4/SentryApollo4HttpInterceptor : com/apollographql/apollo4/network/http/HttpInterceptor {
public final class io/sentry/apollo4/SentryApollo4HttpInterceptor : com/apollographql/apollo/network/http/HttpInterceptor {
public static final field Companion Lio/sentry/apollo4/SentryApollo4HttpInterceptor$Companion;
public static final field DEFAULT_CAPTURE_FAILED_REQUESTS Z
public static final field SENTRY_APOLLO_3_OPERATION_TYPE Ljava/lang/String;
public static final field SENTRY_APOLLO_3_VARIABLES Ljava/lang/String;
public fun <init> ()V
public fun <init> (Lio/sentry/IScopes;)V
public fun <init> (Lio/sentry/IScopes;Lio/sentry/apollo4/SentryApollo4HttpInterceptor$BeforeSpanCallback;)V
public fun <init> (Lio/sentry/IScopes;Lio/sentry/apollo4/SentryApollo4HttpInterceptor$BeforeSpanCallback;Z)V
public fun <init> (Lio/sentry/IScopes;Lio/sentry/apollo4/SentryApollo4HttpInterceptor$BeforeSpanCallback;ZLjava/util/List;)V
public synthetic fun <init> (Lio/sentry/IScopes;Lio/sentry/apollo4/SentryApollo4HttpInterceptor$BeforeSpanCallback;ZLjava/util/List;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun dispose ()V
public fun intercept (Lcom/apollographql/apollo4/api/http/HttpRequest;Lcom/apollographql/apollo4/network/http/HttpInterceptorChain;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun intercept (Lcom/apollographql/apollo/api/http/HttpRequest;Lcom/apollographql/apollo/network/http/HttpInterceptorChain;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
}

public abstract interface class io/sentry/apollo4/SentryApollo4HttpInterceptor$BeforeSpanCallback {
public abstract fun execute (Lio/sentry/ISpan;Lcom/apollographql/apollo4/api/http/HttpRequest;Lcom/apollographql/apollo4/api/http/HttpResponse;)Lio/sentry/ISpan;
public abstract fun execute (Lio/sentry/ISpan;Lcom/apollographql/apollo/api/http/HttpRequest;Lcom/apollographql/apollo/api/http/HttpResponse;)Lio/sentry/ISpan;
}

public final class io/sentry/apollo4/SentryApollo4HttpInterceptor$Companion {
}

public final class io/sentry/apollo4/SentryApollo4Interceptor : com/apollographql/apollo4/interceptor/ApolloInterceptor {
public final class io/sentry/apollo4/SentryApollo4Interceptor : com/apollographql/apollo/interceptor/ApolloInterceptor {
public fun <init> ()V
public fun intercept (Lcom/apollographql/apollo4/api/ApolloRequest;Lcom/apollographql/apollo4/interceptor/ApolloInterceptorChain;)Lkotlinx/coroutines/flow/Flow;
}

public final class io/sentry/apollo4/SentryApolloBuilderExtensionsKt {
public static final fun sentryTracing (Lcom/apollographql/apollo4/ApolloClient$Builder;)Lcom/apollographql/apollo4/ApolloClient$Builder;
public static final fun sentryTracing (Lcom/apollographql/apollo4/ApolloClient$Builder;Lio/sentry/IScopes;)Lcom/apollographql/apollo4/ApolloClient$Builder;
public static final fun sentryTracing (Lcom/apollographql/apollo4/ApolloClient$Builder;Lio/sentry/IScopes;Z)Lcom/apollographql/apollo4/ApolloClient$Builder;
public static final fun sentryTracing (Lcom/apollographql/apollo4/ApolloClient$Builder;Lio/sentry/IScopes;ZLjava/util/List;)Lcom/apollographql/apollo4/ApolloClient$Builder;
public static final fun sentryTracing (Lcom/apollographql/apollo4/ApolloClient$Builder;Lio/sentry/IScopes;ZLjava/util/List;Lio/sentry/apollo4/SentryApollo4HttpInterceptor$BeforeSpanCallback;)Lcom/apollographql/apollo4/ApolloClient$Builder;
public static final fun sentryTracing (Lcom/apollographql/apollo4/ApolloClient$Builder;ZLjava/util/List;Lio/sentry/apollo4/SentryApollo4HttpInterceptor$BeforeSpanCallback;)Lcom/apollographql/apollo4/ApolloClient$Builder;
public static synthetic fun sentryTracing$default (Lcom/apollographql/apollo4/ApolloClient$Builder;Lio/sentry/IScopes;ZLjava/util/List;Lio/sentry/apollo4/SentryApollo4HttpInterceptor$BeforeSpanCallback;ILjava/lang/Object;)Lcom/apollographql/apollo4/ApolloClient$Builder;
public static synthetic fun sentryTracing$default (Lcom/apollographql/apollo4/ApolloClient$Builder;ZLjava/util/List;Lio/sentry/apollo4/SentryApollo4HttpInterceptor$BeforeSpanCallback;ILjava/lang/Object;)Lcom/apollographql/apollo4/ApolloClient$Builder;
public fun <init> (Lio/sentry/IScopes;)V
public synthetic fun <init> (Lio/sentry/IScopes;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun intercept (Lcom/apollographql/apollo/api/ApolloRequest;Lcom/apollographql/apollo/interceptor/ApolloInterceptorChain;)Lkotlinx/coroutines/flow/Flow;
}

2 changes: 2 additions & 0 deletions sentry-apollo-4/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ dependencies {
testImplementation(Config.TestLibs.mockitoInline)
testImplementation(Config.TestLibs.mockWebserver)
testImplementation(Config.Libs.apolloKotlin4)
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3")
testImplementation("org.jetbrains.kotlin:kotlin-reflect:2.0.0")
}

configure<SourceSetContainer> {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package io.sentry.apollo4

/**
* Common constants used across the module
*/
internal const val OPERATION_ID_HEADER_NAME = "SENTRY-APOLLO-4-OPERATION-ID"
internal const val OPERATION_NAME_HEADER_NAME = "SENTRY-APOLLO-4-OPERATION-NAME"
internal const val OPERATION_TYPE_HEADER_NAME = "SENTRY-APOLLO-4-OPERATION-TYPE"
internal const val VARIABLES_HEADER_NAME = "SENTRY-APOLLO-4-VARIABLES"
internal val INTERNAL_HEADER_NAMES by lazy {
listOf(
OPERATION_ID_HEADER_NAME,
OPERATION_NAME_HEADER_NAME,
OPERATION_TYPE_HEADER_NAME,
VARIABLES_HEADER_NAME
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@ package io.sentry.apollo4
*/
class SentryApollo4ClientException(message: String?) : Exception(message) {
companion object {
private const val serialVersionUID = 4312120066430858144L
private const val serialVersionUID = 4312160066430858144L
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
package io.sentry.apollo4

import com.apollographql.apollo.api.http.DefaultHttpRequestComposer.Companion.HEADER_APOLLO_OPERATION_ID
import com.apollographql.apollo.api.http.DefaultHttpRequestComposer.Companion.HEADER_APOLLO_OPERATION_NAME
import com.apollographql.apollo.api.http.HttpHeader
import com.apollographql.apollo.api.http.HttpRequest
import com.apollographql.apollo.api.http.HttpResponse
Expand Down Expand Up @@ -68,9 +66,9 @@ class SentryApollo4HttpInterceptor @JvmOverloads constructor(
): HttpResponse {
val activeSpan = if (Platform.isAndroid()) scopes.transaction else scopes.span

val operationName = getHeader("X-APOLLO-OPERATION-NAME", request.headers)
val operationType = decodeHeaderValue(request, SENTRY_APOLLO_4_OPERATION_TYPE)
val operationId = getHeader("X-APOLLO-OPERATION-ID", request.headers)
val operationId = decodeHeaderValue(request, OPERATION_ID_HEADER_NAME)
val operationName = decodeHeaderValue(request, OPERATION_NAME_HEADER_NAME)
val operationType = decodeHeaderValue(request, OPERATION_TYPE_HEADER_NAME)

var span: ISpan? = null

Expand Down Expand Up @@ -140,13 +138,12 @@ class SentryApollo4HttpInterceptor @JvmOverloads constructor(
}

private fun isIgnored(): Boolean {
return SpanUtils.isIgnored(scopes.getOptions().getIgnoredSpanOrigins(), TRACE_ORIGIN)
return SpanUtils.isIgnored(scopes.getOptions().ignoredSpanOrigins, TRACE_ORIGIN)
}

private fun removeSentryInternalHeaders(headers: List<HttpHeader>): List<HttpHeader> {
return headers.filterNot {
it.name.equals(SENTRY_APOLLO_4_VARIABLES, true) ||
it.name.equals(SENTRY_APOLLO_4_OPERATION_TYPE, true)
return headers.filterNot { header ->
INTERNAL_HEADER_NAMES.any { internalHeader -> header.name.equals(internalHeader, true) }
}
}

Expand All @@ -161,7 +158,7 @@ class SentryApollo4HttpInterceptor @JvmOverloads constructor(
val method = request.method.name

val operation = if (operationType != null) "http.graphql.$operationType" else "http.graphql"
val variables = decodeHeaderValue(request, SENTRY_APOLLO_4_VARIABLES)
val variables = decodeHeaderValue(request, VARIABLES_HEADER_NAME)

val description = "${operationType ?: method} ${operationName ?: urlDetails.urlOrFallback}"

Expand All @@ -177,7 +174,7 @@ class SentryApollo4HttpInterceptor @JvmOverloads constructor(
variables?.let {
setData("variables", it)
}
setData(HTTP_METHOD_KEY, method.toUpperCase(Locale.ROOT))
setData(HTTP_METHOD_KEY, method.uppercase(Locale.ROOT))
}
}

Expand Down Expand Up @@ -227,7 +224,7 @@ class SentryApollo4HttpInterceptor @JvmOverloads constructor(
} catch (e: Throwable) {
scopes.options.logger.log(
SentryLevel.ERROR,
"An error occurred while executing beforeSpan on ApolloInterceptor",
"An error occurred while executing beforeSpan in ApolloInterceptor",
e
)
}
Expand Down Expand Up @@ -327,7 +324,7 @@ class SentryApollo4HttpInterceptor @JvmOverloads constructor(
return
}

// if there response body does not have the errors field, do not raise an issue
// if the response body does not have the errors field, do not raise an issue
if (body.isEmpty() || !regex.containsMatchIn(body)) {
return
}
Expand All @@ -340,7 +337,7 @@ class SentryApollo4HttpInterceptor @JvmOverloads constructor(
// but that's not possible
val urlDetails = UrlUtils.parse(request.url)

// return if its not a target match
// return if it's not a target match
if (!PropagationTargetsUtils.contain(failedRequestTargets, urlDetails.urlOrFallback)) {
return
}
Expand Down Expand Up @@ -451,8 +448,6 @@ class SentryApollo4HttpInterceptor @JvmOverloads constructor(
}

companion object {
const val SENTRY_APOLLO_4_VARIABLES = "SENTRY-APOLLO-4-VARIABLES"
const val SENTRY_APOLLO_4_OPERATION_TYPE = "SENTRY-APOLLO-4-OPERATION-TYPE"
const val DEFAULT_CAPTURE_FAILED_REQUESTS = true
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,29 +10,41 @@ import com.apollographql.apollo.api.Subscription
import com.apollographql.apollo.api.variables
import com.apollographql.apollo.interceptor.ApolloInterceptor
import com.apollographql.apollo.interceptor.ApolloInterceptorChain
import io.sentry.apollo4.SentryApollo4HttpInterceptor.Companion.SENTRY_APOLLO_4_OPERATION_TYPE
import io.sentry.apollo4.SentryApollo4HttpInterceptor.Companion.SENTRY_APOLLO_4_VARIABLES
import io.sentry.IScopes
import io.sentry.ScopesAdapter
import io.sentry.vendor.Base64
import kotlinx.coroutines.flow.Flow
import org.jetbrains.annotations.ApiStatus

class SentryApollo4Interceptor : ApolloInterceptor {
/**
* Interceptor that adds the GraphQL request information to the outgoing HTTP request's headers so that
* the information can be accessed by {@link SentryApollo4HttpInterceptor}
*/
class SentryApollo4Interceptor @JvmOverloads constructor(
@ApiStatus.Internal private val scopes: IScopes = ScopesAdapter.getInstance()
) : ApolloInterceptor {

override fun <D : Operation.Data> intercept(
request: ApolloRequest<D>,
chain: ApolloInterceptorChain
): Flow<ApolloResponse<D>> {
val builder = request.newBuilder()
.addHttpHeader(SENTRY_APOLLO_4_OPERATION_TYPE, Base64.encodeToString(operationType(request).toByteArray(), Base64.NO_WRAP))
.addHttpHeader(OPERATION_ID_HEADER_NAME, encodeHeaderValue(request.operation.id()))
.addHttpHeader(OPERATION_NAME_HEADER_NAME, encodeHeaderValue(request.operation.name()))
.addHttpHeader(OPERATION_TYPE_HEADER_NAME, encodeHeaderValue(operationType(request)))

request.scalarAdapters?.let {
builder.addHttpHeader(SENTRY_APOLLO_4_VARIABLES, Base64.encodeToString(request.operation.variables(it).valueMap.toString().toByteArray(), Base64.NO_WRAP))
builder.addHttpHeader(VARIABLES_HEADER_NAME, encodeHeaderValue(request.operation.variables(it).valueMap.toString()))
}
builder.addHttpHeader("X-APOLLO-OPERATION-NAME", request.operation.name())
builder.addHttpHeader("X-APOLLO-OPERATION-ID", request.operation.id())

return chain.proceed(builder.build())
}
}

private fun encodeHeaderValue(value: String): String {
return Base64.encodeToString(value.toByteArray(), Base64.NO_WRAP)
}

private fun <D : Operation.Data> operationType(apolloRequest: ApolloRequest<D>) = when (apolloRequest.operation) {
is Query -> "query"
is Mutation -> "mutation"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package io.sentry.apollo4

import com.apollographql.apollo.ApolloCall
import com.apollographql.apollo.ApolloClient
import com.apollographql.apollo.api.ApolloResponse
import com.apollographql.apollo.api.Operation
import com.apollographql.apollo.api.http.HttpRequest
import com.apollographql.apollo.api.http.HttpResponse
import com.apollographql.apollo.exception.ApolloException
Expand All @@ -11,6 +14,7 @@ import io.sentry.SentryOptions
import io.sentry.SentryOptions.DEFAULT_PROPAGATION_TARGETS
import io.sentry.TypeCheckHint
import io.sentry.apollo4.SentryApollo4HttpInterceptor.Companion.DEFAULT_CAPTURE_FAILED_REQUESTS
import io.sentry.apollo4.generated.LaunchDetailsQuery
import io.sentry.exception.ExceptionMechanismException
import io.sentry.protocol.SdkVersion
import io.sentry.protocol.SentryId
Expand All @@ -25,14 +29,20 @@ import org.mockito.kotlin.mock
import org.mockito.kotlin.never
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever
import kotlin.reflect.KSuspendFunction1
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertNotNull
import kotlin.test.assertNull
import kotlin.test.assertTrue

class SentryApollo4InterceptorClientErrors {
class SentryApollo4BuilderExtensionsClientErrorsTestWithV4Implementation : SentryApollo4BuilderExtensionsClientErrorsTest(ApolloCall<*>::execute)
class SentryApollo4BuilderExtensionsClientErrorsTestWithV3Implementation : SentryApollo4BuilderExtensionsClientErrorsTest(ApolloCall<*>::executeV3)

abstract class SentryApollo4BuilderExtensionsClientErrorsTest(
private val executeQueryImplementation: KSuspendFunction1<ApolloCall<*>, ApolloResponse<out Operation.Data>>
) {
class Fixture {
val server = MockWebServer()
lateinit var scopes: IScopes
Expand Down Expand Up @@ -268,7 +278,6 @@ class SentryApollo4InterceptorClientErrors {

assertEquals("Test", request.cookies)
assertNotNull(request.headers)
assertEquals("LaunchDetails", request.headers?.get("X-APOLLO-OPERATION-NAME"))
},
any<Hint>()
)
Expand Down Expand Up @@ -379,7 +388,7 @@ class SentryApollo4InterceptorClientErrors {
private fun executeQuery(sut: ApolloClient, id: String = "83") = runBlocking {
val coroutine = launch {
try {
sut.query(LaunchDetailsQuery(id)).execute()
executeQueryImplementation(sut.query(LaunchDetailsQuery(id)))
} catch (e: ApolloException) {
return@launch
}
Expand Down
Loading
Loading