Skip to content

Commit a8a0c2b

Browse files
authored
feat: Touch events now track components with sentry-label (#2068)
Support tracking touch events on components with the prop `sentry-label`. Note that this does not throw a typescript error as ["it is not a valid JS Identifier"](https://www.typescriptlang.org/docs/handbook/jsx.html#attribute-type-checking) It will fall back to other properties, with the hierarchy `sentry-label` > `accessibilityLabel` > `displayName`. Also, removed the tracking of `name` on components without a displayName, found that doing so just caused components to be logged twice.
1 parent 6ac64ba commit a8a0c2b

File tree

4 files changed

+105
-58
lines changed

4 files changed

+105
-58
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
## Unreleased
44

5+
- feat: Touch events now track components with `sentry-label` prop, falls back to `accessibilityLabel` and then finally `displayName`. #2068
56
- fix: Respect sentryOption.debug setting instead of #DEBUG build flag for outputting logs #2039
67
- fix: Passing correct mutableOptions to iOS SDK (#2037)
78
- Bump: Bump @sentry/javascript dependencies to 6.17.9 #2082

sample/src/screens/HomeScreen.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -153,21 +153,24 @@ const HomeScreen = (props: Props) => {
153153
<TouchableOpacity
154154
onPress={() => {
155155
Sentry.captureMessage('Test Message');
156-
}}>
156+
}}
157+
sentry-label="captureMessage">
157158
<Text style={styles.buttonText}>Capture Message</Text>
158159
</TouchableOpacity>
159160
<View style={styles.spacer} />
160161
<TouchableOpacity
161162
onPress={() => {
162163
Sentry.captureException(new Error('Test Error'));
163-
}}>
164+
}}
165+
accessibilityLabel="captureException">
164166
<Text style={styles.buttonText}>Capture Exception</Text>
165167
</TouchableOpacity>
166168
<View style={styles.spacer} />
167169
<TouchableOpacity
168170
onPress={() => {
169171
throw new Error('Thrown Error');
170-
}}>
172+
}}
173+
sentry-label="throwError">
171174
<Text style={styles.buttonText}>Uncaught Thrown Error</Text>
172175
</TouchableOpacity>
173176
<View style={styles.spacer} />

src/js/touchevents.tsx

Lines changed: 68 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { addBreadcrumb } from "@sentry/core";
22
import { Severity } from "@sentry/types";
3+
import { logger } from "@sentry/utils";
34
import * as React from "react";
45
import { StyleSheet, View } from "react-native";
56

@@ -38,11 +39,14 @@ const DEFAULT_BREADCRUMB_CATEGORY = "touch";
3839
const DEFAULT_BREADCRUMB_TYPE = "user";
3940
const DEFAULT_MAX_COMPONENT_TREE_SIZE = 20;
4041

42+
const PROP_KEY = "sentry-label";
43+
4144
interface ElementInstance {
4245
elementType?: {
4346
displayName?: string;
4447
name?: string;
4548
};
49+
memoizedProps?: Record<string, unknown>;
4650
return?: ElementInstance;
4751
}
4852

@@ -66,32 +70,39 @@ class TouchEventBoundary extends React.Component<TouchEventBoundaryProps> {
6670
<View
6771
style={touchEventStyles.wrapperView}
6872
// eslint-disable-next-line @typescript-eslint/no-explicit-any
69-
onTouchStart={this._onTouchStart as any}
73+
onTouchStart={this._onTouchStart.bind(this) as any}
7074
>
7175
{this.props.children}
7276
</View>
7377
);
7478
}
7579

7680
/**
77-
*
81+
* Logs the touch event given the component tree names and a label.
7882
*/
7983
private _logTouchEvent(
8084
componentTreeNames: string[],
81-
displayName: string | null
85+
activeLabel?: string
8286
): void {
83-
addBreadcrumb({
87+
const crumb = {
8488
category: this.props.breadcrumbCategory,
8589
data: { componentTree: componentTreeNames },
8690
level: Severity.Info,
87-
message: displayName
88-
? `Touch event within element: ${displayName}`
91+
message: activeLabel
92+
? `Touch event within element: ${activeLabel}`
8993
: `Touch event within component tree`,
9094
type: this.props.breadcrumbType,
91-
});
95+
};
96+
97+
addBreadcrumb(crumb);
98+
99+
logger.log(`[TouchEvents] ${crumb.message}`);
92100
}
93101

