Skip to content

Commit a736b76

Browse files
antonisclaude
andauthored
feat(core): Use accessibilityLabel, aria-label, and testID as touch breadcrumb label fallbacks (#6103)
* feat(core): Use accessibilityLabel, aria-label, and testID as touch breadcrumb label fallbacks Closes #6096 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * chore: Update CHANGELOG link to PR Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 5fda63b commit a736b76

3 files changed

Lines changed: 245 additions & 9 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@
88
99
## Unreleased
1010

11+
### Features
12+
13+
- Use `accessibilityLabel`, `aria-label`, and `testID` as fallback labels for touch breadcrumbs when `sentry-label` is not set ([#6103](https://github.com/getsentry/sentry-react-native/pull/6103))
14+
1115
### Fixes
1216

1317
- Fix the issue with uploading iOS Debug Symbols in EAS Build when using pnpm ([#6076](https://github.com/getsentry/sentry-react-native/issues/6076))

packages/core/src/js/touchevents.tsx

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,9 @@ const SENTRY_SPAN_ATTRIBUTES_PROP_KEY = 'sentry-span-attributes';
8787
const SENTRY_COMPONENT_PROP_KEY = 'data-sentry-component';
8888
const SENTRY_ELEMENT_PROP_KEY = 'data-sentry-element';
8989
const SENTRY_FILE_PROP_KEY = 'data-sentry-source-file';
90+
const ACCESSIBILITY_LABEL_PROP_KEY = 'accessibilityLabel';
91+
const ARIA_LABEL_PROP_KEY = 'aria-label';
92+
const TEST_ID_PROP_KEY = 'testID';
9093

9194
interface ElementInstance {
9295
elementType?: {
@@ -364,15 +367,31 @@ function getFileName(props: Record<string, unknown>): string | undefined {
364367
}
365368

366369
function getLabelValue(props: Record<string, unknown>, labelKey: string | undefined): string | undefined {
367-
return typeof props[SENTRY_LABEL_PROP_KEY] === 'string' && props[SENTRY_LABEL_PROP_KEY].length > 0
368-
? props[SENTRY_LABEL_PROP_KEY]
369-
: // For some reason type narrowing doesn't work as expected with indexing when checking it all in one go in
370-
// the "check-label" if sentence, so we have to assign it to a variable here first
371-
// oxlint-disable-next-line typescript-eslint(no-unnecessary-type-assertion)
372-
typeof labelKey === 'string' && typeof props[labelKey] == 'string' && (props[labelKey] as string).length > 0
373-
? // oxlint-disable-next-line typescript-eslint(no-unnecessary-type-assertion)
374-
(props[labelKey] as string)
375-
: undefined;
370+
if (typeof props[SENTRY_LABEL_PROP_KEY] === 'string' && props[SENTRY_LABEL_PROP_KEY].length > 0) {
371+
return props[SENTRY_LABEL_PROP_KEY];
372+
}
373+
374+
// For some reason type narrowing doesn't work as expected with indexing when checking it all in one go in
375+
// the "check-label" if sentence, so we have to assign it to a variable here first
376+
// oxlint-disable-next-line typescript-eslint(no-unnecessary-type-assertion)
377+
if (typeof labelKey === 'string' && typeof props[labelKey] == 'string' && (props[labelKey] as string).length > 0) {
378+
// oxlint-disable-next-line typescript-eslint(no-unnecessary-type-assertion)
379+
return props[labelKey] as string;
380+
}
381+
382+
if (typeof props[ACCESSIBILITY_LABEL_PROP_KEY] === 'string' && props[ACCESSIBILITY_LABEL_PROP_KEY].length > 0) {
383+
return props[ACCESSIBILITY_LABEL_PROP_KEY];
384+
}
385+
386+
if (typeof props[ARIA_LABEL_PROP_KEY] === 'string' && props[ARIA_LABEL_PROP_KEY].length > 0) {
387+
return props[ARIA_LABEL_PROP_KEY];
388+
}
389+
390+
if (typeof props[TEST_ID_PROP_KEY] === 'string' && props[TEST_ID_PROP_KEY].length > 0) {
391+
return props[TEST_ID_PROP_KEY];
392+
}
393+
394+
return undefined;
376395
}
377396

378397
function getSpanAttributes(currentInst: ElementInstance): Record<string, SpanAttributeValue> | undefined {

packages/core/test/touchevents.test.tsx

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,219 @@ describe('TouchEventBoundary._onTouchStart', () => {
111111
});
112112
});
113113

114+
it('accessibilityLabel is used as label fallback when sentry-label is not set', () => {
115+
const { defaultProps } = TouchEventBoundary;
116+
const boundary = new TouchEventBoundary(defaultProps);
117+
118+
const event = {
119+
_targetInst: {
120+
elementType: {
121+
displayName: 'Button',
122+
},
123+
memoizedProps: {
124+
accessibilityLabel: 'Save workout',
125+
},
126+
},
127+
};
128+
129+
// @ts-expect-error Calling private member
130+
boundary._onTouchStart(event);
131+
132+
expect(addBreadcrumb).toHaveBeenCalledWith({
133+
category: defaultProps.breadcrumbCategory,
134+
data: {
135+
path: [{ name: 'Button', label: 'Save workout' }],
136+
},
137+
level: 'info' as SeverityLevel,
138+
message: 'Touch event within element: Save workout',
139+
type: defaultProps.breadcrumbType,
140+
});
141+
});
142+
143+
it('testID is used as label fallback when sentry-label and accessibilityLabel are not set', () => {
144+
const { defaultProps } = TouchEventBoundary;
145+
const boundary = new TouchEventBoundary(defaultProps);
146+
147+
const event = {
148+
_targetInst: {
149+
elementType: {
150+
displayName: 'Button',
151+
},
152+
memoizedProps: {
153+
testID: 'save-workout-button',
154+
},
155+
},
156+
};
157+
158+
// @ts-expect-error Calling private member
159+
boundary._onTouchStart(event);
160+
161+
expect(addBreadcrumb).toHaveBeenCalledWith({
162+
category: defaultProps.breadcrumbCategory,
163+
data: {
164+
path: [{ name: 'Button', label: 'save-workout-button' }],
165+
},
166+
level: 'info' as SeverityLevel,
167+
message: 'Touch event within element: save-workout-button',
168+
type: defaultProps.breadcrumbType,
169+
});
170+
});
171+
172+
it('sentry-label takes priority over accessibilityLabel and testID', () => {
173+
const { defaultProps } = TouchEventBoundary;
174+
const boundary = new TouchEventBoundary(defaultProps);
175+
176+
const event = {
177+
_targetInst: {
178+
elementType: {
179+
displayName: 'Button',
180+
},
181+
memoizedProps: {
182+
'sentry-label': 'explicit-label',
183+
accessibilityLabel: 'Save workout',
184+
testID: 'save-workout-button',
185+
},
186+
},
187+
};
188+
189+
// @ts-expect-error Calling private member
190+
boundary._onTouchStart(event);
191+
192+
expect(addBreadcrumb).toHaveBeenCalledWith({
193+
category: defaultProps.breadcrumbCategory,
194+
data: {
195+
path: [{ name: 'Button', label: 'explicit-label' }],
196+
},
197+
level: 'info' as SeverityLevel,
198+
message: 'Touch event within element: explicit-label',
199+
type: defaultProps.breadcrumbType,
200+
});
201+
});
202+
203+
it('custom labelName takes priority over accessibilityLabel', () => {
204+
const { defaultProps } = TouchEventBoundary;
205+
const boundary = new TouchEventBoundary({
206+
...defaultProps,
207+
labelName: 'custom-label-key',
208+
});
209+
210+
const event = {
211+
_targetInst: {
212+
elementType: {
213+
displayName: 'Button',
214+
},
215+
memoizedProps: {
216+
'custom-label-key': 'Custom label',
217+
accessibilityLabel: 'Save workout',
218+
testID: 'save-workout-button',
219+
},
220+
},
221+
};
222+
223+
// @ts-expect-error Calling private member
224+
boundary._onTouchStart(event);
225+
226+
expect(addBreadcrumb).toHaveBeenCalledWith({
227+
category: defaultProps.breadcrumbCategory,
228+
data: {
229+
path: [{ name: 'Button', label: 'Custom label' }],
230+
},
231+
level: 'info' as SeverityLevel,
232+
message: 'Touch event within element: Custom label',
233+
type: defaultProps.breadcrumbType,
234+
});
235+
});
236+
237+
it('aria-label is used as fallback after accessibilityLabel', () => {
238+
const { defaultProps } = TouchEventBoundary;
239+
const boundary = new TouchEventBoundary(defaultProps);
240+
241+
const event = {
242+
_targetInst: {
243+
elementType: {
244+
displayName: 'Button',
245+
},
246+
memoizedProps: {
247+
'aria-label': 'Close dialog',
248+
},
249+
},
250+
};
251+
252+
// @ts-expect-error Calling private member
253+
boundary._onTouchStart(event);
254+
255+
expect(addBreadcrumb).toHaveBeenCalledWith({
256+
category: defaultProps.breadcrumbCategory,
257+
data: {
258+
path: [{ name: 'Button', label: 'Close dialog' }],
259+
},
260+
level: 'info' as SeverityLevel,
261+
message: 'Touch event within element: Close dialog',
262+
type: defaultProps.breadcrumbType,
263+
});
264+
});
265+
266+
it('accessibilityLabel takes priority over aria-label and testID', () => {
267+
const { defaultProps } = TouchEventBoundary;
268+
const boundary = new TouchEventBoundary(defaultProps);
269+
270+
const event = {
271+
_targetInst: {
272+
elementType: {
273+
displayName: 'Button',
274+
},
275+
memoizedProps: {
276+
accessibilityLabel: 'Save workout',
277+
'aria-label': 'Close dialog',
278+
testID: 'save-workout-button',
279+
},
280+
},
281+
};
282+
283+
// @ts-expect-error Calling private member
284+
boundary._onTouchStart(event);
285+
286+
expect(addBreadcrumb).toHaveBeenCalledWith({
287+
category: defaultProps.breadcrumbCategory,
288+
data: {
289+
path: [{ name: 'Button', label: 'Save workout' }],
290+
},
291+
level: 'info' as SeverityLevel,
292+
message: 'Touch event within element: Save workout',
293+
type: defaultProps.breadcrumbType,
294+
});
295+
});
296+
297+
it('accessibilityLabel takes priority over testID', () => {
298+
const { defaultProps } = TouchEventBoundary;
299+
const boundary = new TouchEventBoundary(defaultProps);
300+
301+
const event = {
302+
_targetInst: {
303+
elementType: {
304+
displayName: 'Button',
305+
},
306+
memoizedProps: {
307+
accessibilityLabel: 'Save workout',
308+
testID: 'save-workout-button',
309+
},
310+
},
311+
};
312+
313+
// @ts-expect-error Calling private member
314+
boundary._onTouchStart(event);
315+
316+
expect(addBreadcrumb).toHaveBeenCalledWith({
317+
category: defaultProps.breadcrumbCategory,
318+
data: {
319+
path: [{ name: 'Button', label: 'Save workout' }],
320+
},
321+
level: 'info' as SeverityLevel,
322+
message: 'Touch event within element: Save workout',
323+
type: defaultProps.breadcrumbType,
324+
});
325+
});
326+
114327
it('ignoreNames', () => {
115328
const { defaultProps } = TouchEventBoundary;
116329
const boundary = new TouchEventBoundary({

0 commit comments

Comments
 (0)