diff --git a/CHANGELOG.md b/CHANGELOG.md index c70df9cf08c..81644432d4a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,19 @@ ## Unreleased +### Features + +- Support for automatically capturing Failed GraphQL (Apollo 3) Client errors ([#2781](https://github.com/getsentry/sentry-java/pull/2781)) + +```kotlin +import com.apollographql.apollo3.ApolloClient + +val apolloClient = ApolloClient.Builder() + .serverUrl("https://example.com/graphql") + .sentryTracing(captureFailedRequests = true) + .build() +``` + ### Dependencies - Bump Native SDK from v0.6.2 to v0.6.3 ([#2746](https://github.com/getsentry/sentry-java/pull/2746)) diff --git a/sentry-android-core/proguard-rules.pro b/sentry-android-core/proguard-rules.pro index a2b2d0d4b69..6e3c84c3bba 100644 --- a/sentry-android-core/proguard-rules.pro +++ b/sentry-android-core/proguard-rules.pro @@ -38,3 +38,13 @@ -keep class * extends io.sentry.SentryOptions { *; } ##---------------End: proguard configuration for android-core ---------- + +##---------------Begin: proguard configuration for sentry-apollo-3 ---------- + +# don't warn about missing classes, as it depends on the sentry-apollo-3 jar dependency. +-dontwarn io.sentry.apollo3.SentryApollo3ClientException + +# we don't want this class to be obfuscated, otherwise issue's titles are obfuscated as well. +-keep class io.sentry.apollo3.SentryApollo3ClientException { (...); } + +##---------------End: proguard configuration for sentry-apollo-3 ---------- diff --git a/sentry-android-okhttp/src/main/java/io/sentry/android/okhttp/SentryOkHttpInterceptor.kt b/sentry-android-okhttp/src/main/java/io/sentry/android/okhttp/SentryOkHttpInterceptor.kt index f434764c695..129e33bba74 100644 --- a/sentry-android-okhttp/src/main/java/io/sentry/android/okhttp/SentryOkHttpInterceptor.kt +++ b/sentry-android-okhttp/src/main/java/io/sentry/android/okhttp/SentryOkHttpInterceptor.kt @@ -10,6 +10,7 @@ import io.sentry.ISpan import io.sentry.IntegrationName import io.sentry.SentryEvent import io.sentry.SentryIntegrationPackageStorage +import io.sentry.SentryOptions.DEFAULT_PROPAGATION_TARGETS import io.sentry.SpanStatus import io.sentry.TypeCheckHint.OKHTTP_REQUEST import io.sentry.TypeCheckHint.OKHTTP_RESPONSE @@ -46,7 +47,7 @@ class SentryOkHttpInterceptor( private val failedRequestStatusCodes: List = listOf( HttpStatusCodeRange(HttpStatusCodeRange.DEFAULT_MIN, HttpStatusCodeRange.DEFAULT_MAX) ), - private val failedRequestTargets: List = listOf(".*") + private val failedRequestTargets: List = listOf(DEFAULT_PROPAGATION_TARGETS) ) : Interceptor, IntegrationName { constructor() : this(HubAdapter.getInstance()) diff --git a/sentry-apollo-3/api/sentry-apollo-3.api b/sentry-apollo-3/api/sentry-apollo-3.api index 34cfe52ce58..ab0ea327fcc 100644 --- a/sentry-apollo-3/api/sentry-apollo-3.api +++ b/sentry-apollo-3/api/sentry-apollo-3.api @@ -3,15 +3,22 @@ public final class io/sentry/apollo3/BuildConfig { public static final field VERSION_NAME Ljava/lang/String; } +public final class io/sentry/apollo3/SentryApollo3ClientException : java/lang/Exception { + public fun (Ljava/lang/String;)V + public final fun getSerialVersionUID ()J +} + public final class io/sentry/apollo3/SentryApollo3HttpInterceptor : com/apollographql/apollo3/network/http/HttpInterceptor, io/sentry/IntegrationName { 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 DEFAULT_CAPTURE_FAILED_REQUESTS Z 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 ()V public fun (Lio/sentry/IHub;)V public fun (Lio/sentry/IHub;Lio/sentry/apollo3/SentryApollo3HttpInterceptor$BeforeSpanCallback;)V - public synthetic fun (Lio/sentry/IHub;Lio/sentry/apollo3/SentryApollo3HttpInterceptor$BeforeSpanCallback;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Lio/sentry/IHub;Lio/sentry/apollo3/SentryApollo3HttpInterceptor$BeforeSpanCallback;Z)V + public fun (Lio/sentry/IHub;Lio/sentry/apollo3/SentryApollo3HttpInterceptor$BeforeSpanCallback;ZLjava/util/List;)V + public synthetic fun (Lio/sentry/IHub;Lio/sentry/apollo3/SentryApollo3HttpInterceptor$BeforeSpanCallback;ZLjava/util/List;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public fun dispose ()V public fun getIntegrationName ()Ljava/lang/String; public fun intercept (Lcom/apollographql/apollo3/api/http/HttpRequest;Lcom/apollographql/apollo3/network/http/HttpInterceptorChain;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; @@ -33,8 +40,10 @@ 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; + public static final fun sentryTracing (Lcom/apollographql/apollo3/ApolloClient$Builder;Lio/sentry/IHub;Lio/sentry/apollo3/SentryApollo3HttpInterceptor$BeforeSpanCallback;Z)Lcom/apollographql/apollo3/ApolloClient$Builder; + public static final fun sentryTracing (Lcom/apollographql/apollo3/ApolloClient$Builder;Lio/sentry/IHub;Lio/sentry/apollo3/SentryApollo3HttpInterceptor$BeforeSpanCallback;ZLjava/util/List;)Lcom/apollographql/apollo3/ApolloClient$Builder; + public static final fun sentryTracing (Lcom/apollographql/apollo3/ApolloClient$Builder;Lio/sentry/apollo3/SentryApollo3HttpInterceptor$BeforeSpanCallback;ZLjava/util/List;)Lcom/apollographql/apollo3/ApolloClient$Builder; + public static synthetic fun sentryTracing$default (Lcom/apollographql/apollo3/ApolloClient$Builder;Lio/sentry/IHub;Lio/sentry/apollo3/SentryApollo3HttpInterceptor$BeforeSpanCallback;ZLjava/util/List;ILjava/lang/Object;)Lcom/apollographql/apollo3/ApolloClient$Builder; + public static synthetic fun sentryTracing$default (Lcom/apollographql/apollo3/ApolloClient$Builder;Lio/sentry/apollo3/SentryApollo3HttpInterceptor$BeforeSpanCallback;ZLjava/util/List;ILjava/lang/Object;)Lcom/apollographql/apollo3/ApolloClient$Builder; } diff --git a/sentry-apollo-3/src/main/java/io/sentry/apollo3/SentryApollo3ClientException.kt b/sentry-apollo-3/src/main/java/io/sentry/apollo3/SentryApollo3ClientException.kt new file mode 100644 index 00000000000..6814c8b1cc1 --- /dev/null +++ b/sentry-apollo-3/src/main/java/io/sentry/apollo3/SentryApollo3ClientException.kt @@ -0,0 +1,9 @@ +package io.sentry.apollo3 + +/** + * Used for holding an Apollo3 client error, for example. An integration that does not throw when API + * returns 4xx, 5xx or the `errors` field. + */ +class SentryApollo3ClientException(message: String?) : Exception(message) { + val serialVersionUID = 1L +} diff --git a/sentry-apollo-3/src/main/java/io/sentry/apollo3/SentryApollo3HttpInterceptor.kt b/sentry-apollo-3/src/main/java/io/sentry/apollo3/SentryApollo3HttpInterceptor.kt index 736fd84256b..da03f21ed40 100644 --- a/sentry-apollo-3/src/main/java/io/sentry/apollo3/SentryApollo3HttpInterceptor.kt +++ b/sentry-apollo-3/src/main/java/io/sentry/apollo3/SentryApollo3HttpInterceptor.kt @@ -1,33 +1,57 @@ package io.sentry.apollo3 +import com.apollographql.apollo3.api.http.DefaultHttpRequestComposer.Companion.HEADER_APOLLO_OPERATION_ID +import com.apollographql.apollo3.api.http.DefaultHttpRequestComposer.Companion.HEADER_APOLLO_OPERATION_NAME 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.BaggageHeader +import io.sentry.BaggageHeader.BAGGAGE_HEADER import io.sentry.Breadcrumb import io.sentry.Hint import io.sentry.HubAdapter import io.sentry.IHub import io.sentry.ISpan import io.sentry.IntegrationName +import io.sentry.SentryEvent import io.sentry.SentryIntegrationPackageStorage import io.sentry.SentryLevel +import io.sentry.SentryOptions.DEFAULT_PROPAGATION_TARGETS import io.sentry.SpanStatus -import io.sentry.TypeCheckHint +import io.sentry.TypeCheckHint.APOLLO_REQUEST +import io.sentry.TypeCheckHint.APOLLO_RESPONSE +import io.sentry.exception.ExceptionMechanismException +import io.sentry.protocol.Mechanism +import io.sentry.protocol.Request +import io.sentry.protocol.Response +import io.sentry.util.HttpUtils import io.sentry.util.PropagationTargetsUtils import io.sentry.util.UrlUtils import io.sentry.vendor.Base64 +import okio.Buffer +import org.jetbrains.annotations.ApiStatus -class SentryApollo3HttpInterceptor @JvmOverloads constructor(private val hub: IHub = HubAdapter.getInstance(), private val beforeSpan: BeforeSpanCallback? = null) : - HttpInterceptor, IntegrationName { +class SentryApollo3HttpInterceptor @JvmOverloads constructor( + @ApiStatus.Internal private val hub: IHub = HubAdapter.getInstance(), + private val beforeSpan: BeforeSpanCallback? = null, + private val captureFailedRequests: Boolean = DEFAULT_CAPTURE_FAILED_REQUESTS, + private val failedRequestTargets: List = listOf(DEFAULT_PROPAGATION_TARGETS) +) : HttpInterceptor, IntegrationName { init { addIntegrationToSdkVersion() - SentryIntegrationPackageStorage.getInstance().addPackage("maven:io.sentry:sentry-apollo-3", BuildConfig.VERSION_NAME) + if (captureFailedRequests) { + SentryIntegrationPackageStorage.getInstance() + .addIntegration("Apollo3ClientError") + } + SentryIntegrationPackageStorage.getInstance() + .addPackage("maven:io.sentry:sentry-apollo-3", BuildConfig.VERSION_NAME) + } + + private val regex: Regex by lazy { + "(?i)\"errors\"\\s*:\\s*\\[".toRegex() } override suspend fun intercept( @@ -35,52 +59,82 @@ class SentryApollo3HttpInterceptor @JvmOverloads constructor(private val hub: IH chain: HttpInterceptorChain ): HttpResponse { val activeSpan = hub.span - return if (activeSpan == null) { - chain.proceed(request) - } else { - val span = startChild(request, activeSpan) - var cleanedHeaders = removeSentryInternalHeaders(request.headers).toMutableList() + val operationName = getHeader(HEADER_APOLLO_OPERATION_NAME, request.headers) + val operationType = decodeHeaderValue(request, SENTRY_APOLLO_3_OPERATION_TYPE) + val operationId = getHeader(HEADER_APOLLO_OPERATION_ID, request.headers) + + var cleanedHeaders = removeSentryInternalHeaders(request.headers).toMutableList() + var span: ISpan? = null - if (!span.isNoOp && PropagationTargetsUtils.contain(hub.options.tracePropagationTargets, request.url)) { + if (activeSpan != null) { + span = startChild(request, activeSpan, operationName, operationType, operationId) + + if (!span.isNoOp && PropagationTargetsUtils.contain( + hub.options.tracePropagationTargets, + request.url + ) + ) { val sentryTraceHeader = span.toSentryTrace() - val baggageHeader = span.toBaggageHeader(request.headers.filter { it.name == BaggageHeader.BAGGAGE_HEADER }.map { it.value }) + val baggageHeader = span.toBaggageHeader( + cleanedHeaders.filter { + it.name.equals( + BAGGAGE_HEADER, + true + ) + }.map { it.value } + ) cleanedHeaders.add(HttpHeader(sentryTraceHeader.name, sentryTraceHeader.value)) baggageHeader?.let { newHeader -> - cleanedHeaders = cleanedHeaders.filterNot { it.name == BaggageHeader.BAGGAGE_HEADER }.toMutableList().apply { - add(HttpHeader(newHeader.name, newHeader.value)) - } + cleanedHeaders = + cleanedHeaders.filterNot { it.name.equals(BAGGAGE_HEADER, true) } + .toMutableList().apply { + add(HttpHeader(newHeader.name, newHeader.value)) + } } } + } - val requestBuilder = request.newBuilder().apply { - headers(cleanedHeaders) - } - - val modifiedRequest = requestBuilder.build() - var httpResponse: HttpResponse? = null - var statusCode: Int? = null + val requestBuilder = request.newBuilder().apply { + headers(cleanedHeaders) + } - 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 + 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) + + captureEvent(modifiedRequest, httpResponse, operationName, operationType) + + return httpResponse + } catch (e: Throwable) { + // https://github.com/apollographql/apollo-kotlin/issues/4711 will change error handling in v4 + when (e) { + is ApolloHttpException -> { + statusCode = e.statusCode + span?.status = + SpanStatus.fromHttpStatusCode(statusCode, SpanStatus.INTERNAL_ERROR) } - span.throwable = e - throw e - } finally { - finish(span, modifiedRequest, httpResponse, statusCode) + + else -> span?.status = SpanStatus.INTERNAL_ERROR } + span?.throwable = e + throw e + } finally { + finish( + span, + modifiedRequest, + httpResponse, + statusCode, + operationName, + operationType, + operationId + ) } } @@ -89,17 +143,23 @@ class SentryApollo3HttpInterceptor @JvmOverloads constructor(private val hub: IH } private fun removeSentryInternalHeaders(headers: List): List { - return headers.filterNot { it.name == SENTRY_APOLLO_3_VARIABLES || it.name == SENTRY_APOLLO_3_OPERATION_NAME || it.name == SENTRY_APOLLO_3_OPERATION_TYPE } + return headers.filterNot { + it.name.equals(SENTRY_APOLLO_3_VARIABLES, true) || + it.name.equals(SENTRY_APOLLO_3_OPERATION_TYPE, true) + } } - private fun startChild(request: HttpRequest, activeSpan: ISpan): ISpan { + private fun startChild( + request: HttpRequest, + activeSpan: ISpan, + operationName: String?, + operationType: String?, + operationId: String? + ): ISpan { val urlDetails = UrlUtils.parse(request.url) - val method = request.method + val method = request.method.name - val operationName = operationNameFromHeaders(request) - val operationType = decodeHeaderValue(request, SENTRY_APOLLO_3_OPERATION_TYPE) val operation = if (operationType != null) "http.graphql.$operationType" else "http.graphql" - val operationId = request.valueForHeader("X-APOLLO-OPERATION-ID") val variables = decodeHeaderValue(request, SENTRY_APOLLO_3_VARIABLES) val description = "${operationType ?: method} ${operationName ?: urlDetails.urlOrFallback}" @@ -114,40 +174,63 @@ class SentryApollo3HttpInterceptor @JvmOverloads constructor(private val hub: IH variables?.let { setData("variables", it) } + setData("http.method", method) } } - private fun operationNameFromHeaders(request: HttpRequest): String? { - return decodeHeaderValue(request, SENTRY_APOLLO_3_OPERATION_NAME) - ?: request.valueForHeader("X-APOLLO-OPERATION-NAME") - } - private fun decodeHeaderValue(request: HttpRequest, headerName: String): String? { - return request.valueForHeader(headerName)?.let { + return getHeader(headerName, request.headers)?.let { try { String(Base64.decode(it, Base64.NO_WRAP)) - } catch (e: IllegalArgumentException) { - hub.options.logger.log(SentryLevel.ERROR, "Error decoding internal apolloHeader $headerName", e) + } catch (e: Throwable) { + hub.options.logger.log( + SentryLevel.ERROR, + "Error decoding internal apolloHeader $headerName", + e + ) return null } } } - private fun HttpRequest.valueForHeader(key: String) = headers.firstOrNull { it.name == key }?.value + private fun finish( + span: ISpan?, + request: HttpRequest, + response: HttpResponse?, + statusCode: Int?, + operationName: String?, + operationType: String?, + operationId: String? + ) { + var responseContentLength: Long? = null + response?.body?.buffer?.size?.ifHasValidLength { + responseContentLength = it + } - 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 + if (span != null) { + statusCode?.let { + span.setData("http.response.status_code", statusCode) + } + responseContentLength?.let { + span.setData("http.response_content_length", it) + } + 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 + ) } - } catch (e: Throwable) { - hub.options.logger.log(SentryLevel.ERROR, "An error occurred while executing beforeSpan on ApolloInterceptor", e) } + span.finish() } - span.finish() val breadcrumb = Breadcrumb.http(request.url, request.method.name, statusCode) @@ -155,23 +238,26 @@ class SentryApollo3HttpInterceptor @JvmOverloads constructor(private val hub: IH breadcrumb.setData("request_body_size", contentLength) } + operationName?.let { + breadcrumb.setData("operation_name", it) + } + operationType?.let { + breadcrumb.setData("operation_type", it) + } + operationId?.let { + breadcrumb.setData("operation_id", it) + } + val hint = Hint().also { - it.set(TypeCheckHint.APOLLO_REQUEST, request) + it.set(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) - } + responseContentLength?.let { + breadcrumb.setData("response_body_size", it) } - hint.set(TypeCheckHint.APOLLO_RESPONSE, httpResponse) + hint.set(APOLLO_RESPONSE, httpResponse) } hub.addBreadcrumb(breadcrumb, hint) @@ -179,16 +265,174 @@ class SentryApollo3HttpInterceptor @JvmOverloads constructor(private val hub: IH // 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) } } + private fun getHeader(key: String, headers: List): String? { + return headers.firstOrNull { it.name.equals(key, true) }?.value + } + + private fun getHeaders(headers: List): MutableMap? { + // Headers are only sent if isSendDefaultPii is enabled due to PII + if (!hub.options.isSendDefaultPii) { + return null + } + + val headersMap = mutableMapOf() + + for (item in headers) { + val name = item.name + + // header is only sent if isn't sensitive + if (HttpUtils.containsSensitiveHeader(name)) { + continue + } + + headersMap[name] = item.value + } + return headersMap.ifEmpty { null } + } + + private fun captureEvent( + request: HttpRequest, + response: HttpResponse, + operationName: String?, + operationType: String? + ) { + // return if the feature is disabled + if (!captureFailedRequests) { + return + } + + // wrap everything up in a try catch block so every exception is swallowed and degraded + // gracefully + try { + // we pay the price to read the response in the memory to check if there's any errors + // GraphQL does not throw status code 400+ for every type of error + val body = try { + response.body?.peek()?.readUtf8() ?: "" + } catch (e: Throwable) { + hub.options.logger.log( + SentryLevel.ERROR, + "Error reading the response body.", + e + ) + // bail out because the response body has the most important information + return + } + + // if there response body does not have the errors field, do not raise an issue + if (body.isEmpty() || !regex.containsMatchIn(body)) { + return + } + + // not possible to get a parameterized url, but we remove at least the + // query string and the fragment. + // url example: https://api.github.com/users/getsentry/repos/#fragment?query=query + // url will be: https://api.github.com/users/getsentry/repos/ + // ideally we'd like a parameterized url: https://api.github.com/users/{user}/repos/ + // but that's not possible + val urlDetails = UrlUtils.parse(request.url) + + // return if its not a target match + if (!PropagationTargetsUtils.contain(failedRequestTargets, urlDetails.urlOrFallback)) { + return + } + + val mechanism = Mechanism().apply { + type = "SentryApollo3Interceptor" + } + + val fingerprints = mutableListOf() + + val builder = StringBuilder() + builder.append("GraphQL Request failed") + operationName?.let { + builder.append(", name: $it") + fingerprints.add(operationName) + } + operationType?.let { + builder.append(", type: $it") + fingerprints.add(operationType) + } + + val exception = SentryApollo3ClientException(builder.toString()) + val mechanismException = + ExceptionMechanismException(mechanism, exception, Thread.currentThread(), true) + val event = SentryEvent(mechanismException) + + val hint = Hint() + hint.set(APOLLO_REQUEST, request) + hint.set(APOLLO_RESPONSE, response) + + val sentryRequest = Request().apply { + urlDetails.applyToRequest(this) + // Cookie is only sent if isSendDefaultPii is enabled + cookies = + if (hub.options.isSendDefaultPii) getHeader("Cookie", request.headers) else null + method = request.method.name + headers = getHeaders(request.headers) + apiTarget = "graphql" + + request.body?.let { + bodySize = it.contentLength + + val buffer = Buffer() + + try { + it.writeTo(buffer) + data = buffer.readUtf8() + } catch (e: Throwable) { + hub.options.logger.log( + SentryLevel.ERROR, + "Error reading the request body.", + e + ) + // continue because the response body alone can already give some insights + } finally { + buffer.close() + } + } + } + + val sentryResponse = Response().apply { + // Set-Cookie is only sent if isSendDefaultPii is enabled due to PII + cookies = if (hub.options.isSendDefaultPii) { + getHeader( + "Set-Cookie", + response.headers + ) + } else { + null + } + headers = getHeaders(response.headers) + statusCode = response.statusCode + + response.body?.buffer?.size?.ifHasValidLength { contentLength -> + bodySize = contentLength + } + data = body + } + + fingerprints.add(response.statusCode.toString()) + + event.request = sentryRequest + event.contexts.setResponse(sentryResponse) + event.fingerprints = fingerprints + + hub.captureEvent(event, hint) + } catch (e: Throwable) { + hub.options.logger.log( + SentryLevel.ERROR, + "Error capturing the GraphQL error.", + e + ) + } + } + /** * The BeforeSpan callback */ @@ -205,7 +449,7 @@ class SentryApollo3HttpInterceptor @JvmOverloads constructor(private val hub: IH 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" + const val DEFAULT_CAPTURE_FAILED_REQUESTS = false } } diff --git a/sentry-apollo-3/src/main/java/io/sentry/apollo3/SentryApollo3Interceptor.kt b/sentry-apollo-3/src/main/java/io/sentry/apollo3/SentryApollo3Interceptor.kt index d4b98ffc425..0dd13930226 100644 --- a/sentry-apollo-3/src/main/java/io/sentry/apollo3/SentryApollo3Interceptor.kt +++ b/sentry-apollo-3/src/main/java/io/sentry/apollo3/SentryApollo3Interceptor.kt @@ -10,7 +10,6 @@ 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 io.sentry.apollo3.SentryApollo3HttpInterceptor.Companion.SENTRY_APOLLO_3_OPERATION_NAME import io.sentry.apollo3.SentryApollo3HttpInterceptor.Companion.SENTRY_APOLLO_3_OPERATION_TYPE import io.sentry.apollo3.SentryApollo3HttpInterceptor.Companion.SENTRY_APOLLO_3_VARIABLES import io.sentry.vendor.Base64 @@ -24,7 +23,6 @@ class SentryApollo3Interceptor : ApolloInterceptor { ): Flow> { val builder = request.newBuilder() .addHttpHeader(SENTRY_APOLLO_3_OPERATION_TYPE, Base64.encodeToString(operationType(request).toByteArray(), Base64.NO_WRAP)) - .addHttpHeader(SENTRY_APOLLO_3_OPERATION_NAME, Base64.encodeToString(request.operation.name().toByteArray(), Base64.NO_WRAP)) request.scalarAdapters?.let { builder.addHttpHeader(SENTRY_APOLLO_3_VARIABLES, Base64.encodeToString(request.operation.variables(it).valueMap.toString().toByteArray(), Base64.NO_WRAP)) diff --git a/sentry-apollo-3/src/main/java/io/sentry/apollo3/SentryApolloBuilderExtensions.kt b/sentry-apollo-3/src/main/java/io/sentry/apollo3/SentryApolloBuilderExtensions.kt index e4b18622c8e..52aaa243abf 100644 --- a/sentry-apollo-3/src/main/java/io/sentry/apollo3/SentryApolloBuilderExtensions.kt +++ b/sentry-apollo-3/src/main/java/io/sentry/apollo3/SentryApolloBuilderExtensions.kt @@ -3,14 +3,37 @@ package io.sentry.apollo3 import com.apollographql.apollo3.ApolloClient import io.sentry.HubAdapter import io.sentry.IHub +import io.sentry.SentryOptions.DEFAULT_PROPAGATION_TARGETS +import io.sentry.apollo3.SentryApollo3HttpInterceptor.Companion.DEFAULT_CAPTURE_FAILED_REQUESTS @JvmOverloads -fun ApolloClient.Builder.sentryTracing(hub: IHub = HubAdapter.getInstance(), beforeSpan: SentryApollo3HttpInterceptor.BeforeSpanCallback? = null): ApolloClient.Builder { +fun ApolloClient.Builder.sentryTracing( + hub: IHub = HubAdapter.getInstance(), + beforeSpan: SentryApollo3HttpInterceptor.BeforeSpanCallback? = null, + captureFailedRequests: Boolean = DEFAULT_CAPTURE_FAILED_REQUESTS, + failedRequestTargets: List = listOf(DEFAULT_PROPAGATION_TARGETS) +): ApolloClient.Builder { addInterceptor(SentryApollo3Interceptor()) - addHttpInterceptor(SentryApollo3HttpInterceptor(hub, beforeSpan)) + addHttpInterceptor( + SentryApollo3HttpInterceptor( + hub = hub, + beforeSpan = beforeSpan, + captureFailedRequests = captureFailedRequests, + failedRequestTargets = failedRequestTargets + ) + ) return this } -fun ApolloClient.Builder.sentryTracing(beforeSpan: SentryApollo3HttpInterceptor.BeforeSpanCallback? = null): ApolloClient.Builder { - return sentryTracing(HubAdapter.getInstance(), beforeSpan) +fun ApolloClient.Builder.sentryTracing( + beforeSpan: SentryApollo3HttpInterceptor.BeforeSpanCallback? = null, + captureFailedRequests: Boolean = DEFAULT_CAPTURE_FAILED_REQUESTS, + failedRequestTargets: List = listOf(DEFAULT_PROPAGATION_TARGETS) +): ApolloClient.Builder { + return sentryTracing( + hub = HubAdapter.getInstance(), + beforeSpan = beforeSpan, + captureFailedRequests = captureFailedRequests, + failedRequestTargets = failedRequestTargets + ) } diff --git a/sentry-apollo-3/src/test/java/io/sentry/apollo3/SentryApollo3InterceptorClientErrors.kt b/sentry-apollo-3/src/test/java/io/sentry/apollo3/SentryApollo3InterceptorClientErrors.kt new file mode 100644 index 00000000000..2bf0bece694 --- /dev/null +++ b/sentry-apollo-3/src/test/java/io/sentry/apollo3/SentryApollo3InterceptorClientErrors.kt @@ -0,0 +1,391 @@ +package io.sentry.apollo3 + +import com.apollographql.apollo3.ApolloClient +import com.apollographql.apollo3.api.http.HttpRequest +import com.apollographql.apollo3.api.http.HttpResponse +import com.apollographql.apollo3.exception.ApolloException +import io.sentry.Hint +import io.sentry.IHub +import io.sentry.SentryIntegrationPackageStorage +import io.sentry.SentryOptions +import io.sentry.SentryOptions.DEFAULT_PROPAGATION_TARGETS +import io.sentry.TypeCheckHint +import io.sentry.apollo3.SentryApollo3HttpInterceptor.Companion.DEFAULT_CAPTURE_FAILED_REQUESTS +import io.sentry.exception.ExceptionMechanismException +import io.sentry.protocol.SdkVersion +import io.sentry.protocol.SentryId +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import okhttp3.mockwebserver.MockResponse +import okhttp3.mockwebserver.MockWebServer +import okhttp3.mockwebserver.SocketPolicy +import org.mockito.kotlin.any +import org.mockito.kotlin.check +import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNotNull +import kotlin.test.assertNull +import kotlin.test.assertTrue + +class SentryApollo3InterceptorClientErrors { + class Fixture { + val server = MockWebServer() + lateinit var hub: IHub + + private val responseBodyOk = + """{ + "data": { + "launch": { + "__typename": "Launch", + "id": "83", + "site": "CCAFS SLC 40", + "mission": { + "__typename": "Mission", + "name": "Amos-17", + "missionPatch": "https://images2.imgbox.com/a0/ab/XUoByiuR_o.png" + } + } + } +}""" + + val responseBodyNotOk = + """{ + "errors": [ + { + "message": "Cannot query field \"mySite\" on type \"Launch\". Did you mean \"site\"?", + "extensions": { + "code": "GRAPHQL_VALIDATION_FAILED" + } + } + ] +}""" + + fun getSut( + captureFailedRequests: Boolean = DEFAULT_CAPTURE_FAILED_REQUESTS, + failedRequestTargets: List = listOf(DEFAULT_PROPAGATION_TARGETS), + httpStatusCode: Int = 200, + responseBody: String = responseBodyOk, + sendDefaultPii: Boolean = false, + socketPolicy: SocketPolicy = SocketPolicy.KEEP_OPEN + ): ApolloClient { + SentryIntegrationPackageStorage.getInstance().clearStorage() + + hub = mock().apply { + whenever(options).thenReturn( + SentryOptions().apply { + dsn = "https://key@sentry.io/proj" + sdkVersion = SdkVersion("test", "1.2.3") + isSendDefaultPii = sendDefaultPii + } + ) + } + whenever(hub.captureEvent(any(), any())).thenReturn(SentryId.EMPTY_ID) + + val response = MockResponse() + .setBody(responseBody) + .setSocketPolicy(socketPolicy) + .setResponseCode(httpStatusCode) + + if (sendDefaultPii) { + response.addHeader("Set-Cookie", "Test") + } + + server.enqueue( + response + ) + + val builder = ApolloClient.Builder() + .serverUrl(server.url("?myQuery=query#myFragment").toString()) + .sentryTracing( + hub = hub, + captureFailedRequests = captureFailedRequests, + failedRequestTargets = failedRequestTargets + ) + if (sendDefaultPii) { + builder.addHttpHeader("Cookie", "Test") + } + + return builder.build() + } + } + + private val fixture = Fixture() + + // region captureFailedRequests + + @Test + fun `does not capture errors if captureFailedRequests is disabled`() { + val sut = fixture.getSut(responseBody = fixture.responseBodyNotOk) + executeQuery(sut) + + verify(fixture.hub, never()).captureEvent(any(), any()) + } + + @Test + fun `capture errors if captureFailedRequests is enabled`() { + val sut = + fixture.getSut(captureFailedRequests = true, responseBody = fixture.responseBodyNotOk) + executeQuery(sut) + + verify(fixture.hub).captureEvent(any(), any()) + } + + // endregion + + // region Apollo3ClientError + + @Test + fun `does not add Apollo3ClientError integration if captureFailedRequests is disabled`() { + fixture.getSut() + + assertFalse(SentryIntegrationPackageStorage.getInstance().integrations.contains("Apollo3ClientError")) + } + + @Test + fun `adds Apollo3ClientError integration if captureFailedRequests is enabled`() { + fixture.getSut(captureFailedRequests = true) + + assertTrue(SentryIntegrationPackageStorage.getInstance().integrations.contains("Apollo3ClientError")) + } + + // endregion + + // region failedRequestTargets + + @Test + fun `does not capture errors if failedRequestTargets does not match`() { + val sut = fixture.getSut( + captureFailedRequests = true, + failedRequestTargets = listOf("nope.com"), + responseBody = fixture.responseBodyNotOk + ) + executeQuery(sut) + + verify(fixture.hub, never()).captureEvent(any(), any()) + } + + @Test + fun `capture errors if failedRequestTargets matches`() { + val sut = + fixture.getSut(captureFailedRequests = true, responseBody = fixture.responseBodyNotOk) + executeQuery(sut) + + verify(fixture.hub).captureEvent(any(), any()) + } + + // endregion + + // region SentryEvent + + @Test + fun `capture errors with SentryApollo3Interceptor mechanism`() { + val sut = + fixture.getSut(captureFailedRequests = true, responseBody = fixture.responseBodyNotOk) + executeQuery(sut) + + verify(fixture.hub).captureEvent( + check { + val throwable = (it.throwableMechanism as ExceptionMechanismException) + assertEquals("SentryApollo3Interceptor", throwable.exceptionMechanism.type) + }, + any() + ) + } + + @Test + fun `capture errors with title`() { + val sut = + fixture.getSut(captureFailedRequests = true, responseBody = fixture.responseBodyNotOk) + executeQuery(sut) + + verify(fixture.hub).captureEvent( + check { + val throwable = (it.throwableMechanism as ExceptionMechanismException) + assertEquals("GraphQL Request failed, name: LaunchDetails, type: query", throwable.throwable.message) + }, + any() + ) + } + + @Test + fun `capture errors with snapshot flag set`() { + val sut = + fixture.getSut(captureFailedRequests = true, responseBody = fixture.responseBodyNotOk) + executeQuery(sut) + + verify(fixture.hub).captureEvent( + check { + val throwable = (it.throwableMechanism as ExceptionMechanismException) + assertTrue(throwable.isSnapshot) + }, + any() + ) + } + + private val escapeDolar = "\$id" + + @Test + fun `capture errors with request context`() { + val sut = + fixture.getSut(captureFailedRequests = true, responseBody = fixture.responseBodyNotOk) + executeQuery(sut) + val body = + """ +{"operationName":"LaunchDetails","variables":{"id":"83"},"query":"query LaunchDetails($escapeDolar: ID!) { launch(id: $escapeDolar) { id site mission { name missionPatch(size: LARGE) } rocket { name type } } }"} + """.trimIndent() + + verify(fixture.hub).captureEvent( + check { + val request = it.request!! + + assertEquals("http://localhost:${fixture.server.port}/", request.url) + assertEquals("myQuery=query", request.queryString) + assertEquals("myFragment", request.fragment) + assertEquals("Post", request.method) + assertEquals("graphql", request.apiTarget) + assertEquals(193L, request.bodySize) + assertEquals(body, request.data) + assertNull(request.cookies) + assertNull(request.headers) + }, + any() + ) + } + + @Test + fun `capture errors with more request context if sendDefaultPii is enabled`() { + val sut = + fixture.getSut(captureFailedRequests = true, responseBody = fixture.responseBodyNotOk, sendDefaultPii = true) + executeQuery(sut) + + verify(fixture.hub).captureEvent( + check { + val request = it.request!! + + assertEquals("Test", request.cookies) + assertNotNull(request.headers) + assertEquals("LaunchDetails", request.headers?.get("X-APOLLO-OPERATION-NAME")) + }, + any() + ) + } + + @Test + fun `capture errors with response context`() { + val sut = + fixture.getSut(captureFailedRequests = true, responseBody = fixture.responseBodyNotOk) + executeQuery(sut) + + verify(fixture.hub).captureEvent( + check { + val response = it.contexts.response!! + + assertEquals(200, response.statusCode) + assertEquals(200, response.bodySize) + assertEquals(fixture.responseBodyNotOk, response.data) + assertNull(response.cookies) + assertNull(response.headers) + }, + any() + ) + } + + @Test + fun `capture errors with more response context if sendDefaultPii is enabled`() { + val sut = + fixture.getSut(captureFailedRequests = true, responseBody = fixture.responseBodyNotOk, sendDefaultPii = true) + executeQuery(sut) + + verify(fixture.hub).captureEvent( + check { + val response = it.contexts.response!! + + assertEquals("Test", response.cookies) + assertNotNull(response.headers) + assertEquals(200, response.headers?.get("Content-Length")?.toInt()) + }, + any() + ) + } + + @Test + fun `capture errors with specific fingerprints`() { + val sut = + fixture.getSut(captureFailedRequests = true, responseBody = fixture.responseBodyNotOk) + executeQuery(sut) + + verify(fixture.hub).captureEvent( + check { + assertEquals(listOf("LaunchDetails", "query", "200"), it.fingerprints) + }, + any() + ) + } + + // endregion + + // region errors + + @Test + fun `capture errors if response code is equal or higher than 400`() { + val sut = + fixture.getSut(captureFailedRequests = true, responseBody = fixture.responseBodyNotOk, httpStatusCode = 500) + executeQuery(sut) + + // HttpInterceptor does not throw for >= 400 + verify(fixture.hub).captureEvent(any(), any()) + } + + @Test + fun `capture errors swallow any exception during the error transformation`() { + val sut = + fixture.getSut(captureFailedRequests = true, responseBody = fixture.responseBodyNotOk) + + whenever(fixture.hub.captureEvent(any(), any())).thenThrow(RuntimeException()) + + executeQuery(sut) + } + + // endregion + + // region hints + + @Test + fun `hints are set when capturing errors`() { + val sut = + fixture.getSut(captureFailedRequests = true, responseBody = fixture.responseBodyNotOk) + executeQuery(sut) + + verify(fixture.hub).captureEvent( + any(), + check { + val request = it.get(TypeCheckHint.APOLLO_REQUEST) + assertNotNull(request) + assertTrue(request is HttpRequest) + + val response = it.get(TypeCheckHint.APOLLO_RESPONSE) + assertNotNull(response) + assertTrue(response is HttpResponse) + } + ) + } + + // endregion + + private fun executeQuery(sut: ApolloClient, id: String = "83") = runBlocking { + val coroutine = launch { + try { + sut.query(LaunchDetailsQuery(id)).execute() + } catch (e: ApolloException) { + return@launch + } + } + + coroutine.join() + } +} diff --git a/sentry-apollo-3/src/test/java/io/sentry/apollo3/SentryApollo3InterceptorTest.kt b/sentry-apollo-3/src/test/java/io/sentry/apollo3/SentryApollo3InterceptorTest.kt index a45f58d7145..f2efa49da8b 100644 --- a/sentry-apollo-3/src/test/java/io/sentry/apollo3/SentryApollo3InterceptorTest.kt +++ b/sentry-apollo-3/src/test/java/io/sentry/apollo3/SentryApollo3InterceptorTest.kt @@ -7,6 +7,7 @@ import io.sentry.Breadcrumb import io.sentry.IHub import io.sentry.ITransaction import io.sentry.SentryOptions +import io.sentry.SentryOptions.DEFAULT_PROPAGATION_TARGETS import io.sentry.SentryTraceHeader import io.sentry.SentryTracer import io.sentry.SpanStatus @@ -40,7 +41,7 @@ class SentryApollo3InterceptorTest { whenever(options).thenReturn( SentryOptions().apply { dsn = "https://key@sentry.io/proj" - isTraceSampling = true + setTracePropagationTargets(listOf(DEFAULT_PROPAGATION_TARGETS)) sdkVersion = SdkVersion("test", "1.2.3") } ) @@ -115,7 +116,7 @@ class SentryApollo3InterceptorTest { verify(fixture.hub).captureTransaction( check { - assertTransactionDetails(it) + assertTransactionDetails(it, httpStatusCode = 403) assertEquals(SpanStatus.PERMISSION_DENIED, it.spans.first().status) }, anyOrNull(), @@ -130,7 +131,7 @@ class SentryApollo3InterceptorTest { verify(fixture.hub).captureTransaction( check { - assertTransactionDetails(it) + assertTransactionDetails(it, httpStatusCode = null, contentLength = null) assertEquals(SpanStatus.INTERNAL_ERROR, it.spans.first().status) }, anyOrNull(), @@ -239,8 +240,11 @@ class SentryApollo3InterceptorTest { verify(fixture.hub).addBreadcrumb( check { assertEquals("http", it.type) - assertEquals(280L, it.data["response_body_size"]) + // response_body_size is added but mock webserver returns 0 always + assertEquals(0L, it.data["response_body_size"]) assertEquals(193L, it.data["request_body_size"]) + assertEquals("LaunchDetails", it.data["operation_name"]) + assertNotNull(it.data["operation_id"]) }, anyOrNull() ) @@ -255,13 +259,20 @@ class SentryApollo3InterceptorTest { assert(packageInfo.version == BuildConfig.VERSION_NAME) } - private fun assertTransactionDetails(it: SentryTransaction) { + private fun assertTransactionDetails(it: SentryTransaction, httpStatusCode: Int? = 200, contentLength: Long? = 0L) { assertEquals(1, it.spans.size) val httpClientSpan = it.spans.first() assertEquals("http.graphql", httpClientSpan.op) assertTrue { httpClientSpan.description?.startsWith("Post LaunchDetails") == true } assertNotNull(httpClientSpan.data) { assertNotNull(it["operationId"]) + assertEquals("Post", it["http.method"]) + httpStatusCode?.let { code -> + assertEquals(code, it["http.response.status_code"]) + } + contentLength?.let { contentLength -> + assertEquals(contentLength, it["http.response_content_length"]) + } } } diff --git a/sentry-apollo-3/src/test/java/io/sentry/apollo3/SentryApollo3InterceptorWithVariablesTest.kt b/sentry-apollo-3/src/test/java/io/sentry/apollo3/SentryApollo3InterceptorWithVariablesTest.kt index 3813811a01a..eba4a649515 100644 --- a/sentry-apollo-3/src/test/java/io/sentry/apollo3/SentryApollo3InterceptorWithVariablesTest.kt +++ b/sentry-apollo-3/src/test/java/io/sentry/apollo3/SentryApollo3InterceptorWithVariablesTest.kt @@ -141,8 +141,10 @@ class SentryApollo3InterceptorWithVariablesTest { verify(fixture.hub).addBreadcrumb( check { assertEquals("http", it.type) - assertEquals(280L, it.data["response_body_size"]) + // response_body_size is added but mock webserver returns 0 always + assertEquals(0L, it.data["response_body_size"]) assertEquals(193L, it.data["request_body_size"]) + assertEquals("query", it.data["operation_type"]) }, anyOrNull() ) @@ -153,8 +155,7 @@ class SentryApollo3InterceptorWithVariablesTest { executeQuery(fixture.getSut()) val recorderRequest = fixture.server.takeRequest() assertNull(recorderRequest.headers[SentryApollo3HttpInterceptor.SENTRY_APOLLO_3_VARIABLES]) - assertNull(recorderRequest.headers[SentryApollo3HttpInterceptor.SENTRY_APOLLO_3_OPERATION_NAME]) - assertNull(recorderRequest.headers[SentryApollo3HttpInterceptor.SENTRY_APOLLO_3_VARIABLES]) + assertNull(recorderRequest.headers[SentryApollo3HttpInterceptor.SENTRY_APOLLO_3_OPERATION_TYPE]) } private fun assertTransactionDetails(it: SentryTransaction) { diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 72fa5937703..32bfa06b625 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -1626,6 +1626,7 @@ public final class io/sentry/SentryNanotimeDateProvider : io/sentry/SentryDatePr } public class io/sentry/SentryOptions { + public static final field DEFAULT_PROPAGATION_TARGETS Ljava/lang/String; public fun ()V public fun addBundleId (Ljava/lang/String;)V public fun addCollector (Lio/sentry/ICollector;)V diff --git a/sentry/src/main/java/io/sentry/SentryOptions.java b/sentry/src/main/java/io/sentry/SentryOptions.java index 5c0b3a1e08a..c5bbb2aeebc 100644 --- a/sentry/src/main/java/io/sentry/SentryOptions.java +++ b/sentry/src/main/java/io/sentry/SentryOptions.java @@ -44,6 +44,8 @@ @Open public class SentryOptions { + @ApiStatus.Internal public static final @NotNull String DEFAULT_PROPAGATION_TARGETS = ".*"; + /** Default Log level if not specified Default is DEBUG */ static final SentryLevel DEFAULT_DIAGNOSTIC_LEVEL = SentryLevel.DEBUG; @@ -358,7 +360,7 @@ public class SentryOptions { private @Nullable List tracePropagationTargets = null; private final @NotNull List defaultTracePropagationTargets = - Collections.singletonList(".*"); + Collections.singletonList(DEFAULT_PROPAGATION_TARGETS); /** Proguard UUID. */ private @Nullable String proguardUuid; @@ -1781,7 +1783,7 @@ public void setTracingOrigins(final @Nullable List tracingOrigins) { @ApiStatus.Internal public void setTracePropagationTargets(final @Nullable List tracePropagationTargets) { if (tracePropagationTargets == null) { - this.tracePropagationTargets = tracePropagationTargets; + this.tracePropagationTargets = null; } else { @NotNull final List filteredTracePropagationTargets = new ArrayList<>(); for (String target : tracePropagationTargets) {