Skip to content

Commit

Permalink
Feat/apollo v3 (#2109)
Browse files Browse the repository at this point in the history
Co-authored-by: Alexander Dinauer <adinauer@users.noreply.github.com>
  • Loading branch information
lbloder and adinauer committed Jul 19, 2022
1 parent 504fce8 commit 5740f43
Show file tree
Hide file tree
Showing 26 changed files with 1,259 additions and 2 deletions.
1 change: 1 addition & 0 deletions .craft.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,4 @@ targets:
maven:io.sentry:sentry-compose:
maven:io.sentry:sentry-compose-android:
maven:io.sentry:sentry-compose-desktop:
maven:io.sentry:sentry-apollo-3:
1 change: 1 addition & 0 deletions .github/ISSUE_TEMPLATE/bug_report_android.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ body:
- sentry-android-timber
- sentry-android-fragment
- sentry-apollo
- sentry-apollo-3
- other
validations:
required: true
Expand Down
1 change: 1 addition & 0 deletions .github/ISSUE_TEMPLATE/bug_report_java.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ body:
- sentry-jul
- sentry-jdbc
- sentry-apollo
- sentry-apollo-3
- sentry-kotlin-extensions
- sentry-servlet
- sentry-servlet-jakarta
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

### Features

- Add integration for Apollo-Kotlin 3 ([#2109](https://github.com/getsentry/sentry-java/pull/2109))
- New package `sentry-android-navigation` for AndroidX Navigation support ([#2136](https://github.com/getsentry/sentry-java/pull/2136))
- New package `sentry-compose` for Jetpack Compose support (Navigation) ([#2136](https://github.com/getsentry/sentry-java/pull/2136))
- Add sample rate to baggage as well as trace in envelope header and flatten user ([#2135](https://github.com/getsentry/sentry-java/pull/2135))
Expand Down
4 changes: 3 additions & 1 deletion buildSrc/src/main/java/Config.kt
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,8 @@ object Config {
val composeFoundation = "androidx.compose.foundation:foundation:$composeVersion"
val composeFoundationLayout = "androidx.compose.foundation:foundation-layout:$composeVersion"
val composeMaterial = "androidx.compose.material3:material3:1.0.0-alpha13"

val apolloKotlin = "com.apollographql.apollo3:apollo-runtime:3.3.0"
}

object AnnotationProcessors {
Expand Down Expand Up @@ -146,7 +148,7 @@ object Config {
val mockitoInline = "org.mockito:mockito-inline:4.3.1"
val awaitility = "org.awaitility:awaitility-kotlin:4.1.1"
val mockWebserver = "com.squareup.okhttp3:mockwebserver:${Libs.okHttpVersion}"
val mockWebserver3 = "com.squareup.okhttp3:mockwebserver:3.14.9"
val mockWebserver4 = "com.squareup.okhttp3:mockwebserver:4.9.3"
val jsonUnit = "net.javacrumbs.json-unit:json-unit:2.32.0"
val hsqldb = "org.hsqldb:hsqldb:2.6.1"
val javaFaker = "com.github.javafaker:javafaker:1.0.2"
Expand Down
34 changes: 34 additions & 0 deletions sentry-apollo-3/api/sentry-apollo-3.api
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
public final class io/sentry/apollo3/SentryApollo3HttpInterceptor : com/apollographql/apollo3/network/http/HttpInterceptor {
public static final field Companion Lio/sentry/apollo3/SentryApollo3HttpInterceptor$Companion;
public static final field SENTRY_APOLLO_3_OPERATION_NAME Ljava/lang/String;
public static final field SENTRY_APOLLO_3_OPERATION_TYPE Ljava/lang/String;
public static final field SENTRY_APOLLO_3_VARIABLES Ljava/lang/String;
public fun <init> ()V
public fun <init> (Lio/sentry/IHub;)V
public fun <init> (Lio/sentry/IHub;Lio/sentry/apollo3/SentryApollo3HttpInterceptor$BeforeSpanCallback;)V
public synthetic fun <init> (Lio/sentry/IHub;Lio/sentry/apollo3/SentryApollo3HttpInterceptor$BeforeSpanCallback;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun dispose ()V
public fun intercept (Lcom/apollographql/apollo3/api/http/HttpRequest;Lcom/apollographql/apollo3/network/http/HttpInterceptorChain;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
}

public abstract interface class io/sentry/apollo3/SentryApollo3HttpInterceptor$BeforeSpanCallback {
public abstract fun execute (Lio/sentry/ISpan;Lcom/apollographql/apollo3/api/http/HttpRequest;Lcom/apollographql/apollo3/api/http/HttpResponse;)Lio/sentry/ISpan;
}

public final class io/sentry/apollo3/SentryApollo3HttpInterceptor$Companion {
}

public final class io/sentry/apollo3/SentryApollo3Interceptor : com/apollographql/apollo3/interceptor/ApolloInterceptor {
public fun <init> ()V
public fun intercept (Lcom/apollographql/apollo3/api/ApolloRequest;Lcom/apollographql/apollo3/interceptor/ApolloInterceptorChain;)Lkotlinx/coroutines/flow/Flow;
}

public final class io/sentry/apollo3/SentryApolloBuilderExtensionsKt {
public static final fun sentryTracing (Lcom/apollographql/apollo3/ApolloClient$Builder;)Lcom/apollographql/apollo3/ApolloClient$Builder;
public static final fun sentryTracing (Lcom/apollographql/apollo3/ApolloClient$Builder;Lio/sentry/IHub;)Lcom/apollographql/apollo3/ApolloClient$Builder;
public static final fun sentryTracing (Lcom/apollographql/apollo3/ApolloClient$Builder;Lio/sentry/IHub;Lio/sentry/apollo3/SentryApollo3HttpInterceptor$BeforeSpanCallback;)Lcom/apollographql/apollo3/ApolloClient$Builder;
public static final fun sentryTracing (Lcom/apollographql/apollo3/ApolloClient$Builder;Lio/sentry/apollo3/SentryApollo3HttpInterceptor$BeforeSpanCallback;)Lcom/apollographql/apollo3/ApolloClient$Builder;
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;
public static synthetic fun sentryTracing$default (Lcom/apollographql/apollo3/ApolloClient$Builder;Lio/sentry/apollo3/SentryApollo3HttpInterceptor$BeforeSpanCallback;ILjava/lang/Object;)Lcom/apollographql/apollo3/ApolloClient$Builder;
}

79 changes: 79 additions & 0 deletions sentry-apollo-3/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
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<JavaPluginExtension> {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}

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

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

implementation(Config.Libs.apolloKotlin)

compileOnly(Config.CompileOnly.nopen)
errorprone(Config.CompileOnly.nopenChecker)
errorprone(Config.CompileOnly.errorprone)
errorprone(Config.CompileOnly.errorProneNullAway)
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.mockWebserver4)
}

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

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

tasks.jacocoTestReport {
reports {
xml.required.set(true)
html.required.set(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,181 @@
package io.sentry.apollo3

import com.apollographql.apollo3.api.http.HttpHeader
import com.apollographql.apollo3.api.http.HttpRequest
import com.apollographql.apollo3.api.http.HttpResponse
import com.apollographql.apollo3.exception.ApolloHttpException
import com.apollographql.apollo3.exception.ApolloNetworkException
import com.apollographql.apollo3.network.http.HttpInterceptor
import com.apollographql.apollo3.network.http.HttpInterceptorChain
import io.sentry.Breadcrumb
import io.sentry.Hint
import io.sentry.HubAdapter
import io.sentry.IHub
import io.sentry.ISpan
import io.sentry.SentryLevel
import io.sentry.SpanStatus
import io.sentry.TracingOrigins
import io.sentry.TypeCheckHint

class SentryApollo3HttpInterceptor @JvmOverloads constructor(private val hub: IHub = HubAdapter.getInstance(), private val beforeSpan: BeforeSpanCallback? = null) :
HttpInterceptor {

override suspend fun intercept(
request: HttpRequest,
chain: HttpInterceptorChain
): HttpResponse {
val activeSpan = hub.span
return if (activeSpan == null) {
chain.proceed(request)
} else {
val span = startChild(request, activeSpan)

val cleanedHeaders = removeSentryInternalHeaders(request.headers)

val requestBuilder = request.newBuilder().apply {
headers(cleanedHeaders)
}

if (TracingOrigins.contain(hub.options.tracingOrigins, request.url)) {
val sentryTraceHeader = span.toSentryTrace()
val baggageHeader = span.toBaggageHeader()
requestBuilder.addHeader(sentryTraceHeader.name, sentryTraceHeader.value)

baggageHeader?.let {
requestBuilder.addHeader(it.name, it.value)
}
}

val modifiedRequest = requestBuilder.build()
var httpResponse: HttpResponse? = null
var statusCode: Int? = null

try {
httpResponse = chain.proceed(modifiedRequest)
statusCode = httpResponse.statusCode
span.status = SpanStatus.fromHttpStatusCode(statusCode, SpanStatus.UNKNOWN)
return httpResponse
} catch (e: Throwable) {
when (e) {
is ApolloHttpException -> {
statusCode = e.statusCode
span.status = SpanStatus.fromHttpStatusCode(statusCode, SpanStatus.INTERNAL_ERROR)
}
is ApolloNetworkException -> span.status = SpanStatus.INTERNAL_ERROR
else -> SpanStatus.INTERNAL_ERROR
}
span.throwable = e
throw e
} finally {
finish(span, modifiedRequest, httpResponse, statusCode)
}
}
}

private fun removeSentryInternalHeaders(headers: List<HttpHeader>): List<HttpHeader> {
return headers.filterNot { it.name == SENTRY_APOLLO_3_VARIABLES || it.name == SENTRY_APOLLO_3_OPERATION_NAME || it.name == SENTRY_APOLLO_3_OPERATION_TYPE }
}

private fun startChild(request: HttpRequest, activeSpan: ISpan): ISpan {
val url = request.url
val method = request.method

val operationName = operationNameFromHeaders(request)
val operation = operationName ?: "apollo.client"
val operationType = request.valueForHeader(SENTRY_APOLLO_3_OPERATION_TYPE) ?: method
val operationId = request.valueForHeader("X-APOLLO-OPERATION-ID")
val variables = request.valueForHeader(SENTRY_APOLLO_3_VARIABLES)
val description = "$operationType ${operationName ?: url}"

return activeSpan.startChild(operation, description).apply {
operationId?.let {
setData("operationId", it)
}

variables?.let {
setData("variables", it)
}
}
}

private fun operationNameFromHeaders(request: HttpRequest): String? {
return request.valueForHeader(SENTRY_APOLLO_3_OPERATION_NAME) ?: request.valueForHeader("X-APOLLO-OPERATION-NAME")
}

private fun HttpRequest.valueForHeader(key: String) = headers.firstOrNull { it.name == key }?.value

private fun finish(span: ISpan, request: HttpRequest, response: HttpResponse? = null, statusCode: Int?) {
if (beforeSpan != null) {
try {
val result = beforeSpan.execute(span, request, response)
if (result == null) {
// Span is dropped
span.spanContext.sampled = false
}
} catch (e: Throwable) {
hub.options.logger.log(SentryLevel.ERROR, "An error occurred while executing beforeSpan on ApolloInterceptor", e)
}
}
span.finish()

val breadcrumb =
Breadcrumb.http(request.url, request.method.name, statusCode)

request.body?.contentLength.ifHasValidLength { contentLength ->
breadcrumb.setData("request_body_size", contentLength)
}

val hint = Hint().also {
it.set(TypeCheckHint.APOLLO_REQUEST, request)
}

response?.let { httpResponse ->
// Content-Length header is not present on batched operations
httpResponse.headersContentLength().ifHasValidLength { contentLength ->
breadcrumb.setData("response_body_size", contentLength)
}

if (!breadcrumb.data.containsKey("response_body_size")) {
httpResponse.body?.buffer?.size?.ifHasValidLength { contentLength ->
breadcrumb.setData("response_body_size", contentLength)
}
}

hint.set(TypeCheckHint.APOLLO_RESPONSE, httpResponse)
}

hub.addBreadcrumb(breadcrumb, hint)
}

// Extensions

private fun HttpResponse.headersContentLength(): Long {
return headers.firstOrNull { it.name == "Content-Length" }?.value?.toLongOrNull() ?: -1L
}

private fun Long?.ifHasValidLength(fn: (Long) -> Unit) {
if (this != null && this != -1L) {
fn.invoke(this)
}
}

/**
* The BeforeSpan callback
*/
fun interface BeforeSpanCallback {
/**
* Mutates span before being added.
*
* @param span the span to mutate or drop
* @param request the Apollo request object
* @param response the Apollo response object
*/
fun execute(span: ISpan, request: HttpRequest, response: HttpResponse?): ISpan?
}

companion object {
const val SENTRY_APOLLO_3_VARIABLES = "SENTRY-APOLLO-3-VARIABLES"
const val SENTRY_APOLLO_3_OPERATION_NAME = "SENTRY-APOLLO-3-OPERATION-NAME"
const val SENTRY_APOLLO_3_OPERATION_TYPE = "SENTRY-APOLLO-3-OPERATION-TYPE"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package io.sentry.apollo3

import com.apollographql.apollo3.api.ApolloRequest
import com.apollographql.apollo3.api.ApolloResponse
import com.apollographql.apollo3.api.CustomScalarAdapters
import com.apollographql.apollo3.api.Mutation
import com.apollographql.apollo3.api.Operation
import com.apollographql.apollo3.api.Query
import com.apollographql.apollo3.api.Subscription
import com.apollographql.apollo3.api.variables
import com.apollographql.apollo3.interceptor.ApolloInterceptor
import com.apollographql.apollo3.interceptor.ApolloInterceptorChain
import kotlinx.coroutines.flow.Flow

class SentryApollo3Interceptor : ApolloInterceptor {

override fun <D : Operation.Data> intercept(
request: ApolloRequest<D>,
chain: ApolloInterceptorChain
): Flow<ApolloResponse<D>> {
val builder = request.newBuilder()
.addHttpHeader(SentryApollo3HttpInterceptor.SENTRY_APOLLO_3_OPERATION_TYPE, operationType(request))
.addHttpHeader(SentryApollo3HttpInterceptor.SENTRY_APOLLO_3_OPERATION_NAME, request.operation.name())

request.scalarAdapters?.let {
builder.addHttpHeader(SentryApollo3HttpInterceptor.SENTRY_APOLLO_3_VARIABLES, request.operation.variables(it).valueMap.toString())
}
return chain.proceed(builder.build())
}
}

private fun <D : Operation.Data> operationType(apolloRequest: ApolloRequest<D>) = when (apolloRequest.operation) {
is Query -> "query"
is Mutation -> "mutation"
is Subscription -> "subscription"
else -> apolloRequest.operation.javaClass.simpleName
}

private val <D : Operation.Data> ApolloRequest<D>.scalarAdapters
get() = executionContext[CustomScalarAdapters]

0 comments on commit 5740f43

Please sign in to comment.