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

Provide automatic breadcrumbs and transactions for click/scroll events for Compose #2390

Merged
merged 31 commits into from Dec 14, 2022
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
8be4802
Add integration for Compose clickables
markushi Nov 23, 2022
7efaaff
Fix only auto-create breadcrumbs for clicks when option is enabled
markushi Nov 23, 2022
56325fd
Fix missing .api file update and formatting
markushi Nov 23, 2022
b58a8d7
Only create clickable transaction when there's none running
markushi Nov 24, 2022
611beca
Determine Compose click and scroll targets at runtime
markushi Nov 30, 2022
abcd310
Merge branch 'main' into feat/compose-ui-transactions
markushi Nov 30, 2022
f185262
Remove obsolete implementation
markushi Nov 30, 2022
89260ac
Update sentry/src/main/java/io/sentry/SentryOptions.java
markushi Nov 30, 2022
3dfe721
Format code
getsentry-bot Nov 30, 2022
409d152
Enable UserInteractionIntegration according to settings
markushi Dec 1, 2022
50a302d
Merge branch 'feat/compose-ui-transactions' of github.com:getsentry/s…
markushi Dec 1, 2022
7267040
Re-structure code, use reflection for compose click/scroll transactions
markushi Dec 5, 2022
27ba08a
Add UI tests for Jetpack Compose user interaction breadcrumbs
markushi Dec 7, 2022
f569e29
Merge branch 'main' of github.com:getsentry/sentry-java into feat/com…
markushi Dec 7, 2022
2ffbc8b
Remove obsolete dependencies
markushi Dec 7, 2022
3d8b833
Add changelog entry
markushi Dec 7, 2022
a979bf3
Update sentry-android-core/src/main/java/io/sentry/android/core/inter…
markushi Dec 9, 2022
ed79435
Refactor based on PR comments
markushi Dec 9, 2022
c106ca2
Merge branch 'feat/compose-ui-transactions' of github.com:getsentry/s…
markushi Dec 9, 2022
44c7096
Adapts code to PR comments
markushi Dec 9, 2022
8438cb4
Port ComposeGestureTargetLocator back to Java
markushi Dec 12, 2022
6cead69
Integrate PR comments
markushi Dec 13, 2022
9f4600e
Add README to sentry-compose-helper module
markushi Dec 13, 2022
306bd0a
Merge branch 'main' into feat/compose-ui-transactions
markushi Dec 13, 2022
50f6792
Fix failing tests
markushi Dec 13, 2022
1c582d8
Format code
getsentry-bot Dec 13, 2022
c1753c0
Fix missing code formatting
markushi Dec 13, 2022
f846c6c
Merge branch 'feat/compose-ui-transactions' of github.com:getsentry/s…
markushi Dec 13, 2022
14fddbd
Add potential fix for saucelabs ui tests
markushi Dec 14, 2022
534cda5
Fix unused imports
markushi Dec 14, 2022
47bc669
Merge branch 'main' into feat/compose-ui-transactions
markushi Dec 14, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -146,7 +146,7 @@ static void initializeIntegrationsAndProcessors(
final List<GestureTargetLocator> gestureTargetLocators = new ArrayList<>(2);
gestureTargetLocators.add(new AndroidViewGestureTargetLocator(isAndroidXScrollViewAvailable));
if (isComposeGestureTargetLocatorAvailable) {
gestureTargetLocators.add(new ComposeGestureTargetLocator(options));
gestureTargetLocators.add(new ComposeGestureTargetLocator());
}
options.setGestureTargetLocators(gestureTargetLocators);
}
Expand Down
Expand Up @@ -5,10 +5,7 @@
import io.sentry.Sentry;
import io.sentry.SentryOptions;
import io.sentry.SpanStatus;
import io.sentry.internal.gestures.GestureTargetLocator;
import io.sentry.protocol.SdkVersion;
import java.util.ArrayList;
import java.util.List;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.TestOnly;
Expand Down
Expand Up @@ -20,45 +20,14 @@ public AndroidViewGestureTargetLocator(final boolean isAndroidXAvailable) {
this.isAndroidXAvailable = isAndroidXAvailable;
}

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

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());
}

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

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

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

