Skip to content

Commit 55b103c

Browse files
authored
Start a new automatic transaction on every click (#2891)
1 parent ee21d53 commit 55b103c

File tree

3 files changed

+63
-29
lines changed

3 files changed

+63
-29
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ Breaking changes:
1313
- Add deadline timeout for automatic transactions ([#2865](https://github.com/getsentry/sentry-java/pull/2865))
1414
- This affects all automatically generated transactions on Android (UI, clicks), the default timeout is 30s
1515
- Apollo v2 BeforeSpanCallback now allows returning null ([#2890](https://github.com/getsentry/sentry-java/pull/2890))
16+
- Automatic user interaction tracking: every click now starts a new automatic transaction ([#2891](https://github.com/getsentry/sentry-java/pull/2891))
17+
- Previously performing a click on the same UI widget twice would keep the existing transaction running, the new behavior now better aligns with other SDKs
1618

1719
### Fixes
1820

sentry-android-core/src/main/java/io/sentry/android/core/internal/gestures/SentryGestureListener.java

Lines changed: 56 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,13 @@
3232
@ApiStatus.Internal
3333
public final class SentryGestureListener implements GestureDetector.OnGestureListener {
3434

35+
private enum GestureType {
36+
Click,
37+
Scroll,
38+
Swipe,
39+
Unknown
40+
}
41+
3542
static final String UI_ACTION = "ui.action";
3643
private static final String TRACE_ORIGIN = "auto.ui.gesture_listener";
3744

@@ -41,7 +48,7 @@ public final class SentryGestureListener implements GestureDetector.OnGestureLis
4148

4249
private @Nullable UiElement activeUiElement = null;
4350
private @Nullable ITransaction activeTransaction = null;
44-
private @Nullable String activeEventType = null;
51+
private @NotNull GestureType activeEventType = GestureType.Unknown;
4552

4653
private final ScrollState scrollState = new ScrollState();
4754

@@ -61,7 +68,7 @@ public void onUp(final @NotNull MotionEvent motionEvent) {
6168
return;
6269
}
6370

64-
if (scrollState.type == null) {
71+
if (scrollState.type == GestureType.Unknown) {
6572
options
6673
.getLogger()
6774
.log(SentryLevel.DEBUG, "Unable to define scroll type. No breadcrumb captured.");
@@ -107,8 +114,8 @@ public boolean onSingleTapUp(final @Nullable MotionEvent motionEvent) {
107114
return false;
108115
}
109116

110-
addBreadcrumb(target, "click", Collections.emptyMap(), motionEvent);
111-
startTracing(target, "click");
117+
addBreadcrumb(target, GestureType.Click, Collections.emptyMap(), motionEvent);
118+
startTracing(target, GestureType.Click);
112119
return false;
113120
}
114121

@@ -123,7 +130,7 @@ public boolean onScroll(
123130
return false;
124131
}
125132

126-
if (scrollState.type == null) {
133+
if (scrollState.type == GestureType.Unknown) {
127134
final @Nullable UiElement target =
128135
ViewUtils.findTarget(
129136
options, decorView, firstEvent.getX(), firstEvent.getY(), UiElement.Type.SCROLLABLE);
@@ -140,7 +147,7 @@ public boolean onScroll(
140147
}
141148

142149
scrollState.setTarget(target);
143-
scrollState.type = "scroll";
150+
scrollState.type = GestureType.Scroll;
144151
}
145152
return false;
146153
}
@@ -151,7 +158,7 @@ public boolean onFling(
151158
final @Nullable MotionEvent motionEvent1,
152159
final float v,
153160
final float v1) {
154-
scrollState.type = "swipe";
161+
scrollState.type = GestureType.Swipe;
155162
return false;
156163
}
157164

@@ -164,32 +171,37 @@ public void onLongPress(MotionEvent motionEvent) {}
164171
// region utils
165172
private void addBreadcrumb(
166173
final @NotNull UiElement target,
167-
final @NotNull String eventType,
174+
final @NotNull GestureType eventType,
168175
final @NotNull Map<String, Object> additionalData,
169176
final @NotNull MotionEvent motionEvent) {
170177

171178
if (!options.isEnableUserInteractionBreadcrumbs()) {
172179
return;
173180
}
174181

182+
final String type = getGestureType(eventType);
183+
175184
final Hint hint = new Hint();
176185
hint.set(ANDROID_MOTION_EVENT, motionEvent);
177186
hint.set(ANDROID_VIEW, target.getView());
178187

179188
hub.addBreadcrumb(
180189
Breadcrumb.userInteraction(
181-
eventType,
182-
target.getResourceName(),
183-
target.getClassName(),
184-
target.getTag(),
185-
additionalData),
190+
type, target.getResourceName(), target.getClassName(), target.getTag(), additionalData),
186191
hint);
187192
}
188193

189-
private void startTracing(final @NotNull UiElement target, final @NotNull String eventType) {
190-
final UiElement uiElement = activeUiElement;
194+
private void startTracing(final @NotNull UiElement target, final @NotNull GestureType eventType) {
195+
196+
final boolean isNewGestureSameAsActive =
197+
(eventType == activeEventType && target.equals(activeUiElement));
198+
final boolean isClickGesture = eventType == GestureType.Click;
199+
// we always want to start new transaction/traces for clicks, for swipe/scroll only if the
200+
// target changed
201+
final boolean isNewInteraction = isClickGesture || !isNewGestureSameAsActive;
202+
191203
if (!(options.isTracingEnabled() && options.isEnableUserInteractionTracing())) {
192-
if (!(target.equals(uiElement) && eventType.equals(activeEventType))) {
204+
if (isNewInteraction) {
193205
TracingUtils.startNewTrace(hub);
194206
activeUiElement = target;
195207
activeEventType = eventType;
@@ -206,9 +218,7 @@ private void startTracing(final @NotNull UiElement target, final @NotNull String
206218
final @Nullable String viewIdentifier = target.getIdentifier();
207219

208220
if (activeTransaction != null) {
209-
if (target.equals(uiElement)
210-
&& eventType.equals(activeEventType)
211-
&& !activeTransaction.isFinished()) {
221+
if (!isNewInteraction && !activeTransaction.isFinished()) {
212222
options
213223
.getLogger()
214224
.log(
@@ -233,7 +243,7 @@ private void startTracing(final @NotNull UiElement target, final @NotNull String
233243

234244
// we can only bind to the scope if there's no running transaction
235245
final String name = getActivityName(activity) + "." + viewIdentifier;
236-
final String op = UI_ACTION + "." + eventType;
246+
final String op = UI_ACTION + "." + getGestureType(eventType);
237247

238248
final TransactionOptions transactionOptions = new TransactionOptions();
239249
transactionOptions.setWaitForChildren(true);
@@ -270,13 +280,15 @@ void stopTracing(final @NotNull SpanStatus status) {
270280
}
271281
hub.configureScope(
272282
scope -> {
283+
// avoid method refs on Android due to some issues with older AGP setups
284+
// noinspection Convert2MethodRef
273285
clearScope(scope);
274286
});
275287
activeTransaction = null;
276288
if (activeUiElement != null) {
277289
activeUiElement = null;
278290
}
279-
activeEventType = null;
291+
activeEventType = GestureType.Unknown;
280292
}
281293

282294
@VisibleForTesting
@@ -337,11 +349,32 @@ void applyScope(final @NotNull Scope scope, final @NotNull ITransaction transact
337349
}
338350
return decorView;
339351
}
352+
353+
@NotNull
354+
private static String getGestureType(final @NotNull GestureType eventType) {
355+
final @NotNull String type;
356+
switch (eventType) {
357+
case Click:
358+
type = "click";
359+
break;
360+
case Scroll:
361+
type = "scroll";
362+
break;
363+
case Swipe:
364+
type = "swipe";
365+
break;
366+
default:
367+
case Unknown:
368+
type = "unknown";
369+
break;
370+
}
371+
return type;
372+
}
340373
// endregion
341374

342375
// region scroll logic
343376
private static final class ScrollState {
344-
private @Nullable String type = null;
377+
private @NotNull GestureType type = GestureType.Unknown;
345378
private @Nullable UiElement target;
346379
private float startX = 0f;
347380
private float startY = 0f;
@@ -378,7 +411,7 @@ private void setTarget(final @NotNull UiElement target) {
378411

379412
private void reset() {
380413
target = null;
381-
type = null;
414+
type = GestureType.Unknown;
382415
startX = 0f;
383416
startY = 0f;
384417
}

sentry-android-core/src/test/java/io/sentry/android/core/internal/gestures/SentryGestureListenerTracingTest.kt

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import org.mockito.kotlin.clearInvocations
2828
import org.mockito.kotlin.doAnswer
2929
import org.mockito.kotlin.mock
3030
import org.mockito.kotlin.never
31+
import org.mockito.kotlin.times
3132
import org.mockito.kotlin.verify
3233
import org.mockito.kotlin.whenever
3334
import kotlin.test.Test
@@ -333,20 +334,18 @@ class SentryGestureListenerTracingTest {
333334
SpanContext(SentryId.EMPTY_ID, SpanId.EMPTY_ID, "op", null, null)
334335
)
335336

337+
// when the same button is clicked twice
338+
sut.onSingleTapUp(fixture.event)
336339
sut.onSingleTapUp(fixture.event)
337340

338-
verify(fixture.hub).startTransaction(
341+
// then two transaction should be captured
342+
verify(fixture.hub, times(2)).startTransaction(
339343
check {
340344
assertEquals("Activity.test_button", it.name)
341345
assertEquals(TransactionNameSource.COMPONENT, it.transactionNameSource)
342346
},
343347
any<TransactionOptions>()
344348
)
345-
346-
// second view interaction
347-
sut.onSingleTapUp(fixture.event)
348-
349-
verify(fixture.transaction).scheduleFinish()
350349
}
351350

352351
@Test

0 commit comments

Comments
 (0)