Skip to content

Commit 4a6664f

Browse files
vaindlucas-zimermankrystofwoldrich
authored
feat: touch breadcrumbs info improvements (#3899)
* cleanup & fix touchevent ignore behaviour * feat: support annotated components in touch evetns * feat: deduplicate subsequent events in the touch path * chore: add changelog * Update CHANGELOG.md Co-authored-by: LucasZF <lucas-zimerman1@hotmail.com> * Update CHANGELOG.md * review comments --------- Co-authored-by: LucasZF <lucas-zimerman1@hotmail.com> Co-authored-by: Krystof Woldrich <31292499+krystofwoldrich@users.noreply.github.com>
1 parent 11321ee commit 4a6664f

File tree

9 files changed

+207
-69
lines changed

9 files changed

+207
-69
lines changed

CHANGELOG.md

+7
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Changelog
22

3+
## Unreleased
4+
5+
### Features
6+
7+
- Improve touch event component info if annotated with [`@sentry/babel-plugin-component-annotate`](https://www.npmjs.com/package/@sentry/babel-plugin-component-annotate) ([#3899](https://github.com/getsentry/sentry-react-native/pull/3899))
8+
39
## 5.24.1
410

511
### Fixes
@@ -1033,6 +1039,7 @@ This has been fixed in [version `5.9.1`](https://github.com/getsentry/sentry-rea
10331039
## 5.4.0
10341040

10351041
### Features
1042+
10361043
- Add TS 4.1 typings ([#2995](https://github.com/getsentry/sentry-react-native/pull/2995))
10371044
- TS 3.8 are present and work automatically with older projects
10381045
- Add CPU Info to Device Context ([#2984](https://github.com/getsentry/sentry-react-native/pull/2984))

samples/expo/babel.config.js

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
module.exports = function(api) {
1+
const componentAnnotatePlugin = require('@sentry/babel-plugin-component-annotate');
2+
3+
module.exports = function (api) {
24
api.cache(false);
35
return {
46
presets: ['babel-preset-expo'],
@@ -11,6 +13,7 @@ module.exports = function(api) {
1113
},
1214
},
1315
],
16+
componentAnnotatePlugin,
1417
],
1518
};
1619
};

samples/expo/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
"devDependencies": {
3434
"@babel/core": "^7.20.0",
3535
"@babel/preset-env": "7.1.6",
36+
"@sentry/babel-plugin-component-annotate": "^2.18.0",
3637
"@types/node": "20.10.4"
3738
},
3839
"overrides": {

samples/expo/yarn.lock

+5
Original file line numberDiff line numberDiff line change
@@ -2413,6 +2413,11 @@
24132413
component-type "^1.2.1"
24142414
join-component "^1.1.0"
24152415

2416+
"@sentry/babel-plugin-component-annotate@^2.18.0":
2417+
version "2.18.0"
2418+
resolved "https://registry.yarnpkg.com/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-2.18.0.tgz#3bee98f94945643b0762ceed1f6cca60db52bdbd"
2419+
integrity sha512-9L4RbhS3WNtc/SokIhc0dwgcvs78YSQPakZejsrIgnzLzCi8mS6PeT+BY0+QCtsXxjd1egM8hqcJeB0lukBkXA==
2420+
24162421
"@sideway/address@^4.1.3":
24172422
version "4.1.4"
24182423
resolved "https://registry.yarnpkg.com/@sideway/address/-/address-4.1.4.tgz#03dccebc6ea47fdc226f7d3d1ad512955d4783f0"

samples/react-native/babel.config.js

+3
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
const componentAnnotatePlugin = require('@sentry/babel-plugin-component-annotate');
2+
13
module.exports = {
24
presets: ['module:@react-native/babel-preset'],
35
plugins: [
@@ -9,5 +11,6 @@ module.exports = {
911
},
1012
},
1113
],
14+
componentAnnotatePlugin,
1215
],
1316
};

samples/react-native/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
"@react-native/eslint-config": "^0.73.1",
4444
"@react-native/metro-config": "^0.73.1",
4545
"@react-native/typescript-config": "^0.73.1",
46+
"@sentry/babel-plugin-component-annotate": "^2.18.0",
4647
"@types/react": "^18.2.65",
4748
"@types/react-native-vector-icons": "^6.4.18",
4849
"@types/react-test-renderer": "^18.0.0",

samples/react-native/yarn.lock

+5
Original file line numberDiff line numberDiff line change
@@ -3127,6 +3127,11 @@
31273127
color "^4.2.3"
31283128
warn-once "^0.1.0"
31293129

3130+
"@sentry/babel-plugin-component-annotate@^2.18.0":
3131+
version "2.18.0"
3132+
resolved "https://registry.yarnpkg.com/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-2.18.0.tgz#3bee98f94945643b0762ceed1f6cca60db52bdbd"
3133+
integrity sha512-9L4RbhS3WNtc/SokIhc0dwgcvs78YSQPakZejsrIgnzLzCi8mS6PeT+BY0+QCtsXxjd1egM8hqcJeB0lukBkXA==
3134+
31303135
"@sideway/address@^4.1.3":
31313136
version "4.1.4"
31323137
resolved "https://registry.yarnpkg.com/@sideway/address/-/address-4.1.4.tgz#03dccebc6ea47fdc226f7d3d1ad512955d4783f0"

src/js/touchevents.tsx

+81-62
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { addBreadcrumb, getCurrentHub } from '@sentry/core';
22
import type { SeverityLevel } from '@sentry/types';
33
import { logger } from '@sentry/utils';
44
import * as React from 'react';
5-
import type { GestureResponderEvent} from 'react-native';
5+
import type { GestureResponderEvent } from 'react-native';
66
import { StyleSheet, View } from 'react-native';
77

88
import { createIntegration } from './integrations/factory';
@@ -53,6 +53,9 @@ const DEFAULT_BREADCRUMB_TYPE = 'user';
5353
const DEFAULT_MAX_COMPONENT_TREE_SIZE = 20;
5454

5555
const SENTRY_LABEL_PROP_KEY = 'sentry-label';
56+
const SENTRY_COMPONENT_PROP_KEY = 'data-sentry-component';
57+
const SENTRY_ELEMENT_PROP_KEY = 'data-sentry-element';
58+
const SENTRY_FILE_PROP_KEY = 'data-sentry-source-file';
5659

5760
interface ElementInstance {
5861
elementType?: {
@@ -63,6 +66,13 @@ interface ElementInstance {
6366
return?: ElementInstance;
6467
}
6568

69+
interface TouchedComponentInfo {
70+
name?: string;
71+
label?: string;
72+
element?: string;
73+
file?: string;
74+
}
75+
6676
interface PrivateGestureResponderEvent extends GestureResponderEvent {
6777
_targetInst?: ElementInstance;
6878
}
@@ -71,7 +81,6 @@ interface PrivateGestureResponderEvent extends GestureResponderEvent {
7181
* Boundary to log breadcrumbs for interaction events.
7282
*/
7383
class TouchEventBoundary extends React.Component<TouchEventBoundaryProps> {
74-
7584
public static displayName: string = '__Sentry.TouchEventBoundary';
7685
public static defaultProps: Partial<TouchEventBoundaryProps> = {
7786
breadcrumbCategory: DEFAULT_BREADCRUMB_CATEGORY,
@@ -113,18 +122,17 @@ class TouchEventBoundary extends React.Component<TouchEventBoundaryProps> {
113122
/**
114123
* Logs the touch event given the component tree names and a label.
115124
*/
116-
private _logTouchEvent(
117-
componentTreeNames: string[],
118-
activeLabel?: string
119-
): void {
125+
private _logTouchEvent(touchPath: TouchedComponentInfo[], label?: string): void {
120126
const level = 'info' as SeverityLevel;
127+
128+
const root = touchPath[0];
129+
const detail = label ? label : `${root.name}${root.file ? ` (${root.file})` : ''}`;
130+
121131
const crumb = {
122132
category: this.props.breadcrumbCategory,
123-
data: { componentTree: componentTreeNames },
133+
data: { path: touchPath },
124134
level: level,
125-
message: activeLabel
126-
? `Touch event within element: ${activeLabel}`
127-
: 'Touch event within component tree',
135+
message: `Touch event within element: ${detail}`,
128136
type: this.props.breadcrumbType,
129137
};
130138
addBreadcrumb(crumb);
@@ -147,7 +155,7 @@ class TouchEventBoundary extends React.Component<TouchEventBoundaryProps> {
147155
return ignoreNames.some(
148156
(ignoreName: string | RegExp) =>
149157
(typeof ignoreName === 'string' && name === ignoreName) ||
150-
(ignoreName instanceof RegExp && name.match(ignoreName))
158+
(ignoreName instanceof RegExp && name.match(ignoreName)),
151159
);
152160
}
153161

@@ -166,80 +174,91 @@ class TouchEventBoundary extends React.Component<TouchEventBoundaryProps> {
166174
}
167175

168176
let currentInst: ElementInstance | undefined = e._targetInst;
169-
170-
let activeLabel: string | undefined;
171-
let activeDisplayName: string | undefined;
172-
const componentTreeNames: string[] = [];
177+
const touchPath: TouchedComponentInfo[] = [];
173178

174179
while (
175180
currentInst &&
176181
// maxComponentTreeSize will always be defined as we have a defaultProps. But ts needs a check so this is here.
177182
this.props.maxComponentTreeSize &&
178-
componentTreeNames.length < this.props.maxComponentTreeSize
183+
touchPath.length < this.props.maxComponentTreeSize
179184
) {
180185
if (
181186
// If the loop gets to the boundary itself, break.
182-
currentInst.elementType?.displayName ===
183-
TouchEventBoundary.displayName
187+
currentInst.elementType?.displayName === TouchEventBoundary.displayName
184188
) {
185189
break;
186190
}
187191

188-
const props = currentInst.memoizedProps;
189-
const sentryLabel =
190-
typeof props?.[SENTRY_LABEL_PROP_KEY] !== 'undefined'
191-
? `${props[SENTRY_LABEL_PROP_KEY]}`
192+
const props = currentInst.memoizedProps ?? {};
193+
const info: TouchedComponentInfo = {};
194+
195+
// provided by @sentry/babel-plugin-component-annotate
196+
if (typeof props[SENTRY_COMPONENT_PROP_KEY] === 'string' && props[SENTRY_COMPONENT_PROP_KEY].length > 0 && props[SENTRY_COMPONENT_PROP_KEY] !== 'unknown') {
197+
info.name = props[SENTRY_COMPONENT_PROP_KEY];
198+
}
199+
if (typeof props[SENTRY_ELEMENT_PROP_KEY] === 'string' && props[SENTRY_ELEMENT_PROP_KEY].length > 0 && props[SENTRY_ELEMENT_PROP_KEY] !== 'unknown') {
200+
info.element = props[SENTRY_ELEMENT_PROP_KEY];
201+
}
202+
if (typeof props[SENTRY_FILE_PROP_KEY] === 'string' && props[SENTRY_FILE_PROP_KEY].length > 0 && props[SENTRY_FILE_PROP_KEY] !== 'unknown') {
203+
info.file = props[SENTRY_FILE_PROP_KEY];
204+
}
205+
206+
// use custom label if provided by the user, or displayName if available
207+
const labelValue =
208+
typeof props[SENTRY_LABEL_PROP_KEY] === 'string'
209+
? props[SENTRY_LABEL_PROP_KEY]
210+
: // For some reason type narrowing doesn't work as expected with indexing when checking it all in one go in
211+
// the "check-label" if sentence, so we have to assign it to a variable here first
212+
typeof this.props.labelName === 'string'
213+
? props[this.props.labelName]
192214
: undefined;
193215

194-
// For some reason type narrowing doesn't work as expected with indexing when checking it all in one go in
195-
// the "check-label" if sentence, so we have to assign it to a variable here first
196-
let labelValue;
197-
if (typeof this.props.labelName === 'string')
198-
labelValue = props?.[this.props.labelName];
199-
200-
// Check the label first
201-
if (sentryLabel && !this._isNameIgnored(sentryLabel)) {
202-
if (!activeLabel) {
203-
activeLabel = sentryLabel;
204-
}
205-
componentTreeNames.push(sentryLabel);
206-
} else if (
207-
typeof labelValue === 'string' &&
208-
!this._isNameIgnored(labelValue)
209-
) {
210-
if (!activeLabel) {
211-
activeLabel = labelValue;
212-
}
213-
componentTreeNames.push(labelValue);
214-
} else if (currentInst.elementType) {
215-
const { elementType } = currentInst;
216-
217-
if (
218-
elementType.displayName &&
219-
!this._isNameIgnored(elementType.displayName)
220-
) {
221-
// Check display name
222-
if (!activeDisplayName) {
223-
activeDisplayName = elementType.displayName;
224-
}
225-
componentTreeNames.push(elementType.displayName);
226-
}
216+
if (typeof labelValue === 'string' && labelValue.length > 0) {
217+
info.label = labelValue;
218+
}
219+
220+
if (!info.name && currentInst.elementType?.displayName) {
221+
info.name = currentInst.elementType?.displayName;
227222
}
228223

224+
this._pushIfNotIgnored(touchPath, info);
225+
229226
currentInst = currentInst.return;
230227
}
231228

232-
const finalLabel = activeLabel ?? activeDisplayName;
233-
234-
if (componentTreeNames.length > 0 || finalLabel) {
235-
this._logTouchEvent(componentTreeNames, finalLabel);
229+
const label = touchPath.find(info => info.label)?.label;
230+
if (touchPath.length > 0) {
231+
this._logTouchEvent(touchPath, label);
236232
}
237233

238234
this._tracingIntegration?.startUserInteractionTransaction({
239-
elementId: activeLabel,
235+
elementId: label,
240236
op: UI_ACTION_TOUCH,
241237
});
242238
}
239+
240+
/**
241+
* Pushes the name to the componentTreeNames array if it is not ignored.
242+
*/
243+
private _pushIfNotIgnored(touchPath: TouchedComponentInfo[], value: TouchedComponentInfo): boolean {
244+
if (!value.name && !value.label) {
245+
return false;
246+
}
247+
if (value.name && this._isNameIgnored(value.name)) {
248+
return false;
249+
}
250+
if (value.label && this._isNameIgnored(value.label)) {
251+
return false;
252+
}
253+
254+
// Deduplicate same subsequent items.
255+
if (touchPath.length > 0 && JSON.stringify(touchPath[touchPath.length - 1]) === JSON.stringify(value)) {
256+
return false;
257+
}
258+
259+
touchPath.push(value);
260+
return true;
261+
}
243262
}
244263

245264
/**
@@ -250,9 +269,9 @@ class TouchEventBoundary extends React.Component<TouchEventBoundaryProps> {
250269
const withTouchEventBoundary = (
251270
// eslint-disable-next-line @typescript-eslint/no-explicit-any
252271
InnerComponent: React.ComponentType<any>,
253-
boundaryProps?: TouchEventBoundaryProps
272+
boundaryProps?: TouchEventBoundaryProps,
254273
): React.FunctionComponent => {
255-
const WrappedComponent: React.FunctionComponent = (props) => (
274+
const WrappedComponent: React.FunctionComponent = props => (
256275
<TouchEventBoundary {...(boundaryProps ?? {})}>
257276
<InnerComponent {...props} />
258277
</TouchEventBoundary>

0 commit comments

Comments
 (0)