Skip to content
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 @@ -17,6 +17,7 @@
* Feat: Include unfinished spans in transaction (#1699)
* Fix: Move tags from transaction.contexts.trace.tags to transaction.tags (#1700)
* Feat: Add static helpers for creating breadcrumbs (#1702)
* Feat: Performance support for Android Apollo (#1705)

Breaking changes:

Expand Down
5 changes: 5 additions & 0 deletions buildSrc/src/main/java/Config.kt
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,10 @@ object Config {
private val feignVersion = "11.6"
val feignCore = "io.github.openfeign:feign-core:$feignVersion"
val feignGson = "io.github.openfeign:feign-gson:$feignVersion"

private val apolloVersion = "2.5.9"
val apolloAndroid = "com.apollographql.apollo:apollo-runtime:$apolloVersion"
val apolloCoroutines = "com.apollographql.apollo:apollo-coroutines-support:$apolloVersion"
}

object AnnotationProcessors {
Expand All @@ -111,6 +115,7 @@ object Config {
val mockitoInline = "org.mockito:mockito-inline:3.10.0"
val awaitility = "org.awaitility:awaitility-kotlin:4.1.0"
val mockWebserver = "com.squareup.okhttp3:mockwebserver:4.9.0"
val mockWebserver3 = "com.squareup.okhttp3:mockwebserver:3.14.9"
// bumping to 2.26.0 breaks tests
val jsonUnit = "net.javacrumbs.json-unit:json-unit:2.11.1"
}
Expand Down
14 changes: 14 additions & 0 deletions sentry-apollo/api/sentry-apollo.api
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
public final class io/sentry/apollo/SentryApolloInterceptor : com/apollographql/apollo/interceptor/ApolloInterceptor {
public fun <init> ()V
public fun <init> (Lio/sentry/IHub;)V
public fun <init> (Lio/sentry/IHub;Lio/sentry/apollo/SentryApolloInterceptor$BeforeSpanCallback;)V
public synthetic fun <init> (Lio/sentry/IHub;Lio/sentry/apollo/SentryApolloInterceptor$BeforeSpanCallback;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun <init> (Lio/sentry/apollo/SentryApolloInterceptor$BeforeSpanCallback;)V
public fun dispose ()V
public fun interceptAsync (Lcom/apollographql/apollo/interceptor/ApolloInterceptor$InterceptorRequest;Lcom/apollographql/apollo/interceptor/ApolloInterceptorChain;Ljava/util/concurrent/Executor;Lcom/apollographql/apollo/interceptor/ApolloInterceptor$CallBack;)V
}

public abstract interface class io/sentry/apollo/SentryApolloInterceptor$BeforeSpanCallback {
public abstract fun execute (Lio/sentry/ISpan;Lcom/apollographql/apollo/interceptor/ApolloInterceptor$InterceptorRequest;Lcom/apollographql/apollo/interceptor/ApolloInterceptor$InterceptorResponse;)Lio/sentry/ISpan;
}

80 changes: 80 additions & 0 deletions sentry-apollo/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import net.ltgt.gradle.errorprone.errorprone
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
`java-library`
kotlin("jvm")
jacoco
id(Config.QualityPlugins.errorProne)
id(Config.QualityPlugins.gradleVersions)
id(Config.BuildPlugins.buildConfig) version Config.BuildPlugins.buildConfigVersion
}

configure<JavaPluginConvention> {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}

tasks.withType<KotlinCompile>().configureEach {
kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8.toString()
}

dependencies {
api(projects.sentry)
api(projects.sentryKotlinExtensions)

implementation(Config.Libs.apolloAndroid)

compileOnly(Config.CompileOnly.nopen)
errorprone(Config.CompileOnly.nopenChecker)
errorprone(Config.CompileOnly.errorprone)
errorprone(Config.CompileOnly.errorProneNullAway)
errorproneJavac(Config.CompileOnly.errorProneJavac8)
compileOnly(Config.CompileOnly.jetbrainsAnnotations)

// tests
testImplementation(projects.sentryTestSupport)
testImplementation(Config.Libs.coroutinesCore)
testImplementation(kotlin(Config.kotlinStdLib))
testImplementation(Config.TestLibs.kotlinTestJunit)
testImplementation(Config.TestLibs.mockitoKotlin)
testImplementation(Config.TestLibs.mockitoInline)
testImplementation(Config.TestLibs.mockWebserver3)
testImplementation(Config.Libs.apolloCoroutines)
}

configure<SourceSetContainer> {
test {
java.srcDir("src/test/java")
}
}

jacoco {
toolVersion = Config.QualityPlugins.Jacoco.version
}

tasks.jacocoTestReport {
reports {
xml.isEnabled = true
html.isEnabled = false
}
}

tasks {
jacocoTestCoverageVerification {
violationRules {
rule { limit { minimum = Config.QualityPlugins.Jacoco.minimumCoverage } }
}
}
check {
dependsOn(jacocoTestCoverageVerification)
dependsOn(jacocoTestReport)
}
}

tasks.withType<JavaCompile>().configureEach {
options.errorprone {
check("NullAway", net.ltgt.gradle.errorprone.CheckSeverity.ERROR)
option("NullAway:AnnotatedPackages", "io.sentry")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
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 io.sentry.HubAdapter
import io.sentry.IHub
import io.sentry.ISpan
import io.sentry.SentryLevel
import io.sentry.SpanStatus
import java.util.concurrent.Executor

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)

override fun interceptAsync(request: InterceptorRequest, chain: ApolloInterceptorChain, dispatcher: Executor, callBack: CallBack) {
val activeSpan = hub.span
if (activeSpan == null) {
chain.proceedAsync(request, dispatcher, callBack)
} else {
val span = startChild(request, activeSpan)
val sentryTraceHeader = span.toSentryTrace()

// we have no access to URI, no way to verify tracing origins
Copy link
Member

Choose a reason for hiding this comment

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

Should we address this before we ship it?
Sounds like we should bring tracing options (allowlist of outgoing URLs to add the header info) going forward

Copy link
Contributor

Choose a reason for hiding this comment

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

looks like a limitation of apollo

Copy link
Contributor

Choose a reason for hiding this comment

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

you can do with the response object, response.httpResponse.get().request().url() but its too late

val headers = request.requestHeaders.toBuilder().addHeader(sentryTraceHeader.name, sentryTraceHeader.value).build()
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
span.status = response.httpResponse.map { SpanStatus.fromHttpStatusCode(it.code(), SpanStatus.UNKNOWN) }
.or(SpanStatus.UNKNOWN)

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) 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 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 description = "$operationType $operation"
return activeSpan.startChild(operation, 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)
}
}
newSpan.finish()
}

/**
* The BeforeSpan callback
*/
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
}
}
Loading