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

Encode/Decode internal apollo3 headers #2707

Merged
merged 7 commits into from
May 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
### Fixes

- Ensure screenshots and view hierarchies are captured on the main thread ([#2712](https://github.com/getsentry/sentry-java/pull/2712))
- Base64 encode internal Apollo3 Headers ([#2707](https://github.com/getsentry/sentry-java/pull/2707))

## 6.19.0

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import io.sentry.SpanStatus
import io.sentry.TypeCheckHint
import io.sentry.util.PropagationTargetsUtils
import io.sentry.util.UrlUtils
import io.sentry.vendor.Base64

class SentryApollo3HttpInterceptor @JvmOverloads constructor(private val hub: IHub = HubAdapter.getInstance(), private val beforeSpan: BeforeSpanCallback? = null) :
HttpInterceptor, IntegrationName {
Expand Down Expand Up @@ -96,10 +97,11 @@ class SentryApollo3HttpInterceptor @JvmOverloads constructor(private val hub: IH
val method = request.method

val operationName = operationNameFromHeaders(request)
val operationType = request.valueForHeader(SENTRY_APOLLO_3_OPERATION_TYPE)
val operationType = decodeHeaderValue(request, SENTRY_APOLLO_3_OPERATION_TYPE)
val operation = if (operationType != null) "http.graphql.$operationType" else "http.graphql"
val operationId = request.valueForHeader("X-APOLLO-OPERATION-ID")
val variables = request.valueForHeader(SENTRY_APOLLO_3_VARIABLES)
val variables = decodeHeaderValue(request, SENTRY_APOLLO_3_VARIABLES)

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

return activeSpan.startChild(operation, description).apply {
Expand All @@ -116,7 +118,19 @@ class SentryApollo3HttpInterceptor @JvmOverloads constructor(private val hub: IH
}

private fun operationNameFromHeaders(request: HttpRequest): String? {
return request.valueForHeader(SENTRY_APOLLO_3_OPERATION_NAME) ?: request.valueForHeader("X-APOLLO-OPERATION-NAME")
return decodeHeaderValue(request, SENTRY_APOLLO_3_OPERATION_NAME)
?: request.valueForHeader("X-APOLLO-OPERATION-NAME")
}

private fun decodeHeaderValue(request: HttpRequest, headerName: String): String? {
return request.valueForHeader(headerName)?.let {
try {
String(Base64.decode(it, Base64.DEFAULT))
} catch (e: IllegalArgumentException) {
hub.options.logger.log(SentryLevel.ERROR, "Error decoding internal apolloHeader $headerName", e)
return null
}
}
}

private fun HttpRequest.valueForHeader(key: String) = headers.firstOrNull { it.name == key }?.value
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ 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.apollo3.SentryApollo3HttpInterceptor.Companion.SENTRY_APOLLO_3_OPERATION_NAME
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 {
Expand All @@ -19,11 +23,11 @@ class SentryApollo3Interceptor : ApolloInterceptor {
chain: ApolloInterceptorChain
): Flow<ApolloResponse<D>> {
val builder = request.newBuilder()
.addHttpHeader(SentryApollo3HttpInterceptor.SENTRY_APOLLO_3_OPERATION_TYPE, operationType(request))
.addHttpHeader(SentryApollo3HttpInterceptor.SENTRY_APOLLO_3_OPERATION_NAME, request.operation.name())
.addHttpHeader(SENTRY_APOLLO_3_OPERATION_TYPE, Base64.encodeToString(operationType(request).toByteArray(), Base64.DEFAULT))
.addHttpHeader(SENTRY_APOLLO_3_OPERATION_NAME, Base64.encodeToString(request.operation.name().toByteArray(), Base64.DEFAULT))

request.scalarAdapters?.let {
builder.addHttpHeader(SentryApollo3HttpInterceptor.SENTRY_APOLLO_3_VARIABLES, request.operation.variables(it).valueMap.toString())
builder.addHttpHeader(SENTRY_APOLLO_3_VARIABLES, Base64.encodeToString(request.operation.variables(it).valueMap.toString().toByteArray(), Base64.DEFAULT))
}
return chain.proceed(builder.build())
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,7 @@ class SentryApollo3InterceptorTest {
}
}

private fun executeQuery(sut: ApolloClient = fixture.getSut(), isSpanActive: Boolean = true) = runBlocking {
private fun executeQuery(sut: ApolloClient = fixture.getSut(), isSpanActive: Boolean = true, id: String = "83") = runBlocking {
var tx: ITransaction? = null
if (isSpanActive) {
tx = SentryTracer(TransactionContext("op", "desc", TracesSamplingDecision(true)), fixture.hub)
Expand All @@ -274,7 +274,7 @@ class SentryApollo3InterceptorTest {

val coroutine = launch {
try {
sut.query(LaunchDetailsQuery("83")).execute()
sut.query(LaunchDetailsQuery(id)).execute()
} catch (e: ApolloException) {
return@launch
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,21 @@ class SentryApollo3InterceptorWithVariablesTest {
)
}

@Test
fun `handles non-ascii header values correctly`() {
executeQuery(id = "á")

verify(fixture.hub).captureTransaction(
check {
assertTransactionDetails(it)
assertEquals(SpanStatus.OK, it.spans.first().status)
},
anyOrNull<TraceContext>(),
anyOrNull(),
anyOrNull()
)
}

@Test
fun `adds breadcrumb when http calls succeeds`() {
executeQuery(fixture.getSut())
Expand Down Expand Up @@ -153,7 +168,7 @@ class SentryApollo3InterceptorWithVariablesTest {
}
}

private fun executeQuery(sut: ApolloClient = fixture.getSut(), isSpanActive: Boolean = true) = runBlocking {
private fun executeQuery(sut: ApolloClient = fixture.getSut(), isSpanActive: Boolean = true, id: String = "83") = runBlocking {
var tx: ITransaction? = null
if (isSpanActive) {
tx = SentryTracer(TransactionContext("op", "desc", TracesSamplingDecision(true)), fixture.hub)
Expand All @@ -162,7 +177,7 @@ class SentryApollo3InterceptorWithVariablesTest {

val coroutine = launch {
try {
sut.query(LaunchDetailsQuery("83")).execute()
sut.query(LaunchDetailsQuery(id)).execute()
} catch (e: ApolloException) {
return@launch
}
Expand Down