Skip to content

Commit 85231c0

Browse files
authored
Merge pull request Expensify#76547 from software-mansion-labs/dariusz-biela/perf/sentry/inp-labels-patch
[No QA] Patch Sentry htmlTreeAsString to support data-sentry-label attributes
2 parents 19638ba + ff19756 commit 85231c0

9 files changed

Lines changed: 170 additions & 2 deletions

File tree

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
diff --git a/node_modules/@sentry/core/build/cjs/utils/browser.js b/node_modules/@sentry/core/build/cjs/utils/browser.js
2+
index 3169c25..19acc49 100644
3+
--- a/node_modules/@sentry/core/build/cjs/utils/browser.js
4+
+++ b/node_modules/@sentry/core/build/cjs/utils/browser.js
5+
@@ -9,8 +9,8 @@ const DEFAULT_MAX_STRING_LENGTH = 80;
6+
7+
/**
8+
* Given a child DOM element, returns a query-selector statement describing that
9+
- * and its ancestors
10+
- * e.g. [HTMLElement] => body > div > input#foo.btn[name=baz]
11+
+ * and its ancestors, prefixed with data-sentry-label if found on ancestor
12+
+ * e.g. [HTMLElement] => [data-sentry-label="MyLabel"] div.css-146c3p1.r-1udh08x.r-1udbk01.r-1iln25a > svg
13+
* @returns generated DOM path
14+
*/
15+
function htmlTreeAsString(
16+
@@ -53,7 +53,30 @@ function htmlTreeAsString(
17+
currentElem = currentElem.parentNode;
18+
}
19+
20+
- return out.reverse().join(separator);
21+
+ const cssSelector = out.reverse().join(separator);
22+
+
23+
+ // If cssSelector already contains data-sentry-label, return as is
24+
+ if (cssSelector.includes('[data-sentry-label="')) {
25+
+ return cssSelector;
26+
+ }
27+
+
28+
+ // Search for data-sentry-label up to 15 levels (beyond the 5 levels of cssSelector)
29+
+ let labelElem = elem;
30+
+ let dataLabel = null;
31+
+ for (let i = 0; i < 15 && labelElem; i++) {
32+
+ // @ts-expect-error WINDOW has HTMLElement
33+
+ if (WINDOW.HTMLElement && labelElem instanceof HTMLElement && labelElem.dataset && labelElem.dataset['sentryLabel']) {
34+
+ dataLabel = labelElem.dataset['sentryLabel'];
35+
+ break;
36+
+ }
37+
+ labelElem = labelElem.parentNode;
38+
+ }
39+
+
40+
+ if (dataLabel) {
41+
+ return `[data-sentry-label="${dataLabel}"] ${cssSelector}`;
42+
+ }
43+
+
44+
+ return cssSelector;
45+
} catch {
46+
return '<unknown>';
47+
}
48+
@@ -77,8 +100,12 @@ function _htmlElementAsString(el, keyAttrs) {
49+
50+
// @ts-expect-error WINDOW has HTMLElement
51+
if (WINDOW.HTMLElement) {
52+
- // If using the component name annotation plugin, this value may be available on the DOM node
53+
if (elem instanceof HTMLElement && elem.dataset) {
54+
+ // Check for data-sentry-label first - return in attribute format [data-sentry-label="value"]
55+
+ if (elem.dataset['sentryLabel']) {
56+
+ return `[data-sentry-label="${elem.dataset['sentryLabel']}"]`;
57+
+ }
58+
+ // If using the component name annotation plugin, this value may be available on the DOM node
59+
if (elem.dataset['sentryComponent']) {
60+
return elem.dataset['sentryComponent'];
61+
}
62+
diff --git a/node_modules/@sentry/core/build/esm/utils/browser.js b/node_modules/@sentry/core/build/esm/utils/browser.js
63+
index 2ad52b0..fd184fb 100644
64+
--- a/node_modules/@sentry/core/build/esm/utils/browser.js
65+
+++ b/node_modules/@sentry/core/build/esm/utils/browser.js
66+
@@ -7,8 +7,8 @@ const DEFAULT_MAX_STRING_LENGTH = 80;
67+
68+
/**
69+
* Given a child DOM element, returns a query-selector statement describing that
70+
- * and its ancestors
71+
- * e.g. [HTMLElement] => body > div > input#foo.btn[name=baz]
72+
+ * and its ancestors, prefixed with data-sentry-label if found on ancestor
73+
+ * e.g. [HTMLElement] => [data-sentry-label="MyLabel"] div.css-146c3p1.r-1udh08x.r-1udbk01.r-1iln25a > svg
74+
* @returns generated DOM path
75+
*/
76+
function htmlTreeAsString(
77+
@@ -51,7 +51,30 @@ function htmlTreeAsString(
78+
currentElem = currentElem.parentNode;
79+
}
80+
81+
- return out.reverse().join(separator);
82+
+ const cssSelector = out.reverse().join(separator);
83+
+
84+
+ // If cssSelector already contains data-sentry-label, return as is
85+
+ if (cssSelector.includes('[data-sentry-label="')) {
86+
+ return cssSelector;
87+
+ }
88+
+
89+
+ // Search for data-sentry-label up to 15 levels (beyond the 5 levels of cssSelector)
90+
+ let labelElem = elem;
91+
+ let dataLabel = null;
92+
+ for (let i = 0; i < 15 && labelElem; i++) {
93+
+ // @ts-expect-error WINDOW has HTMLElement
94+
+ if (WINDOW.HTMLElement && labelElem instanceof HTMLElement && labelElem.dataset && labelElem.dataset['sentryLabel']) {
95+
+ dataLabel = labelElem.dataset['sentryLabel'];
96+
+ break;
97+
+ }
98+
+ labelElem = labelElem.parentNode;
99+
+ }
100+
+
101+
+ if (dataLabel) {
102+
+ return `[data-sentry-label="${dataLabel}"] ${cssSelector}`;
103+
+ }
104+
+
105+
+ return cssSelector;
106+
} catch {
107+
return '<unknown>';
108+
}
109+
@@ -75,8 +98,12 @@ function _htmlElementAsString(el, keyAttrs) {
110+
111+
// @ts-expect-error WINDOW has HTMLElement
112+
if (WINDOW.HTMLElement) {
113+
- // If using the component name annotation plugin, this value may be available on the DOM node
114+
if (elem instanceof HTMLElement && elem.dataset) {
115+
+ // Check for data-sentry-label first - return in attribute format [data-sentry-label="value"]
116+
+ if (elem.dataset['sentryLabel']) {
117+
+ return `[data-sentry-label="${elem.dataset['sentryLabel']}"]`;
118+
+ }
119+
+ // If using the component name annotation plugin, this value may be available on the DOM node
120+
if (elem.dataset['sentryComponent']) {
121+
return elem.dataset['sentryComponent'];
122+
}

patches/sentry-core/details.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# `@sentry/core` patches
2+
3+
### [@sentry+core+10.24.0+001+data-sentry-label-support.patch](@sentry+core+10.24.0+001+data-sentry-label-support.patch)
4+
5+
- Reason: Enhances the `htmlTreeAsString` function to support `data-sentry-label` attributes for better element identification in Sentry spans. The patch:
6+
- Always includes `data-sentry-label` in the list of checked attributes for each DOM element
7+
- Searches up to 15 levels up the DOM tree to find a `data-sentry-label` attribute
8+
- Prefixes the CSS selector with the found `data-sentry-label` value (e.g., `[data-sentry-label="MyLabel"] div.css-146c3p1.r-1udh08x.r-1udbk01.r-1iln25a > svg`)
9+
10+
This allows us to identify UI elements by meaningful labels rather than just CSS selectors, making Sentry spans more actionable.
11+
- Upstream PR/issue: https://github.com/getsentry/sentry-javascript/pull/18398
12+
- E/App issue: https://github.com/Expensify/App/issues/76128
13+
- PR Introducing Patch: https://github.com/Expensify/App/pull/76547

src/CONST/index.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7482,6 +7482,16 @@ const CONST = {
74827482
REGULAR: 'regular',
74837483
INVERTED: 'inverted',
74847484
},
7485+
7486+
SENTRY_LABEL: {
7487+
NAVIGATION_TAB_BAR: {
7488+
EXPENSIFY_LOGO: 'NavigationTabBar-ExpensifyLogo',
7489+
INBOX: 'NavigationTabBar-Inbox',
7490+
REPORTS: 'NavigationTabBar-Reports',
7491+
WORKSPACES: 'NavigationTabBar-Workspaces',
7492+
ACCOUNT: 'NavigationTabBar-Account',
7493+
},
7494+
},
74857495
} as const;
74867496

74877497
const CONTINUATION_DETECTION_SEARCH_FILTER_KEYS = [

src/components/Button/index.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,9 @@ type ButtonProps = Partial<ChildrenProps> & {
176176
* Whether the button should stay visually normal even when disabled.
177177
*/
178178
shouldStayNormalOnDisable?: boolean;
179+
180+
/** Label for Sentry tracking. On web, this will be added as data-sentry-label attribute. */
181+
sentryLabel?: string;
179182
};
180183

181184
type KeyboardShortcutComponentProps = Pick<ButtonProps, 'isDisabled' | 'isLoading' | 'onPress' | 'pressOnEnter' | 'allowBubble' | 'enterKeyEventListenerPriority' | 'isPressOnEnterActive'>;
@@ -282,6 +285,7 @@ function Button({
282285
secondLineText = '',
283286
shouldBlendOpacity = false,
284287
shouldStayNormalOnDisable = false,
288+
sentryLabel,
285289
ref,
286290
...rest
287291
}: ButtonProps) {
@@ -527,6 +531,7 @@ function Button({
527531
hoverDimmingValue={1}
528532
onHoverIn={!isDisabled || !shouldStayNormalOnDisable ? () => setIsHovered(true) : undefined}
529533
onHoverOut={!isDisabled || !shouldStayNormalOnDisable ? () => setIsHovered(false) : undefined}
534+
sentryLabel={sentryLabel}
530535
>
531536
{shouldBlendOpacity && <View style={[StyleSheet.absoluteFill, buttonBlendForegroundStyle]} />}
532537
{renderContent()}

src/components/Checkbox.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,9 @@ type CheckboxProps = Partial<ChildrenProps> & {
6363

6464
/** Reference to the outer element */
6565
ref?: ForwardedRef<View>;
66+
67+
/** Label for Sentry tracking. On web, this will be added as data-sentry-label attribute. */
68+
sentryLabel?: string;
6669
};
6770

6871
function Checkbox({
@@ -84,6 +87,7 @@ function Checkbox({
8487
wrapperStyle,
8588
testID,
8689
ref,
90+
sentryLabel,
8791
}: CheckboxProps) {
8892
const theme = useTheme();
8993
const styles = useThemeStyles();
@@ -135,6 +139,7 @@ function Checkbox({
135139
accessibilityLabel={accessibilityLabel}
136140
pressDimmingValue={1}
137141
wrapperStyle={wrapperStyle}
142+
sentryLabel={sentryLabel}
138143
>
139144
{children ?? (
140145
<View

src/components/Navigation/NavigationTabBar/index.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,7 @@ function NavigationTabBar({selectedTab, isTopLevelBar = false, shouldShowFloatin
261261
testID="ExpensifyLogoButton"
262262
onPress={navigateToChats}
263263
wrapperStyle={styles.leftNavigationTabBarItem}
264+
sentryLabel={CONST.SENTRY_LABEL.NAVIGATION_TAB_BAR.EXPENSIFY_LOGO}
264265
>
265266
<ImageSVG
266267
style={StyleUtils.getAvatarStyle(CONST.AVATAR_SIZE.DEFAULT)}
@@ -272,6 +273,7 @@ function NavigationTabBar({selectedTab, isTopLevelBar = false, shouldShowFloatin
272273
role={CONST.ROLE.BUTTON}
273274
accessibilityLabel={translate('common.inbox')}
274275
style={({hovered}) => [styles.leftNavigationTabBarItem, hovered && styles.navigationTabBarItemHovered]}
276+
sentryLabel={CONST.SENTRY_LABEL.NAVIGATION_TAB_BAR.INBOX}
275277
>
276278
{({hovered}) => (
277279
<>
@@ -312,6 +314,7 @@ function NavigationTabBar({selectedTab, isTopLevelBar = false, shouldShowFloatin
312314
role={CONST.ROLE.BUTTON}
313315
accessibilityLabel={translate('common.reports')}
314316
style={({hovered}) => [styles.leftNavigationTabBarItem, hovered && styles.navigationTabBarItemHovered]}
317+
sentryLabel={CONST.SENTRY_LABEL.NAVIGATION_TAB_BAR.REPORTS}
315318
>
316319
{({hovered}) => (
317320
<>
@@ -343,6 +346,7 @@ function NavigationTabBar({selectedTab, isTopLevelBar = false, shouldShowFloatin
343346
role={CONST.ROLE.BUTTON}
344347
accessibilityLabel={translate('common.workspacesTabTitle')}
345348
style={({hovered}) => [styles.leftNavigationTabBarItem, hovered && styles.navigationTabBarItemHovered]}
349+
sentryLabel={CONST.SENTRY_LABEL.NAVIGATION_TAB_BAR.WORKSPACES}
346350
>
347351
{({hovered}) => (
348352
<>
@@ -410,6 +414,7 @@ function NavigationTabBar({selectedTab, isTopLevelBar = false, shouldShowFloatin
410414
accessibilityLabel={translate('common.inbox')}
411415
wrapperStyle={styles.flex1}
412416
style={styles.navigationTabBarItem}
417+
sentryLabel={CONST.SENTRY_LABEL.NAVIGATION_TAB_BAR.INBOX}
413418
>
414419
<View>
415420
<Icon
@@ -446,6 +451,7 @@ function NavigationTabBar({selectedTab, isTopLevelBar = false, shouldShowFloatin
446451
accessibilityLabel={translate('common.reports')}
447452
wrapperStyle={styles.flex1}
448453
style={styles.navigationTabBarItem}
454+
sentryLabel={CONST.SENTRY_LABEL.NAVIGATION_TAB_BAR.REPORTS}
449455
>
450456
<View>
451457
<Icon
@@ -477,6 +483,7 @@ function NavigationTabBar({selectedTab, isTopLevelBar = false, shouldShowFloatin
477483
accessibilityLabel={translate('common.workspacesTabTitle')}
478484
wrapperStyle={styles.flex1}
479485
style={styles.navigationTabBarItem}
486+
sentryLabel={CONST.SENTRY_LABEL.NAVIGATION_TAB_BAR.WORKSPACES}
480487
>
481488
<View>
482489
<Icon

src/components/Pressable/GenericPressable/implementation/index.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import type {Role} from 'react-native';
33
import type PressableProps from '@components/Pressable/GenericPressable/types';
44
import GenericPressable from './BaseGenericPressable';
55

6-
function WebGenericPressable({focusable = true, ref, ...props}: PressableProps) {
6+
function WebGenericPressable({focusable = true, ref, sentryLabel, ...props}: PressableProps) {
77
const accessible = (props.accessible ?? props.accessible === undefined) ? true : props.accessible;
88

99
return (
@@ -22,7 +22,7 @@ function WebGenericPressable({focusable = true, ref, ...props}: PressableProps)
2222
aria-valuemin={props.accessibilityValue?.min}
2323
aria-valuemax={props.accessibilityValue?.max}
2424
aria-valuetext={props.accessibilityValue?.text}
25-
dataSet={{tag: 'pressable', ...(props.noDragArea && {dragArea: false}), ...props.dataSet}}
25+
dataSet={{tag: 'pressable', ...(props.noDragArea && {dragArea: false}), ...(sentryLabel && {sentryLabel}), ...props.dataSet}}
2626
/>
2727
);
2828
}

src/components/Pressable/GenericPressable/types.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,11 @@ type PressableProps = RNPressableProps &
156156
*/
157157
isNested?: boolean;
158158

159+
/**
160+
* Label for Sentry tracking. On web, this will be added as data-sentry-label attribute.
161+
*/
162+
sentryLabel?: string;
163+
159164
/**
160165
* Reference to the outer element.
161166
*/

src/pages/home/sidebar/NavigationTabBarAvatar.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ function NavigationTabBarAvatar({onPress, isSelected = false, style}: Navigation
7272
accessibilityLabel={translate('sidebarScreen.buttonMySettings')}
7373
wrapperStyle={styles.flex1}
7474
style={({hovered}) => [style, hovered && styles.navigationTabBarItemHovered]}
75+
sentryLabel={CONST.SENTRY_LABEL.NAVIGATION_TAB_BAR.ACCOUNT}
7576
>
7677
{({hovered}) => (
7778
<>

0 commit comments

Comments
 (0)