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

Feat: Performance support for Android Apollo #1705

Merged
merged 8 commits into from
Sep 15, 2021
Merged
Show file tree
Hide file tree
Changes from 3 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 @@ -7,6 +7,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"
bruno-garcia marked this conversation as resolved.
Show resolved Hide resolved
// 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,105 @@
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.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.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.setTag("operationId", requestWithHeader.operation.operationId())
span.setTag("variables", requestWithHeader.operation.variables().valueMap().toString())
marandaneto marked this conversation as resolved.
Show resolved Hide resolved

chain.proceedAsync(requestWithHeader, dispatcher, object : CallBack {
override fun onResponse(response: InterceptorResponse) {
span.status = response.httpResponse.map { SpanStatus.fromHttpStatusCode(it.code(), SpanStatus.INTERNAL_ERROR) }
.or(SpanStatus.UNKNOWN)
maciejwalkowiak marked this conversation as resolved.
Show resolved Hide resolved

finish(span, requestWithHeader, response)
callBack.onResponse(response)
}

override fun onFetch(sourceType: FetchSourceType) {
callBack.onFetch(sourceType)
}

override fun onFailure(e: ApolloException) {
span.apply {
status = 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) {
newSpan = beforeSpan.execute(span, request, response)
maciejwalkowiak marked this conversation as resolved.
Show resolved Hide resolved
}
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