Skip to content

Commit 5740f43

Browse files
lbloderadinauer
andauthored
Feat/apollo v3 (#2109)
Co-authored-by: Alexander Dinauer <[email protected]>
1 parent 504fce8 commit 5740f43

26 files changed

+1259
-2
lines changed

.craft.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,3 +45,4 @@ targets:
4545
maven:io.sentry:sentry-compose:
4646
maven:io.sentry:sentry-compose-android:
4747
maven:io.sentry:sentry-compose-desktop:
48+
maven:io.sentry:sentry-apollo-3:

.github/ISSUE_TEMPLATE/bug_report_android.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ body:
1414
- sentry-android-timber
1515
- sentry-android-fragment
1616
- sentry-apollo
17+
- sentry-apollo-3
1718
- other
1819
validations:
1920
required: true

.github/ISSUE_TEMPLATE/bug_report_java.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ body:
1212
- sentry-jul
1313
- sentry-jdbc
1414
- sentry-apollo
15+
- sentry-apollo-3
1516
- sentry-kotlin-extensions
1617
- sentry-servlet
1718
- sentry-servlet-jakarta

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
### Features
2121

22+
- Add integration for Apollo-Kotlin 3 ([#2109](https://github.com/getsentry/sentry-java/pull/2109))
2223
- New package `sentry-android-navigation` for AndroidX Navigation support ([#2136](https://github.com/getsentry/sentry-java/pull/2136))
2324
- New package `sentry-compose` for Jetpack Compose support (Navigation) ([#2136](https://github.com/getsentry/sentry-java/pull/2136))
2425
- Add sample rate to baggage as well as trace in envelope header and flatten user ([#2135](https://github.com/getsentry/sentry-java/pull/2135))

buildSrc/src/main/java/Config.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,8 @@ object Config {
119119
val composeFoundation = "androidx.compose.foundation:foundation:$composeVersion"
120120
val composeFoundationLayout = "androidx.compose.foundation:foundation-layout:$composeVersion"
121121
val composeMaterial = "androidx.compose.material3:material3:1.0.0-alpha13"
122+
123+
val apolloKotlin = "com.apollographql.apollo3:apollo-runtime:3.3.0"
122124
}
123125

124126
object AnnotationProcessors {
@@ -146,7 +148,7 @@ object Config {
146148
val mockitoInline = "org.mockito:mockito-inline:4.3.1"
147149
val awaitility = "org.awaitility:awaitility-kotlin:4.1.1"
148150
val mockWebserver = "com.squareup.okhttp3:mockwebserver:${Libs.okHttpVersion}"
149-
val mockWebserver3 = "com.squareup.okhttp3:mockwebserver:3.14.9"
151+
val mockWebserver4 = "com.squareup.okhttp3:mockwebserver:4.9.3"
150152
val jsonUnit = "net.javacrumbs.json-unit:json-unit:2.32.0"
151153
val hsqldb = "org.hsqldb:hsqldb:2.6.1"
152154
val javaFaker = "com.github.javafaker:javafaker:1.0.2"
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
public final class io/sentry/apollo3/SentryApollo3HttpInterceptor : com/apollographql/apollo3/network/http/HttpInterceptor {
2+
public static final field Companion Lio/sentry/apollo3/SentryApollo3HttpInterceptor$Companion;
3+
public static final field SENTRY_APOLLO_3_OPERATION_NAME Ljava/lang/String;
4+
public static final field SENTRY_APOLLO_3_OPERATION_TYPE Ljava/lang/String;
5+
public static final field SENTRY_APOLLO_3_VARIABLES Ljava/lang/String;
6+
public fun <init> ()V
7+
public fun <init> (Lio/sentry/IHub;)V
8+
public fun <init> (Lio/sentry/IHub;Lio/sentry/apollo3/SentryApollo3HttpInterceptor$BeforeSpanCallback;)V
9+
public synthetic fun <init> (Lio/sentry/IHub;Lio/sentry/apollo3/SentryApollo3HttpInterceptor$BeforeSpanCallback;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
10+
public fun dispose ()V
11+
public fun intercept (Lcom/apollographql/apollo3/api/http/HttpRequest;Lcom/apollographql/apollo3/network/http/HttpInterceptorChain;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
12+
}
13+
14+
public abstract interface class io/sentry/apollo3/SentryApollo3HttpInterceptor$BeforeSpanCallback {
15+
public abstract fun execute (Lio/sentry/ISpan;Lcom/apollographql/apollo3/api/http/HttpRequest;Lcom/apollographql/apollo3/api/http/HttpResponse;)Lio/sentry/ISpan;
16+
}
17+
18+
public final class io/sentry/apollo3/SentryApollo3HttpInterceptor$Companion {
19+
}
20+
21+
public final class io/sentry/apollo3/SentryApollo3Interceptor : com/apollographql/apollo3/interceptor/ApolloInterceptor {
22+
public fun <init> ()V
23+
public fun intercept (Lcom/apollographql/apollo3/api/ApolloRequest;Lcom/apollographql/apollo3/interceptor/ApolloInterceptorChain;)Lkotlinx/coroutines/flow/Flow;
24+
}
25+
26+
public final class io/sentry/apollo3/SentryApolloBuilderExtensionsKt {
27+
public static final fun sentryTracing (Lcom/apollographql/apollo3/ApolloClient$Builder;)Lcom/apollographql/apollo3/ApolloClient$Builder;
28+
public static final fun sentryTracing (Lcom/apollographql/apollo3/ApolloClient$Builder;Lio/sentry/IHub;)Lcom/apollographql/apollo3/ApolloClient$Builder;
29+
public static final fun sentryTracing (Lcom/apollographql/apollo3/ApolloClient$Builder;Lio/sentry/IHub;Lio/sentry/apollo3/SentryApollo3HttpInterceptor$BeforeSpanCallback;)Lcom/apollographql/apollo3/ApolloClient$Builder;
30+
public static final fun sentryTracing (Lcom/apollographql/apollo3/ApolloClient$Builder;Lio/sentry/apollo3/SentryApollo3HttpInterceptor$BeforeSpanCallback;)Lcom/apollographql/apollo3/ApolloClient$Builder;
31+
public static synthetic fun sentryTracing$default (Lcom/apollographql/apollo3/ApolloClient$Builder;Lio/sentry/IHub;Lio/sentry/apollo3/SentryApollo3HttpInterceptor$BeforeSpanCallback;ILjava/lang/Object;)Lcom/apollographql/apollo3/ApolloClient$Builder;
32+
public static synthetic fun sentryTracing$default (Lcom/apollographql/apollo3/ApolloClient$Builder;Lio/sentry/apollo3/SentryApollo3HttpInterceptor$BeforeSpanCallback;ILjava/lang/Object;)Lcom/apollographql/apollo3/ApolloClient$Builder;
33+
}
34+

sentry-apollo-3/build.gradle.kts

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
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<JavaPluginExtension> {
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+
kotlinOptions.languageVersion = Config.kotlinCompatibleLanguageVersion
21+
}
22+
23+
dependencies {
24+
api(projects.sentry)
25+
api(projects.sentryKotlinExtensions)
26+
27+
implementation(Config.Libs.apolloKotlin)
28+
29+
compileOnly(Config.CompileOnly.nopen)
30+
errorprone(Config.CompileOnly.nopenChecker)
31+
errorprone(Config.CompileOnly.errorprone)
32+
errorprone(Config.CompileOnly.errorProneNullAway)
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.mockWebserver4)
43+
}
44+
45+
configure<SourceSetContainer> {
46+
test {
47+
java.srcDir("src/test/java")
48+
}
49+
}
50+
51+
jacoco {
52+
toolVersion = Config.QualityPlugins.Jacoco.version
53+
}
54+
55+
tasks.jacocoTestReport {
56+
reports {
57+
xml.required.set(true)
58+
html.required.set(false)
59+
}
60+
}
61+
62+
tasks {
63+
jacocoTestCoverageVerification {
64+
violationRules {
65+
rule { limit { minimum = Config.QualityPlugins.Jacoco.minimumCoverage } }
66+
}
67+
}
68+
check {
69+
dependsOn(jacocoTestCoverageVerification)
70+
dependsOn(jacocoTestReport)
71+
}
72+
}
73+
74+
tasks.withType<JavaCompile>().configureEach {
75+
options.errorprone {
76+
check("NullAway", net.ltgt.gradle.errorprone.CheckSeverity.ERROR)
77+
option("NullAway:AnnotatedPackages", "io.sentry")
78+
}
79+
}
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
package io.sentry.apollo3
2+
3+
import com.apollographql.apollo3.api.http.HttpHeader
4+
import com.apollographql.apollo3.api.http.HttpRequest
5+
import com.apollographql.apollo3.api.http.HttpResponse
6+
import com.apollographql.apollo3.exception.ApolloHttpException
7+
import com.apollographql.apollo3.exception.ApolloNetworkException
8+
import com.apollographql.apollo3.network.http.HttpInterceptor
9+
import com.apollographql.apollo3.network.http.HttpInterceptorChain
10+
import io.sentry.Breadcrumb
11+
import io.sentry.Hint
12+
import io.sentry.HubAdapter
13+
import io.sentry.IHub
14+
import io.sentry.ISpan
15+
import io.sentry.SentryLevel
16+
import io.sentry.SpanStatus
17+
import io.sentry.TracingOrigins
18+
import io.sentry.TypeCheckHint
19+
20+
class SentryApollo3HttpInterceptor @JvmOverloads constructor(private val hub: IHub = HubAdapter.getInstance(), private val beforeSpan: BeforeSpanCallback? = null) :
21+
HttpInterceptor {
22+
23+
override suspend fun intercept(
24+
request: HttpRequest,
25+
chain: HttpInterceptorChain
26+
): HttpResponse {
27+
val activeSpan = hub.span
28+
return if (activeSpan == null) {
29+
chain.proceed(request)
30+
} else {
31+
val span = startChild(request, activeSpan)
32+
33+
val cleanedHeaders = removeSentryInternalHeaders(request.headers)
34+
35+
val requestBuilder = request.newBuilder().apply {
36+
headers(cleanedHeaders)
37+
}
38+
39+
if (TracingOrigins.contain(hub.options.tracingOrigins, request.url)) {
40+
val sentryTraceHeader = span.toSentryTrace()
41+
val baggageHeader = span.toBaggageHeader()
42+
requestBuilder.addHeader(sentryTraceHeader.name, sentryTraceHeader.value)
43+
44+
baggageHeader?.let {
45+
requestBuilder.addHeader(it.name, it.value)
46+
}
47+
}
48+
49+
val modifiedRequest = requestBuilder.build()
50+
var httpResponse: HttpResponse? = null
51+
var statusCode: Int? = null
52+
53+
try {
54+
httpResponse = chain.proceed(modifiedRequest)
55+
statusCode = httpResponse.statusCode
56+
span.status = SpanStatus.fromHttpStatusCode(statusCode, SpanStatus.UNKNOWN)
57+
return httpResponse
58+
} catch (e: Throwable) {
59+
when (e) {
60+
is ApolloHttpException -> {
61+
statusCode = e.statusCode
62+
span.status = SpanStatus.fromHttpStatusCode(statusCode, SpanStatus.INTERNAL_ERROR)
63+
}
64+
is ApolloNetworkException -> span.status = SpanStatus.INTERNAL_ERROR
65+
else -> SpanStatus.INTERNAL_ERROR
66+
}
67+
span.throwable = e
68+
throw e
69+
} finally {
70+
finish(span, modifiedRequest, httpResponse, statusCode)
71+
}
72+
}
73+
}
74+
75+
private fun removeSentryInternalHeaders(headers: List<HttpHeader>): List<HttpHeader> {
76+
return headers.filterNot { it.name == SENTRY_APOLLO_3_VARIABLES || it.name == SENTRY_APOLLO_3_OPERATION_NAME || it.name == SENTRY_APOLLO_3_OPERATION_TYPE }
77+
}
78+
79+
private fun startChild(request: HttpRequest, activeSpan: ISpan): ISpan {
80+
val url = request.url
81+
val method = request.method
82+
83+
val operationName = operationNameFromHeaders(request)
84+
val operation = operationName ?: "apollo.client"
85+
val operationType = request.valueForHeader(SENTRY_APOLLO_3_OPERATION_TYPE) ?: method
86+
val operationId = request.valueForHeader("X-APOLLO-OPERATION-ID")
87+
val variables = request.valueForHeader(SENTRY_APOLLO_3_VARIABLES)
88+
val description = "$operationType ${operationName ?: url}"
89+
90+
return activeSpan.startChild(operation, description).apply {
91+
operationId?.let {
92+
setData("operationId", it)
93+
}
94+
95+
variables?.let {
96+
setData("variables", it)
97+
}
98+
}
99+
}
100+
101+
private fun operationNameFromHeaders(request: HttpRequest): String? {
102+
return request.valueForHeader(SENTRY_APOLLO_3_OPERATION_NAME) ?: request.valueForHeader("X-APOLLO-OPERATION-NAME")
103+
}
104+
105+
private fun HttpRequest.valueForHeader(key: String) = headers.firstOrNull { it.name == key }?.value
106+
107+
private fun finish(span: ISpan, request: HttpRequest, response: HttpResponse? = null, statusCode: Int?) {
108+
if (beforeSpan != null) {
109+
try {
110+
val result = beforeSpan.execute(span, request, response)
111+
if (result == null) {
112+
// Span is dropped
113+
span.spanContext.sampled = false
114+
}
115+
} catch (e: Throwable) {
116+
hub.options.logger.log(SentryLevel.ERROR, "An error occurred while executing beforeSpan on ApolloInterceptor", e)
117+
}
118+
}
119+
span.finish()
120+
121+
val breadcrumb =
122+
Breadcrumb.http(request.url, request.method.name, statusCode)
123+
124+
request.body?.contentLength.ifHasValidLength { contentLength ->
125+
breadcrumb.setData("request_body_size", contentLength)
126+
}
127+
128+
val hint = Hint().also {
129+
it.set(TypeCheckHint.APOLLO_REQUEST, request)
130+
}
131+
132+
response?.let { httpResponse ->
133+
// Content-Length header is not present on batched operations
134+
httpResponse.headersContentLength().ifHasValidLength { contentLength ->
135+
breadcrumb.setData("response_body_size", contentLength)
136+
}
137+
138+
if (!breadcrumb.data.containsKey("response_body_size")) {
139+
httpResponse.body?.buffer?.size?.ifHasValidLength { contentLength ->
140+
breadcrumb.setData("response_body_size", contentLength)
141+
}
142+
}
143+
144+
hint.set(TypeCheckHint.APOLLO_RESPONSE, httpResponse)
145+
}
146+
147+
hub.addBreadcrumb(breadcrumb, hint)
148+
}
149+
150+
// Extensions
151+
152+
private fun HttpResponse.headersContentLength(): Long {
153+
return headers.firstOrNull { it.name == "Content-Length" }?.value?.toLongOrNull() ?: -1L
154+
}
155+
156+
private fun Long?.ifHasValidLength(fn: (Long) -> Unit) {
157+
if (this != null && this != -1L) {
158+
fn.invoke(this)
159+
}
160+
}
161+
162+
/**
163+
* The BeforeSpan callback
164+
*/
165+
fun interface BeforeSpanCallback {
166+
/**
167+
* Mutates span before being added.
168+
*
169+
* @param span the span to mutate or drop
170+
* @param request the Apollo request object
171+
* @param response the Apollo response object
172+
*/
173+
fun execute(span: ISpan, request: HttpRequest, response: HttpResponse?): ISpan?
174+
}
175+
176+
companion object {
177+
const val SENTRY_APOLLO_3_VARIABLES = "SENTRY-APOLLO-3-VARIABLES"
178+
const val SENTRY_APOLLO_3_OPERATION_NAME = "SENTRY-APOLLO-3-OPERATION-NAME"
179+
const val SENTRY_APOLLO_3_OPERATION_TYPE = "SENTRY-APOLLO-3-OPERATION-TYPE"
180+
}
181+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package io.sentry.apollo3
2+
3+
import com.apollographql.apollo3.api.ApolloRequest
4+
import com.apollographql.apollo3.api.ApolloResponse
5+
import com.apollographql.apollo3.api.CustomScalarAdapters
6+
import com.apollographql.apollo3.api.Mutation
7+
import com.apollographql.apollo3.api.Operation
8+
import com.apollographql.apollo3.api.Query
9+
import com.apollographql.apollo3.api.Subscription
10+
import com.apollographql.apollo3.api.variables
11+
import com.apollographql.apollo3.interceptor.ApolloInterceptor
12+
import com.apollographql.apollo3.interceptor.ApolloInterceptorChain
13+
import kotlinx.coroutines.flow.Flow
14+
15+
class SentryApollo3Interceptor : ApolloInterceptor {
16+
17+
override fun <D : Operation.Data> intercept(
18+
request: ApolloRequest<D>,
19+
chain: ApolloInterceptorChain
20+
): Flow<ApolloResponse<D>> {
21+
val builder = request.newBuilder()
22+
.addHttpHeader(SentryApollo3HttpInterceptor.SENTRY_APOLLO_3_OPERATION_TYPE, operationType(request))
23+
.addHttpHeader(SentryApollo3HttpInterceptor.SENTRY_APOLLO_3_OPERATION_NAME, request.operation.name())
24+
25+
request.scalarAdapters?.let {
26+
builder.addHttpHeader(SentryApollo3HttpInterceptor.SENTRY_APOLLO_3_VARIABLES, request.operation.variables(it).valueMap.toString())
27+
}
28+
return chain.proceed(builder.build())
29+
}
30+
}
31+
32+
private fun <D : Operation.Data> operationType(apolloRequest: ApolloRequest<D>) = when (apolloRequest.operation) {
33+
is Query -> "query"
34+
is Mutation -> "mutation"
35+
is Subscription -> "subscription"
36+
else -> apolloRequest.operation.javaClass.simpleName
37+
}
38+
39+
private val <D : Operation.Data> ApolloRequest<D>.scalarAdapters
40+
get() = executionContext[CustomScalarAdapters]

0 commit comments

Comments
 (0)