Skip to content

Commit

Permalink
Feat: Performance support for Android Apollo (#1705)
Browse files Browse the repository at this point in the history
  • Loading branch information
maciejwalkowiak authored Sep 15, 2021
1 parent 87e9f5f commit 1f575c6
Show file tree
Hide file tree
Showing 10 changed files with 984 additions and 0 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,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
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

0 comments on commit 1f575c6

Please sign in to comment.