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.
🤖 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]
There was a problem hiding this comment.
@romtsn do you have any thoughts on this?
There was a problem hiding this comment.
Yes, I think we should follow already existing conventions (that is, ui.multiClick and ui.slowClickDetected) as well as the timeout thresholds and the entire breadcrumb structure from here. We could then only change the frontend part to say "tap" instead of "click", for example here: https://github.com/getsentry/sentry/blob/8c43a8eb55c4b134d12376102d9338fab3c23166/static/app/utils/replays/getFrameDetails.tsx#L143-L155
Perhaps, we'll also have to change how selectors are being stringified, but uncertain if it's necessary yet.
There was a problem hiding this comment.
After checking closely:
The stringifyNodeAttributes in the Sentry frontend already handles our node shape correctly: It already special-cases data-sentry-component meaning it uses it as the display name and hides it from the attribute list. So mobile components will show as SubmitButton[sentry-label="Submit"] on the replay timeline.
The tap -> click change you proposed is reasonable but purely cosmetic — will draft a PR to the main repo's frontend later on. Here is what I did for now:
ui.frustration->ui.multiClickacross all files (JS, Android, iOS, tests, changelog, etc)- breadcrumb data reshaped to match web SDK:
clickCount,metric: true, route, node with DOM-compatible shape
There was a problem hiding this comment.
Nice, I guess it's going to be a challenge to reshape the data from the native SDKs, but we could also adapt the frontend/backend part for it, if necessary
- 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.
Rename breadcrumb category from ui.frustration to ui.multiClick and reshape the data payload to match the web JS SDK's rage click format, so the Sentry replay timeline renders rage taps with the fire icon and 'Rage Click' label automatically. Changes to the breadcrumb shape: - category: ui.frustration → ui.multiClick - type: user → default - data.tapCount → data.clickCount - data.type (rage_tap) removed - data.metric: true added (marks as metric event) - data.route added (current screen from navigation tracing) - data.node added with DOM-compatible shape: tagName, textContent, attributes (data-sentry-component, data-sentry-source-file, sentry-label) — this allows the existing stringifyNodeAttributes in the Sentry frontend to render component names for mobile taps. Native replay converters updated on both Android and iOS to handle ui.multiClick instead of ui.frustration.
…sitives When distinct child elements share a labeled ancestor, the tap identity was based solely on the parent label, causing false rage tap detection when tapping different controls in quick succession. Now the identity always includes the root component name and file, even when a label is present (e.g. label:form|name:SubmitButton|file:form.tsx).
- iOS: Add NSArray type check on path data in convertMultiClick to prevent runtime crash from unrecognized selector on non-array values (HIGH, Sentry bot). - Clear tap buffer when detection is disabled via updateOptions to prevent stale taps from causing false positives on re-enable (LOW, Sentry bot). - Move changelog entry from released 8.8.0 section to Unreleased (danger bot). - Add time window integration test to touchevents that varies timestamps between taps, verifying rageTapTimeWindow actually excludes old taps (sentry-warden).

📢 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