You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
201 lines
5.3 KiB
Dart
201 lines
5.3 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:clarity_flutter/clarity_flutter.dart';
|
|
|
|
/// A wrapper widget that tracks user interactions with Clarity for UX frustration analysis.
|
|
/// Helps identify rage clicks, dead clicks, and rapid scrolls.
|
|
///
|
|
/// Usage:
|
|
/// ```dart
|
|
/// ClarityInteractionTracker(
|
|
/// semanticLabel: 'Login Button',
|
|
/// interactionType: ClarityInteractionType.button,
|
|
/// onTap: () { /* your action */ },
|
|
/// child: YourWidget(),
|
|
/// )
|
|
/// ```
|
|
class ClarityInteractionTracker extends StatelessWidget {
|
|
final Widget child;
|
|
final String semanticLabel;
|
|
final VoidCallback? onTap;
|
|
final ClarityInteractionType interactionType;
|
|
final Map<String, String>? customTags;
|
|
final bool enabled;
|
|
|
|
const ClarityInteractionTracker({
|
|
super.key,
|
|
required this.child,
|
|
required this.semanticLabel,
|
|
this.onTap,
|
|
this.interactionType = ClarityInteractionType.button,
|
|
this.customTags,
|
|
this.enabled = true,
|
|
});
|
|
|
|
void _logInteraction(BuildContext context) {
|
|
if (!enabled) return;
|
|
|
|
try {
|
|
// Set common tags
|
|
Clarity.setCustomTag('interaction_type', interactionType.name);
|
|
Clarity.setCustomTag('interaction_label',
|
|
semanticLabel.length > 100 ? semanticLabel.substring(0, 100) : semanticLabel
|
|
);
|
|
Clarity.setCustomTag('interaction_enabled', enabled.toString());
|
|
|
|
// Set route/screen name if available
|
|
final route = ModalRoute.of(context);
|
|
if (route != null && route.settings.name != null) {
|
|
Clarity.setCustomTag('screen_route', route.settings.name!);
|
|
}
|
|
|
|
// Add custom tags if provided
|
|
if (customTags != null) {
|
|
customTags!.forEach((key, value) {
|
|
// Ensure key and value fit Clarity's 255 char limit
|
|
final truncatedValue = value.length > 250 ? value.substring(0, 250) : value;
|
|
Clarity.setCustomTag(key, truncatedValue);
|
|
});
|
|
}
|
|
|
|
// Send event to Clarity
|
|
Clarity.sendCustomEvent('user_interaction_${interactionType.name}');
|
|
} catch (e) {
|
|
// Silence Clarity errors to not interrupt user flow
|
|
debugPrint('Clarity interaction tracking error: $e');
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Semantics(
|
|
label: semanticLabel,
|
|
button: interactionType == ClarityInteractionType.button,
|
|
enabled: enabled,
|
|
excludeSemantics: false,
|
|
child: GestureDetector(
|
|
behavior: HitTestBehavior.opaque,
|
|
onTap: enabled && onTap != null
|
|
? () {
|
|
_logInteraction(context);
|
|
onTap?.call();
|
|
}
|
|
: null,
|
|
child: child,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Types of interactions that can be tracked
|
|
enum ClarityInteractionType {
|
|
button,
|
|
link,
|
|
card,
|
|
listItem,
|
|
icon,
|
|
image,
|
|
input,
|
|
custom,
|
|
}
|
|
|
|
/// Extension to easily wrap any widget with Clarity tracking
|
|
extension ClarityTrackingExtension on Widget {
|
|
/// Wraps this widget with Clarity interaction tracking
|
|
Widget trackWithClarity({
|
|
required String label,
|
|
VoidCallback? onTap,
|
|
ClarityInteractionType type = ClarityInteractionType.custom,
|
|
Map<String, String>? customTags,
|
|
bool enabled = true,
|
|
}) {
|
|
return ClarityInteractionTracker(
|
|
semanticLabel: label,
|
|
onTap: onTap,
|
|
interactionType: type,
|
|
customTags: customTags,
|
|
enabled: enabled,
|
|
child: this,
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Wrapper for ScrollView to track rapid scrolling behavior
|
|
class ClarityScrollTracker extends StatefulWidget {
|
|
final Widget child;
|
|
final String scrollViewLabel;
|
|
final ScrollController? controller;
|
|
|
|
const ClarityScrollTracker({
|
|
super.key,
|
|
required this.child,
|
|
required this.scrollViewLabel,
|
|
this.controller,
|
|
});
|
|
|
|
@override
|
|
State<ClarityScrollTracker> createState() => _ClarityScrollTrackerState();
|
|
}
|
|
|
|
class _ClarityScrollTrackerState extends State<ClarityScrollTracker> {
|
|
late ScrollController _controller;
|
|
bool _isRapidScrolling = false;
|
|
DateTime? _lastScrollTime;
|
|
int _rapidScrollCount = 0;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_controller = widget.controller ?? ScrollController();
|
|
_controller.addListener(_onScroll);
|
|
}
|
|
|
|
void _onScroll() {
|
|
final now = DateTime.now();
|
|
|
|
// Detect rapid scrolling (scroll events within 100ms)
|
|
if (_lastScrollTime != null) {
|
|
final timeDiff = now.difference(_lastScrollTime!).inMilliseconds;
|
|
|
|
if (timeDiff < 100) {
|
|
_rapidScrollCount++;
|
|
|
|
if (!_isRapidScrolling && _rapidScrollCount > 5) {
|
|
_isRapidScrolling = true;
|
|
_logRapidScroll();
|
|
}
|
|
} else if (timeDiff > 500) {
|
|
// Reset if scrolling slows down
|
|
_isRapidScrolling = false;
|
|
_rapidScrollCount = 0;
|
|
}
|
|
}
|
|
|
|
_lastScrollTime = now;
|
|
}
|
|
|
|
void _logRapidScroll() {
|
|
try {
|
|
Clarity.setCustomTag('scroll_type', 'rapid');
|
|
Clarity.setCustomTag('scroll_view', widget.scrollViewLabel);
|
|
Clarity.setCustomTag('scroll_position', _controller.position.pixels.toString());
|
|
Clarity.sendCustomEvent('rapid_scroll_detected');
|
|
} catch (e) {
|
|
debugPrint('Clarity scroll tracking error: $e');
|
|
}
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
if (widget.controller == null) {
|
|
_controller.dispose();
|
|
}
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return widget.child;
|
|
}
|
|
}
|
|
|