Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for Sentry Kotlin Compiler Plugin #2695

Merged
merged 11 commits into from
May 26, 2023
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
- Auxiliary information (such as current memory load) at the time of ANR event.
- If you would like us to provide support for the old approach working alongside the new one on Android 11 and above (e.g. for raising events for slow code on main thread), consider upvoting [this issue](https://github.com/getsentry/sentry-java/issues/2693).
- The old watchdog implementation will continue working for older API versions (Android < 11)
- Add support for Sentry Kotlin Compiler Plugin ([#2695](https://github.com/getsentry/sentry-java/pull/2695))
markushi marked this conversation as resolved.
Show resolved Hide resolved

### Fixes

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 @@ -13,6 +13,7 @@
import io.sentry.SentryEvent;
import io.sentry.SentryLevel;
import io.sentry.android.core.internal.gestures.ViewUtils;
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 @@ -54,7 +55,7 @@ public ViewHierarchyEventProcessor(final @NotNull SentryAndroidOptions options)

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

if (viewHierarchy != null) {
hint.setViewHierarchy(Attachment.fromViewHierarchy(viewHierarchy));
Expand All @@ -65,8 +66,11 @@ public ViewHierarchyEventProcessor(final @NotNull SentryAndroidOptions options)

@Nullable
public static byte[] snapshotViewHierarchyAsData(
@Nullable Activity activity, @NotNull ISerializer serializer, @NotNull ILogger logger) {
@Nullable ViewHierarchy viewHierarchy = snapshotViewHierarchy(activity, logger);
final @Nullable Activity activity,
final @NotNull ISerializer serializer,
final @NotNull ILogger logger) {
@Nullable
ViewHierarchy viewHierarchy = snapshotViewHierarchy(activity, logger, new ArrayList<>(0));

if (viewHierarchy == null) {
logger.log(SentryLevel.ERROR, "Could not get ViewHierarchy.");
Expand All @@ -89,7 +93,9 @@ public static byte[] snapshotViewHierarchyAsData(

@Nullable
public static ViewHierarchy snapshotViewHierarchy(
@Nullable Activity activity, @NotNull ILogger logger) {
final @Nullable Activity activity,
final @NotNull ILogger logger,
final @NotNull List<ViewHierarchyExporter> exporters) {
markushi marked this conversation as resolved.
Show resolved Hide resolved
if (activity == null) {
logger.log(SentryLevel.INFO, "Missing activity for view hierarchy snapshot.");
return null;
Expand All @@ -108,7 +114,7 @@ public static ViewHierarchy snapshotViewHierarchy(
}

try {
final @NotNull ViewHierarchy viewHierarchy = snapshotViewHierarchy(decorView);
final @NotNull ViewHierarchy viewHierarchy = snapshotViewHierarchy(decorView, exporters);
return viewHierarchy;
} catch (Throwable t) {
logger.log(SentryLevel.ERROR, "Failed to process view hierarchy.", t);
Expand All @@ -117,23 +123,34 @@ public static ViewHierarchy snapshotViewHierarchy(
}

@NotNull
public static ViewHierarchy snapshotViewHierarchy(@NotNull final View view) {
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 @@ -146,7 +163,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 @@ -516,7 +516,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
10 changes: 10 additions & 0 deletions sentry-compose-helper/api/sentry-compose-helper.api
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
public class io/sentry/compose/SentryComposeUtil {
public fun <init> ()V
public static final fun getLayoutNodeXY (Landroidx/compose/ui/node/LayoutNode;)[I
}

public final class io/sentry/compose/gestures/ComposeGestureTargetLocator : io/sentry/internal/gestures/GestureTargetLocator {
public fun <init> ()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> ()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