feat(core): Add rage tap detection with ui.frustration breadcrumbs#5992
feat(core): Add rage tap detection with ui.frustration breadcrumbs#5992
Conversation
Detect rapid consecutive taps on the same UI element and surface them as frustration signals across the SDK: - New RageTapDetector class tracks recent taps in a circular buffer and matches them by component identity (label or name+file). When N taps on the same target occur within a configurable time window, a ui.frustration breadcrumb is emitted automatically. - TouchEventBoundary gains three new props: enableRageTapDetection (default: true), rageTapThreshold (default: 3), and rageTapTimeWindow (default: 1000ms). - Native replay breadcrumb converters on both Android (Java) and iOS (Objective-C) now handle the ui.frustration category, converting it to an RRWeb breadcrumb event so rage taps appear on the session replay timeline with the same touch-path message format as regular ui.tap events. - 7 new JS tests cover detection, threshold configuration, time window expiry, buffer reset, disabled mode, and component-name fallback. Android and iOS converter tests verify the new category is handled correctly.
Semver Impact of This PR⚪ None (no version bump detected) 📋 Changelog PreviewThis is how your changes will appear in the changelog.
Plus 14 more 🤖 This preview updates automatically when you update the PR. |
|
4cfa1af to
7d06010
Compare
- New ragetap.test.ts with 10 unit tests for RageTapDetector: threshold detection, different targets, time window expiry, buffer reset, disabled mode, custom threshold/timeWindow, component name+file identity, empty path, and consecutive rage tap triggers. - 3 integration tests in touchevents.test.tsx verifying TouchEventBoundary wires the detector correctly: end-to-end detection, disabled prop, and custom threshold/timeWindow props. - Android converter test (Kotlin) and iOS converter test (Swift) for the ui.frustration breadcrumb category in RNSentryReplayBreadcrumbConverter.
|
@cursor review |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 3 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit a34b7f2. Configure here.
| if ("touch".equals(breadcrumb.getCategory())) { | ||
| return convertTouchBreadcrumb(breadcrumb); | ||
| } | ||
| if ("ui.frustration".equals(breadcrumb.getCategory())) { |
There was a problem hiding this comment.
I think we should either align with the ui.multiClick naming from JS (which probably doesn't make sense on mobile) or update the backend to handle the new category name.
As is the frustration will probably appear as a generic breadcrumb on the replay timeline but without the special rage click treatment (fire icon, "Rage Click" label, click count display).
Looping in @romtsn who has an overview of all the mobile replay implementation for more context 🙇
Also linking the related docs I could find:
- Rage Click Issues (product docs): [Rage Click Issues]
- Replay Issue Types (JavaScript SDK docs): [Replay Issues]
- Intro blog post on Rage & Dead Clicks: [Rage & Dead Clicks]
- Fix false-positive detection: reset tap buffer when target changes instead of relying on time-window pruning, which could make non-consecutive taps appear consecutive after interleaved taps aged out (Medium severity, reported by Sentry bugbot). - Add null check for breadcrumb data in Android convertFrustrationBreadcrumb, matching the iOS implementation that already guards against nil data (Low severity). - Remove hardcoded MAX_RECENT_TAPS buffer limit that would silently break detection for thresholds > 10. The buffer is now naturally bounded by target-change resets and time-window pruning. - Deduplicate TouchedComponentInfo: export from ragetap.ts and import in touchevents.tsx instead of maintaining identical interfaces in both files. - Read rage tap props at event time via updateOptions() instead of freezing them in the constructor, consistent with how all other TouchEventBoundary props are consumed.
| function getTapIdentity(root: TouchedComponentInfo, label?: string): string { | ||
| if (label) { | ||
| return `label:${label}`; | ||
| } | ||
| return `name:${root.name ?? ''}|file:${root.file ?? ''}`; | ||
| } |
There was a problem hiding this comment.
Bug: Rage tap detection generates false positives when distinct child elements share a labeled ancestor, as the tap identity is based on the parent's label, not the actual element.
Severity: MEDIUM
Suggested Fix
Modify getTapIdentity to generate a more specific identity when a label is present. The identity should incorporate both the label and unique properties of the tapped element, such as its name and file. For example, return label:${label}|name:${root.name ?? ''}|file:${root.file ?? ''} to distinguish between different children under the same labeled parent.
Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.
Location: packages/core/src/js/ragetap.ts#L121-L126
Potential issue: The rage tap detection logic in `getTapIdentity` prioritizes a shared
parent label over the specific element being tapped. When multiple distinct child
elements are nested within a single parent container that has a `sentry-label`, taps on
any of these different children will generate the same tap identity (`label:${label}`).
This causes the system to incorrectly register a rage tap when a user is interacting
with different controls in quick succession, leading to false positive frustration
signals in analytics and replays.

📢 Type of change
📜 Description
Detects rage taps (rapid consecutive taps on the same UI element) and surfaces them as first-class frustration signals across the SDK.
Design decisions
TouchEventBoundary.ui.frustrationbreadcrumbs.📝 Checklist
sendDefaultPIIis enabled