Commit 9b6b2ba
authored
feat(core): Add rage tap detection with ui.frustration breadcrumbs (#5992)
* feat(core): Add rage tap detection with ui.frustration breadcrumbs
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.
* test(core): Add tests for rage tap detection and replay converters
- 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.
* docs(changelog): Add entry for rage tap detection (#5992)
* fix(core): Address review feedback for rage tap detection
- 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.
* refactor(core): Align rage tap detection with ui.multiClick convention
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.
* fix(core): Include component name in tap identity to prevent false positives
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).
* fix(core): Address latest review feedback
- 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).
* fix(core): Address antonis review feedback
- Drop level: 'warning' from ui.multiClick breadcrumb to match the
web JS SDK which defaults to info.
- Export DEFAULT_RAGE_TAP_THRESHOLD and DEFAULT_RAGE_TAP_TIME_WINDOW
from ragetap.ts and import them in touchevents.tsx defaultProps for
a single source of truth.
- Initialize RageTapDetector with props in the constructor and sync
via componentDidUpdate, instead of calling updateOptions on every
tap event.
- Remove incorrect @testonly annotation from Android
convertMultiClickBreadcrumb since it is called from convert() in
production code.
- Add comment explaining id: 0 placeholder in the node object (mobile
replays don't have rrweb node IDs).
- Add tests for updateOptions: buffer cleared on disable, and
threshold change applies immediately.
- Run yarn fix for import ordering lint.
* fix(android): Check path is a List in convertMultiClickBreadcrumb
Align with the iOS converter which validates the path type before use.
Prevents potential ClassCastException if a non-list value is passed.
* docs(changelog): Move rage tap entry to Unreleased section1 parent c0a7ee7 commit 9b6b2ba
9 files changed
Lines changed: 702 additions & 7 deletions
File tree
- packages/core
- RNSentryAndroidTester/app/src/test/java/io/sentry/rnsentryandroidtester
- RNSentryCocoaTester/RNSentryCocoaTesterTests
- android/src/main/java/io/sentry/react
- ios
- src/js
- test
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
11 | 11 | | |
12 | 12 | | |
13 | 13 | | |
| 14 | + | |
14 | 15 | | |
15 | 16 | | |
16 | 17 | | |
| |||
Lines changed: 28 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
90 | 90 | | |
91 | 91 | | |
92 | 92 | | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
93 | 121 | | |
94 | 122 | | |
95 | 123 | | |
| |||
Lines changed: 27 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
100 | 100 | | |
101 | 101 | | |
102 | 102 | | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
103 | 130 | | |
104 | 131 | | |
105 | 132 | | |
| |||
Lines changed: 18 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
32 | 32 | | |
33 | 33 | | |
34 | 34 | | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
35 | 38 | | |
36 | 39 | | |
37 | 40 | | |
| |||
72 | 75 | | |
73 | 76 | | |
74 | 77 | | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
75 | 93 | | |
76 | 94 | | |
77 | 95 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
35 | 35 | | |
36 | 36 | | |
37 | 37 | | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
38 | 42 | | |
39 | 43 | | |
40 | 44 | | |
| |||
75 | 79 | | |
76 | 80 | | |
77 | 81 | | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
78 | 102 | | |
79 | 103 | | |
80 | 104 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
0 commit comments