From 1f67a0e598b07ef98922de91e6b963f12c59e426 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Thu, 12 Nov 2026 11:22:02 +0100 Subject: [PATCH 1/3] feat(android): Add a WebView loadUrl adapter for the Sentry Android Gradle Plugin Co-Authored-By: Claude Opus 5.5 --- .../api/sentry-android-core.api | 5 + .../android/core/SentryWebViewTracing.java | 45 ++++- .../core/SentryWebViewTracingAdapter.java | 56 ++++++ .../core/SentryWebViewTracingAdapterTest.kt | 169 ++++++++++++++++++ 4 files changed, 269 insertions(+), 6 deletions(-) create mode 100644 sentry-android-core/src/main/java/io/sentry/android/core/SentryWebViewTracingAdapter.java create mode 100644 sentry-android-core/src/test/java/io/sentry/android/core/SentryWebViewTracingAdapterTest.kt diff --git a/sentry-android-core/api/sentry-android-core.api b/sentry-android-core/api/sentry-android-core.api index 712f981ecd..96aa43187f 100644 --- a/sentry-android-core/api/sentry-android-core.api +++ b/sentry-android-core/api/sentry-android-core.api @@ -635,6 +635,11 @@ public final class io/sentry/android/core/SentryWebViewTracing { public static fun addTracePropagation (Landroid/webkit/WebView;[Ljava/lang/String;)Z } +public final class io/sentry/android/core/SentryWebViewTracingAdapter { + public static fun loadUrl (Landroid/webkit/WebView;Ljava/lang/String;)V + public static fun loadUrl (Landroid/webkit/WebView;Ljava/lang/String;Ljava/util/Map;)V +} + public final class io/sentry/android/core/SentryWebViewTracingOptions { public fun ()V public fun getAllowedHosts ()Ljava/util/List; diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/SentryWebViewTracing.java b/sentry-android-core/src/main/java/io/sentry/android/core/SentryWebViewTracing.java index 133a1c6d8b..db06ad7b31 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/SentryWebViewTracing.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/SentryWebViewTracing.java @@ -16,6 +16,7 @@ import io.sentry.util.ExceptionUtils; import io.sentry.util.LoadClass; import io.sentry.util.TracingUtils; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -48,6 +49,13 @@ public final class SentryWebViewTracing { private static final @NotNull Map replacements = Collections.synchronizedMap(new WeakHashMap<>()); + /** + * The hosts the app passed explicitly, per WebView, so an automatic refresh keeps them instead of + * widening the WebView to the hosts in the options. Weak keys, so it doesn't keep WebViews alive. + */ + private static final @NotNull Map> explicitHosts = + Collections.synchronizedMap(new WeakHashMap<>()); + private SentryWebViewTracing() {} /** @@ -60,13 +68,9 @@ private SentryWebViewTracing() {} */ @MainThread public static boolean addTracePropagation(final @NotNull WebView webView) { + explicitHosts.remove(webView); final @NotNull IScopes scopes = Sentry.getCurrentScopes(); - final @NotNull SentryOptions options = scopes.getOptions(); - final @NotNull List allowedHosts = - options instanceof SentryAndroidOptions - ? ((SentryAndroidOptions) options).getWebViewTracing().getAllowedHosts() - : Collections.emptyList(); - return addTracePropagation(webView, allowedHosts, scopes, new LoadClass()); + return addTracePropagation(webView, optionsHosts(scopes.getOptions()), scopes, new LoadClass()); } /** @@ -108,9 +112,38 @@ public static boolean addTracePropagation( @MainThread public static boolean addTracePropagation( final @NotNull WebView webView, final @NotNull List allowedHosts) { + explicitHosts.put(webView, new ArrayList<>(allowedHosts)); return addTracePropagation(webView, allowedHosts, Sentry.getCurrentScopes(), new LoadClass()); } + /** + * Refreshes the trace propagated to the WebView's next page load, for {@link + * SentryWebViewTracingAdapter}. Uses the hosts the app last passed explicitly for this WebView, + * or else the hosts in the options. Does nothing, and leaves any installed script alone, when + * there are no hosts or it is called off the WebView's thread. + * + * @return true if the trace propagation script was installed + */ + static boolean refreshTracePropagation(final @NotNull WebView webView) { + if (!isOnWebViewThread(webView)) { + return false; + } + final @NotNull IScopes scopes = Sentry.getCurrentScopes(); + final @Nullable List explicit = explicitHosts.get(webView); + final @NotNull List hosts = + explicit != null ? explicit : optionsHosts(scopes.getOptions()); + if (hosts.isEmpty()) { + return false; + } + return addTracePropagation(webView, hosts, scopes, new LoadClass()); + } + + private static @NotNull List optionsHosts(final @NotNull SentryOptions options) { + return options instanceof SentryAndroidOptions + ? ((SentryAndroidOptions) options).getWebViewTracing().getAllowedHosts() + : Collections.emptyList(); + } + static boolean addTracePropagation( final @NotNull WebView webView, final @NotNull List allowedHosts, diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/SentryWebViewTracingAdapter.java b/sentry-android-core/src/main/java/io/sentry/android/core/SentryWebViewTracingAdapter.java new file mode 100644 index 0000000000..74e71f2e13 --- /dev/null +++ b/sentry-android-core/src/main/java/io/sentry/android/core/SentryWebViewTracingAdapter.java @@ -0,0 +1,56 @@ +package io.sentry.android.core; + +import android.webkit.WebView; +import io.sentry.Sentry; +import io.sentry.SentryLevel; +import io.sentry.util.ExceptionUtils; +import java.util.Locale; +import java.util.Map; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * Target of the Sentry Android Gradle Plugin's WebView instrumentation, which replaces {@code + * WebView.loadUrl} calls with calls to this class. Refreshes the trace propagated to the page, then + * calls the original {@code loadUrl}. It never changes how the load itself behaves. + */ +@ApiStatus.Internal +public final class SentryWebViewTracingAdapter { + + private SentryWebViewTracingAdapter() {} + + public static void loadUrl(final @NotNull WebView webView, final @Nullable String url) { + refresh(webView, url); + webView.loadUrl(url); + } + + public static void loadUrl( + final @NotNull WebView webView, + final @Nullable String url, + final @Nullable Map additionalHttpHeaders) { + refresh(webView, url); + webView.loadUrl(url, additionalHttpHeaders); + } + + private static void refresh(final @NotNull WebView webView, final @Nullable String url) { + // Only page loads get a trace. Skips e.g. javascript: URLs, which run code in the current page. + if (url == null || !isHttpUrl(url)) { + return; + } + try { + SentryWebViewTracing.refreshTracePropagation(webView); + } catch (Throwable e) { + ExceptionUtils.rethrowIfFatal(e); + Sentry.getCurrentScopes() + .getOptions() + .getLogger() + .log(SentryLevel.WARNING, "Failed to refresh the WebView trace propagation.", e); + } + } + + private static boolean isHttpUrl(final @NotNull String url) { + final @NotNull String lower = url.trim().toLowerCase(Locale.ROOT); + return lower.startsWith("https://") || lower.startsWith("http://"); + } +} diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/SentryWebViewTracingAdapterTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/SentryWebViewTracingAdapterTest.kt new file mode 100644 index 0000000000..2f60c2228b --- /dev/null +++ b/sentry-android-core/src/test/java/io/sentry/android/core/SentryWebViewTracingAdapterTest.kt @@ -0,0 +1,169 @@ +package io.sentry.android.core + +import android.webkit.WebView +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.webkit.ScriptHandler +import androidx.webkit.WebViewCompat +import androidx.webkit.WebViewFeature +import com.google.common.truth.Truth.assertThat +import io.sentry.OptionsContainer +import io.sentry.Sentry +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertFailsWith +import org.junit.runner.RunWith +import org.mockito.MockedStatic +import org.mockito.Mockito.mockStatic +import org.mockito.kotlin.any +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.verify +import org.robolectric.Shadows.shadowOf + +@RunWith(AndroidJUnit4::class) +class SentryWebViewTracingAdapterTest { + + private lateinit var webView: WebView + private lateinit var webViewCompat: MockedStatic + private lateinit var webViewFeature: MockedStatic + private val installedOrigins = mutableListOf>() + private var installFailure: Throwable? = null + + @BeforeTest + fun setUp() { + webView = WebView(ApplicationProvider.getApplicationContext()) + webViewFeature = mockStatic(WebViewFeature::class.java) + webViewFeature + .`when` { WebViewFeature.isFeatureSupported(WebViewFeature.DOCUMENT_START_SCRIPT) } + .thenReturn(true) + webViewCompat = mockStatic(WebViewCompat::class.java) + webViewCompat + .`when` { WebViewCompat.addDocumentStartJavaScript(eq(webView), any(), any()) } + .thenAnswer { invocation -> + installFailure?.let { throw it } + installedOrigins.add(invocation.getArgument(2)) + mock() + } + } + + @AfterTest + fun tearDown() { + webViewCompat.close() + webViewFeature.close() + Sentry.close() + } + + private fun initSentry(allowedHosts: List = listOf("example.com")) { + Sentry.init(OptionsContainer.create(SentryAndroidOptions::class.java)) { + it.dsn = "https://key@sentry.io/123" + it.webViewTracing.allowedHosts = allowedHosts + } + } + + @Test + fun `refreshes the trace for the hosts in the options and loads the url`() { + initSentry() + + SentryWebViewTracingAdapter.loadUrl(webView, "https://example.com") + SentryWebViewTracingAdapter.loadUrl(webView, "https://example.com/next") + + assertThat(installedOrigins).hasSize(2) + assertThat(installedOrigins.last()) + .containsExactly("https://example.com", "https://*.example.com") + assertThat(shadowOf(webView).lastLoadedUrl).isEqualTo("https://example.com/next") + } + + @Test + fun `passes the headers through`() { + initSentry() + + SentryWebViewTracingAdapter.loadUrl(webView, "https://example.com", mapOf("a" to "b")) + + assertThat(installedOrigins).hasSize(1) + assertThat(shadowOf(webView).lastLoadedUrl).isEqualTo("https://example.com") + assertThat(shadowOf(webView).lastAdditionalHttpHeaders).containsExactly("a", "b") + } + + @Test + fun `keeps the hosts the app passed explicitly`() { + initSentry(allowedHosts = listOf("example.com", "other.com")) + assertThat(SentryWebViewTracing.addTracePropagation(webView, "docs.example.com")).isTrue() + + SentryWebViewTracingAdapter.loadUrl(webView, "https://docs.example.com") + + assertThat(installedOrigins.last()) + .containsExactly("https://docs.example.com", "https://*.docs.example.com") + } + + @Test + fun `uses the options again after the app connects the WebView from the options`() { + initSentry() + SentryWebViewTracing.addTracePropagation(webView, "docs.example.com") + SentryWebViewTracing.addTracePropagation(webView) + + SentryWebViewTracingAdapter.loadUrl(webView, "https://example.com") + + assertThat(installedOrigins.last()) + .containsExactly("https://example.com", "https://*.example.com") + } + + @Test + fun `without hosts only loads the url and keeps a script the app installed`() { + initSentry(allowedHosts = emptyList()) + val handler = mock() + webView.setTag(R.id.sentry_webview_trace_propagation, handler) + + SentryWebViewTracingAdapter.loadUrl(webView, "https://example.com") + + assertThat(installedOrigins).isEmpty() + verify(handler, never()).remove() + assertThat(shadowOf(webView).lastLoadedUrl).isEqualTo("https://example.com") + } + + @Test + fun `does not refresh for urls that are not http`() { + initSentry() + + SentryWebViewTracingAdapter.loadUrl(webView, "javascript:void(0)") + SentryWebViewTracingAdapter.loadUrl(webView, "about:blank") + SentryWebViewTracingAdapter.loadUrl(webView, null) + + assertThat(installedOrigins).isEmpty() + } + + @Test + fun `loads the url when the refresh fails`() { + initSentry() + installFailure = IllegalStateException("WebView destroyed") + + SentryWebViewTracingAdapter.loadUrl(webView, "https://example.com") + + assertThat(shadowOf(webView).lastLoadedUrl).isEqualTo("https://example.com") + } + + @Test + fun `rethrows fatal errors from the refresh`() { + initSentry() + installFailure = OutOfMemoryError() + + assertFailsWith { + SentryWebViewTracingAdapter.loadUrl(webView, "https://example.com") + } + } + + @Test + fun `does not refresh off the WebView's thread`() { + initSentry() + var refreshed: Boolean? = null + + val thread = Thread { refreshed = SentryWebViewTracing.refreshTracePropagation(webView) } + thread.start() + thread.join() + + assertThat(refreshed).isFalse() + assertThat(installedOrigins).isEmpty() + } +} From 9a4bc20ad29e181429f3c4e0e0d25bcb3a67e17b Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Thu, 12 Nov 2026 11:24:37 +0100 Subject: [PATCH 2/3] changelog Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 31ff06f1e9..2dc396c529 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ > - Java 8 support will be dropped. SDK 9.0.0 requires Java 11+. See [#6469](https://github.com/getsentry/sentry-java/issues/6469). > - The Android `minSdk` will be raised from 21 to 24. See [#5508](https://github.com/getsentry/sentry-java/issues/5508). +## Unreleased + +### Internal + +- Add a `WebView.loadUrl` adapter for automatic WebView trace propagation in the Sentry Android Gradle Plugin ([#6592](https://github.com/getsentry/sentry-java/pull/6592)) + ## 8.64.0 ### Breaking Changes From 633c58566b2d53ab2b61d4552075037d34178c9e Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Thu, 12 Nov 2026 11:38:21 +0100 Subject: [PATCH 3/3] fix(android): Keep the WebView loadUrl adapter from throwing LinkageError Also keeps the automatic refresh out of the manual adoption count, warns about a missing androidx.webkit only once, and documents that the refresh keeps explicitly passed hosts. Co-Authored-By: Claude Opus 5.5 --- .../android/core/SentryWebViewTracing.java | 57 ++++++++++++--- .../core/SentryWebViewTracingAdapter.java | 19 +++-- .../core/SentryWebViewTracingAdapterTest.kt | 72 +++++++++++++++++++ 3 files changed, 134 insertions(+), 14 deletions(-) diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/SentryWebViewTracing.java b/sentry-android-core/src/main/java/io/sentry/android/core/SentryWebViewTracing.java index db06ad7b31..7d0beb1fa5 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/SentryWebViewTracing.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/SentryWebViewTracing.java @@ -22,8 +22,10 @@ import java.util.List; import java.util.Map; import java.util.WeakHashMap; +import java.util.concurrent.atomic.AtomicBoolean; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.TestOnly; /** * Connects the app's trace with the trace of web content shown in a {@link WebView}. @@ -56,6 +58,9 @@ public final class SentryWebViewTracing { private static final @NotNull Map> explicitHosts = Collections.synchronizedMap(new WeakHashMap<>()); + /** Whether the automatic refresh already warned that androidx.webkit is missing. */ + private static final @NotNull AtomicBoolean missingWebkitWarned = new AtomicBoolean(false); + private SentryWebViewTracing() {} /** @@ -103,6 +108,10 @@ public static boolean addTracePropagation( *

The root span name ({@code sentry-transaction}) is removed from {@code baggage}, as it is * free-form text that may carry user data. * + *

With the Sentry Android Gradle Plugin's WebView instrumentation, which refreshes the trace + * on every {@code loadUrl} call, the refresh keeps these hosts for this WebView instead of using + * the ones in {@link SentryWebViewTracingOptions}. + * * @param webView the WebView to connect * @param allowedHosts hosts whose pages receive the trace * @return true if the script was installed; false if androidx.webkit is missing, the WebView does @@ -125,6 +134,11 @@ public static boolean addTracePropagation( * @return true if the trace propagation script was installed */ static boolean refreshTracePropagation(final @NotNull WebView webView) { + return refreshTracePropagation(webView, new LoadClass()); + } + + static boolean refreshTracePropagation( + final @NotNull WebView webView, final @NotNull LoadClass loadClass) { if (!isOnWebViewThread(webView)) { return false; } @@ -135,7 +149,7 @@ static boolean refreshTracePropagation(final @NotNull WebView webView) { if (hosts.isEmpty()) { return false; } - return addTracePropagation(webView, hosts, scopes, new LoadClass()); + return addTracePropagation(webView, hosts, scopes, loadClass, true); } private static @NotNull List optionsHosts(final @NotNull SentryOptions options) { @@ -149,17 +163,35 @@ static boolean addTracePropagation( final @NotNull List allowedHosts, final @NotNull IScopes scopes, final @NotNull LoadClass loadClass) { - // Registered on use rather than on success, like the other SDKs, so adoption counts apps that - // wired the feature up even when this call cannot install. - addIntegrationToSdkVersion(INTEGRATION_NAME); + return addTracePropagation(webView, allowedHosts, scopes, loadClass, false); + } + + /** + * @param automatic whether the call comes from the automatic refresh, which runs on every {@code + * loadUrl}: it doesn't record manual adoption, as the Gradle plugin reports its own feature, + * and warns about a missing androidx.webkit only once + */ + private static boolean addTracePropagation( + final @NotNull WebView webView, + final @NotNull List allowedHosts, + final @NotNull IScopes scopes, + final @NotNull LoadClass loadClass, + final boolean automatic) { + if (!automatic) { + // Registered on use rather than on success, like the other SDKs, so adoption counts apps that + // wired the feature up even when this call cannot install. + addIntegrationToSdkVersion(INTEGRATION_NAME); + } if (!loadClass.isClassAvailable(SCRIPT_HANDLER_CLASS_NAME, scopes.getOptions())) { - scopes - .getOptions() - .getLogger() - .log( - SentryLevel.WARNING, - "WebView trace propagation requires the androidx.webkit dependency, skipping."); + if (!automatic || missingWebkitWarned.compareAndSet(false, true)) { + scopes + .getOptions() + .getLogger() + .log( + SentryLevel.WARNING, + "WebView trace propagation requires the androidx.webkit dependency, skipping."); + } return false; } return install(webView, allowedHosts, scopes, new AndroidxWebViewScriptInstaller()); @@ -296,6 +328,11 @@ private static long replacementsOf(final @NotNull WebView webView) { return count == null ? 0 : count; } + @TestOnly + static void resetMissingWebkitWarning() { + missingWebkitWarned.set(false); + } + private static boolean isOnWebViewThread(final @NotNull WebView webView) { return Looper.myLooper() == webViewLooper(webView); } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/SentryWebViewTracingAdapter.java b/sentry-android-core/src/main/java/io/sentry/android/core/SentryWebViewTracingAdapter.java index 74e71f2e13..0b33287ce7 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/SentryWebViewTracingAdapter.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/SentryWebViewTracingAdapter.java @@ -38,17 +38,28 @@ private static void refresh(final @NotNull WebView webView, final @Nullable Stri if (url == null || !isHttpUrl(url)) { return; } + // The catch is broad on purpose: this runs inside the app's loadUrl call, which must behave + // exactly as without Sentry. A failed refresh only means the page doesn't get the trace, and + // the + // load below still happens. Fatal errors are rethrown, except LinkageError, which here means an + // optional androidx.webkit that is missing or too old. try { SentryWebViewTracing.refreshTracePropagation(webView); + } catch (LinkageError e) { + log(e); } catch (Throwable e) { ExceptionUtils.rethrowIfFatal(e); - Sentry.getCurrentScopes() - .getOptions() - .getLogger() - .log(SentryLevel.WARNING, "Failed to refresh the WebView trace propagation.", e); + log(e); } } + private static void log(final @NotNull Throwable e) { + Sentry.getCurrentScopes() + .getOptions() + .getLogger() + .log(SentryLevel.WARNING, "Failed to refresh the WebView trace propagation.", e); + } + private static boolean isHttpUrl(final @NotNull String url) { final @NotNull String lower = url.trim().toLowerCase(Locale.ROOT); return lower.startsWith("https://") || lower.startsWith("http://"); diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/SentryWebViewTracingAdapterTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/SentryWebViewTracingAdapterTest.kt index 2f60c2228b..92f51e7664 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/SentryWebViewTracingAdapterTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/SentryWebViewTracingAdapterTest.kt @@ -7,8 +7,12 @@ import androidx.webkit.ScriptHandler import androidx.webkit.WebViewCompat import androidx.webkit.WebViewFeature import com.google.common.truth.Truth.assertThat +import io.sentry.ILogger import io.sentry.OptionsContainer import io.sentry.Sentry +import io.sentry.SentryIntegrationPackageStorage +import io.sentry.SentryLevel +import io.sentry.util.LoadClass import kotlin.test.AfterTest import kotlin.test.BeforeTest import kotlin.test.Test @@ -21,11 +25,35 @@ import org.mockito.kotlin.eq import org.mockito.kotlin.mock import org.mockito.kotlin.never import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever import org.robolectric.Shadows.shadowOf @RunWith(AndroidJUnit4::class) class SentryWebViewTracingAdapterTest { + private class RecordingLogger : ILogger { + val messages = mutableListOf() + + override fun log(level: SentryLevel, message: String, vararg args: Any?) { + messages.add(message) + } + + override fun log(level: SentryLevel, message: String, throwable: Throwable?) { + messages.add(message) + } + + override fun log( + level: SentryLevel, + throwable: Throwable?, + message: String, + vararg args: Any?, + ) { + messages.add(message) + } + + override fun isEnabled(level: SentryLevel?): Boolean = true + } + private lateinit var webView: WebView private lateinit var webViewCompat: MockedStatic private lateinit var webViewFeature: MockedStatic @@ -154,6 +182,50 @@ class SentryWebViewTracingAdapterTest { } } + @Test + fun `loads the url when the refresh hits a LinkageError`() { + initSentry() + mockStatic(SentryWebViewTracing::class.java).use { tracing -> + tracing + .`when` { SentryWebViewTracing.refreshTracePropagation(any()) } + .thenThrow(NoClassDefFoundError("androidx/webkit/WebViewCompat")) + + SentryWebViewTracingAdapter.loadUrl(webView, "https://example.com") + } + + assertThat(shadowOf(webView).lastLoadedUrl).isEqualTo("https://example.com") + } + + @Test + fun `does not record manual adoption`() { + SentryIntegrationPackageStorage.getInstance().clearStorage() + initSentry() + + SentryWebViewTracingAdapter.loadUrl(webView, "https://example.com") + + assertThat(installedOrigins).hasSize(1) + assertThat(SentryIntegrationPackageStorage.getInstance().integrations) + .doesNotContain("WebViewTracePropagation") + } + + @Test + fun `warns about a missing androidx webkit only once`() { + val logger = RecordingLogger() + Sentry.init(OptionsContainer.create(SentryAndroidOptions::class.java)) { + it.dsn = "https://key@sentry.io/123" + it.isDebug = true + it.setLogger(logger) + it.webViewTracing.allowedHosts = listOf("example.com") + } + SentryWebViewTracing.resetMissingWebkitWarning() + val loadClass = mock() + whenever(loadClass.isClassAvailable(any(), any())).thenReturn(false) + + repeat(3) { SentryWebViewTracing.refreshTracePropagation(webView, loadClass) } + + assertThat(logger.messages.filter { it.contains("requires the androidx.webkit") }).hasSize(1) + } + @Test fun `does not refresh off the WebView's thread`() { initSentry()