diff --git a/CHANGELOG.md b/CHANGELOG.md index 58951b26c6b..8ddc5b5d7bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ - Connect Android traces with web content in a `WebView` using `SentryWebViewTracing.addTracePropagation(webView, "example.com")` ([#6395](https://github.com/getsentry/sentry-java/pull/6395)) - Requires the `androidx.webkit:webkit` dependency. Only list hosts whose content you control. - Call it on the main thread before each page load you want connected. + - Set `options.webViewTracing.allowedHosts`, or the `io.sentry.webview-tracing.allowed-hosts` manifest entry, to call `addTracePropagation(webView)` without passing hosts ([#6516](https://github.com/getsentry/sentry-java/pull/6516)) - See the [WebView docs](https://docs.sentry.io/platforms/android/configuration/webview/) for details. - Monitor every Spring `@Scheduled` method with Sentry Crons by setting `sentry.monitor-scheduled-tasks=true`, without adding `@SentryCheckIn` to each method. Sentry creates each monitor from the method's `@Scheduled` schedule. Exclude methods with `sentry.exclude-scheduled-tasks` ([#6275](https://github.com/getsentry/sentry-java/pull/6275), [#6276](https://github.com/getsentry/sentry-java/pull/6276), [#6277](https://github.com/getsentry/sentry-java/pull/6277)) - Quartz `SentryJobListener` can send a monitor config derived from the trigger with check-ins, so Sentry creates or updates the monitor from code. Set the `sentry-upsert-monitor-config` job or trigger data key to `"true"` to send the schedule ([#6216](https://github.com/getsentry/sentry-java/pull/6216)) diff --git a/sentry-android-core/api/sentry-android-core.api b/sentry-android-core/api/sentry-android-core.api index 85c33e2772d..b14b6873e21 100644 --- a/sentry-android-core/api/sentry-android-core.api +++ b/sentry-android-core/api/sentry-android-core.api @@ -441,6 +441,7 @@ public final class io/sentry/android/core/SentryAndroidOptions : io/sentry/Sentr public fun getScreenshot ()Lio/sentry/android/core/SentryScreenshotOptions; public fun getStartupCrashDurationThresholdMillis ()J public fun getTombstoneMergeTimeThresholdMillis ()J + public fun getWebViewTracing ()Lio/sentry/android/core/SentryWebViewTracingOptions; public fun isAnrEnabled ()Z public fun isAnrProfilingEnabled ()Z public fun isAnrReportInDebug ()Z @@ -629,10 +630,17 @@ public abstract interface class io/sentry/android/core/SentryUserFeedbackForm$Op } public final class io/sentry/android/core/SentryWebViewTracing { + public static fun addTracePropagation (Landroid/webkit/WebView;)Z public static fun addTracePropagation (Landroid/webkit/WebView;Ljava/util/List;)Z public static fun addTracePropagation (Landroid/webkit/WebView;[Ljava/lang/String;)Z } +public final class io/sentry/android/core/SentryWebViewTracingOptions { + public fun ()V + public fun getAllowedHosts ()Ljava/util/List; + public fun setAllowedHosts (Ljava/util/List;)V +} + public class io/sentry/android/core/SpanFrameMetricsCollector : io/sentry/IPerformanceContinuousCollector, io/sentry/android/core/internal/util/SentryFrameMetricsCollector$FrameMetricsCollectorListener { protected final field lock Lio/sentry/util/AutoClosableReentrantLock; public fun (Lio/sentry/android/core/SentryAndroidOptions;Lio/sentry/android/core/internal/util/SentryFrameMetricsCollector;)V diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java b/sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java index 7de5a0c2716..da60741a4c9 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java @@ -104,6 +104,8 @@ final class ManifestMetadataReader { @ApiStatus.Experimental static final String TRACE_SAMPLING = "io.sentry.traces.trace-sampling"; static final String TRACE_PROPAGATION_TARGETS = "io.sentry.traces.trace-propagation-targets"; + static final String WEBVIEW_TRACING_ALLOWED_HOSTS = "io.sentry.webview-tracing.allowed-hosts"; + static final String ATTACH_THREADS = "io.sentry.attach-threads"; static final String PROGUARD_UUID = "io.sentry.proguard-uuid"; static final String IDLE_TIMEOUT = "io.sentry.traces.idle-timeout"; @@ -545,6 +547,12 @@ static void applyMetadata( options.setTracePropagationTargets(tracePropagationTargets); } + final @Nullable List webViewTracingAllowedHosts = + readList(metadata, logger, WEBVIEW_TRACING_ALLOWED_HOSTS); + if (webViewTracingAllowedHosts != null) { + options.getWebViewTracing().setAllowedHosts(webViewTracingAllowedHosts); + } + options.setEnableFramesTracking(readBool(metadata, logger, PERFORM_FRAMES_TRACKING, true)); options.setProguardUuid( diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroidOptions.java b/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroidOptions.java index c135172bc5c..4c75e61a9d4 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroidOptions.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroidOptions.java @@ -321,6 +321,10 @@ public interface BeforeCaptureCallback { */ private final @NotNull SentryScreenshotOptions screenshot = new SentryScreenshotOptions(); + /** Options for connecting the app's trace with web content in a {@code WebView}. */ + private final @NotNull SentryWebViewTracingOptions webViewTracing = + new SentryWebViewTracingOptions(); + /** * Sample rate for capturing main-thread stack profiles when a foreground ANR is detected. {@code * null} disables ANR profiling (default). Values must be between {@code 0.0} and {@code 1.0}. @@ -950,6 +954,15 @@ public void setEnableSystemEventBreadcrumbsExtras( return screenshot; } + /** + * Returns the options for connecting the app's trace with web content in a {@code WebView}. + * + * @return the WebView tracing options + */ + public @NotNull SentryWebViewTracingOptions getWebViewTracing() { + return webViewTracing; + } + /** * Returns the sample rate used when deciding whether to capture a main-thread stack profile for a * detected foreground ANR. 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 ad15c82ab3a..133a1c6d8b5 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 @@ -12,6 +12,7 @@ import io.sentry.IScopes; import io.sentry.Sentry; import io.sentry.SentryLevel; +import io.sentry.SentryOptions; import io.sentry.util.ExceptionUtils; import io.sentry.util.LoadClass; import io.sentry.util.TracingUtils; @@ -49,6 +50,25 @@ public final class SentryWebViewTracing { private SentryWebViewTracing() {} + /** + * Same as {@link #addTracePropagation(WebView, List)}, using the hosts set in {@link + * SentryWebViewTracingOptions#setAllowedHosts(List)}. If none are set, no page is connected and + * this returns false. + * + * @param webView the WebView to connect + * @return true if the trace propagation script was installed + */ + @MainThread + public static boolean addTracePropagation(final @NotNull WebView 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()); + } + /** * Same as {@link #addTracePropagation(WebView, List)}. * diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/SentryWebViewTracingOptions.java b/sentry-android-core/src/main/java/io/sentry/android/core/SentryWebViewTracingOptions.java new file mode 100644 index 00000000000..ffc175b7401 --- /dev/null +++ b/sentry-android-core/src/main/java/io/sentry/android/core/SentryWebViewTracingOptions.java @@ -0,0 +1,34 @@ +package io.sentry.android.core; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import org.jetbrains.annotations.NotNull; + +/** Options for connecting the app's trace with web content in a {@code WebView}. */ +public final class SentryWebViewTracingOptions { + + private @NotNull List allowedHosts = Collections.emptyList(); + + /** + * Returns the hosts {@link SentryWebViewTracing#addTracePropagation(android.webkit.WebView)} + * propagates the trace to. + * + * @return the allowed hosts, empty by default + */ + public @NotNull List getAllowedHosts() { + return allowedHosts; + } + + /** + * Sets the hosts {@link SentryWebViewTracing#addTracePropagation(android.webkit.WebView)} + * propagates the trace to. This is a trust boundary: only list hosts whose content you control. + * It is separate from {@code tracePropagationTargets}, which controls outgoing HTTP requests. + * + * @param allowedHosts the allowed hosts, matched like the ones passed to {@link + * SentryWebViewTracing#addTracePropagation(android.webkit.WebView, List)} + */ + public void setAllowedHosts(final @NotNull List allowedHosts) { + this.allowedHosts = new ArrayList<>(allowedHosts); + } +} diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/ManifestMetadataReaderTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/ManifestMetadataReaderTest.kt index 463b5d82ddb..9ae1a9b023d 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/ManifestMetadataReaderTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/ManifestMetadataReaderTest.kt @@ -1211,6 +1211,32 @@ class ManifestMetadataReaderTest { ) } + @Test + fun `applyMetadata reads webView tracing allowed hosts to options`() { + // Arrange + val bundle = + bundleOf(ManifestMetadataReader.WEBVIEW_TRACING_ALLOWED_HOSTS to "example.com,other.com") + val context = fixture.getContext(metaData = bundle) + + // Act + ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider) + + // Assert + assertEquals(listOf("example.com", "other.com"), fixture.options.webViewTracing.allowedHosts) + } + + @Test + fun `applyMetadata keeps webView tracing allowed hosts empty by default`() { + // Arrange + val context = fixture.getContext() + + // Act + ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider) + + // Assert + assertTrue(fixture.options.webViewTracing.allowedHosts.isEmpty()) + } + @Test fun `applyMetadata reads null tracePropagationTargets and sets empty list`() { // Arrange diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidOptionsTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidOptionsTest.kt index d580ff54cd0..a223358c1d1 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidOptionsTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidOptionsTest.kt @@ -255,6 +255,18 @@ class SentryAndroidOptionsTest { assertEquals(10000L, sentryOptions.tombstoneMergeTimeThresholdMillis) } + @Test + fun `webView tracing allowed hosts are empty by default and copied when set`() { + val sentryOptions = SentryAndroidOptions() + assertTrue(sentryOptions.webViewTracing.allowedHosts.isEmpty()) + + val hosts = mutableListOf("example.com") + sentryOptions.webViewTracing.allowedHosts = hosts + hosts.add("other.com") + + assertEquals(listOf("example.com"), sentryOptions.webViewTracing.allowedHosts) + } + private class CustomDebugImagesLoader : IDebugImagesLoader { override fun loadDebugImages(): List? = null diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/SentryWebViewTracingTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/SentryWebViewTracingTest.kt index fd532c2ff65..f6bb1747426 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/SentryWebViewTracingTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/SentryWebViewTracingTest.kt @@ -9,6 +9,7 @@ import androidx.webkit.WebViewCompat import androidx.webkit.WebViewFeature import com.google.common.truth.Truth.assertThat import io.sentry.IScopes +import io.sentry.OptionsContainer import io.sentry.Sentry import io.sentry.SentryIntegrationPackageStorage import io.sentry.TransactionOptions @@ -20,6 +21,7 @@ import kotlin.test.assertFailsWith import org.junit.runner.RunWith import org.mockito.Mockito.mockStatic import org.mockito.kotlin.any +import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.eq import org.mockito.kotlin.mock import org.mockito.kotlin.times @@ -367,6 +369,44 @@ class SentryWebViewTracingTest { } } + @Test + fun `without hosts uses the allowed hosts from the options`() { + Sentry.init(OptionsContainer.create(SentryAndroidOptions::class.java)) { + it.dsn = "https://key@sentry.io/123" + it.webViewTracing.allowedHosts = listOf("example.com") + } + val origins = argumentCaptor>() + mockStatic(WebViewCompat::class.java).use { webViewCompat -> + mockStatic(WebViewFeature::class.java).use { webViewFeature -> + webViewFeature + .`when` { + WebViewFeature.isFeatureSupported(WebViewFeature.DOCUMENT_START_SCRIPT) + } + .thenReturn(true) + webViewCompat + .`when` { + WebViewCompat.addDocumentStartJavaScript(eq(webView), any(), origins.capture()) + } + .thenReturn(mock()) + + assertThat(SentryWebViewTracing.addTracePropagation(webView)).isTrue() + } + } + + assertThat(origins.firstValue).containsExactly("https://example.com", "https://*.example.com") + } + + @Test + fun `without hosts connects no page when the options have none`() { + Sentry.init(OptionsContainer.create(SentryAndroidOptions::class.java)) { + it.dsn = "https://key@sentry.io/123" + it.setTracePropagationTargets(listOf("example.com")) + } + + assertThat(SentryWebViewTracing.addTracePropagation(webView)).isFalse() + assertThat(webView.getTag(R.id.sentry_webview_trace_propagation)).isNull() + } + @Test fun `androidx installer clears the handler even when its removal throws`() { val handler = mock()