Skip to content

Commit 1f575c6

Browse files
Feat: Performance support for Android Apollo (#1705)
1 parent 87e9f5f commit 1f575c6

File tree

10 files changed

+984
-0
lines changed

10 files changed

+984
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
* Feat: Include unfinished spans in transaction (#1699)
1919
* Fix: Move tags from transaction.contexts.trace.tags to transaction.tags (#1700)
2020
* Feat: Add static helpers for creating breadcrumbs (#1702)
21+
* Feat: Performance support for Android Apollo (#1705)
2122

2223
Breaking changes:
2324

buildSrc/src/main/java/Config.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,10 @@ object Config {
9191
private val feignVersion = "11.6"
9292
val feignCore = "io.github.openfeign:feign-core:$feignVersion"
9393
val feignGson = "io.github.openfeign:feign-gson:$feignVersion"
94+
95+
private val apolloVersion = "2.5.9"
96+
val apolloAndroid = "com.apollographql.apollo:apollo-runtime:$apolloVersion"
97+
val apolloCoroutines = "com.apollographql.apollo:apollo-coroutines-support:$apolloVersion"
9498
}
9599

96100
object AnnotationProcessors {
@@ -111,6 +115,7 @@ object Config {
111115
val mockitoInline = "org.mockito:mockito-inline:3.10.0"
112116
val awaitility = "org.awaitility:awaitility-kotlin:4.1.0"
113117
val mockWebserver = "com.squareup.okhttp3:mockwebserver:4.9.0"
118+
val mockWebserver3 = "com.squareup.okhttp3:mockwebserver:3.14.9"
114119
// bumping to 2.26.0 breaks tests
115120
val jsonUnit = "net.javacrumbs.json-unit:json-unit:2.11.1"
116121
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
public final class io/sentry/apollo/SentryApolloInterceptor : com/apollographql/apollo/interceptor/ApolloInterceptor {
2+
public fun <init> ()V
3+
public fun <init> (Lio/sentry/IHub;)V
4+
public fun <init> (Lio/sentry/IHub;Lio/sentry/apollo/SentryApolloInterceptor$BeforeSpanCallback;)V
5+
public synthetic fun <init> (Lio/sentry/IHub;Lio/sentry/apollo/SentryApolloInterceptor$BeforeSpanCallback;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
6+
public fun <init> (Lio/sentry/apollo/SentryApolloInterceptor$BeforeSpanCallback;)V
7+
public fun dispose ()V
8+
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
9+
}
10+
11+
public abstract interface class io/sentry/apollo/SentryApolloInterceptor$BeforeSpanCallback {
12+
public abstract fun execute (Lio/sentry/ISpan;Lcom/apollographql/apollo/interceptor/ApolloInterceptor$InterceptorRequest;Lcom/apollographql/apollo/interceptor/ApolloInterceptor$InterceptorResponse;)Lio/sentry/ISpan;
13+
}
14+

sentry-apollo/build.gradle.kts

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import net.ltgt.gradle.errorprone.errorprone
2+
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
3+
4+
plugins {
5+
`java-library`
6+
kotlin("jvm")
7+
jacoco
8+
id(Config.QualityPlugins.errorProne)
9+
id(Config.QualityPlugins.gradleVersions)
10+
id(Config.BuildPlugins.buildConfig) version Config.BuildPlugins.buildConfigVersion
11+
}
12+
13+
configure<JavaPluginConvention> {
14+
sourceCompatibility = JavaVersion.VERSION_1_8
15+
targetCompatibility = JavaVersion.VERSION_1_8
16+
}
17+
18+
tasks.withType<KotlinCompile>().configureEach {
19+
kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8.toString()
20+
}
21+
22+
dependencies {
23+
api(projects.sentry)
24+
api(projects.sentryKotlinExtensions)
25+
26+
implementation(Config.Libs.apolloAndroid)
27+
28+
compileOnly(Config.CompileOnly.nopen)
29+
errorprone(Config.CompileOnly.nopenChecker)
30+
errorprone(Config.CompileOnly.errorprone)
31+
errorprone(Config.CompileOnly.errorProneNullAway)
32+
errorproneJavac(Config.CompileOnly.errorProneJavac8)
33+
compileOnly(Config.CompileOnly.jetbrainsAnnotations)
34+
35+
// tests
36+
testImplementation(projects.sentryTestSupport)
37+
testImplementation(Config.Libs.coroutinesCore)
38+
testImplementation(kotlin(Config.kotlinStdLib))
39+
testImplementation(Config.TestLibs.kotlinTestJunit)
40+
testImplementation(Config.TestLibs.mockitoKotlin)
41+
testImplementation(Config.TestLibs.mockitoInline)
42+
testImplementation(Config.TestLibs.mockWebserver3)
43+
testImplementation(Config.Libs.apolloCoroutines)
44+
}
45+
46+
configure<SourceSetContainer> {
47+
test {
48+
java.srcDir("src/test/java")
49+
}
50+
}
51+
52+
jacoco {
53+
toolVersion = Config.QualityPlugins.Jacoco.version
54+
}
55+
56+
tasks.jacocoTestReport {
57+
reports {
58+
xml.isEnabled = true
59+
html.isEnabled = false
60+
}
61+
}
62+
63+
tasks {
64+
jacocoTestCoverageVerification {
65+
violationRules {
66+
rule { limit { minimum = Config.QualityPlugins.Jacoco.minimumCoverage } }
67+
}
68+
}
69+
check {
70+
dependsOn(jacocoTestCoverageVerification)
71+
dependsOn(jacocoTestReport)
72+
}
73+
}
74+
75+
tasks.withType<JavaCompile>().configureEach {
76+
options.errorprone {
77+
check("NullAway", net.ltgt.gradle.errorprone.CheckSeverity.ERROR)
78+
option("NullAway:AnnotatedPackages", "io.sentry")
79+
}
80+
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
package io.sentry.apollo
2+
3+
import com.apollographql.apollo.api.Mutation
4+
import com.apollographql.apollo.api.Query
5+
import com.apollographql.apollo.api.Subscription
6+
import com.apollographql.apollo.exception.ApolloException
7+
import com.apollographql.apollo.exception.ApolloHttpException
8+
import com.apollographql.apollo.interceptor.ApolloInterceptor
9+
import com.apollographql.apollo.interceptor.ApolloInterceptor.CallBack
10+
import com.apollographql.apollo.interceptor.ApolloInterceptor.FetchSourceType
11+
import com.apollographql.apollo.interceptor.ApolloInterceptor.InterceptorRequest
12+
import com.apollographql.apollo.interceptor.ApolloInterceptor.InterceptorResponse
13+
import com.apollographql.apollo.interceptor.ApolloInterceptorChain
14+
import io.sentry.HubAdapter
15+
import io.sentry.IHub
16+
import io.sentry.ISpan
17+
import io.sentry.SentryLevel
18+
import io.sentry.SpanStatus
19+
import java.util.concurrent.Executor
20+
21+
class SentryApolloInterceptor(
22+
private val hub: IHub = HubAdapter.getInstance(),
23+
private val beforeSpan: BeforeSpanCallback? = null
24+
) : ApolloInterceptor {
25+
26+
constructor(hub: IHub) : this(hub, null)
27+
constructor(beforeSpan: BeforeSpanCallback) : this(HubAdapter.getInstance(), beforeSpan)
28+
29+
override fun interceptAsync(request: InterceptorRequest, chain: ApolloInterceptorChain, dispatcher: Executor, callBack: CallBack) {
30+
val activeSpan = hub.span
31+
if (activeSpan == null) {
32+
chain.proceedAsync(request, dispatcher, callBack)
33+
} else {
34+
val span = startChild(request, activeSpan)
35+
val sentryTraceHeader = span.toSentryTrace()
36+
37+
// we have no access to URI, no way to verify tracing origins
38+
val headers = request.requestHeaders.toBuilder().addHeader(sentryTraceHeader.name, sentryTraceHeader.value).build()
39+
val requestWithHeader = request.toBuilder().requestHeaders(headers).build()
40+
span.setData("operationId", requestWithHeader.operation.operationId())
41+
span.setData("variables", requestWithHeader.operation.variables().valueMap().toString())
42+
43+
chain.proceedAsync(requestWithHeader, dispatcher, object : CallBack {
44+
override fun onResponse(response: InterceptorResponse) {
45+
// onResponse is called only for statuses 2xx
46+
span.status = response.httpResponse.map { SpanStatus.fromHttpStatusCode(it.code(), SpanStatus.UNKNOWN) }
47+
.or(SpanStatus.UNKNOWN)
48+
49+
finish(span, requestWithHeader, response)
50+
callBack.onResponse(response)
51+
}
52+
53+
override fun onFetch(sourceType: FetchSourceType) {
54+
callBack.onFetch(sourceType)
55+
}
56+
57+
override fun onFailure(e: ApolloException) {
58+
span.apply {
59+
status = if (e is ApolloHttpException) SpanStatus.fromHttpStatusCode(e.code(), SpanStatus.INTERNAL_ERROR) else SpanStatus.INTERNAL_ERROR
60+
throwable = e
61+
}
62+
finish(span, requestWithHeader)
63+
callBack.onFailure(e)
64+
}
65+
66+
override fun onCompleted() {
67+
callBack.onCompleted()
68+
}
69+
})
70+
}
71+
}
72+
73+
override fun dispose() {}
74+
75+
private fun startChild(request: InterceptorRequest, activeSpan: ISpan): ISpan {
76+
val operation = request.operation.name().name()
77+
val operationType = when (request.operation) {
78+
is Query -> "query"
79+
is Mutation -> "mutation"
80+
is Subscription -> "subscription"
81+
else -> request.operation.javaClass.simpleName
82+
}
83+
val description = "$operationType $operation"
84+
return activeSpan.startChild(operation, description)
85+
}
86+
87+
private fun finish(span: ISpan, request: InterceptorRequest, response: InterceptorResponse? = null) {
88+
var newSpan: ISpan = span
89+
if (beforeSpan != null) {
90+
try {
91+
newSpan = beforeSpan.execute(span, request, response)
92+
} catch (e: Exception) {
93+
hub.options.logger.log(SentryLevel.ERROR, "An error occurred while executing beforeSpan on ApolloInterceptor", e)
94+
}
95+
}
96+
newSpan.finish()
97+
}
98+
99+
/**
100+
* The BeforeSpan callback
101+
*/
102+
interface BeforeSpanCallback {
103+
/**
104+
* Mutates span before being added.
105+
*
106+
* @param span the span to mutate or drop
107+
* @param request the HTTP request executed by okHttp
108+
* @param response the HTTP response received by okHttp
109+
*/
110+
fun execute(span: ISpan, request: InterceptorRequest, response: InterceptorResponse?): ISpan
111+
}
112+
}

0 commit comments

Comments
 (0)