Skip to content

Commit b91c34b

Browse files
Merge 77e8028 into d31895e
2 parents d31895e + 77e8028 commit b91c34b

File tree

4 files changed

+141
-80
lines changed

4 files changed

+141
-80
lines changed

ios/RNSentryReplayBreadcrumbConverter.m

Lines changed: 67 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -30,39 +30,7 @@ - (instancetype _Nonnull)init {
3030
}
3131

3232
if ([breadcrumb.category isEqualToString:@"touch"]) {
33-
NSMutableString *message;
34-
if (breadcrumb.data) {
35-
NSMutableArray *path = [breadcrumb.data valueForKey:@"path"];
36-
if (path != nil) {
37-
message = [[NSMutableString alloc] init];
38-
for (NSInteger i = MIN(3, [path count] - 1); i >= 0; i--) {
39-
NSDictionary *item = [path objectAtIndex:i];
40-
[message appendString:[item objectForKey:@"name"]];
41-
if ([item objectForKey:@"element"] || [item objectForKey:@"file"]) {
42-
[message appendString:@"("];
43-
if ([item objectForKey:@"element"]) {
44-
[message appendString:[item objectForKey:@"element"]];
45-
if ([item objectForKey:@"file"]) {
46-
[message appendString:@", "];
47-
[message appendString:[item objectForKey:@"file"]];
48-
}
49-
} else if ([item objectForKey:@"file"]) {
50-
[message appendString:[item objectForKey:@"file"]];
51-
}
52-
[message appendString:@")"];
53-
}
54-
if (i > 0) {
55-
[message appendString:@" > "];
56-
}
57-
}
58-
}
59-
}
60-
return [SentrySessionReplayIntegration
61-
createBreadcrumbwithTimestamp:breadcrumb.timestamp
62-
category:@"ui.tap"
63-
message:message
64-
level:breadcrumb.level
65-
data:breadcrumb.data];
33+
return [self convertTouch:breadcrumb];
6634
}
6735

