Skip to content
Open
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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions sentry-android-core/api/sentry-android-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -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 <init> ()V
public fun getAllowedHosts ()Ljava/util/List;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,16 @@
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;
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}.
Expand All @@ -48,6 +51,16 @@ public final class SentryWebViewTracing {
private static final @NotNull Map<WebView, Long> 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<WebView, List<String>> 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() {}

/**
Expand All @@ -60,13 +73,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<String> allowedHosts =
options instanceof SentryAndroidOptions
? ((SentryAndroidOptions) options).getWebViewTracing().getAllowedHosts()
: Collections.<String>emptyList();
return addTracePropagation(webView, allowedHosts, scopes, new LoadClass());
return addTracePropagation(webView, optionsHosts(scopes.getOptions()), scopes, new LoadClass());
}

/**
Expand Down Expand Up @@ -99,6 +108,10 @@ public static boolean addTracePropagation(
* <p>The root span name ({@code sentry-transaction}) is removed from {@code baggage}, as it is
* free-form text that may carry user data.
*
* <p>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
Expand All @@ -108,25 +121,77 @@ public static boolean addTracePropagation(
@MainThread
public static boolean addTracePropagation(
final @NotNull WebView webView, final @NotNull List<String> 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) {
return refreshTracePropagation(webView, new LoadClass());
}

static boolean refreshTracePropagation(
final @NotNull WebView webView, final @NotNull LoadClass loadClass) {
if (!isOnWebViewThread(webView)) {
return false;
}
final @NotNull IScopes scopes = Sentry.getCurrentScopes();
final @Nullable List<String> explicit = explicitHosts.get(webView);
final @NotNull List<String> hosts =
explicit != null ? explicit : optionsHosts(scopes.getOptions());
if (hosts.isEmpty()) {
return false;
}
return addTracePropagation(webView, hosts, scopes, loadClass, true);
}

private static @NotNull List<String> optionsHosts(final @NotNull SentryOptions options) {
return options instanceof SentryAndroidOptions
? ((SentryAndroidOptions) options).getWebViewTracing().getAllowedHosts()
: Collections.<String>emptyList();
}

static boolean addTracePropagation(
final @NotNull WebView webView,
final @NotNull List<String> 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<String> 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());
Expand Down Expand Up @@ -263,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);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
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<String, String> 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;
}
// 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);
log(e);
}
Comment thread
cursor[bot] marked this conversation as resolved.
Resolved
}

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://");
}
}
Loading
Loading