Skip to content

Commit 50ebe9e

Browse files
antonisclaude
andauthored
feat(android): Add WebView tracing options with allowed hosts (#6516)
* feat(android): Connect traces with web content in a WebView Add SentryWebViewTracing.addTracePropagation, which writes the current trace into pages from allowed hosts as sentry-trace and baggage meta tags before any page script runs, so @sentry/browser in the page continues the trace on pageload. It uses androidx.webkit's document-start script, a compileOnly dependency checked at runtime, and adds a sample screen and an instrumented test. Refs JAVA-716 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * changelog Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(android): Keep non-default ports and handle old androidx.webkit A WebView origin rule without a port only matches port 443, so a non-default port in an allowedHosts entry is now kept in the rule. androidx.webkit is compileOnly, so an app may ship an older version than the SDK was compiled against. A LinkageError is now caught, like for androidx.activity in the feedback form, and the call returns false. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(android): Remove the previous WebView script after an off-thread call A call off the WebView's thread returns false without touching the WebView, which left Sentry's previous script installed. The removal is now posted to the WebView's thread, and skipped if a call on that thread replaced the script in the meantime. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(android): Track WebView script replacements per WebView The off-thread cleanup compared against one counter shared by all WebViews, bumped before the replacement finished. A concurrent off-thread call could then remove a script that was just installed, and a call on another WebView cancelled this WebView's cleanup. The count is now kept per WebView, with weak keys, and bumped once the replacement is done. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * ref(samples): Move the WebView trace screen to the Tracing tab The sample no longer shows an iframe; the instrumented test in sentry-uitest-android still checks that iframes don't get the trace. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(android): Match internationalized hosts in WebView trace propagation The page's location.hostname is in punycode, so an allowedHosts entry like münchen.de never matched. Non-ASCII entries are now converted with IDN.toASCII, and entries that cannot be converted are dropped. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(changelog): Shorten WebView trace propagation entry Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(android): Never throw from the posted WebView script removal Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(android): Rethrow fatal errors from the posted WebView script removal Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Add docs link to the changelog * Updated link to WebView documentation in CHANGELOG. * feat(android): Use tracePropagationTargets as the default WebView hosts Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(android): Add WebView tracing options with allowed hosts Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * ref(android): Default WebView allowed hosts to an empty list Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 144b2ee commit 50ebe9e

9 files changed

Lines changed: 162 additions & 0 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
- Connect Android traces with web content in a `WebView` using `SentryWebViewTracing.addTracePropagation(webView, "example.com")` ([#6395](https://github.com/getsentry/sentry-java/pull/6395))
1313
- Requires the `androidx.webkit:webkit` dependency. Only list hosts whose content you control.
1414
- Call it on the main thread before each page load you want connected.
15+
- 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))
1516
- See the [WebView docs](https://docs.sentry.io/platforms/android/configuration/webview/) for details.
1617
- 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))
1718
- 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))

‎sentry-android-core/api/sentry-android-core.api‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -441,6 +441,7 @@ public final class io/sentry/android/core/SentryAndroidOptions : io/sentry/Sentr
441441
public fun getScreenshot ()Lio/sentry/android/core/SentryScreenshotOptions;
442442
public fun getStartupCrashDurationThresholdMillis ()J
443443
public fun getTombstoneMergeTimeThresholdMillis ()J
444+
public fun getWebViewTracing ()Lio/sentry/android/core/SentryWebViewTracingOptions;
444445
public fun isAnrEnabled ()Z
445446
public fun isAnrProfilingEnabled ()Z
446447
public fun isAnrReportInDebug ()Z
@@ -629,10 +630,17 @@ public abstract interface class io/sentry/android/core/SentryUserFeedbackForm$Op
629630
}
630631