@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, coordinates)) {
if (touchWithinBounds(view, x, y)) {
if (targetType == UiElement.Type.CLICKABLE && isViewTappable(view)) {
return createUiElement(view);
} else if (targetType == UiElement.Type.SCROLLABLE
Expand All @@ -69,12 +38,43 @@ && isViewScrollable(view, isAndroidXAvailable)) {
return null;
}

private UiElement createUiElement(@NotNull View targetView) {
private UiElement createUiElement(final @NotNull View targetView) {
final String resourceName = ViewUtils.getResourceIdWithFallback(targetView);
@Nullable String className = targetView.getClass().getCanonicalName();
if (className == null) {
className = targetView.getClass().getSimpleName();
}
return new UiElement(targetView, className, resourceName, 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());
}
}
Expand Up @@ -47,8 +47,8 @@ final class ViewUtils {
}
}

for (GestureTargetLocator provider : options.getGestureTargetLocators()) {
final @Nullable UiElement newTarget = provider.locate(view, x, y, targetType);
for (GestureTargetLocator locator : options.getGestureTargetLocators()) {
final @Nullable UiElement newTarget = locator.locate(view, x, y, targetType);
if (newTarget != null) {
if (targetType == UiElement.Type.CLICKABLE) {
target = newTarget;
Expand Down
40 changes: 40 additions & 0 deletions sentry-compose-helper/build.gradle.kts
@@ -0,0 +1,40 @@
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
`java-library`
kotlin("jvm")
markushi marked this conversation as resolved.
Show resolved Hide resolved
jacoco
id("org.jetbrains.compose")
id(Config.QualityPlugins.gradleVersions)
id(Config.BuildPlugins.buildConfig) version Config.BuildPlugins.buildConfigVersion
}

configure<JavaPluginExtension> {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}

tasks.withType<KotlinCompile>().configureEach {
kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8.toString()
}

dependencies {
api(projects.sentry)
api(compose.runtime)
markushi marked this conversation as resolved.
Show resolved Hide resolved
api(compose.ui)
}

configure<SourceSetContainer> {
test {
java.srcDir("src/test/java")
}
}

val embeddedJar by configurations.creating {
isCanBeConsumed = true
isCanBeResolved = false
}

artifacts {
add("embeddedJar", File("$buildDir/libs/sentry-compose-helper-$version.jar"))
}
@@ -0,0 +1,106 @@
package io.sentry.compose.gestures;

import androidx.compose.ui.layout.LayoutCoordinatesKt;
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.internal.gestures.GestureTargetLocator;
import io.sentry.internal.gestures.UiElement;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

@SuppressWarnings("KotlinInternalInJava")
public final class ComposeGestureTargetLocator implements GestureTargetLocator {

@Override
public @Nullable UiElement locate(
@NotNull Object root, float x, float y, UiElement.Type targetType) {
@Nullable String targetTag = null;

if (!(root instanceof Owner)) {
return null;
}

final @NotNull Queue<LayoutNode> queue = new LinkedList<>();
queue.add(((Owner) root).getRoot());

while (!queue.isEmpty()) {
final @Nullable LayoutNode node = queue.poll();
if (node == null) {
continue;
}

final boolean isPlaced = node.isPlaced();
final boolean inBounds = layoutNodeBoundsContain(node, x, y);

if (node.isPlaced() && layoutNodeBoundsContain(node, x, y)) {
markushi marked this conversation as resolved.
Show resolved Hide resolved
boolean isClickable = false;
boolean isScrollable = false;
@Nullable String testTag = null;

final List<ModifierInfo> modifiers = node.getModifierInfo();
for (ModifierInfo modifierInfo : modifiers) {
if (modifierInfo.getModifier() instanceof SemanticsModifier) {
final SemanticsModifier semanticsModifierCore =
(SemanticsModifier) modifierInfo.getModifier();
final SemanticsConfiguration semanticsConfiguration =
semanticsModifierCore.getSemanticsConfiguration();
for (Map.Entry<? extends SemanticsPropertyKey<?>, ?> entry : semanticsConfiguration) {
final @Nullable String key = entry.getKey().getName();
markushi marked this conversation as resolved.
Show resolved Hide resolved
switch (key) {
case "ScrollBy":
isScrollable = true;
break;
case "OnClick":
isClickable = true;
break;
case "TestTag":
if (entry.getValue() instanceof String) {
testTag = (String) entry.getValue();
}
break;
}
}
}
}

if (isClickable && targetType == UiElement.Type.CLICKABLE) {
targetTag = testTag;
}
if (isScrollable && targetType == UiElement.Type.SCROLLABLE) {
targetTag = testTag;
// skip any children for scrollable targets
break;
}
}
queue.addAll(node.getZSortedChildren().asMutableList());
}

if (targetTag == null) {
return null;
} else {
return new UiElement(null, null, null, targetTag);
}
}

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));

return x >= nodeX && x <= (nodeX + nodeWidth) && y >= nodeY && y <= (nodeY + nodeHeight);
}
}
22 changes: 22 additions & 0 deletions sentry-compose/build.gradle.kts
@@ -1,3 +1,4 @@
import com.android.build.gradle.internal.tasks.LibraryAarJarsTask
import io.gitlab.arturbosch.detekt.Detekt
import org.jetbrains.dokka.gradle.DokkaTask

Expand Down Expand Up @@ -46,6 +47,7 @@ kotlin {
dependencies {
api(projects.sentry)
implementation(Config.Libs.kotlinStdLib)
api(projects.sentryComposeHelper)
}
}

Expand Down Expand Up @@ -137,3 +139,23 @@ tasks.withType<DokkaTask>().configureEach {
}
}
}

/**
* Due to https://youtrack.jetbrains.com/issue/KT-30878
* you can not java sources in a KMP-enabled project which has the android-lib plugin applied.
* Thus we compile relevant java code in sentry-compose-helper first and embed it in here.
*/
val embedComposeHelperConfig by configurations.creating {
isCanBeConsumed = false
isCanBeResolved = true
}

dependencies {
embedComposeHelperConfig(
project(":" + projects.sentryComposeHelper.name, "embeddedJar")
)
}

tasks.withType<LibraryAarJarsTask> {
mainScopeClassFiles.setFrom(embedComposeHelperConfig)
}