Skip to content
2 changes: 2 additions & 0 deletions sentry-apollo-3/api/sentry-apollo-3.api
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ public final class io/sentry/apollo3/SentryApollo3HttpInterceptor$Companion {

public final class io/sentry/apollo3/SentryApollo3Interceptor : com/apollographql/apollo3/interceptor/ApolloInterceptor {
public fun <init> ()V
public fun <init> (Lio/sentry/IScopes;)V
public synthetic fun <init> (Lio/sentry/IScopes;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun intercept (Lcom/apollographql/apollo3/api/ApolloRequest;Lcom/apollographql/apollo3/interceptor/ApolloInterceptorChain;)Lkotlinx/coroutines/flow/Flow;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import io.sentry.exception.ExceptionMechanismException
import io.sentry.protocol.Mechanism
import io.sentry.protocol.Request
import io.sentry.protocol.Response
import io.sentry.util.GraphqlUtils
import io.sentry.util.HttpUtils
import io.sentry.util.IntegrationUtils.addIntegrationToSdkVersion
import io.sentry.util.Platform
Expand Down Expand Up @@ -174,7 +175,9 @@ constructor(

operationId?.let { setData("operationId", it) }

variables?.let { setData("variables", it) }
if (scopes.options.dataCollectionResolver.isGraphqlVariablesWithLegacyAlways) {
variables?.let { setData("variables", it) }
}
setData(HTTP_METHOD_KEY, method.uppercase())
}
}
Expand Down Expand Up @@ -366,7 +369,7 @@ constructor(

try {
it.writeTo(buffer)
data = buffer.readUtf8()
data = GraphqlUtils.filterRequestBody(buffer.readUtf8(), scopes.options)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GraphQL/Apollo also allow Http.Get for queries (https://graphql.org/learn/serving-over-http/#get-request-and-parameters) so we might need to filter that too, not just the request body.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In theory customers can just use:

dataCollection.setUrlQueryParams(
       KeyValueCollectionBehavior.denyList("query", "variables"));

It looks like other SDKs also aren't using the graphql options on GET params.
We can just mention this in docs.

} catch (e: Throwable) {
scopes.options.logger.log(SentryLevel.ERROR, "Error reading the request body.", e)
// continue because the response body alone can already give some insights
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,16 @@ 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 io.sentry.IScopes
import io.sentry.ScopesAdapter
import io.sentry.apollo3.SentryApollo3HttpInterceptor.Companion.SENTRY_APOLLO_3_OPERATION_TYPE
import io.sentry.apollo3.SentryApollo3HttpInterceptor.Companion.SENTRY_APOLLO_3_VARIABLES
import io.sentry.vendor.Base64
import kotlinx.coroutines.flow.Flow

class SentryApollo3Interceptor : ApolloInterceptor {
class SentryApollo3Interceptor
@JvmOverloads
constructor(private val scopes: IScopes = ScopesAdapter.getInstance()) : ApolloInterceptor {
override fun <D : Operation.Data> intercept(
request: ApolloRequest<D>,
chain: ApolloInterceptorChain,
Expand All @@ -28,14 +32,16 @@ class SentryApollo3Interceptor : ApolloInterceptor {
Base64.encodeToString(operationType(request).toByteArray(), Base64.NO_WRAP),
)

request.scalarAdapters?.let {
builder.addHttpHeader(
SENTRY_APOLLO_3_VARIABLES,
Base64.encodeToString(
request.operation.variables(it).valueMap.toString().toByteArray(),
Base64.NO_WRAP,
),
)
if (scopes.options.dataCollectionResolver.isGraphqlVariablesWithLegacyAlways) {
request.scalarAdapters?.let {
builder.addHttpHeader(
SENTRY_APOLLO_3_VARIABLES,
Base64.encodeToString(
request.operation.variables(it).valueMap.toString().toByteArray(),
Base64.NO_WRAP,
),
)
}
}
return chain.proceed(builder.build())
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ fun ApolloClient.Builder.sentryTracing(
failedRequestTargets: List<String> = listOf(DEFAULT_PROPAGATION_TARGETS),
beforeSpan: SentryApollo3HttpInterceptor.BeforeSpanCallback? = null,
): ApolloClient.Builder {
addInterceptor(SentryApollo3Interceptor())
addInterceptor(SentryApollo3Interceptor(scopes))
addHttpInterceptor(
SentryApollo3HttpInterceptor(
scopes = scopes,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ class SentryApollo3InterceptorClientErrors {
responseBody: String = responseBodyOk,
sendDefaultPii: Boolean = false,
socketPolicy: SocketPolicy = SocketPolicy.KEEP_OPEN,
configureOptions: SentryOptions.() -> Unit = {},
): ApolloClient {
SentryIntegrationPackageStorage.getInstance().clearStorage()

Expand All @@ -83,6 +84,7 @@ class SentryApollo3InterceptorClientErrors {
dsn = "https://key@sentry.io/proj"
sdkVersion = SdkVersion("test", "1.2.3")
isSendDefaultPii = sendDefaultPii
configureOptions()
}
)
}
Expand Down Expand Up @@ -266,6 +268,60 @@ class SentryApollo3InterceptorClientErrors {
)
}

@Test
fun `data collection can disable the GraphQL document independently`() {
val sut =
fixture.getSut(responseBody = fixture.responseBodyNotOk) {
dataCollection.graphql.setDocument(false)
}
executeQuery(sut)

verify(fixture.scopes)
.captureEvent(
check {
val body = it.request!!.data as String
assertFalse(body.contains("\"query\""))
assertTrue(body.contains("\"variables\""))
},
any<Hint>(),
)
}

@Test
fun `data collection can disable GraphQL variables independently`() {
val sut =
fixture.getSut(responseBody = fixture.responseBodyNotOk) {
dataCollection.graphql.setVariables(false)
}
executeQuery(sut)

verify(fixture.scopes)
.captureEvent(
check {
val body = it.request!!.data as String
assertTrue(body.contains("\"query\""))
assertFalse(body.contains("\"variables\""))
},
any<Hint>(),
)
}

@Test
fun `data collection can disable the GraphQL request body`() {
val sut =
fixture.getSut(responseBody = fixture.responseBodyNotOk) {
dataCollection.graphql.setDocument(false)
dataCollection.graphql.setVariables(false)
}
executeQuery(sut)

verify(fixture.scopes)
.captureEvent(
check { assertNull(it.request!!.data) },
any<Hint>(),
)
}

@Test
fun `capture errors with more request context if sendDefaultPii is enabled`() {
val sut = fixture.getSut(responseBody = fixture.responseBodyNotOk, sendDefaultPii = true)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,9 @@ class SentryApollo3InterceptorWithVariablesTest {
}""",
socketPolicy: SocketPolicy = SocketPolicy.KEEP_OPEN,
beforeSpan: BeforeSpanCallback? = null,
options: SentryOptions = SentryOptions().apply { dsn = "http://key@localhost/proj" },
): ApolloClient {
whenever(scopes.options)
.thenReturn(SentryOptions().apply { dsn = "http://key@localhost/proj" })
whenever(scopes.options).thenReturn(options)

server.enqueue(
MockResponse()
Expand Down Expand Up @@ -91,6 +91,28 @@ class SentryApollo3InterceptorWithVariablesTest {
)
}

@Test
fun `does not attach GraphQL variables when data collection disables them`() {
val options =
SentryOptions().apply {
dsn = "http://key@localhost/proj"
dataCollection.graphql.setVariables(false)
}

executeQuery(fixture.getSut(options = options))

verify(fixture.scopes)
.captureTransaction(
check {
assertNull(it.spans.first().data?.get("variables"))
assertNotNull(it.spans.first().data?.get("operationId"))
},
anyOrNull<TraceContext>(),
anyOrNull(),
anyOrNull(),
)
}

@Test
fun `creates a span around the failed request`() {
executeQuery(fixture.getSut(httpStatusCode = 403))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import io.sentry.exception.ExceptionMechanismException
import io.sentry.protocol.Mechanism
import io.sentry.protocol.Request
import io.sentry.protocol.Response
import io.sentry.util.GraphqlUtils
import io.sentry.util.HttpUtils
import io.sentry.util.IntegrationUtils.addIntegrationToSdkVersion
import io.sentry.util.Platform
Expand Down Expand Up @@ -173,7 +174,9 @@ constructor(

operationId?.let { setData("operationId", it) }

variables?.let { setData("variables", it) }
if (scopes.options.dataCollectionResolver.isGraphqlVariablesWithLegacyAlways) {
variables?.let { setData("variables", it) }
}
setData(HTTP_METHOD_KEY, method.uppercase(Locale.ROOT))
}
}
Expand Down Expand Up @@ -365,7 +368,7 @@ constructor(

try {
it.writeTo(buffer)
data = buffer.readUtf8()
data = GraphqlUtils.filterRequestBody(buffer.readUtf8(), scopes.options)
} catch (e: Throwable) {
scopes.options.logger.log(SentryLevel.ERROR, "Error reading the request body.", e)
// continue because the response body alone can already give some insights
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,13 @@ constructor(@ApiStatus.Internal private val scopes: IScopes = ScopesAdapter.getI
.addHttpHeader(OPERATION_NAME_HEADER_NAME, encodeHeaderValue(request.operation.name()))
.addHttpHeader(OPERATION_TYPE_HEADER_NAME, encodeHeaderValue(operationType(request)))

request.scalarAdapters?.let {
builder.addHttpHeader(
VARIABLES_HEADER_NAME,
encodeHeaderValue(request.operation.variables(it).valueMap.toString()),
)
if (scopes.options.dataCollectionResolver.isGraphqlVariablesWithLegacyAlways) {
request.scalarAdapters?.let {
builder.addHttpHeader(
VARIABLES_HEADER_NAME,
encodeHeaderValue(request.operation.variables(it).valueMap.toString()),
)
}
}

return chain.proceed(builder.build())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ fun ApolloClient.Builder.sentryTracing(
failedRequestTargets: List<String> = listOf(DEFAULT_PROPAGATION_TARGETS),
beforeSpan: SentryApollo4HttpInterceptor.BeforeSpanCallback? = null,
): ApolloClient.Builder {
addInterceptor(SentryApollo4Interceptor())
addInterceptor(SentryApollo4Interceptor(scopes))
addHttpInterceptor(
SentryApollo4HttpInterceptor(
scopes = scopes,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ abstract class SentryApollo4BuilderExtensionsClientErrorsTest(
responseBody: String = responseBodyOk,
sendDefaultPii: Boolean = false,
socketPolicy: SocketPolicy = SocketPolicy.KEEP_OPEN,
configureOptions: SentryOptions.() -> Unit = {},
): ApolloClient {
SentryIntegrationPackageStorage.getInstance().clearStorage()

Expand All @@ -97,6 +98,7 @@ abstract class SentryApollo4BuilderExtensionsClientErrorsTest(
dsn = "https://key@sentry.io/proj"
sdkVersion = SdkVersion("test", "1.2.3")
isSendDefaultPii = sendDefaultPii
configureOptions()
}
)
}
Expand Down Expand Up @@ -280,6 +282,60 @@ abstract class SentryApollo4BuilderExtensionsClientErrorsTest(
)
}

@Test
fun `data collection can disable the GraphQL document independently`() {
val sut =
fixture.getSut(responseBody = fixture.responseBodyNotOk) {
dataCollection.graphql.setDocument(false)
}
executeQuery(sut)

verify(fixture.scopes)
.captureEvent(
check {
val body = it.request!!.data as String
assertFalse(body.contains("\"query\""))
assertTrue(body.contains("\"variables\""))
},
any<Hint>(),
)
}

@Test
fun `data collection can disable GraphQL variables independently`() {
val sut =
fixture.getSut(responseBody = fixture.responseBodyNotOk) {
dataCollection.graphql.setVariables(false)
}
executeQuery(sut)

verify(fixture.scopes)
.captureEvent(
check {
val body = it.request!!.data as String
assertTrue(body.contains("\"query\""))
assertFalse(body.contains("\"variables\""))
},
any<Hint>(),
)
}

@Test
fun `data collection can disable the GraphQL request body`() {
val sut =
fixture.getSut(responseBody = fixture.responseBodyNotOk) {
dataCollection.graphql.setDocument(false)
dataCollection.graphql.setVariables(false)
}
executeQuery(sut)

verify(fixture.scopes)
.captureEvent(
check { assertNull(it.request!!.data) },
any<Hint>(),
)
}

@Test
fun `capture errors with more request context if sendDefaultPii is enabled`() {
val sut = fixture.getSut(responseBody = fixture.responseBodyNotOk, sendDefaultPii = true)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import kotlin.reflect.KSuspendFunction1
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.test.assertNull
import kotlin.test.assertTrue
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
Expand Down Expand Up @@ -69,9 +70,9 @@ abstract class SentryApollo4BuilderExtensionsTest(
}""",
socketPolicy: SocketPolicy = SocketPolicy.KEEP_OPEN,
beforeSpan: BeforeSpanCallback? = null,
options: SentryOptions = SentryOptions().apply { dsn = "http://key@localhost/proj" },
): ApolloClient {
whenever(scopes.options)
.thenReturn(SentryOptions().apply { dsn = "http://key@localhost/proj" })
whenever(scopes.options).thenReturn(options)

server.enqueue(
MockResponse()
Expand Down Expand Up @@ -105,6 +106,28 @@ abstract class SentryApollo4BuilderExtensionsTest(
)
}

@Test
fun `does not attach GraphQL variables when data collection disables them`() {
val options =
SentryOptions().apply {
dsn = "http://key@localhost/proj"
dataCollection.graphql.setVariables(false)
}

executeQuery(fixture.getSut(options = options))

verify(fixture.scopes)
.captureTransaction(
check {
assertNull(it.spans.first().data?.get("variables"))
assertNotNull(it.spans.first().data?.get("operationId"))
},
anyOrNull<TraceContext>(),
anyOrNull(),
anyOrNull(),
)
}

@Test
fun `creates span around failed request`() {
executeQuery(fixture.getSut(httpStatusCode = 403))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,9 @@ class SentryApolloInterceptor(
val requestWithHeader = request.toBuilder().requestHeaders(headers).build()

span.setData("operationId", requestWithHeader.operation.operationId())
span.setData("variables", requestWithHeader.operation.variables().valueMap().toString())
if (scopes.options.dataCollectionResolver.isGraphqlVariablesWithLegacyAlways) {
span.setData("variables", requestWithHeader.operation.variables().valueMap().toString())
}

chain.proceedAsync(
requestWithHeader,
Expand Down
Loading
Loading