Skip to content

Commit d913348

Browse files
committed
feat: collect touch element path
1 parent 4daecc2 commit d913348

File tree

1 file changed

+105
-45
lines changed

1 file changed

+105
-45
lines changed

flutter/lib/src/user_interaction/sentry_user_interaction_widget.dart

+105-45
Original file line numberDiff line numberDiff line change
@@ -294,24 +294,50 @@ class _SentryUserInteractionWidgetState
294294
}
295295

296296
void _onPointerDown(PointerDownEvent event) {
297-
_lastPointerId = event.pointer;
298-
_lastPointerDownLocation = event.localPosition;
297+
try {
298+
_lastPointerId = event.pointer;
299+
_lastPointerDownLocation = event.localPosition;
300+
} catch (exception, stacktrace) {
301+
_options?.logger(
302+
SentryLevel.error,
303+
'Error while handling pointer-down event $event in $SentryUserInteractionWidget',
304+
exception: exception,
305+
stackTrace: stacktrace,
306+
);
307+
// ignore: invalid_use_of_internal_member
308+
if (_options?.automatedTestMode ?? false) {
309+
rethrow;
310+
}
311+
}
299312
}
300313

301314
void _onPointerUp(PointerUpEvent event) {
302-
// Figure out if something was tapped
303-
final location = _lastPointerDownLocation;
304-
if (location == null || event.pointer != _lastPointerId) {
305-
return;
306-
}
307-
final delta = Offset(
308-
location.dx - event.localPosition.dx,
309-
location.dy - event.localPosition.dy,
310-
);
315+
try {
316+
// Figure out if something was tapped
317+
final location = _lastPointerDownLocation;
318+
if (location == null || event.pointer != _lastPointerId) {
319+
return;
320+
}
321+
final delta = Offset(
322+
location.dx - event.localPosition.dx,
323+
location.dy - event.localPosition.dy,
324+
);
311325

312-
if (delta.distanceSquared < _tapDeltaArea) {
313-
// Widget was tapped
314-
_onTappedAt(event.localPosition);
326+
if (delta.distanceSquared < _tapDeltaArea) {
327+
// Widget was tapped
328+
_onTappedAt(event.localPosition);
329+
}
330+
} catch (exception, stacktrace) {
331+
_options?.logger(
332+
SentryLevel.error,
333+
'Error while handling pointer-up event $event in $SentryUserInteractionWidget',
334+
exception: exception,
335+
stackTrace: stacktrace,
336+
);
337+
// ignore: invalid_use_of_internal_member
338+
if (_options?.automatedTestMode ?? false) {
339+
rethrow;
340+
}
315341
}
316342
}
317343

@@ -331,11 +357,11 @@ class _SentryUserInteractionWidgetState
331357
return;
332358
}
333359

334-
Map<String, dynamic>? data = {};
335-
final description = _findDescriptionOf(info.element);
336-
if (description.isNotEmpty) {
337-
data['label'] = description;
338-
}
360+
final label = _getLabelRecursively(info.element);
361+
final data = {
362+
'path': _getTouchPath(info.element),
363+
if (label != null) 'label': label
364+
};
339365

340366
final crumb = Breadcrumb.userInteraction(
341367
subCategory: 'click',
@@ -347,6 +373,37 @@ class _SentryUserInteractionWidgetState
347373
_hub.addBreadcrumb(crumb, hint: hint);
348374
}
349375

376+
List<Map<String, String?>> _getTouchPath(Element element) {
377+
final path = <Map<String, String?>>[];
378+
379+
bool addToPath(Element element) {
380+
// Break at the boundary (i.e. this [SentryUserInteractionWidget]).
381+
if (element.widget == widget) {
382+
return false;
383+
}
384+
385+
final widgetName = element.widget.toStringShort();
386+
if (!widgetName.startsWith('_')) {
387+
final info = {
388+
'name': WidgetUtils.toStringValue(element.widget.key),
389+
'element': _getElementType(element) ?? widgetName,
390+
'label': _getLabel(element, true),
391+
}..removeWhere((key, value) => value == null);
392+
if (info.isNotEmpty) {
393+
path.add(info);
394+
}
395+
}
396+
397+
return path.length < 10;
398+
}
399+
400+
if (addToPath(element)) {
401+
element.visitAncestorElements(addToPath);
402+
}
403+
404+
return path;
405+
}
406+
350407
void _startTransactionOnTap(UserInteractionInfo info, String? widgetKey) {
351408
if (widgetKey == null ||
352409
!(_options?.isTracingEnabled() ?? false) ||
@@ -416,8 +473,31 @@ class _SentryUserInteractionWidgetState
416473
});
417474
}
418475

419-
String _findDescriptionOf(Element element) {
420-
var description = '';
476+
String? _getLabel(Element element, bool allowText) {
477+
String? label;
478+
479+
if (_options?.sendDefaultPii ?? false) {
480+
final widget = element.widget;
481+
if (allowText && widget is Text) {
482+
label = widget.data;
483+
} else if (widget is Semantics) {
484+
label = widget.properties.label;
485+
} else if (widget is Icon) {
486+
label = widget.semanticLabel;
487+
} else if (widget is Tooltip) {
488+
label = widget.message;
489+
}
490+
491+
if (label?.isEmpty ?? true) {
492+
label = null;
493+
}
494+
}
495+
496+
return label;
497+
}
498+
499+
String? _getLabelRecursively(Element element) {
500+
String? label;
421501

422502
if (_options?.sendDefaultPii ?? false) {
423503
final widget = element.widget;
@@ -427,36 +507,16 @@ class _SentryUserInteractionWidgetState
427507

428508
// traverse tree to find a suiting element
429509
void descriptionFinder(Element element) {
430-
bool foundDescription = false;
431-
432-
final widget = element.widget;
433-
if (allowText && widget is Text) {
434-
final data = widget.data;
435-
if (data != null && data.isNotEmpty) {
436-
description = data;
437-
foundDescription = true;
438-
}
439-
} else if (widget is Semantics) {
440-
if (widget.properties.label?.isNotEmpty ?? false) {
441-
description = widget.properties.label!;
442-
foundDescription = true;
443-
}
444-
} else if (widget is Icon) {
445-
if (widget.semanticLabel?.isNotEmpty ?? false) {
446-
description = widget.semanticLabel!;
447-
foundDescription = true;
448-
}
449-
}
450-
451-
if (!foundDescription) {
510+
label ??= _getLabel(element, allowText);
511+
if (label == null) {
452512
element.visitChildren(descriptionFinder);
453513
}
454514
}
455515

456-
element.visitChildren(descriptionFinder);
516+
descriptionFinder(element);
457517
}
458518

459-
return description;
519+
return label;
460520
}
461521

462522
UserInteractionInfo? _getElementAt(Offset position) {

0 commit comments

Comments
 (0)