From 5f3a5c8e05be7f7454edaa972d71f423591ad3ba Mon Sep 17 00:00:00 2001 From: Krystof Woldrich <31292499+krystofwoldrich@users.noreply.github.com> Date: Tue, 2 Jul 2024 10:04:09 +0200 Subject: [PATCH] fix(replay): Add tests for touch events (#3924) --- .../RNSentryReplayBreadcrumbConverterTest.kt | 55 +++++++++ .../project.pbxproj | 17 +++ ...RNSentryCocoaTesterTests-Bridging-Header.h | 5 + ...SentryReplayBreadcrumbConverterTests.swift | 47 ++++++++ .../RNSentryReplayBreadcrumbConverter.java | 92 ++++++++++----- ios/RNSentryReplayBreadcrumbConverter.h | 2 + ios/RNSentryReplayBreadcrumbConverter.m | 100 ++++++++++------ samples/react-native/src/App.tsx | 4 +- src/js/touchevents.tsx | 109 ++++++++++-------- 9 files changed, 324 insertions(+), 107 deletions(-) create mode 100644 RNSentryAndroidTester/app/src/test/java/io/sentry/rnsentryandroidtester/RNSentryReplayBreadcrumbConverterTest.kt create mode 100644 RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryCocoaTesterTests-Bridging-Header.h create mode 100644 RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryReplayBreadcrumbConverterTests.swift diff --git a/RNSentryAndroidTester/app/src/test/java/io/sentry/rnsentryandroidtester/RNSentryReplayBreadcrumbConverterTest.kt b/RNSentryAndroidTester/app/src/test/java/io/sentry/rnsentryandroidtester/RNSentryReplayBreadcrumbConverterTest.kt new file mode 100644 index 0000000000..7b3c6b8ac1 --- /dev/null +++ b/RNSentryAndroidTester/app/src/test/java/io/sentry/rnsentryandroidtester/RNSentryReplayBreadcrumbConverterTest.kt @@ -0,0 +1,55 @@ +package io.sentry.rnsentryandroidtester + +import io.sentry.react.RNSentryReplayBreadcrumbConverter +import org.junit.Assert.assertEquals +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@RunWith(JUnit4::class) +class RNSentryReplayBreadcrumbConverterTest { + + @Test + fun doesNotConvertNullPath() { + val actual = RNSentryReplayBreadcrumbConverter.getTouchPathMessage(null) + assertEquals(null, actual) + } + + @Test + fun doesNotConvertPathContainingNull() { + val actual = RNSentryReplayBreadcrumbConverter.getTouchPathMessage(arrayListOf(arrayOfNulls(1))) + assertEquals(null, actual) + } + + @Test + fun doesNotConvertPathWithValuesMissingNameAndLevel() { + val actual = RNSentryReplayBreadcrumbConverter.getTouchPathMessage(arrayListOf(mapOf( + "element" to "element4", + "file" to "file4"))) + assertEquals(null, actual) + } + + @Test + fun doesConvertValidPathExample1() { + val actual = RNSentryReplayBreadcrumbConverter.getTouchPathMessage(listOf( + mapOf("label" to "label0"), + mapOf("name" to "name1"), + mapOf("name" to "item2", "label" to "label2"), + mapOf("name" to "item3", "label" to "label3", "element" to "element3"), + mapOf("name" to "item4", "label" to "label4", "file" to "file4"), + mapOf("name" to "item5", "label" to "label5", "element" to "element5", "file" to "file5"))) + assertEquals("label3(element3) > label2 > name1 > label0", actual) + } + + @Test + fun doesConvertValidPathExample2() { + val actual = RNSentryReplayBreadcrumbConverter.getTouchPathMessage(listOf( + mapOf("name" to "item2", "label" to "label2"), + mapOf("name" to "item3", "label" to "label3", "element" to "element3"), + mapOf("name" to "item4", "label" to "label4", "file" to "file4"), + mapOf("name" to "item5", "label" to "label5", "element" to "element5", "file" to "file5"), + mapOf("label" to "label6"), + mapOf("name" to "name7"))) + assertEquals("label5(element5, file5) > label4(file4) > label3(element3) > label2", actual) + } +} \ No newline at end of file diff --git a/RNSentryCocoaTester/RNSentryCocoaTester.xcodeproj/project.pbxproj b/RNSentryCocoaTester/RNSentryCocoaTester.xcodeproj/project.pbxproj index a5f58926c2..6b29d2af2e 100644 --- a/RNSentryCocoaTester/RNSentryCocoaTester.xcodeproj/project.pbxproj +++ b/RNSentryCocoaTester/RNSentryCocoaTester.xcodeproj/project.pbxproj @@ -8,6 +8,7 @@ /* Begin PBXBuildFile section */ 330F308C2C0F3840002A0D4E /* RNSentryBreadcrumbTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 330F308B2C0F3840002A0D4E /* RNSentryBreadcrumbTests.m */; }; + 336084392C32E382008CC412 /* RNSentryReplayBreadcrumbConverterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 336084382C32E382008CC412 /* RNSentryReplayBreadcrumbConverterTests.swift */; }; 33958C692BFCF12600AD1FB6 /* RNSentryOnDrawReporterTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 33958C682BFCF12600AD1FB6 /* RNSentryOnDrawReporterTests.m */; }; 33AFDFED2B8D14B300AAB120 /* RNSentryFramesTrackerListenerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 33AFDFEC2B8D14B300AAB120 /* RNSentryFramesTrackerListenerTests.m */; }; 33AFDFF12B8D15E500AAB120 /* RNSentryDependencyContainerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 33AFDFF02B8D15E500AAB120 /* RNSentryDependencyContainerTests.m */; }; @@ -19,6 +20,9 @@ 1482D5685A340AB93348A43D /* Pods-RNSentryCocoaTesterTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RNSentryCocoaTesterTests.release.xcconfig"; path = "Target Support Files/Pods-RNSentryCocoaTesterTests/Pods-RNSentryCocoaTesterTests.release.xcconfig"; sourceTree = ""; }; 330F308B2C0F3840002A0D4E /* RNSentryBreadcrumbTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RNSentryBreadcrumbTests.m; sourceTree = ""; }; 330F308D2C0F385A002A0D4E /* RNSentryBreadcrumb.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RNSentryBreadcrumb.h; path = ../ios/RNSentryBreadcrumb.h; sourceTree = ""; }; + 336084372C32E382008CC412 /* RNSentryCocoaTesterTests-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "RNSentryCocoaTesterTests-Bridging-Header.h"; sourceTree = ""; }; + 336084382C32E382008CC412 /* RNSentryReplayBreadcrumbConverterTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RNSentryReplayBreadcrumbConverterTests.swift; sourceTree = ""; }; + 3360843A2C32E3A8008CC412 /* RNSentryReplayBreadcrumbConverter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RNSentryReplayBreadcrumbConverter.h; path = ../ios/RNSentryReplayBreadcrumbConverter.h; sourceTree = ""; }; 3360898D29524164007C7730 /* RNSentryCocoaTesterTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RNSentryCocoaTesterTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 338739072A7D7D2800950DDD /* RNSentryTests.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RNSentryTests.h; sourceTree = ""; }; 33958C672BFCEF5A00AD1FB6 /* RNSentryOnDrawReporter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RNSentryOnDrawReporter.h; path = ../ios/RNSentryOnDrawReporter.h; sourceTree = ""; }; @@ -76,6 +80,7 @@ 3360899029524164007C7730 /* RNSentryCocoaTesterTests */ = { isa = PBXGroup; children = ( + 336084382C32E382008CC412 /* RNSentryReplayBreadcrumbConverterTests.swift */, 33F58ACF2977037D008F60EA /* RNSentryTests.mm */, 338739072A7D7D2800950DDD /* RNSentryTests.h */, 33AFDFEC2B8D14B300AAB120 /* RNSentryFramesTrackerListenerTests.m */, @@ -84,6 +89,7 @@ 33AFDFF22B8D15F600AAB120 /* RNSentryDependencyContainerTests.h */, 33958C682BFCF12600AD1FB6 /* RNSentryOnDrawReporterTests.m */, 330F308B2C0F3840002A0D4E /* RNSentryBreadcrumbTests.m */, + 336084372C32E382008CC412 /* RNSentryCocoaTesterTests-Bridging-Header.h */, ); path = RNSentryCocoaTesterTests; sourceTree = ""; @@ -91,6 +97,7 @@ 33AFE0122B8F319000AAB120 /* RNSentry */ = { isa = PBXGroup; children = ( + 3360843A2C32E3A8008CC412 /* RNSentryReplayBreadcrumbConverter.h */, 330F308D2C0F385A002A0D4E /* RNSentryBreadcrumb.h */, 33958C672BFCEF5A00AD1FB6 /* RNSentryOnDrawReporter.h */, 33AFE0132B8F31AF00AAB120 /* RNSentryDependencyContainer.h */, @@ -134,10 +141,12 @@ isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1540; LastUpgradeCheck = 1420; TargetAttributes = { 3360898C29524164007C7730 = { CreatedOnToolsVersion = 14.2; + LastSwiftMigration = 1540; }; }; }; @@ -207,6 +216,7 @@ buildActionMask = 2147483647; files = ( 33AFDFF12B8D15E500AAB120 /* RNSentryDependencyContainerTests.m in Sources */, + 336084392C32E382008CC412 /* RNSentryReplayBreadcrumbConverterTests.swift in Sources */, 33F58AD02977037D008F60EA /* RNSentryTests.mm in Sources */, 33958C692BFCF12600AD1FB6 /* RNSentryOnDrawReporterTests.m in Sources */, 330F308C2C0F3840002A0D4E /* RNSentryBreadcrumbTests.m in Sources */, @@ -333,6 +343,7 @@ isa = XCBuildConfiguration; baseConfigurationReference = E2321E7CFA55AB617247098E /* Pods-RNSentryCocoaTesterTests.debug.xcconfig */; buildSettings = { + CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; @@ -387,6 +398,9 @@ SUPPORTS_MACCATALYST = NO; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_OBJC_BRIDGING_HEADER = "RNSentryCocoaTesterTests/RNSentryCocoaTesterTests-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = 1; }; name = Debug; @@ -395,6 +409,7 @@ isa = XCBuildConfiguration; baseConfigurationReference = 1482D5685A340AB93348A43D /* Pods-RNSentryCocoaTesterTests.release.xcconfig */; buildSettings = { + CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; @@ -449,6 +464,8 @@ SUPPORTS_MACCATALYST = NO; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_OBJC_BRIDGING_HEADER = "RNSentryCocoaTesterTests/RNSentryCocoaTesterTests-Bridging-Header.h"; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = 1; }; name = Release; diff --git a/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryCocoaTesterTests-Bridging-Header.h b/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryCocoaTesterTests-Bridging-Header.h new file mode 100644 index 0000000000..a95330baa6 --- /dev/null +++ b/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryCocoaTesterTests-Bridging-Header.h @@ -0,0 +1,5 @@ +// +// Use this file to import your target's public headers that you would like to expose to Swift. +// + +#import "RNSentryReplayBreadcrumbConverter.h" diff --git a/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryReplayBreadcrumbConverterTests.swift b/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryReplayBreadcrumbConverterTests.swift new file mode 100644 index 0000000000..09b94b2bca --- /dev/null +++ b/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryReplayBreadcrumbConverterTests.swift @@ -0,0 +1,47 @@ +import XCTest + +final class RNSentryReplayBreadcrumbConverterTests: XCTestCase { + + func testTouchMessageReturnsNilOnEmptyArray() throws { + let actual = RNSentryReplayBreadcrumbConverter.getTouchPathMessage(from: []) + XCTAssertEqual(actual, nil); + } + + func testTouchMessageReturnsNilOnNilArray() throws { + let actual = RNSentryReplayBreadcrumbConverter.getTouchPathMessage(from: nil as [Any]?) + XCTAssertEqual(actual, nil); + } + + func testTouchMessageReturnsNilOnMissingNameAndLevel() throws { + let testPath: [Any?] = [["element": "element4", "file": "file4"]] + let actual = RNSentryReplayBreadcrumbConverter.getTouchPathMessage(from: testPath as [Any]) + XCTAssertEqual(actual, nil); + } + + func testTouchMessageReturnsMessageOnValidPathExample1() throws { + let testPath: [Any?] = [ + ["label": "label0"], + ["name": "name1"], + ["name": "item2", "label": "label2"], + ["name": "item3", "label": "label3", "element": "element3"], + ["name": "item4", "label": "label4", "file": "file4"], + ["name": "item5", "label": "label5", "element": "element5", "file": "file5"], + ] + let actual = RNSentryReplayBreadcrumbConverter.getTouchPathMessage(from: testPath as [Any]) + XCTAssertEqual(actual, "label3(element3) > label2 > name1 > label0"); + } + + func testTouchMessageReturnsMessageOnValidPathExample2() throws { + let testPath: [Any?] = [ + ["name": "item2", "label": "label2"], + ["name": "item3", "label": "label3", "element": "element3"], + ["name": "item4", "label": "label4", "file": "file4"], + ["name": "item5", "label": "label5", "element": "element5", "file": "file5"], + ["label": "label6"], + ["name": "name7"], + ] + let actual = RNSentryReplayBreadcrumbConverter.getTouchPathMessage(from: testPath as [Any]) + XCTAssertEqual(actual, "label5(element5, file5) > label4(file4) > label3(element3) > label2"); + } + +} diff --git a/android/src/main/java/io/sentry/react/RNSentryReplayBreadcrumbConverter.java b/android/src/main/java/io/sentry/react/RNSentryReplayBreadcrumbConverter.java index be6d62783c..36b3b9c57a 100644 --- a/android/src/main/java/io/sentry/react/RNSentryReplayBreadcrumbConverter.java +++ b/android/src/main/java/io/sentry/react/RNSentryReplayBreadcrumbConverter.java @@ -6,13 +6,13 @@ import io.sentry.rrweb.RRWebBreadcrumbEvent; import io.sentry.rrweb.RRWebSpanEvent; -import java.util.ArrayList; import java.util.HashMap; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.TestOnly; -import java.util.HashMap; +import java.util.List; +import java.util.Map; public final class RNSentryReplayBreadcrumbConverter extends DefaultReplayBreadcrumbConverter { public RNSentryReplayBreadcrumbConverter() { @@ -59,31 +59,9 @@ public RNSentryReplayBreadcrumbConverter() { final RRWebBreadcrumbEvent rrWebBreadcrumb = new RRWebBreadcrumbEvent(); rrWebBreadcrumb.setCategory("ui.tap"); - ArrayList path = (ArrayList) breadcrumb.getData("path"); - if (path != null) { - StringBuilder message = new StringBuilder(); - for (int i = Math.min(3, path.size()); i >= 0; i--) { - HashMap item = (HashMap) path.get(i); - message.append(item.get("name")); - if (item.containsKey("element") || item.containsKey("file")) { - message.append('('); - if (item.containsKey("element")) { - message.append(item.get("element")); - if (item.containsKey("file")) { - message.append(", "); - message.append(item.get("file")); - } - } else if (item.containsKey("file")) { - message.append(item.get("file")); - } - message.append(')'); - } - if (i > 0) { - message.append(" > "); - } - } - rrWebBreadcrumb.setMessage(message.toString()); - } + + rrWebBreadcrumb.setMessage(RNSentryReplayBreadcrumbConverter + .getTouchPathMessage(breadcrumb.getData("path"))); rrWebBreadcrumb.setLevel(breadcrumb.getLevel()); rrWebBreadcrumb.setData(breadcrumb.getData()); @@ -93,6 +71,66 @@ public RNSentryReplayBreadcrumbConverter() { return rrWebBreadcrumb; } + @TestOnly + public static @Nullable String getTouchPathMessage(final @Nullable Object maybePath) { + if (!(maybePath instanceof List)) { + return null; + } + + final @NotNull List path = (List) maybePath; + if (path.size() == 0) { + return null; + } + + final @NotNull StringBuilder message = new StringBuilder(); + for (int i = Math.min(3, path.size() - 1); i >= 0; i--) { + final @Nullable Object maybeItem = path.get(i); + if (!(maybeItem instanceof Map)) { + return null; + } + + final @NotNull Map item = (Map) maybeItem; + final @Nullable Object maybeName = item.get("name"); + final @Nullable Object maybeLabel = item.get("label"); + boolean hasName = maybeName instanceof String; + boolean hasLabel = maybeLabel instanceof String; + if (!hasName && !hasLabel) { + return null; // This again should never be allowed in JS, but to be safe we check it here + } + if (hasLabel) { + message.append(maybeLabel); + } else { // hasName is true + message.append(maybeName); + } + + final @Nullable Object maybeElement = item.get("element"); + final @Nullable Object maybeFile = item.get("file"); + boolean hasElement = maybeElement instanceof String; + boolean hasFile = maybeFile instanceof String; + if (hasElement && hasFile) { + message.append('(') + .append(maybeElement) + .append(", ") + .append(maybeFile) + .append(')'); + } else if (hasElement) { + message.append('(') + .append(maybeElement) + .append(')'); + } else if (hasFile) { + message.append('(') + .append(maybeFile) + .append(')'); + } + + if (i > 0) { + message.append(" > "); + } + } + + return message.toString(); + } + @TestOnly public @Nullable RRWebEvent convertNetworkBreadcrumb(final @NotNull Breadcrumb breadcrumb) { final Double startTimestamp = breadcrumb.getData("start_timestamp") instanceof Number diff --git a/ios/RNSentryReplayBreadcrumbConverter.h b/ios/RNSentryReplayBreadcrumbConverter.h index 98030b67b4..92a2451cca 100644 --- a/ios/RNSentryReplayBreadcrumbConverter.h +++ b/ios/RNSentryReplayBreadcrumbConverter.h @@ -8,5 +8,7 @@ - (instancetype _Nonnull)init; ++ (NSString* _Nullable) getTouchPathMessageFrom:(NSArray* _Nullable) path; + @end #endif diff --git a/ios/RNSentryReplayBreadcrumbConverter.m b/ios/RNSentryReplayBreadcrumbConverter.m index 7963824241..17be5509cd 100644 --- a/ios/RNSentryReplayBreadcrumbConverter.m +++ b/ios/RNSentryReplayBreadcrumbConverter.m @@ -30,39 +30,7 @@ - (instancetype _Nonnull)init { } if ([breadcrumb.category isEqualToString:@"touch"]) { - NSMutableString *message; - if (breadcrumb.data) { - NSMutableArray *path = [breadcrumb.data valueForKey:@"path"]; - if (path != nil) { - message = [[NSMutableString alloc] init]; - for (NSInteger i = MIN(3, [path count] - 1); i >= 0; i--) { - NSDictionary *item = [path objectAtIndex:i]; - [message appendString:[item objectForKey:@"name"]]; - if ([item objectForKey:@"element"] || [item objectForKey:@"file"]) { - [message appendString:@"("]; - if ([item objectForKey:@"element"]) { - [message appendString:[item objectForKey:@"element"]]; - if ([item objectForKey:@"file"]) { - [message appendString:@", "]; - [message appendString:[item objectForKey:@"file"]]; - } - } else if ([item objectForKey:@"file"]) { - [message appendString:[item objectForKey:@"file"]]; - } - [message appendString:@")"]; - } - if (i > 0) { - [message appendString:@" > "]; - } - } - } - } - return [SentrySessionReplayIntegration - createBreadcrumbwithTimestamp:breadcrumb.timestamp - category:@"ui.tap" - message:message - level:breadcrumb.level - data:breadcrumb.data]; + return [self convertTouch:breadcrumb]; } if ([breadcrumb.category isEqualToString:@"navigation"]) { @@ -93,6 +61,72 @@ - (instancetype _Nonnull)init { return nativeBreadcrumb; } +- (id _Nullable) convertTouch:(SentryBreadcrumb *_Nonnull)breadcrumb { + if (breadcrumb.data == nil) { + return nil; + } + + NSMutableArray *path = [breadcrumb.data valueForKey:@"path"]; + NSString* message = [RNSentryReplayBreadcrumbConverter getTouchPathMessageFrom:path]; + + return [SentrySessionReplayIntegration + createBreadcrumbwithTimestamp:breadcrumb.timestamp + category:@"ui.tap" + message:message + level:breadcrumb.level + data:breadcrumb.data]; +} + ++ (NSString* _Nullable) getTouchPathMessageFrom:(NSArray* _Nullable) path { + if (path == nil) { + return nil; + } + + NSInteger pathCount = [path count]; + if (pathCount <= 0) { + return nil; + } + + NSMutableString *message = [[NSMutableString alloc] init]; + for (NSInteger i = MIN(3, pathCount - 1); i >= 0; i--) { + NSDictionary *item = [path objectAtIndex:i]; + if (item == nil) { + return nil; // There should be no nil (undefined) from JS, but to be safe we check it here + } + + id name = [item objectForKey:@"name"]; + id label = [item objectForKey:@"label"]; + BOOL hasName = [name isKindOfClass:[NSString class]]; + BOOL hasLabel = [label isKindOfClass:[NSString class]]; + if (!hasName && !hasLabel) { + return nil; // This again should never be allowed in JS, but to be safe we check it here + } + if (hasLabel) { + [message appendString:(NSString *)label]; + } else if (hasName) { + [message appendString:(NSString *)name]; + } + + id element = [item objectForKey:@"element"]; + id file = [item objectForKey:@"file"]; + BOOL hasElement = [element isKindOfClass:[NSString class]]; + BOOL hasFile = [file isKindOfClass:[NSString class]]; + if (hasElement && hasFile) { + [message appendFormat:@"(%@, %@)", (NSString *)element, (NSString *)file]; + } else if (hasElement) { + [message appendFormat:@"(%@)", (NSString *)element]; + } else if (hasFile) { + [message appendFormat:@"(%@)", (NSString *)file]; + } + + if (i > 0) { + [message appendString:@" > "]; + } + } + + return message; +} + - (id _Nullable)convertNavigation: (SentryBreadcrumb *_Nonnull)breadcrumb { NSNumber* startTimestamp = [breadcrumb.data[@"start_timestamp"] isKindOfClass:[NSNumber class]] ? breadcrumb.data[@"start_timestamp"] : nil; diff --git a/samples/react-native/src/App.tsx b/samples/react-native/src/App.tsx index 89717b4a5a..c73b838eaf 100644 --- a/samples/react-native/src/App.tsx +++ b/samples/react-native/src/App.tsx @@ -28,11 +28,13 @@ import { Provider } from 'react-redux'; import { store } from './reduxApp'; import { GestureHandlerRootView } from 'react-native-gesture-handler'; import GesturesTracingScreen from './Screens/GesturesTracingScreen'; -import { Platform, StyleSheet, View } from 'react-native'; +import { LogBox, Platform, StyleSheet, View } from 'react-native'; import { HttpClient } from '@sentry/integrations'; import Ionicons from 'react-native-vector-icons/Ionicons'; import PlaygroundScreen from './Screens/PlaygroundScreen'; +LogBox.ignoreAllLogs(); + const isMobileOs = Platform.OS === 'android' || Platform.OS === 'ios'; const reactNavigationInstrumentation = diff --git a/src/js/touchevents.tsx b/src/js/touchevents.tsx index 785dc2977c..ebb2ba26c7 100644 --- a/src/js/touchevents.tsx +++ b/src/js/touchevents.tsx @@ -1,6 +1,6 @@ import { addBreadcrumb, getCurrentHub } from '@sentry/core'; import type { SeverityLevel } from '@sentry/types'; -import { logger } from '@sentry/utils'; +import { dropUndefinedKeys, logger } from '@sentry/utils'; import * as React from 'react'; import type { GestureResponderEvent } from 'react-native'; import { StyleSheet, View } from 'react-native'; @@ -189,50 +189,7 @@ class TouchEventBoundary extends React.Component { break; } - const props = currentInst.memoizedProps ?? {}; - const info: TouchedComponentInfo = {}; - - // provided by @sentry/babel-plugin-component-annotate - if ( - typeof props[SENTRY_COMPONENT_PROP_KEY] === 'string' && - props[SENTRY_COMPONENT_PROP_KEY].length > 0 && - props[SENTRY_COMPONENT_PROP_KEY] !== 'unknown' - ) { - info.name = props[SENTRY_COMPONENT_PROP_KEY]; - } - if ( - typeof props[SENTRY_ELEMENT_PROP_KEY] === 'string' && - props[SENTRY_ELEMENT_PROP_KEY].length > 0 && - props[SENTRY_ELEMENT_PROP_KEY] !== 'unknown' - ) { - info.element = props[SENTRY_ELEMENT_PROP_KEY]; - } - if ( - typeof props[SENTRY_FILE_PROP_KEY] === 'string' && - props[SENTRY_FILE_PROP_KEY].length > 0 && - props[SENTRY_FILE_PROP_KEY] !== 'unknown' - ) { - info.file = props[SENTRY_FILE_PROP_KEY]; - } - - // use custom label if provided by the user, or displayName if available - const labelValue = - typeof props[SENTRY_LABEL_PROP_KEY] === 'string' - ? 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 - typeof this.props.labelName === 'string' - ? props[this.props.labelName] - : undefined; - - if (typeof labelValue === 'string' && labelValue.length > 0) { - info.label = labelValue; - } - - if (!info.name && currentInst.elementType?.displayName) { - info.name = currentInst.elementType?.displayName; - } - + const info = getTouchedComponentInfo(currentInst, this.props.labelName); this._pushIfNotIgnored(touchPath, info); currentInst = currentInst.return; @@ -252,7 +209,11 @@ class TouchEventBoundary extends React.Component { /** * Pushes the name to the componentTreeNames array if it is not ignored. */ - private _pushIfNotIgnored(touchPath: TouchedComponentInfo[], value: TouchedComponentInfo): boolean { + private _pushIfNotIgnored(touchPath: TouchedComponentInfo[], value: TouchedComponentInfo | undefined): boolean { + if (!value) { + return false; + } + if (!value.name && !value.label) { return false; } @@ -273,6 +234,62 @@ class TouchEventBoundary extends React.Component { } } +function getTouchedComponentInfo(currentInst: ElementInstance, labelKey: string | undefined): TouchedComponentInfo | undefined { + const displayName = currentInst.elementType?.displayName; + + const props = currentInst.memoizedProps; + if (!props) { + // Early return if no props are available, as we can't extract any useful information + if (displayName) { + return { + name: displayName, + }; + } + return undefined; + } + + return dropUndefinedKeys({ + // provided by @sentry/babel-plugin-component-annotate + name: getComponentName(props) || displayName, + element: getElementName(props), + file: getFileName(props), + + // `sentry-label` or user defined label key + label: getLabelValue(props, labelKey), + }); +} + +function getComponentName(props: Record): string | undefined { + return typeof props[SENTRY_COMPONENT_PROP_KEY] === 'string' && + props[SENTRY_COMPONENT_PROP_KEY].length > 0 && + props[SENTRY_COMPONENT_PROP_KEY] !== 'unknown' && + props[SENTRY_COMPONENT_PROP_KEY] || undefined; +} + +function getElementName(props: Record): string | undefined { + return typeof props[SENTRY_ELEMENT_PROP_KEY] === 'string' && + props[SENTRY_ELEMENT_PROP_KEY].length > 0 && + props[SENTRY_ELEMENT_PROP_KEY] !== 'unknown' && + props[SENTRY_ELEMENT_PROP_KEY] || undefined; +} + +function getFileName(props: Record): string | undefined { + return typeof props[SENTRY_FILE_PROP_KEY] === 'string' && + props[SENTRY_FILE_PROP_KEY].length > 0 && + props[SENTRY_FILE_PROP_KEY] !== 'unknown' && + props[SENTRY_FILE_PROP_KEY] || 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] as string + // 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 + : typeof labelKey === 'string' && typeof props[labelKey] == 'string' && (props[labelKey] as string).length > 0 + ? props[labelKey] as string + : undefined; +} + /** * Convenience Higher-Order-Component for TouchEventBoundary * @param WrappedComponent any React Component