Skip to content
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