All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.sentry.apollo.SentryApolloInterceptor.kt Maven / Gradle / Ivy

package io.sentry.apollo

import com.apollographql.apollo.api.Mutation
import com.apollographql.apollo.api.Query
import com.apollographql.apollo.api.Subscription
import com.apollographql.apollo.exception.ApolloException
import com.apollographql.apollo.exception.ApolloHttpException
import com.apollographql.apollo.interceptor.ApolloInterceptor
import com.apollographql.apollo.interceptor.ApolloInterceptor.CallBack
import com.apollographql.apollo.interceptor.ApolloInterceptor.FetchSourceType
import com.apollographql.apollo.interceptor.ApolloInterceptor.InterceptorRequest
import com.apollographql.apollo.interceptor.ApolloInterceptor.InterceptorResponse
import com.apollographql.apollo.interceptor.ApolloInterceptorChain
import com.apollographql.apollo.request.RequestHeaders
import io.sentry.BaggageHeader
import io.sentry.Breadcrumb
import io.sentry.Hint
import io.sentry.HubAdapter
import io.sentry.IHub
import io.sentry.ISpan
import io.sentry.SentryIntegrationPackageStorage
import io.sentry.SentryLevel
import io.sentry.SpanDataConvention
import io.sentry.SpanStatus
import io.sentry.TypeCheckHint.APOLLO_REQUEST
import io.sentry.TypeCheckHint.APOLLO_RESPONSE
import io.sentry.util.IntegrationUtils.addIntegrationToSdkVersion
import io.sentry.util.TracingUtils
import java.util.Locale
import java.util.concurrent.Executor

private const val TRACE_ORIGIN = "auto.graphql.apollo"

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)

    init {
        addIntegrationToSdkVersion(javaClass)
        SentryIntegrationPackageStorage.getInstance().addPackage("maven:io.sentry:sentry-apollo", BuildConfig.VERSION_NAME)
    }

    override fun interceptAsync(request: InterceptorRequest, chain: ApolloInterceptorChain, dispatcher: Executor, callBack: CallBack) {
        val activeSpan = if (io.sentry.util.Platform.isAndroid()) hub.transaction else hub.span
        if (activeSpan == null) {
            val headers = addTracingHeaders(request, null)
            val modifiedRequest = request.toBuilder().requestHeaders(headers).build()
            chain.proceedAsync(modifiedRequest, dispatcher, callBack)
        } else {
            val span = startChild(request, activeSpan)
            span.spanContext.origin = TRACE_ORIGIN

            val headers = addTracingHeaders(request, span)
            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
                        val statusCode: Int? = response.httpResponse.map { it.code() }.orNull()
                        if (statusCode != null) {
                            span.status = SpanStatus.fromHttpStatusCode(statusCode, SpanStatus.UNKNOWN)
                            span.setData(SpanDataConvention.HTTP_STATUS_CODE_KEY, statusCode)
                        } else {
                            span.status = SpanStatus.UNKNOWN
                        }
                        response.httpResponse.map { it.request().method() }.orNull()?.let {
                            span.setData(
                                SpanDataConvention.HTTP_METHOD_KEY,
                                it.toUpperCase(Locale.ROOT)
                            )
                        }

                        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) {
                                setData(SpanDataConvention.HTTP_STATUS_CODE_KEY, e.code())
                                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 addTracingHeaders(request: InterceptorRequest, span: ISpan?): RequestHeaders {
        val requestHeaderBuilder = request.requestHeaders.toBuilder()

        if (hub.options.isTraceSampling) {
            // we have no access to URI, no way to verify tracing origins
            TracingUtils.trace(
                hub,
                listOf(request.requestHeaders.headerValue(BaggageHeader.BAGGAGE_HEADER)),
                span
            )?.let { tracingHeaders ->
                requestHeaderBuilder.addHeader(
                    tracingHeaders.sentryTraceHeader.name,
                    tracingHeaders.sentryTraceHeader.value
                )
                tracingHeaders.baggageHeader?.let {
                    requestHeaderBuilder.addHeader(it.name, it.value)
                }
            }
        }

        return requestHeaderBuilder.build()
    }

    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 op = "http.graphql.$operationType"
        val description = "$operationType $operation"
        return activeSpan.startChild(op, 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)
            }
        }
        if (newSpan == null) {
            // span is dropped
            span.spanContext.sampled = false
        } else {
            span.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().apply {
                    set(APOLLO_REQUEST, httpRequest)
                    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?
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy