Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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 .github/ISSUE_TEMPLATE/bug_report_android.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ body:
- sentry-android
- sentry-android-ndk
- sentry-android-timber
- sentry-android-cronet
- sentry-android-fragment
- sentry-android-navigation3
- sentry-android-sqlite
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

### Features

- Add experimental, opt-in Cronet request spans and HTTP breadcrumbs with `sentry-android-cronet` ([#6185](https://github.com/getsentry/sentry-java/pull/6185))

- Deprecate `sendDefaultPii` in favor of `dataCollection` ahead of its removal in 9.0 ([#6158](https://github.com/getsentry/sentry-java/pull/6158))
- Make the tombstone merge time threshold configurable via `SentryAndroidOptions.setTombstoneMergeTimeThresholdMillis` and the `io.sentry.tombstone.merge-time-threshold-millis` manifest option ([#6154](https://github.com/getsentry/sentry-java/pull/6154))

Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ Sentry SDK for Java and Android
| sentry-android-core | [![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-android-core?style=for-the-badge&logo=sentry&color=green)](https://central.sonatype.com/artifact/io.sentry/sentry-android-core) | 21 |
| sentry-android-distribution | [![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-android-distribution?style=for-the-badge&logo=sentry&color=green)](https://central.sonatype.com/artifact/io.sentry/sentry-android-distribution) | 21 |
| sentry-android-ndk | [![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-android-ndk?style=for-the-badge&logo=sentry&color=green)](https://central.sonatype.com/artifact/io.sentry/sentry-android-ndk) | 21 |
| sentry-android-cronet | [![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-android-cronet?style=for-the-badge&logo=sentry&color=green)](https://central.sonatype.com/artifact/io.sentry/sentry-android-cronet) | 21 |
| sentry-android-timber | [![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-android-timber?style=for-the-badge&logo=sentry&color=green)](https://central.sonatype.com/artifact/io.sentry/sentry-android-timber) | 21 |
| sentry-android-fragment | [![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-android-fragment?style=for-the-badge&logo=sentry&color=green)](https://central.sonatype.com/artifact/io.sentry/sentry-android-fragment) | 21 |
| sentry-android-navigation | [![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-android-navigation?style=for-the-badge&logo=sentry&color=green)](https://central.sonatype.com/artifact/io.sentry/sentry-android-navigation) | 21 |
Expand Down
2 changes: 2 additions & 0 deletions buildSrc/src/main/java/Config.kt
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ object Config {
object Sentry {
val SENTRY_JAVA_SDK_NAME = "sentry.java"
val SENTRY_ANDROID_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.android"
val SENTRY_CRONET_SDK_NAME = "$SENTRY_ANDROID_SDK_NAME.cronet"
val SENTRY_TIMBER_SDK_NAME = "$SENTRY_ANDROID_SDK_NAME.timber"
val SENTRY_LOGBACK_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.logback"
val SENTRY_JUL_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.jul"
Expand Down Expand Up @@ -100,6 +101,7 @@ object Config {
"sentry-android-navigation",
"sentry-android-navigation3",
"sentry-android-timber",
"sentry-android-cronet",
"sentry-compose-android",
"sentry-android-sqlite",
"sentry-android-replay"
Expand Down
1 change: 1 addition & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ sentry = { id = "io.sentry.android.gradle", version.ref = "sagp"}
shadow = { id = "com.gradleup.shadow", version = "9.4.1" }

[libraries]
cronet = { module = "org.chromium.net:cronet-api", version = "119.6045.31" }
animalsniffer-gradle-plugin = { module = "ru.vyarus:gradle-animalsniffer-plugin", version.ref = "animalsniffer" }
apache-httpclient = { module = "org.apache.httpcomponents.client5:httpclient5", version = "5.0.4" }
apollo2-coroutines = { module = "com.apollographql.apollo:apollo-coroutines-support", version.ref = "apollo" }
Expand Down
34 changes: 34 additions & 0 deletions sentry-android-cronet/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# sentry-android-cronet

Experimental, opt-in request tracing and HTTP breadcrumbs for [Cronet](https://developer.android.com/develop/connectivity/cronet).

## Usage

Add `io.sentry:sentry-android-cronet` using the same version as your Sentry Android SDK. Supply your own Cronet implementation; this module does not select or bundle an engine.

Wrap your existing callback once per request:

```kotlin
val url = "https://example.com/api"
val callback = SentryCronetCallback(url, "GET", applicationCallback)
val request = cronetEngine.newUrlRequestBuilder(url, callback, executor)
.setHttpMethod("GET")
.build()
callback.start(request) // Instead of request.start()
```

Construct the callback in the initiating Sentry scope. Pass the same URL and method used by the request builder, including `POST` when using an upload provider. Call `start` on the callback with that request, on the executor required by Cronet. Do not reuse a callback for multiple requests.

The callback starts one `http.client` child span under the active transaction when the request starts. It finishes the span and adds an HTTP breadcrumb on success, failure, or cancellation. Without an active transaction, it still adds a breadcrumb. Span data includes the HTTP method, response status, and Cronet's negotiated protocol (including `h3` when reported by Cronet). URL handling uses the same filtering utilities as the OkHttp integration.

Your callback still controls redirects and response-body reads. The span covers the original request through redirects and body consumption, and retains the original URL and method. It does not represent individual redirect hops. Terminal callbacks run after Sentry finishes the span. Exceptions from nonterminal callbacks are left to Cronet, which reports them through `onFailed`.

## Draft Scope

- No automatic engine instrumentation or bytecode instrumentation.
- No `sentry-trace`, `baggage`, or `traceparent` header injection. Redirect-safe propagation needs a separate design.
- No separate DNS, connect, or TLS spans, request/response body capture, or automatic failed-request error events.
- No support for `BidirectionalStream` or Android's separate `android.net.http` API.
- This draft has not been validated with a real Cronet engine or device. Provider compatibility and the public API need Android SDK team review before release.

Public Android integration documentation, release-registry registration, and the `.craft.yml` SDK entry are follow-ups before publishing this module.
22 changes: 22 additions & 0 deletions sentry-android-cronet/api/sentry-android-cronet.api
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
public final class io/sentry/android/cronet/BuildConfig {
public static final field BUILD_TYPE Ljava/lang/String;
public static final field DEBUG Z
public static final field LIBRARY_PACKAGE_NAME Ljava/lang/String;
public static final field SENTRY_CRONET_SDK_NAME Ljava/lang/String;
public static final field VERSION_NAME Ljava/lang/String;
public fun <init> ()V
}

public final class io/sentry/android/cronet/SentryCronetCallback : org/chromium/net/UrlRequest$Callback {
public fun <init> (Ljava/lang/String;Ljava/lang/String;Lorg/chromium/net/UrlRequest$Callback;)V
public fun <init> (Ljava/lang/String;Ljava/lang/String;Lorg/chromium/net/UrlRequest$Callback;Lio/sentry/IScopes;)V
public synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;Lorg/chromium/net/UrlRequest$Callback;Lio/sentry/IScopes;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun onCanceled (Lorg/chromium/net/UrlRequest;Lorg/chromium/net/UrlResponseInfo;)V
public fun onFailed (Lorg/chromium/net/UrlRequest;Lorg/chromium/net/UrlResponseInfo;Lorg/chromium/net/CronetException;)V
public fun onReadCompleted (Lorg/chromium/net/UrlRequest;Lorg/chromium/net/UrlResponseInfo;Ljava/nio/ByteBuffer;)V
public fun onRedirectReceived (Lorg/chromium/net/UrlRequest;Lorg/chromium/net/UrlResponseInfo;Ljava/lang/String;)V
public fun onResponseStarted (Lorg/chromium/net/UrlRequest;Lorg/chromium/net/UrlResponseInfo;)V
public fun onSucceeded (Lorg/chromium/net/UrlRequest;Lorg/chromium/net/UrlResponseInfo;)V
public final fun start (Lorg/chromium/net/UrlRequest;)V
}

91 changes: 91 additions & 0 deletions sentry-android-cronet/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import io.gitlab.arturbosch.detekt.Detekt
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.dsl.KotlinVersion

plugins {
id("com.android.library")
alias(libs.plugins.kotlin.android)
alias(libs.plugins.gradle.versions)
alias(libs.plugins.detekt)
}

android {
compileSdk = libs.versions.compileSdk.get().toInt()
compileSdkMinor = libs.versions.compileSdkMinor.get().toInt()
namespace = "io.sentry.android.cronet"

defaultConfig {
minSdk = libs.versions.minSdk.get().toInt()

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"

// for AGP 4.1
buildConfigField("String", "VERSION_NAME", "\"${project.version}\"")
buildConfigField(
"String",
"SENTRY_CRONET_SDK_NAME",
"\"${Config.Sentry.SENTRY_CRONET_SDK_NAME}\"",
)
}

buildTypes {
getByName("debug") { consumerProguardFiles("proguard-rules.pro") }
getByName("release") { consumerProguardFiles("proguard-rules.pro") }
}

// AGP 9 only generates unit tests for the testBuildType. The debug variant is
// disabled, so unit tests must target release to run at all.
testBuildType = "release"

kotlin {
compilerOptions.jvmTarget = JvmTarget.JVM_1_8
compilerOptions.languageVersion = KotlinVersion.KOTLIN_1_9
compilerOptions.apiVersion = KotlinVersion.KOTLIN_1_9
}

testOptions {
animationsDisabled = true
unitTests.apply {
isReturnDefaultValues = true
isIncludeAndroidResources = true
}
}

lint {
warningsAsErrors = true
checkDependencies = true

// We run a full lint analysis as build part in CI, so skip vital checks for assemble tasks.
checkReleaseBuilds = false
}

buildFeatures { buildConfig = true }

androidComponents.beforeVariants {
it.enable = !Config.Android.shouldSkipDebugVariant(it.buildType)
}
}

kotlin { explicitApi() }

dependencies {
api(projects.sentry)

compileOnly(libs.cronet)
compileOnly(libs.jetbrains.annotations)

implementation(kotlin(Config.kotlinStdLib, Config.kotlinStdLibVersionAndroid))

// tests
testImplementation(libs.cronet)
testImplementation(libs.kotlin.test.junit)
testImplementation(libs.androidx.test.ext.junit)
testImplementation(libs.mockito.kotlin)
testImplementation(libs.mockito.inline)
testImplementation(libs.google.truth)
}

tasks.withType<Detekt>().configureEach {
// Target version of the generated JVM bytecode. It is used for type resolution.
jvmTarget = JavaVersion.VERSION_1_8.toString()
}
2 changes: 2 additions & 0 deletions sentry-android-cronet/proguard-rules.pro
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Preserve source information for stack traces.
-keepattributes LineNumberTable,SourceFile
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
package io.sentry.android.cronet

import io.sentry.Breadcrumb
import io.sentry.IScopes
import io.sentry.ISpan
import io.sentry.Sentry
import io.sentry.SentryIntegrationPackageStorage
import io.sentry.SpanDataConvention
import io.sentry.SpanStatus
import io.sentry.transport.CurrentDateProvider
import io.sentry.util.IntegrationUtils.addIntegrationToSdkVersion
import io.sentry.util.SpanUtils
import io.sentry.util.UrlUtils
import java.nio.ByteBuffer
import java.util.concurrent.RejectedExecutionException
import java.util.concurrent.atomic.AtomicBoolean
import org.chromium.net.CronetException
import org.chromium.net.UrlRequest
import org.chromium.net.UrlResponseInfo
import org.jetbrains.annotations.ApiStatus

/**
* Opt-in tracing and breadcrumbs for one Cronet request. Pass this callback to Cronet's request
* builder, then call [start] instead of [UrlRequest.start]. Do not reuse it for another request.
*
* The supplied method must match the request builder (including POST for uploads). Redirect and
* response-body flow control remain the delegate's responsibility. No request headers are added.
*/
@ApiStatus.Experimental
public class SentryCronetCallback
@JvmOverloads
public constructor(
private val url: String,
private val method: String,
private val delegate: UrlRequest.Callback,
private val scopes: IScopes = Sentry.getCurrentScopes(),
) : UrlRequest.Callback() {
private companion object {
private const val TRACE_ORIGIN = "auto.http.cronet"

init {
SentryIntegrationPackageStorage.getInstance()
.addPackage("maven:io.sentry:sentry-android-cronet", BuildConfig.VERSION_NAME)
}
}

private val started = AtomicBoolean(false)
private val finished = AtomicBoolean(false)
private var span: ISpan? = null
private var startTimestamp: Long = 0

init {
addIntegrationToSdkVersion("Cronet")
}

/** Starts the request and its span together, excluding time spent configuring the builder. */
public fun start(request: UrlRequest) {
check(started.compareAndSet(false, true)) { "Use a new SentryCronetCallback for each request." }
startTimestamp = CurrentDateProvider.getInstance().currentTimeMillis
val details = UrlUtils.parse(url, scopes.options.dataCollectionResolver)
if (!SpanUtils.isIgnored(scopes.options.ignoredSpanOrigins, TRACE_ORIGIN)) {
span = scopes.transaction?.startChild("http.client", "$method ${details.urlOrFallback}")
span?.spanContext?.origin = TRACE_ORIGIN
span?.setData(SpanDataConvention.HTTP_METHOD_KEY, method)
details.applyToSpan(span)
}
try {
request.start()
} catch (e: IllegalStateException) {
finishFailedStart(e)
} catch (e: IllegalArgumentException) {
finishFailedStart(e)
} catch (e: RejectedExecutionException) {
finishFailedStart(e)
}
}

private fun finishFailedStart(error: RuntimeException): Nothing {
finish(null, SpanStatus.INTERNAL_ERROR, error)
throw error
}

@Throws(Exception::class)
override fun onRedirectReceived(
request: UrlRequest,
info: UrlResponseInfo,
newLocationUrl: String,
) {
delegate.onRedirectReceived(request, info, newLocationUrl)
}

@Throws(Exception::class)
override fun onResponseStarted(request: UrlRequest, info: UrlResponseInfo) {
delegate.onResponseStarted(request, info)
}

@Throws(Exception::class)
override fun onReadCompleted(request: UrlRequest, info: UrlResponseInfo, byteBuffer: ByteBuffer) {
delegate.onReadCompleted(request, info, byteBuffer)
}

override fun onSucceeded(request: UrlRequest, info: UrlResponseInfo) {
finish(info, SpanStatus.fromHttpStatusCode(info.httpStatusCode), null)
delegate.onSucceeded(request, info)
}

override fun onFailed(request: UrlRequest, info: UrlResponseInfo?, error: CronetException) {
finish(info, SpanStatus.INTERNAL_ERROR, error)
delegate.onFailed(request, info, error)
}

override fun onCanceled(request: UrlRequest, info: UrlResponseInfo?) {
finish(info, SpanStatus.CANCELLED, null)
delegate.onCanceled(request, info)
}

private fun finish(info: UrlResponseInfo?, status: SpanStatus?, error: Throwable?) {
if (!started.get() || !finished.compareAndSet(false, true)) {
return
}
val breadcrumb =
Breadcrumb.http(url, method, info?.httpStatusCode, scopes.options.dataCollectionResolver)
if (info != null) {
span?.setData(SpanDataConvention.HTTP_STATUS_CODE_KEY, info.httpStatusCode)
if (info.negotiatedProtocol.isNotEmpty()) {
span?.setData("protocol", info.negotiatedProtocol)
breadcrumb.setData("protocol", info.negotiatedProtocol)
}
}
span?.throwable = error
span?.status = status
span?.finish()
breadcrumb.setData(SpanDataConvention.HTTP_START_TIMESTAMP, startTimestamp)
breadcrumb.setData(
SpanDataConvention.HTTP_END_TIMESTAMP,
CurrentDateProvider.getInstance().currentTimeMillis,
)
scopes.addBreadcrumb(breadcrumb)
}
}
Loading
Loading