Skip to content

Commit

Permalink
Provide automatic breadcrumbs and transactions for click/scroll event…
Browse files Browse the repository at this point in the history
…s for Compose (#2390)

Co-authored-by: Roman Zavarnitsyn <rom4ek93@gmail.com>
Co-authored-by: Sentry Github Bot <bot+github-bot@sentry.io>
  • Loading branch information
3 people committed Dec 14, 2022
1 parent b5b855d commit 87598a5
Show file tree
Hide file tree
Showing 29 changed files with 769 additions and 185 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -6,6 +6,7 @@

- Add time-to-initial-display span to Activity transactions ([#2369](https://github.com/getsentry/sentry-java/pull/2369))
- Start a session after init if AutoSessionTracking is enabled ([#2356](https://github.com/getsentry/sentry-java/pull/2356))
- Provide automatic breadcrumbs and transactions for click/scroll events for Compose ([#2390](https://github.com/getsentry/sentry-java/pull/2390))

### Dependencies

Expand Down
4 changes: 0 additions & 4 deletions sentry-android-core/api/sentry-android-core.api
Expand Up @@ -160,8 +160,6 @@ public final class io/sentry/android/core/SentryAndroidOptions : io/sentry/Sentr
public fun isEnableAutoActivityLifecycleTracing ()Z
public fun isEnableFramesTracking ()Z
public fun isEnableSystemEventBreadcrumbs ()Z
public fun isEnableUserInteractionBreadcrumbs ()Z
public fun isEnableUserInteractionTracing ()Z
public fun setAnrEnabled (Z)V
public fun setAnrReportInDebug (Z)V
public fun setAnrTimeoutIntervalMillis (J)V
Expand All @@ -175,8 +173,6 @@ public final class io/sentry/android/core/SentryAndroidOptions : io/sentry/Sentr
public fun setEnableAutoActivityLifecycleTracing (Z)V
public fun setEnableFramesTracking (Z)V
public fun setEnableSystemEventBreadcrumbs (Z)V
public fun setEnableUserInteractionBreadcrumbs (Z)V
public fun setEnableUserInteractionTracing (Z)V
public fun setProfilingTracesHz (I)V
public fun setProfilingTracesIntervalMillis (I)V
}
Expand Down
1 change: 1 addition & 0 deletions sentry-android-core/build.gradle.kts
Expand Up @@ -81,6 +81,7 @@ dependencies {
api(projects.sentry)
compileOnly(projects.sentryAndroidFragment)
compileOnly(projects.sentryAndroidTimber)
compileOnly(projects.sentryCompose)

// lifecycle processor, session tracking
implementation(Config.Libs.lifecycleProcess)
Expand Down
Expand Up @@ -12,17 +12,22 @@
import io.sentry.SendFireAndForgetOutboxSender;
import io.sentry.SentryLevel;
import io.sentry.android.core.cache.AndroidEnvelopeCache;
import io.sentry.android.core.internal.gestures.AndroidViewGestureTargetLocator;
import io.sentry.android.core.internal.modules.AssetsModulesLoader;
import io.sentry.android.core.internal.util.SentryFrameMetricsCollector;
import io.sentry.android.fragment.FragmentLifecycleIntegration;
import io.sentry.android.timber.SentryTimberIntegration;
import io.sentry.compose.gestures.ComposeGestureTargetLocator;
import io.sentry.internal.gestures.GestureTargetLocator;
import io.sentry.transport.NoOpEnvelopeCache;
import io.sentry.util.Objects;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
Expand Down Expand Up @@ -130,6 +135,25 @@ static void initializeIntegrationsAndProcessors(
options.setTransactionProfiler(
new AndroidTransactionProfiler(context, options, buildInfoProvider, frameMetricsCollector));
options.setModulesLoader(new AssetsModulesLoader(context, options.getLogger()));

final boolean isAndroidXScrollViewAvailable =
loadClass.isClassAvailable("androidx.core.view.ScrollingView", options);

if (options.getGestureTargetLocators().isEmpty()) {
final List<GestureTargetLocator> gestureTargetLocators = new ArrayList<>(2);
gestureTargetLocators.add(new AndroidViewGestureTargetLocator(isAndroidXScrollViewAvailable));
try {
gestureTargetLocators.add(new ComposeGestureTargetLocator());
} catch (NoClassDefFoundError error) {
options
.getLogger()
.log(
SentryLevel.DEBUG,
"ComposeGestureTargetLocator not available, consider adding the `sentry-compose` library.",
error);
}
options.setGestureTargetLocators(gestureTargetLocators);
}
}

private static void installDefaultIntegrations(
Expand Down
Expand Up @@ -39,9 +39,6 @@ public final class SentryAndroidOptions extends SentryOptions {
/** Enable or disable automatic breadcrumbs for App Components Using ComponentCallbacks */
private boolean enableAppComponentBreadcrumbs = true;

/** Enable or disable automatic breadcrumbs for User interactions Using Window.Callback */
private boolean enableUserInteractionBreadcrumbs = true;

/**
* Enables the Auto instrumentation for Activity lifecycle tracing.
*
Expand Down Expand Up @@ -93,9 +90,6 @@ public final class SentryAndroidOptions extends SentryOptions {
*/
private int profilingTracesHz = 101;

/** Enables the Auto instrumentation for user interaction tracing. */
private boolean enableUserInteractionTracing = false;

/** Interface that loads the debug images list */
private @NotNull IDebugImagesLoader debugImagesLoader = NoOpDebugImagesLoader.getInstance();

Expand Down Expand Up @@ -241,14 +235,6 @@ public void setEnableAppComponentBreadcrumbs(boolean enableAppComponentBreadcrum
this.enableAppComponentBreadcrumbs = enableAppComponentBreadcrumbs;
}

public boolean isEnableUserInteractionBreadcrumbs() {
return enableUserInteractionBreadcrumbs;
}

public void setEnableUserInteractionBreadcrumbs(boolean enableUserInteractionBreadcrumbs) {
this.enableUserInteractionBreadcrumbs = enableUserInteractionBreadcrumbs;
}

/**
* Enable or disable all the automatic breadcrumbs
*
Expand All @@ -259,7 +245,7 @@ public void enableAllAutoBreadcrumbs(boolean enable) {
enableAppComponentBreadcrumbs = enable;
enableSystemEventBreadcrumbs = enable;
enableAppLifecycleBreadcrumbs = enable;
enableUserInteractionBreadcrumbs = enable;
setEnableUserInteractionBreadcrumbs(enable);
}

/**
Expand Down Expand Up @@ -343,14 +329,6 @@ public void setAttachScreenshot(boolean attachScreenshot) {
this.attachScreenshot = attachScreenshot;
}

public boolean isEnableUserInteractionTracing() {
return enableUserInteractionTracing;
}

public void setEnableUserInteractionTracing(boolean enableUserInteractionTracing) {
this.enableUserInteractionTracing = enableUserInteractionTracing;
}

public boolean isCollectAdditionalContext() {
return collectAdditionalContext;
}
Expand Down
Expand Up @@ -25,16 +25,12 @@ public final class UserInteractionIntegration
private @Nullable SentryAndroidOptions options;

private final boolean isAndroidXAvailable;
private final boolean isAndroidXScrollViewAvailable;

public UserInteractionIntegration(
final @NotNull Application application, final @NotNull LoadClass classLoader) {
this.application = Objects.requireNonNull(application, "Application is required");

isAndroidXAvailable =
classLoader.isClassAvailable("androidx.core.view.GestureDetectorCompat", options);
isAndroidXScrollViewAvailable =
classLoader.isClassAvailable("androidx.core.view.ScrollingView", options);
}

private void startTracking(final @NotNull Activity activity) {
Expand All @@ -53,7 +49,7 @@ private void startTracking(final @NotNull Activity activity) {
}

final SentryGestureListener gestureListener =
new SentryGestureListener(activity, hub, options, isAndroidXScrollViewAvailable);
new SentryGestureListener(activity, hub, options);
window.setCallback(new SentryWindowCallback(delegate, activity, gestureListener, options));
}
}
Expand Down Expand Up @@ -112,14 +108,14 @@ public void register(@NotNull IHub hub, @NotNull SentryOptions options) {

this.hub = Objects.requireNonNull(hub, "Hub is required");

final boolean integrationEnabled =
this.options.isEnableUserInteractionBreadcrumbs()
|| this.options.isEnableUserInteractionTracing();
this.options
.getLogger()
.log(
SentryLevel.DEBUG,
"UserInteractionIntegration enabled: %s",
this.options.isEnableUserInteractionBreadcrumbs());
.log(SentryLevel.DEBUG, "UserInteractionIntegration enabled: %s", integrationEnabled);

if (this.options.isEnableUserInteractionBreadcrumbs()) {
if (integrationEnabled) {
if (isAndroidXAvailable) {
application.registerActivityLifecycleCallbacks(this);
this.options.getLogger().log(SentryLevel.DEBUG, "UserInteractionIntegration installed.");
Expand Down
@@ -0,0 +1,85 @@
package io.sentry.android.core.internal.gestures;

import android.content.res.Resources;
import android.view.View;
import android.widget.AbsListView;
import android.widget.ScrollView;
import androidx.core.view.ScrollingView;
import io.sentry.internal.gestures.GestureTargetLocator;
import io.sentry.internal.gestures.UiElement;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

@ApiStatus.Internal
public final class AndroidViewGestureTargetLocator implements GestureTargetLocator {

private final boolean isAndroidXAvailable;
private final int[] coordinates = new int[2];

public AndroidViewGestureTargetLocator(final boolean isAndroidXAvailable) {
this.isAndroidXAvailable = isAndroidXAvailable;
}

@Override
public @Nullable UiElement locate(
@NotNull Object root, float x, float y, UiElement.Type targetType) {
if (!(root instanceof View)) {
return null;
}
final View view = (View) root;
if (touchWithinBounds(view, x, y)) {
if (targetType == UiElement.Type.CLICKABLE && isViewTappable(view)) {
return createUiElement(view);
} else if (targetType == UiElement.Type.SCROLLABLE
&& isViewScrollable(view, isAndroidXAvailable)) {
return createUiElement(view);
}
}
return null;
}

private UiElement createUiElement(final @NotNull View targetView) {
try {
final String resourceName = ViewUtils.getResourceId(targetView);
@Nullable String className = targetView.getClass().getCanonicalName();
if (className == null) {
className = targetView.getClass().getSimpleName();
}
return new UiElement(targetView, className, resourceName, null);
} catch (Resources.NotFoundException ignored) {
return null;
}
}

private boolean touchWithinBounds(final @NotNull View view, final float x, final float y) {
view.getLocationOnScreen(coordinates);
int vx = coordinates[0];
int vy = coordinates[1];

int w = view.getWidth();
int h = view.getHeight();

return !(x < vx || x > vx + w || y < vy || y > vy + h);
}

private static boolean isViewTappable(final @NotNull View view) {
return view.isClickable() && view.getVisibility() == View.VISIBLE;
}

private static boolean isViewScrollable(
final @NotNull View view, final boolean isAndroidXAvailable) {
return (isJetpackScrollingView(view, isAndroidXAvailable)
|| AbsListView.class.isAssignableFrom(view.getClass())
|| ScrollView.class.isAssignableFrom(view.getClass()))
&& view.getVisibility() == View.VISIBLE;
}

private static boolean isJetpackScrollingView(
final @NotNull View view, final boolean isAndroidXAvailable) {
if (!isAndroidXAvailable) {
return false;
}
return ScrollingView.class.isAssignableFrom(view.getClass());
}
}

0 comments on commit 87598a5

Please sign in to comment.