diff --git a/CHANGELOG.md b/CHANGELOG.md index 5837019405..85c35ff5cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,10 @@ ## Unreleased +### Features + +- 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)) + ### Fixes - Fix the issue with uploading iOS Debug Symbols in EAS Build when using pnpm ([#6076](https://github.com/getsentry/sentry-react-native/issues/6076)) diff --git a/packages/core/src/js/touchevents.tsx b/packages/core/src/js/touchevents.tsx index 7cf64956b8..466c93d761 100644 --- a/packages/core/src/js/touchevents.tsx +++ b/packages/core/src/js/touchevents.tsx @@ -87,6 +87,9 @@ const SENTRY_SPAN_ATTRIBUTES_PROP_KEY = 'sentry-span-attributes'; const SENTRY_COMPONENT_PROP_KEY = 'data-sentry-component'; const SENTRY_ELEMENT_PROP_KEY = 'data-sentry-element'; const SENTRY_FILE_PROP_KEY = 'data-sentry-source-file'; +const ACCESSIBILITY_LABEL_PROP_KEY = 'accessibilityLabel'; +const ARIA_LABEL_PROP_KEY = 'aria-label'; +const TEST_ID_PROP_KEY = 'testID'; interface ElementInstance { elementType?: { @@ -364,15 +367,31 @@ function getFileName(props: Record): string | undefined { } function getLabelValue(props: Record, labelKey: string | undefined): string | undefined { - return typeof props[SENTRY_LABEL_PROP_KEY] === 'string' && props[SENTRY_LABEL_PROP_KEY].length > 0 - ? props[SENTRY_LABEL_PROP_KEY] - : // For some reason type narrowing doesn't work as expected with indexing when checking it all in one go in - // the "check-label" if sentence, so we have to assign it to a variable here first - // oxlint-disable-next-line typescript-eslint(no-unnecessary-type-assertion) - typeof labelKey === 'string' && typeof props[labelKey] == 'string' && (props[labelKey] as string).length > 0 - ? // oxlint-disable-next-line typescript-eslint(no-unnecessary-type-assertion) - (props[labelKey] as string) - : undefined; + if (typeof props[SENTRY_LABEL_PROP_KEY] === 'string' && props[SENTRY_LABEL_PROP_KEY].length > 0) { + return props[SENTRY_LABEL_PROP_KEY]; + } + + // For some reason type narrowing doesn't work as expected with indexing when checking it all in one go in + // the "check-label" if sentence, so we have to assign it to a variable here first + // oxlint-disable-next-line typescript-eslint(no-unnecessary-type-assertion) + if (typeof labelKey === 'string' && typeof props[labelKey] == 'string' && (props[labelKey] as string).length > 0) { + // oxlint-disable-next-line typescript-eslint(no-unnecessary-type-assertion) + return props[labelKey] as string; + } + + if (typeof props[ACCESSIBILITY_LABEL_PROP_KEY] === 'string' && props[ACCESSIBILITY_LABEL_PROP_KEY].length > 0) { + return props[ACCESSIBILITY_LABEL_PROP_KEY]; + } + + if (typeof props[ARIA_LABEL_PROP_KEY] === 'string' && props[ARIA_LABEL_PROP_KEY].length > 0) { + return props[ARIA_LABEL_PROP_KEY]; + } + + if (typeof props[TEST_ID_PROP_KEY] === 'string' && props[TEST_ID_PROP_KEY].length > 0) { + return props[TEST_ID_PROP_KEY]; + } + + return undefined; } function getSpanAttributes(currentInst: ElementInstance): Record | undefined { diff --git a/packages/core/test/touchevents.test.tsx b/packages/core/test/touchevents.test.tsx index ba54115330..3bed16b5ed 100644 --- a/packages/core/test/touchevents.test.tsx +++ b/packages/core/test/touchevents.test.tsx @@ -111,6 +111,219 @@ describe('TouchEventBoundary._onTouchStart', () => { }); }); + it('accessibilityLabel is used as label fallback when sentry-label is not set', () => { + const { defaultProps } = TouchEventBoundary; + const boundary = new TouchEventBoundary(defaultProps); + + const event = { + _targetInst: { + elementType: { + displayName: 'Button', + }, + memoizedProps: { + accessibilityLabel: 'Save workout', + }, + }, + }; + + // @ts-expect-error Calling private member + boundary._onTouchStart(event); + + expect(addBreadcrumb).toHaveBeenCalledWith({ + category: defaultProps.breadcrumbCategory, + data: { + path: [{ name: 'Button', label: 'Save workout' }], + }, + level: 'info' as SeverityLevel, + message: 'Touch event within element: Save workout', + type: defaultProps.breadcrumbType, + }); + }); + + it('testID is used as label fallback when sentry-label and accessibilityLabel are not set', () => { + const { defaultProps } = TouchEventBoundary; + const boundary = new TouchEventBoundary(defaultProps); + + const event = { + _targetInst: { + elementType: { + displayName: 'Button', + }, + memoizedProps: { + testID: 'save-workout-button', + }, + }, + }; + + // @ts-expect-error Calling private member + boundary._onTouchStart(event); + + expect(addBreadcrumb).toHaveBeenCalledWith({ + category: defaultProps.breadcrumbCategory, + data: { + path: [{ name: 'Button', label: 'save-workout-button' }], + }, + level: 'info' as SeverityLevel, + message: 'Touch event within element: save-workout-button', + type: defaultProps.breadcrumbType, + }); + }); + + it('sentry-label takes priority over accessibilityLabel and testID', () => { + const { defaultProps } = TouchEventBoundary; + const boundary = new TouchEventBoundary(defaultProps); + + const event = { + _targetInst: { + elementType: { + displayName: 'Button', + }, + memoizedProps: { + 'sentry-label': 'explicit-label', + accessibilityLabel: 'Save workout', + testID: 'save-workout-button', + }, + }, + }; + + // @ts-expect-error Calling private member + boundary._onTouchStart(event); + + expect(addBreadcrumb).toHaveBeenCalledWith({ + category: defaultProps.breadcrumbCategory, + data: { + path: [{ name: 'Button', label: 'explicit-label' }], + }, + level: 'info' as SeverityLevel, + message: 'Touch event within element: explicit-label', + type: defaultProps.breadcrumbType, + }); + }); + + it('custom labelName takes priority over accessibilityLabel', () => { + const { defaultProps } = TouchEventBoundary; + const boundary = new TouchEventBoundary({ + ...defaultProps, + labelName: 'custom-label-key', + }); + + const event = { + _targetInst: { + elementType: { + displayName: 'Button', + }, + memoizedProps: { + 'custom-label-key': 'Custom label', + accessibilityLabel: 'Save workout', + testID: 'save-workout-button', + }, + }, + }; + + // @ts-expect-error Calling private member + boundary._onTouchStart(event); + + expect(addBreadcrumb).toHaveBeenCalledWith({ + category: defaultProps.breadcrumbCategory, + data: { + path: [{ name: 'Button', label: 'Custom label' }], + }, + level: 'info' as SeverityLevel, + message: 'Touch event within element: Custom label', + type: defaultProps.breadcrumbType, + }); + }); + + it('aria-label is used as fallback after accessibilityLabel', () => { + const { defaultProps } = TouchEventBoundary; + const boundary = new TouchEventBoundary(defaultProps); + + const event = { + _targetInst: { + elementType: { + displayName: 'Button', + }, + memoizedProps: { + 'aria-label': 'Close dialog', + }, + }, + }; + + // @ts-expect-error Calling private member + boundary._onTouchStart(event); + + expect(addBreadcrumb).toHaveBeenCalledWith({ + category: defaultProps.breadcrumbCategory, + data: { + path: [{ name: 'Button', label: 'Close dialog' }], + }, + level: 'info' as SeverityLevel, + message: 'Touch event within element: Close dialog', + type: defaultProps.breadcrumbType, + }); + }); + + it('accessibilityLabel takes priority over aria-label and testID', () => { + const { defaultProps } = TouchEventBoundary; + const boundary = new TouchEventBoundary(defaultProps); + + const event = { + _targetInst: { + elementType: { + displayName: 'Button', + }, + memoizedProps: { + accessibilityLabel: 'Save workout', + 'aria-label': 'Close dialog', + testID: 'save-workout-button', + }, + }, + }; + + // @ts-expect-error Calling private member + boundary._onTouchStart(event); + + expect(addBreadcrumb).toHaveBeenCalledWith({ + category: defaultProps.breadcrumbCategory, + data: { + path: [{ name: 'Button', label: 'Save workout' }], + }, + level: 'info' as SeverityLevel, + message: 'Touch event within element: Save workout', + type: defaultProps.breadcrumbType, + }); + }); + + it('accessibilityLabel takes priority over testID', () => { + const { defaultProps } = TouchEventBoundary; + const boundary = new TouchEventBoundary(defaultProps); + + const event = { + _targetInst: { + elementType: { + displayName: 'Button', + }, + memoizedProps: { + accessibilityLabel: 'Save workout', + testID: 'save-workout-button', + }, + }, + }; + + // @ts-expect-error Calling private member + boundary._onTouchStart(event); + + expect(addBreadcrumb).toHaveBeenCalledWith({ + category: defaultProps.breadcrumbCategory, + data: { + path: [{ name: 'Button', label: 'Save workout' }], + }, + level: 'info' as SeverityLevel, + message: 'Touch event within element: Save workout', + type: defaultProps.breadcrumbType, + }); + }); + it('ignoreNames', () => { const { defaultProps } = TouchEventBoundary; const boundary = new TouchEventBoundary({