6836
if ([breadcrumb.category isEqualToString:@"navigation"]) {
@@ -93,6 +61,72 @@ - (instancetype _Nonnull)init {
9361
return nativeBreadcrumb;
9462
}
9563

64+
- (id<SentryRRWebEvent> _Nullable) convertTouch:(SentryBreadcrumb *_Nonnull)breadcrumb {
65+
if (breadcrumb.data == nil) {
66+
return nil;
67+
}
68+
69+
NSMutableArray *path = [breadcrumb.data valueForKey:@"path"];
70+
NSString* message = [self getMessageFrom:path];
71+
72+
return [SentrySessionReplayIntegration
73+
createBreadcrumbwithTimestamp:breadcrumb.timestamp
74+
category:@"ui.tap"
75+
message:message
76+
level:breadcrumb.level
77+
data:breadcrumb.data];
78+
}
79+
80+
- (NSString* _Nullable) getMessageFrom:(NSArray* _Nullable) path {
81+
if (path == nil) {
82+
return nil;
83+
}
84+
85+
NSInteger pathCount = [path count];
86+
if (pathCount <= 0) {
87+
return nil;
88+
}
89+
90+
NSMutableString *message = [[NSMutableString alloc] init];
91+
for (NSInteger i = MIN(3, pathCount - 1); i >= 0; i--) {
92+
NSDictionary *item = [path objectAtIndex:i];
93+
if (item == nil) {
94+
return nil; // There should be no nil (undefined) from JS, but to be safe we check it here
95+
}
96+
97+
id name = [item objectForKey:@"name"];
98+
id label = [item objectForKey:@"label"];
99+
BOOL hasName = [name isKindOfClass:[NSString class]];
100+
BOOL hasLabel = [label isKindOfClass:[NSString class]];
101+
if (!hasName && !hasLabel) {
102+
return nil; // This again should never be allowed in JS, but to be safe we check it here
103+
}
104+
if (hasLabel) {
105+
[message appendString:(NSString *)label];
106+
} else if (hasName) {
107+
[message appendString:(NSString *)name];
108+
}
109+
110+
id element = [item objectForKey:@"element"];
111+
id file = [item objectForKey:@"file"];
112+
BOOL hasElement = [element isKindOfClass:[NSString class]];
113+
BOOL hasFile = [file isKindOfClass:[NSString class]];
114+
if (hasElement && hasFile) {
115+
[message appendFormat:@"(%@, %@)", (NSString *)element, (NSString *)file];
116+
} else if (hasElement) {
117+
[message appendFormat:@"(%@)", (NSString *)element];
118+
} else if (hasFile) {
119+
[message appendFormat:@"(%@)", (NSString *)file];
120+
}
121+
122+
if (i > 0) {
123+
[message appendString:@" > "];
124+
}
125+
}
126+
127+
return message;
128+
}
129+
96130
- (id<SentryRRWebEvent> _Nullable)convertNavigation: (SentryBreadcrumb *_Nonnull)breadcrumb {
97131
NSNumber* startTimestamp = [breadcrumb.data[@"start_timestamp"] isKindOfClass:[NSNumber class]]
98132
? breadcrumb.data[@"start_timestamp"] : nil;

samples/react-native/src/App.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,13 @@ import { Provider } from 'react-redux';
2828
import { store } from './reduxApp';
2929
import { GestureHandlerRootView } from 'react-native-gesture-handler';
3030
import GesturesTracingScreen from './Screens/GesturesTracingScreen';
31-
import { Platform, StyleSheet, View } from 'react-native';
31+
import { LogBox, Platform, StyleSheet, View } from 'react-native';
3232
import { HttpClient } from '@sentry/integrations';
3333
import Ionicons from 'react-native-vector-icons/Ionicons';
3434
import PlaygroundScreen from './Screens/PlaygroundScreen';
3535

36+
LogBox.ignoreAllLogs();
37+
3638
const isMobileOs = Platform.OS === 'android' || Platform.OS === 'ios';
3739

3840
const reactNavigationInstrumentation =

src/js/touchevents.tsx

Lines changed: 56 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { addBreadcrumb, getCurrentHub } from '@sentry/core';
22
import type { SeverityLevel } from '@sentry/types';
3-
import { logger } from '@sentry/utils';
3+
import { dropUndefinedKeys, logger } from '@sentry/utils';
44
import * as React from 'react';
55
import type { GestureResponderEvent } from 'react-native';
66
import { StyleSheet, View } from 'react-native';
@@ -189,50 +189,7 @@ class TouchEventBoundary extends React.Component<TouchEventBoundaryProps> {
189189
break;
190190
}
191191

192-
const props = currentInst.memoizedProps ?? {};
193-
const info: TouchedComponentInfo = {};
194-
195-
// provided by @sentry/babel-plugin-component-annotate
196-
if (
197-
typeof props[SENTRY_COMPONENT_PROP_KEY] === 'string' &&
198-
props[SENTRY_COMPONENT_PROP_KEY].length > 0 &&
199-
props[SENTRY_COMPONENT_PROP_KEY] !== 'unknown'
200-
) {
201-
info.name = props[SENTRY_COMPONENT_PROP_KEY];
202-
}
203-
if (
204-
typeof props[SENTRY_ELEMENT_PROP_KEY] === 'string' &&
205-
props[SENTRY_ELEMENT_PROP_KEY].length > 0 &&
206-
props[SENTRY_ELEMENT_PROP_KEY] !== 'unknown'
207-
) {
208-
info.element = props[SENTRY_ELEMENT_PROP_KEY];
209-
}
210-
if (
211-
typeof props[SENTRY_FILE_PROP_KEY] === 'string' &&
212-
props[SENTRY_FILE_PROP_KEY].length > 0 &&
213-
props[SENTRY_FILE_PROP_KEY] !== 'unknown'
214-
) {
215-
info.file = props[SENTRY_FILE_PROP_KEY];
216-
}
217-
218-
// use custom label if provided by the user, or displayName if available
219-
const labelValue =
220-
typeof props[SENTRY_LABEL_PROP_KEY] === 'string'
221-
? props[SENTRY_LABEL_PROP_KEY]
222-
: // For some reason type narrowing doesn't work as expected with indexing when checking it all in one go in
223-
// the "check-label" if sentence, so we have to assign it to a variable here first
224-
typeof this.props.labelName === 'string'
225-
? props[this.props.labelName]
226-
: undefined;
227-
228-
if (typeof labelValue === 'string' && labelValue.length > 0) {
229-
info.label = labelValue;
230-
}
231-
232-
if (!info.name && currentInst.elementType?.displayName) {
233-
info.name = currentInst.elementType?.displayName;
234-
}
235-
192+
const info = getTouchedComponentInfo(currentInst, this.props.labelName);
236193
this._pushIfNotIgnored(touchPath, info);
237194

238195
currentInst = currentInst.return;
@@ -252,7 +209,11 @@ class TouchEventBoundary extends React.Component<TouchEventBoundaryProps> {
252209
/**
253210
* Pushes the name to the componentTreeNames array if it is not ignored.
254211
*/
255-
private _pushIfNotIgnored(touchPath: TouchedComponentInfo[], value: TouchedComponentInfo): boolean {
212+
private _pushIfNotIgnored(touchPath: TouchedComponentInfo[], value: TouchedComponentInfo | undefined): boolean {
213+
if (!value) {
214+
return false;
215+
}
216+
256217
if (!value.name && !value.label) {
257218
return false;
258219
}
@@ -273,6 +234,55 @@ class TouchEventBoundary extends React.Component<TouchEventBoundaryProps> {
273234
}
274235
}
275236

237+
function getTouchedComponentInfo(currentInst: ElementInstance, labelKey: string | undefined): TouchedComponentInfo | undefined {
238+
const props = currentInst.memoizedProps;
239+
if (!props) {
240+
// Early return if no props are available, as we can't extract any useful information
241+
return undefined;
242+
}
243+
244+
// provided by @sentry/babel-plugin-component-annotate
245+
const info: TouchedComponentInfo = {};
246+
info.name = getComponentName(props) || currentInst.elementType?.displayName;
247+
info.element = getElementName(props);
248+
info.file = getFileName(props);
249+
250+
// `sentry-label` or user defined label key
251+
info.label = getLabelValue(props, labelKey);
252+
return dropUndefinedKeys(info);
253+
}
254+
255+
function getComponentName(props: Record<string, unknown>): string | undefined {
256+
return typeof props[SENTRY_COMPONENT_PROP_KEY] === 'string' &&
257+
props[SENTRY_COMPONENT_PROP_KEY].length > 0 &&
258+
props[SENTRY_COMPONENT_PROP_KEY] !== 'unknown' &&
259+
props[SENTRY_COMPONENT_PROP_KEY] || undefined;
260+
}
261+
262+
function getElementName(props: Record<string, unknown>): string | undefined {
263+
return typeof props[SENTRY_ELEMENT_PROP_KEY] === 'string' &&
264+
props[SENTRY_ELEMENT_PROP_KEY].length > 0 &&
265+
props[SENTRY_ELEMENT_PROP_KEY] !== 'unknown' &&
266+
props[SENTRY_ELEMENT_PROP_KEY] || undefined;
267+
}
268+
269+
function getFileName(props: Record<string, unknown>): string | undefined {
270+
return typeof props[SENTRY_FILE_PROP_KEY] === 'string' &&
271+
props[SENTRY_FILE_PROP_KEY].length > 0 &&
272+
props[SENTRY_FILE_PROP_KEY] !== 'unknown' &&
273+
props[SENTRY_FILE_PROP_KEY] || undefined;
274+
}
275+
276+
function getLabelValue(props: Record<string, unknown>, labelKey: string | undefined): string | undefined {
277+
return typeof props[SENTRY_LABEL_PROP_KEY] === 'string' && props[SENTRY_LABEL_PROP_KEY].length > 0
278+
? props[SENTRY_LABEL_PROP_KEY] as string
279+
// For some reason type narrowing doesn't work as expected with indexing when checking it all in one go in
280+
// the "check-label" if sentence, so we have to assign it to a variable here first
281+
: typeof labelKey === 'string' && typeof props[labelKey] == 'string' && (props[labelKey] as string).length > 0
282+
? props[labelKey] as string
283+
: undefined;
284+
}
285+
276286
/**
277287
* Convenience Higher-Order-Component for TouchEventBoundary
278288
* @param WrappedComponent any React Component

test/touchevents.test.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,12 @@ describe('TouchEventBoundary._onTouchStart', () => {
101101
expect(addBreadcrumb).toBeCalledWith({
102102
category: defaultProps.breadcrumbCategory,
103103
data: {
104+
<<<<<<< HEAD
104105
path: [{ name: 'View' }, { name: 'Connect(View)' }, { label: 'LABEL!' }],
106+
=======
107+
componentTree: ['View', 'Connect(View)', 'LABEL!'],
108+
target: "LABEL!",
109+
>>>>>>> a9139c63 (fix touchevent tests)
105110
},
106111
level: 'info' as SeverityLevel,
107112
message: 'Touch event within element: LABEL!',
@@ -160,7 +165,12 @@ describe('TouchEventBoundary._onTouchStart', () => {
160165
expect(addBreadcrumb).toBeCalledWith({
161166
category: defaultProps.breadcrumbCategory,
162167
data: {
168+
<<<<<<< HEAD
163169
path: [{ name: 'Styled(View)' }],
170+
=======
171+
componentTree: ['Styled(View2)', 'Styled(View)'],
172+
target: 'Styled(View2)',
173+
>>>>>>> a9139c63 (fix touchevent tests)
164174
},
165175
level: 'info' as SeverityLevel,
166176
message: 'Touch event within element: Styled(View)',
@@ -210,7 +220,12 @@ describe('TouchEventBoundary._onTouchStart', () => {
210220
expect(addBreadcrumb).toBeCalledWith({
211221
category: defaultProps.breadcrumbCategory,
212222
data: {
223+
<<<<<<< HEAD
213224
path: [{ label: 'Connect(View)' }, { name: 'Styled(View)' }],
225+
=======
226+
componentTree: ['Connect(View)', 'Styled(View)'],
227+
target: 'Connect(View)',
228+
>>>>>>> a9139c63 (fix touchevent tests)
214229
},
215230
level: 'info' as SeverityLevel,
216231
message: 'Touch event within element: Connect(View)',

0 commit comments

Comments
 (0)