94-
private _isNameIgnored = (name: string): boolean => {
102+
/**
103+
* Checks if the name is supposed to be ignored.
104+
*/
105+
private _isNameIgnored(name: string): boolean {
95106
let ignoreNames = this.props.ignoreNames || [];
96107
// eslint-disable-next-line deprecation/deprecation
97108
if (this.props.ignoredDisplayNames) {
@@ -105,17 +116,23 @@ class TouchEventBoundary extends React.Component<TouchEventBoundaryProps> {
105116
(typeof ignoreName === "string" && name === ignoreName) ||
106117
(ignoreName instanceof RegExp && name.match(ignoreName))
107118
);
108-
};
119+
}
109120

110121
// Originally was going to clean the names of any HOCs as well but decided that it might hinder debugging effectively. Will leave here in case
111122
// private readonly _cleanName = (name: string): string =>
112123
// name.replace(/.*\(/g, "").replace(/\)/g, "");
113124

114-
private _onTouchStart = (e: { _targetInst?: ElementInstance }): void => {
125+
/**
126+
* Traverses through the component tree when a touch happens and logs it.
127+
* @param e
128+
*/
129+
// eslint-disable-next-line complexity
130+
private _onTouchStart(e: { _targetInst?: ElementInstance }): void {
115131
if (e._targetInst) {
116132
let currentInst: ElementInstance | undefined = e._targetInst;
117133

118-
let activeDisplayName = null;
134+
let activeLabel: string | undefined;
135+
let activeDisplayName: string | undefined;
119136
const componentTreeNames: string[] = [];
120137

121138
while (
@@ -124,40 +141,59 @@ class TouchEventBoundary extends React.Component<TouchEventBoundaryProps> {
124141
this.props.maxComponentTreeSize &&
125142
componentTreeNames.length < this.props.maxComponentTreeSize
126143
) {
127-
if (currentInst.elementType) {
128-
if (
129-
// If the loop gets to the boundary itself, break.
130-
currentInst.elementType.displayName ===
131-
TouchEventBoundary.displayName
132-
) {
133-
break;
144+
if (
145+
// If the loop gets to the boundary itself, break.
146+
currentInst.elementType?.displayName ===
147+
TouchEventBoundary.displayName
148+
) {
149+
break;
150+
}
151+
152+
const props = currentInst.memoizedProps;
153+
const label =
154+
typeof props?.[PROP_KEY] !== "undefined"
155+
? `${props[PROP_KEY]}`
156+
: undefined;
157+
158+
// Check the label first
159+
if (label && !this._isNameIgnored(label)) {
160+
if (!activeLabel) {
161+
activeLabel = label;
134162
}
163+
componentTreeNames.push(label);
164+
} else if (
165+
typeof props?.accessibilityLabel === "string" &&
166+
!this._isNameIgnored(props.accessibilityLabel)
167+
) {
168+
if (!activeLabel) {
169+
activeLabel = props.accessibilityLabel;
170+
}
171+
componentTreeNames.push(props.accessibilityLabel);
172+
} else if (currentInst.elementType) {
173+
const { elementType } = currentInst;
135174

136175
if (
137-
typeof currentInst.elementType.displayName === "string" &&
138-
!this._isNameIgnored(currentInst.elementType.displayName)
176+
elementType.displayName &&
177+
!this._isNameIgnored(elementType.displayName)
139178
) {
140-
const { displayName } = currentInst.elementType;
141-
if (activeDisplayName === null) {
142-
activeDisplayName = displayName;
179+
// Check display name
180+
if (!activeDisplayName) {
181+
activeDisplayName = elementType.displayName;
143182
}
144-
componentTreeNames.push(displayName);
145-
} else if (
146-
typeof currentInst.elementType.name === "string" &&
147-
!this._isNameIgnored(currentInst.elementType.name)
148-
) {
149-
componentTreeNames.push(currentInst.elementType.name);
183+
componentTreeNames.push(elementType.displayName);
150184
}
151185
}
152186

153187
currentInst = currentInst.return;
154188
}
155189

156-
if (componentTreeNames.length > 0 || activeDisplayName) {
157-
this._logTouchEvent(componentTreeNames, activeDisplayName);
190+
const finalLabel = activeLabel ?? activeDisplayName;
191+
192+
if (componentTreeNames.length > 0 || finalLabel) {
193+
this._logTouchEvent(componentTreeNames, finalLabel);
158194
}
159195
}
160-
};
196+
}
161197
}
162198

163199
/**

test/touchevents.test.tsx

Lines changed: 30 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ afterEach(() => {
1010
});
1111

1212
describe("TouchEventBoundary._onTouchStart", () => {
13-
it("tree without displayName", () => {
13+
it("tree without displayName or label is not logged", () => {
1414
const { defaultProps } = TouchEventBoundary;
1515
const boundary = new TouchEventBoundary(defaultProps);
1616

@@ -40,25 +40,17 @@ describe("TouchEventBoundary._onTouchStart", () => {
4040
// @ts-ignore Calling private member
4141
boundary._onTouchStart(event);
4242

43-
expect(addBreadcrumb).toBeCalledWith({
44-
category: defaultProps.breadcrumbCategory,
45-
data: {
46-
componentTree: ["View", "Text", "CoolComponent", "Screen"],
47-
},
48-
level: Severity.Info,
49-
message: "Touch event within component tree",
50-
type: defaultProps.breadcrumbType,
51-
});
43+
expect(addBreadcrumb).not.toBeCalled();
5244
});
5345

54-
it("displayName is displayed", () => {
46+
it("label is preferred over accessibilityLabel and displayName", () => {
5547
const { defaultProps } = TouchEventBoundary;
5648
const boundary = new TouchEventBoundary(defaultProps);
5749

5850
const event = {
5951
_targetInst: {
6052
elementType: {
61-
name: "View",
53+
displayName: "View",
6254
},
6355
return: {
6456
elementType: {
@@ -68,6 +60,12 @@ describe("TouchEventBoundary._onTouchStart", () => {
6860
elementType: {
6961
displayName: "Connect(View)",
7062
},
63+
return: {
64+
memoizedProps: {
65+
"sentry-label": "LABEL!",
66+
accessibilityLabel: "access!",
67+
},
68+
},
7169
},
7270
},
7371
},
@@ -79,10 +77,10 @@ describe("TouchEventBoundary._onTouchStart", () => {
7977
expect(addBreadcrumb).toBeCalledWith({
8078
category: defaultProps.breadcrumbCategory,
8179
data: {
82-
componentTree: ["View", "Text", "Connect(View)"],
80+
componentTree: ["View", "Connect(View)", "LABEL!"],
8381
},
8482
level: Severity.Info,
85-
message: "Touch event within element: Connect(View)",
83+
message: "Touch event within element: LABEL!",
8684
type: defaultProps.breadcrumbType,
8785
});
8886
});
@@ -91,7 +89,7 @@ describe("TouchEventBoundary._onTouchStart", () => {
9189
const { defaultProps } = TouchEventBoundary;
9290
const boundary = new TouchEventBoundary({
9391
...defaultProps,
94-
ignoreNames: ["View", /^Connect\(/, new RegExp("^Happy\\(")],
92+
ignoreNames: ["View", "Ignore", /^Connect\(/, new RegExp("^Happy\\(")],
9593
});
9694

9795
const event = {
@@ -108,12 +106,21 @@ describe("TouchEventBoundary._onTouchStart", () => {
108106
displayName: "Connect(View)",
109107
},
110108
return: {
109+
memoizedProps: {
110+
"sentry-label": "Ignore",
111+
accessibilityLabel: "Ignore",
112+
},
111113
elementType: {
112-
displayName: "Styled(View)",
114+
displayName: "Styled(View2)",
113115
},
114116
return: {
115117
elementType: {
116-
displayName: "Happy(View)",
118+
displayName: "Styled(View)",
119+
},
120+
return: {
121+
elementType: {
122+
displayName: "Happy(View)",
123+
},
117124
},
118125
},
119126
},
@@ -128,10 +135,10 @@ describe("TouchEventBoundary._onTouchStart", () => {
128135
expect(addBreadcrumb).toBeCalledWith({
129136
category: defaultProps.breadcrumbCategory,
130137
data: {
131-
componentTree: ["Text", "Styled(View)"],
138+
componentTree: ["Styled(View2)", "Styled(View)"],
132139
},
133140
level: Severity.Info,
134-
message: "Touch event within element: Styled(View)",
141+
message: "Touch event within element: Styled(View2)",
135142
type: defaultProps.breadcrumbType,
136143
});
137144
});
@@ -140,7 +147,7 @@ describe("TouchEventBoundary._onTouchStart", () => {
140147
const { defaultProps } = TouchEventBoundary;
141148
const boundary = new TouchEventBoundary({
142149
...defaultProps,
143-
maxComponentTreeSize: 3,
150+
maxComponentTreeSize: 2,
144151
});
145152

146153
const event = {
@@ -153,8 +160,8 @@ describe("TouchEventBoundary._onTouchStart", () => {
153160
name: "Text",
154161
},
155162
return: {
156-
elementType: {
157-
displayName: "Connect(View)",
163+
memoizedProps: {
164+
accessibilityLabel: "Connect(View)",
158165
},
159166
return: {
160167
elementType: {
@@ -177,7 +184,7 @@ describe("TouchEventBoundary._onTouchStart", () => {
177184
expect(addBreadcrumb).toBeCalledWith({
178185
category: defaultProps.breadcrumbCategory,
179186
data: {
180-
componentTree: ["View", "Text", "Connect(View)"],
187+
componentTree: ["Connect(View)", "Styled(View)"],
181188
},
182189
level: Severity.Info,
183190
message: "Touch event within element: Connect(View)",

0 commit comments

Comments
 (0)