Skip to content

Commit a9dadc4

Browse files
authored
Merge branch 'feat/7.0.0' into feat/get-span-root-span
2 parents 909262a + 55b103c commit a9dadc4

File tree

5 files changed

+95
-36
lines changed

5 files changed

+95
-36
lines changed

CHANGELOG.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,10 @@ Breaking changes:
1212
- Reduce timeout of AsyncHttpTransport to avoid ANR ([#2879](https://github.com/getsentry/sentry-java/pull/2879))
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
15-
- If global hub mode is enabled (default on Android), Sentry.getSpan() returns the root span instead of the latest span ([#2855](https://github.com/getsentry/sentry-java/pull/2855))
15+
- 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
18+
- Android: If global hub mode is enabled (default on Android), Sentry.getSpan() returns the root span instead of the latest span ([#2855](https://github.com/getsentry/sentry-java/pull/2855))
1619

1720
### Fixes
1821

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

sentry-apollo/src/main/java/io/sentry/apollo/SentryApolloInterceptor.kt

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -142,15 +142,20 @@ class SentryApolloInterceptor(
142142
}
143143

144144
private fun finish(span: ISpan, request: InterceptorRequest, response: InterceptorResponse? = null) {
145-
var newSpan: ISpan = span
145+
var newSpan: ISpan? = span
146146
if (beforeSpan != null) {
147147
try {
148148
newSpan = beforeSpan.execute(span, request, response)
149149
} catch (e: Exception) {
150150
hub.options.logger.log(SentryLevel.ERROR, "An error occurred while executing beforeSpan on ApolloInterceptor", e)
151151
}
152152
}
153-
newSpan.finish()
153+
if (newSpan == null) {
154+
// span is dropped
155+
span.spanContext.sampled = false
156+
} else {
157+
span.finish()
158+
}
154159

155160
response?.let {
156161
if (it.httpResponse.isPresent) {
@@ -166,9 +171,9 @@ class SentryApolloInterceptor(
166171
breadcrumb.setData("response_body_size", contentLength)
167172
}
168173

169-
val hint = Hint().also {
170-
it.set(APOLLO_REQUEST, httpRequest)
171-
it.set(APOLLO_RESPONSE, httpResponse)
174+
val hint = Hint().apply {
175+
set(APOLLO_REQUEST, httpRequest)
176+
set(APOLLO_RESPONSE, httpResponse)
172177
}
173178
hub.addBreadcrumb(breadcrumb, hint)
174179
}
@@ -192,6 +197,6 @@ class SentryApolloInterceptor(
192197
* @param request the HTTP request executed by okHttp
193198
* @param response the HTTP response received by okHttp
194199
*/
195-
fun execute(span: ISpan, request: InterceptorRequest, response: InterceptorResponse?): ISpan
200+
fun execute(span: ISpan, request: InterceptorRequest, response: InterceptorResponse?): ISpan?
196201
}
197202
}

sentry-apollo/src/test/java/io/sentry/apollo/SentryApolloInterceptorTest.kt

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import kotlin.test.Test
3535
import kotlin.test.assertEquals
3636
import kotlin.test.assertNotNull
3737
import kotlin.test.assertNull
38+
import kotlin.test.assertTrue
3839

3940
class SentryApolloInterceptorTest {
4041

@@ -177,6 +178,24 @@ class SentryApolloInterceptorTest {
177178
)
178179
}
179180

181+
@Test
182+
fun `when beforeSpan callback returns null, span is dropped`() {
183+
executeQuery(
184+
fixture.getSut { _, _, _ ->
185+
null
186+
}
187+
)
188+
189+
verify(fixture.hub).captureTransaction(
190+
check {
191+
assertTrue(it.spans.isEmpty())
192+
},
193+
anyOrNull<TraceContext>(),
194+
anyOrNull(),
195+
anyOrNull()
196+
)
197+
}
198+
180199
@Test
181200
fun `when customizer throws, exception is handled`() {
182201
executeQuery(

0 commit comments

Comments
 (0)