631632
public final class io/sentry/android/core/SentryWebViewTracing {
633+
public static fun addTracePropagation (Landroid/webkit/WebView;)Z
632634
public static fun addTracePropagation (Landroid/webkit/WebView;Ljava/util/List;)Z
633635
public static fun addTracePropagation (Landroid/webkit/WebView;[Ljava/lang/String;)Z
634636
}
635637

638+
public final class io/sentry/android/core/SentryWebViewTracingOptions {
639+
public fun <init> ()V
640+
public fun getAllowedHosts ()Ljava/util/List;
641+
public fun setAllowedHosts (Ljava/util/List;)V
642+
}
643+
636644
public class io/sentry/android/core/SpanFrameMetricsCollector : io/sentry/IPerformanceContinuousCollector, io/sentry/android/core/internal/util/SentryFrameMetricsCollector$FrameMetricsCollectorListener {
637645
protected final field lock Lio/sentry/util/AutoClosableReentrantLock;
638646
public fun <init> (Lio/sentry/android/core/SentryAndroidOptions;Lio/sentry/android/core/internal/util/SentryFrameMetricsCollector;)V

‎sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,8 @@ final class ManifestMetadataReader {
104104
@ApiStatus.Experimental static final String TRACE_SAMPLING = "io.sentry.traces.trace-sampling";
105105
static final String TRACE_PROPAGATION_TARGETS = "io.sentry.traces.trace-propagation-targets";
106106

107+
static final String WEBVIEW_TRACING_ALLOWED_HOSTS = "io.sentry.webview-tracing.allowed-hosts";
108+
107109
static final String ATTACH_THREADS = "io.sentry.attach-threads";
108110
static final String PROGUARD_UUID = "io.sentry.proguard-uuid";
109111
static final String IDLE_TIMEOUT = "io.sentry.traces.idle-timeout";
@@ -545,6 +547,12 @@ static void applyMetadata(
545547
options.setTracePropagationTargets(tracePropagationTargets);
546548
}
547549

550+
final @Nullable List<String> webViewTracingAllowedHosts =
551+
readList(metadata, logger, WEBVIEW_TRACING_ALLOWED_HOSTS);
552+
if (webViewTracingAllowedHosts != null) {
553+
options.getWebViewTracing().setAllowedHosts(webViewTracingAllowedHosts);
554+
}
555+
548556
options.setEnableFramesTracking(readBool(metadata, logger, PERFORM_FRAMES_TRACKING, true));
549557

550558
options.setProguardUuid(

‎sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroidOptions.java‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,10 @@ public interface BeforeCaptureCallback {
321321
*/
322322
private final @NotNull SentryScreenshotOptions screenshot = new SentryScreenshotOptions();
323323

324+
/** Options for connecting the app's trace with web content in a {@code WebView}. */
325+
private final @NotNull SentryWebViewTracingOptions webViewTracing =
326+
new SentryWebViewTracingOptions();
327+
324328
/**
325329
* Sample rate for capturing main-thread stack profiles when a foreground ANR is detected. {@code
326330
* null} disables ANR profiling (default). Values must be between {@code 0.0} and {@code 1.0}.
@@ -950,6 +954,15 @@ public void setEnableSystemEventBreadcrumbsExtras(
950954
return screenshot;
951955
}
952956

957+
/**
958+
* Returns the options for connecting the app's trace with web content in a {@code WebView}.
959+
*
960+
* @return the WebView tracing options
961+
*/
962+
public @NotNull SentryWebViewTracingOptions getWebViewTracing() {
963+
return webViewTracing;
964+
}
965+
953966
/**
954967
* Returns the sample rate used when deciding whether to capture a main-thread stack profile for a
955968
* detected foreground ANR.

‎sentry-android-core/src/main/java/io/sentry/android/core/SentryWebViewTracing.java‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import io.sentry.IScopes;
1313
import io.sentry.Sentry;
1414
import io.sentry.SentryLevel;
15+
import io.sentry.SentryOptions;
1516
import io.sentry.util.ExceptionUtils;
1617
import io.sentry.util.LoadClass;
1718
import io.sentry.util.TracingUtils;
@@ -49,6 +50,25 @@ public final class SentryWebViewTracing {
4950

5051
private SentryWebViewTracing() {}
5152

53+
/**
54+
* Same as {@link #addTracePropagation(WebView, List)}, using the hosts set in {@link
55+
* SentryWebViewTracingOptions#setAllowedHosts(List)}. If none are set, no page is connected and
56+
* this returns false.
57+
*
58+
* @param webView the WebView to connect
59+
* @return true if the trace propagation script was installed
60+
*/
61+
@MainThread
62+
public static boolean addTracePropagation(final @NotNull WebView webView) {
63+
final @NotNull IScopes scopes = Sentry.getCurrentScopes();
64+
final @NotNull SentryOptions options = scopes.getOptions();
65+
final @NotNull List<String> allowedHosts =
66+
options instanceof SentryAndroidOptions
67+
? ((SentryAndroidOptions) options).getWebViewTracing().getAllowedHosts()
68+
: Collections.<String>emptyList();
69+
return addTracePropagation(webView, allowedHosts, scopes, new LoadClass());
70+
}
71+
5272
/**
5373
* Same as {@link #addTracePropagation(WebView, List)}.
5474
*
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package io.sentry.android.core;
2+
3+
import java.util.ArrayList;
4+
import java.util.Collections;
5+
import java.util.List;
6+
import org.jetbrains.annotations.NotNull;
7+
8+
/** Options for connecting the app's trace with web content in a {@code WebView}. */
9+
public final class SentryWebViewTracingOptions {
10+
11+
private @NotNull List<String> allowedHosts = Collections.emptyList();
12+
13+
/**
14+
* Returns the hosts {@link SentryWebViewTracing#addTracePropagation(android.webkit.WebView)}
15+
* propagates the trace to.
16+
*
17+
* @return the allowed hosts, empty by default
18+
*/
19+
public @NotNull List<String> getAllowedHosts() {
20+
return allowedHosts;
21+
}
22+
23+
/**
24+
* Sets the hosts {@link SentryWebViewTracing#addTracePropagation(android.webkit.WebView)}
25+
* propagates the trace to. This is a trust boundary: only list hosts whose content you control.
26+
* It is separate from {@code tracePropagationTargets}, which controls outgoing HTTP requests.
27+
*
28+
* @param allowedHosts the allowed hosts, matched like the ones passed to {@link
29+
* SentryWebViewTracing#addTracePropagation(android.webkit.WebView, List)}
30+
*/
31+
public void setAllowedHosts(final @NotNull List<String> allowedHosts) {
32+
this.allowedHosts = new ArrayList<>(allowedHosts);
33+
}
34+
}

‎sentry-android-core/src/test/java/io/sentry/android/core/ManifestMetadataReaderTest.kt‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1211,6 +1211,32 @@ class ManifestMetadataReaderTest {
12111211
)
12121212
}
12131213

1214+
@Test
1215+
fun `applyMetadata reads webView tracing allowed hosts to options`() {
1216+
// Arrange
1217+
val bundle =
1218+
bundleOf(ManifestMetadataReader.WEBVIEW_TRACING_ALLOWED_HOSTS to "example.com,other.com")
1219+
val context = fixture.getContext(metaData = bundle)
1220+
1221+
// Act
1222+
ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider)
1223+
1224+
// Assert
1225+
assertEquals(listOf("example.com", "other.com"), fixture.options.webViewTracing.allowedHosts)
1226+
}
1227+
1228+
@Test
1229+
fun `applyMetadata keeps webView tracing allowed hosts empty by default`() {
1230+
// Arrange
1231+
val context = fixture.getContext()
1232+
1233+
// Act
1234+
ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider)
1235+
1236+
// Assert
1237+
assertTrue(fixture.options.webViewTracing.allowedHosts.isEmpty())
1238+
}
1239+
12141240
@Test
12151241
fun `applyMetadata reads null tracePropagationTargets and sets empty list`() {
12161242
// Arrange

‎sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidOptionsTest.kt‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,18 @@ class SentryAndroidOptionsTest {
255255
assertEquals(10000L, sentryOptions.tombstoneMergeTimeThresholdMillis)
256256
}
257257

258+
@Test
259+
fun `webView tracing allowed hosts are empty by default and copied when set`() {
260+
val sentryOptions = SentryAndroidOptions()
261+
assertTrue(sentryOptions.webViewTracing.allowedHosts.isEmpty())
262+
263+
val hosts = mutableListOf("example.com")
264+
sentryOptions.webViewTracing.allowedHosts = hosts
265+
hosts.add("other.com")
266+
267+
assertEquals(listOf("example.com"), sentryOptions.webViewTracing.allowedHosts)
268+
}
269+
258270
private class CustomDebugImagesLoader : IDebugImagesLoader {
259271
override fun loadDebugImages(): List<DebugImage>? = null
260272

‎sentry-android-core/src/test/java/io/sentry/android/core/SentryWebViewTracingTest.kt‎

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import androidx.webkit.WebViewCompat
99
import androidx.webkit.WebViewFeature
1010
import com.google.common.truth.Truth.assertThat
1111
import io.sentry.IScopes
12+
import io.sentry.OptionsContainer
1213
import io.sentry.Sentry
1314
import io.sentry.SentryIntegrationPackageStorage
1415
import io.sentry.TransactionOptions
@@ -20,6 +21,7 @@ import kotlin.test.assertFailsWith
2021
import org.junit.runner.RunWith
2122
import org.mockito.Mockito.mockStatic
2223
import org.mockito.kotlin.any
24+
import org.mockito.kotlin.argumentCaptor
2325
import org.mockito.kotlin.eq
2426
import org.mockito.kotlin.mock
2527
import org.mockito.kotlin.times
@@ -367,6 +369,44 @@ class SentryWebViewTracingTest {
367369
}
368370
}
369371

372+
@Test
373+
fun `without hosts uses the allowed hosts from the options`() {
374+
Sentry.init(OptionsContainer.create(SentryAndroidOptions::class.java)) {
375+
it.dsn = "https://key@sentry.io/123"
376+
it.webViewTracing.allowedHosts = listOf("example.com")
377+
}
378+
val origins = argumentCaptor<Set<String>>()
379+
mockStatic(WebViewCompat::class.java).use { webViewCompat ->
380+
mockStatic(WebViewFeature::class.java).use { webViewFeature ->
381+
webViewFeature
382+
.`when`<Boolean> {
383+
WebViewFeature.isFeatureSupported(WebViewFeature.DOCUMENT_START_SCRIPT)
384+
}
385+
.thenReturn(true)
386+
webViewCompat
387+
.`when`<ScriptHandler> {
388+
WebViewCompat.addDocumentStartJavaScript(eq(webView), any(), origins.capture())
389+
}
390+
.thenReturn(mock<ScriptHandler>())
391+
392+
assertThat(SentryWebViewTracing.addTracePropagation(webView)).isTrue()
393+
}
394+
}
395+
396+
assertThat(origins.firstValue).containsExactly("https://example.com", "https://*.example.com")
397+
}
398+
399+
@Test
400+
fun `without hosts connects no page when the options have none`() {
401+
Sentry.init(OptionsContainer.create(SentryAndroidOptions::class.java)) {
402+
it.dsn = "https://key@sentry.io/123"
403+
it.setTracePropagationTargets(listOf("example.com"))
404+
}
405+
406+
assertThat(SentryWebViewTracing.addTracePropagation(webView)).isFalse()
407+
assertThat(webView.getTag(R.id.sentry_webview_trace_propagation)).isNull()
408+
}
409+
370410
@Test
371411
fun `androidx installer clears the handler even when its removal throws`() {
372412
val handler = mock<ScriptHandler>()

0 commit comments

Comments
 (0)