Skip to content

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

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

feat(core): Add rage tap detection with ui.frustration breadcrumbs#5992
alwx wants to merge 9 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
  • feat(core): Add GlobalErrorBoundary for non-rendering errors by alwx in #6023
  • chore(deps): update CLI to v3.4.0 by github-actions in #6026
  • feat: Expose screenshot masking options for error screenshots by antonis in #6007
  • fix(replay): Check captureReplay return value in iOS bridge by antonis in #6008
  • chore(deps): bump getsentry/craft from 2.25.2 to 2.25.4 by dependabot in #6019
  • chore(deps): bump getsentry/craft/.github/workflows/changelog-preview.yml from 2.25.2 to 2.25.4 by dependabot in #6021
  • chore(deps): bump github/codeql-action from 4.35.1 to 4.35.2 by dependabot in #6022
  • chore(deps): bump actions/setup-node from 6.3.0 to 6.4.0 by dependabot in #6020
  • ci(danger): Demote Android SDK version mismatch from fail to warn by antonis in #6018
  • chore(deps): update Android SDK to v8.39.1 by github-actions in #6010
  • chore(deps): update JavaScript SDK to v10.49.0 by github-actions in #6011
  • ci: Integrate Warden for AI-powered PR code review by antonis in #6003
  • chore(lint): Fixes lint issue on main by antonis in #6013
  • feat(expo): Warn when prebuilt native projects are missing Sentry config by alwx in #5984

🤖 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 7cbafd3

@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:

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@romtsn do you have any thoughts on this?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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.multiClick across all files (JS, Android, iOS, tests, changelog, etc)
  • breadcrumb data reshaped to match web SDK: clickCount, metric: true, route, node with DOM-compatible shape

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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.
Comment thread packages/core/src/js/ragetap.ts
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.
@alwx alwx requested review from antonis and romtsn April 20, 2026 12:22
…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).
Comment thread packages/core/ios/RNSentryReplayBreadcrumbConverter.m Outdated
Comment thread packages/core/test/touchevents.test.tsx
Comment thread packages/core/src/js/ragetap.ts
- 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).
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.

3 participants