Skip to content

Commit

Permalink
Merge 826f763 into a485ab0
Browse files Browse the repository at this point in the history
  • Loading branch information
markushi authored May 25, 2023
2 parents a485ab0 + 826f763 commit 5ea803d
Show file tree
Hide file tree
Showing 18 changed files with 478 additions and 38 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

## Unreleased

### Features
- Add support for Sentry Kotlin Compiler Plugin ([#2695](https://github.com/getsentry/sentry-java/pull/2695))
- In conjunction with our sentry-kotlin-compiler-plugin we improved Jetpack Compose support for
- [View Hierarchy](https://docs.sentry.io/platforms/android/enriching-events/viewhierarchy/) support for Jetpack Compose screens
- Automatic breadcrumbs for [user interactions](https://docs.sentry.io/platforms/android/performance/instrumentation/automatic-instrumentation/#user-interaction-instrumentation)

### Fixes

- Base64 encode internal Apollo3 Headers ([#2707](https://github.com/getsentry/sentry-java/pull/2707))
Expand Down
3 changes: 2 additions & 1 deletion sentry-android-core/api/sentry-android-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -317,8 +317,9 @@ public final class io/sentry/android/core/ViewHierarchyEventProcessor : io/sentr
public fun <init> (Lio/sentry/android/core/SentryAndroidOptions;)V
public fun process (Lio/sentry/SentryEvent;Lio/sentry/Hint;)Lio/sentry/SentryEvent;
public static fun snapshotViewHierarchy (Landroid/app/Activity;Lio/sentry/ILogger;)Lio/sentry/protocol/ViewHierarchy;
public static fun snapshotViewHierarchy (Landroid/app/Activity;Lio/sentry/util/thread/IMainThreadChecker;Lio/sentry/ILogger;)Lio/sentry/protocol/ViewHierarchy;
public static fun snapshotViewHierarchy (Landroid/app/Activity;Ljava/util/List;Lio/sentry/util/thread/IMainThreadChecker;Lio/sentry/ILogger;)Lio/sentry/protocol/ViewHierarchy;
public static fun snapshotViewHierarchy (Landroid/view/View;)Lio/sentry/protocol/ViewHierarchy;
public static fun snapshotViewHierarchy (Landroid/view/View;Ljava/util/List;)Lio/sentry/protocol/ViewHierarchy;
public static fun snapshotViewHierarchyAsData (Landroid/app/Activity;Lio/sentry/util/thread/IMainThreadChecker;Lio/sentry/ISerializer;Lio/sentry/ILogger;)[B
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@
import io.sentry.cache.PersistingOptionsObserver;
import io.sentry.cache.PersistingScopeObserver;
import io.sentry.compose.gestures.ComposeGestureTargetLocator;
import io.sentry.compose.viewhierarchy.ComposeViewHierarchyExporter;
import io.sentry.internal.gestures.GestureTargetLocator;
import io.sentry.internal.viewhierarchy.ViewHierarchyExporter;
import io.sentry.transport.NoOpEnvelopeCache;
import io.sentry.util.Objects;
import java.io.BufferedInputStream;
Expand All @@ -44,9 +46,12 @@
@SuppressWarnings("Convert2MethodRef") // older AGP versions do not support method references
final class AndroidOptionsInitializer {

static final String SENTRY_COMPOSE_INTEGRATION_CLASS_NAME =
static final String SENTRY_COMPOSE_GESTURE_INTEGRATION_CLASS_NAME =
"io.sentry.compose.gestures.ComposeGestureTargetLocator";

static final String SENTRY_COMPOSE_VIEW_HIERARCHY_INTEGRATION_CLASS_NAME =
"io.sentry.compose.viewhierarchy.ComposeViewHierarchyExporter";

static final String COMPOSE_CLASS_NAME = "androidx.compose.ui.node.Owner";

/** private ctor */
Expand Down Expand Up @@ -151,22 +156,34 @@ static void initializeIntegrationsAndProcessors(

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

if (options.getGestureTargetLocators().isEmpty()) {
final List<GestureTargetLocator> gestureTargetLocators = new ArrayList<>(2);
gestureTargetLocators.add(new AndroidViewGestureTargetLocator(isAndroidXScrollViewAvailable));

final boolean isComposeUpstreamAvailable =
loadClass.isClassAvailable(COMPOSE_CLASS_NAME, options);
final boolean isComposeAvailable =
(isComposeUpstreamAvailable
&& loadClass.isClassAvailable(SENTRY_COMPOSE_INTEGRATION_CLASS_NAME, options));
&& loadClass.isClassAvailable(
SENTRY_COMPOSE_GESTURE_INTEGRATION_CLASS_NAME, options));

if (isComposeAvailable) {
gestureTargetLocators.add(new ComposeGestureTargetLocator());
gestureTargetLocators.add(new ComposeGestureTargetLocator(options.getLogger()));
}
options.setGestureTargetLocators(gestureTargetLocators);
}

if (options.getViewHierarchyExporters().isEmpty()
&& isComposeUpstreamAvailable
&& loadClass.isClassAvailable(
SENTRY_COMPOSE_VIEW_HIERARCHY_INTEGRATION_CLASS_NAME, options)) {

final List<ViewHierarchyExporter> viewHierarchyExporters = new ArrayList<>(1);
viewHierarchyExporters.add(new ComposeViewHierarchyExporter(options.getLogger()));
options.setViewHierarchyExporters(viewHierarchyExporters);
}

options.setMainThreadChecker(AndroidMainThreadChecker.getInstance());
if (options.getCollectors().isEmpty()) {
options.addCollector(new AndroidMemoryCollector());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import io.sentry.SentryLevel;
import io.sentry.android.core.internal.gestures.ViewUtils;
import io.sentry.android.core.internal.util.AndroidMainThreadChecker;
import io.sentry.internal.viewhierarchy.ViewHierarchyExporter;
import io.sentry.protocol.ViewHierarchy;
import io.sentry.protocol.ViewHierarchyNode;
import io.sentry.util.HintUtils;
Expand Down Expand Up @@ -60,7 +61,11 @@ public ViewHierarchyEventProcessor(final @NotNull SentryAndroidOptions options)

final @Nullable Activity activity = CurrentActivityHolder.getInstance().getActivity();
final @Nullable ViewHierarchy viewHierarchy =
snapshotViewHierarchy(activity, options.getMainThreadChecker(), options.getLogger());
snapshotViewHierarchy(
activity,
options.getViewHierarchyExporters(),
options.getMainThreadChecker(),
options.getLogger());

if (viewHierarchy != null) {
hint.setViewHierarchy(Attachment.fromViewHierarchy(viewHierarchy));
Expand All @@ -74,8 +79,10 @@ public static byte[] snapshotViewHierarchyAsData(
@NotNull IMainThreadChecker mainThreadChecker,
@NotNull ISerializer serializer,
@NotNull ILogger logger) {

@Nullable
ViewHierarchy viewHierarchy = snapshotViewHierarchy(activity, mainThreadChecker, logger);
ViewHierarchy viewHierarchy =
snapshotViewHierarchy(activity, new ArrayList<>(0), mainThreadChecker, logger);

if (viewHierarchy == null) {
logger.log(SentryLevel.ERROR, "Could not get ViewHierarchy.");
Expand All @@ -98,15 +105,18 @@ public static byte[] snapshotViewHierarchyAsData(

@Nullable
public static ViewHierarchy snapshotViewHierarchy(
@Nullable Activity activity, @NotNull ILogger logger) {
return snapshotViewHierarchy(activity, AndroidMainThreadChecker.getInstance(), logger);
final @Nullable Activity activity, final @NotNull ILogger logger) {
return snapshotViewHierarchy(
activity, new ArrayList<>(0), AndroidMainThreadChecker.getInstance(), logger);
}

@Nullable
public static ViewHierarchy snapshotViewHierarchy(
@Nullable Activity activity,
@NotNull IMainThreadChecker mainThreadChecker,
@NotNull ILogger logger) {
final @Nullable Activity activity,
final @NotNull List<ViewHierarchyExporter> exporters,
final @NotNull IMainThreadChecker mainThreadChecker,
final @NotNull ILogger logger) {

if (activity == null) {
logger.log(SentryLevel.INFO, "Missing activity for view hierarchy snapshot.");
return null;
Expand All @@ -126,14 +136,14 @@ public static ViewHierarchy snapshotViewHierarchy(

try {
if (mainThreadChecker.isMainThread()) {
return snapshotViewHierarchy(decorView);
return snapshotViewHierarchy(decorView, exporters);
} else {
final CountDownLatch latch = new CountDownLatch(1);
final AtomicReference<ViewHierarchy> viewHierarchy = new AtomicReference<>(null);
activity.runOnUiThread(
() -> {
try {
viewHierarchy.set(snapshotViewHierarchy(decorView));
viewHierarchy.set(snapshotViewHierarchy(decorView, exporters));
latch.countDown();
} catch (Throwable t) {
logger.log(SentryLevel.ERROR, "Failed to process view hierarchy.", t);
Expand All @@ -150,23 +160,39 @@ public static ViewHierarchy snapshotViewHierarchy(
}

@NotNull
public static ViewHierarchy snapshotViewHierarchy(@NotNull final View view) {
public static ViewHierarchy snapshotViewHierarchy(final @NotNull View view) {
return snapshotViewHierarchy(view, new ArrayList<>(0));
}

@NotNull
public static ViewHierarchy snapshotViewHierarchy(
final @NotNull View view, final @NotNull List<ViewHierarchyExporter> exporters) {
final List<ViewHierarchyNode> windows = new ArrayList<>(1);
final ViewHierarchy viewHierarchy = new ViewHierarchy("android_view_system", windows);

final @NotNull ViewHierarchyNode node = viewToNode(view);
windows.add(node);
addChildren(view, node);
addChildren(view, node, exporters);

return viewHierarchy;
}

private static void addChildren(
@NotNull final View view, @NotNull final ViewHierarchyNode parentNode) {
final @NotNull View view,
final @NotNull ViewHierarchyNode parentNode,
final @NotNull List<ViewHierarchyExporter> exporters) {
if (!(view instanceof ViewGroup)) {
return;
}

// In case any external exporter recognizes it's own widget (e.g. AndroidComposeView)
// we can immediately return
for (ViewHierarchyExporter exporter : exporters) {
if (exporter.export(parentNode, view)) {
return;
}
}

final @NotNull ViewGroup viewGroup = ((ViewGroup) view);
final int childCount = viewGroup.getChildCount();
if (childCount == 0) {
Expand All @@ -179,7 +205,7 @@ private static void addChildren(
if (child != null) {
final @NotNull ViewHierarchyNode childNode = viewToNode(child);
childNodes.add(childNode);
addChildren(child, childNode);
addChildren(child, childNode, exporters);
}
}
parentNode.setChildren(childNodes);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -559,7 +559,7 @@ class AndroidOptionsInitializerTest {
fixture.initSutWithClassLoader(
classesToLoad = listOf(
AndroidOptionsInitializer.COMPOSE_CLASS_NAME,
AndroidOptionsInitializer.SENTRY_COMPOSE_INTEGRATION_CLASS_NAME
AndroidOptionsInitializer.SENTRY_COMPOSE_GESTURE_INTEGRATION_CLASS_NAME
)
)

Expand Down
12 changes: 11 additions & 1 deletion sentry-compose-helper/api/sentry-compose-helper.api
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
public class io/sentry/compose/SentryComposeHelper {
public fun <init> (Lio/sentry/ILogger;)V
public fun getLayoutNodeBoundsInWindow (Landroidx/compose/ui/node/LayoutNode;)Landroidx/compose/ui/geometry/Rect;
}

public final class io/sentry/compose/gestures/ComposeGestureTargetLocator : io/sentry/internal/gestures/GestureTargetLocator {
public fun <init> ()V
public fun <init> (Lio/sentry/ILogger;)V
public fun locate (Ljava/lang/Object;FFLio/sentry/internal/gestures/UiElement$Type;)Lio/sentry/internal/gestures/UiElement;
}

Expand All @@ -10,3 +15,8 @@ public final class io/sentry/compose/helper/BuildConfig {
public static final field VERSION_NAME Ljava/lang/String;
}

public final class io/sentry/compose/viewhierarchy/ComposeViewHierarchyExporter : io/sentry/internal/viewhierarchy/ViewHierarchyExporter {
public fun <init> (Lio/sentry/ILogger;)V
public fun export (Lio/sentry/protocol/ViewHierarchyNode;Ljava/lang/Object;)Z
}

10 changes: 10 additions & 0 deletions sentry-compose-helper/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,16 @@ kotlin {
compileOnly(compose.ui)
}
}
val jvmTest by getting {
dependencies {
implementation(compose.runtime)
implementation(compose.ui)

implementation(Config.TestLibs.kotlinTestJunit)
implementation(Config.TestLibs.mockitoKotlin)
implementation(Config.TestLibs.mockitoInline)
}
}
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package io.sentry.compose;

import androidx.compose.ui.geometry.Rect;
import androidx.compose.ui.layout.LayoutCoordinatesKt;
import androidx.compose.ui.node.LayoutNode;
import androidx.compose.ui.node.LayoutNodeLayoutDelegate;
import io.sentry.ILogger;
import io.sentry.SentryLevel;
import java.lang.reflect.Field;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

public class SentryComposeHelper {

private final @NotNull ILogger logger;
private Field layoutDelegateField = null;

public SentryComposeHelper(final @NotNull ILogger logger) {
this.logger = logger;
try {
final Class<?> clazz = Class.forName("androidx.compose.ui.node.LayoutNode");
layoutDelegateField = clazz.getDeclaredField("layoutDelegate");
layoutDelegateField.setAccessible(true);
} catch (Exception e) {
logger.log(SentryLevel.WARNING, "Could not find LayoutNode.layoutDelegate field");
}
}

public @Nullable Rect getLayoutNodeBoundsInWindow(@NotNull final LayoutNode node) {
if (layoutDelegateField != null) {
try {
final LayoutNodeLayoutDelegate delegate =
(LayoutNodeLayoutDelegate) layoutDelegateField.get(node);
return LayoutCoordinatesKt.boundsInWindow(delegate.getOuterCoordinator().getCoordinates());
} catch (Exception e) {
logger.log(SentryLevel.WARNING, "Could not fetch position for LayoutNode", e);
}
}
return null;
}
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
package io.sentry.compose.gestures;

import androidx.compose.ui.layout.LayoutCoordinatesKt;
import androidx.compose.ui.geometry.Rect;
import androidx.compose.ui.layout.ModifierInfo;
import androidx.compose.ui.node.LayoutNode;
import androidx.compose.ui.node.Owner;
import androidx.compose.ui.semantics.SemanticsConfiguration;
import androidx.compose.ui.semantics.SemanticsModifier;
import androidx.compose.ui.semantics.SemanticsPropertyKey;
import io.sentry.ILogger;
import io.sentry.SentryIntegrationPackageStorage;
import io.sentry.compose.SentryComposeHelper;
import io.sentry.compose.helper.BuildConfig;
import io.sentry.internal.gestures.GestureTargetLocator;
import io.sentry.internal.gestures.UiElement;
Expand All @@ -21,7 +23,10 @@
@SuppressWarnings("KotlinInternalInJava")
public final class ComposeGestureTargetLocator implements GestureTargetLocator {

public ComposeGestureTargetLocator() {
private final @NotNull SentryComposeHelper composeHelper;

public ComposeGestureTargetLocator(final @NotNull ILogger logger) {
this.composeHelper = new SentryComposeHelper(logger);
SentryIntegrationPackageStorage.getInstance().addIntegration("ComposeUserInteraction");
SentryIntegrationPackageStorage.getInstance()
.addPackage("maven:io.sentry:sentry-compose", BuildConfig.VERSION_NAME);
Expand All @@ -45,7 +50,7 @@ public ComposeGestureTargetLocator() {
continue;
}

if (node.isPlaced() && layoutNodeBoundsContain(node, x, y)) {
if (node.isPlaced() && layoutNodeBoundsContain(composeHelper, node, x, y)) {
boolean isClickable = false;
boolean isScrollable = false;
@Nullable String testTag = null;
Expand All @@ -63,7 +68,7 @@ public ComposeGestureTargetLocator() {
isScrollable = true;
} else if ("OnClick".equals(key)) {
isClickable = true;
} else if ("TestTag".equals(key)) {
} else if ("SentryTag".equals(key) || "TestTag".equals(key)) {
if (entry.getValue() instanceof String) {
testTag = (String) entry.getValue();
}
Expand Down Expand Up @@ -92,16 +97,19 @@ public ComposeGestureTargetLocator() {
}

private static boolean layoutNodeBoundsContain(
@NotNull LayoutNode node, final float x, final float y) {
final int nodeHeight = node.getHeight();
final int nodeWidth = node.getWidth();

// Offset is a Kotlin value class, packing x/y into a long
// TODO find a way to use the existing APIs
final long nodePosition = LayoutCoordinatesKt.positionInWindow(node.getCoordinates());
final int nodeX = (int) Float.intBitsToFloat((int) (nodePosition >> 32));
final int nodeY = (int) Float.intBitsToFloat((int) (nodePosition));
@NotNull SentryComposeHelper composeHelper,
@NotNull LayoutNode node,
final float x,
final float y) {

return x >= nodeX && x <= (nodeX + nodeWidth) && y >= nodeY && y <= (nodeY + nodeHeight);
final @Nullable Rect bounds = composeHelper.getLayoutNodeBoundsInWindow(node);
if (bounds == null) {
return false;
} else {
return x >= bounds.getLeft()
&& x <= bounds.getRight()
&& y >= bounds.getTop()
&& y <= bounds.getBottom();
}
}
}
Loading

0 comments on commit 5ea803d

Please sign in to comment.