Skip to content

feat(core): Add rage tap detection with ui.frustration breadcrumbs#5992

Open
alwx wants to merge 5 commits intomainfrom
feat/rage-tap-detection
Open

feat(core): Add rage tap detection with ui.frustration breadcrumbs#5992
alwx wants to merge 5 commits intomainfrom
feat/rage-tap-detection

Conversation

@alwx
Copy link
Copy Markdown
Contributor

@alwx alwx commented Apr 14, 2026

📢 Type of change

  • Bugfix
  • New feature
  • Enhancement
  • Refactoring

📜 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

  • Component identity over coordinates — taps are matched by label or component name + file rather than screen coordinates. More robust for shifting layouts and list items, and reuses data already captured by TouchEventBoundary.
  • Breadcrumb-first — emits ui.frustration breadcrumbs.

📝 Checklist

  • I added tests to verify changes
  • No new PII added or SDK only sends newly added PII if sendDefaultPII is enabled
  • I updated the docs if needed.
  • I updated the wizard if needed.
  • All tests passing
  • No breaking changes

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.
@alwx alwx self-assigned this Apr 14, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 14, 2026

Semver Impact of This PR

None (no version bump detected)

📋 Changelog Preview

This is how your changes will appear in the changelog.
Entries from this PR are highlighted with a left border (blockquote style).


  • feat(core): Add rage tap detection with ui.frustration breadcrumbs by alwx in #5992
  • fix(core): Fix sourcemap upload when withSentry is used programmatically by antonis in #6006
  • feat(tracing): Implement strict trace continuation by antonis in #5829
  • chore(deps): bump follow-redirects from 1.15.11 to 1.16.0 by dependabot in #6004
  • chore: Bump sample app to React Native 0.85.1 by antonis in #5991
  • chore(deps): Bump E2E tests to 0.85.1 by antonis in #5990
  • Add deeplinkIntegration for automatic deep link breadcrumbs by alwx in #5983
  • fix(core): Retry native module resolution to prevent silent event drops by antonis in #5981
  • feat(core): Name navigation spans using dispatched action payload by alwx in #5982
  • ci: Gate size analysis on ready-to-merge label for PRs by antonis in #5963
  • chore(deps): update Android SDK to v8.38.0 by github-actions in #5971
  • chore(deps): update Sentry Android Gradle Plugin to v6.4.0 by github-actions in #5974
  • chore(deps): update Cocoa SDK to v9.10.0 by github-actions in #5972
  • chore(deps): update JavaScript SDK to v10.48.0 by github-actions in #5975
  • chore(deps): bump actions/github-script from 8 to 9 by dependabot in #5980
  • chore(deps): bump actions/create-github-app-token from 3.0.0 to 3.1.1 by dependabot in #5979
  • chore(deps): update Bundler Plugins to v5.2.0 by github-actions in #5968
  • chore(deps): bump axios from 1.13.5 to 1.15.0 by dependabot in #5978
  • chore(deps): bump addressable from 2.8.7 to 2.9.0 in /performance-tests by dependabot in #5969
  • chore(deps): bump basic-ftp from 5.2.0 to 5.2.2 by dependabot in #5977
  • fix(profiling): Fix app start transaction profile timestamp offset by antonis in #5962
  • fix(android): Use componentStack as fallback for missing error stack traces by antonis in #5965
  • chore(deps): bump addressable from 2.8.7 to 2.9.0 in /samples/react-native-macos by dependabot in #5967
  • chore(deps): bump addressable from 2.8.7 to 2.9.0 in /samples/react-native by dependabot in #5966

Plus 14 more


🤖 This preview updates automatically when you update the PR.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 14, 2026

Fails
🚫 Pull request is not ready for merge, please add the "ready-to-merge" label to the pull request
Messages
📖 Do not forget to update Sentry-docs with your feature once the pull request gets approved.

Generated by 🚫 dangerJS against 65fe114

@alwx alwx changed the title feat(core): Add rage tap detection with ui.frustration breadcrumbs WIP: feat(core): Add rage tap detection with ui.frustration breadcrumbs Apr 14, 2026
@alwx alwx force-pushed the feat/rage-tap-detection branch from 4cfa1af to 7d06010 Compare April 15, 2026 12:51
- 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.
@alwx alwx marked this pull request as ready for review April 16, 2026 09:58
@alwx alwx changed the title WIP: feat(core): Add rage tap detection with ui.frustration breadcrumbs feat(core): Add rage tap detection with ui.frustration breadcrumbs Apr 16, 2026
@alwx
Copy link
Copy Markdown
Contributor Author

alwx commented Apr 16, 2026

@cursor review

Comment thread packages/core/src/js/ragetap.ts Outdated
Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 3 potential issues.

Fix All in Cursor

❌ 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.

Comment thread packages/core/src/js/ragetap.ts Outdated
Comment thread packages/core/src/js/ragetap.ts
Comment thread packages/core/src/js/touchevents.tsx Outdated
if ("touch".equals(breadcrumb.getCategory())) {
return convertTouchBreadcrumb(breadcrumb);
}
if ("ui.frustration".equals(breadcrumb.getCategory())) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

- 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.
Comment on lines +121 to +126
function getTapIdentity(root: TouchedComponentInfo, label?: string): string {
if (label) {
return `label:${label}`;
}
return `name:${root.name ?? ''}|file:${root.file ?? ''}`;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants