Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
669c5aa
feat(android): Connect traces with web content in a WebView
antonis Oct 27, 2026
3592723
changelog
antonis Oct 27, 2026
3c84e98
fix(android): Keep non-default ports and handle old androidx.webkit
antonis Oct 28, 2026
23b79a7
fix(android): Remove the previous WebView script after an off-thread …
antonis Oct 28, 2026
8db8222
fix(android): Track WebView script replacements per WebView
antonis Oct 28, 2026
bbf1517
ref(samples): Move the WebView trace screen to the Tracing tab
antonis Oct 28, 2026
7675f4b
Merge branch 'main' into feat/webview-trace-propagation
antonis Nov 3, 2026
09986d3
fix(android): Match internationalized hosts in WebView trace propagation
antonis Nov 3, 2026
a4bf66e
Merge remote-tracking branch 'origin/main' into feat/webview-trace-pr…
antonis Nov 5, 2026
1bc0dab
docs(changelog): Shorten WebView trace propagation entry
antonis Nov 5, 2026
42ab290
fix(android): Never throw from the posted WebView script removal
antonis Nov 5, 2026
b167883
fix(android): Rethrow fatal errors from the posted WebView script rem…
antonis Nov 5, 2026
4077fc5
Merge remote-tracking branch 'origin/main' into feat/webview-trace-pr…
antonis Nov 9, 2026
3be7c7a
Add docs link to the changelog
antonis Nov 9, 2026
952ff1b
Updated link to WebView documentation in CHANGELOG.
antonis Nov 9, 2026
baf1646
feat(android): Use tracePropagationTargets as the default WebView hosts
antonis Nov 9, 2026
e9dd09e
feat(android): Add WebView tracing options with allowed hosts
antonis Nov 10, 2026
0f97f2d
Merge remote-tracking branch 'origin/main' into feat/webview-trace-pr…
antonis Nov 10, 2026
f148bd1
ref(android): Default WebView allowed hosts to an empty list
antonis Nov 10, 2026
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 CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
8 changes: 8 additions & 0 deletions sentry-android-core/api/sentry-android-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 <init> ()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 <init> (Lio/sentry/android/core/SentryAndroidOptions;Lio/sentry/android/core/internal/util/SentryFrameMetricsCollector;)V
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -545,6 +547,12 @@ static void applyMetadata(
options.setTracePropagationTargets(tracePropagationTargets);
}

final @Nullable List<String> 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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}.
Expand Down Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<String> allowedHosts =
options instanceof SentryAndroidOptions
? ((SentryAndroidOptions) options).getWebViewTracing().getAllowedHosts()
: Collections.<String>emptyList();
return addTracePropagation(webView, allowedHosts, scopes, new LoadClass());
}

/**
* Same as {@link #addTracePropagation(WebView, List)}.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String> 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<String> 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<String> allowedHosts) {
this.allowedHosts = new ArrayList<>(allowedHosts);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<DebugImage>? = null

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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<Set<String>>()
mockStatic(WebViewCompat::class.java).use { webViewCompat ->
mockStatic(WebViewFeature::class.java).use { webViewFeature ->
webViewFeature
.`when`<Boolean> {
WebViewFeature.isFeatureSupported(WebViewFeature.DOCUMENT_START_SCRIPT)
}
.thenReturn(true)
webViewCompat
.`when`<ScriptHandler> {
WebViewCompat.addDocumentStartJavaScript(eq(webView), any(), origins.capture())
}
.thenReturn(mock<ScriptHandler>())

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<ScriptHandler>()
Expand Down
Loading