From 00e9040a2dd01f7b8f89855842368f8f8d7c260f Mon Sep 17 00:00:00 2001 From: Krystof Woldrich <31292499+krystofwoldrich@users.noreply.github.com> Date: Tue, 21 Jan 2025 17:22:22 +0100 Subject: [PATCH 01/32] ref(ios): Extract Cocoa SDK init into standalone file (#4442) --- CHANGELOG.md | 1 + .../project.pbxproj | 4 + .../RNSentryStart+Test.h | 8 + .../RNSentryCocoaTesterTests/RNSentryTests.mm | 93 ++++----- packages/core/ios/RNSentry.h | 5 - packages/core/ios/RNSentry.mm | 171 +--------------- packages/core/ios/RNSentryStart.h | 20 ++ packages/core/ios/RNSentryStart.m | 192 ++++++++++++++++++ 8 files changed, 267 insertions(+), 227 deletions(-) create mode 100644 packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryStart+Test.h create mode 100644 packages/core/ios/RNSentryStart.h create mode 100644 packages/core/ios/RNSentryStart.m diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e5ce40fb2..0bcb7ef63a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ ### Internal - Initialize `RNSentryTimeToDisplay` during native module `init` on iOS ([#4443](https://github.com/getsentry/sentry-react-native/pull/4443)) +- Extract iOS native initialization to standalone structures ([#4442](https://github.com/getsentry/sentry-react-native/pull/4442)) ### Dependencies diff --git a/packages/core/RNSentryCocoaTester/RNSentryCocoaTester.xcodeproj/project.pbxproj b/packages/core/RNSentryCocoaTester/RNSentryCocoaTester.xcodeproj/project.pbxproj index f78b1be0e0..112c485d6f 100644 --- a/packages/core/RNSentryCocoaTester/RNSentryCocoaTester.xcodeproj/project.pbxproj +++ b/packages/core/RNSentryCocoaTester/RNSentryCocoaTester.xcodeproj/project.pbxproj @@ -25,6 +25,8 @@ 332D33482CDBDC7300547D76 /* RNSentry.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = RNSentry.h; path = ../ios/RNSentry.h; sourceTree = SOURCE_ROOT; }; 332D33492CDCC8E100547D76 /* RNSentryTests.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RNSentryTests.h; sourceTree = ""; }; 332D334A2CDCC8EB00547D76 /* RNSentryCocoaTesterTests-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "RNSentryCocoaTesterTests-Bridging-Header.h"; sourceTree = ""; }; + 333B58A82D35BA93000F8D04 /* RNSentryStart.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = RNSentryStart.h; path = ../ios/RNSentryStart.h; sourceTree = SOURCE_ROOT; }; + 333B58A92D35BB2D000F8D04 /* RNSentryStart+Test.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "RNSentryStart+Test.h"; path = "RNSentryCocoaTesterTests/RNSentryStart+Test.h"; sourceTree = SOURCE_ROOT; }; 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 = ""; }; 3360843C2C340C76008CC412 /* RNSentryBreadcrumbTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RNSentryBreadcrumbTests.swift; sourceTree = ""; }; @@ -116,6 +118,8 @@ 33AFE0122B8F319000AAB120 /* RNSentry */ = { isa = PBXGroup; children = ( + 333B58A92D35BB2D000F8D04 /* RNSentryStart+Test.h */, + 333B58A82D35BA93000F8D04 /* RNSentryStart.h */, 3380C6C02CDEC56B0018B9B6 /* Replay */, 332D33482CDBDC7300547D76 /* RNSentry.h */, 3360843A2C32E3A8008CC412 /* RNSentryReplayBreadcrumbConverter.h */, diff --git a/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryStart+Test.h b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryStart+Test.h new file mode 100644 index 0000000000..fcdfe7872b --- /dev/null +++ b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryStart+Test.h @@ -0,0 +1,8 @@ +#import "RNSentryStart.h" + +@interface +RNSentryStart (Test) + ++ (void)setEventOriginTag:(SentryEvent *)event; + +@end diff --git a/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryTests.mm b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryTests.mm index 6e63793b85..9cefc4747a 100644 --- a/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryTests.mm +++ b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryTests.mm @@ -1,4 +1,5 @@ #import "RNSentryTests.h" +#import "RNSentryStart+Test.h" #import #import #import @@ -13,7 +14,6 @@ @implementation RNSentryInitNativeSdkTests - (void)testCreateOptionsWithDictionaryRemovesPerformanceProperties { - RNSentry *rnSentry = [[RNSentry alloc] init]; NSError *error = nil; NSDictionary *_Nonnull mockedReactNativeDictionary = @@ -25,8 +25,8 @@ - (void)testCreateOptionsWithDictionaryRemovesPerformanceProperties , @"enableTracing" : @YES, } ; -SentryOptions *actualOptions = [rnSentry createOptionsWithDictionary:mockedReactNativeDictionary - error:&error]; +SentryOptions *actualOptions = + [RNSentryStart createOptionsWithDictionary:mockedReactNativeDictionary error:&error]; XCTAssertNotNil(actualOptions, @"Did not create sentry options"); XCTAssertNil(error, @"Should not pass no error"); @@ -40,14 +40,13 @@ - (void)testCreateOptionsWithDictionaryRemovesPerformanceProperties - (void)testCaptureFailedRequestsIsDisabled { - RNSentry *rnSentry = [[RNSentry alloc] init]; NSError *error = nil; NSDictionary *_Nonnull mockedReactNativeDictionary = @{ @"dsn" : @"https://abcd@efgh.ingest.sentry.io/123456", }; - SentryOptions *actualOptions = [rnSentry createOptionsWithDictionary:mockedReactNativeDictionary - error:&error]; + SentryOptions *actualOptions = + [RNSentryStart createOptionsWithDictionary:mockedReactNativeDictionary error:&error]; XCTAssertNotNil(actualOptions, @"Did not create sentry options"); XCTAssertNil(error, @"Should not pass no error"); @@ -56,14 +55,13 @@ - (void)testCaptureFailedRequestsIsDisabled - (void)testCreateOptionsWithDictionaryNativeCrashHandlingDefault { - RNSentry *rnSentry = [[RNSentry alloc] init]; NSError *error = nil; NSDictionary *_Nonnull mockedReactNativeDictionary = @{ @"dsn" : @"https://abcd@efgh.ingest.sentry.io/123456", }; - SentryOptions *actualOptions = [rnSentry createOptionsWithDictionary:mockedReactNativeDictionary - error:&error]; + SentryOptions *actualOptions = + [RNSentryStart createOptionsWithDictionary:mockedReactNativeDictionary error:&error]; XCTAssertNotNil(actualOptions, @"Did not create sentry options"); XCTAssertNil(error, @"Should not pass no error"); XCTAssertEqual([actualOptions.integrations containsObject:@"SentryCrashIntegration"], true, @@ -72,14 +70,13 @@ - (void)testCreateOptionsWithDictionaryNativeCrashHandlingDefault - (void)testCreateOptionsWithDictionaryAutoPerformanceTracingDefault { - RNSentry *rnSentry = [[RNSentry alloc] init]; NSError *error = nil; NSDictionary *_Nonnull mockedReactNativeDictionary = @{ @"dsn" : @"https://abcd@efgh.ingest.sentry.io/123456", }; - SentryOptions *actualOptions = [rnSentry createOptionsWithDictionary:mockedReactNativeDictionary - error:&error]; + SentryOptions *actualOptions = + [RNSentryStart createOptionsWithDictionary:mockedReactNativeDictionary error:&error]; XCTAssertNotNil(actualOptions, @"Did not create sentry options"); XCTAssertNil(error, @"Should not pass no error"); XCTAssertEqual( @@ -88,15 +85,14 @@ - (void)testCreateOptionsWithDictionaryAutoPerformanceTracingDefault - (void)testCreateOptionsWithDictionaryNativeCrashHandlingEnabled { - RNSentry *rnSentry = [[RNSentry alloc] init]; NSError *error = nil; NSDictionary *_Nonnull mockedReactNativeDictionary = @{ @"dsn" : @"https://abcd@efgh.ingest.sentry.io/123456", @"enableNativeCrashHandling" : @YES, }; - SentryOptions *actualOptions = [rnSentry createOptionsWithDictionary:mockedReactNativeDictionary - error:&error]; + SentryOptions *actualOptions = + [RNSentryStart createOptionsWithDictionary:mockedReactNativeDictionary error:&error]; XCTAssertNotNil(actualOptions, @"Did not create sentry options"); XCTAssertNil(error, @"Should not pass no error"); XCTAssertEqual([actualOptions.integrations containsObject:@"SentryCrashIntegration"], true, @@ -105,15 +101,14 @@ - (void)testCreateOptionsWithDictionaryNativeCrashHandlingEnabled - (void)testCreateOptionsWithDictionaryAutoPerformanceTracingEnabled { - RNSentry *rnSentry = [[RNSentry alloc] init]; NSError *error = nil; NSDictionary *_Nonnull mockedReactNativeDictionary = @{ @"dsn" : @"https://abcd@efgh.ingest.sentry.io/123456", @"enableAutoPerformanceTracing" : @YES, }; - SentryOptions *actualOptions = [rnSentry createOptionsWithDictionary:mockedReactNativeDictionary - error:&error]; + SentryOptions *actualOptions = + [RNSentryStart createOptionsWithDictionary:mockedReactNativeDictionary error:&error]; XCTAssertNotNil(actualOptions, @"Did not create sentry options"); XCTAssertNil(error, @"Should not pass no error"); XCTAssertEqual( @@ -122,15 +117,14 @@ - (void)testCreateOptionsWithDictionaryAutoPerformanceTracingEnabled - (void)testCreateOptionsWithDictionaryNativeCrashHandlingDisabled { - RNSentry *rnSentry = [[RNSentry alloc] init]; NSError *error = nil; NSDictionary *_Nonnull mockedReactNativeDictionary = @{ @"dsn" : @"https://abcd@efgh.ingest.sentry.io/123456", @"enableNativeCrashHandling" : @NO, }; - SentryOptions *actualOptions = [rnSentry createOptionsWithDictionary:mockedReactNativeDictionary - error:&error]; + SentryOptions *actualOptions = + [RNSentryStart createOptionsWithDictionary:mockedReactNativeDictionary error:&error]; XCTAssertNotNil(actualOptions, @"Did not create sentry options"); XCTAssertNil(error, @"Should not pass no error"); XCTAssertEqual([actualOptions.integrations containsObject:@"SentryCrashIntegration"], false, @@ -139,15 +133,14 @@ - (void)testCreateOptionsWithDictionaryNativeCrashHandlingDisabled - (void)testCreateOptionsWithDictionaryAutoPerformanceTracingDisabled { - RNSentry *rnSentry = [[RNSentry alloc] init]; NSError *error = nil; NSDictionary *_Nonnull mockedReactNativeDictionary = @{ @"dsn" : @"https://abcd@efgh.ingest.sentry.io/123456", @"enableAutoPerformanceTracing" : @NO, }; - SentryOptions *actualOptions = [rnSentry createOptionsWithDictionary:mockedReactNativeDictionary - error:&error]; + SentryOptions *actualOptions = + [RNSentryStart createOptionsWithDictionary:mockedReactNativeDictionary error:&error]; XCTAssertNotNil(actualOptions, @"Did not create sentry options"); XCTAssertNil(error, @"Should not pass no error"); XCTAssertEqual(actualOptions.enableAutoPerformanceTracing, false, @@ -156,7 +149,6 @@ - (void)testCreateOptionsWithDictionaryAutoPerformanceTracingDisabled - (void)testCreateOptionsWithDictionarySpotlightEnabled { - RNSentry *rnSentry = [[RNSentry alloc] init]; NSError *error = nil; NSDictionary *_Nonnull mockedReactNativeDictionary = @{ @@ -164,8 +156,8 @@ - (void)testCreateOptionsWithDictionarySpotlightEnabled @"spotlight" : @YES, @"defaultSidecarUrl" : @"http://localhost:8969/teststream", }; - SentryOptions *actualOptions = [rnSentry createOptionsWithDictionary:mockedReactNativeDictionary - error:&error]; + SentryOptions *actualOptions = + [RNSentryStart createOptionsWithDictionary:mockedReactNativeDictionary error:&error]; XCTAssertNotNil(actualOptions, @"Did not create sentry options"); XCTAssertNil(error, @"Should not pass no error"); XCTAssertTrue(actualOptions.enableSpotlight, @"Did not enable spotlight"); @@ -174,7 +166,6 @@ - (void)testCreateOptionsWithDictionarySpotlightEnabled - (void)testCreateOptionsWithDictionarySpotlightOne { - RNSentry *rnSentry = [[RNSentry alloc] init]; NSError *error = nil; NSDictionary *_Nonnull mockedReactNativeDictionary = @{ @@ -182,8 +173,8 @@ - (void)testCreateOptionsWithDictionarySpotlightOne @"spotlight" : @1, @"defaultSidecarUrl" : @"http://localhost:8969/teststream", }; - SentryOptions *actualOptions = [rnSentry createOptionsWithDictionary:mockedReactNativeDictionary - error:&error]; + SentryOptions *actualOptions = + [RNSentryStart createOptionsWithDictionary:mockedReactNativeDictionary error:&error]; XCTAssertNotNil(actualOptions, @"Did not create sentry options"); XCTAssertNil(error, @"Should not pass no error"); XCTAssertTrue(actualOptions.enableSpotlight, @"Did not enable spotlight"); @@ -192,15 +183,14 @@ - (void)testCreateOptionsWithDictionarySpotlightOne - (void)testCreateOptionsWithDictionarySpotlightUrl { - RNSentry *rnSentry = [[RNSentry alloc] init]; NSError *error = nil; NSDictionary *_Nonnull mockedReactNativeDictionary = @{ @"dsn" : @"https://abcd@efgh.ingest.sentry.io/123456", @"spotlight" : @"http://localhost:8969/teststream", }; - SentryOptions *actualOptions = [rnSentry createOptionsWithDictionary:mockedReactNativeDictionary - error:&error]; + SentryOptions *actualOptions = + [RNSentryStart createOptionsWithDictionary:mockedReactNativeDictionary error:&error]; XCTAssertNotNil(actualOptions, @"Did not create sentry options"); XCTAssertNil(error, @"Should not pass no error"); XCTAssertTrue(actualOptions.enableSpotlight, @"Did not enable spotlight"); @@ -209,15 +199,14 @@ - (void)testCreateOptionsWithDictionarySpotlightUrl - (void)testCreateOptionsWithDictionarySpotlightDisabled { - RNSentry *rnSentry = [[RNSentry alloc] init]; NSError *error = nil; NSDictionary *_Nonnull mockedReactNativeDictionary = @{ @"dsn" : @"https://abcd@efgh.ingest.sentry.io/123456", @"spotlight" : @NO, }; - SentryOptions *actualOptions = [rnSentry createOptionsWithDictionary:mockedReactNativeDictionary - error:&error]; + SentryOptions *actualOptions = + [RNSentryStart createOptionsWithDictionary:mockedReactNativeDictionary error:&error]; XCTAssertNotNil(actualOptions, @"Did not create sentry options"); XCTAssertNil(error, @"Should not pass no error"); XCTAssertFalse(actualOptions.enableSpotlight, @"Did not disable spotlight"); @@ -225,15 +214,14 @@ - (void)testCreateOptionsWithDictionarySpotlightDisabled - (void)testCreateOptionsWithDictionarySpotlightZero { - RNSentry *rnSentry = [[RNSentry alloc] init]; NSError *error = nil; NSDictionary *_Nonnull mockedReactNativeDictionary = @{ @"dsn" : @"https://abcd@efgh.ingest.sentry.io/123456", @"spotlight" : @0, }; - SentryOptions *actualOptions = [rnSentry createOptionsWithDictionary:mockedReactNativeDictionary - error:&error]; + SentryOptions *actualOptions = + [RNSentryStart createOptionsWithDictionary:mockedReactNativeDictionary error:&error]; XCTAssertNotNil(actualOptions, @"Did not create sentry options"); XCTAssertNil(error, @"Should not pass no error"); XCTAssertFalse(actualOptions.enableSpotlight, @"Did not disable spotlight"); @@ -241,14 +229,13 @@ - (void)testCreateOptionsWithDictionarySpotlightZero - (void)testPassesErrorOnWrongDsn { - RNSentry *rnSentry = [[RNSentry alloc] init]; NSError *error = nil; NSDictionary *_Nonnull mockedReactNativeDictionary = @{ @"dsn" : @"not_a_valid_dsn", }; - SentryOptions *actualOptions = [rnSentry createOptionsWithDictionary:mockedReactNativeDictionary - error:&error]; + SentryOptions *actualOptions = + [RNSentryStart createOptionsWithDictionary:mockedReactNativeDictionary error:&error]; XCTAssertNil(actualOptions, @"Created invalid sentry options"); XCTAssertNotNil(error, @"Did not created error on invalid dsn"); @@ -256,14 +243,14 @@ - (void)testPassesErrorOnWrongDsn - (void)testBeforeBreadcrumbsCallbackFiltersOutSentryDsnRequestBreadcrumbs { - RNSentry *rnSentry = [[RNSentry alloc] init]; NSError *error = nil; NSDictionary *_Nonnull mockedDictionary = @{ @"dsn" : @"https://abc@def.ingest.sentry.io/1234567", @"devServerUrl" : @"http://localhost:8081" }; - SentryOptions *options = [rnSentry createOptionsWithDictionary:mockedDictionary error:&error]; + SentryOptions *options = [RNSentryStart createOptionsWithDictionary:mockedDictionary + error:&error]; SentryBreadcrumb *breadcrumb = [[SentryBreadcrumb alloc] init]; breadcrumb.type = @"http"; @@ -276,14 +263,14 @@ - (void)testBeforeBreadcrumbsCallbackFiltersOutSentryDsnRequestBreadcrumbs - (void)testBeforeBreadcrumbsCallbackFiltersOutDevServerRequestBreadcrumbs { - RNSentry *rnSentry = [[RNSentry alloc] init]; NSError *error = nil; NSString *mockDevServer = @"http://localhost:8081"; NSDictionary *_Nonnull mockedDictionary = @{ @"dsn" : @"https://abc@def.ingest.sentry.io/1234567", @"devServerUrl" : mockDevServer }; - SentryOptions *options = [rnSentry createOptionsWithDictionary:mockedDictionary error:&error]; + SentryOptions *options = [RNSentryStart createOptionsWithDictionary:mockedDictionary + error:&error]; SentryBreadcrumb *breadcrumb = [[SentryBreadcrumb alloc] init]; breadcrumb.type = @"http"; @@ -296,14 +283,14 @@ - (void)testBeforeBreadcrumbsCallbackFiltersOutDevServerRequestBreadcrumbs - (void)testBeforeBreadcrumbsCallbackDoesNotFiltersOutNonDevServerOrDsnRequestBreadcrumbs { - RNSentry *rnSentry = [[RNSentry alloc] init]; NSError *error = nil; NSDictionary *_Nonnull mockedDictionary = @{ @"dsn" : @"https://abc@def.ingest.sentry.io/1234567", @"devServerUrl" : @"http://localhost:8081" }; - SentryOptions *options = [rnSentry createOptionsWithDictionary:mockedDictionary error:&error]; + SentryOptions *options = [RNSentryStart createOptionsWithDictionary:mockedDictionary + error:&error]; SentryBreadcrumb *breadcrumb = [[SentryBreadcrumb alloc] init]; breadcrumb.type = @"http"; @@ -316,13 +303,13 @@ - (void)testBeforeBreadcrumbsCallbackDoesNotFiltersOutNonDevServerOrDsnRequestBr - (void)testBeforeBreadcrumbsCallbackKeepsBreadcrumbWhenDevServerUrlIsNotPassedAndDsnDoesNotMatch { - RNSentry *rnSentry = [[RNSentry alloc] init]; NSError *error = nil; NSDictionary *_Nonnull mockedDictionary = @{ // dsn is always validated in SentryOptions initialization @"dsn" : @"https://abc@def.ingest.sentry.io/1234567" }; - SentryOptions *options = [rnSentry createOptionsWithDictionary:mockedDictionary error:&error]; + SentryOptions *options = [RNSentryStart createOptionsWithDictionary:mockedDictionary + error:&error]; SentryBreadcrumb *breadcrumb = [[SentryBreadcrumb alloc] init]; breadcrumb.type = @"http"; @@ -335,13 +322,12 @@ - (void)testBeforeBreadcrumbsCallbackKeepsBreadcrumbWhenDevServerUrlIsNotPassedA - (void)testEventFromSentryCocoaReactNativeHasOriginAndEnvironmentTags { - RNSentry *rnSentry = [[RNSentry alloc] init]; SentryEvent *testEvent = [[SentryEvent alloc] init]; testEvent.sdk = @{ @"name" : @"sentry.cocoa.react-native", }; - [rnSentry setEventOriginTag:testEvent]; + [RNSentryStart setEventOriginTag:testEvent]; XCTAssertEqual(testEvent.tags[@"event.origin"], @"ios"); XCTAssertEqual(testEvent.tags[@"event.environment"], @"native"); @@ -349,7 +335,6 @@ - (void)testEventFromSentryCocoaReactNativeHasOriginAndEnvironmentTags - (void)testEventFromSentryReactNativeOriginAndEnvironmentTagsAreOverwritten { - RNSentry *rnSentry = [[RNSentry alloc] init]; SentryEvent *testEvent = [[SentryEvent alloc] init]; testEvent.sdk = @{ @"name" : @"sentry.cocoa.react-native", @@ -359,7 +344,7 @@ - (void)testEventFromSentryReactNativeOriginAndEnvironmentTagsAreOverwritten @"event.environment" : @"testEventEnvironmentTag", }; - [rnSentry setEventOriginTag:testEvent]; + [RNSentryStart setEventOriginTag:testEvent]; XCTAssertEqual(testEvent.tags[@"event.origin"], @"ios"); XCTAssertEqual(testEvent.tags[@"event.environment"], @"native"); diff --git a/packages/core/ios/RNSentry.h b/packages/core/ios/RNSentry.h index cfd0b74b28..66dc7219ac 100644 --- a/packages/core/ios/RNSentry.h +++ b/packages/core/ios/RNSentry.h @@ -20,11 +20,6 @@ SentrySDK (Private) @interface RNSentry : RCTEventEmitter -- (SentryOptions *_Nullable)createOptionsWithDictionary:(NSDictionary *_Nonnull)options - error:(NSError *_Nullable *_Nonnull)errorPointer; - -- (void)setEventOriginTag:(SentryEvent *)event; - - (NSDictionary *_Nonnull)fetchNativeStackFramesBy:(NSArray *)instructionsAddr symbolicate:(SymbolicateCallbackType)symbolicate; diff --git a/packages/core/ios/RNSentry.mm b/packages/core/ios/RNSentry.mm index 79ff76d0ae..6907513da5 100644 --- a/packages/core/ios/RNSentry.mm +++ b/packages/core/ios/RNSentry.mm @@ -49,6 +49,7 @@ # import "RNSentryRNSScreen.h" #endif +#import "RNSentryStart.h" #import "RNSentryVersion.h" @interface @@ -63,7 +64,6 @@ + (void)storeEnvelope:(SentryEnvelope *)envelope; static bool hasFetchedAppStart; @implementation RNSentry { - bool sentHybridSdkDidBecomeActive; bool hasListeners; RNSentryTimeToDisplay *_timeToDisplay; } @@ -94,181 +94,16 @@ - (instancetype)init : (RCTPromiseRejectBlock)reject) { NSError *error = nil; - SentryOptions *sentryOptions = [self createOptionsWithDictionary:options error:&error]; + SentryOptions *sentryOptions = [RNSentryStart createOptionsWithDictionary:options error:&error]; if (error != nil) { reject(@"SentryReactNative", error.localizedDescription, error); return; } - NSString *sdkVersion = [PrivateSentrySDKOnly getSdkVersionString]; - [PrivateSentrySDKOnly setSdkName:NATIVE_SDK_NAME andVersionString:sdkVersion]; - [PrivateSentrySDKOnly addSdkPackage:REACT_NATIVE_SDK_PACKAGE_NAME - version:REACT_NATIVE_SDK_PACKAGE_VERSION]; - - [SentrySDK startWithOptions:sentryOptions]; - -#if TARGET_OS_IPHONE || TARGET_OS_MACCATALYST - BOOL appIsActive = - [[UIApplication sharedApplication] applicationState] == UIApplicationStateActive; -#else - BOOL appIsActive = [[NSApplication sharedApplication] isActive]; -#endif - - // If the app is active/in foreground, and we have not sent the SentryHybridSdkDidBecomeActive - // notification, send it. - if (appIsActive && !sentHybridSdkDidBecomeActive - && (PrivateSentrySDKOnly.options.enableAutoSessionTracking - || PrivateSentrySDKOnly.options.enableWatchdogTerminationTracking)) { - [[NSNotificationCenter defaultCenter] postNotificationName:@"SentryHybridSdkDidBecomeActive" - object:nil]; - - sentHybridSdkDidBecomeActive = true; - } - -#if SENTRY_TARGET_REPLAY_SUPPORTED - [RNSentryReplay postInit]; -#endif - + [RNSentryStart startWithOptions:sentryOptions]; resolve(@YES); } -- (SentryOptions *_Nullable)createOptionsWithDictionary:(NSDictionary *_Nonnull)options - error:(NSError *_Nonnull *_Nonnull)errorPointer -{ - SentryBeforeSendEventCallback beforeSend = ^SentryEvent *(SentryEvent *event) - { - // We don't want to send an event after startup that came from a Unhandled JS Exception of - // react native Because we sent it already before the app crashed. - if (nil != event.exceptions.firstObject.type && - [event.exceptions.firstObject.type rangeOfString:@"Unhandled JS Exception"].location - != NSNotFound) { - return nil; - } - - [self setEventOriginTag:event]; - - return event; - }; - - NSMutableDictionary *mutableOptions = [options mutableCopy]; - [mutableOptions setValue:beforeSend forKey:@"beforeSend"]; - - // remove performance traces sample rate and traces sampler since we don't want to synchronize - // these configurations to the Native SDKs. The user could tho initialize the SDK manually and - // set themselves. - [mutableOptions removeObjectForKey:@"tracesSampleRate"]; - [mutableOptions removeObjectForKey:@"tracesSampler"]; - [mutableOptions removeObjectForKey:@"enableTracing"]; - -#if SENTRY_TARGET_REPLAY_SUPPORTED - [RNSentryReplay updateOptions:mutableOptions]; -#endif - - SentryOptions *sentryOptions = [[SentryOptions alloc] initWithDict:mutableOptions - didFailWithError:errorPointer]; - if (*errorPointer != nil) { - return nil; - } - - // Exclude Dev Server and Sentry Dsn request from Breadcrumbs - NSString *dsn = [self getURLFromDSN:[mutableOptions valueForKey:@"dsn"]]; - NSString *devServerUrl = [mutableOptions valueForKey:@"devServerUrl"]; - sentryOptions.beforeBreadcrumb - = ^SentryBreadcrumb *_Nullable(SentryBreadcrumb *_Nonnull breadcrumb) - { - NSString *url = breadcrumb.data[@"url"] ?: @""; - - if ([@"http" isEqualToString:breadcrumb.type] - && ((dsn != nil && [url hasPrefix:dsn]) - || (devServerUrl != nil && [url hasPrefix:devServerUrl]))) { - return nil; - } - return breadcrumb; - }; - - if ([mutableOptions valueForKey:@"enableNativeCrashHandling"] != nil) { - BOOL enableNativeCrashHandling = [mutableOptions[@"enableNativeCrashHandling"] boolValue]; - - if (!enableNativeCrashHandling) { - NSMutableArray *integrations = sentryOptions.integrations.mutableCopy; - [integrations removeObject:@"SentryCrashIntegration"]; - sentryOptions.integrations = integrations; - } - } - - // Set spotlight option - if ([mutableOptions valueForKey:@"spotlight"] != nil) { - id spotlightValue = [mutableOptions valueForKey:@"spotlight"]; - if ([spotlightValue isKindOfClass:[NSString class]]) { - NSLog(@"Using Spotlight on address: %@", spotlightValue); - sentryOptions.enableSpotlight = true; - sentryOptions.spotlightUrl = spotlightValue; - } else if ([spotlightValue isKindOfClass:[NSNumber class]]) { - sentryOptions.enableSpotlight = [spotlightValue boolValue]; - id defaultSpotlightUrl = [mutableOptions valueForKey:@"defaultSidecarUrl"]; - if (defaultSpotlightUrl != nil) { - sentryOptions.spotlightUrl = defaultSpotlightUrl; - } - } - } - - // Enable the App start and Frames tracking measurements - if ([mutableOptions valueForKey:@"enableAutoPerformanceTracing"] != nil) { - BOOL enableAutoPerformanceTracing = - [mutableOptions[@"enableAutoPerformanceTracing"] boolValue]; - PrivateSentrySDKOnly.appStartMeasurementHybridSDKMode = enableAutoPerformanceTracing; -#if TARGET_OS_IPHONE || TARGET_OS_MACCATALYST - PrivateSentrySDKOnly.framesTrackingMeasurementHybridSDKMode = enableAutoPerformanceTracing; -#endif - } - - // Failed requests can only be enabled in one SDK to avoid duplicates - sentryOptions.enableCaptureFailedRequests = NO; - - return sentryOptions; -} - -- (NSString *_Nullable)getURLFromDSN:(NSString *)dsn -{ - NSURL *url = [NSURL URLWithString:dsn]; - if (!url) { - return nil; - } - return [NSString stringWithFormat:@"%@://%@", url.scheme, url.host]; -} - -- (void)setEventOriginTag:(SentryEvent *)event -{ - if (event.sdk != nil) { - NSString *sdkName = event.sdk[@"name"]; - - // If the event is from react native, it gets set - // there and we do not handle it here. - if ([sdkName isEqual:NATIVE_SDK_NAME]) { - [self setEventEnvironmentTag:event origin:@"ios" environment:@"native"]; - } - } -} - -- (void)setEventEnvironmentTag:(SentryEvent *)event - origin:(NSString *)origin - environment:(NSString *)environment -{ - NSMutableDictionary *newTags = [NSMutableDictionary new]; - - if (nil != event.tags && [event.tags count] > 0) { - [newTags addEntriesFromDictionary:event.tags]; - } - if (nil != origin) { - [newTags setValue:origin forKey:@"event.origin"]; - } - if (nil != environment) { - [newTags setValue:environment forKey:@"event.environment"]; - } - - event.tags = newTags; -} - RCT_EXPORT_METHOD(initNativeReactNavigationNewFrameTracking : (RCTPromiseResolveBlock)resolve rejecter : (RCTPromiseRejectBlock)reject) diff --git a/packages/core/ios/RNSentryStart.h b/packages/core/ios/RNSentryStart.h new file mode 100644 index 0000000000..bc5adf35af --- /dev/null +++ b/packages/core/ios/RNSentryStart.h @@ -0,0 +1,20 @@ +#import +#import + +@interface RNSentryStart : NSObject +SENTRY_NO_INIT + ++ (SentryOptions *_Nullable)createOptionsWithDictionary:(NSDictionary *_Nonnull)options + error:(NSError *_Nonnull *_Nonnull)errorPointer; + +/** + * @experimental + * Inits and configures Sentry for React Native applications. Make sure to + * set a valid DSN. + * + * @discussion Call this method on the main thread. When calling it from a background thread, the + * SDK starts on the main thread async. + */ ++ (void)startWithOptions:(SentryOptions *)options NS_SWIFT_NAME(start(options:)); + +@end diff --git a/packages/core/ios/RNSentryStart.m b/packages/core/ios/RNSentryStart.m new file mode 100644 index 0000000000..c364b6c630 --- /dev/null +++ b/packages/core/ios/RNSentryStart.m @@ -0,0 +1,192 @@ +#import "RNSentryStart.h" +#import "RNSentryReplay.h" +#import "RNSentryVersion.h" + +#import +#import +#import + +@implementation RNSentryStart + ++ (void)startWithOptions:(SentryOptions *)options NS_SWIFT_NAME(start(options:)) +{ + NSString *sdkVersion = [PrivateSentrySDKOnly getSdkVersionString]; + [PrivateSentrySDKOnly setSdkName:NATIVE_SDK_NAME andVersionString:sdkVersion]; + [PrivateSentrySDKOnly addSdkPackage:REACT_NATIVE_SDK_PACKAGE_NAME + version:REACT_NATIVE_SDK_PACKAGE_VERSION]; + + [SentrySDK startWithOptions:options]; + +#if SENTRY_TARGET_REPLAY_SUPPORTED + [RNSentryReplay postInit]; +#endif + + [self postDidBecomeActiveNotification]; +} + ++ (SentryOptions *_Nullable)createOptionsWithDictionary:(NSDictionary *_Nonnull)options + error:(NSError *_Nonnull *_Nonnull)errorPointer +{ + SentryBeforeSendEventCallback beforeSend = ^SentryEvent *(SentryEvent *event) + { + // We don't want to send an event after startup that came from a Unhandled JS Exception of + // react native Because we sent it already before the app crashed. + if (nil != event.exceptions.firstObject.type && + [event.exceptions.firstObject.type rangeOfString:@"Unhandled JS Exception"].location + != NSNotFound) { + return nil; + } + + [self setEventOriginTag:event]; + + return event; + }; + + NSMutableDictionary *mutableOptions = [options mutableCopy]; + [mutableOptions setValue:beforeSend forKey:@"beforeSend"]; + + // remove performance traces sample rate and traces sampler since we don't want to synchronize + // these configurations to the Native SDKs. The user could tho initialize the SDK manually and + // set themselves. + [mutableOptions removeObjectForKey:@"tracesSampleRate"]; + [mutableOptions removeObjectForKey:@"tracesSampler"]; + [mutableOptions removeObjectForKey:@"enableTracing"]; + +#if SENTRY_TARGET_REPLAY_SUPPORTED + [RNSentryReplay updateOptions:mutableOptions]; +#endif + + SentryOptions *sentryOptions = [[SentryOptions alloc] initWithDict:mutableOptions + didFailWithError:errorPointer]; + if (*errorPointer != nil) { + return nil; + } + + // Exclude Dev Server and Sentry Dsn request from Breadcrumbs + NSString *dsn = [self getURLFromDSN:[mutableOptions valueForKey:@"dsn"]]; + NSString *devServerUrl = [mutableOptions valueForKey:@"devServerUrl"]; + sentryOptions.beforeBreadcrumb + = ^SentryBreadcrumb *_Nullable(SentryBreadcrumb *_Nonnull breadcrumb) + { + NSString *url = breadcrumb.data[@"url"] ?: @""; + + if ([@"http" isEqualToString:breadcrumb.type] + && ((dsn != nil && [url hasPrefix:dsn]) + || (devServerUrl != nil && [url hasPrefix:devServerUrl]))) { + return nil; + } + return breadcrumb; + }; + + // JS options.enableNativeCrashHandling equals to native options.enableCrashHandler + if ([mutableOptions valueForKey:@"enableNativeCrashHandling"] != nil) { + BOOL enableNativeCrashHandling = [mutableOptions[@"enableNativeCrashHandling"] boolValue]; + + if (!enableNativeCrashHandling) { + NSMutableArray *integrations = sentryOptions.integrations.mutableCopy; + [integrations removeObject:@"SentryCrashIntegration"]; + sentryOptions.integrations = integrations; + } + } + + // Set spotlight option + if ([mutableOptions valueForKey:@"spotlight"] != nil) { + id spotlightValue = [mutableOptions valueForKey:@"spotlight"]; + if ([spotlightValue isKindOfClass:[NSString class]]) { + NSLog(@"Using Spotlight on address: %@", spotlightValue); + sentryOptions.enableSpotlight = true; + sentryOptions.spotlightUrl = spotlightValue; + } else if ([spotlightValue isKindOfClass:[NSNumber class]]) { + sentryOptions.enableSpotlight = [spotlightValue boolValue]; + id defaultSpotlightUrl = [mutableOptions valueForKey:@"defaultSidecarUrl"]; + if (defaultSpotlightUrl != nil) { + sentryOptions.spotlightUrl = defaultSpotlightUrl; + } + } + } + + // Enable the App start and Frames tracking measurements + if ([mutableOptions valueForKey:@"enableAutoPerformanceTracing"] != nil) { + BOOL enableAutoPerformanceTracing = + [mutableOptions[@"enableAutoPerformanceTracing"] boolValue]; + PrivateSentrySDKOnly.appStartMeasurementHybridSDKMode = enableAutoPerformanceTracing; +#if TARGET_OS_IPHONE || TARGET_OS_MACCATALYST + PrivateSentrySDKOnly.framesTrackingMeasurementHybridSDKMode = enableAutoPerformanceTracing; +#endif + } + + // Failed requests can only be enabled in one SDK to avoid duplicates + sentryOptions.enableCaptureFailedRequests = NO; + + return sentryOptions; +} + ++ (void)setEventOriginTag:(SentryEvent *)event +{ + if (event.sdk != nil) { + NSString *sdkName = event.sdk[@"name"]; + + // If the event is from react native, it gets set + // there and we do not handle it here. + if ([sdkName isEqual:NATIVE_SDK_NAME]) { + [self setEventEnvironmentTag:event origin:@"ios" environment:@"native"]; + } + } +} + ++ (void)setEventEnvironmentTag:(SentryEvent *)event + origin:(NSString *)origin + environment:(NSString *)environment +{ + NSMutableDictionary *newTags = [NSMutableDictionary new]; + + if (nil != event.tags && [event.tags count] > 0) { + [newTags addEntriesFromDictionary:event.tags]; + } + if (nil != origin) { + [newTags setValue:origin forKey:@"event.origin"]; + } + if (nil != environment) { + [newTags setValue:environment forKey:@"event.environment"]; + } + + event.tags = newTags; +} + ++ (NSString *_Nullable)getURLFromDSN:(NSString *)dsn +{ + NSURL *url = [NSURL URLWithString:dsn]; + if (!url) { + return nil; + } + return [NSString stringWithFormat:@"%@://%@", url.scheme, url.host]; +} + +static bool sentHybridSdkDidBecomeActive = NO; + ++ (void)postDidBecomeActiveNotification +{ +#if TARGET_OS_IPHONE || TARGET_OS_MACCATALYST + BOOL appIsActive = + [[UIApplication sharedApplication] applicationState] == UIApplicationStateActive; +#else + BOOL appIsActive = [[NSApplication sharedApplication] isActive]; +#endif + + // If the app is active/in foreground, and we have not sent the SentryHybridSdkDidBecomeActive + // notification, send it. + if (appIsActive && !sentHybridSdkDidBecomeActive + && (PrivateSentrySDKOnly.options.enableAutoSessionTracking + || PrivateSentrySDKOnly.options.enableWatchdogTerminationTracking)) { + // Updates Native App State Manager + // https://github.com/getsentry/sentry-cocoa/blob/888a145b144b8077e03151a886520f332e47e297/Sources/Sentry/SentryAppStateManager.m#L136 + // Triggers Session Tracker + // https://github.com/getsentry/sentry-cocoa/blob/888a145b144b8077e03151a886520f332e47e297/Sources/Sentry/SentrySessionTracker.m#L144 + [[NSNotificationCenter defaultCenter] postNotificationName:@"SentryHybridSdkDidBecomeActive" + object:nil]; + + sentHybridSdkDidBecomeActive = true; + } +} + +@end From 2cb7eb2c718a1720435a4bb346f73424bf3eb06c Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Wed, 22 Jan 2025 09:40:27 +0200 Subject: [PATCH 02/32] ref(android): Extracts Android native initialization to standalone structures (#4445) * Extract Android SDK Init * Update tests * Adds changelog * Fix lint issues * Rename RNSentryStart instance for clarity * Converts RNSentryStart to utility class * Update CHANGELOG.md --------- Co-authored-by: Krystof Woldrich <31292499+krystofwoldrich@users.noreply.github.com> --- CHANGELOG.md | 1 + .../io/sentry/react/RNSentryModuleImplTest.kt | 166 ---------- .../java/io/sentry/react/RNSentryStartTest.kt | 191 +++++++++++ .../io/sentry/react/RNSentryModuleImpl.java | 282 +--------------- .../java/io/sentry/react/RNSentryStart.java | 311 ++++++++++++++++++ 5 files changed, 505 insertions(+), 446 deletions(-) create mode 100644 packages/core/RNSentryAndroidTester/app/src/test/java/io/sentry/react/RNSentryStartTest.kt create mode 100644 packages/core/android/src/main/java/io/sentry/react/RNSentryStart.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 226a44056b..1a40b6ffbf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ - Initialize `RNSentryTimeToDisplay` during native module `init` on iOS ([#4443](https://github.com/getsentry/sentry-react-native/pull/4443)) - Extract iOS native initialization to standalone structures ([#4442](https://github.com/getsentry/sentry-react-native/pull/4442)) +- Extract Android native initialization to standalone structures ([#4445](https://github.com/getsentry/sentry-react-native/pull/4445)) ### Dependencies diff --git a/packages/core/RNSentryAndroidTester/app/src/test/java/io/sentry/react/RNSentryModuleImplTest.kt b/packages/core/RNSentryAndroidTester/app/src/test/java/io/sentry/react/RNSentryModuleImplTest.kt index adffbf78ad..34af996a76 100644 --- a/packages/core/RNSentryAndroidTester/app/src/test/java/io/sentry/react/RNSentryModuleImplTest.kt +++ b/packages/core/RNSentryAndroidTester/app/src/test/java/io/sentry/react/RNSentryModuleImplTest.kt @@ -3,20 +3,13 @@ package io.sentry.react import android.content.pm.PackageInfo import android.content.pm.PackageManager import com.facebook.react.bridge.Arguments -import com.facebook.react.bridge.JavaOnlyMap import com.facebook.react.bridge.Promise import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.bridge.WritableMap -import com.facebook.react.common.JavascriptException -import io.sentry.Breadcrumb import io.sentry.ILogger import io.sentry.SentryLevel -import io.sentry.android.core.SentryAndroidOptions import org.junit.After import org.junit.Assert.assertEquals -import org.junit.Assert.assertFalse -import org.junit.Assert.assertNull -import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -103,163 +96,4 @@ class RNSentryModuleImplTest { val capturedMap = writableMapCaptor.value assertEquals(false, capturedMap.getBoolean("has_fetched")) } - - @Test - fun `when the spotlight option is enabled, the spotlight SentryAndroidOption is set to true and the default url is used`() { - val options = - JavaOnlyMap.of( - "spotlight", - true, - "defaultSidecarUrl", - "http://localhost:8969/teststream", - ) - val actualOptions = SentryAndroidOptions() - module.getSentryAndroidOptions(actualOptions, options, logger) - assert(actualOptions.isEnableSpotlight) - assertEquals("http://localhost:8969/teststream", actualOptions.spotlightConnectionUrl) - } - - @Test - fun `when the spotlight url is passed, the spotlight is enabled for the given url`() { - val options = JavaOnlyMap.of("spotlight", "http://localhost:8969/teststream") - val actualOptions = SentryAndroidOptions() - module.getSentryAndroidOptions(actualOptions, options, logger) - assert(actualOptions.isEnableSpotlight) - assertEquals("http://localhost:8969/teststream", actualOptions.spotlightConnectionUrl) - } - - @Test - fun `when the spotlight option is disabled, the spotlight SentryAndroidOption is set to false`() { - val options = JavaOnlyMap.of("spotlight", false) - val actualOptions = SentryAndroidOptions() - module.getSentryAndroidOptions(actualOptions, options, logger) - assertFalse(actualOptions.isEnableSpotlight) - } - - @Test - fun `the JavascriptException is added to the ignoredExceptionsForType list on initialisation`() { - val actualOptions = SentryAndroidOptions() - module.getSentryAndroidOptions(actualOptions, JavaOnlyMap.of(), logger) - assertTrue(actualOptions.ignoredExceptionsForType.contains(JavascriptException::class.java)) - } - - @Test - fun `beforeBreadcrumb callback filters out Sentry DSN requests breadcrumbs`() { - val options = SentryAndroidOptions() - val rnOptions = - JavaOnlyMap.of( - "dsn", - "https://abc@def.ingest.sentry.io/1234567", - "devServerUrl", - "http://localhost:8081", - ) - module.getSentryAndroidOptions(options, rnOptions, logger) - - val breadcrumb = - Breadcrumb().apply { - type = "http" - setData("url", "https://def.ingest.sentry.io/1234567") - } - - val result = options.beforeBreadcrumb?.execute(breadcrumb, mock()) - - assertNull("Breadcrumb should be filtered out", result) - } - - @Test - fun `beforeBreadcrumb callback filters out dev server breadcrumbs`() { - val mockDevServerUrl = "http://localhost:8081" - val options = SentryAndroidOptions() - val rnOptions = - JavaOnlyMap.of( - "dsn", - "https://abc@def.ingest.sentry.io/1234567", - "devServerUrl", - mockDevServerUrl, - ) - module.getSentryAndroidOptions(options, rnOptions, logger) - - val breadcrumb = - Breadcrumb().apply { - type = "http" - setData("url", mockDevServerUrl) - } - - val result = options.beforeBreadcrumb?.execute(breadcrumb, mock()) - - assertNull("Breadcrumb should be filtered out", result) - } - - @Test - fun `beforeBreadcrumb callback does not filter out non dev server or dsn breadcrumbs`() { - val options = SentryAndroidOptions() - val rnOptions = - JavaOnlyMap.of( - "dsn", - "https://abc@def.ingest.sentry.io/1234567", - "devServerUrl", - "http://localhost:8081", - ) - module.getSentryAndroidOptions(options, rnOptions, logger) - - val breadcrumb = - Breadcrumb().apply { - type = "http" - setData("url", "http://testurl.com/service") - } - - val result = options.beforeBreadcrumb?.execute(breadcrumb, mock()) - - assertEquals(breadcrumb, result) - } - - @Test - fun `the breadcrumb is not filtered out when the dev server url and dsn are not passed`() { - val options = SentryAndroidOptions() - module.getSentryAndroidOptions(options, JavaOnlyMap(), logger) - - val breadcrumb = - Breadcrumb().apply { - type = "http" - setData("url", "http://testurl.com/service") - } - - val result = options.beforeBreadcrumb?.execute(breadcrumb, mock()) - - assertEquals(breadcrumb, result) - } - - @Test - fun `the breadcrumb is not filtered out when the dev server url is not passed and the dsn does not match`() { - val options = SentryAndroidOptions() - val rnOptions = JavaOnlyMap.of("dsn", "https://abc@def.ingest.sentry.io/1234567") - module.getSentryAndroidOptions(options, rnOptions, logger) - - val breadcrumb = - Breadcrumb().apply { - type = "http" - setData("url", "http://testurl.com/service") - } - - val result = options.beforeBreadcrumb?.execute(breadcrumb, mock()) - - assertEquals(breadcrumb, result) - } - - @Test - fun `the breadcrumb is not filtered out when the dev server url does not match and the dsn is not passed`() { - val options = SentryAndroidOptions() - val rnOptions = JavaOnlyMap.of("devServerUrl", "http://localhost:8081") - module.getSentryAndroidOptions(options, rnOptions, logger) - - val breadcrumb = - Breadcrumb().apply { - type = "http" - setData("url", "http://testurl.com/service") - } - - val result = options.beforeBreadcrumb?.execute(breadcrumb, mock()) - - assertEquals(breadcrumb, result) - } } diff --git a/packages/core/RNSentryAndroidTester/app/src/test/java/io/sentry/react/RNSentryStartTest.kt b/packages/core/RNSentryAndroidTester/app/src/test/java/io/sentry/react/RNSentryStartTest.kt new file mode 100644 index 0000000000..c2ee6f1d88 --- /dev/null +++ b/packages/core/RNSentryAndroidTester/app/src/test/java/io/sentry/react/RNSentryStartTest.kt @@ -0,0 +1,191 @@ +package io.sentry.react + +import android.app.Activity +import com.facebook.react.bridge.JavaOnlyMap +import com.facebook.react.common.JavascriptException +import io.sentry.Breadcrumb +import io.sentry.ILogger +import io.sentry.android.core.SentryAndroidOptions +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.mockito.Mockito.mock +import org.mockito.MockitoAnnotations + +@RunWith(JUnit4::class) +class RNSentryStartTest { + private lateinit var logger: ILogger + + private lateinit var activity: Activity + + @Before + fun setUp() { + MockitoAnnotations.openMocks(this) + logger = mock(ILogger::class.java) + activity = mock(Activity::class.java) + } + + @Test + fun `when the spotlight option is enabled, the spotlight SentryAndroidOption is set to true and the default url is used`() { + val options = + JavaOnlyMap.of( + "spotlight", + true, + "defaultSidecarUrl", + "http://localhost:8969/teststream", + ) + val actualOptions = SentryAndroidOptions() + RNSentryStart.getSentryAndroidOptions(actualOptions, options, activity, logger) + assert(actualOptions.isEnableSpotlight) + assertEquals("http://localhost:8969/teststream", actualOptions.spotlightConnectionUrl) + } + + @Test + fun `when the spotlight url is passed, the spotlight is enabled for the given url`() { + val options = JavaOnlyMap.of("spotlight", "http://localhost:8969/teststream") + val actualOptions = SentryAndroidOptions() + RNSentryStart.getSentryAndroidOptions(actualOptions, options, activity, logger) + assert(actualOptions.isEnableSpotlight) + assertEquals("http://localhost:8969/teststream", actualOptions.spotlightConnectionUrl) + } + + @Test + fun `when the spotlight option is disabled, the spotlight SentryAndroidOption is set to false`() { + val options = JavaOnlyMap.of("spotlight", false) + val actualOptions = SentryAndroidOptions() + RNSentryStart.getSentryAndroidOptions(actualOptions, options, activity, logger) + assertFalse(actualOptions.isEnableSpotlight) + } + + @Test + fun `the JavascriptException is added to the ignoredExceptionsForType list on initialisation`() { + val actualOptions = SentryAndroidOptions() + RNSentryStart.getSentryAndroidOptions(actualOptions, JavaOnlyMap.of(), activity, logger) + assertTrue(actualOptions.ignoredExceptionsForType.contains(JavascriptException::class.java)) + } + + @Test + fun `beforeBreadcrumb callback filters out Sentry DSN requests breadcrumbs`() { + val options = SentryAndroidOptions() + val rnOptions = + JavaOnlyMap.of( + "dsn", + "https://abc@def.ingest.sentry.io/1234567", + "devServerUrl", + "http://localhost:8081", + ) + RNSentryStart.getSentryAndroidOptions(options, rnOptions, activity, logger) + + val breadcrumb = + Breadcrumb().apply { + type = "http" + setData("url", "https://def.ingest.sentry.io/1234567") + } + + val result = options.beforeBreadcrumb?.execute(breadcrumb, mock()) + + assertNull("Breadcrumb should be filtered out", result) + } + + @Test + fun `beforeBreadcrumb callback filters out dev server breadcrumbs`() { + val mockDevServerUrl = "http://localhost:8081" + val options = SentryAndroidOptions() + val rnOptions = + JavaOnlyMap.of( + "dsn", + "https://abc@def.ingest.sentry.io/1234567", + "devServerUrl", + mockDevServerUrl, + ) + RNSentryStart.getSentryAndroidOptions(options, rnOptions, activity, logger) + + val breadcrumb = + Breadcrumb().apply { + type = "http" + setData("url", mockDevServerUrl) + } + + val result = options.beforeBreadcrumb?.execute(breadcrumb, mock()) + + assertNull("Breadcrumb should be filtered out", result) + } + + @Test + fun `beforeBreadcrumb callback does not filter out non dev server or dsn breadcrumbs`() { + val options = SentryAndroidOptions() + val rnOptions = + JavaOnlyMap.of( + "dsn", + "https://abc@def.ingest.sentry.io/1234567", + "devServerUrl", + "http://localhost:8081", + ) + RNSentryStart.getSentryAndroidOptions(options, rnOptions, activity, logger) + + val breadcrumb = + Breadcrumb().apply { + type = "http" + setData("url", "http://testurl.com/service") + } + + val result = options.beforeBreadcrumb?.execute(breadcrumb, mock()) + + assertEquals(breadcrumb, result) + } + + @Test + fun `the breadcrumb is not filtered out when the dev server url and dsn are not passed`() { + val options = SentryAndroidOptions() + RNSentryStart.getSentryAndroidOptions(options, JavaOnlyMap(), activity, logger) + + val breadcrumb = + Breadcrumb().apply { + type = "http" + setData("url", "http://testurl.com/service") + } + + val result = options.beforeBreadcrumb?.execute(breadcrumb, mock()) + + assertEquals(breadcrumb, result) + } + + @Test + fun `the breadcrumb is not filtered out when the dev server url is not passed and the dsn does not match`() { + val options = SentryAndroidOptions() + val rnOptions = JavaOnlyMap.of("dsn", "https://abc@def.ingest.sentry.io/1234567") + RNSentryStart.getSentryAndroidOptions(options, rnOptions, activity, logger) + + val breadcrumb = + Breadcrumb().apply { + type = "http" + setData("url", "http://testurl.com/service") + } + + val result = options.beforeBreadcrumb?.execute(breadcrumb, mock()) + + assertEquals(breadcrumb, result) + } + + @Test + fun `the breadcrumb is not filtered out when the dev server url does not match and the dsn is not passed`() { + val options = SentryAndroidOptions() + val rnOptions = JavaOnlyMap.of("devServerUrl", "http://localhost:8081") + RNSentryStart.getSentryAndroidOptions(options, rnOptions, activity, logger) + + val breadcrumb = + Breadcrumb().apply { + type = "http" + setData("url", "http://testurl.com/service") + } + + val result = options.beforeBreadcrumb?.execute(breadcrumb, mock()) + + assertEquals(breadcrumb, result) + } +} diff --git a/packages/core/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java b/packages/core/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java index 486ae72c48..3ffc3eb617 100644 --- a/packages/core/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java +++ b/packages/core/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java @@ -20,13 +20,11 @@ import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReadableMap; import com.facebook.react.bridge.ReadableMapKeySetIterator; -import com.facebook.react.bridge.ReadableType; import com.facebook.react.bridge.UiThreadUtil; import com.facebook.react.bridge.WritableArray; import com.facebook.react.bridge.WritableMap; import com.facebook.react.bridge.WritableNativeArray; import com.facebook.react.bridge.WritableNativeMap; -import com.facebook.react.common.JavascriptException; import com.facebook.react.modules.core.DeviceEventManagerModule; import io.sentry.Breadcrumb; import io.sentry.HubAdapter; @@ -34,25 +32,16 @@ import io.sentry.IScope; import io.sentry.ISentryExecutorService; import io.sentry.ISerializer; -import io.sentry.Integration; import io.sentry.Sentry; import io.sentry.SentryDate; import io.sentry.SentryDateProvider; -import io.sentry.SentryEvent; import io.sentry.SentryExecutorService; import io.sentry.SentryLevel; import io.sentry.SentryOptions; -import io.sentry.SentryReplayOptions; -import io.sentry.UncaughtExceptionHandlerIntegration; import io.sentry.android.core.AndroidLogger; import io.sentry.android.core.AndroidProfiler; -import io.sentry.android.core.AnrIntegration; -import io.sentry.android.core.BuildConfig; import io.sentry.android.core.BuildInfoProvider; -import io.sentry.android.core.CurrentActivityHolder; import io.sentry.android.core.InternalSentrySdk; -import io.sentry.android.core.NdkIntegration; -import io.sentry.android.core.SentryAndroid; import io.sentry.android.core.SentryAndroidDateProvider; import io.sentry.android.core.SentryAndroidOptions; import io.sentry.android.core.ViewHierarchyEventProcessor; @@ -61,11 +50,8 @@ import io.sentry.android.core.performance.AppStartMetrics; import io.sentry.protocol.SdkVersion; import io.sentry.protocol.SentryId; -import io.sentry.protocol.SentryPackage; import io.sentry.protocol.User; import io.sentry.protocol.ViewHierarchy; -import io.sentry.react.replay.RNSentryReplayMask; -import io.sentry.react.replay.RNSentryReplayUnmask; import io.sentry.util.DebugMetaPropertiesApplier; import io.sentry.util.FileUtils; import io.sentry.util.JsonSerializationUtils; @@ -77,8 +63,6 @@ import java.io.FileReader; import java.io.IOException; import java.io.InputStream; -import java.net.URI; -import java.net.URISyntaxException; import java.nio.charset.Charset; import java.util.HashMap; import java.util.Iterator; @@ -178,216 +162,12 @@ public void initNativeReactNavigationNewFrameTracking(Promise promise) { } public void initNativeSdk(final ReadableMap rnOptions, Promise promise) { - SentryAndroid.init( - this.getReactApplicationContext(), - options -> getSentryAndroidOptions(options, rnOptions, logger)); + RNSentryStart.startWithOptions( + this.getReactApplicationContext(), rnOptions, getCurrentActivity(), logger); promise.resolve(true); } - protected void getSentryAndroidOptions( - @NotNull SentryAndroidOptions options, @NotNull ReadableMap rnOptions, ILogger logger) { - @Nullable SdkVersion sdkVersion = options.getSdkVersion(); - if (sdkVersion == null) { - sdkVersion = new SdkVersion(RNSentryVersion.ANDROID_SDK_NAME, BuildConfig.VERSION_NAME); - } else { - sdkVersion.setName(RNSentryVersion.ANDROID_SDK_NAME); - } - sdkVersion.addPackage( - RNSentryVersion.REACT_NATIVE_SDK_PACKAGE_NAME, - RNSentryVersion.REACT_NATIVE_SDK_PACKAGE_VERSION); - - options.setSentryClientName(sdkVersion.getName() + "/" + sdkVersion.getVersion()); - options.setNativeSdkName(RNSentryVersion.NATIVE_SDK_NAME); - options.setSdkVersion(sdkVersion); - - if (rnOptions.hasKey("debug") && rnOptions.getBoolean("debug")) { - options.setDebug(true); - } - if (rnOptions.hasKey("dsn") && rnOptions.getString("dsn") != null) { - String dsn = rnOptions.getString("dsn"); - logger.log(SentryLevel.INFO, String.format("Starting with DSN: '%s'", dsn)); - options.setDsn(dsn); - } else { - // SentryAndroid needs an empty string fallback for the dsn. - options.setDsn(""); - } - if (rnOptions.hasKey("sampleRate")) { - options.setSampleRate(rnOptions.getDouble("sampleRate")); - } - if (rnOptions.hasKey("sendClientReports")) { - options.setSendClientReports(rnOptions.getBoolean("sendClientReports")); - } - if (rnOptions.hasKey("maxBreadcrumbs")) { - options.setMaxBreadcrumbs(rnOptions.getInt("maxBreadcrumbs")); - } - if (rnOptions.hasKey("maxCacheItems")) { - options.setMaxCacheItems(rnOptions.getInt("maxCacheItems")); - } - if (rnOptions.hasKey("environment") && rnOptions.getString("environment") != null) { - options.setEnvironment(rnOptions.getString("environment")); - } - if (rnOptions.hasKey("release") && rnOptions.getString("release") != null) { - options.setRelease(rnOptions.getString("release")); - } - if (rnOptions.hasKey("dist") && rnOptions.getString("dist") != null) { - options.setDist(rnOptions.getString("dist")); - } - if (rnOptions.hasKey("enableAutoSessionTracking")) { - options.setEnableAutoSessionTracking(rnOptions.getBoolean("enableAutoSessionTracking")); - } - if (rnOptions.hasKey("sessionTrackingIntervalMillis")) { - options.setSessionTrackingIntervalMillis(rnOptions.getInt("sessionTrackingIntervalMillis")); - } - if (rnOptions.hasKey("shutdownTimeout")) { - options.setShutdownTimeoutMillis(rnOptions.getInt("shutdownTimeout")); - } - if (rnOptions.hasKey("enableNdkScopeSync")) { - options.setEnableScopeSync(rnOptions.getBoolean("enableNdkScopeSync")); - } - if (rnOptions.hasKey("attachStacktrace")) { - options.setAttachStacktrace(rnOptions.getBoolean("attachStacktrace")); - } - if (rnOptions.hasKey("attachThreads")) { - // JS use top level stacktrace and android attaches Threads which hides them so - // by default we hide. - options.setAttachThreads(rnOptions.getBoolean("attachThreads")); - } - if (rnOptions.hasKey("attachScreenshot")) { - options.setAttachScreenshot(rnOptions.getBoolean("attachScreenshot")); - } - if (rnOptions.hasKey("attachViewHierarchy")) { - options.setAttachViewHierarchy(rnOptions.getBoolean("attachViewHierarchy")); - } - if (rnOptions.hasKey("sendDefaultPii")) { - options.setSendDefaultPii(rnOptions.getBoolean("sendDefaultPii")); - } - if (rnOptions.hasKey("maxQueueSize")) { - options.setMaxQueueSize(rnOptions.getInt("maxQueueSize")); - } - if (rnOptions.hasKey("enableNdk")) { - options.setEnableNdk(rnOptions.getBoolean("enableNdk")); - } - if (rnOptions.hasKey("spotlight")) { - if (rnOptions.getType("spotlight") == ReadableType.Boolean) { - options.setEnableSpotlight(rnOptions.getBoolean("spotlight")); - options.setSpotlightConnectionUrl(rnOptions.getString("defaultSidecarUrl")); - } else if (rnOptions.getType("spotlight") == ReadableType.String) { - options.setEnableSpotlight(true); - options.setSpotlightConnectionUrl(rnOptions.getString("spotlight")); - } - } - - SentryReplayOptions replayOptions = getReplayOptions(rnOptions); - options.setSessionReplay(replayOptions); - if (isReplayEnabled(replayOptions)) { - options.getReplayController().setBreadcrumbConverter(new RNSentryReplayBreadcrumbConverter()); - } - - // Exclude Dev Server and Sentry Dsn request from Breadcrumbs - String dsn = getURLFromDSN(rnOptions.getString("dsn")); - String devServerUrl = rnOptions.getString("devServerUrl"); - options.setBeforeBreadcrumb( - (breadcrumb, hint) -> { - Object urlObject = breadcrumb.getData("url"); - String url = urlObject instanceof String ? (String) urlObject : ""; - if ("http".equals(breadcrumb.getType()) - && ((dsn != null && url.startsWith(dsn)) - || (devServerUrl != null && url.startsWith(devServerUrl)))) { - return null; - } - return breadcrumb; - }); - - // React native internally throws a JavascriptException. - // we want to ignore it on the native side to avoid sending it twice. - options.addIgnoredExceptionForType(JavascriptException.class); - - options.setBeforeSend( - (event, hint) -> { - setEventOriginTag(event); - addPackages(event, options.getSdkVersion()); - - return event; - }); - - if (rnOptions.hasKey("enableNativeCrashHandling") - && !rnOptions.getBoolean("enableNativeCrashHandling")) { - final List integrations = options.getIntegrations(); - for (final Integration integration : integrations) { - if (integration instanceof UncaughtExceptionHandlerIntegration - || integration instanceof AnrIntegration - || integration instanceof NdkIntegration) { - integrations.remove(integration); - } - } - } - logger.log( - SentryLevel.INFO, String.format("Native Integrations '%s'", options.getIntegrations())); - - final CurrentActivityHolder currentActivityHolder = CurrentActivityHolder.getInstance(); - final Activity currentActivity = getCurrentActivity(); - if (currentActivity != null) { - currentActivityHolder.setActivity(currentActivity); - } - } - - private boolean isReplayEnabled(SentryReplayOptions replayOptions) { - return replayOptions.getSessionSampleRate() != null - || replayOptions.getOnErrorSampleRate() != null; - } - - private SentryReplayOptions getReplayOptions(@NotNull ReadableMap rnOptions) { - final SdkVersion replaySdkVersion = - new SdkVersion( - RNSentryVersion.REACT_NATIVE_SDK_NAME, - RNSentryVersion.REACT_NATIVE_SDK_PACKAGE_VERSION); - @NotNull - final SentryReplayOptions androidReplayOptions = - new SentryReplayOptions(false, replaySdkVersion); - - if (!(rnOptions.hasKey("replaysSessionSampleRate") - || rnOptions.hasKey("replaysOnErrorSampleRate"))) { - return androidReplayOptions; - } - - androidReplayOptions.setSessionSampleRate( - rnOptions.hasKey("replaysSessionSampleRate") - ? rnOptions.getDouble("replaysSessionSampleRate") - : null); - androidReplayOptions.setOnErrorSampleRate( - rnOptions.hasKey("replaysOnErrorSampleRate") - ? rnOptions.getDouble("replaysOnErrorSampleRate") - : null); - - if (!rnOptions.hasKey("mobileReplayOptions")) { - return androidReplayOptions; - } - @Nullable final ReadableMap rnMobileReplayOptions = rnOptions.getMap("mobileReplayOptions"); - if (rnMobileReplayOptions == null) { - return androidReplayOptions; - } - - androidReplayOptions.setMaskAllText( - !rnMobileReplayOptions.hasKey("maskAllText") - || rnMobileReplayOptions.getBoolean("maskAllText")); - androidReplayOptions.setMaskAllImages( - !rnMobileReplayOptions.hasKey("maskAllImages") - || rnMobileReplayOptions.getBoolean("maskAllImages")); - - final boolean redactVectors = - !rnMobileReplayOptions.hasKey("maskAllVectors") - || rnMobileReplayOptions.getBoolean("maskAllVectors"); - if (redactVectors) { - androidReplayOptions.addMaskViewClass("com.horcrux.svg.SvgView"); // react-native-svg - } - - androidReplayOptions.setMaskViewContainerClass(RNSentryReplayMask.class.getName()); - androidReplayOptions.setUnmaskViewContainerClass(RNSentryReplayUnmask.class.getName()); - - return androidReplayOptions; - } - public void crash() { throw new RuntimeException("TEST - Sentry Client Crash (only works in release mode)"); } @@ -974,51 +754,6 @@ public void crashedLastRun(Promise promise) { promise.resolve(Sentry.isCrashedLastRun()); } - private void setEventOriginTag(SentryEvent event) { - // We hardcode native-java as only java events are processed by the Android SDK. - SdkVersion sdk = event.getSdk(); - if (sdk != null) { - switch (sdk.getName()) { - case RNSentryVersion.NATIVE_SDK_NAME: - setEventEnvironmentTag(event, "native"); - break; - case RNSentryVersion.ANDROID_SDK_NAME: - setEventEnvironmentTag(event, "java"); - break; - default: - break; - } - } - } - - private void setEventEnvironmentTag(SentryEvent event, String environment) { - event.setTag("event.origin", "android"); - event.setTag("event.environment", environment); - } - - private void addPackages(SentryEvent event, SdkVersion sdk) { - SdkVersion eventSdk = event.getSdk(); - if (eventSdk != null - && "sentry.javascript.react-native".equals(eventSdk.getName()) - && sdk != null) { - List sentryPackages = sdk.getPackages(); - if (sentryPackages != null) { - for (SentryPackage sentryPackage : sentryPackages) { - eventSdk.addPackage(sentryPackage.getName(), sentryPackage.getVersion()); - } - } - - List integrations = sdk.getIntegrations(); - if (integrations != null) { - for (String integration : integrations) { - eventSdk.addIntegration(integration); - } - } - - event.setSdk(eventSdk); - } - } - private boolean checkAndroidXAvailability() { try { Class.forName("androidx.core.app.FrameMetricsAggregator"); @@ -1032,17 +767,4 @@ private boolean checkAndroidXAvailability() { private boolean isFrameMetricsAggregatorAvailable() { return androidXAvailable && frameMetricsAggregator != null; } - - public static @Nullable String getURLFromDSN(@Nullable String dsn) { - if (dsn == null) { - return null; - } - URI uri = null; - try { - uri = new URI(dsn); - } catch (URISyntaxException e) { - return null; - } - return uri.getScheme() + "://" + uri.getHost(); - } } diff --git a/packages/core/android/src/main/java/io/sentry/react/RNSentryStart.java b/packages/core/android/src/main/java/io/sentry/react/RNSentryStart.java new file mode 100644 index 0000000000..263633c4a8 --- /dev/null +++ b/packages/core/android/src/main/java/io/sentry/react/RNSentryStart.java @@ -0,0 +1,311 @@ +package io.sentry.react; + +import android.app.Activity; +import android.content.Context; +import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.bridge.ReadableType; +import com.facebook.react.common.JavascriptException; +import io.sentry.ILogger; +import io.sentry.Integration; +import io.sentry.SentryEvent; +import io.sentry.SentryLevel; +import io.sentry.SentryReplayOptions; +import io.sentry.UncaughtExceptionHandlerIntegration; +import io.sentry.android.core.AnrIntegration; +import io.sentry.android.core.BuildConfig; +import io.sentry.android.core.CurrentActivityHolder; +import io.sentry.android.core.NdkIntegration; +import io.sentry.android.core.SentryAndroid; +import io.sentry.android.core.SentryAndroidOptions; +import io.sentry.protocol.SdkVersion; +import io.sentry.protocol.SentryPackage; +import io.sentry.react.replay.RNSentryReplayMask; +import io.sentry.react.replay.RNSentryReplayUnmask; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.List; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public final class RNSentryStart { + + private RNSentryStart() { + throw new AssertionError("Utility class should not be instantiated"); + } + + public static void startWithOptions( + @NotNull final Context context, + @NotNull final ReadableMap rnOptions, + @Nullable Activity currentActivity, + @NotNull ILogger logger) { + SentryAndroid.init( + context, options -> getSentryAndroidOptions(options, rnOptions, currentActivity, logger)); + } + + static void getSentryAndroidOptions( + @NotNull SentryAndroidOptions options, + @NotNull ReadableMap rnOptions, + @Nullable Activity currentActivity, + ILogger logger) { + @Nullable SdkVersion sdkVersion = options.getSdkVersion(); + if (sdkVersion == null) { + sdkVersion = new SdkVersion(RNSentryVersion.ANDROID_SDK_NAME, BuildConfig.VERSION_NAME); + } else { + sdkVersion.setName(RNSentryVersion.ANDROID_SDK_NAME); + } + sdkVersion.addPackage( + RNSentryVersion.REACT_NATIVE_SDK_PACKAGE_NAME, + RNSentryVersion.REACT_NATIVE_SDK_PACKAGE_VERSION); + + options.setSentryClientName(sdkVersion.getName() + "/" + sdkVersion.getVersion()); + options.setNativeSdkName(RNSentryVersion.NATIVE_SDK_NAME); + options.setSdkVersion(sdkVersion); + + if (rnOptions.hasKey("debug") && rnOptions.getBoolean("debug")) { + options.setDebug(true); + } + if (rnOptions.hasKey("dsn") && rnOptions.getString("dsn") != null) { + String dsn = rnOptions.getString("dsn"); + logger.log(SentryLevel.INFO, String.format("Starting with DSN: '%s'", dsn)); + options.setDsn(dsn); + } else { + // SentryAndroid needs an empty string fallback for the dsn. + options.setDsn(""); + } + if (rnOptions.hasKey("sampleRate")) { + options.setSampleRate(rnOptions.getDouble("sampleRate")); + } + if (rnOptions.hasKey("sendClientReports")) { + options.setSendClientReports(rnOptions.getBoolean("sendClientReports")); + } + if (rnOptions.hasKey("maxBreadcrumbs")) { + options.setMaxBreadcrumbs(rnOptions.getInt("maxBreadcrumbs")); + } + if (rnOptions.hasKey("maxCacheItems")) { + options.setMaxCacheItems(rnOptions.getInt("maxCacheItems")); + } + if (rnOptions.hasKey("environment") && rnOptions.getString("environment") != null) { + options.setEnvironment(rnOptions.getString("environment")); + } + if (rnOptions.hasKey("release") && rnOptions.getString("release") != null) { + options.setRelease(rnOptions.getString("release")); + } + if (rnOptions.hasKey("dist") && rnOptions.getString("dist") != null) { + options.setDist(rnOptions.getString("dist")); + } + if (rnOptions.hasKey("enableAutoSessionTracking")) { + options.setEnableAutoSessionTracking(rnOptions.getBoolean("enableAutoSessionTracking")); + } + if (rnOptions.hasKey("sessionTrackingIntervalMillis")) { + options.setSessionTrackingIntervalMillis(rnOptions.getInt("sessionTrackingIntervalMillis")); + } + if (rnOptions.hasKey("shutdownTimeout")) { + options.setShutdownTimeoutMillis(rnOptions.getInt("shutdownTimeout")); + } + if (rnOptions.hasKey("enableNdkScopeSync")) { + options.setEnableScopeSync(rnOptions.getBoolean("enableNdkScopeSync")); + } + if (rnOptions.hasKey("attachStacktrace")) { + options.setAttachStacktrace(rnOptions.getBoolean("attachStacktrace")); + } + if (rnOptions.hasKey("attachThreads")) { + // JS use top level stacktrace and android attaches Threads which hides them so + // by default we hide. + options.setAttachThreads(rnOptions.getBoolean("attachThreads")); + } + if (rnOptions.hasKey("attachScreenshot")) { + options.setAttachScreenshot(rnOptions.getBoolean("attachScreenshot")); + } + if (rnOptions.hasKey("attachViewHierarchy")) { + options.setAttachViewHierarchy(rnOptions.getBoolean("attachViewHierarchy")); + } + if (rnOptions.hasKey("sendDefaultPii")) { + options.setSendDefaultPii(rnOptions.getBoolean("sendDefaultPii")); + } + if (rnOptions.hasKey("maxQueueSize")) { + options.setMaxQueueSize(rnOptions.getInt("maxQueueSize")); + } + if (rnOptions.hasKey("enableNdk")) { + options.setEnableNdk(rnOptions.getBoolean("enableNdk")); + } + if (rnOptions.hasKey("spotlight")) { + if (rnOptions.getType("spotlight") == ReadableType.Boolean) { + options.setEnableSpotlight(rnOptions.getBoolean("spotlight")); + options.setSpotlightConnectionUrl(rnOptions.getString("defaultSidecarUrl")); + } else if (rnOptions.getType("spotlight") == ReadableType.String) { + options.setEnableSpotlight(true); + options.setSpotlightConnectionUrl(rnOptions.getString("spotlight")); + } + } + + SentryReplayOptions replayOptions = getReplayOptions(rnOptions); + options.setSessionReplay(replayOptions); + if (isReplayEnabled(replayOptions)) { + options.getReplayController().setBreadcrumbConverter(new RNSentryReplayBreadcrumbConverter()); + } + + // Exclude Dev Server and Sentry Dsn request from Breadcrumbs + String dsn = getURLFromDSN(rnOptions.getString("dsn")); + String devServerUrl = rnOptions.getString("devServerUrl"); + options.setBeforeBreadcrumb( + (breadcrumb, hint) -> { + Object urlObject = breadcrumb.getData("url"); + String url = urlObject instanceof String ? (String) urlObject : ""; + if ("http".equals(breadcrumb.getType()) + && ((dsn != null && url.startsWith(dsn)) + || (devServerUrl != null && url.startsWith(devServerUrl)))) { + return null; + } + return breadcrumb; + }); + + // React native internally throws a JavascriptException. + // we want to ignore it on the native side to avoid sending it twice. + options.addIgnoredExceptionForType(JavascriptException.class); + + options.setBeforeSend( + (event, hint) -> { + setEventOriginTag(event); + addPackages(event, options.getSdkVersion()); + + return event; + }); + + if (rnOptions.hasKey("enableNativeCrashHandling") + && !rnOptions.getBoolean("enableNativeCrashHandling")) { + final List integrations = options.getIntegrations(); + for (final Integration integration : integrations) { + if (integration instanceof UncaughtExceptionHandlerIntegration + || integration instanceof AnrIntegration + || integration instanceof NdkIntegration) { + integrations.remove(integration); + } + } + } + logger.log( + SentryLevel.INFO, String.format("Native Integrations '%s'", options.getIntegrations())); + + setCurrentActivity(currentActivity); + } + + private static void setCurrentActivity(Activity currentActivity) { + final CurrentActivityHolder currentActivityHolder = CurrentActivityHolder.getInstance(); + if (currentActivity != null) { + currentActivityHolder.setActivity(currentActivity); + } + } + + private static boolean isReplayEnabled(SentryReplayOptions replayOptions) { + return replayOptions.getSessionSampleRate() != null + || replayOptions.getOnErrorSampleRate() != null; + } + + private static SentryReplayOptions getReplayOptions(@NotNull ReadableMap rnOptions) { + final SdkVersion replaySdkVersion = + new SdkVersion( + RNSentryVersion.REACT_NATIVE_SDK_NAME, + RNSentryVersion.REACT_NATIVE_SDK_PACKAGE_VERSION); + @NotNull + final SentryReplayOptions androidReplayOptions = + new SentryReplayOptions(false, replaySdkVersion); + + if (!(rnOptions.hasKey("replaysSessionSampleRate") + || rnOptions.hasKey("replaysOnErrorSampleRate"))) { + return androidReplayOptions; + } + + androidReplayOptions.setSessionSampleRate( + rnOptions.hasKey("replaysSessionSampleRate") + ? rnOptions.getDouble("replaysSessionSampleRate") + : null); + androidReplayOptions.setOnErrorSampleRate( + rnOptions.hasKey("replaysOnErrorSampleRate") + ? rnOptions.getDouble("replaysOnErrorSampleRate") + : null); + + if (!rnOptions.hasKey("mobileReplayOptions")) { + return androidReplayOptions; + } + @Nullable final ReadableMap rnMobileReplayOptions = rnOptions.getMap("mobileReplayOptions"); + if (rnMobileReplayOptions == null) { + return androidReplayOptions; + } + + androidReplayOptions.setMaskAllText( + !rnMobileReplayOptions.hasKey("maskAllText") + || rnMobileReplayOptions.getBoolean("maskAllText")); + androidReplayOptions.setMaskAllImages( + !rnMobileReplayOptions.hasKey("maskAllImages") + || rnMobileReplayOptions.getBoolean("maskAllImages")); + + final boolean redactVectors = + !rnMobileReplayOptions.hasKey("maskAllVectors") + || rnMobileReplayOptions.getBoolean("maskAllVectors"); + if (redactVectors) { + androidReplayOptions.addMaskViewClass("com.horcrux.svg.SvgView"); // react-native-svg + } + + androidReplayOptions.setMaskViewContainerClass(RNSentryReplayMask.class.getName()); + androidReplayOptions.setUnmaskViewContainerClass(RNSentryReplayUnmask.class.getName()); + + return androidReplayOptions; + } + + private static void setEventOriginTag(SentryEvent event) { + // We hardcode native-java as only java events are processed by the Android SDK. + SdkVersion sdk = event.getSdk(); + if (sdk != null) { + switch (sdk.getName()) { + case RNSentryVersion.NATIVE_SDK_NAME: + setEventEnvironmentTag(event, "native"); + break; + case RNSentryVersion.ANDROID_SDK_NAME: + setEventEnvironmentTag(event, "java"); + break; + default: + break; + } + } + } + + private static void setEventEnvironmentTag(SentryEvent event, String environment) { + event.setTag("event.origin", "android"); + event.setTag("event.environment", environment); + } + + private static void addPackages(SentryEvent event, SdkVersion sdk) { + SdkVersion eventSdk = event.getSdk(); + if (eventSdk != null + && "sentry.javascript.react-native".equals(eventSdk.getName()) + && sdk != null) { + List sentryPackages = sdk.getPackages(); + if (sentryPackages != null) { + for (SentryPackage sentryPackage : sentryPackages) { + eventSdk.addPackage(sentryPackage.getName(), sentryPackage.getVersion()); + } + } + + List integrations = sdk.getIntegrations(); + if (integrations != null) { + for (String integration : integrations) { + eventSdk.addIntegration(integration); + } + } + + event.setSdk(eventSdk); + } + } + + private static @Nullable String getURLFromDSN(@Nullable String dsn) { + if (dsn == null) { + return null; + } + URI uri = null; + try { + uri = new URI(dsn); + } catch (URISyntaxException e) { + return null; + } + return uri.getScheme() + "://" + uri.getHost(); + } +} From 7144a643afc47a146a5553fe5d358e713d38bdd1 Mon Sep 17 00:00:00 2001 From: Krystof Woldrich <31292499+krystofwoldrich@users.noreply.github.com> Date: Wed, 22 Jan 2025 14:24:46 +0100 Subject: [PATCH 03/32] feat(experimental): Add native `startWithConfigureOptions` for Apple platforms (#4444) --- CHANGELOG.md | 1 + packages/core/RNSentry.podspec | 2 +- .../project.pbxproj | 8 + ...RNSentryCocoaTesterTests-Bridging-Header.h | 3 + .../RNSentryStartTests.swift | 248 ++++++++++++++++++ .../RNSentryCocoaTesterTests/RNSentryTests.mm | 12 +- packages/core/ios/RNSentry.h | 3 + packages/core/ios/RNSentry.mm | 4 +- packages/core/ios/RNSentrySDK.h | 18 ++ packages/core/ios/RNSentrySDK.m | 17 ++ packages/core/ios/RNSentryStart.h | 6 + packages/core/ios/RNSentryStart.m | 97 ++++--- .../sentryreactnativesample/AppDelegate.mm | 6 + 13 files changed, 380 insertions(+), 45 deletions(-) create mode 100644 packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryStartTests.swift create mode 100644 packages/core/ios/RNSentrySDK.h create mode 100644 packages/core/ios/RNSentrySDK.m diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a40b6ffbf..4e9096102d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ - Rename `navigation.processing` span to more expressive `Navigation dispatch to screen A mounted/navigation cancelled` ([#4423](https://github.com/getsentry/sentry-react-native/pull/4423)) - Add RN SDK package to `sdk.packages` for Cocoa ([#4381](https://github.com/getsentry/sentry-react-native/pull/4381)) +- Add experimental version of `startWithConfigureOptions` for Apple platforms ([#4444](https://github.com/getsentry/sentry-react-native/pull/4444)) ### Internal diff --git a/packages/core/RNSentry.podspec b/packages/core/RNSentry.podspec index 97c4fd315c..3c595c08eb 100644 --- a/packages/core/RNSentry.podspec +++ b/packages/core/RNSentry.podspec @@ -33,7 +33,7 @@ Pod::Spec.new do |s| s.preserve_paths = '*.js' s.source_files = 'ios/**/*.{h,m,mm}' - s.public_header_files = 'ios/RNSentry.h' + s.public_header_files = 'ios/RNSentry.h', 'ios/RNSentrySDK.h' s.compiler_flags = other_cflags diff --git a/packages/core/RNSentryCocoaTester/RNSentryCocoaTester.xcodeproj/project.pbxproj b/packages/core/RNSentryCocoaTester/RNSentryCocoaTester.xcodeproj/project.pbxproj index 112c485d6f..1621383063 100644 --- a/packages/core/RNSentryCocoaTester/RNSentryCocoaTester.xcodeproj/project.pbxproj +++ b/packages/core/RNSentryCocoaTester/RNSentryCocoaTester.xcodeproj/project.pbxproj @@ -11,6 +11,7 @@ 336084392C32E382008CC412 /* RNSentryReplayBreadcrumbConverterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 336084382C32E382008CC412 /* RNSentryReplayBreadcrumbConverterTests.swift */; }; 3380C6C42CE25ECA0018B9B6 /* RNSentryReplayPostInitTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3380C6C32CE25ECA0018B9B6 /* RNSentryReplayPostInitTests.swift */; }; 33958C692BFCF12600AD1FB6 /* RNSentryOnDrawReporterTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 33958C682BFCF12600AD1FB6 /* RNSentryOnDrawReporterTests.m */; }; + 339C6C3C2D3EB25100CA72ED /* RNSentryStartTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 339C6C3B2D3EB23B00CA72ED /* RNSentryStartTests.swift */; }; 33AFDFED2B8D14B300AAB120 /* RNSentryFramesTrackerListenerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 33AFDFEC2B8D14B300AAB120 /* RNSentryFramesTrackerListenerTests.m */; }; 33AFDFF12B8D15E500AAB120 /* RNSentryDependencyContainerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 33AFDFF02B8D15E500AAB120 /* RNSentryDependencyContainerTests.m */; }; 33F58AD02977037D008F60EA /* RNSentryTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 33F58ACF2977037D008F60EA /* RNSentryTests.mm */; }; @@ -27,6 +28,7 @@ 332D334A2CDCC8EB00547D76 /* RNSentryCocoaTesterTests-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "RNSentryCocoaTesterTests-Bridging-Header.h"; sourceTree = ""; }; 333B58A82D35BA93000F8D04 /* RNSentryStart.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = RNSentryStart.h; path = ../ios/RNSentryStart.h; sourceTree = SOURCE_ROOT; }; 333B58A92D35BB2D000F8D04 /* RNSentryStart+Test.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "RNSentryStart+Test.h"; path = "RNSentryCocoaTesterTests/RNSentryStart+Test.h"; sourceTree = SOURCE_ROOT; }; + 333B58AF2D36A7FD000F8D04 /* RNSentrySDK.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = RNSentrySDK.h; path = ../ios/RNSentrySDK.h; sourceTree = SOURCE_ROOT; }; 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 = ""; }; 3360843C2C340C76008CC412 /* RNSentryBreadcrumbTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RNSentryBreadcrumbTests.swift; sourceTree = ""; }; @@ -37,6 +39,8 @@ 338739072A7D7D2800950DDD /* RNSentryReplay.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = RNSentryReplay.h; path = ../ios/RNSentryReplay.h; sourceTree = ""; }; 33958C672BFCEF5A00AD1FB6 /* RNSentryOnDrawReporter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RNSentryOnDrawReporter.h; path = ../ios/RNSentryOnDrawReporter.h; sourceTree = ""; }; 33958C682BFCF12600AD1FB6 /* RNSentryOnDrawReporterTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RNSentryOnDrawReporterTests.m; sourceTree = ""; }; + 339C6C3B2D3EB23B00CA72ED /* RNSentryStartTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RNSentryStartTests.swift; sourceTree = ""; }; + 339C6C3D2D3FA04D00CA72ED /* RNSentryVersion.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = RNSentryVersion.h; path = ../ios/RNSentryVersion.h; sourceTree = SOURCE_ROOT; }; 33AFDFEC2B8D14B300AAB120 /* RNSentryFramesTrackerListenerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RNSentryFramesTrackerListenerTests.m; sourceTree = ""; }; 33AFDFEE2B8D14C200AAB120 /* RNSentryFramesTrackerListenerTests.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RNSentryFramesTrackerListenerTests.h; sourceTree = ""; }; 33AFDFF02B8D15E500AAB120 /* RNSentryDependencyContainerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RNSentryDependencyContainerTests.m; sourceTree = ""; }; @@ -90,6 +94,7 @@ 3360899029524164007C7730 /* RNSentryCocoaTesterTests */ = { isa = PBXGroup; children = ( + 339C6C3B2D3EB23B00CA72ED /* RNSentryStartTests.swift */, 332D334A2CDCC8EB00547D76 /* RNSentryCocoaTesterTests-Bridging-Header.h */, 332D33492CDCC8E100547D76 /* RNSentryTests.h */, 336084382C32E382008CC412 /* RNSentryReplayBreadcrumbConverterTests.swift */, @@ -118,6 +123,8 @@ 33AFE0122B8F319000AAB120 /* RNSentry */ = { isa = PBXGroup; children = ( + 339C6C3D2D3FA04D00CA72ED /* RNSentryVersion.h */, + 333B58AF2D36A7FD000F8D04 /* RNSentrySDK.h */, 333B58A92D35BB2D000F8D04 /* RNSentryStart+Test.h */, 333B58A82D35BA93000F8D04 /* RNSentryStart.h */, 3380C6C02CDEC56B0018B9B6 /* Replay */, @@ -243,6 +250,7 @@ files = ( AEFB00422CC90C4B00EC8A9A /* RNSentryBreadcrumbTests.swift in Sources */, 332D33472CDBDBB600547D76 /* RNSentryReplayOptionsTests.swift in Sources */, + 339C6C3C2D3EB25100CA72ED /* RNSentryStartTests.swift in Sources */, 33AFDFF12B8D15E500AAB120 /* RNSentryDependencyContainerTests.m in Sources */, 336084392C32E382008CC412 /* RNSentryReplayBreadcrumbConverterTests.swift in Sources */, 33F58AD02977037D008F60EA /* RNSentryTests.mm in Sources */, diff --git a/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryCocoaTesterTests-Bridging-Header.h b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryCocoaTesterTests-Bridging-Header.h index bc2bdd0304..ba8d8f703d 100644 --- a/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryCocoaTesterTests-Bridging-Header.h +++ b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryCocoaTesterTests-Bridging-Header.h @@ -7,3 +7,6 @@ #import "RNSentryReplayBreadcrumbConverter.h" #import "RNSentryReplayMask.h" #import "RNSentryReplayUnmask.h" +#import "RNSentrySDK.h" +#import "RNSentryStart.h" +#import "RNSentryVersion.h" diff --git a/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryStartTests.swift b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryStartTests.swift new file mode 100644 index 0000000000..b9d12200cf --- /dev/null +++ b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryStartTests.swift @@ -0,0 +1,248 @@ +import XCTest + +final class RNSentryStartTests: XCTestCase { + + func testStartDoesNotThrowWithoutConfigure() { + RNSentrySDK.start(configureOptions: nil) + } + + func assertReactDefaults(_ actualOptions: Options?) { + XCTAssertFalse(actualOptions!.enableCaptureFailedRequests) + XCTAssertNil(actualOptions!.tracesSampleRate) + XCTAssertNil(actualOptions!.tracesSampler) + XCTAssertFalse(actualOptions!.enableTracing) + } + + func testStartSetsReactDeafults() { + var actualOptions: Options? + + RNSentrySDK.start { options in + actualOptions = options + } + + XCTAssertNotNil(actualOptions, "start have not provided default options or have not executed configure callback") + assertReactDefaults(actualOptions) + } + + func testAutoStartSetsReactDefaults() throws { + try startFromRN(options: [ + "dsn": "https://abcd@efgh.ingest.sentry.io/123456" + ]) + + let actualOptions = PrivateSentrySDKOnly.options + assertReactDefaults(actualOptions) + } + + func testStartEnablesHybridTracing() throws { + let testCases: [() throws -> Void] = [ + { + RNSentrySDK.start { options in + options.dsn = "https://abcd@efgh.ingest.sentry.io/123456" + } + }, + { + try self.startFromRN(options: [ + "dsn": "https://abcd@efgh.ingest.sentry.io/123456" + ]) + }, + { + RNSentrySDK.start { options in + options.dsn = "https://abcd@efgh.ingest.sentry.io/123456" + options.enableAutoPerformanceTracing = true + } + }, + { + try self.startFromRN(options: [ + "dsn": "https://abcd@efgh.ingest.sentry.io/123456", + "enableAutoPerformanceTracing": true + ]) + } + ] + + // Test each implementation + for startMethod in testCases { + try startMethod() + + let actualOptions = PrivateSentrySDKOnly.options + + XCTAssertTrue(PrivateSentrySDKOnly.appStartMeasurementHybridSDKMode) + XCTAssertTrue(PrivateSentrySDKOnly.framesTrackingMeasurementHybridSDKMode) + } + } + + func testStartDisablesHybridTracing() throws { + let testCases: [() throws -> Void] = [ + { + RNSentrySDK.start { options in + options.dsn = "https://abcd@efgh.ingest.sentry.io/123456" + options.enableAutoPerformanceTracing = false + } + }, + { + try self.startFromRN(options: [ + "dsn": "https://abcd@efgh.ingest.sentry.io/123456", + "enableAutoPerformanceTracing": false + ]) + } + ] + + for startMethod in testCases { + try startMethod() + + let actualOptions = PrivateSentrySDKOnly.options + + XCTAssertFalse(PrivateSentrySDKOnly.appStartMeasurementHybridSDKMode) + XCTAssertFalse(PrivateSentrySDKOnly.framesTrackingMeasurementHybridSDKMode) + } + } + + func testStartIgnoresUnhandledJsExceptions() throws { + let testCases: [() throws -> Void] = [ + { + RNSentrySDK.start { options in + options.dsn = "https://abcd@efgh.ingest.sentry.io/123456" + } + }, + { + try self.startFromRN(options: [ + "dsn": "https://abcd@efgh.ingest.sentry.io/123456" + ]) + } + ] + + for startMethod in testCases { + try startMethod() + + let actualOptions = PrivateSentrySDKOnly.options + + let actualEvent = actualOptions.beforeSend!(createUnhandledJsExceptionEvent()) + + XCTAssertNil(actualEvent) + } + } + + func testStartSetsNativeEventOrigin() throws { + let testCases: [() throws -> Void] = [ + { + RNSentrySDK.start { options in + options.dsn = "https://abcd@efgh.ingest.sentry.io/123456" + } + }, + { + try self.startFromRN(options: [ + "dsn": "https://abcd@efgh.ingest.sentry.io/123456" + ]) + } + ] + + for startMethod in testCases { + try startMethod() + + let actualOptions = PrivateSentrySDKOnly.options + + let actualEvent = actualOptions.beforeSend!(createNativeEvent()) + + XCTAssertNotNil(actualEvent) + XCTAssertNotNil(actualEvent!.tags) + XCTAssertEqual(actualEvent!.tags!["event.origin"], "ios") + XCTAssertEqual(actualEvent!.tags!["event.environment"], "native") + } + } + + func testStartDoesNotOverwriteUserBeforeSend() { + var executed = false + + RNSentrySDK.start { options in + options.dsn = "https://abcd@efgh.ingest.sentry.io/123456" + options.beforeSend = { event in + executed = true + return event + } + } + + PrivateSentrySDKOnly.options.beforeSend!(genericEvent()) + + XCTAssertTrue(executed) + } + + func testStartSetsHybridSdkName() throws { + let testCases: [() throws -> Void] = [ + { + RNSentrySDK.start { options in + options.dsn = "https://abcd@efgh.ingest.sentry.io/123456" + } + }, + { + try self.startFromRN(options: [ + "dsn": "https://abcd@efgh.ingest.sentry.io/123456" + ]) + } + ] + + for startMethod in testCases { + try startMethod() + + let actualEvent = captuteTestEvent() + + XCTAssertNotNil(actualEvent) + XCTAssertNotNil(actualEvent!.sdk) + XCTAssertEqual(actualEvent!.sdk!["name"] as! String, NATIVE_SDK_NAME) + + let packages = actualEvent!.sdk!["packages"] as! [[String: String]] + let reactPackage = packages.first { $0["name"] == REACT_NATIVE_SDK_PACKAGE_NAME } + + XCTAssertNotNil(reactPackage) + XCTAssertEqual(reactPackage!["name"], REACT_NATIVE_SDK_PACKAGE_NAME) + XCTAssertEqual(reactPackage!["version"], REACT_NATIVE_SDK_PACKAGE_VERSION) + } + } + + func startFromRN(options: [String: Any]) throws { + var error: NSError? + RNSentryStart.start(options: options, error: &error) + + if let error = error { + throw error + } + } + + func createUnhandledJsExceptionEvent() -> Event { + let event = Event() + event.exceptions = [] + event.exceptions!.append(Exception(value: "Test", type: "Unhandled JS Exception: undefined is not a function")) + return event + } + + func createNativeEvent() -> Event { + let event = Event() + event.sdk = [ + "name": NATIVE_SDK_NAME, + "version": "1.2.3" + ] + return event + } + + func genericEvent() -> Event { + return Event() + } + + func captuteTestEvent() -> Event? { + var actualEvent: Event? + + // This is the closest to the sent event we can get using the actual Sentry start method + let originalBeforeSend = PrivateSentrySDKOnly.options.beforeSend + PrivateSentrySDKOnly.options.beforeSend = { event in + if let originalBeforeSend = originalBeforeSend { + let processedEvent = originalBeforeSend(event) + actualEvent = processedEvent + return processedEvent + } + actualEvent = event + return event + } + + SentrySDK.capture(message: "Test") + + return actualEvent + } +} diff --git a/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryTests.mm b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryTests.mm index 9cefc4747a..abe2ae70ce 100644 --- a/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryTests.mm +++ b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryTests.mm @@ -2,6 +2,7 @@ #import "RNSentryStart+Test.h" #import #import +#import #import #import #import @@ -12,7 +13,7 @@ @interface RNSentryInitNativeSdkTests : XCTestCase @implementation RNSentryInitNativeSdkTests -- (void)testCreateOptionsWithDictionaryRemovesPerformanceProperties +- (void)testStartWithDictionaryRemovesPerformanceProperties { NSError *error = nil; @@ -25,9 +26,8 @@ - (void)testCreateOptionsWithDictionaryRemovesPerformanceProperties , @"enableTracing" : @YES, } ; -SentryOptions *actualOptions = - [RNSentryStart createOptionsWithDictionary:mockedReactNativeDictionary error:&error]; - +[RNSentryStart startWithOptions:mockedReactNativeDictionary error:&error]; +SentryOptions *actualOptions = PrivateSentrySDKOnly.options; XCTAssertNotNil(actualOptions, @"Did not create sentry options"); XCTAssertNil(error, @"Should not pass no error"); XCTAssertNotNil( @@ -45,8 +45,8 @@ - (void)testCaptureFailedRequestsIsDisabled NSDictionary *_Nonnull mockedReactNativeDictionary = @{ @"dsn" : @"https://abcd@efgh.ingest.sentry.io/123456", }; - SentryOptions *actualOptions = - [RNSentryStart createOptionsWithDictionary:mockedReactNativeDictionary error:&error]; + [RNSentryStart startWithOptions:mockedReactNativeDictionary error:&error]; + SentryOptions *actualOptions = PrivateSentrySDKOnly.options; XCTAssertNotNil(actualOptions, @"Did not create sentry options"); XCTAssertNil(error, @"Should not pass no error"); diff --git a/packages/core/ios/RNSentry.h b/packages/core/ios/RNSentry.h index 66dc7219ac..c7fb93e0ea 100644 --- a/packages/core/ios/RNSentry.h +++ b/packages/core/ios/RNSentry.h @@ -11,6 +11,9 @@ #import #import +// This import exposes public RNSentrySDK start +#import "RNSentrySDK.h" + typedef int (*SymbolicateCallbackType)(const void *, Dl_info *); @interface diff --git a/packages/core/ios/RNSentry.mm b/packages/core/ios/RNSentry.mm index 6907513da5..69fd287403 100644 --- a/packages/core/ios/RNSentry.mm +++ b/packages/core/ios/RNSentry.mm @@ -94,13 +94,11 @@ - (instancetype)init : (RCTPromiseRejectBlock)reject) { NSError *error = nil; - SentryOptions *sentryOptions = [RNSentryStart createOptionsWithDictionary:options error:&error]; + [RNSentryStart startWithOptions:options error:&error]; if (error != nil) { reject(@"SentryReactNative", error.localizedDescription, error); return; } - - [RNSentryStart startWithOptions:sentryOptions]; resolve(@YES); } diff --git a/packages/core/ios/RNSentrySDK.h b/packages/core/ios/RNSentrySDK.h new file mode 100644 index 0000000000..7d3512bb5d --- /dev/null +++ b/packages/core/ios/RNSentrySDK.h @@ -0,0 +1,18 @@ +#import + +@interface RNSentrySDK : NSObject +SENTRY_NO_INIT + +/** + * @experimental + * Inits and configures Sentry for React Native applications. Make sure to + * set a valid DSN. + * + * @discussion Call this method on the main thread. When calling it from a background thread, the + * SDK starts on the main thread async. + */ ++ (void)startWithConfigureOptions: + (void (^_Nullable)(SentryOptions *_Nonnull options))configureOptions + NS_SWIFT_NAME(start(configureOptions:)); + +@end diff --git a/packages/core/ios/RNSentrySDK.m b/packages/core/ios/RNSentrySDK.m new file mode 100644 index 0000000000..b7ed6f4a7b --- /dev/null +++ b/packages/core/ios/RNSentrySDK.m @@ -0,0 +1,17 @@ +#import "RNSentrySDK.h" +#import "RNSentryStart.h" + +@implementation RNSentrySDK + ++ (void)startWithConfigureOptions:(void (^)(SentryOptions *options))configureOptions +{ + SentryOptions *options = [[SentryOptions alloc] init]; + [RNSentryStart updateWithReactDefaults:options]; + if (configureOptions != nil) { + configureOptions(options); + } + [RNSentryStart updateWithReactFinals:options]; + [RNSentryStart startWithOptions:options]; +} + +@end diff --git a/packages/core/ios/RNSentryStart.h b/packages/core/ios/RNSentryStart.h index bc5adf35af..01a0617148 100644 --- a/packages/core/ios/RNSentryStart.h +++ b/packages/core/ios/RNSentryStart.h @@ -4,9 +4,15 @@ @interface RNSentryStart : NSObject SENTRY_NO_INIT ++ (void)startWithOptions:(NSDictionary *_Nonnull)javascriptOptions + error:(NSError *_Nullable *_Nullable)errorPointer; + + (SentryOptions *_Nullable)createOptionsWithDictionary:(NSDictionary *_Nonnull)options error:(NSError *_Nonnull *_Nonnull)errorPointer; ++ (void)updateWithReactDefaults:(SentryOptions *)options; ++ (void)updateWithReactFinals:(SentryOptions *)options; + /** * @experimental * Inits and configures Sentry for React Native applications. Make sure to diff --git a/packages/core/ios/RNSentryStart.m b/packages/core/ios/RNSentryStart.m index c364b6c630..b3d4d5d77e 100644 --- a/packages/core/ios/RNSentryStart.m +++ b/packages/core/ios/RNSentryStart.m @@ -8,6 +8,16 @@ @implementation RNSentryStart ++ (void)startWithOptions:(NSDictionary *_Nonnull)javascriptOptions + error:(NSError *_Nullable *_Nullable)errorPointer +{ + SentryOptions *options = [self createOptionsWithDictionary:javascriptOptions + error:errorPointer]; + [self updateWithReactDefaults:options]; + [self updateWithReactFinals:options]; + [self startWithOptions:options]; +} + + (void)startWithOptions:(SentryOptions *)options NS_SWIFT_NAME(start(options:)) { NSString *sdkVersion = [PrivateSentrySDKOnly getSdkVersionString]; @@ -27,30 +37,7 @@ + (void)startWithOptions:(SentryOptions *)options NS_SWIFT_NAME(start(options:)) + (SentryOptions *_Nullable)createOptionsWithDictionary:(NSDictionary *_Nonnull)options error:(NSError *_Nonnull *_Nonnull)errorPointer { - SentryBeforeSendEventCallback beforeSend = ^SentryEvent *(SentryEvent *event) - { - // We don't want to send an event after startup that came from a Unhandled JS Exception of - // react native Because we sent it already before the app crashed. - if (nil != event.exceptions.firstObject.type && - [event.exceptions.firstObject.type rangeOfString:@"Unhandled JS Exception"].location - != NSNotFound) { - return nil; - } - - [self setEventOriginTag:event]; - - return event; - }; - NSMutableDictionary *mutableOptions = [options mutableCopy]; - [mutableOptions setValue:beforeSend forKey:@"beforeSend"]; - - // remove performance traces sample rate and traces sampler since we don't want to synchronize - // these configurations to the Native SDKs. The user could tho initialize the SDK manually and - // set themselves. - [mutableOptions removeObjectForKey:@"tracesSampleRate"]; - [mutableOptions removeObjectForKey:@"tracesSampler"]; - [mutableOptions removeObjectForKey:@"enableTracing"]; #if SENTRY_TARGET_REPLAY_SUPPORTED [RNSentryReplay updateOptions:mutableOptions]; @@ -63,6 +50,7 @@ + (SentryOptions *_Nullable)createOptionsWithDictionary:(NSDictionary *_Nonnull) } // Exclude Dev Server and Sentry Dsn request from Breadcrumbs + // TODO: Migrate for manual init NSString *dsn = [self getURLFromDSN:[mutableOptions valueForKey:@"dsn"]]; NSString *devServerUrl = [mutableOptions valueForKey:@"devServerUrl"]; sentryOptions.beforeBreadcrumb @@ -105,20 +93,59 @@ + (SentryOptions *_Nullable)createOptionsWithDictionary:(NSDictionary *_Nonnull) } } - // Enable the App start and Frames tracking measurements - if ([mutableOptions valueForKey:@"enableAutoPerformanceTracing"] != nil) { - BOOL enableAutoPerformanceTracing = - [mutableOptions[@"enableAutoPerformanceTracing"] boolValue]; - PrivateSentrySDKOnly.appStartMeasurementHybridSDKMode = enableAutoPerformanceTracing; -#if TARGET_OS_IPHONE || TARGET_OS_MACCATALYST - PrivateSentrySDKOnly.framesTrackingMeasurementHybridSDKMode = enableAutoPerformanceTracing; -#endif - } + return sentryOptions; +} - // Failed requests can only be enabled in one SDK to avoid duplicates - sentryOptions.enableCaptureFailedRequests = NO; +/** + * This function updates the options with RNSentry defaults. These default can be + * overwritten by users during manual native initialization. + */ ++ (void)updateWithReactDefaults:(SentryOptions *)options +{ + // Failed requests are captured only in JS to avoid duplicates + options.enableCaptureFailedRequests = NO; - return sentryOptions; + // Tracing is only enabled in JS to avoid duplicate navigation spans + options.tracesSampleRate = nil; + options.tracesSampler = nil; + options.enableTracing = NO; +} + +/** + * This function updates options with changes RNSentry users should not change + * and so this is applied after the configureOptions callback during manual native initialization. + */ ++ (void)updateWithReactFinals:(SentryOptions *)options +{ + SentryBeforeSendEventCallback userBeforeSend = options.beforeSend; + options.beforeSend = ^SentryEvent *(SentryEvent *event) + { + // Unhandled JS Exception are processed by the SDK on JS layer + // To avoid duplicates we drop them in the native SDKs + if (nil != event.exceptions.firstObject.type && + [event.exceptions.firstObject.type rangeOfString:@"Unhandled JS Exception"].location + != NSNotFound) { + return nil; + } + + [self setEventOriginTag:event]; + if (userBeforeSend == nil) { + return event; + } else { + return userBeforeSend(event); + } + }; + + // App Start Hybrid mode doesn't wait for didFinishLaunchNotification and the + // didBecomeVisibleNotification as they will be missed when auto initializing from JS + // App Start measurements are created right after the tracking starts + PrivateSentrySDKOnly.appStartMeasurementHybridSDKMode = options.enableAutoPerformanceTracing; +#if TARGET_OS_IPHONE || TARGET_OS_MACCATALYST + // Frames Tracking Hybrid Mode ensures tracking + // is enabled without tracing enabled in the native SDK + PrivateSentrySDKOnly.framesTrackingMeasurementHybridSDKMode + = options.enableAutoPerformanceTracing; +#endif } + (void)setEventOriginTag:(SentryEvent *)event diff --git a/samples/react-native/ios/sentryreactnativesample/AppDelegate.mm b/samples/react-native/ios/sentryreactnativesample/AppDelegate.mm index 71a62884ac..2a6a0a0956 100644 --- a/samples/react-native/ios/sentryreactnativesample/AppDelegate.mm +++ b/samples/react-native/ios/sentryreactnativesample/AppDelegate.mm @@ -9,6 +9,7 @@ # import #endif +#import #import #import @@ -57,6 +58,11 @@ - (BOOL)application:(UIApplication *)application // When the native init is enabled the `autoInitializeNativeSdk` // in JS has to be set to `false` // [self initializeSentry]; + // [RNSentrySDK startWithConfigureOptions:^(SentryOptions *options) { + // options.dsn = + // @"https://1df17bd4e543fdb31351dee1768bb679@o447951.ingest.sentry.io/5428561"; + // options.debug = YES; + // }]; self.moduleName = @"sentry-react-native-sample"; // You can add your custom initial props in the dictionary below. From 78506779fe155a29fb893c1cd10a792b30b41928 Mon Sep 17 00:00:00 2001 From: Krystof Woldrich <31292499+krystofwoldrich@users.noreply.github.com> Date: Wed, 22 Jan 2025 17:55:44 +0100 Subject: [PATCH 04/32] feat: Read `sentry.options.json` during cocoa init (#4447) --- CHANGELOG.md | 1 + .../project.pbxproj | 37 ++++++ ...RNSentryCocoaTesterTests-Bridging-Header.h | 2 +- .../RNSentryStartFromFileTests.swift | 115 ++++++++++++++++++ .../RNSentryCocoaTester/RNSentrySDK+Test.h | 9 ++ .../TestAssets/invalid.options.json | 5 + .../TestAssets/invalid.options.txt | 1 + .../TestAssets/valid.options.json | 4 + packages/core/ios/RNSentrySDK.h | 17 ++- packages/core/ios/RNSentrySDK.m | 56 ++++++++- packages/core/ios/RNSentryStart.m | 5 +- packages/core/scripts/sentry-xcode.sh | 19 +++ .../sentryreactnativesample/AppDelegate.mm | 39 +----- samples/react-native/sentry.options.json | 20 +++ 14 files changed, 287 insertions(+), 43 deletions(-) create mode 100644 packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryStartFromFileTests.swift create mode 100644 packages/core/RNSentryCocoaTester/RNSentrySDK+Test.h create mode 100644 packages/core/RNSentryCocoaTester/TestAssets/invalid.options.json create mode 100644 packages/core/RNSentryCocoaTester/TestAssets/invalid.options.txt create mode 100644 packages/core/RNSentryCocoaTester/TestAssets/valid.options.json create mode 100644 samples/react-native/sentry.options.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e9096102d..ef2e9ed714 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ - Rename `navigation.processing` span to more expressive `Navigation dispatch to screen A mounted/navigation cancelled` ([#4423](https://github.com/getsentry/sentry-react-native/pull/4423)) - Add RN SDK package to `sdk.packages` for Cocoa ([#4381](https://github.com/getsentry/sentry-react-native/pull/4381)) - Add experimental version of `startWithConfigureOptions` for Apple platforms ([#4444](https://github.com/getsentry/sentry-react-native/pull/4444)) +- Add initialization using `sentry.options.json` for Apple platforms ([#4447](https://github.com/getsentry/sentry-react-native/pull/4447)) ### Internal diff --git a/packages/core/RNSentryCocoaTester/RNSentryCocoaTester.xcodeproj/project.pbxproj b/packages/core/RNSentryCocoaTester/RNSentryCocoaTester.xcodeproj/project.pbxproj index 1621383063..0d82e39ef1 100644 --- a/packages/core/RNSentryCocoaTester/RNSentryCocoaTester.xcodeproj/project.pbxproj +++ b/packages/core/RNSentryCocoaTester/RNSentryCocoaTester.xcodeproj/project.pbxproj @@ -12,6 +12,10 @@ 3380C6C42CE25ECA0018B9B6 /* RNSentryReplayPostInitTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3380C6C32CE25ECA0018B9B6 /* RNSentryReplayPostInitTests.swift */; }; 33958C692BFCF12600AD1FB6 /* RNSentryOnDrawReporterTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 33958C682BFCF12600AD1FB6 /* RNSentryOnDrawReporterTests.m */; }; 339C6C3C2D3EB25100CA72ED /* RNSentryStartTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 339C6C3B2D3EB23B00CA72ED /* RNSentryStartTests.swift */; }; + 339C6C422D3FD3AE00CA72ED /* RNSentryStartFromFileTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 339C6C412D3FD39500CA72ED /* RNSentryStartFromFileTests.swift */; }; + 339C6C482D3FD9A700CA72ED /* invalid.options.json in Resources */ = {isa = PBXBuildFile; fileRef = 339C6C462D3FD91900CA72ED /* invalid.options.json */; }; + 339C6C492D3FD9A700CA72ED /* invalid.options.txt in Resources */ = {isa = PBXBuildFile; fileRef = 339C6C452D3FD90200CA72ED /* invalid.options.txt */; }; + 339C6C4B2D3FD9B200CA72ED /* valid.options.json in Resources */ = {isa = PBXBuildFile; fileRef = 339C6C4A2D3FD9AB00CA72ED /* valid.options.json */; }; 33AFDFED2B8D14B300AAB120 /* RNSentryFramesTrackerListenerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 33AFDFEC2B8D14B300AAB120 /* RNSentryFramesTrackerListenerTests.m */; }; 33AFDFF12B8D15E500AAB120 /* RNSentryDependencyContainerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 33AFDFF02B8D15E500AAB120 /* RNSentryDependencyContainerTests.m */; }; 33F58AD02977037D008F60EA /* RNSentryTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 33F58ACF2977037D008F60EA /* RNSentryTests.mm */; }; @@ -41,6 +45,11 @@ 33958C682BFCF12600AD1FB6 /* RNSentryOnDrawReporterTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RNSentryOnDrawReporterTests.m; sourceTree = ""; }; 339C6C3B2D3EB23B00CA72ED /* RNSentryStartTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RNSentryStartTests.swift; sourceTree = ""; }; 339C6C3D2D3FA04D00CA72ED /* RNSentryVersion.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = RNSentryVersion.h; path = ../ios/RNSentryVersion.h; sourceTree = SOURCE_ROOT; }; + 339C6C412D3FD39500CA72ED /* RNSentryStartFromFileTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RNSentryStartFromFileTests.swift; sourceTree = ""; }; + 339C6C442D3FD62D00CA72ED /* RNSentrySDK+Test.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "RNSentrySDK+Test.h"; sourceTree = ""; }; + 339C6C452D3FD90200CA72ED /* invalid.options.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = invalid.options.txt; sourceTree = ""; }; + 339C6C462D3FD91900CA72ED /* invalid.options.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = invalid.options.json; sourceTree = ""; }; + 339C6C4A2D3FD9AB00CA72ED /* valid.options.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = valid.options.json; sourceTree = ""; }; 33AFDFEC2B8D14B300AAB120 /* RNSentryFramesTrackerListenerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RNSentryFramesTrackerListenerTests.m; sourceTree = ""; }; 33AFDFEE2B8D14C200AAB120 /* RNSentryFramesTrackerListenerTests.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RNSentryFramesTrackerListenerTests.h; sourceTree = ""; }; 33AFDFF02B8D15E500AAB120 /* RNSentryDependencyContainerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RNSentryDependencyContainerTests.m; sourceTree = ""; }; @@ -75,6 +84,7 @@ 3360896929524163007C7730 = { isa = PBXGroup; children = ( + 339C6C432D3FD41C00CA72ED /* TestAssets */, 33AFE0122B8F319000AAB120 /* RNSentry */, 3360899029524164007C7730 /* RNSentryCocoaTesterTests */, 3360897329524163007C7730 /* Products */, @@ -94,6 +104,7 @@ 3360899029524164007C7730 /* RNSentryCocoaTesterTests */ = { isa = PBXGroup; children = ( + 339C6C412D3FD39500CA72ED /* RNSentryStartFromFileTests.swift */, 339C6C3B2D3EB23B00CA72ED /* RNSentryStartTests.swift */, 332D334A2CDCC8EB00547D76 /* RNSentryCocoaTesterTests-Bridging-Header.h */, 332D33492CDCC8E100547D76 /* RNSentryTests.h */, @@ -120,9 +131,20 @@ path = Replay; sourceTree = ""; }; + 339C6C432D3FD41C00CA72ED /* TestAssets */ = { + isa = PBXGroup; + children = ( + 339C6C4A2D3FD9AB00CA72ED /* valid.options.json */, + 339C6C462D3FD91900CA72ED /* invalid.options.json */, + 339C6C452D3FD90200CA72ED /* invalid.options.txt */, + ); + path = TestAssets; + sourceTree = ""; + }; 33AFE0122B8F319000AAB120 /* RNSentry */ = { isa = PBXGroup; children = ( + 339C6C442D3FD62D00CA72ED /* RNSentrySDK+Test.h */, 339C6C3D2D3FA04D00CA72ED /* RNSentryVersion.h */, 333B58AF2D36A7FD000F8D04 /* RNSentrySDK.h */, 333B58A92D35BB2D000F8D04 /* RNSentryStart+Test.h */, @@ -157,6 +179,7 @@ 3360898929524164007C7730 /* Sources */, BB7D14838753E6599863899B /* Frameworks */, CC7959F3721CB3AD7CB6A047 /* [CP] Copy Pods Resources */, + 339C6C472D3FD99900CA72ED /* Resources */, ); buildRules = ( ); @@ -201,6 +224,19 @@ }; /* End PBXProject section */ +/* Begin PBXResourcesBuildPhase section */ + 339C6C472D3FD99900CA72ED /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 339C6C482D3FD9A700CA72ED /* invalid.options.json in Resources */, + 339C6C4B2D3FD9B200CA72ED /* valid.options.json in Resources */, + 339C6C492D3FD9A700CA72ED /* invalid.options.txt in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + /* Begin PBXShellScriptBuildPhase section */ 30F19D4E16BEEFEC68733838 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; @@ -252,6 +288,7 @@ 332D33472CDBDBB600547D76 /* RNSentryReplayOptionsTests.swift in Sources */, 339C6C3C2D3EB25100CA72ED /* RNSentryStartTests.swift in Sources */, 33AFDFF12B8D15E500AAB120 /* RNSentryDependencyContainerTests.m in Sources */, + 339C6C422D3FD3AE00CA72ED /* RNSentryStartFromFileTests.swift in Sources */, 336084392C32E382008CC412 /* RNSentryReplayBreadcrumbConverterTests.swift in Sources */, 33F58AD02977037D008F60EA /* RNSentryTests.mm in Sources */, 33958C692BFCF12600AD1FB6 /* RNSentryOnDrawReporterTests.m in Sources */, diff --git a/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryCocoaTesterTests-Bridging-Header.h b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryCocoaTesterTests-Bridging-Header.h index ba8d8f703d..08fddcbf8e 100644 --- a/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryCocoaTesterTests-Bridging-Header.h +++ b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryCocoaTesterTests-Bridging-Header.h @@ -7,6 +7,6 @@ #import "RNSentryReplayBreadcrumbConverter.h" #import "RNSentryReplayMask.h" #import "RNSentryReplayUnmask.h" -#import "RNSentrySDK.h" +#import "RNSentrySDK+Test.h" #import "RNSentryStart.h" #import "RNSentryVersion.h" diff --git a/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryStartFromFileTests.swift b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryStartFromFileTests.swift new file mode 100644 index 0000000000..e0269a5961 --- /dev/null +++ b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryStartFromFileTests.swift @@ -0,0 +1,115 @@ +import XCTest + +final class RNSentryStartFromFileTests: XCTestCase { + + func testNoThrowOnMissingOptionsFile() { + var wasConfigurationCalled = false + + RNSentrySDK.start(getNonExistingOptionsPath(), configureOptions: { _ in + wasConfigurationCalled = true + }) + + XCTAssertTrue(wasConfigurationCalled) + + let actualOptions = PrivateSentrySDKOnly.options + XCTAssertNil(actualOptions.dsn) + XCTAssertNil(actualOptions.parsedDsn) + } + + func testNoThrowOnInvalidFileType() { + var wasConfigurationCalled = false + + RNSentrySDK.start(getInvalidOptionsTypePath(), configureOptions: { _ in + wasConfigurationCalled = true + }) + + XCTAssertTrue(wasConfigurationCalled) + + let actualOptions = PrivateSentrySDKOnly.options + XCTAssertNil(actualOptions.dsn) + XCTAssertNil(actualOptions.parsedDsn) + } + + func testNoThrowOnInvalidOptions() { + var wasConfigurationCalled = false + + RNSentrySDK.start(getInvalidOptionsPath(), configureOptions: { _ in + wasConfigurationCalled = true + }) + + XCTAssertTrue(wasConfigurationCalled) + + let actualOptions = PrivateSentrySDKOnly.options + XCTAssertNil(actualOptions.dsn) + XCTAssertNotNil(actualOptions.parsedDsn) + XCTAssertEqual(actualOptions.environment, "environment-from-invalid-file") + } + + func testLoadValidOptions() { + var wasConfigurationCalled = false + + RNSentrySDK.start(getValidOptionsPath(), configureOptions: { _ in + wasConfigurationCalled = true + }) + + XCTAssertTrue(wasConfigurationCalled) + + let actualOptions = PrivateSentrySDKOnly.options + XCTAssertNil(actualOptions.dsn) + XCTAssertNotNil(actualOptions.parsedDsn) + XCTAssertEqual(actualOptions.environment, "environment-from-valid-file") + } + + func testOptionsFromFileInConfigureOptions() { + var wasConfigurationCalled = false + + RNSentrySDK.start(getValidOptionsPath()) { options in + wasConfigurationCalled = true + XCTAssertEqual(options.environment, "environment-from-valid-file") + } + + XCTAssertTrue(wasConfigurationCalled) + } + + func testOptionsOverwrittenInConfigureOptions() { + RNSentrySDK.start(getValidOptionsPath()) { options in + options.environment = "new-environment" + } + + let actualOptions = PrivateSentrySDKOnly.options + XCTAssertEqual(actualOptions.environment, "new-environment") + } + + func getNonExistingOptionsPath() -> String { + return "/non-existing.options.json" + } + + func getInvalidOptionsTypePath() -> String { + guard let path = getTestBundle().path(forResource: "invalid.options", ofType: "txt") else { + fatalError("Could not get invalid type options path") + } + return path + } + + func getInvalidOptionsPath() -> String { + guard let path = getTestBundle().path(forResource: "invalid.options", ofType: "json") else { + fatalError("Could not get invalid options path") + } + return path + } + + func getValidOptionsPath() -> String { + guard let path = getTestBundle().path(forResource: "valid.options", ofType: "json") else { + fatalError("Could not get invalid options path") + } + return path + } + + func getTestBundle() -> Bundle { + let maybeBundle = Bundle.allBundles.first(where: { $0.bundlePath.hasSuffix(".xctest") }) + guard let bundle = maybeBundle else { + fatalError("Could not find test bundle") + } + return bundle + } +} diff --git a/packages/core/RNSentryCocoaTester/RNSentrySDK+Test.h b/packages/core/RNSentryCocoaTester/RNSentrySDK+Test.h new file mode 100644 index 0000000000..06da31b42d --- /dev/null +++ b/packages/core/RNSentryCocoaTester/RNSentrySDK+Test.h @@ -0,0 +1,9 @@ +#import "RNSentrySDK.h" + +@interface +RNSentrySDK (Test) + ++ (void)start:(NSString *)path + configureOptions:(void (^)(SentryOptions *_Nonnull options))configureOptions; + +@end diff --git a/packages/core/RNSentryCocoaTester/TestAssets/invalid.options.json b/packages/core/RNSentryCocoaTester/TestAssets/invalid.options.json new file mode 100644 index 0000000000..bf8f2be64c --- /dev/null +++ b/packages/core/RNSentryCocoaTester/TestAssets/invalid.options.json @@ -0,0 +1,5 @@ +{ + "dsn": "https://abcd@efgh.ingest.sentry.io/123456", + "environment": "environment-from-invalid-file", + "invalid-option": 123 +} diff --git a/packages/core/RNSentryCocoaTester/TestAssets/invalid.options.txt b/packages/core/RNSentryCocoaTester/TestAssets/invalid.options.txt new file mode 100644 index 0000000000..601553b507 --- /dev/null +++ b/packages/core/RNSentryCocoaTester/TestAssets/invalid.options.txt @@ -0,0 +1 @@ +invalid-options diff --git a/packages/core/RNSentryCocoaTester/TestAssets/valid.options.json b/packages/core/RNSentryCocoaTester/TestAssets/valid.options.json new file mode 100644 index 0000000000..641087d5e8 --- /dev/null +++ b/packages/core/RNSentryCocoaTester/TestAssets/valid.options.json @@ -0,0 +1,4 @@ +{ + "dsn": "https://abcd@efgh.ingest.sentry.io/123456", + "environment": "environment-from-valid-file" +} diff --git a/packages/core/ios/RNSentrySDK.h b/packages/core/ios/RNSentrySDK.h index 7d3512bb5d..232071d9bc 100644 --- a/packages/core/ios/RNSentrySDK.h +++ b/packages/core/ios/RNSentrySDK.h @@ -5,8 +5,21 @@ SENTRY_NO_INIT /** * @experimental - * Inits and configures Sentry for React Native applications. Make sure to - * set a valid DSN. + * Inits and configures Sentry for React Native applications using `sentry.options.json` + * configuration file. + * + * @discussion Call this method on the main thread. When calling it from a background thread, the + * SDK starts on the main thread async. + */ ++ (void)start; + +/** + * @experimental + * Inits and configures Sentry for React Native applicationsusing `sentry.options.json` + * configuration file and `configureOptions` callback. + * + * The `configureOptions` callback can overwrite the config file options + * and add non-serializable items to the options object. * * @discussion Call this method on the main thread. When calling it from a background thread, the * SDK starts on the main thread async. diff --git a/packages/core/ios/RNSentrySDK.m b/packages/core/ios/RNSentrySDK.m index b7ed6f4a7b..7d7f4cf9b3 100644 --- a/packages/core/ios/RNSentrySDK.m +++ b/packages/core/ios/RNSentrySDK.m @@ -1,11 +1,65 @@ #import "RNSentrySDK.h" #import "RNSentryStart.h" +static NSString *SENTRY_OPTIONS_RESOURCE_NAME = @"sentry.options"; +static NSString *SENTRY_OPTIONS_RESOURCE_TYPE = @"json"; + @implementation RNSentrySDK ++ (void)start +{ + [self startWithConfigureOptions:nil]; +} + + (void)startWithConfigureOptions:(void (^)(SentryOptions *options))configureOptions { - SentryOptions *options = [[SentryOptions alloc] init]; + NSString *path = [[NSBundle mainBundle] pathForResource:SENTRY_OPTIONS_RESOURCE_NAME + ofType:SENTRY_OPTIONS_RESOURCE_TYPE]; + + [self start:path configureOptions:configureOptions]; +} + ++ (void)start:(NSString *)path configureOptions:(void (^)(SentryOptions *options))configureOptions +{ + NSError *readError = nil; + NSError *parseError = nil; + NSError *optionsError = nil; + + NSData *_Nullable content = nil; + if (path != nil) { + content = [NSData dataWithContentsOfFile:path options:0 error:&readError]; + } + + NSDictionary *dict = nil; + if (content != nil) { + dict = [NSJSONSerialization JSONObjectWithData:content options:0 error:&parseError]; + } + + if (readError != nil) { + NSLog(@"[RNSentry] Failed to load options from %@, with error: %@", path, + readError.localizedDescription); + } + + if (parseError != nil) { + NSLog(@"[RNSentry] Failed to parse JSON from %@, with error: %@", path, + parseError.localizedDescription); + } + + SentryOptions *options = nil; + if (dict != nil) { + options = [RNSentryStart createOptionsWithDictionary:dict error:&optionsError]; + } + + if (optionsError != nil) { + NSLog(@"[RNSentry] Failed to parse options from %@, with error: %@", path, + optionsError.localizedDescription); + } + + if (options == nil) { + // Fallback in case that options file could not be parsed. + options = [[SentryOptions alloc] init]; + } + [RNSentryStart updateWithReactDefaults:options]; if (configureOptions != nil) { configureOptions(options); diff --git a/packages/core/ios/RNSentryStart.m b/packages/core/ios/RNSentryStart.m index b3d4d5d77e..84e2d83b02 100644 --- a/packages/core/ios/RNSentryStart.m +++ b/packages/core/ios/RNSentryStart.m @@ -50,8 +50,9 @@ + (SentryOptions *_Nullable)createOptionsWithDictionary:(NSDictionary *_Nonnull) } // Exclude Dev Server and Sentry Dsn request from Breadcrumbs - // TODO: Migrate for manual init NSString *dsn = [self getURLFromDSN:[mutableOptions valueForKey:@"dsn"]]; + // TODO: For Auto Init from JS dev server is resolved automatically, for init from options file + // dev server has to be specified manually NSString *devServerUrl = [mutableOptions valueForKey:@"devServerUrl"]; sentryOptions.beforeBreadcrumb = ^SentryBreadcrumb *_Nullable(SentryBreadcrumb *_Nonnull breadcrumb) @@ -86,6 +87,8 @@ + (SentryOptions *_Nullable)createOptionsWithDictionary:(NSDictionary *_Nonnull) sentryOptions.spotlightUrl = spotlightValue; } else if ([spotlightValue isKindOfClass:[NSNumber class]]) { sentryOptions.enableSpotlight = [spotlightValue boolValue]; + // TODO: For Auto init from JS set automatically for init from options file have to be + // set manually id defaultSpotlightUrl = [mutableOptions valueForKey:@"defaultSidecarUrl"]; if (defaultSpotlightUrl != nil) { sentryOptions.spotlightUrl = defaultSpotlightUrl; diff --git a/packages/core/scripts/sentry-xcode.sh b/packages/core/scripts/sentry-xcode.sh index 78970c4c60..336d393220 100755 --- a/packages/core/scripts/sentry-xcode.sh +++ b/packages/core/scripts/sentry-xcode.sh @@ -51,3 +51,22 @@ fi if [ -f "$SENTRY_COLLECT_MODULES" ]; then /bin/sh "$SENTRY_COLLECT_MODULES" fi + +SENTRY_OPTIONS_FILE_ERROR_MESSAGE_POSTFIX="Skipping options file copy. To disable this behavior, set SENTRY_COPY_OPTIONS_FILE=false in your environment variables." +SENTRY_OPTIONS_FILE_NAME="sentry.options.json" +SENTRY_OPTIONS_FILE_DESTINATION_PATH="$CONFIGURATION_BUILD_DIR/$UNLOCALIZED_RESOURCES_FOLDER_PATH/$SENTRY_OPTIONS_FILE_NAME" +[ -z "$SENTRY_OPTIONS_FILE_PATH" ] && SENTRY_OPTIONS_FILE_PATH="$RN_PROJECT_ROOT/$SENTRY_OPTIONS_FILE_NAME" +[ -z "$SENTRY_COPY_OPTIONS_FILE" ] && SENTRY_COPY_OPTIONS_FILE=true + +if [ "$SENTRY_COPY_OPTIONS_FILE" = true ]; then + if [[ -z "$CONFIGURATION_BUILD_DIR" ]]; then + echo "[Sentry] CONFIGURATION_BUILD_DIR is not set. $SENTRY_OPTIONS_FILE_ERROR_MESSAGE_POSTFIX" 1>&2 + elif [[ -z "$UNLOCALIZED_RESOURCES_FOLDER_PATH" ]]; then + echo "[Sentry] UNLOCALIZED_RESOURCES_FOLDER_PATH is not set. $SENTRY_OPTIONS_FILE_ERROR_MESSAGE_POSTFIX" 1>&2 + elif [ ! -f "$SENTRY_OPTIONS_FILE_PATH" ]; then + echo "[Sentry] $SENTRY_OPTIONS_FILE_PATH not found. $SENTRY_OPTIONS_FILE_ERROR_MESSAGE_POSTFIX" 1>&2 + else + cp "$SENTRY_OPTIONS_FILE_PATH" "$SENTRY_OPTIONS_FILE_DESTINATION_PATH" + echo "[Sentry] Copied $SENTRY_OPTIONS_FILE_PATH to $SENTRY_OPTIONS_FILE_DESTINATION_PATH" + fi +fi diff --git a/samples/react-native/ios/sentryreactnativesample/AppDelegate.mm b/samples/react-native/ios/sentryreactnativesample/AppDelegate.mm index 2a6a0a0956..fe0893c5b6 100644 --- a/samples/react-native/ios/sentryreactnativesample/AppDelegate.mm +++ b/samples/react-native/ios/sentryreactnativesample/AppDelegate.mm @@ -20,49 +20,12 @@ @implementation AppDelegate -- (void)initializeSentry -{ - [SentrySDK startWithConfigureOptions:^(SentryOptions *options) { - // Only options set here will apply to the iOS SDK - // Options from JS are not passed to the iOS SDK when initialized manually - options.dsn = @"https://1df17bd4e543fdb31351dee1768bb679@o447951.ingest.sentry.io/5428561"; - options.debug = YES; // Enabled debug when first installing is always helpful - - options.beforeSend = ^SentryEvent *(SentryEvent *event) - { - // We don't want to send an event after startup that came from a Unhandled JS Exception - // of react native Because we sent it already before the app crashed. - if (nil != event.exceptions.firstObject.type && - [event.exceptions.firstObject.type rangeOfString:@"Unhandled JS Exception"].location - != NSNotFound) { - NSLog(@"Unhandled JS Exception"); - return nil; - } - - return event; - }; - - // Enable the App start and Frames tracking measurements - // If this is disabled the app start and frames tracking - // won't be passed from native to JS transactions - PrivateSentrySDKOnly.appStartMeasurementHybridSDKMode = true; -#if TARGET_OS_IPHONE || TARGET_OS_MACCATALYST - PrivateSentrySDKOnly.framesTrackingMeasurementHybridSDKMode = true; -#endif - }]; -} - - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { // When the native init is enabled the `autoInitializeNativeSdk` // in JS has to be set to `false` - // [self initializeSentry]; - // [RNSentrySDK startWithConfigureOptions:^(SentryOptions *options) { - // options.dsn = - // @"https://1df17bd4e543fdb31351dee1768bb679@o447951.ingest.sentry.io/5428561"; - // options.debug = YES; - // }]; + // [RNSentrySDK start]; self.moduleName = @"sentry-react-native-sample"; // You can add your custom initial props in the dictionary below. diff --git a/samples/react-native/sentry.options.json b/samples/react-native/sentry.options.json new file mode 100644 index 0000000000..f6465b7923 --- /dev/null +++ b/samples/react-native/sentry.options.json @@ -0,0 +1,20 @@ +{ + "dsn": "https://1df17bd4e543fdb31351dee1768bb679@o447951.ingest.sentry.io/5428561", + "debug": true, + "environment": "dev", + "enableUserInteractionTracing": true, + "enableAutoSessionTracking": true, + "sessionTrackingIntervalMillis": 30000, + "enableTracing": true, + "tracesSampleRate": 1.0, + "attachStacktrace": true, + "attachScreenshot": true, + "attachViewHierarchy": true, + "enableCaptureFailedRequests": true, + "_release": "myapp@1.2.3+1", + "_dist": 1, + "profilesSampleRate": 1.0, + "replaysSessionSampleRate": 1.0, + "replaysOnErrorSampleRate": 1.0, + "spotlight": true +} From 1e5dbde34550da3be4e105ab195547eef65d89a3 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Thu, 23 Jan 2025 16:28:14 +0200 Subject: [PATCH 05/32] Adds utility class for converting `JsonObject` to `WritableMap` (#4479) * Convert json object to writable map * Make class/methods package-private(default) --- .../sentry/react/RNSentryJsonConverterTest.kt | 103 ++++++++++++++++++ .../sentry/react/RNSentryJsonConverter.java | 76 +++++++++++++ 2 files changed, 179 insertions(+) create mode 100644 packages/core/RNSentryAndroidTester/app/src/androidTest/java/io/sentry/react/RNSentryJsonConverterTest.kt create mode 100644 packages/core/android/src/main/java/io/sentry/react/RNSentryJsonConverter.java diff --git a/packages/core/RNSentryAndroidTester/app/src/androidTest/java/io/sentry/react/RNSentryJsonConverterTest.kt b/packages/core/RNSentryAndroidTester/app/src/androidTest/java/io/sentry/react/RNSentryJsonConverterTest.kt new file mode 100644 index 0000000000..e49aa546f8 --- /dev/null +++ b/packages/core/RNSentryAndroidTester/app/src/androidTest/java/io/sentry/react/RNSentryJsonConverterTest.kt @@ -0,0 +1,103 @@ +package io.sentry.react + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.facebook.react.bridge.WritableArray +import com.facebook.react.bridge.WritableMap +import io.sentry.react.RNSentryJsonConverter.convertToWritable +import org.json.JSONArray +import org.json.JSONObject +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class RNSentryJsonConverterTest { + @Test + fun testConvertToWritableWithSimpleJsonObject() { + val jsonObject = + JSONObject().apply { + put("floatKey", 12.3f) + put("doubleKey", 12.3) + put("intKey", 123) + put("stringKey", "test") + put("nullKey", JSONObject.NULL) + } + + val result: WritableMap? = convertToWritable(jsonObject) + + assertNotNull(result) + assertEquals(12.3, result!!.getDouble("floatKey"), 0.0001) + assertEquals(12.3, result.getDouble("doubleKey"), 0.0) + assertEquals(123, result.getInt("intKey")) + assertEquals("test", result.getString("stringKey")) + assertNull(result.getString("nullKey")) + } + + @Test + fun testConvertToWritableWithNestedJsonObject() { + val jsonObject = + JSONObject().apply { + put( + "nested", + JSONObject().apply { + put("key", "value") + }, + ) + } + + val result: WritableMap? = convertToWritable(jsonObject) + + assertNotNull(result) + val nestedMap = result!!.getMap("nested") + assertNotNull(nestedMap) + assertEquals("value", nestedMap!!.getString("key")) + } + + @Test + fun testConvertToWritableWithJsonArray() { + val jsonArray = + JSONArray().apply { + put(1) + put(2.5) + put("string") + put(JSONObject.NULL) + } + + val result: WritableArray = convertToWritable(jsonArray) + + assertEquals(1, result.getInt(0)) + assertEquals(2.5, result.getDouble(1), 0.0) + assertEquals("string", result.getString(2)) + assertNull(result.getString(3)) + } + + @Test + fun testConvertToWritableWithNestedJsonArray() { + val jsonObject = + JSONObject().apply { + put( + "array", + JSONArray().apply { + put( + JSONObject().apply { + put("key1", "value1") + }, + ) + put( + JSONObject().apply { + put("key2", "value2") + }, + ) + }, + ) + } + + val result: WritableMap? = convertToWritable(jsonObject) + + val array = result?.getArray("array") + assertEquals("value1", array?.getMap(0)?.getString("key1")) + assertEquals("value2", array?.getMap(1)?.getString("key2")) + } +} diff --git a/packages/core/android/src/main/java/io/sentry/react/RNSentryJsonConverter.java b/packages/core/android/src/main/java/io/sentry/react/RNSentryJsonConverter.java new file mode 100644 index 0000000000..44ec324eed --- /dev/null +++ b/packages/core/android/src/main/java/io/sentry/react/RNSentryJsonConverter.java @@ -0,0 +1,76 @@ +package io.sentry.react; + +import com.facebook.react.bridge.JavaOnlyArray; +import com.facebook.react.bridge.JavaOnlyMap; +import com.facebook.react.bridge.WritableArray; +import com.facebook.react.bridge.WritableMap; +import io.sentry.ILogger; +import io.sentry.SentryLevel; +import io.sentry.android.core.AndroidLogger; +import java.util.Iterator; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +final class RNSentryJsonConverter { + public static final String NAME = "RNSentry.RNSentryJsonConverter"; + + private static final ILogger logger = new AndroidLogger(NAME); + + private RNSentryJsonConverter() { + throw new AssertionError("Utility class should not be instantiated"); + } + + @Nullable + static WritableMap convertToWritable(@NotNull JSONObject jsonObject) { + try { + WritableMap writableMap = new JavaOnlyMap(); + Iterator iterator = jsonObject.keys(); + while (iterator.hasNext()) { + String key = iterator.next(); + Object value = jsonObject.get(key); + if (value instanceof Float || value instanceof Double) { + writableMap.putDouble(key, jsonObject.getDouble(key)); + } else if (value instanceof Number) { + writableMap.putInt(key, jsonObject.getInt(key)); + } else if (value instanceof String) { + writableMap.putString(key, jsonObject.getString(key)); + } else if (value instanceof JSONObject) { + writableMap.putMap(key, convertToWritable(jsonObject.getJSONObject(key))); + } else if (value instanceof JSONArray) { + writableMap.putArray(key, convertToWritable(jsonObject.getJSONArray(key))); + } else if (value == JSONObject.NULL) { + writableMap.putNull(key); + } + } + return writableMap; + } catch (JSONException e) { + logger.log(SentryLevel.ERROR, "Error parsing json object:" + e.getMessage()); + return null; + } + } + + @NotNull + static WritableArray convertToWritable(@NotNull JSONArray jsonArray) throws JSONException { + WritableArray writableArray = new JavaOnlyArray(); + for (int i = 0; i < jsonArray.length(); i++) { + Object value = jsonArray.get(i); + if (value instanceof Float || value instanceof Double) { + writableArray.pushDouble(jsonArray.getDouble(i)); + } else if (value instanceof Number) { + writableArray.pushInt(jsonArray.getInt(i)); + } else if (value instanceof String) { + writableArray.pushString(jsonArray.getString(i)); + } else if (value instanceof JSONObject) { + writableArray.pushMap(convertToWritable(jsonArray.getJSONObject(i))); + } else if (value instanceof JSONArray) { + writableArray.pushArray(convertToWritable(jsonArray.getJSONArray(i))); + } else if (value == JSONObject.NULL) { + writableArray.pushNull(); + } + } + return writableArray; + } +} From 22a5f81ac7afa2899b8921e705518d6101e38a2f Mon Sep 17 00:00:00 2001 From: Krystof Woldrich <31292499+krystofwoldrich@users.noreply.github.com> Date: Mon, 3 Feb 2025 11:55:19 +0100 Subject: [PATCH 06/32] feat: Automatically load `sentry.options.json` file (#4476) --- CHANGELOG.md | 4 + packages/core/jest.config.tools.js | 2 +- packages/core/src/js/tools/metroconfig.ts | 20 +- .../src/js/tools/sentryMetroSerializer.ts | 1 + .../src/js/tools/sentryOptionsSerializer.ts | 104 +++++++++ packages/core/src/js/tools/utils.ts | 4 +- .../tools/sentryOptionsSerializer.test.ts | 209 ++++++++++++++++++ samples/expo/sentry.options.json | 18 ++ 8 files changed, 358 insertions(+), 4 deletions(-) create mode 100644 packages/core/src/js/tools/sentryOptionsSerializer.ts create mode 100644 packages/core/test/tools/sentryOptionsSerializer.test.ts create mode 100644 samples/expo/sentry.options.json diff --git a/CHANGELOG.md b/CHANGELOG.md index ef2e9ed714..6a73f7f4ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,10 @@ ## Unreleased +### Features + +- Load `optionsFile` into the JS bundle during Metro bundle process ([#4476](https://github.com/getsentry/sentry-react-native/pull/4476)) + ### Fixes - Add mechanism field to unhandled rejection errors ([#4457](https://github.com/getsentry/sentry-react-native/pull/4457)) diff --git a/packages/core/jest.config.tools.js b/packages/core/jest.config.tools.js index 5c5902d8a7..996ad05625 100644 --- a/packages/core/jest.config.tools.js +++ b/packages/core/jest.config.tools.js @@ -1,7 +1,7 @@ module.exports = { collectCoverage: true, preset: 'ts-jest', - setupFilesAfterEnv: ['/test/mockConsole.ts'], + setupFilesAfterEnv: ['jest-extended/all', '/test/mockConsole.ts'], globals: { __DEV__: true, }, diff --git a/packages/core/src/js/tools/metroconfig.ts b/packages/core/src/js/tools/metroconfig.ts index 71c43389a1..e0bd57c178 100644 --- a/packages/core/src/js/tools/metroconfig.ts +++ b/packages/core/src/js/tools/metroconfig.ts @@ -10,6 +10,8 @@ import { createSentryMetroSerializer, unstable_beforeAssetSerializationPlugin } import type { DefaultConfigOptions } from './vendor/expo/expoconfig'; export * from './sentryMetroSerializer'; import { withSentryMiddleware } from './metroMiddleware'; +import { withSentryOptionsFromFile } from './sentryOptionsSerializer'; +import type { MetroCustomSerializer } from './utils'; enableLogger(); @@ -30,6 +32,14 @@ export interface SentryMetroConfigOptions { * @default true */ enableSourceContextInDevelopment?: boolean; + /** + * Load Sentry Options from a file. If `true` it will use the default path. + * If `false` it will not load any options from a file. Only options provided in the code will be used. + * If `string` it will use the provided path. + * + * @default '{projectRoot}/sentry.options.json' + */ + optionsFile?: string | boolean; } export interface SentryExpoConfigOptions { @@ -51,6 +61,7 @@ export function withSentryConfig( annotateReactComponents = false, includeWebReplay = true, enableSourceContextInDevelopment = true, + optionsFile = true, }: SentryMetroConfigOptions = {}, ): MetroConfig { setSentryMetroDevServerEnvFlag(); @@ -68,6 +79,9 @@ export function withSentryConfig( if (enableSourceContextInDevelopment) { newConfig = withSentryMiddleware(newConfig); } + if (optionsFile) { + newConfig = withSentryOptionsFromFile(newConfig, optionsFile); + } return newConfig; } @@ -103,6 +117,10 @@ export function getSentryExpoConfig( newConfig = withSentryMiddleware(newConfig); } + if (options.optionsFile ?? true) { + newConfig = withSentryOptionsFromFile(newConfig, options.optionsFile ?? true); + } + return newConfig; } @@ -155,8 +173,6 @@ export function withSentryBabelTransformer(config: MetroConfig): MetroConfig { }; } -type MetroCustomSerializer = Required['serializer']>['customSerializer'] | undefined; - function withSentryDebugId(config: MetroConfig): MetroConfig { const customSerializer = createSentryMetroSerializer( config.serializer?.customSerializer || undefined, diff --git a/packages/core/src/js/tools/sentryMetroSerializer.ts b/packages/core/src/js/tools/sentryMetroSerializer.ts index fca0979440..feb1e65621 100644 --- a/packages/core/src/js/tools/sentryMetroSerializer.ts +++ b/packages/core/src/js/tools/sentryMetroSerializer.ts @@ -42,6 +42,7 @@ export function unstable_beforeAssetSerializationPlugin({ return [...addDebugIdModule(premodules, debugIdModule)]; } +// TODO: deprecate this and afterwards rename to createSentryDebugIdSerializer /** * Creates a Metro serializer that adds Debug ID module to the plain bundle. * The Debug ID module is a virtual module that provides a debug ID in runtime. diff --git a/packages/core/src/js/tools/sentryOptionsSerializer.ts b/packages/core/src/js/tools/sentryOptionsSerializer.ts new file mode 100644 index 0000000000..f2ab93b383 --- /dev/null +++ b/packages/core/src/js/tools/sentryOptionsSerializer.ts @@ -0,0 +1,104 @@ +import { logger } from '@sentry/core'; +import * as fs from 'fs'; +import type { MetroConfig, Module } from 'metro'; +// eslint-disable-next-line import/no-extraneous-dependencies +import * as countLines from 'metro/src/lib/countLines'; +import * as path from 'path'; + +import type { MetroCustomSerializer, VirtualJSOutput } from './utils'; +import { createSet } from './utils'; + +const DEFAULT_OPTIONS_FILE_NAME = 'sentry.options.json'; + +/** + * Loads Sentry options from a file in + */ +export function withSentryOptionsFromFile(config: MetroConfig, optionsFile: string | boolean): MetroConfig { + if (optionsFile === false) { + return config; + } + + const { projectRoot } = config; + if (!projectRoot) { + // eslint-disable-next-line no-console + console.error('[@sentry/react-native/metro] Project root is required to load Sentry options from a file'); + return config; + } + + let optionsPath = path.join(projectRoot, DEFAULT_OPTIONS_FILE_NAME); + if (typeof optionsFile === 'string' && path.isAbsolute(optionsFile)) { + optionsPath = optionsFile; + } else if (typeof optionsFile === 'string') { + optionsPath = path.join(projectRoot, optionsFile); + } + + const originalSerializer = config.serializer?.customSerializer; + if (!originalSerializer) { + // It's okay to bail here because we don't expose this for direct usage, but as part of `withSentryConfig` + // If used directly in RN, the user is responsible for providing a custom serializer first, Expo provides serializer in default config + // eslint-disable-next-line no-console + console.error( + '[@sentry/react-native/metro] `config.serializer.customSerializer` is required to load Sentry options from a file', + ); + return config; + } + + const sentryOptionsSerializer: MetroCustomSerializer = (entryPoint, preModules, graph, options) => { + const sentryOptionsModule = createSentryOptionsModule(optionsPath); + if (sentryOptionsModule) { + (preModules as Module[]).push(sentryOptionsModule); + } + return originalSerializer(entryPoint, preModules, graph, options); + }; + + return { + ...config, + serializer: { + ...config.serializer, + customSerializer: sentryOptionsSerializer, + }, + }; +} + +function createSentryOptionsModule(filePath: string): Module | null { + let content: string; + try { + content = fs.readFileSync(filePath, 'utf8'); + } catch (error) { + if ((error as NodeJS.ErrnoException).code === 'ENOENT') { + logger.debug(`[@sentry/react-native/metro] Sentry options file does not exist at ${filePath}`); + } else { + logger.error(`[@sentry/react-native/metro] Failed to read Sentry options file at ${filePath}`); + } + return null; + } + + let parsedContent: Record; + try { + parsedContent = JSON.parse(content); + } catch (error) { + logger.error(`[@sentry/react-native/metro] Failed to parse Sentry options file at ${filePath}`); + return null; + } + + const minifiedContent = JSON.stringify(parsedContent); + const optionsCode = `var __SENTRY_OPTIONS__=${minifiedContent};`; + + logger.debug(`[@sentry/react-native/metro] Sentry options added to the bundle from file at ${filePath}`); + return { + dependencies: new Map(), + getSource: () => Buffer.from(optionsCode), + inverseDependencies: createSet(), + path: '__sentry-options__', + output: [ + { + type: 'js/script/virtual', + data: { + code: optionsCode, + lineCount: countLines(optionsCode), + map: [], + }, + }, + ], + }; +} diff --git a/packages/core/src/js/tools/utils.ts b/packages/core/src/js/tools/utils.ts index 769dc9abd4..82ff4075e2 100644 --- a/packages/core/src/js/tools/utils.ts +++ b/packages/core/src/js/tools/utils.ts @@ -1,9 +1,11 @@ import * as crypto from 'crypto'; // eslint-disable-next-line import/no-extraneous-dependencies -import type { Module, ReadOnlyGraph, SerializerOptions } from 'metro'; +import type { MetroConfig, Module, ReadOnlyGraph, SerializerOptions } from 'metro'; // eslint-disable-next-line import/no-extraneous-dependencies import type CountingSet from 'metro/src/lib/CountingSet'; +export type MetroCustomSerializer = Required['serializer']>['customSerializer'] | undefined; + // Variant of MixedOutput // https://github.com/facebook/metro/blob/9b85f83c9cc837d8cd897aa7723be7da5b296067/packages/metro/src/DeltaBundler/types.flow.js#L21 export type VirtualJSOutput = { diff --git a/packages/core/test/tools/sentryOptionsSerializer.test.ts b/packages/core/test/tools/sentryOptionsSerializer.test.ts new file mode 100644 index 0000000000..ed946d098a --- /dev/null +++ b/packages/core/test/tools/sentryOptionsSerializer.test.ts @@ -0,0 +1,209 @@ +import { logger } from '@sentry/core'; +import * as fs from 'fs'; +import type { Graph, Module, SerializerOptions } from 'metro'; + +import { withSentryOptionsFromFile } from '../../src/js/tools/sentryOptionsSerializer'; +import { createSet } from '../../src/js/tools/utils'; + +jest.mock('fs', () => ({ + readFileSync: jest.fn(), +})); + +const consoleErrorSpy = jest.spyOn(console, 'error'); +const loggerDebugSpy = jest.spyOn(logger, 'debug'); +const loggerErrorSpy = jest.spyOn(logger, 'error'); + +const customSerializerMock = jest.fn(); +let mockedPreModules: Module[] = []; + +describe('Sentry Options Serializer', () => { + beforeEach(() => { + jest.resetAllMocks(); + mockedPreModules = createMockedPreModules(); + }); + + afterAll(() => { + jest.clearAllMocks(); + }); + + test('returns original config if optionsFile is false', () => { + const config = () => ({ + projectRoot: '/test', + serializer: { + customSerializer: customSerializerMock, + }, + }); + + const result = withSentryOptionsFromFile(config(), false); + expect(result).toEqual(config()); + }); + + test('logs error and returns original config if projectRoot is missing', () => { + const config = () => ({ + serializer: { + customSerializer: customSerializerMock, + }, + }); + + const result = withSentryOptionsFromFile(config(), true); + + expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining('Project root is required')); + expect(result).toEqual(config()); + }); + + test('logs error and returns original config if customSerializer is missing', () => { + const config = () => ({ + projectRoot: '/test', + serializer: {}, + }); + const consoleErrorSpy = jest.spyOn(console, 'error'); + + const result = withSentryOptionsFromFile(config(), true); + + expect(consoleErrorSpy).toHaveBeenCalledWith( + expect.stringContaining('`config.serializer.customSerializer` is required'), + ); + expect(result).toEqual(config()); + }); + + test('adds sentry options module when file exists and is valid JSON', () => { + const config = () => ({ + projectRoot: '/test', + serializer: { + customSerializer: customSerializerMock, + }, + }); + + const mockOptions = { test: 'value' }; + (fs.readFileSync as jest.Mock).mockReturnValue(JSON.stringify(mockOptions)); + + const actualConfig = withSentryOptionsFromFile(config(), true); + actualConfig.serializer?.customSerializer(null, mockedPreModules, null, null); + + expect(mockedPreModules).toHaveLength(2); + expect(mockedPreModules.at(-1)).toEqual( + expect.objectContaining({ + getSource: expect.any(Function), + path: '__sentry-options__', + output: [ + { + type: 'js/script/virtual', + data: { + code: 'var __SENTRY_OPTIONS__={"test":"value"};', + lineCount: 1, + map: [], + }, + }, + ], + }), + ); + expect(mockedPreModules.at(-1).getSource().toString()).toEqual(mockedPreModules.at(-1).output[0].data.code); + expect(loggerDebugSpy).toHaveBeenCalledWith(expect.stringContaining('options added to the bundle')); + }); + + test('logs error and does not add module when file does not exist', () => { + const config = () => ({ + projectRoot: '/test', + serializer: { + customSerializer: customSerializerMock, + }, + }); + + (fs.readFileSync as jest.Mock).mockImplementation(() => { + throw { code: 'ENOENT' }; + }); + + const actualConfig = withSentryOptionsFromFile(config(), true); + actualConfig.serializer?.customSerializer(null, mockedPreModules, null, null); + + expect(loggerDebugSpy).toHaveBeenCalledWith(expect.stringContaining('options file does not exist')); + expect(mockedPreModules).toMatchObject(createMockedPreModules()); + }); + + test('logs error and does not add module when file contains invalid JSON', () => { + const config = () => ({ + projectRoot: '/test', + serializer: { + customSerializer: customSerializerMock, + }, + }); + + (fs.readFileSync as jest.Mock).mockReturnValue('invalid json'); + + const actualConfig = withSentryOptionsFromFile(config(), true); + actualConfig.serializer?.customSerializer(null, mockedPreModules, null, null); + + expect(loggerErrorSpy).toHaveBeenCalledWith(expect.stringContaining('Failed to parse Sentry options file')); + expect(mockedPreModules).toMatchObject(createMockedPreModules()); + }); + + test('calls original serializer with correct arguments and returns its result', () => { + const mockedEntryPoint = 'entryPoint'; + const mockedGraph: Graph = jest.fn() as unknown as Graph; + const mockedOptions: SerializerOptions = jest.fn() as unknown as SerializerOptions; + const mockedResult = {}; + const originalSerializer = jest.fn().mockReturnValue(mockedResult); + + const actualConfig = withSentryOptionsFromFile( + { + projectRoot: '/test', + serializer: { + customSerializer: originalSerializer, + }, + }, + true, + ); + const actualResult = actualConfig.serializer?.customSerializer( + mockedEntryPoint, + mockedPreModules, + mockedGraph, + mockedOptions, + ); + + expect(originalSerializer).toHaveBeenCalledWith(mockedEntryPoint, mockedPreModules, mockedGraph, mockedOptions); + expect(actualResult).toEqual(mockedResult); + }); + + test('uses custom file path when optionsFile is a string', () => { + const config = () => ({ + projectRoot: '/test', + serializer: { + customSerializer: customSerializerMock, + }, + }); + + withSentryOptionsFromFile(config(), 'custom/path.json').serializer?.customSerializer( + null, + mockedPreModules, + null, + null, + ); + withSentryOptionsFromFile(config(), '/absolute/path.json').serializer?.customSerializer( + null, + mockedPreModules, + null, + null, + ); + + expect(fs.readFileSync).toHaveBeenCalledWith('/test/custom/path.json', expect.anything()); + expect(fs.readFileSync).toHaveBeenCalledWith('/absolute/path.json', expect.anything()); + }); +}); + +function createMockedPreModules(): Module[] { + return [createMinimalModule()]; +} + +function createMinimalModule(): Module { + return { + dependencies: new Map(), + getSource: getEmptySource, + inverseDependencies: createSet(), + path: '__sentry-options__', + output: [], + }; +} + +function getEmptySource(): Buffer { + return Buffer.from(''); +} diff --git a/samples/expo/sentry.options.json b/samples/expo/sentry.options.json new file mode 100644 index 0000000000..53ae525bc0 --- /dev/null +++ b/samples/expo/sentry.options.json @@ -0,0 +1,18 @@ +{ + "dsn": "https://1df17bd4e543fdb31351dee1768bb679@o447951.ingest.sentry.io/5428561", + "debug": true, + "environment": "dev", + "enableUserInteractionTracing": true, + "enableAutoSessionTracking": true, + "sessionTrackingIntervalMillis": 30000, + "enableTracing": true, + "tracesSampleRate": 1.0, + "attachStacktrace": true, + "attachScreenshot": true, + "attachViewHierarchy": true, + "enableCaptureFailedRequests": true, + "profilesSampleRate": 1.0, + "replaysSessionSampleRate": 1.0, + "replaysOnErrorSampleRate": 1.0, + "spotlight": true +} From a1cb36d84366425054c3e8e2ae0105e0acab7117 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Tue, 4 Feb 2025 17:08:11 +0200 Subject: [PATCH 07/32] feat(experimental): Initialize Android SDK from json configuration (#4451) --- CHANGELOG.md | 2 + .../androidTest/assets/invalid.options.json | 3 + .../androidTest/assets/invalid.options.txt | 1 + .../androidTest/assets/sentry.options.json | 5 + .../java/io/sentry/react/RNSentrySDKTest.kt | 200 ++++++++++++++++++ ...SentryCompositeOptionsConfigurationTest.kt | 50 +++++ .../java/io/sentry/react/RNSentryStartTest.kt | 92 ++++++-- ...RNSentryCompositeOptionsConfiguration.java | 25 +++ .../io/sentry/react/RNSentryJsonUtils.java | 41 ++++ .../java/io/sentry/react/RNSentrySDK.java | 68 ++++++ .../java/io/sentry/react/RNSentryStart.java | 118 ++++++++--- packages/core/sentry.gradle | 53 ++++- .../reactnative/sample/MainApplication.kt | 29 +-- 13 files changed, 614 insertions(+), 73 deletions(-) create mode 100644 packages/core/RNSentryAndroidTester/app/src/androidTest/assets/invalid.options.json create mode 100644 packages/core/RNSentryAndroidTester/app/src/androidTest/assets/invalid.options.txt create mode 100644 packages/core/RNSentryAndroidTester/app/src/androidTest/assets/sentry.options.json create mode 100644 packages/core/RNSentryAndroidTester/app/src/androidTest/java/io/sentry/react/RNSentrySDKTest.kt create mode 100644 packages/core/RNSentryAndroidTester/app/src/test/java/io/sentry/react/RNSentryCompositeOptionsConfigurationTest.kt create mode 100644 packages/core/android/src/main/java/io/sentry/react/RNSentryCompositeOptionsConfiguration.java create mode 100644 packages/core/android/src/main/java/io/sentry/react/RNSentryJsonUtils.java create mode 100644 packages/core/android/src/main/java/io/sentry/react/RNSentrySDK.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a73f7f4ca..26de1b8c3a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,7 +23,9 @@ - Rename `navigation.processing` span to more expressive `Navigation dispatch to screen A mounted/navigation cancelled` ([#4423](https://github.com/getsentry/sentry-react-native/pull/4423)) - Add RN SDK package to `sdk.packages` for Cocoa ([#4381](https://github.com/getsentry/sentry-react-native/pull/4381)) - Add experimental version of `startWithConfigureOptions` for Apple platforms ([#4444](https://github.com/getsentry/sentry-react-native/pull/4444)) +- Add experimental version of `init` with optional `OptionsConfiguration` for Android ([#4451](https://github.com/getsentry/sentry-react-native/pull/4451)) - Add initialization using `sentry.options.json` for Apple platforms ([#4447](https://github.com/getsentry/sentry-react-native/pull/4447)) +- Add initialization using `sentry.options.json` for Android ([#4451](https://github.com/getsentry/sentry-react-native/pull/4451)) ### Internal diff --git a/packages/core/RNSentryAndroidTester/app/src/androidTest/assets/invalid.options.json b/packages/core/RNSentryAndroidTester/app/src/androidTest/assets/invalid.options.json new file mode 100644 index 0000000000..be3bb71111 --- /dev/null +++ b/packages/core/RNSentryAndroidTester/app/src/androidTest/assets/invalid.options.json @@ -0,0 +1,3 @@ +{ + "dsn": "invalid-dsn" +} \ No newline at end of file diff --git a/packages/core/RNSentryAndroidTester/app/src/androidTest/assets/invalid.options.txt b/packages/core/RNSentryAndroidTester/app/src/androidTest/assets/invalid.options.txt new file mode 100644 index 0000000000..f07bfaea41 --- /dev/null +++ b/packages/core/RNSentryAndroidTester/app/src/androidTest/assets/invalid.options.txt @@ -0,0 +1 @@ +invalid-options \ No newline at end of file diff --git a/packages/core/RNSentryAndroidTester/app/src/androidTest/assets/sentry.options.json b/packages/core/RNSentryAndroidTester/app/src/androidTest/assets/sentry.options.json new file mode 100644 index 0000000000..f97a8df3f2 --- /dev/null +++ b/packages/core/RNSentryAndroidTester/app/src/androidTest/assets/sentry.options.json @@ -0,0 +1,5 @@ +{ + "dsn": "https://abcd@efgh.ingest.sentry.io/123456", + "enableTracing": true, + "tracesSampleRate": 1.0 +} \ No newline at end of file diff --git a/packages/core/RNSentryAndroidTester/app/src/androidTest/java/io/sentry/react/RNSentrySDKTest.kt b/packages/core/RNSentryAndroidTester/app/src/androidTest/java/io/sentry/react/RNSentrySDKTest.kt new file mode 100644 index 0000000000..3b95742e55 --- /dev/null +++ b/packages/core/RNSentryAndroidTester/app/src/androidTest/java/io/sentry/react/RNSentrySDKTest.kt @@ -0,0 +1,200 @@ +package io.sentry.react + +import android.content.Context +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import com.facebook.react.common.JavascriptException +import io.sentry.Hint +import io.sentry.ILogger +import io.sentry.Sentry +import io.sentry.Sentry.OptionsConfiguration +import io.sentry.SentryEvent +import io.sentry.android.core.AndroidLogger +import io.sentry.android.core.SentryAndroidOptions +import io.sentry.protocol.SdkVersion +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class RNSentrySDKTest { + private val logger: ILogger = AndroidLogger(RNSentrySDKTest::class.java.simpleName) + private lateinit var context: Context + + companion object { + private const val INITIALISATION_ERROR = "Failed to initialize Sentry's React Native SDK" + private const val VALID_OPTIONS = "sentry.options.json" + private const val INVALID_OPTIONS = "invalid.options.json" + private const val INVALID_JSON = "invalid.options.txt" + private const val MISSING = "non-existing-file" + + private val validConfig = + OptionsConfiguration { options -> + options.dsn = "https://abcd@efgh.ingest.sentry.io/123456" + } + private val invalidConfig = + OptionsConfiguration { options -> + options.dsn = "invalid-dsn" + } + private val emptyConfig = OptionsConfiguration {} + } + + @Before + fun setUp() { + context = InstrumentationRegistry.getInstrumentation().context + } + + @After + fun tearDown() { + Sentry.close() + } + + @Test + fun initialisesSuccessfullyWithDefaultValidJsonFile() { // sentry.options.json + RNSentrySDK.init(context) + assertTrue(Sentry.isEnabled()) + } + + @Test + fun initialisesSuccessfullyWithValidConfigurationAndDefaultValidJsonFile() { + RNSentrySDK.init(context, validConfig) + assertTrue(Sentry.isEnabled()) + } + + @Test + fun initialisesSuccessfullyWithValidConfigurationAndInvalidJsonFile() { + RNSentrySDK.init(context, validConfig, INVALID_OPTIONS, logger) + assertTrue(Sentry.isEnabled()) + } + + @Test + fun initialisesSuccessfullyWithValidConfigurationAndMissingJsonFile() { + RNSentrySDK.init(context, validConfig, MISSING, logger) + assertTrue(Sentry.isEnabled()) + } + + @Test + fun initialisesSuccessfullyWithValidConfigurationAndErrorInParsingJsonFile() { + RNSentrySDK.init(context, validConfig, INVALID_JSON, logger) + assertTrue(Sentry.isEnabled()) + } + + @Test + fun initialisesSuccessfullyWithNoConfigurationAndValidJsonFile() { + RNSentrySDK.init(context, emptyConfig, VALID_OPTIONS, logger) + assertTrue(Sentry.isEnabled()) + } + + @Test + fun failsToInitialiseWithNoConfigurationAndInvalidJsonFile() { + try { + RNSentrySDK.init(context, emptyConfig, INVALID_OPTIONS, logger) + } catch (e: Exception) { + assertEquals(INITIALISATION_ERROR, e.message) + } + assertFalse(Sentry.isEnabled()) + } + + @Test + fun failsToInitialiseWithInvalidConfigAndInvalidJsonFile() { + try { + RNSentrySDK.init(context, invalidConfig, INVALID_OPTIONS, logger) + } catch (e: Exception) { + assertEquals(INITIALISATION_ERROR, e.message) + } + assertFalse(Sentry.isEnabled()) + } + + @Test + fun failsToInitialiseWithInvalidConfigAndValidJsonFile() { + try { + RNSentrySDK.init(context, invalidConfig, VALID_OPTIONS, logger) + } catch (e: Exception) { + assertEquals(INITIALISATION_ERROR, e.message) + } + assertFalse(Sentry.isEnabled()) + } + + @Test + fun failsToInitialiseWithInvalidConfigurationAndDefaultValidJsonFile() { + try { + RNSentrySDK.init(context, invalidConfig) + } catch (e: Exception) { + assertEquals(INITIALISATION_ERROR, e.message) + } + assertFalse(Sentry.isEnabled()) + } + + @Test + fun defaultsAndFinalsAreSetWithValidJsonFile() { + RNSentrySDK.init(context, emptyConfig, VALID_OPTIONS, logger) + val actualOptions = Sentry.getCurrentHub().options as SentryAndroidOptions + verifyDefaults(actualOptions) + verifyFinals(actualOptions) + // options file + assert(actualOptions.dsn == "https://abcd@efgh.ingest.sentry.io/123456") + } + + @Test + fun defaultsAndFinalsAreSetWithValidConfiguration() { + RNSentrySDK.init(context, validConfig, MISSING, logger) + val actualOptions = Sentry.getCurrentHub().options as SentryAndroidOptions + verifyDefaults(actualOptions) + verifyFinals(actualOptions) + // configuration + assert(actualOptions.dsn == "https://abcd@efgh.ingest.sentry.io/123456") + } + + @Test + fun defaultsOverrideOptionsJsonFile() { + RNSentrySDK.init(context, emptyConfig, VALID_OPTIONS, logger) + val actualOptions = Sentry.getCurrentHub().options as SentryAndroidOptions + assertNull(actualOptions.tracesSampleRate) + assertEquals(false, actualOptions.enableTracing) + } + + @Test + fun configurationOverridesDefaultOptions() { + val validConfig = + OptionsConfiguration { options -> + options.dsn = "https://abcd@efgh.ingest.sentry.io/123456" + options.tracesSampleRate = 0.5 + options.enableTracing = true + } + RNSentrySDK.init(context, validConfig, MISSING, logger) + val actualOptions = Sentry.getCurrentHub().options as SentryAndroidOptions + assertEquals(0.5, actualOptions.tracesSampleRate) + assertEquals(true, actualOptions.enableTracing) + assert(actualOptions.dsn == "https://abcd@efgh.ingest.sentry.io/123456") + } + + private fun verifyDefaults(actualOptions: SentryAndroidOptions) { + assertTrue(actualOptions.ignoredExceptionsForType.contains(JavascriptException::class.java)) + assertEquals(RNSentryVersion.ANDROID_SDK_NAME, actualOptions.sdkVersion?.name) + assertEquals( + io.sentry.android.core.BuildConfig.VERSION_NAME, + actualOptions.sdkVersion?.version, + ) + val pack = actualOptions.sdkVersion?.packages?.first { it.name == RNSentryVersion.REACT_NATIVE_SDK_PACKAGE_NAME } + assertNotNull(pack) + assertEquals(RNSentryVersion.REACT_NATIVE_SDK_PACKAGE_VERSION, pack?.version) + assertNull(actualOptions.tracesSampleRate) + assertNull(actualOptions.tracesSampler) + assertEquals(false, actualOptions.enableTracing) + } + + private fun verifyFinals(actualOptions: SentryAndroidOptions) { + val event = + SentryEvent().apply { sdk = SdkVersion(RNSentryVersion.ANDROID_SDK_NAME, "1.0") } + val result = actualOptions.beforeSend?.execute(event, Hint()) + assertNotNull(result) + assertEquals("android", result?.getTag("event.origin")) + assertEquals("java", result?.getTag("event.environment")) + } +} diff --git a/packages/core/RNSentryAndroidTester/app/src/test/java/io/sentry/react/RNSentryCompositeOptionsConfigurationTest.kt b/packages/core/RNSentryAndroidTester/app/src/test/java/io/sentry/react/RNSentryCompositeOptionsConfigurationTest.kt new file mode 100644 index 0000000000..699fd81ccb --- /dev/null +++ b/packages/core/RNSentryAndroidTester/app/src/test/java/io/sentry/react/RNSentryCompositeOptionsConfigurationTest.kt @@ -0,0 +1,50 @@ +package io.sentry.react + +import io.sentry.Sentry.OptionsConfiguration +import io.sentry.android.core.SentryAndroidOptions +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify + +@RunWith(JUnit4::class) +class RNSentryCompositeOptionsConfigurationTest { + @Test + fun `configure should call base and overriding configurations`() { + val baseConfig: OptionsConfiguration = mock() + val overridingConfig: OptionsConfiguration = mock() + + val compositeConfig = RNSentryCompositeOptionsConfiguration(baseConfig, overridingConfig) + val options = SentryAndroidOptions() + compositeConfig.configure(options) + + verify(baseConfig).configure(options) + verify(overridingConfig).configure(options) + } + + @Test + fun `configure should apply base configuration and override values`() { + val baseConfig = + OptionsConfiguration { options -> + options.dsn = "https://base-dsn@sentry.io" + options.isDebug = false + options.release = "some-release" + } + val overridingConfig = + OptionsConfiguration { options -> + options.dsn = "https://over-dsn@sentry.io" + options.isDebug = true + options.environment = "production" + } + + val compositeConfig = RNSentryCompositeOptionsConfiguration(baseConfig, overridingConfig) + val options = SentryAndroidOptions() + compositeConfig.configure(options) + + assert(options.dsn == "https://over-dsn@sentry.io") // overridden value + assert(options.isDebug) // overridden value + assert(options.release == "some-release") // base value not overridden + assert(options.environment == "production") // overridden value not in base + } +} diff --git a/packages/core/RNSentryAndroidTester/app/src/test/java/io/sentry/react/RNSentryStartTest.kt b/packages/core/RNSentryAndroidTester/app/src/test/java/io/sentry/react/RNSentryStartTest.kt index c2ee6f1d88..fa177159e5 100644 --- a/packages/core/RNSentryAndroidTester/app/src/test/java/io/sentry/react/RNSentryStartTest.kt +++ b/packages/core/RNSentryAndroidTester/app/src/test/java/io/sentry/react/RNSentryStartTest.kt @@ -5,9 +5,13 @@ import com.facebook.react.bridge.JavaOnlyMap import com.facebook.react.common.JavascriptException import io.sentry.Breadcrumb import io.sentry.ILogger +import io.sentry.SentryEvent +import io.sentry.android.core.CurrentActivityHolder import io.sentry.android.core.SentryAndroidOptions +import io.sentry.protocol.SdkVersion import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse +import org.junit.Assert.assertNotNull import org.junit.Assert.assertNull import org.junit.Assert.assertTrue import org.junit.Before @@ -40,7 +44,7 @@ class RNSentryStartTest { "http://localhost:8969/teststream", ) val actualOptions = SentryAndroidOptions() - RNSentryStart.getSentryAndroidOptions(actualOptions, options, activity, logger) + RNSentryStart.getSentryAndroidOptions(actualOptions, options, logger) assert(actualOptions.isEnableSpotlight) assertEquals("http://localhost:8969/teststream", actualOptions.spotlightConnectionUrl) } @@ -49,7 +53,7 @@ class RNSentryStartTest { fun `when the spotlight url is passed, the spotlight is enabled for the given url`() { val options = JavaOnlyMap.of("spotlight", "http://localhost:8969/teststream") val actualOptions = SentryAndroidOptions() - RNSentryStart.getSentryAndroidOptions(actualOptions, options, activity, logger) + RNSentryStart.getSentryAndroidOptions(actualOptions, options, logger) assert(actualOptions.isEnableSpotlight) assertEquals("http://localhost:8969/teststream", actualOptions.spotlightConnectionUrl) } @@ -58,17 +62,10 @@ class RNSentryStartTest { fun `when the spotlight option is disabled, the spotlight SentryAndroidOption is set to false`() { val options = JavaOnlyMap.of("spotlight", false) val actualOptions = SentryAndroidOptions() - RNSentryStart.getSentryAndroidOptions(actualOptions, options, activity, logger) + RNSentryStart.getSentryAndroidOptions(actualOptions, options, logger) assertFalse(actualOptions.isEnableSpotlight) } - @Test - fun `the JavascriptException is added to the ignoredExceptionsForType list on initialisation`() { - val actualOptions = SentryAndroidOptions() - RNSentryStart.getSentryAndroidOptions(actualOptions, JavaOnlyMap.of(), activity, logger) - assertTrue(actualOptions.ignoredExceptionsForType.contains(JavascriptException::class.java)) - } - @Test fun `beforeBreadcrumb callback filters out Sentry DSN requests breadcrumbs`() { val options = SentryAndroidOptions() @@ -79,7 +76,7 @@ class RNSentryStartTest { "devServerUrl", "http://localhost:8081", ) - RNSentryStart.getSentryAndroidOptions(options, rnOptions, activity, logger) + RNSentryStart.getSentryAndroidOptions(options, rnOptions, logger) val breadcrumb = Breadcrumb().apply { @@ -103,7 +100,7 @@ class RNSentryStartTest { "devServerUrl", mockDevServerUrl, ) - RNSentryStart.getSentryAndroidOptions(options, rnOptions, activity, logger) + RNSentryStart.getSentryAndroidOptions(options, rnOptions, logger) val breadcrumb = Breadcrumb().apply { @@ -126,7 +123,7 @@ class RNSentryStartTest { "devServerUrl", "http://localhost:8081", ) - RNSentryStart.getSentryAndroidOptions(options, rnOptions, activity, logger) + RNSentryStart.getSentryAndroidOptions(options, rnOptions, logger) val breadcrumb = Breadcrumb().apply { @@ -142,7 +139,7 @@ class RNSentryStartTest { @Test fun `the breadcrumb is not filtered out when the dev server url and dsn are not passed`() { val options = SentryAndroidOptions() - RNSentryStart.getSentryAndroidOptions(options, JavaOnlyMap(), activity, logger) + RNSentryStart.getSentryAndroidOptions(options, JavaOnlyMap(), logger) val breadcrumb = Breadcrumb().apply { @@ -159,7 +156,7 @@ class RNSentryStartTest { fun `the breadcrumb is not filtered out when the dev server url is not passed and the dsn does not match`() { val options = SentryAndroidOptions() val rnOptions = JavaOnlyMap.of("dsn", "https://abc@def.ingest.sentry.io/1234567") - RNSentryStart.getSentryAndroidOptions(options, rnOptions, activity, logger) + RNSentryStart.getSentryAndroidOptions(options, rnOptions, logger) val breadcrumb = Breadcrumb().apply { @@ -176,7 +173,7 @@ class RNSentryStartTest { fun `the breadcrumb is not filtered out when the dev server url does not match and the dsn is not passed`() { val options = SentryAndroidOptions() val rnOptions = JavaOnlyMap.of("devServerUrl", "http://localhost:8081") - RNSentryStart.getSentryAndroidOptions(options, rnOptions, activity, logger) + RNSentryStart.getSentryAndroidOptions(options, rnOptions, logger) val breadcrumb = Breadcrumb().apply { @@ -188,4 +185,67 @@ class RNSentryStartTest { assertEquals(breadcrumb, result) } + + @Test + fun `the JavascriptException is added to the ignoredExceptionsForType list on with react defaults`() { + val actualOptions = SentryAndroidOptions() + RNSentryStart.updateWithReactDefaults(actualOptions, activity) + assertTrue(actualOptions.ignoredExceptionsForType.contains(JavascriptException::class.java)) + } + + @Test + fun `the sdk version information is added to the initialisation options with react defaults`() { + val actualOptions = SentryAndroidOptions() + RNSentryStart.updateWithReactDefaults(actualOptions, activity) + assertEquals(RNSentryVersion.ANDROID_SDK_NAME, actualOptions.sdkVersion?.name) + assertEquals( + io.sentry.android.core.BuildConfig.VERSION_NAME, + actualOptions.sdkVersion?.version, + ) + assertEquals(true, actualOptions.sdkVersion?.packages?.isNotEmpty()) + assertEquals( + RNSentryVersion.REACT_NATIVE_SDK_PACKAGE_NAME, + actualOptions.sdkVersion + ?.packages + ?.last() + ?.name, + ) + assertEquals( + RNSentryVersion.REACT_NATIVE_SDK_PACKAGE_VERSION, + actualOptions.sdkVersion + ?.packages + ?.last() + ?.version, + ) + } + + @Test + fun `the tracing options are added to the initialisation options with react defaults`() { + val actualOptions = SentryAndroidOptions() + RNSentryStart.updateWithReactDefaults(actualOptions, activity) + assertNull(actualOptions.tracesSampleRate) + assertNull(actualOptions.tracesSampler) + assertEquals(false, actualOptions.enableTracing) + } + + @Test + fun `the current activity is added to the initialisation options with react defaults`() { + val actualOptions = SentryAndroidOptions() + RNSentryStart.updateWithReactDefaults(actualOptions, activity) + assertEquals(activity, CurrentActivityHolder.getInstance().activity) + } + + @Test + fun `beforeSend callback that sets event tags is set with react finals`() { + val options = SentryAndroidOptions() + val event = + SentryEvent().apply { sdk = SdkVersion(RNSentryVersion.ANDROID_SDK_NAME, "1.0") } + + RNSentryStart.updateWithReactFinals(options) + val result = options.beforeSend?.execute(event, mock()) + + assertNotNull(result) + assertEquals("android", result?.getTag("event.origin")) + assertEquals("java", result?.getTag("event.environment")) + } } diff --git a/packages/core/android/src/main/java/io/sentry/react/RNSentryCompositeOptionsConfiguration.java b/packages/core/android/src/main/java/io/sentry/react/RNSentryCompositeOptionsConfiguration.java new file mode 100644 index 0000000000..0069abb660 --- /dev/null +++ b/packages/core/android/src/main/java/io/sentry/react/RNSentryCompositeOptionsConfiguration.java @@ -0,0 +1,25 @@ +package io.sentry.react; + +import io.sentry.Sentry.OptionsConfiguration; +import io.sentry.android.core.SentryAndroidOptions; +import java.util.List; +import org.jetbrains.annotations.NotNull; + +class RNSentryCompositeOptionsConfiguration implements OptionsConfiguration { + private final @NotNull List> configurations; + + @SafeVarargs + protected RNSentryCompositeOptionsConfiguration( + @NotNull OptionsConfiguration... configurations) { + this.configurations = List.of(configurations); + } + + @Override + public void configure(@NotNull SentryAndroidOptions options) { + for (OptionsConfiguration configuration : configurations) { + if (configuration != null) { + configuration.configure(options); + } + } + } +} diff --git a/packages/core/android/src/main/java/io/sentry/react/RNSentryJsonUtils.java b/packages/core/android/src/main/java/io/sentry/react/RNSentryJsonUtils.java new file mode 100644 index 0000000000..9c7cf5d3ff --- /dev/null +++ b/packages/core/android/src/main/java/io/sentry/react/RNSentryJsonUtils.java @@ -0,0 +1,41 @@ +package io.sentry.react; + +import android.content.Context; +import io.sentry.ILogger; +import io.sentry.SentryLevel; +import java.io.BufferedReader; +import java.io.InputStream; +import java.io.InputStreamReader; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.json.JSONObject; + +final class RNSentryJsonUtils { + private RNSentryJsonUtils() { + throw new AssertionError("Utility class should not be instantiated"); + } + + static @Nullable JSONObject getOptionsFromConfigurationFile( + @NotNull Context context, @NotNull String fileName, @NotNull ILogger logger) { + try (InputStream inputStream = context.getAssets().open(fileName); + BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) { + + StringBuilder stringBuilder = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + stringBuilder.append(line); + } + String configFileContent = stringBuilder.toString(); + return new JSONObject(configFileContent); + + } catch (Exception e) { + logger.log( + SentryLevel.ERROR, + "Failed to read configuration file. Please make sure " + + fileName + + " exists in the root of your project.", + e); + return null; + } + } +} diff --git a/packages/core/android/src/main/java/io/sentry/react/RNSentrySDK.java b/packages/core/android/src/main/java/io/sentry/react/RNSentrySDK.java new file mode 100644 index 0000000000..ca219351fe --- /dev/null +++ b/packages/core/android/src/main/java/io/sentry/react/RNSentrySDK.java @@ -0,0 +1,68 @@ +package io.sentry.react; + +import android.content.Context; +import com.facebook.react.bridge.ReadableMap; +import io.sentry.ILogger; +import io.sentry.Sentry; +import io.sentry.SentryLevel; +import io.sentry.android.core.AndroidLogger; +import io.sentry.android.core.SentryAndroidOptions; +import org.jetbrains.annotations.NotNull; +import org.json.JSONObject; + +public final class RNSentrySDK { + private static final String CONFIGURATION_FILE = "sentry.options.json"; + private static final String NAME = "RNSentrySDK"; + + private static final ILogger logger = new AndroidLogger(NAME); + + private RNSentrySDK() { + throw new AssertionError("Utility class should not be instantiated"); + } + + static void init( + @NotNull final Context context, + @NotNull Sentry.OptionsConfiguration configuration, + @NotNull String configurationFile, + @NotNull ILogger logger) { + try { + JSONObject jsonObject = + RNSentryJsonUtils.getOptionsFromConfigurationFile(context, configurationFile, logger); + if (jsonObject == null) { + RNSentryStart.startWithConfiguration(context, configuration); + return; + } + ReadableMap rnOptions = RNSentryJsonConverter.convertToWritable(jsonObject); + if (rnOptions == null) { + RNSentryStart.startWithConfiguration(context, configuration); + return; + } + RNSentryStart.startWithOptions(context, rnOptions, configuration, logger); + } catch (Exception e) { + logger.log( + SentryLevel.ERROR, "Failed to start Sentry with options from configuration file.", e); + throw new RuntimeException("Failed to initialize Sentry's React Native SDK", e); + } + } + + /** + * @experimental Start the Native Android SDK with the provided configuration options. Uses as a + * base configurations the `sentry.options.json` configuration file if it exists. + * @param context Android Context + * @param configuration configuration options + */ + public static void init( + @NotNull final Context context, + @NotNull Sentry.OptionsConfiguration configuration) { + init(context, configuration, CONFIGURATION_FILE, logger); + } + + /** + * @experimental Start the Native Android SDK with options from `sentry.options.json` + * configuration file. + * @param context Android Context + */ + public static void init(@NotNull final Context context) { + init(context, options -> {}, CONFIGURATION_FILE, logger); + } +} diff --git a/packages/core/android/src/main/java/io/sentry/react/RNSentryStart.java b/packages/core/android/src/main/java/io/sentry/react/RNSentryStart.java index 263633c4a8..86699ced05 100644 --- a/packages/core/android/src/main/java/io/sentry/react/RNSentryStart.java +++ b/packages/core/android/src/main/java/io/sentry/react/RNSentryStart.java @@ -7,8 +7,10 @@ import com.facebook.react.common.JavascriptException; import io.sentry.ILogger; import io.sentry.Integration; +import io.sentry.Sentry; import io.sentry.SentryEvent; import io.sentry.SentryLevel; +import io.sentry.SentryOptions.BeforeSendCallback; import io.sentry.SentryReplayOptions; import io.sentry.UncaughtExceptionHandlerIntegration; import io.sentry.android.core.AnrIntegration; @@ -27,40 +29,57 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -public final class RNSentryStart { +final class RNSentryStart { private RNSentryStart() { throw new AssertionError("Utility class should not be instantiated"); } - public static void startWithOptions( + static void startWithConfiguration( + @NotNull final Context context, + @NotNull Sentry.OptionsConfiguration configuration) { + Sentry.OptionsConfiguration defaults = + options -> updateWithReactDefaults(options, null); + RNSentryCompositeOptionsConfiguration compositeConfiguration = + new RNSentryCompositeOptionsConfiguration( + defaults, configuration, RNSentryStart::updateWithReactFinals); + SentryAndroid.init(context, compositeConfiguration); + } + + static void startWithOptions( + @NotNull final Context context, + @NotNull final ReadableMap rnOptions, + @NotNull Sentry.OptionsConfiguration configuration, + @NotNull ILogger logger) { + Sentry.OptionsConfiguration defaults = + options -> updateWithReactDefaults(options, null); + Sentry.OptionsConfiguration rnConfigurationOptions = + options -> getSentryAndroidOptions(options, rnOptions, logger); + RNSentryCompositeOptionsConfiguration compositeConfiguration = + new RNSentryCompositeOptionsConfiguration( + rnConfigurationOptions, defaults, configuration, RNSentryStart::updateWithReactFinals); + SentryAndroid.init(context, compositeConfiguration); + } + + static void startWithOptions( @NotNull final Context context, @NotNull final ReadableMap rnOptions, @Nullable Activity currentActivity, @NotNull ILogger logger) { - SentryAndroid.init( - context, options -> getSentryAndroidOptions(options, rnOptions, currentActivity, logger)); + Sentry.OptionsConfiguration defaults = + options -> updateWithReactDefaults(options, currentActivity); + Sentry.OptionsConfiguration rnConfigurationOptions = + options -> getSentryAndroidOptions(options, rnOptions, logger); + RNSentryCompositeOptionsConfiguration compositeConfiguration = + new RNSentryCompositeOptionsConfiguration( + rnConfigurationOptions, defaults, RNSentryStart::updateWithReactFinals); + SentryAndroid.init(context, compositeConfiguration); } static void getSentryAndroidOptions( @NotNull SentryAndroidOptions options, @NotNull ReadableMap rnOptions, - @Nullable Activity currentActivity, - ILogger logger) { - @Nullable SdkVersion sdkVersion = options.getSdkVersion(); - if (sdkVersion == null) { - sdkVersion = new SdkVersion(RNSentryVersion.ANDROID_SDK_NAME, BuildConfig.VERSION_NAME); - } else { - sdkVersion.setName(RNSentryVersion.ANDROID_SDK_NAME); - } - sdkVersion.addPackage( - RNSentryVersion.REACT_NATIVE_SDK_PACKAGE_NAME, - RNSentryVersion.REACT_NATIVE_SDK_PACKAGE_VERSION); - - options.setSentryClientName(sdkVersion.getName() + "/" + sdkVersion.getVersion()); - options.setNativeSdkName(RNSentryVersion.NATIVE_SDK_NAME); - options.setSdkVersion(sdkVersion); - + @NotNull ILogger logger) { if (rnOptions.hasKey("debug") && rnOptions.getBoolean("debug")) { options.setDebug(true); } @@ -159,18 +178,6 @@ static void getSentryAndroidOptions( return breadcrumb; }); - // React native internally throws a JavascriptException. - // we want to ignore it on the native side to avoid sending it twice. - options.addIgnoredExceptionForType(JavascriptException.class); - - options.setBeforeSend( - (event, hint) -> { - setEventOriginTag(event); - addPackages(event, options.getSdkVersion()); - - return event; - }); - if (rnOptions.hasKey("enableNativeCrashHandling") && !rnOptions.getBoolean("enableNativeCrashHandling")) { final List integrations = options.getIntegrations(); @@ -184,10 +191,57 @@ static void getSentryAndroidOptions( } logger.log( SentryLevel.INFO, String.format("Native Integrations '%s'", options.getIntegrations())); + } + + /** + * This function updates the options with RNSentry defaults. These default can be overwritten by + * users during manual native initialization. + */ + static void updateWithReactDefaults( + @NotNull SentryAndroidOptions options, @Nullable Activity currentActivity) { + @Nullable SdkVersion sdkVersion = options.getSdkVersion(); + if (sdkVersion == null) { + sdkVersion = new SdkVersion(RNSentryVersion.ANDROID_SDK_NAME, BuildConfig.VERSION_NAME); + } else { + sdkVersion.setName(RNSentryVersion.ANDROID_SDK_NAME); + } + sdkVersion.addPackage( + RNSentryVersion.REACT_NATIVE_SDK_PACKAGE_NAME, + RNSentryVersion.REACT_NATIVE_SDK_PACKAGE_VERSION); + + options.setSentryClientName(sdkVersion.getName() + "/" + sdkVersion.getVersion()); + options.setNativeSdkName(RNSentryVersion.NATIVE_SDK_NAME); + options.setSdkVersion(sdkVersion); + + // Tracing is only enabled in JS to avoid duplicate navigation spans + options.setTracesSampleRate(null); + options.setTracesSampler(null); + options.setEnableTracing(false); + + // React native internally throws a JavascriptException. + // we want to ignore it on the native side to avoid sending it twice. + options.addIgnoredExceptionForType(JavascriptException.class); setCurrentActivity(currentActivity); } + /** + * This function updates options with changes RNSentry users should not change and so this is + * applied after the configureOptions callback during manual native initialization. + */ + static void updateWithReactFinals(@NotNull SentryAndroidOptions options) { + BeforeSendCallback userBeforeSend = options.getBeforeSend(); + options.setBeforeSend( + (event, hint) -> { + setEventOriginTag(event); + addPackages(event, options.getSdkVersion()); + if (userBeforeSend != null) { + return userBeforeSend.execute(event, hint); + } + return event; + }); + } + private static void setCurrentActivity(Activity currentActivity) { final CurrentActivityHolder currentActivityHolder = CurrentActivityHolder.getInstance(); if (currentActivity != null) { diff --git a/packages/core/sentry.gradle b/packages/core/sentry.gradle index fbbf567412..990703527f 100644 --- a/packages/core/sentry.gradle +++ b/packages/core/sentry.gradle @@ -3,7 +3,7 @@ import org.apache.tools.ant.taskdefs.condition.Os import java.util.regex.Matcher import java.util.regex.Pattern -project.ext.shouldSentryAutoUploadNative = { -> +project.ext.shouldSentryAutoUploadNative = { -> return System.getenv('SENTRY_DISABLE_NATIVE_DEBUG_UPLOAD') != 'true' } @@ -15,9 +15,60 @@ project.ext.shouldSentryAutoUpload = { -> return shouldSentryAutoUploadGeneral() && shouldSentryAutoUploadNative() } +project.ext.shouldCopySentryOptionsFile = { -> // If not set, default to true + return System.getenv('SENTRY_COPY_OPTIONS_FILE') != 'false' +} + def config = project.hasProperty("sentryCli") ? project.sentryCli : []; +def configFile = "sentry.options.json" // Sentry condiguration file +def androidAssetsDir = new File("$rootDir/app/src/main/assets") // Path to Android assets folder + +tasks.register("copySentryJsonConfiguration") { + onlyIf { shouldCopySentryOptionsFile() } + doLast { + def appRoot = project.rootDir.parentFile ?: project.rootDir + def sentryOptionsFile = new File(appRoot, configFile) + if (sentryOptionsFile.exists()) { + if (!androidAssetsDir.exists()) { + androidAssetsDir.mkdirs() + } + + copy { + from sentryOptionsFile + into androidAssetsDir + rename { String fileName -> configFile } + } + logger.lifecycle("Copied ${configFile} to Android assets") + } else { + logger.warn("${configFile} not found in app root (${appRoot})") + } + } +} + +tasks.register("cleanupTemporarySentryJsonConfiguration") { + onlyIf { shouldCopySentryOptionsFile() } + doLast { + def sentryOptionsFile = new File(androidAssetsDir, configFile) + if (sentryOptionsFile.exists()) { + logger.lifecycle("Deleting temporary file: ${sentryOptionsFile.path}") + sentryOptionsFile.delete() + } + } +} + gradle.projectsEvaluated { + // Add a task that copies the sentry.options.json file before the build starts + tasks.named("preBuild").configure { + dependsOn("copySentryJsonConfiguration") + } + // Cleanup sentry.options.json from assets after the build + tasks.matching { task -> + task.name == "build" || task.name.startsWith("assemble") || task.name.startsWith("install") + }.configureEach { + finalizedBy("cleanupTemporarySentryJsonConfiguration") + } + def releases = extractReleasesInfo() if (config.flavorAware && config.sentryProperties) { diff --git a/samples/react-native/android/app/src/main/java/io/sentry/reactnative/sample/MainApplication.kt b/samples/react-native/android/app/src/main/java/io/sentry/reactnative/sample/MainApplication.kt index 07747f085c..6546ca8b10 100644 --- a/samples/react-native/android/app/src/main/java/io/sentry/reactnative/sample/MainApplication.kt +++ b/samples/react-native/android/app/src/main/java/io/sentry/reactnative/sample/MainApplication.kt @@ -11,10 +11,7 @@ import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost import com.facebook.react.defaults.DefaultReactNativeHost import com.facebook.react.soloader.OpenSourceMergedSoMapping import com.facebook.soloader.SoLoader -import io.sentry.Hint -import io.sentry.SentryEvent -import io.sentry.SentryOptions.BeforeSendCallback -import io.sentry.android.core.SentryAndroid +import io.sentry.react.RNSentrySDK class MainApplication : Application(), @@ -51,28 +48,12 @@ class MainApplication : } private fun initializeSentry() { - SentryAndroid.init(this) { options -> - // Only options set here will apply to the Android SDK - // Options from JS are not passed to the Android SDK when initialized manually + RNSentrySDK.init(this) { options -> + // Options set here will apply to the Android SDK overriding the ones from `sentry.options.json` options.dsn = "https://1df17bd4e543fdb31351dee1768bb679@o447951.ingest.sentry.io/5428561" options.isDebug = true - - options.beforeSend = - BeforeSendCallback { event: SentryEvent, hint: Hint? -> - // React native internally throws a JavascriptException - // Since we catch it before that, we don't want to send this one - // because we would send it twice - try { - val ex = event.exceptions!![0] - if (null != ex && ex.type!!.contains("JavascriptException")) { - return@BeforeSendCallback null - } - } catch (ignored: Throwable) { - // We do nothing - } - - event - } } + + // RNSentrySDK.init(this) } } From 14fe05db7ca4c5e24a383623aa06db2f45fdf546 Mon Sep 17 00:00:00 2001 From: Krystof Woldrich <31292499+krystofwoldrich@users.noreply.github.com> Date: Wed, 5 Feb 2025 13:15:08 +0100 Subject: [PATCH 08/32] misc: Add `sentry.options.json` example to the changelog (#4509) --- CHANGELOG.md | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8208dd1e92..51248fd473 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,44 @@ ## Unreleased +### Features + +- Capture App Start errors and crashes by initializing Sentry from `sentry.options.json` ([#4472](https://github.com/getsentry/sentry-react-native/pull/4472)) + + Create `sentry.options.json` in the React Native project root and set options the same as you currently have in `Sentry.init` in JS. + + ```json + { + "dsn": "https://key@example.io/value", + } + ``` + + Initialize Sentry on the native layers by newly provided native methods. + + ```kotlin + import io.sentry.react.RNSentrySDK + + class MainApplication : Application(), ReactApplication { + override fun onCreate() { + super.onCreate() + RNSentrySDK.init(this) + } + } + ``` + + ```obj-c + #import + + @implementation AppDelegate + - (BOOL)application:(UIApplication *)application + didFinishLaunchingWithOptions:(NSDictionary *)launchOptions + { + [RNSentrySDK start]; + return [super application:application didFinishLaunchingWithOptions:launchOptions]; + } + @end + ``` + ### Changes - Load `optionsFile` into the JS bundle during Metro bundle process ([#4476](https://github.com/getsentry/sentry-react-native/pull/4476)) From 15a7e6d7d7c6d144a3471cc37d6b7b68e48efb57 Mon Sep 17 00:00:00 2001 From: Krystof Woldrich <31292499+krystofwoldrich@users.noreply.github.com> Date: Thu, 6 Feb 2025 11:48:30 +0100 Subject: [PATCH 09/32] feat(init): Load options from `sentry.options.json` in JS (#4510) --- CHANGELOG.md | 1 + packages/core/src/js/sdk.tsx | 46 ++++++++++++------ packages/core/src/js/utils/worldwide.ts | 2 + packages/core/test/sdk.test.ts | 64 ++++++++++++++++++++++--- 4 files changed, 93 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 51248fd473..dbd1e20772 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -53,6 +53,7 @@ - Add experimental version of `init` with optional `OptionsConfiguration` for Android ([#4451](https://github.com/getsentry/sentry-react-native/pull/4451)) - Add initialization using `sentry.options.json` for Apple platforms ([#4447](https://github.com/getsentry/sentry-react-native/pull/4447)) - Add initialization using `sentry.options.json` for Android ([#4451](https://github.com/getsentry/sentry-react-native/pull/4451)) +- Merge options from file with `Sentry.init` options in JS ([#4510](https://github.com/getsentry/sentry-react-native/pull/4510)) ### Internal diff --git a/packages/core/src/js/sdk.tsx b/packages/core/src/js/sdk.tsx index 3c6fdff90c..3ff40508af 100644 --- a/packages/core/src/js/sdk.tsx +++ b/packages/core/src/js/sdk.tsx @@ -19,6 +19,7 @@ import { useEncodePolyfill } from './transports/encodePolyfill'; import { DEFAULT_BUFFER_SIZE, makeNativeTransportFactory } from './transports/native'; import { getDefaultEnvironment, isExpoGo, isRunningInMetroDevServer } from './utils/environment'; import { safeFactory, safeTracesSampler } from './utils/safe'; +import { RN_GLOBAL_OBJ } from './utils/worldwide'; import { NATIVE } from './wrapper'; const DEFAULT_OPTIONS: ReactNativeOptions = { @@ -47,12 +48,17 @@ export function init(passedOptions: ReactNativeOptions): void { return; } - const maxQueueSize = passedOptions.maxQueueSize + const userOptions = { + ...RN_GLOBAL_OBJ.__SENTRY_OPTIONS__, + ...passedOptions, + }; + + const maxQueueSize = userOptions.maxQueueSize // eslint-disable-next-line deprecation/deprecation - ?? passedOptions.transportOptions?.bufferSize + ?? userOptions.transportOptions?.bufferSize ?? DEFAULT_OPTIONS.maxQueueSize; - const enableNative = passedOptions.enableNative === undefined || passedOptions.enableNative + const enableNative = userOptions.enableNative === undefined || userOptions.enableNative ? NATIVE.isNativeAvailable() : false; @@ -75,11 +81,11 @@ export function init(passedOptions: ReactNativeOptions): void { return `${dsnComponents.protocol}://${dsnComponents.host}${port}`; }; - const userBeforeBreadcrumb = safeFactory(passedOptions.beforeBreadcrumb, { loggerMessage: 'The beforeBreadcrumb threw an error' }); + const userBeforeBreadcrumb = safeFactory(userOptions.beforeBreadcrumb, { loggerMessage: 'The beforeBreadcrumb threw an error' }); // Exclude Dev Server and Sentry Dsn request from Breadcrumbs const devServerUrl = getDevServer()?.url; - const dsn = getURLFromDSN(passedOptions.dsn); + const dsn = getURLFromDSN(userOptions.dsn); const defaultBeforeBreadcrumb = (breadcrumb: Breadcrumb, _hint?: BreadcrumbHint): Breadcrumb | null => { const type = breadcrumb.type || ''; const url = typeof breadcrumb.data?.url === 'string' ? breadcrumb.data.url : ''; @@ -103,26 +109,34 @@ export function init(passedOptions: ReactNativeOptions): void { const options: ReactNativeClientOptions = { ...DEFAULT_OPTIONS, - ...passedOptions, + ...userOptions, enableNative, - enableNativeNagger: shouldEnableNativeNagger(passedOptions.enableNativeNagger), + enableNativeNagger: shouldEnableNativeNagger(userOptions.enableNativeNagger), // If custom transport factory fails the SDK won't initialize - transport: passedOptions.transport + transport: userOptions.transport || makeNativeTransportFactory({ enableNative, }) || makeFetchTransport, transportOptions: { ...DEFAULT_OPTIONS.transportOptions, - ...(passedOptions.transportOptions ?? {}), + ...(userOptions.transportOptions ?? {}), bufferSize: maxQueueSize, }, maxQueueSize, integrations: [], - stackParser: stackParserFromStackParserOptions(passedOptions.stackParser || defaultStackParser), + stackParser: stackParserFromStackParserOptions(userOptions.stackParser || defaultStackParser), beforeBreadcrumb: chainedBeforeBreadcrumb, - initialScope: safeFactory(passedOptions.initialScope, { loggerMessage: 'The initialScope threw an error' }), + initialScope: safeFactory(userOptions.initialScope, { loggerMessage: 'The initialScope threw an error' }), }; + + if (!('autoInitializeNativeSdk' in userOptions) && RN_GLOBAL_OBJ.__SENTRY_OPTIONS__) { + // We expect users to use the file options only in combination with manual native initialization + // eslint-disable-next-line no-console + console.info('Initializing Sentry JS with the options file. Expecting manual native initialization before JS. Native will not be initialized automatically.'); + options.autoInitializeNativeSdk = false; + } + if ('tracesSampler' in options) { options.tracesSampler = safeTracesSampler(options.tracesSampler); } @@ -131,12 +145,12 @@ export function init(passedOptions: ReactNativeOptions): void { options.environment = getDefaultEnvironment(); } - const defaultIntegrations: false | Integration[] = passedOptions.defaultIntegrations === undefined + const defaultIntegrations: false | Integration[] = userOptions.defaultIntegrations === undefined ? getDefaultIntegrations(options) - : passedOptions.defaultIntegrations; + : userOptions.defaultIntegrations; options.integrations = getIntegrationsToSetup({ - integrations: safeFactory(passedOptions.integrations, { loggerMessage: 'The integrations threw an error' }), + integrations: safeFactory(userOptions.integrations, { loggerMessage: 'The integrations threw an error' }), defaultIntegrations, }); initAndBind(ReactNativeClient, options); @@ -145,6 +159,10 @@ export function init(passedOptions: ReactNativeOptions): void { logger.info('Offline caching, native errors features are not available in Expo Go.'); logger.info('Use EAS Build / Native Release Build to test these features.'); } + + if (RN_GLOBAL_OBJ.__SENTRY_OPTIONS__) { + logger.info('Sentry JS initialized with options from the options file.'); + } } /** diff --git a/packages/core/src/js/utils/worldwide.ts b/packages/core/src/js/utils/worldwide.ts index c1a4ae5dbb..0dc265763d 100644 --- a/packages/core/src/js/utils/worldwide.ts +++ b/packages/core/src/js/utils/worldwide.ts @@ -2,6 +2,7 @@ import type { InternalGlobal } from '@sentry/core'; import { GLOBAL_OBJ } from '@sentry/core'; import type { ErrorUtils } from 'react-native/types'; +import type { ReactNativeOptions } from '../options'; import type { ExpoGlobalObject } from './expoglobalobject'; /** Internal Global object interface with common and Sentry specific properties */ @@ -25,6 +26,7 @@ export interface ReactNativeInternalGlobal extends InternalGlobal { __BUNDLE_START_TIME__?: number; nativePerformanceNow?: () => number; TextEncoder?: TextEncoder; + __SENTRY_OPTIONS__?: ReactNativeOptions; } type TextEncoder = { diff --git a/packages/core/test/sdk.test.ts b/packages/core/test/sdk.test.ts index afd6137c8a..0e64264899 100644 --- a/packages/core/test/sdk.test.ts +++ b/packages/core/test/sdk.test.ts @@ -1,13 +1,15 @@ -import type { BaseTransportOptions, Breadcrumb, BreadcrumbHint, ClientOptions, Integration, Scope } from '@sentry/core'; +import type { Breadcrumb, BreadcrumbHint, Integration, Scope } from '@sentry/core'; import { initAndBind, logger } from '@sentry/core'; import { makeFetchTransport } from '@sentry/react'; import { getDevServer } from '../src/js/integrations/debugsymbolicatorutils'; +import type { ReactNativeClientOptions } from '../src/js/options'; import { init, withScope } from '../src/js/sdk'; import type { ReactNativeTracingIntegration } from '../src/js/tracing'; import { REACT_NATIVE_TRACING_INTEGRATION_NAME, reactNativeTracingIntegration } from '../src/js/tracing'; import { makeNativeTransport } from '../src/js/transports/native'; import { getDefaultEnvironment, isExpoGo, notWeb } from '../src/js/utils/environment'; +import { RN_GLOBAL_OBJ } from '../src/js/utils/worldwide'; import { NATIVE } from './mockWrapper'; import { firstArg, secondArg } from './testutils'; @@ -109,6 +111,60 @@ describe('Tests the SDK functionality', () => { }); }); + describe('initialization from sentry.options.json', () => { + it('initializes without __SENTRY_OPTIONS__', () => { + delete RN_GLOBAL_OBJ.__SENTRY_OPTIONS__; + init({}); + expect(initAndBind).toHaveBeenCalledOnce(); + }); + + it('adds options from __SENTRY_OPTIONS__', () => { + RN_GLOBAL_OBJ.__SENTRY_OPTIONS__ = { + dsn: 'https://key@example.io/value', + }; + init({}); + expect(usedOptions()?.dsn).toBe('https://key@example.io/value'); + }); + + it('options init override options from __SENTRY_OPTIONS__', () => { + RN_GLOBAL_OBJ.__SENTRY_OPTIONS__ = { + dsn: 'https://key@example.io/file', + }; + init({ + dsn: 'https://key@example.io/code', + }); + expect(usedOptions()?.dsn).toBe('https://key@example.io/code'); + }); + + it('initializing with __SENTRY_OPTIONS__ disabled native auto initialization', () => { + RN_GLOBAL_OBJ.__SENTRY_OPTIONS__ = {}; + init({}); + expect(usedOptions()?.autoInitializeNativeSdk).toBe(false); + }); + + it('initializing without __SENTRY_OPTIONS__ enables native auto initialization', () => { + RN_GLOBAL_OBJ.__SENTRY_OPTIONS__ = undefined; + init({}); + expect(usedOptions()?.autoInitializeNativeSdk).toBe(true); + }); + + it('initializing with __SENTRY_OPTIONS__ keeps native auto initialization true if set', () => { + RN_GLOBAL_OBJ.__SENTRY_OPTIONS__ = {}; + init({ + autoInitializeNativeSdk: true, + }); + expect(usedOptions()?.autoInitializeNativeSdk).toBe(true); + }); + + it('initializing with __SENTRY_OPTIONS__ keeps native auto initialization false if set', () => { + RN_GLOBAL_OBJ.__SENTRY_OPTIONS__ = {}; + init({ + autoInitializeNativeSdk: false, + }); + expect(usedOptions()?.autoInitializeNativeSdk).toBe(false); + }); + }); + describe('environment', () => { it('detect development environment', () => { (getDefaultEnvironment as jest.Mock).mockImplementation(() => 'development'); @@ -173,7 +229,6 @@ describe('Tests the SDK functionality', () => { (NATIVE.isNativeAvailable as jest.Mock).mockImplementation(() => false); init({}); expect(NATIVE.isNativeAvailable).toBeCalled(); - // @ts-expect-error enableNative not publicly available here. expect(usedOptions()?.enableNative).toEqual(false); expect(usedOptions()?.transport).toEqual(makeFetchTransport); }); @@ -182,7 +237,6 @@ describe('Tests the SDK functionality', () => { (NATIVE.isNativeAvailable as jest.Mock).mockImplementation(() => false); init({ enableNative: true }); expect(NATIVE.isNativeAvailable).toBeCalled(); - // @ts-expect-error enableNative not publicly available here. expect(usedOptions()?.enableNative).toEqual(false); expect(usedOptions()?.transport).toEqual(makeFetchTransport); }); @@ -191,7 +245,6 @@ describe('Tests the SDK functionality', () => { (NATIVE.isNativeAvailable as jest.Mock).mockImplementation(() => false); init({ enableNative: false }); expect(NATIVE.isNativeAvailable).not.toBeCalled(); - // @ts-expect-error enableNative not publicly available here. expect(usedOptions()?.enableNative).toEqual(false); expect(usedOptions()?.transport).toEqual(makeFetchTransport); }); @@ -204,7 +257,6 @@ describe('Tests the SDK functionality', () => { }); expect(usedOptions()?.transport).toEqual(mockTransport); expect(NATIVE.isNativeAvailable).toBeCalled(); - // @ts-expect-error enableNative not publicly available here. expect(usedOptions()?.enableNative).toEqual(false); }); }); @@ -1058,7 +1110,7 @@ function createMockedIntegration({ name }: { name?: string } = {}): Integration }; } -function usedOptions(): ClientOptions | undefined { +function usedOptions(): ReactNativeClientOptions | undefined { return (initAndBind as jest.MockedFunction).mock.calls[0]?.[secondArg]; } From b9ec093320cd286a1916ab1795e20f31d1a2f8ce Mon Sep 17 00:00:00 2001 From: getsentry-bot Date: Thu, 6 Feb 2025 14:02:16 +0000 Subject: [PATCH 10/32] release: 6.7.0-alpha.0 --- CHANGELOG.md | 2 +- dev-packages/e2e-tests/package.json | 4 ++-- dev-packages/type-check/package.json | 2 +- dev-packages/utils/package.json | 2 +- lerna.json | 2 +- .../main/java/io/sentry/react/RNSentryVersion.java | 2 +- packages/core/ios/RNSentryVersion.m | 2 +- packages/core/package.json | 2 +- packages/core/src/js/version.ts | 2 +- performance-tests/TestAppPlain/package.json | 2 +- performance-tests/TestAppSentry/package.json | 4 ++-- samples/expo/app.json | 6 +++--- samples/expo/package.json | 4 ++-- samples/react-native-macos/package.json | 4 ++-- samples/react-native/android/app/build.gradle | 4 ++-- .../ios/sentryreactnativesample/Info.plist | 4 ++-- .../ios/sentryreactnativesampleTests/Info.plist | 4 ++-- samples/react-native/package.json | 4 ++-- yarn.lock | 12 ++++++------ 19 files changed, 34 insertions(+), 34 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dbd1e20772..bb8c861c89 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ > make sure you follow our [migration guide](https://docs.sentry.io/platforms/react-native/migration/) first. -## Unreleased +## 6.7.0-alpha.0 ### Features diff --git a/dev-packages/e2e-tests/package.json b/dev-packages/e2e-tests/package.json index fd1a5e25c7..6b2c4e305c 100644 --- a/dev-packages/e2e-tests/package.json +++ b/dev-packages/e2e-tests/package.json @@ -1,6 +1,6 @@ { "name": "sentry-react-native-e2e-tests", - "version": "6.6.0", + "version": "6.7.0-alpha.0", "private": true, "description": "Sentry React Native End to End Tests Library", "main": "dist/index.js", @@ -14,7 +14,7 @@ "@babel/preset-env": "^7.25.3", "@babel/preset-typescript": "^7.18.6", "@sentry/core": "8.54.0", - "@sentry/react-native": "6.6.0", + "@sentry/react-native": "6.7.0-alpha.0", "@types/node": "^20.9.3", "@types/react": "^18.2.64", "appium": "2.4.1", diff --git a/dev-packages/type-check/package.json b/dev-packages/type-check/package.json index 6ed44c18a9..01cbef78bb 100644 --- a/dev-packages/type-check/package.json +++ b/dev-packages/type-check/package.json @@ -1,7 +1,7 @@ { "name": "sentry-react-native-type-check", "private": true, - "version": "6.6.0", + "version": "6.7.0-alpha.0", "scripts": { "type-check": "./run-type-check.sh" } diff --git a/dev-packages/utils/package.json b/dev-packages/utils/package.json index dc690d20e3..3eab86ea36 100644 --- a/dev-packages/utils/package.json +++ b/dev-packages/utils/package.json @@ -1,6 +1,6 @@ { "name": "sentry-react-native-samples-utils", - "version": "6.6.0", + "version": "6.7.0-alpha.0", "description": "Internal Samples Utils", "main": "index.js", "license": "MIT", diff --git a/lerna.json b/lerna.json index 44a86ffc11..bdcc106f79 100644 --- a/lerna.json +++ b/lerna.json @@ -1,6 +1,6 @@ { "$schema": "node_modules/lerna/schemas/lerna-schema.json", - "version": "6.6.0", + "version": "6.7.0-alpha.0", "packages": [ "packages/*", "dev-packages/*", diff --git a/packages/core/android/src/main/java/io/sentry/react/RNSentryVersion.java b/packages/core/android/src/main/java/io/sentry/react/RNSentryVersion.java index 23b1b258ed..b9a3d71bb7 100644 --- a/packages/core/android/src/main/java/io/sentry/react/RNSentryVersion.java +++ b/packages/core/android/src/main/java/io/sentry/react/RNSentryVersion.java @@ -2,7 +2,7 @@ class RNSentryVersion { static final String REACT_NATIVE_SDK_PACKAGE_NAME = "npm:@sentry/react-native"; - static final String REACT_NATIVE_SDK_PACKAGE_VERSION = "6.6.0"; + static final String REACT_NATIVE_SDK_PACKAGE_VERSION = "6.7.0-alpha.0"; static final String NATIVE_SDK_NAME = "sentry.native.android.react-native"; static final String ANDROID_SDK_NAME = "sentry.java.android.react-native"; static final String REACT_NATIVE_SDK_NAME = "sentry.javascript.react-native"; diff --git a/packages/core/ios/RNSentryVersion.m b/packages/core/ios/RNSentryVersion.m index 5bdb2cbbc3..063c8ee257 100644 --- a/packages/core/ios/RNSentryVersion.m +++ b/packages/core/ios/RNSentryVersion.m @@ -3,4 +3,4 @@ NSString *const NATIVE_SDK_NAME = @"sentry.cocoa.react-native"; NSString *const REACT_NATIVE_SDK_NAME = @"sentry.javascript.react-native"; NSString *const REACT_NATIVE_SDK_PACKAGE_NAME = @"npm:@sentry/react-native"; -NSString *const REACT_NATIVE_SDK_PACKAGE_VERSION = @"6.6.0"; +NSString *const REACT_NATIVE_SDK_PACKAGE_VERSION = @"6.7.0-alpha.0"; diff --git a/packages/core/package.json b/packages/core/package.json index 82f939e104..0370e4c183 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -2,7 +2,7 @@ "name": "@sentry/react-native", "homepage": "https://github.com/getsentry/sentry-react-native", "repository": "https://github.com/getsentry/sentry-react-native", - "version": "6.6.0", + "version": "6.7.0-alpha.0", "description": "Official Sentry SDK for react-native", "typings": "dist/js/index.d.ts", "types": "dist/js/index.d.ts", diff --git a/packages/core/src/js/version.ts b/packages/core/src/js/version.ts index 6718af2331..9714e3938e 100644 --- a/packages/core/src/js/version.ts +++ b/packages/core/src/js/version.ts @@ -1,3 +1,3 @@ export const SDK_PACKAGE_NAME = 'npm:@sentry/react-native'; export const SDK_NAME = 'sentry.javascript.react-native'; -export const SDK_VERSION = '6.6.0'; +export const SDK_VERSION = '6.7.0-alpha.0'; diff --git a/performance-tests/TestAppPlain/package.json b/performance-tests/TestAppPlain/package.json index 67b6a8b3dd..74a009292d 100644 --- a/performance-tests/TestAppPlain/package.json +++ b/performance-tests/TestAppPlain/package.json @@ -1,6 +1,6 @@ { "name": "TestAppPlain", - "version": "6.6.0", + "version": "6.7.0-alpha.0", "private": true, "scripts": { "android": "react-native run-android", diff --git a/performance-tests/TestAppSentry/package.json b/performance-tests/TestAppSentry/package.json index de301f4bf2..7e8f774c2a 100644 --- a/performance-tests/TestAppSentry/package.json +++ b/performance-tests/TestAppSentry/package.json @@ -1,6 +1,6 @@ { "name": "TestAppSentry", - "version": "6.6.0", + "version": "6.7.0-alpha.0", "private": true, "scripts": { "android": "react-native run-android", @@ -8,7 +8,7 @@ "start": "react-native start" }, "dependencies": { - "@sentry/react-native": "6.6.0", + "@sentry/react-native": "6.7.0-alpha.0", "react": "18.1.0", "react-native": "0.70.15" }, diff --git a/samples/expo/app.json b/samples/expo/app.json index c97346d0b5..a9251abe0a 100644 --- a/samples/expo/app.json +++ b/samples/expo/app.json @@ -4,7 +4,7 @@ "slug": "sentry-react-native-expo-sample", "jsEngine": "hermes", "scheme": "sentry-expo-sample", - "version": "6.6.0", + "version": "6.7.0-alpha.0", "orientation": "portrait", "icon": "./assets/icon.png", "userInterfaceStyle": "light", @@ -19,7 +19,7 @@ "ios": { "supportsTablet": true, "bundleIdentifier": "io.sentry.expo.sample", - "buildNumber": "36" + "buildNumber": "37" }, "android": { "adaptiveIcon": { @@ -27,7 +27,7 @@ "backgroundColor": "#ffffff" }, "package": "io.sentry.expo.sample", - "versionCode": 36 + "versionCode": 37 }, "web": { "bundler": "metro", diff --git a/samples/expo/package.json b/samples/expo/package.json index b151813e59..bf2fcbc1d5 100644 --- a/samples/expo/package.json +++ b/samples/expo/package.json @@ -1,6 +1,6 @@ { "name": "sentry-react-native-expo-sample", - "version": "6.6.0", + "version": "6.7.0-alpha.0", "main": "expo-router/entry", "scripts": { "start": "expo start", @@ -16,7 +16,7 @@ "set-version": "npx react-native-version --skip-tag --never-amend" }, "dependencies": { - "@sentry/react-native": "6.6.0", + "@sentry/react-native": "6.7.0-alpha.0", "@types/react": "~18.3.12", "expo": "^52.0.0", "expo-constants": "~17.0.3", diff --git a/samples/react-native-macos/package.json b/samples/react-native-macos/package.json index 47d5e98ced..2fa48cf7cc 100644 --- a/samples/react-native-macos/package.json +++ b/samples/react-native-macos/package.json @@ -1,6 +1,6 @@ { "name": "sentry-react-native-macos-sample", - "version": "6.6.0", + "version": "6.7.0-alpha.0", "private": true, "scripts": { "start": "react-native start --experimental-debugger", @@ -18,7 +18,7 @@ "@react-navigation/stack": "^6.3.20", "@sentry/core": "8.54.0", "@sentry/react": "8.54.0", - "@sentry/react-native": "6.6.0", + "@sentry/react-native": "6.7.0-alpha.0", "delay": "^6.0.0", "react": "18.2.0", "react-native": "0.73.9", diff --git a/samples/react-native/android/app/build.gradle b/samples/react-native/android/app/build.gradle index 4c1341b4c8..2d24b2ede1 100644 --- a/samples/react-native/android/app/build.gradle +++ b/samples/react-native/android/app/build.gradle @@ -136,8 +136,8 @@ android { applicationId "io.sentry.reactnative.sample" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 38 - versionName "6.6.0" + versionCode 39 + versionName "6.7.0-alpha.0" } signingConfigs { diff --git a/samples/react-native/ios/sentryreactnativesample/Info.plist b/samples/react-native/ios/sentryreactnativesample/Info.plist index 12ed645a84..17529727b2 100644 --- a/samples/react-native/ios/sentryreactnativesample/Info.plist +++ b/samples/react-native/ios/sentryreactnativesample/Info.plist @@ -17,11 +17,11 @@ CFBundlePackageType APPL CFBundleShortVersionString - 6.6.0 + 6.7.0 CFBundleSignature ???? CFBundleVersion - 43 + 44 LSRequiresIPhoneOS NSAppTransportSecurity diff --git a/samples/react-native/ios/sentryreactnativesampleTests/Info.plist b/samples/react-native/ios/sentryreactnativesampleTests/Info.plist index 515e2c9f2a..363ef122d5 100644 --- a/samples/react-native/ios/sentryreactnativesampleTests/Info.plist +++ b/samples/react-native/ios/sentryreactnativesampleTests/Info.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 6.6.0 + 6.7.0 CFBundleSignature ???? CFBundleVersion - 43 + 44 diff --git a/samples/react-native/package.json b/samples/react-native/package.json index 02d2f942dd..143a4b6c7f 100644 --- a/samples/react-native/package.json +++ b/samples/react-native/package.json @@ -1,6 +1,6 @@ { "name": "sentry-react-native-sample", - "version": "6.6.0", + "version": "6.7.0-alpha.0", "private": true, "scripts": { "postinstall": "patch-package", @@ -25,7 +25,7 @@ "@react-navigation/native": "^7.0.3", "@react-navigation/native-stack": "^7.0.3", "@react-navigation/stack": "^7.0.3", - "@sentry/react-native": "6.6.0", + "@sentry/react-native": "6.7.0-alpha.0", "delay": "^6.0.0", "react": "18.3.1", "react-native": "0.76.3", diff --git a/yarn.lock b/yarn.lock index 6e3e8b6a99..64b67a8bb0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7805,7 +7805,7 @@ __metadata: languageName: node linkType: hard -"@sentry/react-native@6.6.0, @sentry/react-native@workspace:packages/core": +"@sentry/react-native@6.7.0-alpha.0, @sentry/react-native@workspace:packages/core": version: 0.0.0-use.local resolution: "@sentry/react-native@workspace:packages/core" dependencies: @@ -9480,7 +9480,7 @@ __metadata: dependencies: "@babel/core": ^7.12.9 "@babel/runtime": ^7.12.5 - "@sentry/react-native": 6.6.0 + "@sentry/react-native": 6.7.0-alpha.0 metro-react-native-babel-preset: ^0.72.3 react: 18.1.0 react-native: 0.70.15 @@ -24156,7 +24156,7 @@ __metadata: "@babel/preset-env": ^7.25.3 "@babel/preset-typescript": ^7.18.6 "@sentry/core": 8.54.0 - "@sentry/react-native": 6.6.0 + "@sentry/react-native": 6.7.0-alpha.0 "@types/node": ^20.9.3 "@types/react": ^18.2.64 appium: 2.4.1 @@ -24185,7 +24185,7 @@ __metadata: "@babel/core": ^7.26.0 "@babel/preset-env": ^7.26.0 "@sentry/babel-plugin-component-annotate": ^2.18.0 - "@sentry/react-native": 6.6.0 + "@sentry/react-native": 6.7.0-alpha.0 "@types/node": 20.10.4 "@types/react": ~18.3.12 expo: ^52.0.0 @@ -24222,7 +24222,7 @@ __metadata: "@react-navigation/stack": ^6.3.20 "@sentry/core": 8.54.0 "@sentry/react": 8.54.0 - "@sentry/react-native": 6.6.0 + "@sentry/react-native": 6.7.0-alpha.0 "@types/react": ^18.2.65 "@types/react-native-vector-icons": ^6.4.18 "@types/react-test-renderer": ^18.0.0 @@ -24268,7 +24268,7 @@ __metadata: "@react-navigation/native-stack": ^7.0.3 "@react-navigation/stack": ^7.0.3 "@sentry/babel-plugin-component-annotate": ^2.18.0 - "@sentry/react-native": 6.6.0 + "@sentry/react-native": 6.7.0-alpha.0 "@types/react": ^18.2.65 "@types/react-native-vector-icons": ^6.4.18 "@types/react-test-renderer": ^18.0.0 From b947d7f65be63269d7c0f29a7e11b49fc165e1a7 Mon Sep 17 00:00:00 2001 From: Krystof Woldrich <31292499+krystofwoldrich@users.noreply.github.com> Date: Thu, 13 Feb 2025 14:13:16 +0100 Subject: [PATCH 11/32] misc(sample): Change RN Sample to use native file init by default (#4522) --- samples/react-native/android/app/build.gradle | 1 + .../reactnative/sample/MainApplication.kt | 17 +++-------- .../react-native/android/gradle.properties | 5 ++++ .../sentryreactnativesample.xcscheme | 6 ++++ .../sentryreactnativesample/AppDelegate.mm | 11 +++++-- samples/react-native/package.json | 2 ++ samples/react-native/src/App.tsx | 8 +++-- samples/react-native/src/utils.ts | 30 +++++++++++++++++++ yarn.lock | 19 ++++++++++++ 9 files changed, 81 insertions(+), 18 deletions(-) diff --git a/samples/react-native/android/app/build.gradle b/samples/react-native/android/app/build.gradle index 2d24b2ede1..6c0d4a7ca1 100644 --- a/samples/react-native/android/app/build.gradle +++ b/samples/react-native/android/app/build.gradle @@ -138,6 +138,7 @@ android { targetSdkVersion rootProject.ext.targetSdkVersion versionCode 39 versionName "6.7.0-alpha.0" + buildConfigField "boolean", "SENTRY_DISABLE_NATIVE_START", System.getenv('SENTRY_DISABLE_NATIVE_START') ?: String.valueOf(sentryDisableNativeStart) } signingConfigs { diff --git a/samples/react-native/android/app/src/main/java/io/sentry/reactnative/sample/MainApplication.kt b/samples/react-native/android/app/src/main/java/io/sentry/reactnative/sample/MainApplication.kt index 6546ca8b10..5b5ac47444 100644 --- a/samples/react-native/android/app/src/main/java/io/sentry/reactnative/sample/MainApplication.kt +++ b/samples/react-native/android/app/src/main/java/io/sentry/reactnative/sample/MainApplication.kt @@ -37,23 +37,14 @@ class MainApplication : override fun onCreate() { super.onCreate() - // When the native init is enabled the `autoInitializeNativeSdk` - // in JS has to be set to `false` - // this.initializeSentry() + if (!BuildConfig.SENTRY_DISABLE_NATIVE_START) { + RNSentrySDK.init(this) + } + SoLoader.init(this, OpenSourceMergedSoMapping) if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) { // If you opted-in for the New Architecture, we load the native entry point for this app. load() } } - - private fun initializeSentry() { - RNSentrySDK.init(this) { options -> - // Options set here will apply to the Android SDK overriding the ones from `sentry.options.json` - options.dsn = "https://1df17bd4e543fdb31351dee1768bb679@o447951.ingest.sentry.io/5428561" - options.isDebug = true - } - - // RNSentrySDK.init(this) - } } diff --git a/samples/react-native/android/gradle.properties b/samples/react-native/android/gradle.properties index 600fea4b77..d71a974450 100644 --- a/samples/react-native/android/gradle.properties +++ b/samples/react-native/android/gradle.properties @@ -38,3 +38,8 @@ newArchEnabled=true # Use this property to enable or disable the Hermes JS engine. # If set to false, you will be using JSC instead. hermesEnabled=true + +# Only implemented in this sample project. +# It's used for testing the native SDK auto-start feature. +# true means manual native start is disabled and JS auto initializes native SDK. +sentryDisableNativeStart=false diff --git a/samples/react-native/ios/sentryreactnativesample.xcodeproj/xcshareddata/xcschemes/sentryreactnativesample.xcscheme b/samples/react-native/ios/sentryreactnativesample.xcodeproj/xcshareddata/xcschemes/sentryreactnativesample.xcscheme index 15d942042b..61b12d2c2c 100644 --- a/samples/react-native/ios/sentryreactnativesample.xcodeproj/xcshareddata/xcschemes/sentryreactnativesample.xcscheme +++ b/samples/react-native/ios/sentryreactnativesample.xcodeproj/xcshareddata/xcschemes/sentryreactnativesample.xcscheme @@ -60,6 +60,12 @@ ReferencedContainer = "container:sentryreactnativesample.xcodeproj"> + + + + * arguments = [[NSProcessInfo processInfo] arguments]; + return ![arguments containsObject:@"--sentry-disable-native-start"]; +} + @end diff --git a/samples/react-native/package.json b/samples/react-native/package.json index 143a4b6c7f..2cdee52a1d 100644 --- a/samples/react-native/package.json +++ b/samples/react-native/package.json @@ -29,7 +29,9 @@ "delay": "^6.0.0", "react": "18.3.1", "react-native": "0.76.3", + "react-native-build-config": "^0.3.2", "react-native-gesture-handler": "^2.21.1", + "react-native-launch-arguments": "^4.0.4", "react-native-reanimated": "3.16.1", "react-native-safe-area-context": "4.14.0", "react-native-screens": "4.1.0", diff --git a/samples/react-native/src/App.tsx b/samples/react-native/src/App.tsx index f6d1063736..d84f1b9470 100644 --- a/samples/react-native/src/App.tsx +++ b/samples/react-native/src/App.tsx @@ -31,12 +31,16 @@ import GesturesTracingScreen from './Screens/GesturesTracingScreen'; import { LogBox, Platform, StyleSheet, View } from 'react-native'; import Ionicons from 'react-native-vector-icons/Ionicons'; import PlaygroundScreen from './Screens/PlaygroundScreen'; -import { logWithoutTracing } from './utils'; +import { clearSentryOptionsFromFile, logWithoutTracing, shouldUseAutoStart } from './utils'; import { ErrorEvent } from '@sentry/core'; import HeavyNavigationScreen from './Screens/HeavyNavigationScreen'; import WebviewScreen from './Screens/WebviewScreen'; import { isTurboModuleEnabled } from '@sentry/react-native/dist/js/utils/environment'; +if (shouldUseAutoStart()) { + clearSentryOptionsFromFile(); +} + if (typeof setImmediate === 'undefined') { require('setimmediate'); } @@ -130,7 +134,7 @@ Sentry.init({ spotlight: true, // This should be disabled when manually initializing the native SDK // Note that options from JS are not passed to the native SDKs when initialized manually - autoInitializeNativeSdk: true, + // autoInitializeNativeSdk: true, }); const Stack = isMobileOs diff --git a/samples/react-native/src/utils.ts b/samples/react-native/src/utils.ts index 8681333e30..33ccfdf084 100644 --- a/samples/react-native/src/utils.ts +++ b/samples/react-native/src/utils.ts @@ -1,3 +1,7 @@ +import { LaunchArguments } from 'react-native-launch-arguments'; +import BuildConfig from 'react-native-build-config'; +import { Platform } from 'react-native'; + export function logWithoutTracing(...args: unknown[]) { if ('__sentry_original__' in console.log) { console.log.__sentry_original__(...args); @@ -5,3 +9,29 @@ export function logWithoutTracing(...args: unknown[]) { console.log(...args); } } + +export function shouldUseAutoStart(): boolean { + if (Platform.OS === 'android') { + return !!( + BuildConfig as { + SENTRY_DISABLE_NATIVE_START?: boolean; + } + ).SENTRY_DISABLE_NATIVE_START; + } else if (Platform.OS === 'ios') { + const args = LaunchArguments.value<{ + sentrydisablenativestart?: boolean; + }>(); + return !!args.sentrydisablenativestart; + } else { + return false; + } +} + +export function clearSentryOptionsFromFile() { + ( + globalThis as { + __SENTRY_OPTIONS__?: Record; + } + ).__SENTRY_OPTIONS__ = undefined; + logWithoutTracing('Sentry options from file cleared.'); +} diff --git a/yarn.lock b/yarn.lock index 64b67a8bb0..ddc9c22aa9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -22463,6 +22463,13 @@ __metadata: languageName: node linkType: hard +"react-native-build-config@npm:^0.3.2": + version: 0.3.2 + resolution: "react-native-build-config@npm:0.3.2" + checksum: d2095580be7e6662c968bce5d64cc6524b7f34380999756fb9d4ad24a34e3566b203f93e9bc63bd1e0213d28e1e76dae0c0f39b0b65c55528a876e96dd6c5810 + languageName: node + linkType: hard + "react-native-codegen@npm:^0.70.7": version: 0.70.7 resolution: "react-native-codegen@npm:0.70.7" @@ -22561,6 +22568,16 @@ __metadata: languageName: node linkType: hard +"react-native-launch-arguments@npm:^4.0.4": + version: 4.0.4 + resolution: "react-native-launch-arguments@npm:4.0.4" + peerDependencies: + react: ">=16.8.1" + react-native: ">=0.60.0-rc.0 <1.0.x" + checksum: 7346af606cedc35c58bdccabd88a8ef9b2b55138accf490fe8291c6d7110679f9125af072eaaf896909554cd54be20e863c987d1ce91c39a2f401a999c7fde9f + languageName: node + linkType: hard + "react-native-macos@npm:0.73.34": version: 0.73.34 resolution: "react-native-macos@npm:0.73.34" @@ -24283,7 +24300,9 @@ __metadata: prettier: 2.8.8 react: 18.3.1 react-native: 0.76.3 + react-native-build-config: ^0.3.2 react-native-gesture-handler: ^2.21.1 + react-native-launch-arguments: ^4.0.4 react-native-reanimated: 3.16.1 react-native-safe-area-context: 4.14.0 react-native-screens: 4.1.0 From 6b08b9a7acde82841e76290744e2d6bae2053677 Mon Sep 17 00:00:00 2001 From: Krystof Woldrich <31292499+krystofwoldrich@users.noreply.github.com> Date: Thu, 13 Feb 2025 16:00:13 +0100 Subject: [PATCH 12/32] chore(sample-rn): Remove duplicate init options from code (#4532) --- .../sentryreactnativesample/AppDelegate.mm | 7 +++-- samples/react-native/sentry.options.json | 2 -- samples/react-native/src/App.tsx | 29 ------------------- samples/react-native/src/dsn.ts | 8 ----- 4 files changed, 4 insertions(+), 42 deletions(-) delete mode 100644 samples/react-native/src/dsn.ts diff --git a/samples/react-native/ios/sentryreactnativesample/AppDelegate.mm b/samples/react-native/ios/sentryreactnativesample/AppDelegate.mm index fa061be54c..08e8f79765 100644 --- a/samples/react-native/ios/sentryreactnativesample/AppDelegate.mm +++ b/samples/react-native/ios/sentryreactnativesample/AppDelegate.mm @@ -73,9 +73,10 @@ - (BOOL)concurrentRootEnabled return nullptr; } -- (BOOL) shouldStartSentry { - NSArray* arguments = [[NSProcessInfo processInfo] arguments]; - return ![arguments containsObject:@"--sentry-disable-native-start"]; +- (BOOL)shouldStartSentry +{ + NSArray *arguments = [[NSProcessInfo processInfo] arguments]; + return ![arguments containsObject:@"--sentry-disable-native-start"]; } @end diff --git a/samples/react-native/sentry.options.json b/samples/react-native/sentry.options.json index f6465b7923..53ae525bc0 100644 --- a/samples/react-native/sentry.options.json +++ b/samples/react-native/sentry.options.json @@ -11,8 +11,6 @@ "attachScreenshot": true, "attachViewHierarchy": true, "enableCaptureFailedRequests": true, - "_release": "myapp@1.2.3+1", - "_dist": 1, "profilesSampleRate": 1.0, "replaysSessionSampleRate": 1.0, "replaysOnErrorSampleRate": 1.0, diff --git a/samples/react-native/src/App.tsx b/samples/react-native/src/App.tsx index d84f1b9470..02b4fd2827 100644 --- a/samples/react-native/src/App.tsx +++ b/samples/react-native/src/App.tsx @@ -17,7 +17,6 @@ import Animated, { // Import the Sentry React Native SDK import * as Sentry from '@sentry/react-native'; -import { SENTRY_INTERNAL_DSN } from './dsn'; import ErrorsScreen from './Screens/ErrorsScreen'; import PerformanceScreen from './Screens/PerformanceScreen'; import TrackerScreen from './Screens/TrackerScreen'; @@ -55,10 +54,6 @@ const reactNavigationIntegration = Sentry.reactNavigationIntegration({ }); Sentry.init({ - // Replace the example DSN below with your own DSN: - dsn: SENTRY_INTERNAL_DSN, - debug: true, - environment: 'dev', beforeSend: (event: ErrorEvent) => { logWithoutTracing('Event beforeSend:', event.event_id); return event; @@ -74,7 +69,6 @@ Sentry.init({ didCallNativeInit, ); }, - enableUserInteractionTracing: true, integrations(integrations) { integrations.push( reactNavigationIntegration, @@ -110,30 +104,7 @@ Sentry.init({ ); return integrations.filter(i => i.name !== 'Dedupe'); }, - enableAutoSessionTracking: true, - // For testing, session close when 5 seconds (instead of the default 30) in the background. - sessionTrackingIntervalMillis: 30000, - // This will capture ALL TRACES and likely use up all your quota - enableTracing: true, - tracesSampleRate: 1.0, tracePropagationTargets: ['localhost', /^\//, /^https:\/\//, /^http:\/\//], - attachStacktrace: true, - // Attach screenshots to events. - attachScreenshot: true, - // Attach view hierarchy to events. - attachViewHierarchy: true, - // Enables capture failed requests in JS and native. - enableCaptureFailedRequests: true, - // Sets the `release` and `dist` on Sentry events. Make sure this matches EXACTLY with the values on your sourcemaps - // otherwise they will not work. - // release: 'myapp@1.2.3+1', - // dist: `1`, - profilesSampleRate: 1.0, - replaysSessionSampleRate: 1.0, - replaysOnErrorSampleRate: 1.0, - spotlight: true, - // This should be disabled when manually initializing the native SDK - // Note that options from JS are not passed to the native SDKs when initialized manually // autoInitializeNativeSdk: true, }); diff --git a/samples/react-native/src/dsn.ts b/samples/react-native/src/dsn.ts deleted file mode 100644 index 345276a627..0000000000 --- a/samples/react-native/src/dsn.ts +++ /dev/null @@ -1,8 +0,0 @@ -import * as Sentry from '@sentry/react-native'; - -export const SENTRY_INTERNAL_DSN = - 'https://1df17bd4e543fdb31351dee1768bb679@o447951.ingest.sentry.io/5428561'; - -export const getCurrentDsn = () => { - return Sentry.getCurrentHub().getClient()?.getOptions().dsn; -}; From a7ffa1fdde67e7d5cc259053bb0cb688f25eea83 Mon Sep 17 00:00:00 2001 From: Krystof Woldrich <31292499+krystofwoldrich@users.noreply.github.com> Date: Thu, 13 Feb 2025 16:01:16 +0100 Subject: [PATCH 13/32] chore(sample-rn): Always use fhe file option (including auto init) (#4533) --- samples/react-native/src/App.tsx | 8 ++------ samples/react-native/src/utils.ts | 9 --------- 2 files changed, 2 insertions(+), 15 deletions(-) diff --git a/samples/react-native/src/App.tsx b/samples/react-native/src/App.tsx index 02b4fd2827..23c69e6907 100644 --- a/samples/react-native/src/App.tsx +++ b/samples/react-native/src/App.tsx @@ -30,16 +30,12 @@ import GesturesTracingScreen from './Screens/GesturesTracingScreen'; import { LogBox, Platform, StyleSheet, View } from 'react-native'; import Ionicons from 'react-native-vector-icons/Ionicons'; import PlaygroundScreen from './Screens/PlaygroundScreen'; -import { clearSentryOptionsFromFile, logWithoutTracing, shouldUseAutoStart } from './utils'; +import { logWithoutTracing, shouldUseAutoStart } from './utils'; import { ErrorEvent } from '@sentry/core'; import HeavyNavigationScreen from './Screens/HeavyNavigationScreen'; import WebviewScreen from './Screens/WebviewScreen'; import { isTurboModuleEnabled } from '@sentry/react-native/dist/js/utils/environment'; -if (shouldUseAutoStart()) { - clearSentryOptionsFromFile(); -} - if (typeof setImmediate === 'undefined') { require('setimmediate'); } @@ -105,7 +101,7 @@ Sentry.init({ return integrations.filter(i => i.name !== 'Dedupe'); }, tracePropagationTargets: ['localhost', /^\//, /^https:\/\//, /^http:\/\//], - // autoInitializeNativeSdk: true, + autoInitializeNativeSdk: shouldUseAutoStart(), }); const Stack = isMobileOs diff --git a/samples/react-native/src/utils.ts b/samples/react-native/src/utils.ts index 33ccfdf084..437787dcc3 100644 --- a/samples/react-native/src/utils.ts +++ b/samples/react-native/src/utils.ts @@ -26,12 +26,3 @@ export function shouldUseAutoStart(): boolean { return false; } } - -export function clearSentryOptionsFromFile() { - ( - globalThis as { - __SENTRY_OPTIONS__?: Record; - } - ).__SENTRY_OPTIONS__ = undefined; - logWithoutTracing('Sentry options from file cleared.'); -} From 28cf7b4377cfbb4510be19086ecad3958a2bbbc8 Mon Sep 17 00:00:00 2001 From: Krystof Woldrich <31292499+krystofwoldrich@users.noreply.github.com> Date: Fri, 14 Feb 2025 16:27:48 +0100 Subject: [PATCH 14/32] internal(sample-rn): Add Detox for integration/e2e tests of the rn sample (#4535) --- .github/workflows/sample-application.yml | 172 +++++- samples/react-native/.detoxrc.js | 119 ++++ samples/react-native/android/app/build.gradle | 8 + .../android/app/proguard-rules.pro | 4 + .../sentry/reactnative/sample/DetoxTest.java | 28 + .../android/app/src/main/AndroidManifest.xml | 3 +- .../main/res/xml/network_security_config.xml | 7 + samples/react-native/android/build.gradle | 8 + samples/react-native/e2e/jest.config.js | 13 + samples/react-native/e2e/starter.test.ts | 12 + samples/react-native/jest.config.js | 7 + samples/react-native/package.json | 4 + samples/react-native/src/App.tsx | 7 + yarn.lock | 546 +++++++++++++++++- 14 files changed, 919 insertions(+), 19 deletions(-) create mode 100644 samples/react-native/.detoxrc.js create mode 100644 samples/react-native/android/app/src/androidTest/java/io/sentry/reactnative/sample/DetoxTest.java create mode 100644 samples/react-native/android/app/src/main/res/xml/network_security_config.xml create mode 100644 samples/react-native/e2e/jest.config.js create mode 100644 samples/react-native/e2e/starter.test.ts create mode 100644 samples/react-native/jest.config.js diff --git a/.github/workflows/sample-application.yml b/.github/workflows/sample-application.yml index 3ec82a6e31..e4e9a8f8b3 100644 --- a/.github/workflows/sample-application.yml +++ b/.github/workflows/sample-application.yml @@ -14,6 +14,12 @@ concurrency: env: SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} RN_SENTRY_POD_NAME: RNSentry + IOS_APP_ARCHIVE_PATH: sentry-react-native-sample.app.zip + ANDROID_APP_ARCHIVE_PATH: sentry-react-native-sample.apk.zip + REACT_NATIVE_SAMPLE_PATH: samples/react-native + IOS_DEVICE: 'iPhone 16' + IOS_VERSION: '18.1' + ANDROID_API_LEVEL: '30' jobs: diff_check: @@ -66,7 +72,7 @@ jobs: - uses: ruby/setup-ruby@v1 if: ${{ matrix.platform == 'ios' || matrix.platform == 'macos' }} with: - working-directory: ${{ matrix.platform == 'ios' && ' samples/react-native' || ' samples/react-native-macos' }} + working-directory: ${{ matrix.platform == 'ios' && env.REACT_NATIVE_SAMPLE_PATH || ' samples/react-native-macos' }} ruby-version: '3.3.0' # based on what is used in the sample bundler-cache: true # runs 'bundle install' and caches installed gems automatically cache-version: 1 # cache the installed gems @@ -106,7 +112,7 @@ jobs: - name: Build Android App if: ${{ matrix.platform == 'android' }} - working-directory: samples/react-native/android + working-directory: ${{ env.REACT_NATIVE_SAMPLE_PATH }}/android run: | if [[ ${{ matrix.rn-architecture }} == 'new' ]]; then perl -i -pe's/newArchEnabled=false/newArchEnabled=true/g' gradle.properties @@ -119,11 +125,14 @@ jobs: fi [[ "${{ matrix.build-type }}" == "production" ]] && CONFIG='Release' || CONFIG='Debug' echo "Building $CONFIG" - ./gradlew ":app:assemble$CONFIG" -PreactNativeArchitectures=x86 + [[ "${{ matrix.build-type }}" == "production" ]] && TEST_TYPE='release' || TEST_TYPE='debug' + echo "Building $TEST_TYPE" + + ./gradlew ":app:assemble$CONFIG" app:assembleAndroidTest -DtestBuildType=$TEST_TYPE -PreactNativeArchitectures=x86 - name: Build iOS App if: ${{ matrix.platform == 'ios' }} - working-directory: samples/react-native/ios + working-directory: ${{ env.REACT_NATIVE_SAMPLE_PATH }}/ios run: | [[ "${{ matrix.build-type }}" == "production" ]] && CONFIG='Release' || CONFIG='Debug' echo "Building $CONFIG" @@ -160,9 +169,162 @@ jobs: | tee xcodebuild.log \ | xcbeautify --quieter --is-ci --disable-colored-output + - name: Archive iOS App + if: ${{ matrix.platform == 'ios' && matrix.rn-architecture == 'new' && matrix.build-type == 'production' && matrix.ios-use-frameworks == 'no-frameworks' }} + run: | + cd ${{ env.REACT_NATIVE_SAMPLE_PATH }}/ios/DerivedData/Build/Products/Release-iphonesimulator + zip -r \ + ${{ github.workspace }}/${{ env.IOS_APP_ARCHIVE_PATH }} \ + sentryreactnativesample.app + + - name: Archive Android App + if: ${{ matrix.platform == 'android' && matrix.rn-architecture == 'new' && matrix.build-type == 'production' }} + run: | + mv ${{ env.REACT_NATIVE_SAMPLE_PATH }}/android/app/build/outputs/apk/release/app-release.apk app.apk + mv ${{ env.REACT_NATIVE_SAMPLE_PATH }}/android/app/build/outputs/apk/androidTest/release/app-release-androidTest.apk app-androidTest.apk + zip -j \ + ${{ env.ANDROID_APP_ARCHIVE_PATH }} \ + app.apk \ + app-androidTest.apk + + - name: Upload iOS APP + if: ${{ matrix.platform == 'ios' && matrix.rn-architecture == 'new' && matrix.build-type == 'production' && matrix.ios-use-frameworks == 'no-frameworks' }} + uses: actions/upload-artifact@v4 + with: + name: sample-rn-${{ matrix.rn-architecture }}-${{ matrix.build-type }}-${{ matrix.ios-use-frameworks}}-${{ matrix.platform }} + path: ${{ env.IOS_APP_ARCHIVE_PATH }} + retention-days: 1 + + - name: Upload Android APK + if: ${{ matrix.platform == 'android' && matrix.rn-architecture == 'new' && matrix.build-type == 'production' }} + uses: actions/upload-artifact@v4 + with: + name: sample-rn-${{ matrix.rn-architecture }}-${{ matrix.build-type }}-${{ matrix.platform }} + path: ${{ env.ANDROID_APP_ARCHIVE_PATH }} + retention-days: 1 + - name: Upload logs if: ${{ always() }} uses: actions/upload-artifact@v4 with: name: build-sample-${{ matrix.rn-architecture }}-${{ matrix.platform }}-${{ matrix.build-type }}-${{ matrix.ios-use-frameworks}}-logs - path: samples/react-native/${{ matrix.platform }}/*.log + path: ${{ env.REACT_NATIVE_SAMPLE_PATH }}/${{ matrix.platform }}/*.log + + test: + name: Test ${{ matrix.platform }} ${{ matrix.build-type }} + runs-on: ${{ matrix.runs-on }} + needs: [diff_check, build] + if: ${{ needs.diff_check.outputs.skip_ci != 'true' }} + strategy: + # we want that the matrix keeps running, default is to cancel them if it fails. + fail-fast: false + matrix: + include: + - platform: ios + runs-on: macos-15 + rn-architecture: 'new' + ios-use-frameworks: 'no-frameworks' + build-type: 'production' + + - platform: android + runs-on: ubuntu-latest + rn-architecture: 'new' + build-type: 'production' + + steps: + - uses: actions/checkout@v4 + + - name: Download iOS App Archive + if: ${{ matrix.platform == 'ios' }} + uses: actions/download-artifact@v4 + with: + name: sample-rn-${{ matrix.rn-architecture }}-${{ matrix.build-type }}-${{ matrix.ios-use-frameworks}}-${{ matrix.platform }} + path: ${{ env.REACT_NATIVE_SAMPLE_PATH }} + + - name: Download Android APK + if: ${{ matrix.platform == 'android' }} + uses: actions/download-artifact@v4 + with: + name: sample-rn-${{ matrix.rn-architecture }}-${{ matrix.build-type }}-${{ matrix.platform }} + path: ${{ env.REACT_NATIVE_SAMPLE_PATH }} + + - name: Unzip iOS App Archive + if: ${{ matrix.platform == 'ios' }} + working-directory: ${{ env.REACT_NATIVE_SAMPLE_PATH }} + run: unzip ${{ env.IOS_APP_ARCHIVE_PATH }} + + - name: Unzip Android APK + if: ${{ matrix.platform == 'android' }} + working-directory: ${{ env.REACT_NATIVE_SAMPLE_PATH }} + run: unzip ${{ env.ANDROID_APP_ARCHIVE_PATH }} + + - name: Enable Corepack + run: | + npm install -g corepack@0.29.4 + corepack enable + - uses: actions/setup-node@v4 + with: + node-version: 18 + cache: 'yarn' + cache-dependency-path: yarn.lock + + - name: Install JS Dependencies + run: yarn install + + - name: Install Detox + run: npm install -g detox-cli@20.0.0 + + - name: Install Apple Simulator Utilities + if: ${{ matrix.platform == 'ios' }} + run: | + brew tap wix/brew + brew install applesimutils + + - name: Setup KVM + if: ${{ matrix.platform == 'android' }} + shell: bash + run: | + # check if virtualization is supported... + sudo apt install -y --no-install-recommends cpu-checker coreutils && echo "CPUs=$(nproc --all)" && kvm-ok + # allow access to KVM to run the emulator + echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' \ + | sudo tee /etc/udev/rules.d/99-kvm4all.rules + sudo udevadm control --reload-rules + sudo udevadm trigger --name-match=kvm + + - uses: futureware-tech/simulator-action@dab10d813144ef59b48d401cd95da151222ef8cd # pin@v4 + if: ${{ matrix.platform == 'ios' }} + with: + # the same envs are used by Detox ci.sim configuration + model: ${{ env.IOS_DEVICE }} + os_version: ${{ env.IOS_VERSION }} + + - name: Run Detox iOS Tests + if: ${{ matrix.platform == 'ios' }} + working-directory: ${{ env.REACT_NATIVE_SAMPLE_PATH }} + run: detox test --configuration ci.sim + + - name: Run tests on Android + if: ${{ matrix.platform == 'android' }} + env: + # used by Detox ci.android configuration + ANDROID_AVD_NAME: 'test' # test is default reactivecircus/android-emulator-runner name + uses: reactivecircus/android-emulator-runner@62dbb605bba737720e10b196cb4220d374026a6d # pin@v2.33.0 + with: + api-level: ${{ env.ANDROID_API_LEVEL }} + force-avd-creation: false + disable-animations: true + disable-spellchecker: true + target: 'aosp_atd' + channel: canary # Necessary for ATDs + emulator-options: > + -no-window + -no-snapshot-save + -gpu swiftshader_indirect + -noaudio + -no-boot-anim + -camera-back none + -camera-front none + -timezone US/Pacific + working-directory: ${{ env.REACT_NATIVE_SAMPLE_PATH }} + script: detox test --configuration ci.android diff --git a/samples/react-native/.detoxrc.js b/samples/react-native/.detoxrc.js new file mode 100644 index 0000000000..fd29191f91 --- /dev/null +++ b/samples/react-native/.detoxrc.js @@ -0,0 +1,119 @@ +const process = require('process'); + +/** @type {Detox.DetoxConfig} */ +module.exports = { + testRunner: { + args: { + $0: 'jest', + config: 'e2e/jest.config.js', + }, + jest: { + setupTimeout: 120000, + }, + }, + apps: { + 'ios.debug': { + type: 'ios.app', + binaryPath: + 'ios/build/Build/Products/Debug-iphonesimulator/sentryreactnativesample.app', + build: + 'xcodebuild -workspace ios/sentryreactnativesample.xcworkspace -scheme sentryreactnativesample -configuration Debug -sdk iphonesimulator -derivedDataPath ios/build', + }, + 'ios.release': { + type: 'ios.app', + binaryPath: + 'ios/build/Build/Products/Release-iphonesimulator/sentryreactnativesample.app', + build: + 'xcodebuild -workspace ios/sentryreactnativesample.xcworkspace -scheme sentryreactnativesample -configuration Release -sdk iphonesimulator -derivedDataPath ios/build', + }, + 'android.debug': { + type: 'android.apk', + binaryPath: 'android/app/build/outputs/apk/debug/app-debug.apk', + build: + 'cd android && ./gradlew app:assembleDebug app:assembleAndroidTest -DtestBuildType=debug', + reversePorts: [8081], + }, + 'android.release': { + type: 'android.apk', + binaryPath: 'android/app/build/outputs/apk/release/app-release.apk', + build: + 'cd android && ./gradlew app:assembleRelease app:assembleAndroidTest -DtestBuildType=release', + }, + 'ci.android': { + type: 'android.apk', + binaryPath: 'app.apk', + testBinaryPath: 'app-androidTest.apk', + }, + 'ci.ios': { + type: 'ios.app', + binaryPath: 'sentryreactnativesample.app', + }, + }, + devices: { + simulator: { + type: 'ios.simulator', + device: { + type: 'iPhone 16', + }, + }, + attached: { + type: 'android.attached', + device: { + adbName: '.*', + }, + }, + emulator: { + type: 'android.emulator', + device: { + avdName: 'Pixel_9_API_35', + }, + }, + 'ci.emulator': { + type: 'android.emulator', + device: { + avdName: process.env.ANDROID_AVD_NAME, + }, + }, + 'ci.simulator': { + type: 'ios.simulator', + device: { + type: process.env.IOS_DEVICE, + os: process.env.IOS_VERSION, + }, + }, + }, + configurations: { + 'ios.sim.debug': { + device: 'simulator', + app: 'ios.debug', + }, + 'ios.sim.release': { + device: 'simulator', + app: 'ios.release', + }, + 'android.att.debug': { + device: 'attached', + app: 'android.debug', + }, + 'android.att.release': { + device: 'attached', + app: 'android.release', + }, + 'android.emu.debug': { + device: 'emulator', + app: 'android.debug', + }, + 'android.emu.release': { + device: 'emulator', + app: 'android.release', + }, + 'ci.android': { + device: 'ci.emulator', + app: 'ci.android', + }, + 'ci.sim': { + device: 'ci.simulator', + app: 'ci.ios', + }, + }, +}; diff --git a/samples/react-native/android/app/build.gradle b/samples/react-native/android/app/build.gradle index 6c0d4a7ca1..1652764dfe 100644 --- a/samples/react-native/android/app/build.gradle +++ b/samples/react-native/android/app/build.gradle @@ -139,6 +139,10 @@ android { versionCode 39 versionName "6.7.0-alpha.0" buildConfigField "boolean", "SENTRY_DISABLE_NATIVE_START", System.getenv('SENTRY_DISABLE_NATIVE_START') ?: String.valueOf(sentryDisableNativeStart) + + // Detox + testBuildType System.getProperty('testBuildType', 'debug') + testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' } signingConfigs { @@ -193,11 +197,15 @@ android { signingConfig signingConfigs.debug minifyEnabled enableProguardInReleaseBuilds proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro" + proguardFile "${rootProject.projectDir}/../node_modules/detox/android/detox/proguard-rules-app.pro" } } } dependencies { + androidTestImplementation('com.wix:detox:+') + implementation 'androidx.appcompat:appcompat:1.7.0' + // The version of react-native is set by the React Native Gradle Plugin implementation("com.facebook.react:react-android") diff --git a/samples/react-native/android/app/proguard-rules.pro b/samples/react-native/android/app/proguard-rules.pro index 11b025724a..f4ada6b5a1 100644 --- a/samples/react-native/android/app/proguard-rules.pro +++ b/samples/react-native/android/app/proguard-rules.pro @@ -8,3 +8,7 @@ # http://developer.android.com/guide/developing/tools/proguard.html # Add any project specific keep options here: + +# Detox Release tests were failing on missing kotlin.Result +# It should be covered by node_modules/detox/android/detox/proguard-rules-app.pro but it seems missing +-keep class kotlin.** { *; } diff --git a/samples/react-native/android/app/src/androidTest/java/io/sentry/reactnative/sample/DetoxTest.java b/samples/react-native/android/app/src/androidTest/java/io/sentry/reactnative/sample/DetoxTest.java new file mode 100644 index 0000000000..28b9b28d1c --- /dev/null +++ b/samples/react-native/android/app/src/androidTest/java/io/sentry/reactnative/sample/DetoxTest.java @@ -0,0 +1,28 @@ +package io.sentry.reactnative.sample; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.LargeTest; +import androidx.test.rule.ActivityTestRule; +import com.wix.detox.Detox; +import com.wix.detox.config.DetoxConfig; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +@LargeTest +public class DetoxTest { + @Rule + public ActivityTestRule mActivityRule = + new ActivityTestRule<>(MainActivity.class, false, false); + + @Test + public void runDetoxTests() { + DetoxConfig detoxConfig = new DetoxConfig(); + detoxConfig.idlePolicyConfig.masterTimeoutSec = 90; + detoxConfig.idlePolicyConfig.idleResourceTimeoutSec = 60; + detoxConfig.rnContextLoadTimeoutSec = (BuildConfig.DEBUG ? 180 : 60); + + Detox.runTests(mActivityRule, detoxConfig); + } +} diff --git a/samples/react-native/android/app/src/main/AndroidManifest.xml b/samples/react-native/android/app/src/main/AndroidManifest.xml index e1892528b8..095bdca459 100644 --- a/samples/react-native/android/app/src/main/AndroidManifest.xml +++ b/samples/react-native/android/app/src/main/AndroidManifest.xml @@ -9,7 +9,8 @@ android:roundIcon="@mipmap/ic_launcher_round" android:allowBackup="false" android:theme="@style/AppTheme" - android:supportsRtl="true"> + android:supportsRtl="true" + android:networkSecurityConfig="@xml/network_security_config"> + + + 10.0.2.2 + localhost + + diff --git a/samples/react-native/android/build.gradle b/samples/react-native/android/build.gradle index c6979c3c43..c2b68ea82b 100644 --- a/samples/react-native/android/build.gradle +++ b/samples/react-native/android/build.gradle @@ -20,4 +20,12 @@ buildscript { } } +allprojects { + repositories { + maven { + url("$rootDir/../node_modules/detox/Detox-android") + } + } +} + apply plugin: "com.facebook.react.rootproject" diff --git a/samples/react-native/e2e/jest.config.js b/samples/react-native/e2e/jest.config.js new file mode 100644 index 0000000000..b52d19a014 --- /dev/null +++ b/samples/react-native/e2e/jest.config.js @@ -0,0 +1,13 @@ +/** @type {import('@jest/types').Config.InitialOptions} */ +module.exports = { + preset: 'ts-jest', + rootDir: '..', + testMatch: ['/e2e/**/*.test.ts'], + testTimeout: 120000, + maxWorkers: 1, + globalSetup: 'detox/runners/jest/globalSetup', + globalTeardown: 'detox/runners/jest/globalTeardown', + reporters: ['detox/runners/jest/reporter'], + testEnvironment: 'detox/runners/jest/testEnvironment', + verbose: true, +}; diff --git a/samples/react-native/e2e/starter.test.ts b/samples/react-native/e2e/starter.test.ts new file mode 100644 index 0000000000..b88c9d0882 --- /dev/null +++ b/samples/react-native/e2e/starter.test.ts @@ -0,0 +1,12 @@ +import { describe, it, beforeAll } from '@jest/globals'; +import { device, expect } from 'detox'; + +describe('Shows HomeScreen', () => { + beforeAll(async () => { + await device.launchApp(); + }); + + it('Shows Bottom Tab Bar', async () => { + await expect(element(by.text('Performance'))).toBeVisible(); + }); +}); diff --git a/samples/react-native/jest.config.js b/samples/react-native/jest.config.js new file mode 100644 index 0000000000..27803eeafc --- /dev/null +++ b/samples/react-native/jest.config.js @@ -0,0 +1,7 @@ +/** @type {import('@jest/types').Config.InitialOptions} */ +module.exports = { + testMatch: [ + '/__tests__/**/*-test.ts', + '/__tests__/**/*-test.tsx', + ], +}; diff --git a/samples/react-native/package.json b/samples/react-native/package.json index 2cdee52a1d..128f329fa9 100644 --- a/samples/react-native/package.json +++ b/samples/react-native/package.json @@ -53,6 +53,8 @@ "@react-native/metro-config": "0.76.3", "@react-native/typescript-config": "0.76.3", "@sentry/babel-plugin-component-annotate": "^2.18.0", + "@types/jest": "^29.5.14", + "@types/node": "^22.13.1", "@types/react": "^18.2.65", "@types/react-native-vector-icons": "^6.4.18", "@types/react-test-renderer": "^18.0.0", @@ -60,12 +62,14 @@ "@typescript-eslint/parser": "^7.18.0", "babel-jest": "^29.2.1", "babel-plugin-module-resolver": "^5.0.0", + "detox": "^20.33.0", "eslint": "^8.19.0", "jest": "^29.6.3", "patch-package": "^8.0.0", "prettier": "2.8.8", "react-test-renderer": "18.3.1", "sentry-react-native-samples-utils": "workspace:^", + "ts-jest": "^29.2.5", "typescript": "5.0.4" }, "engines": { diff --git a/samples/react-native/src/App.tsx b/samples/react-native/src/App.tsx index 23c69e6907..62d09942f2 100644 --- a/samples/react-native/src/App.tsx +++ b/samples/react-native/src/App.tsx @@ -36,6 +36,9 @@ import HeavyNavigationScreen from './Screens/HeavyNavigationScreen'; import WebviewScreen from './Screens/WebviewScreen'; import { isTurboModuleEnabled } from '@sentry/react-native/dist/js/utils/environment'; +/* false by default to avoid issues in e2e tests waiting for the animation end */ +const RUNNING_INDICATOR = false; + if (typeof setImmediate === 'undefined') { require('setimmediate'); } @@ -255,6 +258,10 @@ function RunningIndicator() { return null; } + if (!RUNNING_INDICATOR) { + return null; + } + return ; } diff --git a/yarn.lock b/yarn.lock index ddc9c22aa9..ca53bb1707 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4401,6 +4401,13 @@ __metadata: languageName: node linkType: hard +"@flatten-js/interval-tree@npm:^1.1.2": + version: 1.1.3 + resolution: "@flatten-js/interval-tree@npm:1.1.3" + checksum: 8ff9dc4062b20bd1bcff735b6734d93489409af59f87db799abe534d745dd8cd9293a15e720a999058bc97c66b88f1cdb14f6142d122723ffe52032c5ca2efde + languageName: node + linkType: hard + "@hapi/hoek@npm:^9.0.0, @hapi/hoek@npm:^9.3.0": version: 9.3.0 resolution: "@hapi/hoek@npm:9.3.0" @@ -8406,6 +8413,16 @@ __metadata: languageName: node linkType: hard +"@types/jest@npm:^29.5.14": + version: 29.5.14 + resolution: "@types/jest@npm:29.5.14" + dependencies: + expect: ^29.0.0 + pretty-format: ^29.0.0 + checksum: 18dba4623f26661641d757c63da2db45e9524c9be96a29ef713c703a9a53792df9ecee9f7365a0858ddbd6440d98fe6b65ca67895ca5884b73cbc7ffc11f3838 + languageName: node + linkType: hard + "@types/jsdom@npm:^20.0.0": version: 20.0.1 resolution: "@types/jsdom@npm:20.0.1" @@ -8545,6 +8562,15 @@ __metadata: languageName: node linkType: hard +"@types/node@npm:^22.13.1": + version: 22.13.1 + resolution: "@types/node@npm:22.13.1" + dependencies: + undici-types: ~6.20.0 + checksum: a0759e4bedc3fe892c3ddef5fa9cb5251f9c5b24defc1a389438ea3b5b727c481c1a9bc94bae4ecc7426c89ad293cd66633d163da1ab14d74d358cbec9e1ce31 + languageName: node + linkType: hard + "@types/normalize-package-data@npm:^2.4.0": version: 2.4.4 resolution: "@types/normalize-package-data@npm:2.4.4" @@ -9658,7 +9684,7 @@ __metadata: languageName: node linkType: hard -"ajv@npm:^8.0.0, ajv@npm:^8.0.1, ajv@npm:^8.9.0": +"ajv@npm:^8.0.0, ajv@npm:^8.0.1, ajv@npm:^8.6.3, ajv@npm:^8.9.0": version: 8.17.1 resolution: "ajv@npm:8.17.1" dependencies: @@ -10940,7 +10966,7 @@ __metadata: languageName: node linkType: hard -"bluebird@npm:3.7.2, bluebird@npm:^3.1.1, bluebird@npm:^3.4.7, bluebird@npm:^3.5.1, bluebird@npm:^3.5.5, bluebird@npm:^3.7.2": +"bluebird@npm:3.7.2, bluebird@npm:^3.1.1, bluebird@npm:^3.4.7, bluebird@npm:^3.5.1, bluebird@npm:^3.5.4, bluebird@npm:^3.5.5, bluebird@npm:^3.7.2": version: 3.7.2 resolution: "bluebird@npm:3.7.2" checksum: 869417503c722e7dc54ca46715f70e15f4d9c602a423a02c825570862d12935be59ed9c7ba34a9b31f186c017c23cac6b54e35446f8353059c101da73eac22ef @@ -11047,6 +11073,13 @@ __metadata: languageName: node linkType: hard +"browser-process-hrtime@npm:^1.0.0": + version: 1.0.0 + resolution: "browser-process-hrtime@npm:1.0.0" + checksum: e30f868cdb770b1201afb714ad1575dd86366b6e861900884665fb627109b3cc757c40067d3bfee1ff2a29c835257ea30725a8018a9afd02ac1c24b408b1e45f + languageName: node + linkType: hard + "browserslist@npm:^4.23.1, browserslist@npm:^4.23.3": version: 4.23.3 resolution: "browserslist@npm:4.23.3" @@ -11172,6 +11205,87 @@ __metadata: languageName: node linkType: hard +"bunyamin@npm:^1.5.2": + version: 1.6.3 + resolution: "bunyamin@npm:1.6.3" + dependencies: + "@flatten-js/interval-tree": ^1.1.2 + multi-sort-stream: ^1.0.4 + stream-json: ^1.7.5 + trace-event-lib: ^1.3.1 + peerDependencies: + "@types/bunyan": ^1.8.8 + bunyan: ^1.8.15 || ^2.0.0 + peerDependenciesMeta: + "@types/bunyan": + optional: true + bunyan: + optional: true + checksum: 3422db179c2f1d9581740b18de79c925e2ab25ee49ea5e66a5b66db16372d6f641927de55010c997050049d9e9569f4b720d409ffa0a573ded86aef5d49768eb + languageName: node + linkType: hard + +"bunyan-debug-stream@npm:^3.1.0": + version: 3.1.1 + resolution: "bunyan-debug-stream@npm:3.1.1" + dependencies: + chalk: ^4.1.2 + peerDependencies: + bunyan: "*" + peerDependenciesMeta: + bunyan: + optional: true + checksum: e0dd2c42de27857bd7c70b600ac30ecf7ef5efe7837c6ea2d87b98e48c7cd16a4fcce1d08439d9fc5dbff2d672b191357ea579750c9cd6379703109f5077bca4 + languageName: node + linkType: hard + +"bunyan@npm:^1.8.12": + version: 1.8.15 + resolution: "bunyan@npm:1.8.15" + dependencies: + dtrace-provider: ~0.8 + moment: ^2.19.3 + mv: ~2 + safe-json-stringify: ~1 + dependenciesMeta: + dtrace-provider: + optional: true + moment: + optional: true + mv: + optional: true + safe-json-stringify: + optional: true + bin: + bunyan: bin/bunyan + checksum: a479e0787c3a0b6565b54bd15f0b6c729d624c5aba53523e140e49e279b7a78508df93000e758bf6d02361117d6b4e6e5fc1d5ece05366fb6c4ba41bf1ac7d52 + languageName: node + linkType: hard + +"bunyan@npm:^2.0.5": + version: 2.0.5 + resolution: "bunyan@npm:2.0.5" + dependencies: + dtrace-provider: ~0.8 + exeunt: 1.1.0 + moment: ^2.19.3 + mv: ~2 + safe-json-stringify: ~1 + dependenciesMeta: + dtrace-provider: + optional: true + moment: + optional: true + mv: + optional: true + safe-json-stringify: + optional: true + bin: + bunyan: bin/bunyan + checksum: a932e883387e5bef23eee0f1f9af94e8b885da32492eaf7164dc58e3b42e5a65845068beb7ac8fbcff31511a55728c1a826bf48ba3e4edd7e220ebf0fe2ab989 + languageName: node + linkType: hard + "byte-size@npm:8.1.1": version: 8.1.1 resolution: "byte-size@npm:8.1.1" @@ -11235,6 +11349,13 @@ __metadata: languageName: node linkType: hard +"caf@npm:^15.0.1": + version: 15.0.1 + resolution: "caf@npm:15.0.1" + checksum: 832cc5d3a6053efb458ed1c1f5e5d3ebbc7710f2275f033c6362dcfd1565f15e29dbee15fa0f3301ecb5c4dbdc753c070b5a4a6d3dc8e246cb784cb26c601e8b + languageName: node + linkType: hard + "call-bind@npm:^1.0.2, call-bind@npm:^1.0.5, call-bind@npm:^1.0.6, call-bind@npm:^1.0.7": version: 1.0.7 resolution: "call-bind@npm:1.0.7" @@ -11416,6 +11537,17 @@ __metadata: languageName: node linkType: hard +"child-process-promise@npm:^2.2.0": + version: 2.2.1 + resolution: "child-process-promise@npm:2.2.1" + dependencies: + cross-spawn: ^4.0.2 + node-version: ^1.0.0 + promise-polyfill: ^6.0.1 + checksum: fb72dda7ee78099f106d57bf3d7cc3225c16c9ddfe8e364e3535a52396482ee81aecd3eff0da7131ca17b7ba9fcbb8af827da63a03f0c3262c76268696898642 + languageName: node + linkType: hard + "chokidar@npm:^3.4.0": version: 3.6.0 resolution: "chokidar@npm:3.6.0" @@ -12281,6 +12413,16 @@ __metadata: languageName: node linkType: hard +"cross-spawn@npm:^4.0.2": + version: 4.0.2 + resolution: "cross-spawn@npm:4.0.2" + dependencies: + lru-cache: ^4.0.1 + which: ^1.2.9 + checksum: 8ce57b3e11c5c798542a21ddfdc1edef33ab6fe001958b31f3340a6ff684e3334a8baad2751efa78b6200aad442cf12b939396d758b0dd5c42c9b782c28fe06e + languageName: node + linkType: hard + "cross-spawn@npm:^6.0.0": version: 6.0.6 resolution: "cross-spawn@npm:6.0.6" @@ -12584,6 +12726,13 @@ __metadata: languageName: node linkType: hard +"decamelize@npm:^4.0.0": + version: 4.0.0 + resolution: "decamelize@npm:4.0.0" + checksum: b7d09b82652c39eead4d6678bb578e3bebd848add894b76d0f6b395bc45b2d692fb88d977e7cfb93c4ed6c119b05a1347cef261174916c2e75c0a8ca57da1809 + languageName: node + linkType: hard + "decamelize@npm:^6.0.0": version: 6.0.0 resolution: "decamelize@npm:6.0.0" @@ -13047,6 +13196,64 @@ __metadata: languageName: node linkType: hard +"detox-copilot@npm:^0.0.27": + version: 0.0.27 + resolution: "detox-copilot@npm:0.0.27" + checksum: 4f01ed1f21fe3128ee50037b63085fe95ccdc9e723c6b034d53720fa325123e39d4f83d18b1ab88a11a679258b0ff734e74f0738118e260f10945fadbe205443 + languageName: node + linkType: hard + +"detox@npm:^20.33.0": + version: 20.33.0 + resolution: "detox@npm:20.33.0" + dependencies: + ajv: ^8.6.3 + bunyan: ^1.8.12 + bunyan-debug-stream: ^3.1.0 + caf: ^15.0.1 + chalk: ^4.0.0 + child-process-promise: ^2.2.0 + detox-copilot: ^0.0.27 + execa: ^5.1.1 + find-up: ^5.0.0 + fs-extra: ^11.0.0 + funpermaproxy: ^1.1.0 + glob: ^8.0.3 + ini: ^1.3.4 + jest-environment-emit: ^1.0.8 + json-cycle: ^1.3.0 + lodash: ^4.17.11 + multi-sort-stream: ^1.0.3 + multipipe: ^4.0.0 + node-ipc: 9.2.1 + proper-lockfile: ^3.0.2 + resolve-from: ^5.0.0 + sanitize-filename: ^1.6.1 + semver: ^7.0.0 + serialize-error: ^8.0.1 + shell-quote: ^1.7.2 + signal-exit: ^3.0.3 + stream-json: ^1.7.4 + strip-ansi: ^6.0.1 + telnet-client: 1.2.8 + tempfile: ^2.0.0 + trace-event-lib: ^1.3.1 + which: ^1.3.1 + ws: ^7.0.0 + yargs: ^17.0.0 + yargs-parser: ^21.0.0 + yargs-unparser: ^2.0.0 + peerDependencies: + jest: 29.x.x || 28.x.x || ^27.2.5 + peerDependenciesMeta: + jest: + optional: true + bin: + detox: local-cli/cli.js + checksum: 14a9a230f02c6e7e535e96223a9aacbef05c06c20887eac3d1f1df1aca612a1c529f94e265064df39d1c98ee700fbc2a26ebdd18affea5e60f17a527ae42d6e0 + languageName: node + linkType: hard + "devtools-protocol@npm:0.0.1232444": version: 0.0.1232444 resolution: "devtools-protocol@npm:0.0.1232444" @@ -13201,6 +13408,25 @@ __metadata: languageName: node linkType: hard +"dtrace-provider@npm:~0.8": + version: 0.8.8 + resolution: "dtrace-provider@npm:0.8.8" + dependencies: + nan: ^2.14.0 + node-gyp: latest + checksum: f2dc89df6a9c443dc9bae3b53496e0685b5da89142951d451c1ce062c75d96698ffc0b3d90f621a59a6a18578be552378ad4e08210759038910ff2080be556b9 + languageName: node + linkType: hard + +"duplexer2@npm:^0.1.2": + version: 0.1.4 + resolution: "duplexer2@npm:0.1.4" + dependencies: + readable-stream: ^2.0.2 + checksum: 744961f03c7f54313f90555ac20284a3fb7bf22fdff6538f041a86c22499560eb6eac9d30ab5768054137cb40e6b18b40f621094e0261d7d8c35a37b7a5ad241 + languageName: node + linkType: hard + "duplexer@npm:^0.1.1, duplexer@npm:~0.1.1": version: 0.1.2 resolution: "duplexer@npm:0.1.2" @@ -13215,6 +13441,13 @@ __metadata: languageName: node linkType: hard +"easy-stack@npm:^1.0.1": + version: 1.0.1 + resolution: "easy-stack@npm:1.0.1" + checksum: 161a99e497b3857b0be4ec9e1ebbe90b241ea9d84702f9881b8e5b3f6822065b8c4e33436996935103e191bffba3607de70712a792f4d406a050def48c6bc381 + languageName: node + linkType: hard + "edge-paths@npm:^3.0.5": version: 3.0.5 resolution: "edge-paths@npm:3.0.5" @@ -14209,6 +14442,13 @@ __metadata: languageName: node linkType: hard +"event-pubsub@npm:4.3.0": + version: 4.3.0 + resolution: "event-pubsub@npm:4.3.0" + checksum: 6940f57790c01a967b7c637f1c9fd000ee968a1d5894186ffb3356ffbe174c70e22e62adbbcfcee3f305482d99b6abe7613c1c27c909b07adc9127dc16c8cf73 + languageName: node + linkType: hard + "event-target-shim@npm:^5.0.0, event-target-shim@npm:^5.0.1": version: 5.0.1 resolution: "event-target-shim@npm:5.0.1" @@ -14286,6 +14526,13 @@ __metadata: languageName: node linkType: hard +"exeunt@npm:1.1.0": + version: 1.1.0 + resolution: "exeunt@npm:1.1.0" + checksum: c0054fa49d7b3abbc2acecd4c6e34c6ce3a0370f9c31d18cdf64dad6be9a6d3fb84d93be892b7d1906f3f23051b3855bde7b255129fc49605a04392f69e98ea2 + languageName: node + linkType: hard + "exit@npm:^0.1.2": version: 0.1.2 resolution: "exit@npm:0.1.2" @@ -15161,6 +15408,17 @@ __metadata: languageName: node linkType: hard +"fs-extra@npm:^11.0.0": + version: 11.3.0 + resolution: "fs-extra@npm:11.3.0" + dependencies: + graceful-fs: ^4.2.0 + jsonfile: ^6.0.1 + universalify: ^2.0.0 + checksum: f983c706e0c22b0c0747a8e9c76aed6f391ba2d76734cf2757cd84da13417b402ed68fe25bace65228856c61d36d3b41da198f1ffbf33d0b34283a2f7a62c6e9 + languageName: node + linkType: hard + "fs-extra@npm:^11.1.0, fs-extra@npm:^11.2.0": version: 11.2.0 resolution: "fs-extra@npm:11.2.0" @@ -15288,6 +15546,13 @@ __metadata: languageName: node linkType: hard +"funpermaproxy@npm:^1.1.0": + version: 1.1.0 + resolution: "funpermaproxy@npm:1.1.0" + checksum: 74cf0aafeadbd79053324f1fb981c1a4358618722ad01c65bd1466b42498fd07acb7749ab9224b25fc8e81c2e1283b92ceee61dded265bd7527b225351db998b + languageName: node + linkType: hard + "gauge@npm:^5.0.0": version: 5.0.2 resolution: "gauge@npm:5.0.2" @@ -15652,7 +15917,7 @@ __metadata: languageName: node linkType: hard -"glob@npm:^8.1.0": +"glob@npm:^8.0.3, glob@npm:^8.1.0": version: 8.1.0 resolution: "glob@npm:8.1.0" dependencies: @@ -16782,6 +17047,13 @@ __metadata: languageName: node linkType: hard +"is-plain-obj@npm:^2.1.0": + version: 2.1.0 + resolution: "is-plain-obj@npm:2.1.0" + checksum: cec9100678b0a9fe0248a81743041ed990c2d4c99f893d935545cfbc42876cbe86d207f3b895700c690ad2fa520e568c44afc1605044b535a7820c1d40e38daa + languageName: node + linkType: hard + "is-plain-obj@npm:^4.1.0": version: 4.1.0 resolution: "is-plain-obj@npm:4.1.0" @@ -17279,6 +17551,39 @@ __metadata: languageName: node linkType: hard +"jest-environment-emit@npm:^1.0.8": + version: 1.0.8 + resolution: "jest-environment-emit@npm:1.0.8" + dependencies: + bunyamin: ^1.5.2 + bunyan: ^2.0.5 + bunyan-debug-stream: ^3.1.0 + funpermaproxy: ^1.1.0 + lodash.merge: ^4.6.2 + node-ipc: 9.2.1 + strip-ansi: ^6.0.0 + tslib: ^2.5.3 + peerDependencies: + "@jest/environment": ">=27.2.5" + "@jest/types": ">=27.2.5" + jest: ">=27.2.5" + jest-environment-jsdom: ">=27.2.5" + jest-environment-node: ">=27.2.5" + peerDependenciesMeta: + "@jest/environment": + optional: true + "@jest/types": + optional: true + jest: + optional: true + jest-environment-jsdom: + optional: true + jest-environment-node: + optional: true + checksum: 0c7bafbd3a6e5952f6abb45958f0d2997371d29b29f3876afda48d1d734ccd703577aaac0d5afec2e19dc33a9db0e9458721fe73dbe797f0ced21481d908acfd + languageName: node + linkType: hard + "jest-environment-jsdom@npm:^29.2.1, jest-environment-jsdom@npm:^29.6.2": version: 29.7.0 resolution: "jest-environment-jsdom@npm:29.7.0" @@ -17781,6 +18086,22 @@ __metadata: languageName: node linkType: hard +"js-message@npm:1.0.7": + version: 1.0.7 + resolution: "js-message@npm:1.0.7" + checksum: 18dcc4d80356e8b5be978ca7838d96d4e350a1cb8adc5741c229dec6df09f53bfed7c75c1f360171d2d791a14e2f077d6c2b1013ba899ded7a27d7dfcd4f3784 + languageName: node + linkType: hard + +"js-queue@npm:2.0.2": + version: 2.0.2 + resolution: "js-queue@npm:2.0.2" + dependencies: + easy-stack: ^1.0.1 + checksum: 5049c3f648315ed13e46755704ff5453df70f7e8e1812acf1f98d6700efbec32421f76294a0e63fd2a9f8aabaf124233bbb308f9a2caec9d9f3d833ab5a73079 + languageName: node + linkType: hard + "js-tokens@npm:^3.0.0 || ^4.0.0, js-tokens@npm:^4.0.0": version: 4.0.0 resolution: "js-tokens@npm:4.0.0" @@ -17973,6 +18294,13 @@ __metadata: languageName: node linkType: hard +"json-cycle@npm:^1.3.0": + version: 1.5.0 + resolution: "json-cycle@npm:1.5.0" + checksum: 0a44cd349676c6726093c64283fb75402f9104b32325b06c9270af6d639e7caac419f5301a39298aef2ac1659b273b167e02bd622e628c3392cf86f0e77a9f78 + languageName: node + linkType: hard + "json-parse-better-errors@npm:^1.0.1": version: 1.0.2 resolution: "json-parse-better-errors@npm:1.0.2" @@ -18805,6 +19133,16 @@ __metadata: languageName: node linkType: hard +"lru-cache@npm:^4.0.1": + version: 4.1.5 + resolution: "lru-cache@npm:4.1.5" + dependencies: + pseudomap: ^1.0.2 + yallist: ^2.1.2 + checksum: 4bb4b58a36cd7dc4dcec74cbe6a8f766a38b7426f1ff59d4cf7d82a2aa9b9565cd1cb98f6ff60ce5cd174524868d7bc9b7b1c294371851356066ca9ac4cf135a + languageName: node + linkType: hard + "lru-cache@npm:^5.1.1": version: 5.1.1 resolution: "lru-cache@npm:5.1.1" @@ -20258,7 +20596,7 @@ __metadata: languageName: node linkType: hard -"moment@npm:2.30.1, moment@npm:^2.24.0, moment@npm:^2.29.4": +"moment@npm:2.30.1, moment@npm:^2.19.3, moment@npm:^2.24.0, moment@npm:^2.29.4": version: 2.30.1 resolution: "moment@npm:2.30.1" checksum: 859236bab1e88c3e5802afcf797fc801acdbd0ee509d34ea3df6eea21eb6bcc2abd4ae4e4e64aa7c986aa6cba563c6e62806218e6412a765010712e5fa121ba6 @@ -20313,6 +20651,13 @@ __metadata: languageName: node linkType: hard +"multi-sort-stream@npm:^1.0.3, multi-sort-stream@npm:^1.0.4": + version: 1.0.4 + resolution: "multi-sort-stream@npm:1.0.4" + checksum: b234754e0e7489623f5184ba0e887ffd8014fe829c846fd8a95569339b6e19a616ae1d44f3d064279adfbf92fa5c4d016a89fc5026e16dbd680ebd67067b19a0 + languageName: node + linkType: hard + "multimatch@npm:5.0.0": version: 5.0.0 resolution: "multimatch@npm:5.0.0" @@ -20326,6 +20671,16 @@ __metadata: languageName: node linkType: hard +"multipipe@npm:^4.0.0": + version: 4.0.0 + resolution: "multipipe@npm:4.0.0" + dependencies: + duplexer2: ^0.1.2 + object-assign: ^4.1.0 + checksum: 5a494ec2ce5bfdb389882ca595e3c4a33cae6c90dad879db2e3aa9a94484d8b164b0fb7b58ccf7593ae7e8c6213fd3f53a736b2c98e4f14c5ed1d38debc33f98 + languageName: node + linkType: hard + "mute-stream@npm:0.0.7": version: 0.0.7 resolution: "mute-stream@npm:0.0.7" @@ -20347,7 +20702,7 @@ __metadata: languageName: node linkType: hard -"mv@npm:2.1.1": +"mv@npm:2.1.1, mv@npm:~2": version: 2.1.1 resolution: "mv@npm:2.1.1" dependencies: @@ -20369,6 +20724,15 @@ __metadata: languageName: node linkType: hard +"nan@npm:^2.14.0": + version: 2.22.0 + resolution: "nan@npm:2.22.0" + dependencies: + node-gyp: latest + checksum: 222e3a090e326c72f6782d948f44ee9b81cfb2161d5fe53216f04426a273fd094deee9dcc6813096dd2397689a2b10c1a92d3885d2e73fd2488a51547beb2929 + languageName: node + linkType: hard + "nanoid@npm:3.3.7, nanoid@npm:^3.1.23, nanoid@npm:^3.3.7": version: 3.3.7 resolution: "nanoid@npm:3.3.7" @@ -20546,6 +20910,17 @@ __metadata: languageName: node linkType: hard +"node-ipc@npm:9.2.1": + version: 9.2.1 + resolution: "node-ipc@npm:9.2.1" + dependencies: + event-pubsub: 4.3.0 + js-message: 1.0.7 + js-queue: 2.0.2 + checksum: a38aa4c8ca4317b293e0ce21f0a3a4941fc51c054800b35e263fcfe3e0feeb60e7d2c497f015054b28783316c6e7d9cc3837af9d9958bcbd8c577d0cdf6964b7 + languageName: node + linkType: hard + "node-java-connector@npm:1.1.1": version: 1.1.1 resolution: "node-java-connector@npm:1.1.1" @@ -20616,6 +20991,13 @@ __metadata: languageName: node linkType: hard +"node-version@npm:^1.0.0": + version: 1.2.0 + resolution: "node-version@npm:1.2.0" + checksum: 74e92d2a7f0fe0fce3aafd6dcc30b3b440999df68b3d92fcefcad2a52b37bc29c6b542f33760229390bfdc1a4d993fb65b9c199b1f0d568969d07fc1c04bc1e7 + languageName: node + linkType: hard + "nopt@npm:^7.0.0, nopt@npm:^7.2.0, nopt@npm:^7.2.1": version: 7.2.1 resolution: "nopt@npm:7.2.1" @@ -22075,6 +22457,13 @@ __metadata: languageName: node linkType: hard +"promise-polyfill@npm:^6.0.1": + version: 6.1.0 + resolution: "promise-polyfill@npm:6.1.0" + checksum: 6f1899cca37e48f67a424842282acd525d8d99d3536f2d97e37a117cfc4a0006683330ceaf5a15fbc09b4450f319a680292f9970a5f8e9cf90acbce0bdb0f751 + languageName: node + linkType: hard + "promise-retry@npm:^2.0.1": version: 2.0.1 resolution: "promise-retry@npm:2.0.1" @@ -22133,6 +22522,17 @@ __metadata: languageName: node linkType: hard +"proper-lockfile@npm:^3.0.2": + version: 3.2.0 + resolution: "proper-lockfile@npm:3.2.0" + dependencies: + graceful-fs: ^4.1.11 + retry: ^0.12.0 + signal-exit: ^3.0.2 + checksum: 1be1bb702b9d47bdf18d75f22578f51370781feba7d2617f70ff8c66a86bcfa6e55b4f69c57fc326380110f2d1ffdb6e54a4900814bf156c04ee4eb2d3c065aa + languageName: node + linkType: hard + "proto-list@npm:~1.2.1": version: 1.2.4 resolution: "proto-list@npm:1.2.4" @@ -22180,6 +22580,13 @@ __metadata: languageName: node linkType: hard +"pseudomap@npm:^1.0.2": + version: 1.0.2 + resolution: "pseudomap@npm:1.0.2" + checksum: 856c0aae0ff2ad60881168334448e898ad7a0e45fe7386d114b150084254c01e200c957cf378378025df4e052c7890c5bd933939b0e0d2ecfcc1dc2f0b2991f5 + languageName: node + linkType: hard + "psl@npm:^1.1.33": version: 1.9.0 resolution: "psl@npm:1.9.0" @@ -23257,7 +23664,7 @@ __metadata: languageName: node linkType: hard -"readable-stream@npm:^2.0.1, readable-stream@npm:^2.0.5, readable-stream@npm:~2.3.6": +"readable-stream@npm:^2.0.1, readable-stream@npm:^2.0.2, readable-stream@npm:^2.0.5, readable-stream@npm:~2.3.6": version: 2.3.8 resolution: "readable-stream@npm:2.3.8" dependencies: @@ -23949,6 +24356,13 @@ __metadata: languageName: node linkType: hard +"safe-json-stringify@npm:~1": + version: 1.2.0 + resolution: "safe-json-stringify@npm:1.2.0" + checksum: 5bb32db6d6a3ceb3752df51f4043a412419cd3d4fcd5680a865dfa34cd7e575ba659c077d13f52981ced084061df9c75c7fb12e391584d4264e6914c1cd3d216 + languageName: node + linkType: hard + "safe-regex-test@npm:^1.0.3": version: 1.0.3 resolution: "safe-regex-test@npm:1.0.3" @@ -23974,7 +24388,7 @@ __metadata: languageName: node linkType: hard -"sanitize-filename@npm:1.6.3": +"sanitize-filename@npm:1.6.3, sanitize-filename@npm:^1.6.1": version: 1.6.3 resolution: "sanitize-filename@npm:1.6.3" dependencies: @@ -24286,6 +24700,8 @@ __metadata: "@react-navigation/stack": ^7.0.3 "@sentry/babel-plugin-component-annotate": ^2.18.0 "@sentry/react-native": 6.7.0-alpha.0 + "@types/jest": ^29.5.14 + "@types/node": ^22.13.1 "@types/react": ^18.2.65 "@types/react-native-vector-icons": ^6.4.18 "@types/react-test-renderer": ^18.0.0 @@ -24294,6 +24710,7 @@ __metadata: babel-jest: ^29.2.1 babel-plugin-module-resolver: ^5.0.0 delay: ^6.0.0 + detox: ^20.33.0 eslint: ^8.19.0 jest: ^29.6.3 patch-package: ^8.0.0 @@ -24313,6 +24730,7 @@ __metadata: react-test-renderer: 18.3.1 redux: ^4.2.1 sentry-react-native-samples-utils: "workspace:^" + ts-jest: ^29.2.5 typescript: 5.0.4 languageName: unknown linkType: soft @@ -24366,6 +24784,15 @@ __metadata: languageName: node linkType: hard +"serialize-error@npm:^8.0.1": + version: 8.1.0 + resolution: "serialize-error@npm:8.1.0" + dependencies: + type-fest: ^0.20.2 + checksum: 2eef236d50edd2d7926e602c14fb500dc3a125ee52e9f08f67033181b8e0be5d1122498bdf7c23c80683cddcad083a27974e9e7111ce23165f4d3bcdd6d65102 + languageName: node + linkType: hard + "serve-favicon@npm:2.5.0": version: 2.5.0 resolution: "serve-favicon@npm:2.5.0" @@ -24576,6 +25003,13 @@ __metadata: languageName: node linkType: hard +"shell-quote@npm:^1.7.2": + version: 1.8.2 + resolution: "shell-quote@npm:1.8.2" + checksum: 1e97b62ced1c4c5135015978ebf273bed1f425a68cf84163e83fbb0f34b3ff9471e656720dab2b7cbb4ae0f58998e686d17d166c28dfb3662acd009e8bd7faed + languageName: node + linkType: hard + "shelljs@npm:^0.8.3": version: 0.8.5 resolution: "shelljs@npm:0.8.5" @@ -25004,6 +25438,13 @@ __metadata: languageName: node linkType: hard +"stream-chain@npm:^2.2.5": + version: 2.2.5 + resolution: "stream-chain@npm:2.2.5" + checksum: c83cbf504bd11e2bcbe761a92801295b3decac7ffa4092ceffca2eb1b5d0763bcc511fa22cd8044e8a18c21ca66794fd10c8d9cd1292a3e6c0d83a4194c6b8ed + languageName: node + linkType: hard + "stream-combiner@npm:^0.2.2": version: 0.2.2 resolution: "stream-combiner@npm:0.2.2" @@ -25014,6 +25455,15 @@ __metadata: languageName: node linkType: hard +"stream-json@npm:^1.7.4, stream-json@npm:^1.7.5": + version: 1.9.1 + resolution: "stream-json@npm:1.9.1" + dependencies: + stream-chain: ^2.2.5 + checksum: 2ebf0648f9ed82ee79727a9a47805231a70d5032e0c21cee3e05cd3c449d3ce49c72b371555447eeef55904bae22ac64be8ae6086fc6cce0b83b3aa617736b64 + languageName: node + linkType: hard + "stream-slice@npm:^0.1.2": version: 0.1.2 resolution: "stream-slice@npm:0.1.2" @@ -25591,7 +26041,16 @@ __metadata: languageName: node linkType: hard -"temp-dir@npm:1.0.0": +"telnet-client@npm:1.2.8": + version: 1.2.8 + resolution: "telnet-client@npm:1.2.8" + dependencies: + bluebird: ^3.5.4 + checksum: d2430c5449a46f6f4f9a7c2c648164f014c308aa0d3207a4d6b5b7f0e443322d07b180ecac63ad43eadb6557c8ef5ae7dce1ea6276464c8c82c8c6a9c9c01bf2 + languageName: node + linkType: hard + +"temp-dir@npm:1.0.0, temp-dir@npm:^1.0.0": version: 1.0.0 resolution: "temp-dir@npm:1.0.0" checksum: cb2b58ddfb12efa83e939091386ad73b425c9a8487ea0095fe4653192a40d49184a771a1beba99045fbd011e389fd563122d79f54f82be86a55620667e08a6b2 @@ -25624,6 +26083,16 @@ __metadata: languageName: node linkType: hard +"tempfile@npm:^2.0.0": + version: 2.0.0 + resolution: "tempfile@npm:2.0.0" + dependencies: + temp-dir: ^1.0.0 + uuid: ^3.0.1 + checksum: 8a92a0f57e0ae457dfbc156b14c427b42048a86ca6bade311835cc2aeda61b25b82d688f71f2d663dde6f172f479ed07293b53f7981e41cb6f9120a3eb4fe797 + languageName: node + linkType: hard + "tempy@npm:^0.7.1": version: 0.7.1 resolution: "tempy@npm:0.7.1" @@ -25825,6 +26294,15 @@ __metadata: languageName: node linkType: hard +"trace-event-lib@npm:^1.3.1": + version: 1.4.1 + resolution: "trace-event-lib@npm:1.4.1" + dependencies: + browser-process-hrtime: ^1.0.0 + checksum: f10dbfeccee9ec80a8cf69ecadd49fa609fc2593fb50a83cc4b664524c0531f91009134bf54302f9c4911afed119b0eebb8d2724723fc44516e24a40aaae9219 + languageName: node + linkType: hard + "treeverse@npm:^3.0.0": version: 3.0.0 resolution: "treeverse@npm:3.0.0" @@ -25878,7 +26356,7 @@ __metadata: languageName: node linkType: hard -"ts-jest@npm:^29.1.1": +"ts-jest@npm:^29.1.1, ts-jest@npm:^29.2.5": version: 29.2.5 resolution: "ts-jest@npm:29.2.5" dependencies: @@ -26006,6 +26484,13 @@ __metadata: languageName: node linkType: hard +"tslib@npm:^2.5.3": + version: 2.8.1 + resolution: "tslib@npm:2.8.1" + checksum: e4aba30e632b8c8902b47587fd13345e2827fa639e7c3121074d5ee0880723282411a8838f830b55100cbe4517672f84a2472667d355b81e8af165a55dc6203a + languageName: node + linkType: hard + "tsutils@npm:^3.21.0": version: 3.21.0 resolution: "tsutils@npm:3.21.0" @@ -26398,6 +26883,13 @@ __metadata: languageName: node linkType: hard +"undici-types@npm:~6.20.0": + version: 6.20.0 + resolution: "undici-types@npm:6.20.0" + checksum: b7bc50f012dc6afbcce56c9fd62d7e86b20a62ff21f12b7b5cbf1973b9578d90f22a9c7fe50e638e96905d33893bf2f9f16d98929c4673c2480de05c6c96ea8b + languageName: node + linkType: hard + "undici@npm:^6.11.1, undici@npm:^6.18.2": version: 6.21.1 resolution: "undici@npm:6.21.1" @@ -26657,6 +27149,15 @@ __metadata: languageName: node linkType: hard +"uuid@npm:^3.0.1": + version: 3.4.0 + resolution: "uuid@npm:3.4.0" + bin: + uuid: ./bin/uuid + checksum: 58de2feed61c59060b40f8203c0e4ed7fd6f99d42534a499f1741218a1dd0c129f4aa1de797bcf822c8ea5da7e4137aa3673431a96dae729047f7aca7b27866f + languageName: node + linkType: hard + "uuid@npm:^7.0.3": version: 7.0.3 resolution: "uuid@npm:7.0.3" @@ -27029,7 +27530,7 @@ __metadata: languageName: node linkType: hard -"which@npm:^1.2.9": +"which@npm:^1.2.9, which@npm:^1.3.1": version: 1.3.1 resolution: "which@npm:1.3.1" dependencies: @@ -27247,7 +27748,7 @@ __metadata: languageName: node linkType: hard -"ws@npm:^7, ws@npm:^7.5.1, ws@npm:^7.5.10": +"ws@npm:^7, ws@npm:^7.0.0, ws@npm:^7.5.1, ws@npm:^7.5.10": version: 7.5.10 resolution: "ws@npm:7.5.10" peerDependencies: @@ -27378,6 +27879,13 @@ __metadata: languageName: node linkType: hard +"yallist@npm:^2.1.2": + version: 2.1.2 + resolution: "yallist@npm:2.1.2" + checksum: 9ba99409209f485b6fcb970330908a6d41fa1c933f75e08250316cce19383179a6b70a7e0721b89672ebb6199cc377bf3e432f55100da6a7d6e11902b0a642cb + languageName: node + linkType: hard + "yallist@npm:^3.0.2": version: 3.1.1 resolution: "yallist@npm:3.1.1" @@ -27408,7 +27916,7 @@ __metadata: languageName: node linkType: hard -"yargs-parser@npm:21.1.1, yargs-parser@npm:^21.0.1, yargs-parser@npm:^21.1.1": +"yargs-parser@npm:21.1.1, yargs-parser@npm:^21.0.0, yargs-parser@npm:^21.0.1, yargs-parser@npm:^21.1.1": version: 21.1.1 resolution: "yargs-parser@npm:21.1.1" checksum: ed2d96a616a9e3e1cc7d204c62ecc61f7aaab633dcbfab2c6df50f7f87b393993fe6640d017759fe112d0cb1e0119f2b4150a87305cc873fd90831c6a58ccf1c @@ -27432,7 +27940,19 @@ __metadata: languageName: node linkType: hard -"yargs@npm:17.7.2, yargs@npm:^17.3.1, yargs@npm:^17.6.2, yargs@npm:^17.7.2": +"yargs-unparser@npm:^2.0.0": + version: 2.0.0 + resolution: "yargs-unparser@npm:2.0.0" + dependencies: + camelcase: ^6.0.0 + decamelize: ^4.0.0 + flat: ^5.0.2 + is-plain-obj: ^2.1.0 + checksum: 68f9a542c6927c3768c2f16c28f71b19008710abd6b8f8efbac6dcce26bbb68ab6503bed1d5994bdbc2df9a5c87c161110c1dfe04c6a3fe5c6ad1b0e15d9a8a3 + languageName: node + linkType: hard + +"yargs@npm:17.7.2, yargs@npm:^17.0.0, yargs@npm:^17.3.1, yargs@npm:^17.6.2, yargs@npm:^17.7.2": version: 17.7.2 resolution: "yargs@npm:17.7.2" dependencies: From cf00d4dec2822985acf7a6bb70b99d4645ad576e Mon Sep 17 00:00:00 2001 From: Krystof Woldrich <31292499+krystofwoldrich@users.noreply.github.com> Date: Mon, 17 Feb 2025 16:44:39 +0100 Subject: [PATCH 15/32] internal(sample-rn): Add header and message envelope tests (#4536) --- .github/workflows/sample-application.yml | 4 + samples/react-native/.detoxrc.js | 38 +++++++-- .../e2e/captureMessage.test.android.ts | 49 +++++++++++ .../e2e/captureMessage.test.ios.ts | 46 ++++++++++ .../e2e/envelopeHeader.test.android.ts | 63 ++++++++++++++ .../e2e/envelopeHeader.test.ios.ts | 69 +++++++++++++++ .../react-native/e2e/jest.config.android.js | 16 ++++ samples/react-native/e2e/jest.config.ios.js | 13 +++ samples/react-native/e2e/utils/consts.ts | 2 + .../e2e/utils/mockedSentryServer.ts | 84 +++++++++++++++++++ .../react-native/e2e/utils/parseEnvelope.ts | 74 ++++++++++++++++ samples/react-native/e2e/utils/tap.ts | 14 ++++ samples/react-native/package.json | 1 + samples/react-native/scripts/dsn.mjs | 24 ++++++ samples/react-native/scripts/set-aos-dsn.mjs | 5 ++ samples/react-native/scripts/set-ios-dsn.mjs | 5 ++ yarn.lock | 1 + 17 files changed, 499 insertions(+), 9 deletions(-) create mode 100644 samples/react-native/e2e/captureMessage.test.android.ts create mode 100644 samples/react-native/e2e/captureMessage.test.ios.ts create mode 100644 samples/react-native/e2e/envelopeHeader.test.android.ts create mode 100644 samples/react-native/e2e/envelopeHeader.test.ios.ts create mode 100644 samples/react-native/e2e/jest.config.android.js create mode 100644 samples/react-native/e2e/jest.config.ios.js create mode 100644 samples/react-native/e2e/utils/consts.ts create mode 100644 samples/react-native/e2e/utils/mockedSentryServer.ts create mode 100644 samples/react-native/e2e/utils/parseEnvelope.ts create mode 100644 samples/react-native/e2e/utils/tap.ts create mode 100644 samples/react-native/scripts/dsn.mjs create mode 100755 samples/react-native/scripts/set-aos-dsn.mjs create mode 100755 samples/react-native/scripts/set-ios-dsn.mjs diff --git a/.github/workflows/sample-application.yml b/.github/workflows/sample-application.yml index e4e9a8f8b3..53202a0f31 100644 --- a/.github/workflows/sample-application.yml +++ b/.github/workflows/sample-application.yml @@ -114,6 +114,8 @@ jobs: if: ${{ matrix.platform == 'android' }} working-directory: ${{ env.REACT_NATIVE_SAMPLE_PATH }}/android run: | + ../scripts/set-aos-dsn.mjs + if [[ ${{ matrix.rn-architecture }} == 'new' ]]; then perl -i -pe's/newArchEnabled=false/newArchEnabled=true/g' gradle.properties echo 'New Architecture enabled' @@ -134,6 +136,8 @@ jobs: if: ${{ matrix.platform == 'ios' }} working-directory: ${{ env.REACT_NATIVE_SAMPLE_PATH }}/ios run: | + ../scripts/set-ios-dsn.mjs + [[ "${{ matrix.build-type }}" == "production" ]] && CONFIG='Release' || CONFIG='Debug' echo "Building $CONFIG" mkdir -p "DerivedData" diff --git a/samples/react-native/.detoxrc.js b/samples/react-native/.detoxrc.js index fd29191f91..e6ba0d66cd 100644 --- a/samples/react-native/.detoxrc.js +++ b/samples/react-native/.detoxrc.js @@ -1,16 +1,28 @@ const process = require('process'); +const testRunnerIos = { + args: { + $0: 'jest', + config: 'e2e/jest.config.ios.js', + }, + jest: { + setupTimeout: 120000, + }, +}; + +const testRunnerAos = { + args: { + $0: 'jest', + config: 'e2e/jest.config.android.js', + }, + jest: { + setupTimeout: 120000, + }, +}; + /** @type {Detox.DetoxConfig} */ module.exports = { - testRunner: { - args: { - $0: 'jest', - config: 'e2e/jest.config.js', - }, - jest: { - setupTimeout: 120000, - }, - }, + testRunner: {}, apps: { 'ios.debug': { type: 'ios.app', @@ -86,34 +98,42 @@ module.exports = { 'ios.sim.debug': { device: 'simulator', app: 'ios.debug', + testRunner: testRunnerIos, }, 'ios.sim.release': { device: 'simulator', app: 'ios.release', + testRunner: testRunnerIos, }, 'android.att.debug': { device: 'attached', app: 'android.debug', + testRunner: testRunnerAos, }, 'android.att.release': { device: 'attached', app: 'android.release', + testRunner: testRunnerAos, }, 'android.emu.debug': { device: 'emulator', app: 'android.debug', + testRunner: testRunnerAos, }, 'android.emu.release': { device: 'emulator', app: 'android.release', + testRunner: testRunnerAos, }, 'ci.android': { device: 'ci.emulator', app: 'ci.android', + testRunner: testRunnerAos, }, 'ci.sim': { device: 'ci.simulator', app: 'ci.ios', + testRunner: testRunnerIos, }, }, }; diff --git a/samples/react-native/e2e/captureMessage.test.android.ts b/samples/react-native/e2e/captureMessage.test.android.ts new file mode 100644 index 0000000000..a56d3704bc --- /dev/null +++ b/samples/react-native/e2e/captureMessage.test.android.ts @@ -0,0 +1,49 @@ +import { describe, it, beforeAll, expect } from '@jest/globals'; +import { Envelope } from '@sentry/core'; +import { device } from 'detox'; +import { + createSentryServer, + containingEvent, +} from './utils/mockedSentryServer'; +import { HEADER, ITEMS } from './utils/consts'; +import { tap } from './utils/tap'; + +describe('Capture message', () => { + let sentryServer = createSentryServer(); + sentryServer.start(); + + let envelope: Envelope; + + beforeAll(async () => { + await device.launchApp(); + + const envelopePromise = sentryServer.waitForEnvelope(containingEvent); + await tap('Capture message'); + envelope = await envelopePromise; + }); + + afterAll(async () => { + await sentryServer.close(); + }); + + it('envelope contains message event', async () => { + const item = (envelope[ITEMS] as [{ type?: string }, unknown][]).find( + i => i[HEADER].type === 'event', + ); + + expect(item).toEqual([ + { + content_type: 'application/json', + length: expect.any(Number), + type: 'event', + }, + expect.objectContaining({ + level: 'info', + message: { + message: 'Captured message', + }, + platform: 'javascript', + }), + ]); + }); +}); diff --git a/samples/react-native/e2e/captureMessage.test.ios.ts b/samples/react-native/e2e/captureMessage.test.ios.ts new file mode 100644 index 0000000000..25c953b707 --- /dev/null +++ b/samples/react-native/e2e/captureMessage.test.ios.ts @@ -0,0 +1,46 @@ +import { describe, it, beforeAll, expect } from '@jest/globals'; +import { Envelope } from '@sentry/core'; +import { device } from 'detox'; +import { + createSentryServer, + containingEvent, +} from './utils/mockedSentryServer'; +import { HEADER, ITEMS } from './utils/consts'; +import { tap } from './utils/tap'; + +describe('Capture message', () => { + let sentryServer = createSentryServer(); + sentryServer.start(); + + let envelope: Envelope; + + beforeAll(async () => { + await device.launchApp(); + + const envelopePromise = sentryServer.waitForEnvelope(containingEvent); + await tap('Capture message'); + envelope = await envelopePromise; + }); + + afterAll(async () => { + await sentryServer.close(); + }); + + it('envelope contains message event', async () => { + const item = (envelope[ITEMS] as [{ type?: string }, unknown][]).find( + i => i[HEADER].type === 'event', + ); + + expect(item).toEqual([ + { + length: expect.any(Number), + type: 'event', + }, + expect.objectContaining({ + level: 'info', + message: 'Captured message', + platform: 'javascript', + }), + ]); + }); +}); diff --git a/samples/react-native/e2e/envelopeHeader.test.android.ts b/samples/react-native/e2e/envelopeHeader.test.android.ts new file mode 100644 index 0000000000..26700f9245 --- /dev/null +++ b/samples/react-native/e2e/envelopeHeader.test.android.ts @@ -0,0 +1,63 @@ +import { describe, it, beforeAll, expect } from '@jest/globals'; +import { Envelope } from '@sentry/core'; +import { device } from 'detox'; +import { + createSentryServer, + containingEvent, +} from './utils/mockedSentryServer'; +import { HEADER } from './utils/consts'; +import { tap } from './utils/tap'; + +describe('Capture message', () => { + let sentryServer = createSentryServer(); + sentryServer.start(); + + let envelope: Envelope; + + beforeAll(async () => { + await device.launchApp(); + + const envelopePromise = sentryServer.waitForEnvelope(containingEvent); + + await tap('Capture message'); + envelope = await envelopePromise; + }); + + afterAll(async () => { + await sentryServer.close(); + }); + + it('contains event_id and sent_at in the envelope header', async () => { + expect(envelope[HEADER]).toEqual( + expect.objectContaining({ + event_id: expect.any(String), + sent_at: expect.any(String), + }), + ); + }); + + it('contains sdk info in the envelope header', async () => { + expect(envelope[HEADER]).toEqual( + expect.objectContaining({ + sdk: { + name: 'sentry.javascript.react-native', + version: expect.any(String), + }, + sent_at: expect.any(String), + }), + ); + }); + + it('contains trace info in the envelope header', async () => { + expect(envelope[HEADER]).toEqual( + expect.objectContaining({ + trace: { + environment: expect.any(String), + public_key: expect.any(String), + replay_id: expect.any(String), + trace_id: expect.any(String), + }, + }), + ); + }); +}); diff --git a/samples/react-native/e2e/envelopeHeader.test.ios.ts b/samples/react-native/e2e/envelopeHeader.test.ios.ts new file mode 100644 index 0000000000..4b72f76e18 --- /dev/null +++ b/samples/react-native/e2e/envelopeHeader.test.ios.ts @@ -0,0 +1,69 @@ +import { describe, it, beforeAll, expect } from '@jest/globals'; +import { Envelope } from '@sentry/core'; +import { device } from 'detox'; +import { + createSentryServer, + containingEvent, +} from './utils/mockedSentryServer'; +import { HEADER } from './utils/consts'; +import { tap } from './utils/tap'; + +describe('Capture message', () => { + let sentryServer = createSentryServer(); + sentryServer.start(); + + let envelope: Envelope; + + beforeAll(async () => { + await device.launchApp(); + + const envelopePromise = sentryServer.waitForEnvelope(containingEvent); + + await tap('Capture message'); + envelope = await envelopePromise; + }); + + afterAll(async () => { + await sentryServer.close(); + }); + + it('contains event_id and sent_at in the envelope header', async () => { + expect(envelope[HEADER]).toEqual( + expect.objectContaining({ + event_id: expect.any(String), + sent_at: expect.any(String), + }), + ); + }); + + it('contains sdk info in the envelope header', async () => { + expect(envelope[HEADER]).toEqual( + expect.objectContaining({ + sdk: { + features: [], + integrations: [], + name: 'sentry.javascript.react-native', + packages: [], + version: expect.any(String), + }, + sent_at: expect.any(String), + }), + ); + }); + + it('contains trace info in the envelope header', async () => { + expect(envelope[HEADER]).toEqual( + expect.objectContaining({ + trace: { + environment: expect.any(String), + public_key: expect.any(String), + replay_id: expect.any(String), + sample_rate: '1', + sampled: '1', + trace_id: expect.any(String), + transaction: 'ErrorsScreen', + }, + }), + ); + }); +}); diff --git a/samples/react-native/e2e/jest.config.android.js b/samples/react-native/e2e/jest.config.android.js new file mode 100644 index 0000000000..2d755851a0 --- /dev/null +++ b/samples/react-native/e2e/jest.config.android.js @@ -0,0 +1,16 @@ +/** @type {import('@jest/types').Config.InitialOptions} */ +module.exports = { + preset: 'ts-jest', + rootDir: '..', + testMatch: [ + '/e2e/**/*.test.ts', + '/e2e/**/*.test.android.ts', + ], + testTimeout: 120000, + maxWorkers: 1, + globalSetup: 'detox/runners/jest/globalSetup', + globalTeardown: 'detox/runners/jest/globalTeardown', + reporters: ['detox/runners/jest/reporter'], + testEnvironment: 'detox/runners/jest/testEnvironment', + verbose: true, +}; diff --git a/samples/react-native/e2e/jest.config.ios.js b/samples/react-native/e2e/jest.config.ios.js new file mode 100644 index 0000000000..62034bc390 --- /dev/null +++ b/samples/react-native/e2e/jest.config.ios.js @@ -0,0 +1,13 @@ +/** @type {import('@jest/types').Config.InitialOptions} */ +module.exports = { + preset: 'ts-jest', + rootDir: '..', + testMatch: ['/e2e/**/*.test.ts', '/e2e/**/*.test.ios.ts'], + testTimeout: 120000, + maxWorkers: 1, + globalSetup: 'detox/runners/jest/globalSetup', + globalTeardown: 'detox/runners/jest/globalTeardown', + reporters: ['detox/runners/jest/reporter'], + testEnvironment: 'detox/runners/jest/testEnvironment', + verbose: true, +}; diff --git a/samples/react-native/e2e/utils/consts.ts b/samples/react-native/e2e/utils/consts.ts new file mode 100644 index 0000000000..9a751a5fa4 --- /dev/null +++ b/samples/react-native/e2e/utils/consts.ts @@ -0,0 +1,2 @@ +export const HEADER = 0; +export const ITEMS = 1; diff --git a/samples/react-native/e2e/utils/mockedSentryServer.ts b/samples/react-native/e2e/utils/mockedSentryServer.ts new file mode 100644 index 0000000000..40667b0f9d --- /dev/null +++ b/samples/react-native/e2e/utils/mockedSentryServer.ts @@ -0,0 +1,84 @@ +import { IncomingMessage, ServerResponse, createServer } from 'node:http'; +import { createGunzip } from 'node:zlib'; +import { Envelope } from '@sentry/core'; +import { parseEnvelope } from './parseEnvelope'; + +type RecordedRequest = { + path: string | undefined; + headers: Record; + body: Buffer; + envelope: Envelope; +}; + +export function createSentryServer({ port = 8961 } = {}): { + waitForEnvelope: ( + predicate: (envelope: Envelope) => boolean, + ) => Promise; + close: () => Promise; + start: () => void; +} { + let onNextRequestCallback: (request: RecordedRequest) => void = () => {}; + const requests: RecordedRequest[] = []; + + const server = createServer((req: IncomingMessage, res: ServerResponse) => { + let body: Buffer = Buffer.from([]); + + const gunzip = createGunzip(); + req.pipe(gunzip); + + gunzip.on('data', (chunk: Buffer) => { + body = Buffer.concat([body, chunk]); + }); + + gunzip.on('end', () => { + const request = { + path: req.url, + headers: req.headers, + body: body, + envelope: parseEnvelope(body), + }; + requests.push(request); + + body = Buffer.from([]); + + res.writeHead(200); + res.end('OK'); + + onNextRequestCallback(request); + }); + }); + + return { + start: () => { + server.listen(port); + }, + waitForEnvelope: async ( + predicate: (envelope: Envelope) => boolean, + ): Promise => { + return new Promise((resolve, reject) => { + onNextRequestCallback = (request: RecordedRequest) => { + try { + if (predicate(request.envelope)) { + resolve(request.envelope); + return; + } + } catch (e) { + reject(e); + return; + } + }; + }); + }, + close: async () => { + await new Promise(resolve => { + server.close(() => resolve()); + }); + }, + }; +} + +export function containingEvent(envelope: Envelope) { + return envelope[1].some( + item => (item[0] as { type?: string }).type === 'event', + ); +} diff --git a/samples/react-native/e2e/utils/parseEnvelope.ts b/samples/react-native/e2e/utils/parseEnvelope.ts new file mode 100644 index 0000000000..e6b29b201e --- /dev/null +++ b/samples/react-native/e2e/utils/parseEnvelope.ts @@ -0,0 +1,74 @@ +import { + Envelope, + BaseEnvelopeHeaders, + BaseEnvelopeItemHeaders, +} from '@sentry/core'; + +/** + * Parses an envelope + */ +export function parseEnvelope(env: string | Uint8Array): Envelope { + let buffer = typeof env === 'string' ? encodeUTF8(env) : env; + + function readBinary(length?: number): Uint8Array { + if (!length) { + throw new Error('Binary Envelope Items must have a length to be read'); + } + const bin = buffer.subarray(0, length); + // Replace the buffer with the remaining data excluding trailing newline + buffer = buffer.subarray(length + 1); + return bin; + } + + function readJson(): T { + let i = buffer.indexOf(0xa); + // If we couldn't find a newline, we must have found the end of the buffer + if (i < 0) { + i = buffer.length; + } + + return JSON.parse(decodeUTF8(readBinary(i))) as T; + } + + const envelopeHeader = readJson(); + + const items: [any, any][] = []; + + while (buffer.length) { + const itemHeader = readJson(); + const isBinaryAttachment = + itemHeader.type === 'attachment' && + itemHeader.content_type !== 'application/json'; + // TODO: Parse when needed for the tests + const isReplayVideo = (itemHeader.type as string) === 'replay_video'; + + try { + let item: any = {}; + if (isReplayVideo || isBinaryAttachment) { + item = readBinary(itemHeader.length); + } else { + item = readJson(); + } + items.push([itemHeader, item]); + } catch (e) { + console.error(e, 'itemHeader', itemHeader, 'buffer', buffer.toString()); + throw e; + } + } + + return [envelopeHeader, items]; +} + +/** + * Encode a string to UTF8 array. + */ +function encodeUTF8(input: string): Uint8Array { + return new TextEncoder().encode(input); +} + +/** + * Decode a UTF8 array to string. + */ +function decodeUTF8(input: Uint8Array): string { + return new TextDecoder().decode(input); +} diff --git a/samples/react-native/e2e/utils/tap.ts b/samples/react-native/e2e/utils/tap.ts new file mode 100644 index 0000000000..3b12d61e31 --- /dev/null +++ b/samples/react-native/e2e/utils/tap.ts @@ -0,0 +1,14 @@ +import { element, by } from 'detox'; + +export const tap = async (text: string) => { + await element(by.text(createFlexibleRegex(text))).tap(); +}; + +/** + * Creates regex that matches case insensitive and allows flexible spacing between words + */ +function createFlexibleRegex(input: string) { + const words = input.trim().split(/\s+/); + const pattern = words.join('\\s*'); + return new RegExp(pattern, 'i'); +} diff --git a/samples/react-native/package.json b/samples/react-native/package.json index 128f329fa9..c2697d9959 100644 --- a/samples/react-native/package.json +++ b/samples/react-native/package.json @@ -25,6 +25,7 @@ "@react-navigation/native": "^7.0.3", "@react-navigation/native-stack": "^7.0.3", "@react-navigation/stack": "^7.0.3", + "@sentry/core": "8.54.0", "@sentry/react-native": "6.7.0-alpha.0", "delay": "^6.0.0", "react": "18.3.1", diff --git a/samples/react-native/scripts/dsn.mjs b/samples/react-native/scripts/dsn.mjs new file mode 100644 index 0000000000..da2153f203 --- /dev/null +++ b/samples/react-native/scripts/dsn.mjs @@ -0,0 +1,24 @@ +import { fileURLToPath } from 'node:url'; +import * as fs from 'node:fs'; +import * as path from 'node:path'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +export function setIosDsn() { + setDsn('http://key@localhost:8961/123456'); +} + +export function setAndroidDsn() { + setDsn('http://key@10.0.2.2:8961/123456'); +} + +function setDsn(dsn) { + const sentryOptionsPath = path.join(__dirname, '../sentry.options.json'); + const sentryOptions = JSON.parse(fs.readFileSync(sentryOptionsPath, 'utf8')); + sentryOptions.dsn = dsn; + fs.writeFileSync( + sentryOptionsPath, + JSON.stringify(sentryOptions, null, 2) + '\n', + ); + console.log('Dsn set to: ', dsn); +} diff --git a/samples/react-native/scripts/set-aos-dsn.mjs b/samples/react-native/scripts/set-aos-dsn.mjs new file mode 100755 index 0000000000..521cb44a11 --- /dev/null +++ b/samples/react-native/scripts/set-aos-dsn.mjs @@ -0,0 +1,5 @@ +#!/usr/bin/env node + +import { setAndroidDsn } from './dsn.mjs'; + +setAndroidDsn(); diff --git a/samples/react-native/scripts/set-ios-dsn.mjs b/samples/react-native/scripts/set-ios-dsn.mjs new file mode 100755 index 0000000000..ea1ca7e61a --- /dev/null +++ b/samples/react-native/scripts/set-ios-dsn.mjs @@ -0,0 +1,5 @@ +#!/usr/bin/env node + +import { setIosDsn } from './dsn.mjs'; + +setIosDsn(); diff --git a/yarn.lock b/yarn.lock index ca53bb1707..91d20f2005 100644 --- a/yarn.lock +++ b/yarn.lock @@ -24699,6 +24699,7 @@ __metadata: "@react-navigation/native-stack": ^7.0.3 "@react-navigation/stack": ^7.0.3 "@sentry/babel-plugin-component-annotate": ^2.18.0 + "@sentry/core": 8.54.0 "@sentry/react-native": 6.7.0-alpha.0 "@types/jest": ^29.5.14 "@types/node": ^22.13.1 From 0bf66360316a865361f4a8a06cf756a1c79a7462 Mon Sep 17 00:00:00 2001 From: Krystof Woldrich <31292499+krystofwoldrich@users.noreply.github.com> Date: Thu, 20 Feb 2025 10:04:35 +0100 Subject: [PATCH 16/32] fix(sample-e2e): Fix type errors missing sentry/core and afterAll (#4564) --- samples/react-native/e2e/captureMessage.test.android.ts | 2 +- samples/react-native/e2e/captureMessage.test.ios.ts | 2 +- samples/react-native/e2e/envelopeHeader.test.android.ts | 2 +- samples/react-native/e2e/envelopeHeader.test.ios.ts | 2 +- samples/react-native/package.json | 1 + yarn.lock | 1 + 6 files changed, 6 insertions(+), 4 deletions(-) diff --git a/samples/react-native/e2e/captureMessage.test.android.ts b/samples/react-native/e2e/captureMessage.test.android.ts index a56d3704bc..ad6d298a02 100644 --- a/samples/react-native/e2e/captureMessage.test.android.ts +++ b/samples/react-native/e2e/captureMessage.test.android.ts @@ -1,4 +1,4 @@ -import { describe, it, beforeAll, expect } from '@jest/globals'; +import { describe, it, beforeAll, expect, afterAll } from '@jest/globals'; import { Envelope } from '@sentry/core'; import { device } from 'detox'; import { diff --git a/samples/react-native/e2e/captureMessage.test.ios.ts b/samples/react-native/e2e/captureMessage.test.ios.ts index 25c953b707..a379718a68 100644 --- a/samples/react-native/e2e/captureMessage.test.ios.ts +++ b/samples/react-native/e2e/captureMessage.test.ios.ts @@ -1,4 +1,4 @@ -import { describe, it, beforeAll, expect } from '@jest/globals'; +import { describe, it, beforeAll, expect, afterAll } from '@jest/globals'; import { Envelope } from '@sentry/core'; import { device } from 'detox'; import { diff --git a/samples/react-native/e2e/envelopeHeader.test.android.ts b/samples/react-native/e2e/envelopeHeader.test.android.ts index 26700f9245..4be175d6c9 100644 --- a/samples/react-native/e2e/envelopeHeader.test.android.ts +++ b/samples/react-native/e2e/envelopeHeader.test.android.ts @@ -1,4 +1,4 @@ -import { describe, it, beforeAll, expect } from '@jest/globals'; +import { describe, it, beforeAll, expect, afterAll } from '@jest/globals'; import { Envelope } from '@sentry/core'; import { device } from 'detox'; import { diff --git a/samples/react-native/e2e/envelopeHeader.test.ios.ts b/samples/react-native/e2e/envelopeHeader.test.ios.ts index 4b72f76e18..04ce534226 100644 --- a/samples/react-native/e2e/envelopeHeader.test.ios.ts +++ b/samples/react-native/e2e/envelopeHeader.test.ios.ts @@ -1,4 +1,4 @@ -import { describe, it, beforeAll, expect } from '@jest/globals'; +import { describe, it, beforeAll, expect, afterAll } from '@jest/globals'; import { Envelope } from '@sentry/core'; import { device } from 'detox'; import { diff --git a/samples/react-native/package.json b/samples/react-native/package.json index 82e50a6755..b7b8980b88 100644 --- a/samples/react-native/package.json +++ b/samples/react-native/package.json @@ -53,6 +53,7 @@ "@react-native/metro-config": "0.77.0", "@react-native/typescript-config": "0.77.0", "@sentry/babel-plugin-component-annotate": "^3.1.2", + "@sentry/core": "8.54.0", "@types/react": "^18.2.65", "@types/react-native-vector-icons": "^6.4.18", "@types/react-test-renderer": "^18.0.0", diff --git a/yarn.lock b/yarn.lock index 17b6a36ac1..8dee18d805 100644 --- a/yarn.lock +++ b/yarn.lock @@ -25067,6 +25067,7 @@ __metadata: "@react-navigation/native-stack": ^7.2.0 "@react-navigation/stack": ^7.1.1 "@sentry/babel-plugin-component-annotate": ^3.1.2 + "@sentry/core": 8.54.0 "@sentry/react-native": 6.7.0 "@types/react": ^18.2.65 "@types/react-native-vector-icons": ^6.4.18 From e935360ae40e6445f13a3c8a0150a739fd27112e Mon Sep 17 00:00:00 2001 From: Krystof Woldrich <31292499+krystofwoldrich@users.noreply.github.com> Date: Fri, 21 Feb 2025 08:29:27 +0100 Subject: [PATCH 17/32] chore(samples): Add package scripts for native builds, dsn and testing (#4561) --- .github/workflows/sample-application.yml | 72 ++++------ .../react-native-macos/scripts/pod-install.sh | 18 +++ samples/react-native/.detoxrc.js | 124 ++++-------------- samples/react-native/.gitignore | 2 + samples/react-native/package.json | 24 +++- .../scripts/build-android-debug-legacy.sh | 11 ++ .../scripts/build-android-debug.sh | 11 ++ .../scripts/build-android-release-legacy.sh | 11 ++ .../scripts/build-android-release.sh | 11 ++ samples/react-native/scripts/build-android.sh | 29 ++++ .../react-native/scripts/build-ios-debug.sh | 10 ++ .../react-native/scripts/build-ios-release.sh | 10 ++ samples/react-native/scripts/build-ios.sh | 30 +++++ .../pod-install-debug-dynamic-legacy.sh | 12 ++ .../scripts/pod-install-debug-dynamic.sh | 12 ++ .../pod-install-debug-static-legacy.sh | 12 ++ .../scripts/pod-install-debug-static.sh | 12 ++ .../pod-install-release-dynamic-legacy.sh | 12 ++ .../scripts/pod-install-release-dynamic.sh | 12 ++ .../pod-install-release-static-legacy.sh | 12 ++ .../scripts/pod-install-release-static.sh | 12 ++ samples/react-native/scripts/pod-install.sh | 18 +++ samples/react-native/scripts/set-aos-dsn.mjs | 5 - samples/react-native/scripts/set-dsn-aos.mjs | 5 + samples/react-native/scripts/set-dsn-ios.mjs | 5 + .../scripts/{dsn.mjs => set-dsn.mjs} | 0 samples/react-native/scripts/set-ios-dsn.mjs | 5 - samples/react-native/scripts/test-android.sh | 43 ++++++ samples/react-native/scripts/test-ios.sh | 24 ++++ samples/react-native/sentry.options.json | 8 +- 30 files changed, 401 insertions(+), 171 deletions(-) create mode 100755 samples/react-native-macos/scripts/pod-install.sh create mode 100755 samples/react-native/scripts/build-android-debug-legacy.sh create mode 100755 samples/react-native/scripts/build-android-debug.sh create mode 100755 samples/react-native/scripts/build-android-release-legacy.sh create mode 100755 samples/react-native/scripts/build-android-release.sh create mode 100755 samples/react-native/scripts/build-android.sh create mode 100755 samples/react-native/scripts/build-ios-debug.sh create mode 100755 samples/react-native/scripts/build-ios-release.sh create mode 100755 samples/react-native/scripts/build-ios.sh create mode 100755 samples/react-native/scripts/pod-install-debug-dynamic-legacy.sh create mode 100755 samples/react-native/scripts/pod-install-debug-dynamic.sh create mode 100755 samples/react-native/scripts/pod-install-debug-static-legacy.sh create mode 100755 samples/react-native/scripts/pod-install-debug-static.sh create mode 100755 samples/react-native/scripts/pod-install-release-dynamic-legacy.sh create mode 100755 samples/react-native/scripts/pod-install-release-dynamic.sh create mode 100755 samples/react-native/scripts/pod-install-release-static-legacy.sh create mode 100755 samples/react-native/scripts/pod-install-release-static.sh create mode 100755 samples/react-native/scripts/pod-install.sh delete mode 100755 samples/react-native/scripts/set-aos-dsn.mjs create mode 100755 samples/react-native/scripts/set-dsn-aos.mjs create mode 100755 samples/react-native/scripts/set-dsn-ios.mjs rename samples/react-native/scripts/{dsn.mjs => set-dsn.mjs} (100%) delete mode 100755 samples/react-native/scripts/set-ios-dsn.mjs create mode 100755 samples/react-native/scripts/test-android.sh create mode 100755 samples/react-native/scripts/test-ios.sh diff --git a/.github/workflows/sample-application.yml b/.github/workflows/sample-application.yml index 53202a0f31..603cafa619 100644 --- a/.github/workflows/sample-application.yml +++ b/.github/workflows/sample-application.yml @@ -99,66 +99,39 @@ jobs: if: ${{ matrix.platform == 'ios' || matrix.platform == 'macos' }} working-directory: samples run: | - [[ "${{ matrix.platform }}" == "ios" ]] && cd react-native/ios - [[ "${{ matrix.platform }}" == "macos" ]] && cd react-native-macos/macos + [[ "${{ matrix.platform }}" == "ios" ]] && cd react-native + [[ "${{ matrix.platform }}" == "macos" ]] && cd react-native-macos - [[ "${{ matrix.build-type }}" == "production" ]] && ENABLE_PROD=1 || ENABLE_PROD=0 - [[ "${{ matrix.rn-architecture }}" == "new" ]] && ENABLE_NEW_ARCH=1 || ENABLE_NEW_ARCH=0 + [[ "${{ matrix.build-type }}" == "production" ]] && export ENABLE_PROD=1 || export ENABLE_PROD=0 + [[ "${{ matrix.rn-architecture }}" == "new" ]] && export ENABLE_NEW_ARCH=1 || export ENABLE_NEW_ARCH=0 [[ "${{ matrix.ios-use-frameworks }}" == "dynamic-frameworks" ]] && export USE_FRAMEWORKS=dynamic - echo "ENABLE_PROD=$ENABLE_PROD" - echo "ENABLE_NEW_ARCH=$ENABLE_NEW_ARCH" - PRODUCTION=$ENABLE_PROD RCT_NEW_ARCH_ENABLED=$ENABLE_NEW_ARCH bundle exec pod install - cat Podfile.lock | grep $RN_SENTRY_POD_NAME + + ./scripts/pod-install.sh - name: Build Android App if: ${{ matrix.platform == 'android' }} - working-directory: ${{ env.REACT_NATIVE_SAMPLE_PATH }}/android + working-directory: ${{ env.REACT_NATIVE_SAMPLE_PATH }} run: | - ../scripts/set-aos-dsn.mjs - - if [[ ${{ matrix.rn-architecture }} == 'new' ]]; then - perl -i -pe's/newArchEnabled=false/newArchEnabled=true/g' gradle.properties - echo 'New Architecture enabled' - elif [[ ${{ matrix.rn-architecture }} == 'legacy' ]]; then - perl -i -pe's/newArchEnabled=true/newArchEnabled=false/g' gradle.properties - echo 'Legacy Architecture enabled' - else - echo 'No changes for architecture: ${{ matrix.rn-architecture }}' - fi - [[ "${{ matrix.build-type }}" == "production" ]] && CONFIG='Release' || CONFIG='Debug' - echo "Building $CONFIG" - [[ "${{ matrix.build-type }}" == "production" ]] && TEST_TYPE='release' || TEST_TYPE='debug' - echo "Building $TEST_TYPE" + export RN_ARCHITECTURE="${{ matrix.rn-architecture }}" + [[ "${{ matrix.build-type }}" == "production" ]] && export CONFIG='release' || export CONFIG='debug' - ./gradlew ":app:assemble$CONFIG" app:assembleAndroidTest -DtestBuildType=$TEST_TYPE -PreactNativeArchitectures=x86 + ./scripts/set-dsn-aos.mjs + ./scripts/build-android.sh -PreactNativeArchitectures=x86 - name: Build iOS App if: ${{ matrix.platform == 'ios' }} - working-directory: ${{ env.REACT_NATIVE_SAMPLE_PATH }}/ios + working-directory: ${{ env.REACT_NATIVE_SAMPLE_PATH }} run: | - ../scripts/set-ios-dsn.mjs + [[ "${{ matrix.build-type }}" == "production" ]] && export CONFIG='Release' || export CONFIG='Debug' - [[ "${{ matrix.build-type }}" == "production" ]] && CONFIG='Release' || CONFIG='Debug' - echo "Building $CONFIG" - mkdir -p "DerivedData" - derivedData="$(cd "DerivedData" ; pwd -P)" - set -o pipefail && xcodebuild \ - -workspace sentryreactnativesample.xcworkspace \ - -configuration "$CONFIG" \ - -scheme sentryreactnativesample \ - -sdk 'iphonesimulator' \ - -destination 'generic/platform=iOS Simulator' \ - ONLY_ACTIVE_ARCH=yes \ - -derivedDataPath "$derivedData" \ - build \ - | tee xcodebuild.log \ - | xcbeautify --quieter --is-ci --disable-colored-output + ./scripts/set-dsn-ios.mjs + ./scripts/build-ios.sh - name: Build macOS App if: ${{ matrix.platform == 'macos' }} working-directory: samples/react-native-macos/macos run: | - [[ "${{ matrix.build-type }}" == "production" ]] && CONFIG='Release' || CONFIG='Debug' + [[ "${{ matrix.build-type }}" == "production" ]] && export CONFIG='Release' || export CONFIG='Debug' echo "Building $CONFIG" mkdir -p "DerivedData" derivedData="$(cd "DerivedData" ; pwd -P)" @@ -175,8 +148,8 @@ jobs: - name: Archive iOS App if: ${{ matrix.platform == 'ios' && matrix.rn-architecture == 'new' && matrix.build-type == 'production' && matrix.ios-use-frameworks == 'no-frameworks' }} + working-directory: ${{ env.REACT_NATIVE_SAMPLE_PATH }} run: | - cd ${{ env.REACT_NATIVE_SAMPLE_PATH }}/ios/DerivedData/Build/Products/Release-iphonesimulator zip -r \ ${{ github.workspace }}/${{ env.IOS_APP_ARCHIVE_PATH }} \ sentryreactnativesample.app @@ -184,12 +157,10 @@ jobs: - name: Archive Android App if: ${{ matrix.platform == 'android' && matrix.rn-architecture == 'new' && matrix.build-type == 'production' }} run: | - mv ${{ env.REACT_NATIVE_SAMPLE_PATH }}/android/app/build/outputs/apk/release/app-release.apk app.apk - mv ${{ env.REACT_NATIVE_SAMPLE_PATH }}/android/app/build/outputs/apk/androidTest/release/app-release-androidTest.apk app-androidTest.apk zip -j \ ${{ env.ANDROID_APP_ARCHIVE_PATH }} \ - app.apk \ - app-androidTest.apk + ${{ env.REACT_NATIVE_SAMPLE_PATH }}/app.apk \ + ${{ env.REACT_NATIVE_SAMPLE_PATH }}/app-androidTest.apk - name: Upload iOS APP if: ${{ matrix.platform == 'ios' && matrix.rn-architecture == 'new' && matrix.build-type == 'production' && matrix.ios-use-frameworks == 'no-frameworks' }} @@ -306,13 +277,14 @@ jobs: - name: Run Detox iOS Tests if: ${{ matrix.platform == 'ios' }} working-directory: ${{ env.REACT_NATIVE_SAMPLE_PATH }} - run: detox test --configuration ci.sim + run: yarn test-ios - name: Run tests on Android if: ${{ matrix.platform == 'android' }} env: # used by Detox ci.android configuration ANDROID_AVD_NAME: 'test' # test is default reactivecircus/android-emulator-runner name + ANDROID_TYPE: 'android.emulator' uses: reactivecircus/android-emulator-runner@62dbb605bba737720e10b196cb4220d374026a6d # pin@v2.33.0 with: api-level: ${{ env.ANDROID_API_LEVEL }} @@ -331,4 +303,4 @@ jobs: -camera-front none -timezone US/Pacific working-directory: ${{ env.REACT_NATIVE_SAMPLE_PATH }} - script: detox test --configuration ci.android + script: yarn test-android diff --git a/samples/react-native-macos/scripts/pod-install.sh b/samples/react-native-macos/scripts/pod-install.sh new file mode 100755 index 0000000000..a923f8c32a --- /dev/null +++ b/samples/react-native-macos/scripts/pod-install.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +# Exit on error +set -e + +thisFilePath=$(dirname "$0") + +echo "USE_FRAMEWORKS=$USE_FRAMEWORKS" +echo "ENABLE_PROD=$ENABLE_PROD" +echo "ENABLE_NEW_ARCH=$ENABLE_NEW_ARCH" + +cd "${thisFilePath}/.." +bundle install + +cd macos +PRODUCTION=$ENABLE_PROD RCT_NEW_ARCH_ENABLED=$ENABLE_NEW_ARCH bundle exec pod update + +cat Podfile.lock | grep $RN_SENTRY_POD_NAME diff --git a/samples/react-native/.detoxrc.js b/samples/react-native/.detoxrc.js index e6ba0d66cd..11c535ba65 100644 --- a/samples/react-native/.detoxrc.js +++ b/samples/react-native/.detoxrc.js @@ -1,56 +1,9 @@ const process = require('process'); -const testRunnerIos = { - args: { - $0: 'jest', - config: 'e2e/jest.config.ios.js', - }, - jest: { - setupTimeout: 120000, - }, -}; - -const testRunnerAos = { - args: { - $0: 'jest', - config: 'e2e/jest.config.android.js', - }, - jest: { - setupTimeout: 120000, - }, -}; - /** @type {Detox.DetoxConfig} */ module.exports = { testRunner: {}, apps: { - 'ios.debug': { - type: 'ios.app', - binaryPath: - 'ios/build/Build/Products/Debug-iphonesimulator/sentryreactnativesample.app', - build: - 'xcodebuild -workspace ios/sentryreactnativesample.xcworkspace -scheme sentryreactnativesample -configuration Debug -sdk iphonesimulator -derivedDataPath ios/build', - }, - 'ios.release': { - type: 'ios.app', - binaryPath: - 'ios/build/Build/Products/Release-iphonesimulator/sentryreactnativesample.app', - build: - 'xcodebuild -workspace ios/sentryreactnativesample.xcworkspace -scheme sentryreactnativesample -configuration Release -sdk iphonesimulator -derivedDataPath ios/build', - }, - 'android.debug': { - type: 'android.apk', - binaryPath: 'android/app/build/outputs/apk/debug/app-debug.apk', - build: - 'cd android && ./gradlew app:assembleDebug app:assembleAndroidTest -DtestBuildType=debug', - reversePorts: [8081], - }, - 'android.release': { - type: 'android.apk', - binaryPath: 'android/app/build/outputs/apk/release/app-release.apk', - build: - 'cd android && ./gradlew app:assembleRelease app:assembleAndroidTest -DtestBuildType=release', - }, 'ci.android': { type: 'android.apk', binaryPath: 'app.apk', @@ -62,78 +15,47 @@ module.exports = { }, }, devices: { - simulator: { - type: 'ios.simulator', - device: { - type: 'iPhone 16', - }, - }, - attached: { - type: 'android.attached', - device: { - adbName: '.*', - }, - }, - emulator: { - type: 'android.emulator', - device: { - avdName: 'Pixel_9_API_35', - }, - }, 'ci.emulator': { - type: 'android.emulator', + type: process.env.ANDROID_TYPE?.trim(), device: { - avdName: process.env.ANDROID_AVD_NAME, + avdName: process.env.ANDROID_AVD_NAME?.trim(), + adbName: process.env.ANDROID_ADB_NAME?.trim(), }, }, 'ci.simulator': { type: 'ios.simulator', device: { - type: process.env.IOS_DEVICE, - os: process.env.IOS_VERSION, + type: process.env.IOS_DEVICE?.trim(), + os: process.env.IOS_VERSION?.trim(), }, }, }, configurations: { - 'ios.sim.debug': { - device: 'simulator', - app: 'ios.debug', - testRunner: testRunnerIos, - }, - 'ios.sim.release': { - device: 'simulator', - app: 'ios.release', - testRunner: testRunnerIos, - }, - 'android.att.debug': { - device: 'attached', - app: 'android.debug', - testRunner: testRunnerAos, - }, - 'android.att.release': { - device: 'attached', - app: 'android.release', - testRunner: testRunnerAos, - }, - 'android.emu.debug': { - device: 'emulator', - app: 'android.debug', - testRunner: testRunnerAos, - }, - 'android.emu.release': { - device: 'emulator', - app: 'android.release', - testRunner: testRunnerAos, - }, 'ci.android': { device: 'ci.emulator', app: 'ci.android', - testRunner: testRunnerAos, + testRunner: { + args: { + $0: 'jest', + config: 'e2e/jest.config.android.js', + }, + jest: { + setupTimeout: 120000, + }, + }, }, 'ci.sim': { device: 'ci.simulator', app: 'ci.ios', - testRunner: testRunnerIos, + testRunner: { + args: { + $0: 'jest', + config: 'e2e/jest.config.ios.js', + }, + jest: { + setupTimeout: 120000, + }, + }, }, }, }; diff --git a/samples/react-native/.gitignore b/samples/react-native/.gitignore index 40824c4232..9eb5c6ab12 100644 --- a/samples/react-native/.gitignore +++ b/samples/react-native/.gitignore @@ -63,3 +63,5 @@ yarn-error.log .metro-health-check* *.xcarchive +*.apk +**/*.app diff --git a/samples/react-native/package.json b/samples/react-native/package.json index 3ba6ff8eb8..7cc3fecef9 100644 --- a/samples/react-native/package.json +++ b/samples/react-native/package.json @@ -4,16 +4,28 @@ "private": true, "scripts": { "postinstall": "patch-package", - "android": "react-native run-android", - "ios": "react-native run-ios", "start": "react-native start", + "build-android-release": "scripts/build-android-release.sh", + "build-android-release-legacy": "scripts/build-android-release-legacy.sh", + "build-android-debug": "scripts/build-android-debug.sh", + "build-android-debug-legacy": "scripts/build-android-debug-legacy.sh", + "build-ios-release": "scripts/build-ios-release.sh", + "build-ios-debug": "scripts/build-ios-debug.sh", "test": "jest", + "set-test-dsn-android": "scripts/set-dsn-aos.mjs", + "set-test-dsn-ios": "scripts/set-dsn-ios.mjs", + "test-android": "scripts/test-android.sh", + "test-ios": "scripts/test-ios.sh", "lint": "npx eslint . --ext .js,.jsx,.ts,.tsx", "fix": "eslint . --ext .js,.jsx,.ts,.tsx --fix", - "pod-install": "cd ios; RCT_NEW_ARCH_ENABLED=1 bundle exec pod install; cd ..", - "pod-install-production": "cd ios; PRODUCTION=1 RCT_NEW_ARCH_ENABLED=1 bundle exec pod install; cd ..", - "pod-install-legacy": "cd ios; bundle exec pod install; cd ..", - "pod-install-legacy-production": "cd ios; PRODUCTION=1 bundle exec pod install; cd ..", + "pod-install-debug-static": "scripts/pod-install-debug-static.sh", + "pod-install-debug-static-legacy": "scripts/pod-install-debug-static-legacy.sh", + "pod-install-debug-dynamic": "scripts/pod-install-debug-dynamic.sh", + "pod-install-debug-dynamic-legacy": "scripts/pod-install-debug-dynamic-legacy.sh", + "pod-install-release-static": "scripts/pod-install-release-static.sh", + "pod-install-release-static-legacy": "scripts/pod-install-release-static-legacy.sh", + "pod-install-release-dynamic": "scripts/pod-install-release-dynamic.sh", + "pod-install-release-dynamic-legacy": "scripts/pod-install-release-dynamic-legacy.sh", "clean-ios": "cd ios; rm -rf Podfile.lock Pods build; cd ..", "clean-watchman": "watchman watch-del-all", "set-build-number": "npx react-native-version --skip-tag --never-amend --set-build", diff --git a/samples/react-native/scripts/build-android-debug-legacy.sh b/samples/react-native/scripts/build-android-debug-legacy.sh new file mode 100755 index 0000000000..ac0952892d --- /dev/null +++ b/samples/react-native/scripts/build-android-debug-legacy.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +# Exit on error +set -e + +thisFilePath=$(dirname "$0") + +export RN_ARCHITECTURE="legacy" +export CONFIG="debug" + +"${thisFilePath}/build-android.sh" diff --git a/samples/react-native/scripts/build-android-debug.sh b/samples/react-native/scripts/build-android-debug.sh new file mode 100755 index 0000000000..89f9ae626c --- /dev/null +++ b/samples/react-native/scripts/build-android-debug.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +# Exit on error +set -e + +thisFilePath=$(dirname "$0") + +export RN_ARCHITECTURE="new" +export CONFIG="debug" + +"${thisFilePath}/build-android.sh" diff --git a/samples/react-native/scripts/build-android-release-legacy.sh b/samples/react-native/scripts/build-android-release-legacy.sh new file mode 100755 index 0000000000..cf853c15cc --- /dev/null +++ b/samples/react-native/scripts/build-android-release-legacy.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +# Exit on error +set -e + +thisFilePath=$(dirname "$0") + +export RN_ARCHITECTURE="legacy" +export CONFIG="release" + +"${thisFilePath}/build-android.sh" diff --git a/samples/react-native/scripts/build-android-release.sh b/samples/react-native/scripts/build-android-release.sh new file mode 100755 index 0000000000..3403a3c1bb --- /dev/null +++ b/samples/react-native/scripts/build-android-release.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +# Exit on error +set -e + +thisFilePath=$(dirname "$0") + +export RN_ARCHITECTURE="new" +export CONFIG="release" + +"${thisFilePath}/build-android.sh" diff --git a/samples/react-native/scripts/build-android.sh b/samples/react-native/scripts/build-android.sh new file mode 100755 index 0000000000..866b5cc130 --- /dev/null +++ b/samples/react-native/scripts/build-android.sh @@ -0,0 +1,29 @@ +#!/bin/bash + +# Exit on error and print commands +set -xe + +thisFilePath=$(dirname "$0") + +cd "${thisFilePath}/../android" + +rm -rf ../app.apk ../app-androidTest.apk + +if [[ "${RN_ARCHITECTURE}" == 'new' ]]; then + perl -i -pe's/newArchEnabled=false/newArchEnabled=true/g' gradle.properties + echo 'New Architecture enabled' +elif [[ "${RN_ARCHITECTURE}" == 'legacy' ]]; then + perl -i -pe's/newArchEnabled=true/newArchEnabled=false/g' gradle.properties + echo 'Legacy Architecture enabled' +else + echo "No changes for architecture: ${RN_ARCHITECTURE}" +fi + +echo "Building $CONFIG" + +assembleConfig=$(python -c "print(\"${CONFIG}\".capitalize())") + +./gradlew ":app:assemble${assembleConfig}" app:assembleAndroidTest -DtestBuildType=$CONFIG "$@" + +cp "app/build/outputs/apk/${CONFIG}/app-${CONFIG}.apk" ../app.apk +cp "app/build/outputs/apk/androidTest/${CONFIG}/app-${CONFIG}-androidTest.apk" ../app-androidTest.apk diff --git a/samples/react-native/scripts/build-ios-debug.sh b/samples/react-native/scripts/build-ios-debug.sh new file mode 100755 index 0000000000..088ed50f28 --- /dev/null +++ b/samples/react-native/scripts/build-ios-debug.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +# Exit on error and print commands +set -xe + +thisFilePath=$(dirname "$0") + +export CONFIG='Debug' + +"${thisFilePath}/build-ios.sh" diff --git a/samples/react-native/scripts/build-ios-release.sh b/samples/react-native/scripts/build-ios-release.sh new file mode 100755 index 0000000000..4a21d04c17 --- /dev/null +++ b/samples/react-native/scripts/build-ios-release.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +# Exit on error and print commands +set -xe + +thisFilePath=$(dirname "$0") + +export CONFIG='Release' + +"${thisFilePath}/build-ios.sh" diff --git a/samples/react-native/scripts/build-ios.sh b/samples/react-native/scripts/build-ios.sh new file mode 100755 index 0000000000..7897aba332 --- /dev/null +++ b/samples/react-native/scripts/build-ios.sh @@ -0,0 +1,30 @@ +#!/bin/bash + +# Exit on error and print commands +set -xe + +thisFilePath=$(dirname "$0") + +cd "${thisFilePath}/../ios" + +rm -rf ../sentryreactnativesample.app + +echo "Building $CONFIG" + +rm -rf xcodebuild.log + +mkdir -p "DerivedData" +derivedData="$(cd "DerivedData" ; pwd -P)" +set -o pipefail && xcodebuild \ + -workspace sentryreactnativesample.xcworkspace \ + -configuration "$CONFIG" \ + -scheme sentryreactnativesample \ + -sdk 'iphonesimulator' \ + -destination 'generic/platform=iOS Simulator' \ + ONLY_ACTIVE_ARCH=yes \ + -derivedDataPath "$derivedData" \ + build \ + | tee xcodebuild.log \ + | if [ "$CI" = "true" ]; then xcbeautify --quieter --is-ci --disable-colored-output; else xcbeautify; fi + +cp -r "DerivedData/Build/Products/${CONFIG}-iphonesimulator/sentryreactnativesample.app" ../sentryreactnativesample.app diff --git a/samples/react-native/scripts/pod-install-debug-dynamic-legacy.sh b/samples/react-native/scripts/pod-install-debug-dynamic-legacy.sh new file mode 100755 index 0000000000..cea9690bb0 --- /dev/null +++ b/samples/react-native/scripts/pod-install-debug-dynamic-legacy.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +# Exit on error +set -e + +export ENABLE_PROD=0 +export ENABLE_NEW_ARCH=0 +export USE_FRAMEWORKS=dynamic + +thisFilePath=$(dirname "$0") + +"${thisFilePath}/pod-install.sh" diff --git a/samples/react-native/scripts/pod-install-debug-dynamic.sh b/samples/react-native/scripts/pod-install-debug-dynamic.sh new file mode 100755 index 0000000000..ed3acafb8b --- /dev/null +++ b/samples/react-native/scripts/pod-install-debug-dynamic.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +# Exit on error +set -e + +export ENABLE_PROD=0 +export ENABLE_NEW_ARCH=1 +export USE_FRAMEWORKS=dynamic + +thisFilePath=$(dirname "$0") + +"${thisFilePath}/pod-install.sh" diff --git a/samples/react-native/scripts/pod-install-debug-static-legacy.sh b/samples/react-native/scripts/pod-install-debug-static-legacy.sh new file mode 100755 index 0000000000..52b80ba450 --- /dev/null +++ b/samples/react-native/scripts/pod-install-debug-static-legacy.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +# Exit on error +set -e + +export ENABLE_PROD=0 +export ENABLE_NEW_ARCH=0 +export USE_FRAMEWORKS=static + +thisFilePath=$(dirname "$0") + +"${thisFilePath}/pod-install.sh" diff --git a/samples/react-native/scripts/pod-install-debug-static.sh b/samples/react-native/scripts/pod-install-debug-static.sh new file mode 100755 index 0000000000..86049e4425 --- /dev/null +++ b/samples/react-native/scripts/pod-install-debug-static.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +# Exit on error +set -e + +export ENABLE_PROD=0 +export ENABLE_NEW_ARCH=1 +export USE_FRAMEWORKS=static + +thisFilePath=$(dirname "$0") + +"${thisFilePath}/pod-install.sh" diff --git a/samples/react-native/scripts/pod-install-release-dynamic-legacy.sh b/samples/react-native/scripts/pod-install-release-dynamic-legacy.sh new file mode 100755 index 0000000000..d6f7449abc --- /dev/null +++ b/samples/react-native/scripts/pod-install-release-dynamic-legacy.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +# Exit on error +set -e + +export ENABLE_PROD=1 +export ENABLE_NEW_ARCH=0 +export USE_FRAMEWORKS=dynamic + +thisFilePath=$(dirname "$0") + +"${thisFilePath}/pod-install.sh" diff --git a/samples/react-native/scripts/pod-install-release-dynamic.sh b/samples/react-native/scripts/pod-install-release-dynamic.sh new file mode 100755 index 0000000000..9207b45dfe --- /dev/null +++ b/samples/react-native/scripts/pod-install-release-dynamic.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +# Exit on error +set -e + +export ENABLE_PROD=1 +export ENABLE_NEW_ARCH=1 +export USE_FRAMEWORKS=dynamic + +thisFilePath=$(dirname "$0") + +"${thisFilePath}/pod-install.sh" diff --git a/samples/react-native/scripts/pod-install-release-static-legacy.sh b/samples/react-native/scripts/pod-install-release-static-legacy.sh new file mode 100755 index 0000000000..9742caa73a --- /dev/null +++ b/samples/react-native/scripts/pod-install-release-static-legacy.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +# Exit on error +set -e + +export ENABLE_PROD=1 +export ENABLE_NEW_ARCH=0 +export USE_FRAMEWORKS=static + +thisFilePath=$(dirname "$0") + +"${thisFilePath}/pod-install.sh" diff --git a/samples/react-native/scripts/pod-install-release-static.sh b/samples/react-native/scripts/pod-install-release-static.sh new file mode 100755 index 0000000000..8de5b13a61 --- /dev/null +++ b/samples/react-native/scripts/pod-install-release-static.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +# Exit on error +set -e + +export ENABLE_PROD=1 +export ENABLE_NEW_ARCH=1 +export USE_FRAMEWORKS=static + +thisFilePath=$(dirname "$0") + +"${thisFilePath}/pod-install.sh" diff --git a/samples/react-native/scripts/pod-install.sh b/samples/react-native/scripts/pod-install.sh new file mode 100755 index 0000000000..5d1ada6789 --- /dev/null +++ b/samples/react-native/scripts/pod-install.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +# Exit on error +set -e + +thisFilePath=$(dirname "$0") + +echo "USE_FRAMEWORKS=$USE_FRAMEWORKS" +echo "ENABLE_PROD=$ENABLE_PROD" +echo "ENABLE_NEW_ARCH=$ENABLE_NEW_ARCH" + +cd "${thisFilePath}/.." +bundle install + +cd ios +PRODUCTION=$ENABLE_PROD RCT_NEW_ARCH_ENABLED=$ENABLE_NEW_ARCH bundle exec pod update + +cat Podfile.lock | grep $RN_SENTRY_POD_NAME diff --git a/samples/react-native/scripts/set-aos-dsn.mjs b/samples/react-native/scripts/set-aos-dsn.mjs deleted file mode 100755 index 521cb44a11..0000000000 --- a/samples/react-native/scripts/set-aos-dsn.mjs +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env node - -import { setAndroidDsn } from './dsn.mjs'; - -setAndroidDsn(); diff --git a/samples/react-native/scripts/set-dsn-aos.mjs b/samples/react-native/scripts/set-dsn-aos.mjs new file mode 100755 index 0000000000..01bed2c984 --- /dev/null +++ b/samples/react-native/scripts/set-dsn-aos.mjs @@ -0,0 +1,5 @@ +#!/usr/bin/env node + +import { setAndroidDsn } from './set-dsn.mjs'; + +setAndroidDsn(); diff --git a/samples/react-native/scripts/set-dsn-ios.mjs b/samples/react-native/scripts/set-dsn-ios.mjs new file mode 100755 index 0000000000..7757c1e7b7 --- /dev/null +++ b/samples/react-native/scripts/set-dsn-ios.mjs @@ -0,0 +1,5 @@ +#!/usr/bin/env node + +import { setIosDsn } from './set-dsn.mjs'; + +setIosDsn(); diff --git a/samples/react-native/scripts/dsn.mjs b/samples/react-native/scripts/set-dsn.mjs similarity index 100% rename from samples/react-native/scripts/dsn.mjs rename to samples/react-native/scripts/set-dsn.mjs diff --git a/samples/react-native/scripts/set-ios-dsn.mjs b/samples/react-native/scripts/set-ios-dsn.mjs deleted file mode 100755 index ea1ca7e61a..0000000000 --- a/samples/react-native/scripts/set-ios-dsn.mjs +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env node - -import { setIosDsn } from './dsn.mjs'; - -setIosDsn(); diff --git a/samples/react-native/scripts/test-android.sh b/samples/react-native/scripts/test-android.sh new file mode 100755 index 0000000000..bbef32a81f --- /dev/null +++ b/samples/react-native/scripts/test-android.sh @@ -0,0 +1,43 @@ +#!/bin/bash + +# Exit on error and print commands +set -xe + +thisFilePath=$(dirname "$0") + +cd "${thisFilePath}/.." + +if [ -z "$ANDROID_AVD_NAME" ]; then + # Get the name of the first booted or connected Android device + DEVICE_NAME=$(adb devices | grep -w "device" | head -n 1 | cut -f 1) + + if [ -z "$DEVICE_NAME" ]; then + echo "No Android device or emulator found" + exit 1 + fi + + if [[ "$DEVICE_NAME" == *"emulator"* ]]; then + # Get the name of the first booted or connected Android emulator/device + EMULATOR_NAME=$(adb -s "${DEVICE_NAME}" emu avd name | head -n 1 | cut -f 1 ) + + if [ -z "$EMULATOR_NAME" ]; then + echo "No Android emulator found" + exit 1 + fi + + export ANDROID_TYPE="android.emulator" + export ANDROID_AVD_NAME="$EMULATOR_NAME" + echo "Using Android emulator: $EMULATOR_NAME" + else + export ANDROID_TYPE="android.attached" + export ANDROID_ADB_NAME="$DEVICE_NAME" + + adb reverse tcp:8081 tcp:8081 + adb reverse tcp:8961 tcp:8961 + + echo "Using Android device: $DEVICE_NAME" + fi +fi + +# Run the tests +detox test --configuration ci.android diff --git a/samples/react-native/scripts/test-ios.sh b/samples/react-native/scripts/test-ios.sh new file mode 100755 index 0000000000..ee9b1bb83f --- /dev/null +++ b/samples/react-native/scripts/test-ios.sh @@ -0,0 +1,24 @@ +#!/bin/bash + +# Exit on error and print commands +set -xe + +thisFilePath=$(dirname "$0") + +cd "${thisFilePath}/.." + +if [ -z "$IOS_DEVICE" ]; then + # Get the first booted simulator device type and version + BOOTED_DEVICE=$(xcrun simctl list devices | grep "Booted" | head -n 1) + + if [ -z "$BOOTED_DEVICE" ]; then + echo "No booted iOS simulator found" + exit 1 + fi + + # Extract device type from booted device + export IOS_DEVICE=$(echo "$BOOTED_DEVICE" | cut -d "(" -f1 | xargs) + echo "Using booted iOS simulator: $IOS_DEVICE" +fi + +detox test --configuration ci.sim diff --git a/samples/react-native/sentry.options.json b/samples/react-native/sentry.options.json index 53ae525bc0..58425c35d5 100644 --- a/samples/react-native/sentry.options.json +++ b/samples/react-native/sentry.options.json @@ -6,13 +6,13 @@ "enableAutoSessionTracking": true, "sessionTrackingIntervalMillis": 30000, "enableTracing": true, - "tracesSampleRate": 1.0, + "tracesSampleRate": 1, "attachStacktrace": true, "attachScreenshot": true, "attachViewHierarchy": true, "enableCaptureFailedRequests": true, - "profilesSampleRate": 1.0, - "replaysSessionSampleRate": 1.0, - "replaysOnErrorSampleRate": 1.0, + "profilesSampleRate": 1, + "replaysSessionSampleRate": 1, + "replaysOnErrorSampleRate": 1, "spotlight": true } From ae342a3a07de4a5e94f2658b5e3d1a4b8792967a Mon Sep 17 00:00:00 2001 From: Krystof Woldrich <31292499+krystofwoldrich@users.noreply.github.com> Date: Tue, 25 Feb 2025 12:46:16 +0100 Subject: [PATCH 18/32] test(e2e): Verify captured Errors Screen transaction (#4584) --- .../e2e/captureMessage.test.android.ts | 107 ++++++++- .../e2e/captureMessage.test.ios.ts | 90 +++++++- .../e2e/captureTransaction.test.ts | 205 ++++++++++++++++++ samples/react-native/e2e/utils/event.ts | 11 + .../e2e/utils/mockedSentryServer.ts | 46 +++- samples/react-native/e2e/utils/sleep.ts | 3 + 6 files changed, 448 insertions(+), 14 deletions(-) create mode 100644 samples/react-native/e2e/captureTransaction.test.ts create mode 100644 samples/react-native/e2e/utils/event.ts create mode 100644 samples/react-native/e2e/utils/sleep.ts diff --git a/samples/react-native/e2e/captureMessage.test.android.ts b/samples/react-native/e2e/captureMessage.test.android.ts index ad6d298a02..d08473ac2b 100644 --- a/samples/react-native/e2e/captureMessage.test.android.ts +++ b/samples/react-native/e2e/captureMessage.test.android.ts @@ -1,12 +1,12 @@ import { describe, it, beforeAll, expect, afterAll } from '@jest/globals'; -import { Envelope } from '@sentry/core'; +import { Envelope, EventItem } from '@sentry/core'; import { device } from 'detox'; import { createSentryServer, containingEvent, } from './utils/mockedSentryServer'; -import { HEADER, ITEMS } from './utils/consts'; import { tap } from './utils/tap'; +import { getItemOfTypeFrom } from './utils/event'; describe('Capture message', () => { let sentryServer = createSentryServer(); @@ -27,9 +27,7 @@ describe('Capture message', () => { }); it('envelope contains message event', async () => { - const item = (envelope[ITEMS] as [{ type?: string }, unknown][]).find( - i => i[HEADER].type === 'event', - ); + const item = getItemOfTypeFrom(envelope, 'event'); expect(item).toEqual([ { @@ -46,4 +44,103 @@ describe('Capture message', () => { }), ]); }); + + it('contains device context', async () => { + const item = getItemOfTypeFrom(envelope, 'event'); + + expect(item?.[1]).toEqual( + expect.objectContaining({ + contexts: expect.objectContaining({ + device: expect.objectContaining({ + battery_level: expect.any(Number), + battery_temperature: expect.any(Number), + boot_time: expect.any(String), + brand: expect.any(String), + charging: expect.any(Boolean), + connection_type: expect.any(String), + family: expect.any(String), + free_memory: expect.any(Number), + free_storage: expect.any(Number), + id: expect.any(String), + language: expect.any(String), + locale: expect.any(String), + low_memory: expect.any(Boolean), + manufacturer: expect.any(String), + memory_size: expect.any(Number), + model: expect.any(String), + model_id: expect.any(String), + online: expect.any(Boolean), + orientation: expect.any(String), + processor_count: expect.any(Number), + processor_frequency: expect.any(Number), + screen_density: expect.any(Number), + screen_dpi: expect.any(Number), + screen_height_pixels: expect.any(Number), + screen_width_pixels: expect.any(Number), + simulator: expect.any(Boolean), + storage_size: expect.any(Number), + timezone: expect.any(String), + }), + }), + }), + ); + }); + + it('contains app context', async () => { + const item = getItemOfTypeFrom(envelope, 'event'); + + expect(item?.[1]).toEqual( + expect.objectContaining({ + contexts: expect.objectContaining({ + app: expect.objectContaining({ + app_build: expect.any(String), + app_identifier: expect.any(String), + app_name: expect.any(String), + app_start_time: expect.any(String), + app_version: expect.any(String), + in_foreground: expect.any(Boolean), + view_names: ['ErrorsScreen'], + }), + }), + }), + ); + }); + + it('contains os context', async () => { + const item = getItemOfTypeFrom(envelope, 'event'); + + expect(item?.[1]).toEqual( + expect.objectContaining({ + contexts: expect.objectContaining({ + os: { + build: expect.any(String), + kernel_version: expect.any(String), + name: 'Android', + rooted: expect.any(Boolean), + version: expect.any(String), + }, + }), + }), + ); + }); + + it('contains react native context', async () => { + const item = getItemOfTypeFrom(envelope, 'event'); + + expect(item?.[1]).toEqual( + expect.objectContaining({ + contexts: expect.objectContaining({ + react_native_context: { + expo: false, + fabric: expect.any(Boolean), + hermes_debug_info: expect.any(Boolean), + hermes_version: expect.any(String), + js_engine: 'hermes', + react_native_version: expect.any(String), + turbo_module: expect.any(Boolean), + }, + }), + }), + ); + }); }); diff --git a/samples/react-native/e2e/captureMessage.test.ios.ts b/samples/react-native/e2e/captureMessage.test.ios.ts index a379718a68..9e3a804881 100644 --- a/samples/react-native/e2e/captureMessage.test.ios.ts +++ b/samples/react-native/e2e/captureMessage.test.ios.ts @@ -1,12 +1,12 @@ import { describe, it, beforeAll, expect, afterAll } from '@jest/globals'; -import { Envelope } from '@sentry/core'; +import { Envelope, EventItem } from '@sentry/core'; import { device } from 'detox'; import { createSentryServer, containingEvent, } from './utils/mockedSentryServer'; -import { HEADER, ITEMS } from './utils/consts'; import { tap } from './utils/tap'; +import { getItemOfTypeFrom } from './utils/event'; describe('Capture message', () => { let sentryServer = createSentryServer(); @@ -27,9 +27,7 @@ describe('Capture message', () => { }); it('envelope contains message event', async () => { - const item = (envelope[ITEMS] as [{ type?: string }, unknown][]).find( - i => i[HEADER].type === 'event', - ); + const item = getItemOfTypeFrom(envelope, 'event'); expect(item).toEqual([ { @@ -43,4 +41,86 @@ describe('Capture message', () => { }), ]); }); + + it('contains device context', async () => { + const item = getItemOfTypeFrom(envelope, 'event'); + + expect(item?.[1]).toEqual( + expect.objectContaining({ + contexts: expect.objectContaining({ + device: expect.objectContaining({ + arch: expect.any(String), + family: expect.any(String), + free_memory: expect.any(Number), + locale: expect.any(String), + memory_size: expect.any(Number), + model: expect.any(String), + model_id: expect.any(String), + processor_count: expect.any(Number), + simulator: expect.any(Boolean), + thermal_state: expect.any(String), + usable_memory: expect.any(Number), + }), + }), + }), + ); + }); + + it('contains app context', async () => { + const item = getItemOfTypeFrom(envelope, 'event'); + + expect(item?.[1]).toEqual( + expect.objectContaining({ + contexts: expect.objectContaining({ + app: expect.objectContaining({ + app_build: expect.any(String), + app_identifier: expect.any(String), + app_name: expect.any(String), + app_start_time: expect.any(String), + app_version: expect.any(String), + in_foreground: expect.any(Boolean), + // view_names: ['ErrorsScreen-jn5qquvH9Nz'], // TODO: fix this generated hash should not be part of the name + }), + }), + }), + ); + }); + + it('contains os context', async () => { + const item = getItemOfTypeFrom(envelope, 'event'); + + expect(item?.[1]).toEqual( + expect.objectContaining({ + contexts: expect.objectContaining({ + os: { + build: expect.any(String), + kernel_version: expect.any(String), + name: 'iOS', + rooted: expect.any(Boolean), + version: expect.any(String), + }, + }), + }), + ); + }); + + it('contains react native context', async () => { + const item = getItemOfTypeFrom(envelope, 'event'); + + expect(item?.[1]).toEqual( + expect.objectContaining({ + contexts: expect.objectContaining({ + react_native_context: { + expo: false, + fabric: expect.any(Boolean), + hermes_debug_info: expect.any(Boolean), + hermes_version: expect.any(String), + js_engine: 'hermes', + react_native_version: expect.any(String), + turbo_module: expect.any(Boolean), + }, + }), + }), + ); + }); }); diff --git a/samples/react-native/e2e/captureTransaction.test.ts b/samples/react-native/e2e/captureTransaction.test.ts new file mode 100644 index 0000000000..95d109c4e4 --- /dev/null +++ b/samples/react-native/e2e/captureTransaction.test.ts @@ -0,0 +1,205 @@ +import { describe, it, beforeAll, expect, afterAll } from '@jest/globals'; +import { EventItem } from '@sentry/core'; +import { device } from 'detox'; +import { + createSentryServer, + containingTransactionWithName, +} from './utils/mockedSentryServer'; +import { tap } from './utils/tap'; +import { sleep } from './utils/sleep'; +import { getItemOfTypeFrom } from './utils/event'; + +describe('Capture transaction', () => { + let sentryServer = createSentryServer(); + sentryServer.start(); + + const getErrorsEnvelope = () => + sentryServer.getEnvelope(containingTransactionWithName('Errors')); + + const getTrackerEnvelope = () => + sentryServer.getEnvelope(containingTransactionWithName('Tracker')); + + beforeAll(async () => { + await device.launchApp(); + + const waitForPerformanceTransaction = sentryServer.waitForEnvelope( + containingTransactionWithName('Tracker'), // The last created and sent transaction + ); + + await sleep(500); + await tap('Performance'); // Bottom tab + await sleep(200); + await tap('Auto Tracing Example'); // Screen with Full Display + + await waitForPerformanceTransaction; + }); + + afterAll(async () => { + await sentryServer.close(); + }); + + it('envelope contains transaction context', async () => { + const item = getItemOfTypeFrom( + getErrorsEnvelope(), + 'transaction', + ); + + expect(item).toEqual([ + expect.objectContaining({ + length: expect.any(Number), + type: 'transaction', + }), + expect.objectContaining({ + platform: 'javascript', + transaction: 'ErrorsScreen', + contexts: expect.objectContaining({ + trace: { + data: { + 'route.has_been_seen': false, + 'route.key': expect.stringMatching(/^ErrorsScreen/), + 'route.name': 'ErrorsScreen', + 'sentry.idle_span_finish_reason': 'idleTimeout', + 'sentry.op': 'ui.load', + 'sentry.origin': 'auto.app.start', + 'sentry.sample_rate': 1, + 'sentry.source': 'component', + }, + op: 'ui.load', + origin: 'auto.app.start', + span_id: expect.any(String), + trace_id: expect.any(String), + }, + }), + }), + ]); + }); + + it('contains app start measurements', async () => { + const item = getItemOfTypeFrom( + getErrorsEnvelope(), + 'transaction', + ); + + expect( + item?.[1].measurements?.app_start_warm || + item?.[1].measurements?.app_start_cold, + ).toBeDefined(); + expect(item?.[1]).toEqual( + expect.objectContaining({ + measurements: expect.objectContaining({ + time_to_initial_display: { + unit: 'millisecond', + value: expect.any(Number), + }, + // Expect warm or cold app start measurements + ...(item?.[1].measurements?.app_start_warm && { + app_start_warm: { + unit: 'millisecond', + value: expect.any(Number), + }, + }), + ...(item?.[1].measurements?.app_start_cold && { + app_start_cold: { + unit: 'millisecond', + value: expect.any(Number), + }, + }), + }), + }), + ); + }); + + it('contains time to initial display measurements', async () => { + const item = getItemOfTypeFrom( + await getErrorsEnvelope(), + 'transaction', + ); + + expect(item?.[1]).toEqual( + expect.objectContaining({ + measurements: expect.objectContaining({ + time_to_initial_display: { + unit: 'millisecond', + value: expect.any(Number), + }, + }), + }), + ); + }); + + it('contains JS stall measurements', async () => { + const item = getItemOfTypeFrom( + await getErrorsEnvelope(), + 'transaction', + ); + + expect(item?.[1]).toEqual( + expect.objectContaining({ + measurements: expect.objectContaining({ + stall_count: { + unit: 'none', + value: expect.any(Number), + }, + stall_longest_time: { + unit: 'millisecond', + value: expect.any(Number), + }, + stall_total_time: { + unit: 'millisecond', + value: expect.any(Number), + }, + }), + }), + ); + }); + + it('contains time to display measurements', async () => { + const item = getItemOfTypeFrom( + getTrackerEnvelope(), + 'transaction', + ); + + expect(item?.[1]).toEqual( + expect.objectContaining({ + measurements: expect.objectContaining({ + time_to_initial_display: { + unit: 'millisecond', + value: expect.any(Number), + }, + time_to_full_display: { + unit: 'millisecond', + value: expect.any(Number), + }, + }), + }), + ); + }); + + it('contains at least one xhr breadcrumb of request to the tracker endpoint', async () => { + const item = getItemOfTypeFrom( + getTrackerEnvelope(), + 'transaction', + ); + + expect(item?.[1]).toEqual( + expect.objectContaining({ + breadcrumbs: expect.arrayContaining([ + expect.objectContaining({ + category: 'xhr', + data: { + end_timestamp: expect.any(Number), + method: 'GET', + response_body_size: expect.any(Number), + start_timestamp: expect.any(Number), + status_code: expect.any(Number), + url: expect.stringContaining('api.covid19api.com/summary'), + }, + level: 'info', + timestamp: expect.any(Number), + type: 'http', + }), + ]), + }), + ); + }); +}); diff --git a/samples/react-native/e2e/utils/event.ts b/samples/react-native/e2e/utils/event.ts new file mode 100644 index 0000000000..df631feb4e --- /dev/null +++ b/samples/react-native/e2e/utils/event.ts @@ -0,0 +1,11 @@ +import { Envelope, EnvelopeItem } from '@sentry/core'; +import { HEADER, ITEMS } from './consts'; + +export function getItemOfTypeFrom( + envelope: Envelope, + type: string, +): T | undefined { + return (envelope[ITEMS] as [{ type?: string }, unknown][]).find( + i => i[HEADER].type === type, + ) as T | undefined; +} diff --git a/samples/react-native/e2e/utils/mockedSentryServer.ts b/samples/react-native/e2e/utils/mockedSentryServer.ts index 40667b0f9d..7f19a0d24b 100644 --- a/samples/react-native/e2e/utils/mockedSentryServer.ts +++ b/samples/react-native/e2e/utils/mockedSentryServer.ts @@ -1,7 +1,8 @@ import { IncomingMessage, ServerResponse, createServer } from 'node:http'; import { createGunzip } from 'node:zlib'; -import { Envelope } from '@sentry/core'; +import { Envelope, EnvelopeItem } from '@sentry/core'; import { parseEnvelope } from './parseEnvelope'; +import { Event } from '@sentry/core'; type RecordedRequest = { path: string | undefined; @@ -16,6 +17,7 @@ export function createSentryServer({ port = 8961 } = {}): { ) => Promise; close: () => Promise; start: () => void; + getEnvelope: (predicate: (envelope: Envelope) => boolean) => Envelope; } { let onNextRequestCallback: (request: RecordedRequest) => void = () => {}; const requests: RecordedRequest[] = []; @@ -74,11 +76,47 @@ export function createSentryServer({ port = 8961 } = {}): { server.close(() => resolve()); }); }, + getEnvelope: (predicate: (envelope: Envelope) => boolean) => { + const envelope = requests.find( + request => request.envelope && predicate(request.envelope), + )?.envelope; + + if (!envelope) { + throw new Error('Envelope not found'); + } + + return envelope; + }, }; } export function containingEvent(envelope: Envelope) { - return envelope[1].some( - item => (item[0] as { type?: string }).type === 'event', - ); + return envelope[1].some(item => itemHeaderIsType(item[0], 'event')); +} + +export function containingTransactionWithName(name: string) { + return (envelope: Envelope) => + envelope[1].some( + item => + itemHeaderIsType(item[0], 'transaction') && + itemBodyIsEvent(item[1]) && + item[1].transaction && + item[1].transaction.includes(name), + ); +} + +export function itemBodyIsEvent(itemBody: EnvelopeItem[1]): itemBody is Event { + return typeof itemBody === 'object' && 'event_id' in itemBody; +} + +export function itemHeaderIsType(itemHeader: EnvelopeItem[0], type: string) { + if (typeof itemHeader !== 'object' || !('type' in itemHeader)) { + return false; + } + + if (itemHeader.type !== type) { + return false; + } + + return true; } diff --git a/samples/react-native/e2e/utils/sleep.ts b/samples/react-native/e2e/utils/sleep.ts new file mode 100644 index 0000000000..a3b7734163 --- /dev/null +++ b/samples/react-native/e2e/utils/sleep.ts @@ -0,0 +1,3 @@ +export function sleep(ms: number) { + return new Promise(resolve => setTimeout(resolve, ms)); +} From 770f9fb9f1646c97e7b356bb7b05c75dc3d947b4 Mon Sep 17 00:00:00 2001 From: Krystof Woldrich <31292499+krystofwoldrich@users.noreply.github.com> Date: Tue, 25 Feb 2025 14:25:24 +0100 Subject: [PATCH 19/32] test(e2e): Add auto init from JS tests (#4588) --- .github/workflows/sample-application.yml | 22 ++++++++++++++----- samples/react-native/package.json | 1 + .../react-native/scripts/detect-ios-sim.sh | 18 +++++++++++++++ samples/react-native/scripts/test-ios-auto.sh | 12 ++++++++++ samples/react-native/scripts/test-ios.sh | 14 +----------- 5 files changed, 49 insertions(+), 18 deletions(-) create mode 100755 samples/react-native/scripts/detect-ios-sim.sh create mode 100755 samples/react-native/scripts/test-ios-auto.sh diff --git a/.github/workflows/sample-application.yml b/.github/workflows/sample-application.yml index 603cafa619..3bfa290fe3 100644 --- a/.github/workflows/sample-application.yml +++ b/.github/workflows/sample-application.yml @@ -186,7 +186,7 @@ jobs: path: ${{ env.REACT_NATIVE_SAMPLE_PATH }}/${{ matrix.platform }}/*.log test: - name: Test ${{ matrix.platform }} ${{ matrix.build-type }} + name: ${{ matrix.job-name }} runs-on: ${{ matrix.runs-on }} needs: [diff_check, build] if: ${{ needs.diff_check.outputs.skip_ci != 'true' }} @@ -195,16 +195,28 @@ jobs: fail-fast: false matrix: include: - - platform: ios + - job-name: 'Test iOS Release Auto Init' + platform: ios runs-on: macos-15 rn-architecture: 'new' ios-use-frameworks: 'no-frameworks' build-type: 'production' + test-command: 'yarn test-ios-auto' # tests native auto init from JS - - platform: android + - job-name: 'Test iOS Release Manual Init' + platform: ios + runs-on: macos-15 + rn-architecture: 'new' + ios-use-frameworks: 'no-frameworks' + build-type: 'production' + test-command: 'yarn test-ios' + + - job-name: 'Test Android Release Manual Init' + platform: android runs-on: ubuntu-latest rn-architecture: 'new' build-type: 'production' + test-command: 'yarn test-android' steps: - uses: actions/checkout@v4 @@ -277,7 +289,7 @@ jobs: - name: Run Detox iOS Tests if: ${{ matrix.platform == 'ios' }} working-directory: ${{ env.REACT_NATIVE_SAMPLE_PATH }} - run: yarn test-ios + run: ${{ matrix.test-command }} - name: Run tests on Android if: ${{ matrix.platform == 'android' }} @@ -303,4 +315,4 @@ jobs: -camera-front none -timezone US/Pacific working-directory: ${{ env.REACT_NATIVE_SAMPLE_PATH }} - script: yarn test-android + script: ${{ matrix.test-command }} diff --git a/samples/react-native/package.json b/samples/react-native/package.json index fb1ddbcd67..a5b36afd9d 100644 --- a/samples/react-native/package.json +++ b/samples/react-native/package.json @@ -16,6 +16,7 @@ "set-test-dsn-ios": "scripts/set-dsn-ios.mjs", "test-android": "scripts/test-android.sh", "test-ios": "scripts/test-ios.sh", + "test-ios-auto": "scripts/test-ios-auto.sh", "lint": "npx eslint . --ext .js,.jsx,.ts,.tsx", "fix": "eslint . --ext .js,.jsx,.ts,.tsx --fix", "pod-install-debug-static": "scripts/pod-install-debug-static.sh", diff --git a/samples/react-native/scripts/detect-ios-sim.sh b/samples/react-native/scripts/detect-ios-sim.sh new file mode 100755 index 0000000000..1ba6d3b98c --- /dev/null +++ b/samples/react-native/scripts/detect-ios-sim.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +# Exit on error and print commands +set -xe + +if [ -z "$IOS_DEVICE" ]; then + # Get the first booted simulator device type and version + BOOTED_DEVICE=$(xcrun simctl list devices | grep "Booted" | head -n 1) + + if [ -z "$BOOTED_DEVICE" ]; then + echo "No booted iOS simulator found" + exit 1 + fi + + # Extract device type from booted device + export IOS_DEVICE=$(echo "$BOOTED_DEVICE" | cut -d "(" -f1 | xargs) + echo "Using booted iOS simulator: $IOS_DEVICE" +fi diff --git a/samples/react-native/scripts/test-ios-auto.sh b/samples/react-native/scripts/test-ios-auto.sh new file mode 100755 index 0000000000..5798457ec2 --- /dev/null +++ b/samples/react-native/scripts/test-ios-auto.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +# Exit on error and print commands +set -xe + +thisFilePath=$(dirname "$0") + +cd "${thisFilePath}/.." + +"${thisFilePath}/detect-ios-sim.sh" + +detox test --configuration ci.sim --app-launch-args="--sentry-disable-native-start" diff --git a/samples/react-native/scripts/test-ios.sh b/samples/react-native/scripts/test-ios.sh index ee9b1bb83f..3e8d8c64a3 100755 --- a/samples/react-native/scripts/test-ios.sh +++ b/samples/react-native/scripts/test-ios.sh @@ -7,18 +7,6 @@ thisFilePath=$(dirname "$0") cd "${thisFilePath}/.." -if [ -z "$IOS_DEVICE" ]; then - # Get the first booted simulator device type and version - BOOTED_DEVICE=$(xcrun simctl list devices | grep "Booted" | head -n 1) - - if [ -z "$BOOTED_DEVICE" ]; then - echo "No booted iOS simulator found" - exit 1 - fi - - # Extract device type from booted device - export IOS_DEVICE=$(echo "$BOOTED_DEVICE" | cut -d "(" -f1 | xargs) - echo "Using booted iOS simulator: $IOS_DEVICE" -fi +"${thisFilePath}/detect-ios-sim.sh" detox test --configuration ci.sim From cbb85b2d556db8b81de01850caed0b60853ca76c Mon Sep 17 00:00:00 2001 From: Krystof Woldrich <31292499+krystofwoldrich@users.noreply.github.com> Date: Tue, 25 Feb 2025 17:07:39 +0100 Subject: [PATCH 20/32] test(e2e): Add app start crash test for iOS (#4593) --- .github/workflows/sample-application.yml | 2 +- samples/react-native/.detoxrc.js | 17 ++- .../captureAppStartCrash.test.ios.manual.ts | 121 ++++++++++++++++++ .../react-native/e2e/jest.config.android.js | 17 +-- .../{jest.config.js => jest.config.base.js} | 0 .../react-native/e2e/jest.config.ios.auto.js | 11 ++ samples/react-native/e2e/jest.config.ios.js | 13 -- .../e2e/jest.config.ios.manual.js | 11 ++ .../sentryreactnativesample.xcscheme | 4 + .../sentryreactnativesample/AppDelegate.mm | 12 ++ samples/react-native/package.json | 2 +- samples/react-native/scripts/test-ios-auto.sh | 2 +- .../{test-ios.sh => test-ios-manual.sh} | 2 +- 13 files changed, 182 insertions(+), 32 deletions(-) create mode 100644 samples/react-native/e2e/captureAppStartCrash.test.ios.manual.ts rename samples/react-native/e2e/{jest.config.js => jest.config.base.js} (100%) create mode 100644 samples/react-native/e2e/jest.config.ios.auto.js delete mode 100644 samples/react-native/e2e/jest.config.ios.js create mode 100644 samples/react-native/e2e/jest.config.ios.manual.js rename samples/react-native/scripts/{test-ios.sh => test-ios-manual.sh} (78%) diff --git a/.github/workflows/sample-application.yml b/.github/workflows/sample-application.yml index 3bfa290fe3..0c697f22e5 100644 --- a/.github/workflows/sample-application.yml +++ b/.github/workflows/sample-application.yml @@ -209,7 +209,7 @@ jobs: rn-architecture: 'new' ios-use-frameworks: 'no-frameworks' build-type: 'production' - test-command: 'yarn test-ios' + test-command: 'yarn test-ios-manual' - job-name: 'Test Android Release Manual Init' platform: android diff --git a/samples/react-native/.detoxrc.js b/samples/react-native/.detoxrc.js index 11c535ba65..a3501515a9 100644 --- a/samples/react-native/.detoxrc.js +++ b/samples/react-native/.detoxrc.js @@ -44,13 +44,26 @@ module.exports = { }, }, }, - 'ci.sim': { + 'ci.sim.auto': { device: 'ci.simulator', app: 'ci.ios', testRunner: { args: { $0: 'jest', - config: 'e2e/jest.config.ios.js', + config: 'e2e/jest.config.ios.auto.js', + }, + jest: { + setupTimeout: 120000, + }, + }, + }, + 'ci.sim.manual': { + device: 'ci.simulator', + app: 'ci.ios', + testRunner: { + args: { + $0: 'jest', + config: 'e2e/jest.config.ios.manual.js', }, jest: { setupTimeout: 120000, diff --git a/samples/react-native/e2e/captureAppStartCrash.test.ios.manual.ts b/samples/react-native/e2e/captureAppStartCrash.test.ios.manual.ts new file mode 100644 index 0000000000..ea6750434e --- /dev/null +++ b/samples/react-native/e2e/captureAppStartCrash.test.ios.manual.ts @@ -0,0 +1,121 @@ +import { describe, it, beforeAll, expect, afterAll } from '@jest/globals'; +import { Envelope, EventItem } from '@sentry/core'; +import { device } from 'detox'; +import { + createSentryServer, + containingEvent, +} from './utils/mockedSentryServer'; +import { getItemOfTypeFrom } from './utils/event'; + +describe('Capture app start crash', () => { + let sentryServer = createSentryServer(); + sentryServer.start(); + + let envelope: Envelope; + + beforeAll(async () => { + const launchConfig = { + launchArgs: { + 0: '--sentry-crash-on-start', + }, + }; + + const envelopePromise = sentryServer.waitForEnvelope(containingEvent); + + device.launchApp(launchConfig); + device.launchApp(launchConfig); // This launch sends the crash event before the app crashes again + + envelope = await envelopePromise; + }); + + afterAll(async () => { + await sentryServer.close(); + }); + + it('envelope contains sdk metadata', async () => { + const item = getItemOfTypeFrom(envelope, 'event'); + + expect(item).toEqual([ + { + length: expect.any(Number), + type: 'event', + }, + expect.objectContaining({ + platform: 'cocoa', + sdk: { + features: [], + integrations: [ + 'SessionReplay', + 'WatchdogTerminationTracking', + 'Screenshot', + 'Crash', + 'ANRTracking', + 'ViewHierarchy', + 'AutoBreadcrumbTracking', + 'AutoSessionTracking', + 'NetworkTracking', + 'AppStartTracking', + 'FramesTracking', + ], + name: 'sentry.cocoa.react-native', + packages: [ + { + name: 'cocoapods:getsentry/sentry.cocoa.react-native', + version: expect.any(String), + }, + { + name: 'npm:@sentry/react-native', + version: expect.any(String), + }, + ], + version: expect.any(String), + }, + tags: { + 'event.environment': 'native', + 'event.origin': 'ios', + }, + }), + ]); + }); + + it('envelope contains the expected exception', async () => { + const item = getItemOfTypeFrom(envelope, 'event'); + + expect(item).toEqual([ + { + length: expect.any(Number), + type: 'event', + }, + expect.objectContaining({ + exception: { + values: expect.arrayContaining([ + expect.objectContaining({ + mechanism: expect.objectContaining({ + handled: false, + meta: { + mach_exception: { + code: 0, + exception: 10, + name: 'EXC_CRASH', + subcode: 0, + }, + signal: { + code: 0, + name: 'SIGABRT', + number: 6, + }, + }, + type: 'nsexception', + }), + stacktrace: expect.objectContaining({ + frames: expect.any(Array), + }), + type: 'CrashOnStart', + value: 'This was intentional test crash before JS started.', + }), + ]), + }, + }), + ]); + }); +}); diff --git a/samples/react-native/e2e/jest.config.android.js b/samples/react-native/e2e/jest.config.android.js index 2d755851a0..a07210b212 100644 --- a/samples/react-native/e2e/jest.config.android.js +++ b/samples/react-native/e2e/jest.config.android.js @@ -1,16 +1,7 @@ +const baseConfig = require('./jest.config.base'); + /** @type {import('@jest/types').Config.InitialOptions} */ module.exports = { - preset: 'ts-jest', - rootDir: '..', - testMatch: [ - '/e2e/**/*.test.ts', - '/e2e/**/*.test.android.ts', - ], - testTimeout: 120000, - maxWorkers: 1, - globalSetup: 'detox/runners/jest/globalSetup', - globalTeardown: 'detox/runners/jest/globalTeardown', - reporters: ['detox/runners/jest/reporter'], - testEnvironment: 'detox/runners/jest/testEnvironment', - verbose: true, + ...baseConfig, + testMatch: [...baseConfig.testMatch, '/e2e/**/*.test.android.ts'], }; diff --git a/samples/react-native/e2e/jest.config.js b/samples/react-native/e2e/jest.config.base.js similarity index 100% rename from samples/react-native/e2e/jest.config.js rename to samples/react-native/e2e/jest.config.base.js diff --git a/samples/react-native/e2e/jest.config.ios.auto.js b/samples/react-native/e2e/jest.config.ios.auto.js new file mode 100644 index 0000000000..ebfadcaeb6 --- /dev/null +++ b/samples/react-native/e2e/jest.config.ios.auto.js @@ -0,0 +1,11 @@ +const baseConfig = require('./jest.config.base'); + +/** @type {import('@jest/types').Config.InitialOptions} */ +module.exports = { + ...baseConfig, + testMatch: [ + ...baseConfig.testMatch, + '/e2e/**/*.test.ios.ts', + '/e2e/**/*.test.ios.auto.ts', + ], +}; diff --git a/samples/react-native/e2e/jest.config.ios.js b/samples/react-native/e2e/jest.config.ios.js deleted file mode 100644 index 62034bc390..0000000000 --- a/samples/react-native/e2e/jest.config.ios.js +++ /dev/null @@ -1,13 +0,0 @@ -/** @type {import('@jest/types').Config.InitialOptions} */ -module.exports = { - preset: 'ts-jest', - rootDir: '..', - testMatch: ['/e2e/**/*.test.ts', '/e2e/**/*.test.ios.ts'], - testTimeout: 120000, - maxWorkers: 1, - globalSetup: 'detox/runners/jest/globalSetup', - globalTeardown: 'detox/runners/jest/globalTeardown', - reporters: ['detox/runners/jest/reporter'], - testEnvironment: 'detox/runners/jest/testEnvironment', - verbose: true, -}; diff --git a/samples/react-native/e2e/jest.config.ios.manual.js b/samples/react-native/e2e/jest.config.ios.manual.js new file mode 100644 index 0000000000..fe271fca7d --- /dev/null +++ b/samples/react-native/e2e/jest.config.ios.manual.js @@ -0,0 +1,11 @@ +const baseConfig = require('./jest.config.base'); + +/** @type {import('@jest/types').Config.InitialOptions} */ +module.exports = { + ...baseConfig, + testMatch: [ + ...baseConfig.testMatch, + '/e2e/**/*.test.ios.ts', + '/e2e/**/*.test.ios.manual.ts', + ], +}; diff --git a/samples/react-native/ios/sentryreactnativesample.xcodeproj/xcshareddata/xcschemes/sentryreactnativesample.xcscheme b/samples/react-native/ios/sentryreactnativesample.xcodeproj/xcshareddata/xcschemes/sentryreactnativesample.xcscheme index 61b12d2c2c..1b6c7a3f15 100644 --- a/samples/react-native/ios/sentryreactnativesample.xcodeproj/xcshareddata/xcschemes/sentryreactnativesample.xcscheme +++ b/samples/react-native/ios/sentryreactnativesample.xcodeproj/xcshareddata/xcschemes/sentryreactnativesample.xcscheme @@ -65,6 +65,10 @@ argument = "--sentry-disable-native-start" isEnabled = "NO"> + + *arguments = [[NSProcessInfo processInfo] arguments]; + return [arguments containsObject:@"--sentry-crash-on-start"]; +} + @end diff --git a/samples/react-native/package.json b/samples/react-native/package.json index a5b36afd9d..8e80a5d598 100644 --- a/samples/react-native/package.json +++ b/samples/react-native/package.json @@ -15,7 +15,7 @@ "set-test-dsn-android": "scripts/set-dsn-aos.mjs", "set-test-dsn-ios": "scripts/set-dsn-ios.mjs", "test-android": "scripts/test-android.sh", - "test-ios": "scripts/test-ios.sh", + "test-ios-manual": "scripts/test-ios-manual.sh", "test-ios-auto": "scripts/test-ios-auto.sh", "lint": "npx eslint . --ext .js,.jsx,.ts,.tsx", "fix": "eslint . --ext .js,.jsx,.ts,.tsx --fix", diff --git a/samples/react-native/scripts/test-ios-auto.sh b/samples/react-native/scripts/test-ios-auto.sh index 5798457ec2..9192a9619e 100755 --- a/samples/react-native/scripts/test-ios-auto.sh +++ b/samples/react-native/scripts/test-ios-auto.sh @@ -9,4 +9,4 @@ cd "${thisFilePath}/.." "${thisFilePath}/detect-ios-sim.sh" -detox test --configuration ci.sim --app-launch-args="--sentry-disable-native-start" +detox test --configuration ci.sim.auto --app-launch-args="--sentry-disable-native-start" diff --git a/samples/react-native/scripts/test-ios.sh b/samples/react-native/scripts/test-ios-manual.sh similarity index 78% rename from samples/react-native/scripts/test-ios.sh rename to samples/react-native/scripts/test-ios-manual.sh index 3e8d8c64a3..b1c295edda 100755 --- a/samples/react-native/scripts/test-ios.sh +++ b/samples/react-native/scripts/test-ios-manual.sh @@ -9,4 +9,4 @@ cd "${thisFilePath}/.." "${thisFilePath}/detect-ios-sim.sh" -detox test --configuration ci.sim +detox test --configuration ci.sim.manual From b4ee16bebfd73b4edd90929a360e0062529fcc8f Mon Sep 17 00:00:00 2001 From: Krystof Woldrich <31292499+krystofwoldrich@users.noreply.github.com> Date: Wed, 26 Feb 2025 13:13:17 +0100 Subject: [PATCH 21/32] test(e2e): Avoid race conditions when waiting for captured message (#4595) --- .../e2e/captureMessage.test.android.ts | 6 ++++-- .../e2e/captureMessage.test.ios.ts | 6 ++++-- .../e2e/envelopeHeader.test.android.ts | 6 ++++-- .../e2e/envelopeHeader.test.ios.ts | 6 ++++-- .../e2e/utils/mockedSentryServer.ts | 21 +++++++++++++++++++ 5 files changed, 37 insertions(+), 8 deletions(-) diff --git a/samples/react-native/e2e/captureMessage.test.android.ts b/samples/react-native/e2e/captureMessage.test.android.ts index d08473ac2b..c48d9553a5 100644 --- a/samples/react-native/e2e/captureMessage.test.android.ts +++ b/samples/react-native/e2e/captureMessage.test.android.ts @@ -3,7 +3,7 @@ import { Envelope, EventItem } from '@sentry/core'; import { device } from 'detox'; import { createSentryServer, - containingEvent, + containingEventWithAndroidMessage, } from './utils/mockedSentryServer'; import { tap } from './utils/tap'; import { getItemOfTypeFrom } from './utils/event'; @@ -17,7 +17,9 @@ describe('Capture message', () => { beforeAll(async () => { await device.launchApp(); - const envelopePromise = sentryServer.waitForEnvelope(containingEvent); + const envelopePromise = sentryServer.waitForEnvelope( + containingEventWithAndroidMessage('Captured message'), + ); await tap('Capture message'); envelope = await envelopePromise; }); diff --git a/samples/react-native/e2e/captureMessage.test.ios.ts b/samples/react-native/e2e/captureMessage.test.ios.ts index 9e3a804881..40cfcf20c1 100644 --- a/samples/react-native/e2e/captureMessage.test.ios.ts +++ b/samples/react-native/e2e/captureMessage.test.ios.ts @@ -3,7 +3,7 @@ import { Envelope, EventItem } from '@sentry/core'; import { device } from 'detox'; import { createSentryServer, - containingEvent, + containingEventWithMessage, } from './utils/mockedSentryServer'; import { tap } from './utils/tap'; import { getItemOfTypeFrom } from './utils/event'; @@ -17,7 +17,9 @@ describe('Capture message', () => { beforeAll(async () => { await device.launchApp(); - const envelopePromise = sentryServer.waitForEnvelope(containingEvent); + const envelopePromise = sentryServer.waitForEnvelope( + containingEventWithMessage('Captured message'), + ); await tap('Capture message'); envelope = await envelopePromise; }); diff --git a/samples/react-native/e2e/envelopeHeader.test.android.ts b/samples/react-native/e2e/envelopeHeader.test.android.ts index 4be175d6c9..dc4a75676d 100644 --- a/samples/react-native/e2e/envelopeHeader.test.android.ts +++ b/samples/react-native/e2e/envelopeHeader.test.android.ts @@ -3,7 +3,7 @@ import { Envelope } from '@sentry/core'; import { device } from 'detox'; import { createSentryServer, - containingEvent, + containingEventWithAndroidMessage, } from './utils/mockedSentryServer'; import { HEADER } from './utils/consts'; import { tap } from './utils/tap'; @@ -17,7 +17,9 @@ describe('Capture message', () => { beforeAll(async () => { await device.launchApp(); - const envelopePromise = sentryServer.waitForEnvelope(containingEvent); + const envelopePromise = sentryServer.waitForEnvelope( + containingEventWithAndroidMessage('Captured message'), + ); await tap('Capture message'); envelope = await envelopePromise; diff --git a/samples/react-native/e2e/envelopeHeader.test.ios.ts b/samples/react-native/e2e/envelopeHeader.test.ios.ts index 04ce534226..5798b07d5d 100644 --- a/samples/react-native/e2e/envelopeHeader.test.ios.ts +++ b/samples/react-native/e2e/envelopeHeader.test.ios.ts @@ -3,7 +3,7 @@ import { Envelope } from '@sentry/core'; import { device } from 'detox'; import { createSentryServer, - containingEvent, + containingEventWithMessage, } from './utils/mockedSentryServer'; import { HEADER } from './utils/consts'; import { tap } from './utils/tap'; @@ -17,7 +17,9 @@ describe('Capture message', () => { beforeAll(async () => { await device.launchApp(); - const envelopePromise = sentryServer.waitForEnvelope(containingEvent); + const envelopePromise = sentryServer.waitForEnvelope( + containingEventWithMessage('Captured message'), + ); await tap('Capture message'); envelope = await envelopePromise; diff --git a/samples/react-native/e2e/utils/mockedSentryServer.ts b/samples/react-native/e2e/utils/mockedSentryServer.ts index 7f19a0d24b..9c738ce6a5 100644 --- a/samples/react-native/e2e/utils/mockedSentryServer.ts +++ b/samples/react-native/e2e/utils/mockedSentryServer.ts @@ -94,6 +94,27 @@ export function containingEvent(envelope: Envelope) { return envelope[1].some(item => itemHeaderIsType(item[0], 'event')); } +export function containingEventWithAndroidMessage(message: string) { + return (envelope: Envelope) => + envelope[1].some( + item => + itemHeaderIsType(item[0], 'event') && + itemBodyIsEvent(item[1]) && + item[1].message && + (item[1].message as unknown as { message: string }).message === message, + ); +} + +export function containingEventWithMessage(message: string) { + return (envelope: Envelope) => + envelope[1].some( + item => + itemHeaderIsType(item[0], 'event') && + itemBodyIsEvent(item[1]) && + item[1].message === message, + ); +} + export function containingTransactionWithName(name: string) { return (envelope: Envelope) => envelope[1].some( From ee11f5809336a6dbee70153f97641eb54e3dc62f Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Wed, 11 Jun 2025 12:37:58 +0200 Subject: [PATCH 22/32] chore(sample-e2e): Move Detox related files to e2e-detox dir --- .github/workflows/sample-application.yml | 2 +- samples/react-native/.detoxrc.js | 6 ++-- .../captureAppStartCrash.test.ios.manual.ts | 0 .../captureMessage.test.android.ts | 0 .../captureMessage.test.ios.ts | 0 .../captureTransaction.test.ts | 0 .../envelopeHeader.test.android.ts | 0 .../envelopeHeader.test.ios.ts | 0 .../jest.config.android.js} | 3 +- .../{e2e => e2e-detox}/jest.config.base.js | 2 +- .../jest.config.ios.auto.js} | 4 +-- .../jest.config.ios.manual.js} | 6 +++- .../{e2e => e2e-detox}/starter.test.ts | 0 .../{e2e => e2e-detox}/utils/consts.ts | 0 .../{e2e => e2e-detox}/utils/event.ts | 0 .../utils/mockedSentryServer.ts | 0 .../{e2e => e2e-detox}/utils/parseEnvelope.ts | 0 .../{e2e => e2e-detox}/utils/sleep.ts | 0 .../{e2e => e2e-detox}/utils/tap.ts | 0 .../project.pbxproj | 32 +++++++++---------- samples/react-native/package.json | 10 +++--- samples/react-native/scripts/build-android.sh | 6 ++-- .../react-native/scripts/build-ios-debug.sh | 4 +-- .../react-native/scripts/build-ios-release.sh | 4 +-- samples/react-native/scripts/build-ios.sh | 4 +-- .../scripts/{ => detox}/detect-ios-sim.sh | 4 +-- .../scripts/{ => detox}/set-dsn-aos.mjs | 0 .../scripts/{ => detox}/set-dsn-ios.mjs | 0 .../scripts/{ => detox}/set-dsn.mjs | 2 +- .../scripts/{ => detox}/test-android.sh | 2 +- .../scripts/{ => detox}/test-ios-auto.sh | 2 +- .../scripts/{ => detox}/test-ios-manual.sh | 2 +- samples/react-native/sentry.options.json | 2 +- 33 files changed, 50 insertions(+), 47 deletions(-) rename samples/react-native/{e2e => e2e-detox}/captureAppStartCrash.test.ios.manual.ts (100%) rename samples/react-native/{e2e => e2e-detox}/captureMessage.test.android.ts (100%) rename samples/react-native/{e2e => e2e-detox}/captureMessage.test.ios.ts (100%) rename samples/react-native/{e2e => e2e-detox}/captureTransaction.test.ts (100%) rename samples/react-native/{e2e => e2e-detox}/envelopeHeader.test.android.ts (100%) rename samples/react-native/{e2e => e2e-detox}/envelopeHeader.test.ios.ts (100%) rename samples/react-native/{e2e/jest.config.ios.auto.js => e2e-detox/jest.config.android.js} (70%) rename samples/react-native/{e2e => e2e-detox}/jest.config.base.js (88%) rename samples/react-native/{e2e/jest.config.ios.manual.js => e2e-detox/jest.config.ios.auto.js} (68%) rename samples/react-native/{e2e/jest.config.android.js => e2e-detox/jest.config.ios.manual.js} (50%) rename samples/react-native/{e2e => e2e-detox}/starter.test.ts (100%) rename samples/react-native/{e2e => e2e-detox}/utils/consts.ts (100%) rename samples/react-native/{e2e => e2e-detox}/utils/event.ts (100%) rename samples/react-native/{e2e => e2e-detox}/utils/mockedSentryServer.ts (100%) rename samples/react-native/{e2e => e2e-detox}/utils/parseEnvelope.ts (100%) rename samples/react-native/{e2e => e2e-detox}/utils/sleep.ts (100%) rename samples/react-native/{e2e => e2e-detox}/utils/tap.ts (100%) rename samples/react-native/scripts/{ => detox}/detect-ios-sim.sh (91%) rename samples/react-native/scripts/{ => detox}/set-dsn-aos.mjs (100%) rename samples/react-native/scripts/{ => detox}/set-dsn-ios.mjs (100%) rename samples/react-native/scripts/{ => detox}/set-dsn.mjs (88%) rename samples/react-native/scripts/{ => detox}/test-android.sh (97%) rename samples/react-native/scripts/{ => detox}/test-ios-auto.sh (88%) rename samples/react-native/scripts/{ => detox}/test-ios-manual.sh (86%) diff --git a/.github/workflows/sample-application.yml b/.github/workflows/sample-application.yml index 0c697f22e5..899c341dbf 100644 --- a/.github/workflows/sample-application.yml +++ b/.github/workflows/sample-application.yml @@ -185,7 +185,7 @@ jobs: name: build-sample-${{ matrix.rn-architecture }}-${{ matrix.platform }}-${{ matrix.build-type }}-${{ matrix.ios-use-frameworks}}-logs path: ${{ env.REACT_NATIVE_SAMPLE_PATH }}/${{ matrix.platform }}/*.log - test: + test-detox: name: ${{ matrix.job-name }} runs-on: ${{ matrix.runs-on }} needs: [diff_check, build] diff --git a/samples/react-native/.detoxrc.js b/samples/react-native/.detoxrc.js index a3501515a9..86e992abae 100644 --- a/samples/react-native/.detoxrc.js +++ b/samples/react-native/.detoxrc.js @@ -37,7 +37,7 @@ module.exports = { testRunner: { args: { $0: 'jest', - config: 'e2e/jest.config.android.js', + config: 'e2e-detox/jest.config.android.js', }, jest: { setupTimeout: 120000, @@ -50,7 +50,7 @@ module.exports = { testRunner: { args: { $0: 'jest', - config: 'e2e/jest.config.ios.auto.js', + config: 'e2e-detox/jest.config.ios.auto.js', }, jest: { setupTimeout: 120000, @@ -63,7 +63,7 @@ module.exports = { testRunner: { args: { $0: 'jest', - config: 'e2e/jest.config.ios.manual.js', + config: 'e2e-detox/jest.config.ios.manual.js', }, jest: { setupTimeout: 120000, diff --git a/samples/react-native/e2e/captureAppStartCrash.test.ios.manual.ts b/samples/react-native/e2e-detox/captureAppStartCrash.test.ios.manual.ts similarity index 100% rename from samples/react-native/e2e/captureAppStartCrash.test.ios.manual.ts rename to samples/react-native/e2e-detox/captureAppStartCrash.test.ios.manual.ts diff --git a/samples/react-native/e2e/captureMessage.test.android.ts b/samples/react-native/e2e-detox/captureMessage.test.android.ts similarity index 100% rename from samples/react-native/e2e/captureMessage.test.android.ts rename to samples/react-native/e2e-detox/captureMessage.test.android.ts diff --git a/samples/react-native/e2e/captureMessage.test.ios.ts b/samples/react-native/e2e-detox/captureMessage.test.ios.ts similarity index 100% rename from samples/react-native/e2e/captureMessage.test.ios.ts rename to samples/react-native/e2e-detox/captureMessage.test.ios.ts diff --git a/samples/react-native/e2e/captureTransaction.test.ts b/samples/react-native/e2e-detox/captureTransaction.test.ts similarity index 100% rename from samples/react-native/e2e/captureTransaction.test.ts rename to samples/react-native/e2e-detox/captureTransaction.test.ts diff --git a/samples/react-native/e2e/envelopeHeader.test.android.ts b/samples/react-native/e2e-detox/envelopeHeader.test.android.ts similarity index 100% rename from samples/react-native/e2e/envelopeHeader.test.android.ts rename to samples/react-native/e2e-detox/envelopeHeader.test.android.ts diff --git a/samples/react-native/e2e/envelopeHeader.test.ios.ts b/samples/react-native/e2e-detox/envelopeHeader.test.ios.ts similarity index 100% rename from samples/react-native/e2e/envelopeHeader.test.ios.ts rename to samples/react-native/e2e-detox/envelopeHeader.test.ios.ts diff --git a/samples/react-native/e2e/jest.config.ios.auto.js b/samples/react-native/e2e-detox/jest.config.android.js similarity index 70% rename from samples/react-native/e2e/jest.config.ios.auto.js rename to samples/react-native/e2e-detox/jest.config.android.js index ebfadcaeb6..f0102a2a73 100644 --- a/samples/react-native/e2e/jest.config.ios.auto.js +++ b/samples/react-native/e2e-detox/jest.config.android.js @@ -5,7 +5,6 @@ module.exports = { ...baseConfig, testMatch: [ ...baseConfig.testMatch, - '/e2e/**/*.test.ios.ts', - '/e2e/**/*.test.ios.auto.ts', + '/e2e-detox/**/*.test.android.ts', ], }; diff --git a/samples/react-native/e2e/jest.config.base.js b/samples/react-native/e2e-detox/jest.config.base.js similarity index 88% rename from samples/react-native/e2e/jest.config.base.js rename to samples/react-native/e2e-detox/jest.config.base.js index b52d19a014..7528e6eda7 100644 --- a/samples/react-native/e2e/jest.config.base.js +++ b/samples/react-native/e2e-detox/jest.config.base.js @@ -2,7 +2,7 @@ module.exports = { preset: 'ts-jest', rootDir: '..', - testMatch: ['/e2e/**/*.test.ts'], + testMatch: ['/e2e-detox/**/*.test.ts'], testTimeout: 120000, maxWorkers: 1, globalSetup: 'detox/runners/jest/globalSetup', diff --git a/samples/react-native/e2e/jest.config.ios.manual.js b/samples/react-native/e2e-detox/jest.config.ios.auto.js similarity index 68% rename from samples/react-native/e2e/jest.config.ios.manual.js rename to samples/react-native/e2e-detox/jest.config.ios.auto.js index fe271fca7d..23b69ca7d0 100644 --- a/samples/react-native/e2e/jest.config.ios.manual.js +++ b/samples/react-native/e2e-detox/jest.config.ios.auto.js @@ -5,7 +5,7 @@ module.exports = { ...baseConfig, testMatch: [ ...baseConfig.testMatch, - '/e2e/**/*.test.ios.ts', - '/e2e/**/*.test.ios.manual.ts', + '/e2e-detox/**/*.test.ios.ts', + '/e2e-detox/**/*.test.ios.auto.ts', ], }; diff --git a/samples/react-native/e2e/jest.config.android.js b/samples/react-native/e2e-detox/jest.config.ios.manual.js similarity index 50% rename from samples/react-native/e2e/jest.config.android.js rename to samples/react-native/e2e-detox/jest.config.ios.manual.js index a07210b212..af65ce5267 100644 --- a/samples/react-native/e2e/jest.config.android.js +++ b/samples/react-native/e2e-detox/jest.config.ios.manual.js @@ -3,5 +3,9 @@ const baseConfig = require('./jest.config.base'); /** @type {import('@jest/types').Config.InitialOptions} */ module.exports = { ...baseConfig, - testMatch: [...baseConfig.testMatch, '/e2e/**/*.test.android.ts'], + testMatch: [ + ...baseConfig.testMatch, + '/e2e-detox/**/*.test.ios.ts', + '/e2e-detox/**/*.test.ios.manual.ts', + ], }; diff --git a/samples/react-native/e2e/starter.test.ts b/samples/react-native/e2e-detox/starter.test.ts similarity index 100% rename from samples/react-native/e2e/starter.test.ts rename to samples/react-native/e2e-detox/starter.test.ts diff --git a/samples/react-native/e2e/utils/consts.ts b/samples/react-native/e2e-detox/utils/consts.ts similarity index 100% rename from samples/react-native/e2e/utils/consts.ts rename to samples/react-native/e2e-detox/utils/consts.ts diff --git a/samples/react-native/e2e/utils/event.ts b/samples/react-native/e2e-detox/utils/event.ts similarity index 100% rename from samples/react-native/e2e/utils/event.ts rename to samples/react-native/e2e-detox/utils/event.ts diff --git a/samples/react-native/e2e/utils/mockedSentryServer.ts b/samples/react-native/e2e-detox/utils/mockedSentryServer.ts similarity index 100% rename from samples/react-native/e2e/utils/mockedSentryServer.ts rename to samples/react-native/e2e-detox/utils/mockedSentryServer.ts diff --git a/samples/react-native/e2e/utils/parseEnvelope.ts b/samples/react-native/e2e-detox/utils/parseEnvelope.ts similarity index 100% rename from samples/react-native/e2e/utils/parseEnvelope.ts rename to samples/react-native/e2e-detox/utils/parseEnvelope.ts diff --git a/samples/react-native/e2e/utils/sleep.ts b/samples/react-native/e2e-detox/utils/sleep.ts similarity index 100% rename from samples/react-native/e2e/utils/sleep.ts rename to samples/react-native/e2e-detox/utils/sleep.ts diff --git a/samples/react-native/e2e/utils/tap.ts b/samples/react-native/e2e-detox/utils/tap.ts similarity index 100% rename from samples/react-native/e2e/utils/tap.ts rename to samples/react-native/e2e-detox/utils/tap.ts diff --git a/samples/react-native/ios/sentryreactnativesample.xcodeproj/project.pbxproj b/samples/react-native/ios/sentryreactnativesample.xcodeproj/project.pbxproj index cfb63e1edc..5c756c154c 100644 --- a/samples/react-native/ios/sentryreactnativesample.xcodeproj/project.pbxproj +++ b/samples/react-native/ios/sentryreactnativesample.xcodeproj/project.pbxproj @@ -13,10 +13,10 @@ 13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; }; 338BBBC82B614FA10035844C /* NativePlatformSampleModule.mm in Sources */ = {isa = PBXBuildFile; fileRef = 338BBBC62B614FA10035844C /* NativePlatformSampleModule.mm */; }; 33E2D62A29A7719600B5042B /* RCTAssetsModule.m in Sources */ = {isa = PBXBuildFile; fileRef = 33E2D62829A7719600B5042B /* RCTAssetsModule.m */; }; - 5ACADB1A9924EDD0850FACBA /* libPods-sentryreactnativesample-sentryreactnativesampleTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = AFC2BCCFBDE2DC231B5C04E5 /* libPods-sentryreactnativesample-sentryreactnativesampleTests.a */; }; + 70CDA307A4907B49DBB2A9A2 /* Pods_sentryreactnativesample_sentryreactnativesampleTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 559D375348FD651E365E7D18 /* Pods_sentryreactnativesample_sentryreactnativesampleTests.framework */; }; 723FB93385E08A7032AE82F4 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 916F55A329D66130A4DFF319 /* PrivacyInfo.xcprivacy */; }; + 819802E10D600757E83E767A /* Pods_sentryreactnativesample.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A6163B566BBBFEF1CA95B60D /* Pods_sentryreactnativesample.framework */; }; 81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */; }; - F998F5A3F1731876C4EDA235 /* libPods-sentryreactnativesample.a in Frameworks */ = {isa = PBXBuildFile; fileRef = CA08EE94AE203638B8C8B74B /* libPods-sentryreactnativesample.a */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -44,13 +44,13 @@ 33E2D62829A7719600B5042B /* RCTAssetsModule.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = RCTAssetsModule.m; path = sentryreactnativesample/RCTAssetsModule.m; sourceTree = ""; }; 33E2D62929A7719600B5042B /* RCTAssetsModule.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RCTAssetsModule.h; path = sentryreactnativesample/RCTAssetsModule.h; sourceTree = ""; }; 3B4392A12AC88292D35C810B /* Pods-sentryreactnativesample.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-sentryreactnativesample.debug.xcconfig"; path = "Target Support Files/Pods-sentryreactnativesample/Pods-sentryreactnativesample.debug.xcconfig"; sourceTree = ""; }; + 559D375348FD651E365E7D18 /* Pods_sentryreactnativesample_sentryreactnativesampleTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_sentryreactnativesample_sentryreactnativesampleTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 5709B34CF0A7D63546082F79 /* Pods-sentryreactnativesample.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-sentryreactnativesample.release.xcconfig"; path = "Target Support Files/Pods-sentryreactnativesample/Pods-sentryreactnativesample.release.xcconfig"; sourceTree = ""; }; 5B7EB9410499542E8C5724F5 /* Pods-sentryreactnativesample-sentryreactnativesampleTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-sentryreactnativesample-sentryreactnativesampleTests.debug.xcconfig"; path = "Target Support Files/Pods-sentryreactnativesample-sentryreactnativesampleTests/Pods-sentryreactnativesample-sentryreactnativesampleTests.debug.xcconfig"; sourceTree = ""; }; 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = LaunchScreen.storyboard; path = sentryreactnativesample/LaunchScreen.storyboard; sourceTree = ""; }; 89C6BE57DB24E9ADA2F236DE /* Pods-sentryreactnativesample-sentryreactnativesampleTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-sentryreactnativesample-sentryreactnativesampleTests.release.xcconfig"; path = "Target Support Files/Pods-sentryreactnativesample-sentryreactnativesampleTests/Pods-sentryreactnativesample-sentryreactnativesampleTests.release.xcconfig"; sourceTree = ""; }; 916F55A329D66130A4DFF319 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xml; name = PrivacyInfo.xcprivacy; path = sentryreactnativesample/PrivacyInfo.xcprivacy; sourceTree = ""; }; - AFC2BCCFBDE2DC231B5C04E5 /* libPods-sentryreactnativesample-sentryreactnativesampleTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-sentryreactnativesample-sentryreactnativesampleTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; - CA08EE94AE203638B8C8B74B /* libPods-sentryreactnativesample.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-sentryreactnativesample.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + A6163B566BBBFEF1CA95B60D /* Pods_sentryreactnativesample.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_sentryreactnativesample.framework; sourceTree = BUILT_PRODUCTS_DIR; }; ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; }; /* End PBXFileReference section */ @@ -59,7 +59,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 5ACADB1A9924EDD0850FACBA /* libPods-sentryreactnativesample-sentryreactnativesampleTests.a in Frameworks */, + 70CDA307A4907B49DBB2A9A2 /* Pods_sentryreactnativesample_sentryreactnativesampleTests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -67,7 +67,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - F998F5A3F1731876C4EDA235 /* libPods-sentryreactnativesample.a in Frameworks */, + 819802E10D600757E83E767A /* Pods_sentryreactnativesample.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -113,8 +113,8 @@ isa = PBXGroup; children = ( ED297162215061F000B7C4FE /* JavaScriptCore.framework */, - CA08EE94AE203638B8C8B74B /* libPods-sentryreactnativesample.a */, - AFC2BCCFBDE2DC231B5C04E5 /* libPods-sentryreactnativesample-sentryreactnativesampleTests.a */, + A6163B566BBBFEF1CA95B60D /* Pods_sentryreactnativesample.framework */, + 559D375348FD651E365E7D18 /* Pods_sentryreactnativesample_sentryreactnativesampleTests.framework */, ); name = Frameworks; sourceTree = ""; @@ -566,7 +566,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; - CC = ""; + CC = "$(REACT_NATIVE_PATH)/scripts/xcode/ccache-clang.sh"; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_CXX_LANGUAGE_STANDARD = "c++20"; CLANG_CXX_LIBRARY = "libc++"; @@ -594,7 +594,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; - CXX = ""; + CXX = "$(REACT_NATIVE_PATH)/scripts/xcode/ccache-clang++.sh"; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = i386; @@ -625,8 +625,8 @@ "${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers/react/renderer/graphics/platform/ios", ); IPHONEOS_DEPLOYMENT_TARGET = 15.1; - LD = ""; - LDPLUSPLUS = ""; + LD = "$(REACT_NATIVE_PATH)/scripts/xcode/ccache-clang.sh"; + LDPLUSPLUS = "$(REACT_NATIVE_PATH)/scripts/xcode/ccache-clang++.sh"; LD_RUNPATH_SEARCH_PATHS = ( /usr/lib/swift, "$(inherited)", @@ -662,7 +662,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; - CC = ""; + CC = "$(REACT_NATIVE_PATH)/scripts/xcode/ccache-clang.sh"; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_CXX_LANGUAGE_STANDARD = "c++20"; CLANG_CXX_LIBRARY = "libc++"; @@ -690,7 +690,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = YES; - CXX = ""; + CXX = "$(REACT_NATIVE_PATH)/scripts/xcode/ccache-clang++.sh"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = i386; @@ -714,8 +714,8 @@ "${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers/react/renderer/graphics/platform/ios", ); IPHONEOS_DEPLOYMENT_TARGET = 15.1; - LD = ""; - LDPLUSPLUS = ""; + LD = "$(REACT_NATIVE_PATH)/scripts/xcode/ccache-clang.sh"; + LDPLUSPLUS = "$(REACT_NATIVE_PATH)/scripts/xcode/ccache-clang++.sh"; LD_RUNPATH_SEARCH_PATHS = ( /usr/lib/swift, "$(inherited)", diff --git a/samples/react-native/package.json b/samples/react-native/package.json index 8e80a5d598..0060a1ff5d 100644 --- a/samples/react-native/package.json +++ b/samples/react-native/package.json @@ -12,11 +12,11 @@ "build-ios-release": "scripts/build-ios-release.sh", "build-ios-debug": "scripts/build-ios-debug.sh", "test": "jest", - "set-test-dsn-android": "scripts/set-dsn-aos.mjs", - "set-test-dsn-ios": "scripts/set-dsn-ios.mjs", - "test-android": "scripts/test-android.sh", - "test-ios-manual": "scripts/test-ios-manual.sh", - "test-ios-auto": "scripts/test-ios-auto.sh", + "set-test-dsn-android": "scripts/detox/set-dsn-aos.mjs", + "set-test-dsn-ios": "scripts/detox/set-dsn-ios.mjs", + "test-android": "scripts/detox/test-android.sh", + "test-ios-manual": "scripts/detox/test-ios-manual.sh", + "test-ios-auto": "scripts/detox/test-ios-auto.sh", "lint": "npx eslint . --ext .js,.jsx,.ts,.tsx", "fix": "eslint . --ext .js,.jsx,.ts,.tsx --fix", "pod-install-debug-static": "scripts/pod-install-debug-static.sh", diff --git a/samples/react-native/scripts/build-android.sh b/samples/react-native/scripts/build-android.sh index 866b5cc130..3ada91cfd2 100755 --- a/samples/react-native/scripts/build-android.sh +++ b/samples/react-native/scripts/build-android.sh @@ -1,7 +1,7 @@ #!/bin/bash -# Exit on error and print commands -set -xe +# Exit on error +set -e thisFilePath=$(dirname "$0") @@ -21,7 +21,7 @@ fi echo "Building $CONFIG" -assembleConfig=$(python -c "print(\"${CONFIG}\".capitalize())") +assembleConfig=$(python3 -c "print(\"${CONFIG}\".capitalize())") ./gradlew ":app:assemble${assembleConfig}" app:assembleAndroidTest -DtestBuildType=$CONFIG "$@" diff --git a/samples/react-native/scripts/build-ios-debug.sh b/samples/react-native/scripts/build-ios-debug.sh index 088ed50f28..290fd20d94 100755 --- a/samples/react-native/scripts/build-ios-debug.sh +++ b/samples/react-native/scripts/build-ios-debug.sh @@ -1,7 +1,7 @@ #!/bin/bash -# Exit on error and print commands -set -xe +# Exit on error +set -e thisFilePath=$(dirname "$0") diff --git a/samples/react-native/scripts/build-ios-release.sh b/samples/react-native/scripts/build-ios-release.sh index 4a21d04c17..f5de18b73e 100755 --- a/samples/react-native/scripts/build-ios-release.sh +++ b/samples/react-native/scripts/build-ios-release.sh @@ -1,7 +1,7 @@ #!/bin/bash -# Exit on error and print commands -set -xe +# Exit on error +set -e thisFilePath=$(dirname "$0") diff --git a/samples/react-native/scripts/build-ios.sh b/samples/react-native/scripts/build-ios.sh index 7897aba332..3a8cfefe4a 100755 --- a/samples/react-native/scripts/build-ios.sh +++ b/samples/react-native/scripts/build-ios.sh @@ -1,7 +1,7 @@ #!/bin/bash -# Exit on error and print commands -set -xe +# Exit on error +set -e thisFilePath=$(dirname "$0") diff --git a/samples/react-native/scripts/detect-ios-sim.sh b/samples/react-native/scripts/detox/detect-ios-sim.sh similarity index 91% rename from samples/react-native/scripts/detect-ios-sim.sh rename to samples/react-native/scripts/detox/detect-ios-sim.sh index 1ba6d3b98c..067c5abf48 100755 --- a/samples/react-native/scripts/detect-ios-sim.sh +++ b/samples/react-native/scripts/detox/detect-ios-sim.sh @@ -1,7 +1,7 @@ #!/bin/bash -# Exit on error and print commands -set -xe +# Exit on error +set -e if [ -z "$IOS_DEVICE" ]; then # Get the first booted simulator device type and version diff --git a/samples/react-native/scripts/set-dsn-aos.mjs b/samples/react-native/scripts/detox/set-dsn-aos.mjs similarity index 100% rename from samples/react-native/scripts/set-dsn-aos.mjs rename to samples/react-native/scripts/detox/set-dsn-aos.mjs diff --git a/samples/react-native/scripts/set-dsn-ios.mjs b/samples/react-native/scripts/detox/set-dsn-ios.mjs similarity index 100% rename from samples/react-native/scripts/set-dsn-ios.mjs rename to samples/react-native/scripts/detox/set-dsn-ios.mjs diff --git a/samples/react-native/scripts/set-dsn.mjs b/samples/react-native/scripts/detox/set-dsn.mjs similarity index 88% rename from samples/react-native/scripts/set-dsn.mjs rename to samples/react-native/scripts/detox/set-dsn.mjs index da2153f203..6c8ec24bfe 100644 --- a/samples/react-native/scripts/set-dsn.mjs +++ b/samples/react-native/scripts/detox/set-dsn.mjs @@ -13,7 +13,7 @@ export function setAndroidDsn() { } function setDsn(dsn) { - const sentryOptionsPath = path.join(__dirname, '../sentry.options.json'); + const sentryOptionsPath = path.join(__dirname, '../../sentry.options.json'); const sentryOptions = JSON.parse(fs.readFileSync(sentryOptionsPath, 'utf8')); sentryOptions.dsn = dsn; fs.writeFileSync( diff --git a/samples/react-native/scripts/test-android.sh b/samples/react-native/scripts/detox/test-android.sh similarity index 97% rename from samples/react-native/scripts/test-android.sh rename to samples/react-native/scripts/detox/test-android.sh index bbef32a81f..a5dc4fce83 100755 --- a/samples/react-native/scripts/test-android.sh +++ b/samples/react-native/scripts/detox/test-android.sh @@ -5,7 +5,7 @@ set -xe thisFilePath=$(dirname "$0") -cd "${thisFilePath}/.." +cd "${thisFilePath}/../.." if [ -z "$ANDROID_AVD_NAME" ]; then # Get the name of the first booted or connected Android device diff --git a/samples/react-native/scripts/test-ios-auto.sh b/samples/react-native/scripts/detox/test-ios-auto.sh similarity index 88% rename from samples/react-native/scripts/test-ios-auto.sh rename to samples/react-native/scripts/detox/test-ios-auto.sh index 9192a9619e..da9b731642 100755 --- a/samples/react-native/scripts/test-ios-auto.sh +++ b/samples/react-native/scripts/detox/test-ios-auto.sh @@ -5,7 +5,7 @@ set -xe thisFilePath=$(dirname "$0") -cd "${thisFilePath}/.." +cd "${thisFilePath}/../.." "${thisFilePath}/detect-ios-sim.sh" diff --git a/samples/react-native/scripts/test-ios-manual.sh b/samples/react-native/scripts/detox/test-ios-manual.sh similarity index 86% rename from samples/react-native/scripts/test-ios-manual.sh rename to samples/react-native/scripts/detox/test-ios-manual.sh index b1c295edda..a83db50e1b 100755 --- a/samples/react-native/scripts/test-ios-manual.sh +++ b/samples/react-native/scripts/detox/test-ios-manual.sh @@ -5,7 +5,7 @@ set -xe thisFilePath=$(dirname "$0") -cd "${thisFilePath}/.." +cd "${thisFilePath}/../.." "${thisFilePath}/detect-ios-sim.sh" diff --git a/samples/react-native/sentry.options.json b/samples/react-native/sentry.options.json index 58425c35d5..8fd5fe07a4 100644 --- a/samples/react-native/sentry.options.json +++ b/samples/react-native/sentry.options.json @@ -1,5 +1,5 @@ { - "dsn": "https://1df17bd4e543fdb31351dee1768bb679@o447951.ingest.sentry.io/5428561", + "dsn": "http://key@localhost:8961/123456", "debug": true, "environment": "dev", "enableUserInteractionTracing": true, From 1e01928fa07b765ff8a7282056f0de31d13114c9 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Mon, 15 Dec 2025 13:55:52 +0100 Subject: [PATCH 23/32] fix: remove unused SentryPackage import Removed unused import that was causing CI lint failure --- .../android/src/main/java/io/sentry/react/RNSentryStart.java | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/core/android/src/main/java/io/sentry/react/RNSentryStart.java b/packages/core/android/src/main/java/io/sentry/react/RNSentryStart.java index 92bd59001a..18d6b11763 100644 --- a/packages/core/android/src/main/java/io/sentry/react/RNSentryStart.java +++ b/packages/core/android/src/main/java/io/sentry/react/RNSentryStart.java @@ -20,7 +20,6 @@ import io.sentry.android.core.SentryAndroid; import io.sentry.android.core.SentryAndroidOptions; import io.sentry.protocol.SdkVersion; -import io.sentry.protocol.SentryPackage; import io.sentry.react.replay.RNSentryReplayMask; import io.sentry.react.replay.RNSentryReplayUnmask; import java.net.URI; From 50ff76a5342523e7913984d0e4feddd429798140 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Mon, 15 Dec 2025 13:56:42 +0100 Subject: [PATCH 24/32] fix: update RNSentryStartTest for Sentry Android SDK v7 API changes - Removed assertions for packages getter (not available in v7) - Removed assertion for enableTracing property (removed in v7) - Added comments explaining the API changes --- .../java/io/sentry/react/RNSentryStartTest.kt | 20 ++++--------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/packages/core/RNSentryAndroidTester/app/src/test/java/io/sentry/react/RNSentryStartTest.kt b/packages/core/RNSentryAndroidTester/app/src/test/java/io/sentry/react/RNSentryStartTest.kt index fa177159e5..160d0fd9b2 100644 --- a/packages/core/RNSentryAndroidTester/app/src/test/java/io/sentry/react/RNSentryStartTest.kt +++ b/packages/core/RNSentryAndroidTester/app/src/test/java/io/sentry/react/RNSentryStartTest.kt @@ -202,21 +202,8 @@ class RNSentryStartTest { io.sentry.android.core.BuildConfig.VERSION_NAME, actualOptions.sdkVersion?.version, ) - assertEquals(true, actualOptions.sdkVersion?.packages?.isNotEmpty()) - assertEquals( - RNSentryVersion.REACT_NATIVE_SDK_PACKAGE_NAME, - actualOptions.sdkVersion - ?.packages - ?.last() - ?.name, - ) - assertEquals( - RNSentryVersion.REACT_NATIVE_SDK_PACKAGE_VERSION, - actualOptions.sdkVersion - ?.packages - ?.last() - ?.version, - ) + // Note: In Sentry Android SDK v7, SdkVersion doesn't expose packages as a getter + // The React Native package is added via addPackage() but not accessible via getter } @Test @@ -225,7 +212,8 @@ class RNSentryStartTest { RNSentryStart.updateWithReactDefaults(actualOptions, activity) assertNull(actualOptions.tracesSampleRate) assertNull(actualOptions.tracesSampler) - assertEquals(false, actualOptions.enableTracing) + // Note: enableTracing property doesn't exist in Sentry Android SDK v7 + // Tracing is disabled by setting tracesSampleRate and tracesSampler to null } @Test From 0efb3bfe6461c44113cd45d2668336f67925897e Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Mon, 15 Dec 2025 14:07:20 +0100 Subject: [PATCH 25/32] fix: remove unused addPackages method to fix PMD lint error The method was a no-op after v7 API changes, so removing it entirely to avoid unused parameter warnings from PMD. --- .../src/main/java/io/sentry/react/RNSentryStart.java | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/packages/core/android/src/main/java/io/sentry/react/RNSentryStart.java b/packages/core/android/src/main/java/io/sentry/react/RNSentryStart.java index 18d6b11763..fcdacd964e 100644 --- a/packages/core/android/src/main/java/io/sentry/react/RNSentryStart.java +++ b/packages/core/android/src/main/java/io/sentry/react/RNSentryStart.java @@ -232,7 +232,8 @@ static void updateWithReactFinals(@NotNull SentryAndroidOptions options) { options.setBeforeSend( (event, hint) -> { setEventOriginTag(event); - addPackages(event, options.getSdkVersion()); + // Note: In Sentry Android SDK v7, native SDK packages/integrations are already + // included in the SDK version set during initialization, so no need to copy them here. if (userBeforeSend != null) { return userBeforeSend.execute(event, hint); } @@ -325,13 +326,6 @@ private static void setEventEnvironmentTag(SentryEvent event, String environment event.setTag("event.environment", environment); } - private static void addPackages(SentryEvent event, SdkVersion sdk) { - // In Sentry Android SDK v7, SdkVersion doesn't expose getPackages() or getIntegrations() - // The native SDK packages and integrations are already included in the SDK version - // set during initialization, so we don't need to copy them here. - // This method is kept for potential future use or if the API changes. - } - private static @Nullable String getURLFromDSN(@Nullable String dsn) { if (dsn == null) { return null; From 013f560b33f1171f5fa9eb54b1a93ea38a7a5f46 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Mon, 15 Dec 2025 14:08:50 +0100 Subject: [PATCH 26/32] fix: update RNSentrySDKTest for Sentry Android SDK v7 API changes - Removed assertions for enableTracing property (removed in v7) - Removed assertions for packages getter (not available in v7) - Added comments explaining the API changes --- .../java/io/sentry/react/RNSentrySDKTest.kt | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/packages/core/RNSentryAndroidTester/app/src/androidTest/java/io/sentry/react/RNSentrySDKTest.kt b/packages/core/RNSentryAndroidTester/app/src/androidTest/java/io/sentry/react/RNSentrySDKTest.kt index 3b95742e55..bfa1647cbc 100644 --- a/packages/core/RNSentryAndroidTester/app/src/androidTest/java/io/sentry/react/RNSentrySDKTest.kt +++ b/packages/core/RNSentryAndroidTester/app/src/androidTest/java/io/sentry/react/RNSentrySDKTest.kt @@ -156,7 +156,8 @@ class RNSentrySDKTest { RNSentrySDK.init(context, emptyConfig, VALID_OPTIONS, logger) val actualOptions = Sentry.getCurrentHub().options as SentryAndroidOptions assertNull(actualOptions.tracesSampleRate) - assertEquals(false, actualOptions.enableTracing) + // Note: enableTracing property doesn't exist in Sentry Android SDK v7 + // Tracing is disabled by setting tracesSampleRate and tracesSampler to null } @Test @@ -165,12 +166,12 @@ class RNSentrySDKTest { OptionsConfiguration { options -> options.dsn = "https://abcd@efgh.ingest.sentry.io/123456" options.tracesSampleRate = 0.5 - options.enableTracing = true + // Note: enableTracing property doesn't exist in Sentry Android SDK v7 } RNSentrySDK.init(context, validConfig, MISSING, logger) val actualOptions = Sentry.getCurrentHub().options as SentryAndroidOptions assertEquals(0.5, actualOptions.tracesSampleRate) - assertEquals(true, actualOptions.enableTracing) + // Note: enableTracing property doesn't exist in Sentry Android SDK v7 assert(actualOptions.dsn == "https://abcd@efgh.ingest.sentry.io/123456") } @@ -181,12 +182,12 @@ class RNSentrySDKTest { io.sentry.android.core.BuildConfig.VERSION_NAME, actualOptions.sdkVersion?.version, ) - val pack = actualOptions.sdkVersion?.packages?.first { it.name == RNSentryVersion.REACT_NATIVE_SDK_PACKAGE_NAME } - assertNotNull(pack) - assertEquals(RNSentryVersion.REACT_NATIVE_SDK_PACKAGE_VERSION, pack?.version) + // Note: In Sentry Android SDK v7, SdkVersion doesn't expose packages as a getter + // The React Native package is added via addPackage() but not accessible via getter assertNull(actualOptions.tracesSampleRate) assertNull(actualOptions.tracesSampler) - assertEquals(false, actualOptions.enableTracing) + // Note: enableTracing property doesn't exist in Sentry Android SDK v7 + // Tracing is disabled by setting tracesSampleRate and tracesSampler to null } private fun verifyFinals(actualOptions: SentryAndroidOptions) { From d93c1b9990e6fce1177a556b991ef79e2ee7b3ed Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Mon, 15 Dec 2025 14:37:51 +0100 Subject: [PATCH 27/32] fix: use relative path for RNSentrySDK+Test.h import in bridging header The file is in the parent directory, so use ../ prefix to fix the import path --- .../RNSentryCocoaTesterTests-Bridging-Header.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryCocoaTesterTests-Bridging-Header.h b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryCocoaTesterTests-Bridging-Header.h index da0eb2b587..0101ffe504 100644 --- a/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryCocoaTesterTests-Bridging-Header.h +++ b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryCocoaTesterTests-Bridging-Header.h @@ -8,7 +8,7 @@ #import "RNSentryReplayBreadcrumbConverter.h" #import "RNSentryReplayMask.h" #import "RNSentryReplayUnmask.h" -#import "RNSentrySDK+Test.h" +#import "../RNSentrySDK+Test.h" #import "RNSentryStart.h" #import "RNSentryTimeToDisplay.h" #import "RNSentryVersion.h" From b25688fcd5a70c40c4b3fa3c613f1afe324809bb Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Mon, 15 Dec 2025 14:38:13 +0100 Subject: [PATCH 28/32] fix: remove deprecated enableTracing property in iOS RNSentryStart The property is deprecated in v7. Tracing is already disabled by setting tracesSampleRate and tracesSampler to nil. --- packages/core/ios/RNSentryStart.m | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/core/ios/RNSentryStart.m b/packages/core/ios/RNSentryStart.m index fffe6da1d4..d40ac63ac0 100644 --- a/packages/core/ios/RNSentryStart.m +++ b/packages/core/ios/RNSentryStart.m @@ -111,7 +111,8 @@ + (void)updateWithReactDefaults:(SentryOptions *)options // Tracing is only enabled in JS to avoid duplicate navigation spans options.tracesSampleRate = nil; options.tracesSampler = nil; - options.enableTracing = NO; + // Note: enableTracing property is deprecated in Sentry Cocoa SDK v7 + // Tracing is disabled by setting tracesSampleRate and tracesSampler to nil } /** From 86d2829b2b84f8fd903400dd7ad034c74df39d9d Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Mon, 15 Dec 2025 14:52:27 +0100 Subject: [PATCH 29/32] fix: remove enableTracing assertions from iOS tests The enableTracing property is deprecated in Sentry Cocoa SDK v7. Tracing is already verified to be disabled by checking that tracesSampleRate and tracesSampler are nil. --- .../RNSentryCocoaTesterTests/RNSentryTests.m | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryTests.m b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryTests.m index 3339620505..b50fc5e1af 100644 --- a/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryTests.m +++ b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryTests.m @@ -43,7 +43,8 @@ - (void)testCreateOptionsWithDictionaryRemovesPerformanceProperties XCTAssertEqual( actualOptions.tracesSampleRate, nil, @"Traces sample rate should not be passed to native"); XCTAssertEqual(actualOptions.tracesSampler, nil, @"Traces sampler should not be passed to native"); -XCTAssertEqual(actualOptions.enableTracing, false, @"EnableTracing should not be passed to native"); +// Note: enableTracing property is deprecated in Sentry Cocoa SDK v7 +// Tracing is disabled by setting tracesSampleRate and tracesSampler to nil } - (void)testCaptureFailedRequestsIsDisabled @@ -1021,7 +1022,8 @@ - (void)testStartWithDictionaryRemovesPerformanceProperties XCTAssertEqual( actualOptions.tracesSampleRate, nil, @"Traces sample rate should not be passed to native"); XCTAssertEqual(actualOptions.tracesSampler, nil, @"Traces sampler should not be passed to native"); -XCTAssertEqual(actualOptions.enableTracing, false, @"EnableTracing should not be passed to native"); +// Note: enableTracing property is deprecated in Sentry Cocoa SDK v7 +// Tracing is disabled by setting tracesSampleRate and tracesSampler to nil } - (void)testStartCaptureFailedRequestsIsDisabled From d0f1987eaace41cfc778c03338866d472154ff1f Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Mon, 15 Dec 2025 14:53:04 +0100 Subject: [PATCH 30/32] Update Podspec --- packages/core/RNSentry.podspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/RNSentry.podspec b/packages/core/RNSentry.podspec index 519b15ccc0..dbcdeb826c 100644 --- a/packages/core/RNSentry.podspec +++ b/packages/core/RNSentry.podspec @@ -42,7 +42,7 @@ Pod::Spec.new do |s| s.preserve_paths = '*.js' s.source_files = 'ios/**/*.{h,m,mm}' - s.public_header_files = 'ios/RNSentry.h', 'ios/RNSentrySDK.h' + s.public_header_files = 'ios/RNSentry.h', 'ios/RNSentrySDK.h', 'ios/RNSentryStart.h', 'ios/RNSentryVersion.h', 'ios/RNSentryBreadcrumb.h', 'ios/RNSentryReplay.h', 'ios/RNSentryReplayBreadcrumbConverter.h', 'ios/Replay/RNSentryReplayMask.h', 'ios/Replay/RNSentryReplayUnmask.h', 'ios/RNSentryTimeToDisplay.h' s.compiler_flags = other_cflags From 032f74ab8767870938b979ad4a399a048b651dfd Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Mon, 15 Dec 2025 15:02:15 +0100 Subject: [PATCH 31/32] Fix lint issue --- .../RNSentryCocoaTesterTests-Bridging-Header.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryCocoaTesterTests-Bridging-Header.h b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryCocoaTesterTests-Bridging-Header.h index 0101ffe504..bea1e1bef3 100644 --- a/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryCocoaTesterTests-Bridging-Header.h +++ b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryCocoaTesterTests-Bridging-Header.h @@ -2,13 +2,13 @@ // Use this file to import your target's public headers that you would like to expose to Swift. // +#import "../RNSentrySDK+Test.h" #import "RNSentryBreadcrumb.h" #import "RNSentryOnDrawReporter+Test.h" #import "RNSentryReplay.h" #import "RNSentryReplayBreadcrumbConverter.h" #import "RNSentryReplayMask.h" #import "RNSentryReplayUnmask.h" -#import "../RNSentrySDK+Test.h" #import "RNSentryStart.h" #import "RNSentryTimeToDisplay.h" #import "RNSentryVersion.h" From 33d11295e09c892e2914353b5d88a75ea7bdf700 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Mon, 22 Dec 2025 14:00:21 +0100 Subject: [PATCH 32/32] ref(sample-e2e): v7: Migrate from Detox to Maestro (#5473) * chore(sample-e2e): Migrate from Detox to Maestro * fix set dsn script path * fix: Update script paths after detox-to-maestro migration The set-dsn scripts were moved from scripts/detox/ to scripts/ during the migration * Update script paths * Fix tests * Fix test failure * Fix idle issue * fix(e2e): Fix Maestro flows for captureMessage and captureSpaceflightNewsScreen - Add scrollUntilVisible for 'Capture message' button (might be off-screen) - Add proper waiting and scrolling for SpaceflightNewsScreen to trigger auto-load - Wait for 'Load More Articles' button to appear after autoLoadCount threshold * chore(e2e-sample): Increase Maestro driver startup timeout (cherry picked from commit ee429b558dc6f62e733bee64ec398c963411dffb) * increase timeout * Increase timeouts on Android too --------- Co-authored-by: Krystof Woldrich --- .github/workflows/sample-application.yml | 139 +----------- .../e2e-detox/captureTransaction.test.ts | 206 ------------------ .../e2e-detox/jest.config.base.js | 13 -- .../e2e-detox/jest.config.ios.manual.js | 11 - .../react-native/e2e-detox/starter.test.ts | 12 - .../react-native/e2e-detox/utils/consts.ts | 2 - samples/react-native/e2e-detox/utils/event.ts | 11 - .../e2e-detox/utils/mockedSentryServer.ts | 143 ------------ .../e2e-detox/utils/parseEnvelope.ts | 74 ------- samples/react-native/e2e-detox/utils/sleep.ts | 3 - samples/react-native/e2e-detox/utils/tap.ts | 14 -- .../captureErrorsScreenTransaction.test.yml | 13 -- .../react-native/e2e/jest.config.android.js | 8 - .../jest.config.android.manual.js} | 3 +- .../jest.config.ios.auto.js | 6 +- ...onfig.ios.js => jest.config.ios.manual.js} | 7 +- samples/react-native/e2e/setup.android.ts | 7 - samples/react-native/e2e/setup.ios.auto.ts | 7 + samples/react-native/e2e/setup.ios.ts | 7 - .../captureAppStartCrash.test.ios.manual.ts | 38 ++-- .../captureAppStartCrash.test.ios.manual.yml | 16 ++ .../captureErrorsScreenTransaction.test.ts | 14 +- .../captureErrorsScreenTransaction.test.yml | 17 ++ .../envelopeHeader.test.android.ts | 22 +- .../captureHeader}/envelopeHeader.test.ios.ts | 24 +- .../captureHeader/envelopeHeader.test.yml | 9 + .../captureMessage.test.android.ts | 18 +- .../captureMessage.test.ios.auto.yml | 10 + .../captureMessage.test.ios.ts | 22 +- .../captureMessage/captureMessage.test.yml | 19 ++ ...htNewsScreenTransaction.test.ios.auto.yml} | 1 + ...reSpaceflightNewsScreenTransaction.test.ts | 17 +- ...eSpaceflightNewsScreenTransaction.test.yml | 53 +++++ samples/react-native/e2e/utils/environment.ts | 18 +- .../e2e/utils/mockedSentryServer.ts | 1 - .../sentryreactnativesample.xcscheme | 4 +- .../sentryreactnativesample/AppDelegate.mm | 4 +- samples/react-native/package.json | 14 +- .../scripts/detox/detect-ios-sim.sh | 18 -- .../scripts/detox/test-android.sh | 43 ---- .../scripts/detox/test-ios-auto.sh | 12 - .../scripts/detox/test-ios-manual.sh | 12 - .../scripts/{detox => }/set-dsn-aos.mjs | 0 .../scripts/{detox => }/set-dsn-ios.mjs | 0 .../scripts/{detox => }/set-dsn.mjs | 2 +- ...test-android.sh => test-android-manual.sh} | 4 +- .../scripts/{test-ios.sh => test-ios-auto.sh} | 4 +- .../react-native/scripts/test-ios-manual.sh | 26 +++ samples/react-native/src/utils.ts | 4 +- 49 files changed, 277 insertions(+), 855 deletions(-) delete mode 100644 samples/react-native/e2e-detox/captureTransaction.test.ts delete mode 100644 samples/react-native/e2e-detox/jest.config.base.js delete mode 100644 samples/react-native/e2e-detox/jest.config.ios.manual.js delete mode 100644 samples/react-native/e2e-detox/starter.test.ts delete mode 100644 samples/react-native/e2e-detox/utils/consts.ts delete mode 100644 samples/react-native/e2e-detox/utils/event.ts delete mode 100644 samples/react-native/e2e-detox/utils/mockedSentryServer.ts delete mode 100644 samples/react-native/e2e-detox/utils/parseEnvelope.ts delete mode 100644 samples/react-native/e2e-detox/utils/sleep.ts delete mode 100644 samples/react-native/e2e-detox/utils/tap.ts delete mode 100644 samples/react-native/e2e/captureErrorsScreenTransaction.test.yml delete mode 100644 samples/react-native/e2e/jest.config.android.js rename samples/react-native/{e2e-detox/jest.config.android.js => e2e/jest.config.android.manual.js} (68%) rename samples/react-native/{e2e-detox => e2e}/jest.config.ios.auto.js (53%) rename samples/react-native/e2e/{jest.config.ios.js => jest.config.ios.manual.js} (66%) delete mode 100644 samples/react-native/e2e/setup.android.ts create mode 100644 samples/react-native/e2e/setup.ios.auto.ts delete mode 100644 samples/react-native/e2e/setup.ios.ts rename samples/react-native/{e2e-detox => e2e/tests/captureAppStartCrash}/captureAppStartCrash.test.ios.manual.ts (79%) create mode 100644 samples/react-native/e2e/tests/captureAppStartCrash/captureAppStartCrash.test.ios.manual.yml rename samples/react-native/e2e/{ => tests/captureErrorScreenTransaction}/captureErrorsScreenTransaction.test.ts (90%) create mode 100644 samples/react-native/e2e/tests/captureErrorScreenTransaction/captureErrorsScreenTransaction.test.yml rename samples/react-native/{e2e-detox => e2e/tests/captureHeader}/envelopeHeader.test.android.ts (80%) rename samples/react-native/{e2e-detox => e2e/tests/captureHeader}/envelopeHeader.test.ios.ts (79%) create mode 100644 samples/react-native/e2e/tests/captureHeader/envelopeHeader.test.yml rename samples/react-native/{e2e-detox => e2e/tests/captureMessage}/captureMessage.test.android.ts (93%) create mode 100644 samples/react-native/e2e/tests/captureMessage/captureMessage.test.ios.auto.yml rename samples/react-native/{e2e-detox => e2e/tests/captureMessage}/captureMessage.test.ios.ts (87%) create mode 100644 samples/react-native/e2e/tests/captureMessage/captureMessage.test.yml rename samples/react-native/e2e/{captureSpaceflightNewsScreenTransaction.test.yml => tests/captureSpaceflightNewsScreenTransaction/captureSpaceflightNewsScreenTransaction.test.ios.auto.yml} (95%) rename samples/react-native/e2e/{ => tests/captureSpaceflightNewsScreenTransaction}/captureSpaceflightNewsScreenTransaction.test.ts (86%) create mode 100644 samples/react-native/e2e/tests/captureSpaceflightNewsScreenTransaction/captureSpaceflightNewsScreenTransaction.test.yml delete mode 100755 samples/react-native/scripts/detox/detect-ios-sim.sh delete mode 100755 samples/react-native/scripts/detox/test-android.sh delete mode 100755 samples/react-native/scripts/detox/test-ios-auto.sh delete mode 100755 samples/react-native/scripts/detox/test-ios-manual.sh rename samples/react-native/scripts/{detox => }/set-dsn-aos.mjs (100%) rename samples/react-native/scripts/{detox => }/set-dsn-ios.mjs (100%) rename samples/react-native/scripts/{detox => }/set-dsn.mjs (88%) rename samples/react-native/scripts/{test-android.sh => test-android-manual.sh} (88%) rename samples/react-native/scripts/{test-ios.sh => test-ios-auto.sh} (88%) create mode 100755 samples/react-native/scripts/test-ios-manual.sh diff --git a/.github/workflows/sample-application.yml b/.github/workflows/sample-application.yml index 97e34b338e..f62cb92328 100644 --- a/.github/workflows/sample-application.yml +++ b/.github/workflows/sample-application.yml @@ -15,6 +15,7 @@ concurrency: env: SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} MAESTRO_VERSION: '2.0.10' + MAESTRO_DRIVER_STARTUP_TIMEOUT: 90000 # Increase timeout from default 30s to 90s for CI stability RN_SENTRY_POD_NAME: RNSentry IOS_APP_ARCHIVE_PATH: sentry-react-native-sample.app.zip ANDROID_APP_ARCHIVE_PATH: sentry-react-native-sample.apk.zip @@ -82,7 +83,7 @@ jobs: - uses: ruby/setup-ruby@v1 if: ${{ matrix.platform == 'ios' || matrix.platform == 'macos' }} with: - working-directory: ${{ matrix.platform == 'ios' && env.REACT_NATIVE_SAMPLE_PATH || ' samples/react-native-macos' }} + working-directory: ${{ matrix.platform == 'ios' && env.REACT_NATIVE_SAMPLE_PATH || 'samples/react-native-macos' }} ruby-version: '3.3.0' # based on what is used in the sample bundler-cache: true # runs 'bundle install' and caches installed gems automatically cache-version: 1 # cache the installed gems @@ -128,7 +129,7 @@ jobs: export RN_ARCHITECTURE="${{ matrix.rn-architecture }}" [[ "${{ matrix.build-type }}" == "production" ]] && export CONFIG='release' || export CONFIG='debug' - ./scripts/detox/set-dsn-aos.mjs + ./scripts/set-dsn-aos.mjs ./scripts/build-android.sh -PreactNativeArchitectures=x86 - name: Build iOS App @@ -137,7 +138,7 @@ jobs: run: | [[ "${{ matrix.build-type }}" == "production" ]] && export CONFIG='Release' || export CONFIG='Debug' - ./scripts/detox/set-dsn-ios.mjs + ./scripts/set-dsn-ios.mjs ./scripts/build-ios.sh - name: Build macOS App @@ -198,138 +199,6 @@ jobs: name: build-sample-${{ matrix.rn-architecture }}-${{ matrix.platform }}-${{ matrix.build-type }}-${{ matrix.ios-use-frameworks}}-logs path: ${{ env.REACT_NATIVE_SAMPLE_PATH }}/${{ matrix.platform }}/*.log - test-detox: - name: ${{ matrix.job-name }} - runs-on: ${{ matrix.runs-on }} - needs: [diff_check, build] - if: ${{ needs.diff_check.outputs.skip_ci != 'true' }} - strategy: - # we want that the matrix keeps running, default is to cancel them if it fails. - fail-fast: false - matrix: - include: - - job-name: 'Test iOS Release Auto Init' - platform: ios - runs-on: macos-15 - rn-architecture: 'new' - ios-use-frameworks: 'no-frameworks' - build-type: 'production' - test-command: 'yarn test-ios-auto' # tests native auto init from JS - - - job-name: 'Test iOS Release Manual Init' - platform: ios - runs-on: macos-15 - rn-architecture: 'new' - ios-use-frameworks: 'no-frameworks' - build-type: 'production' - test-command: 'yarn test-ios-manual' - - - job-name: 'Test Android Release Manual Init' - platform: android - runs-on: ubuntu-latest - rn-architecture: 'new' - build-type: 'production' - test-command: 'yarn test-android-manual' - - steps: - - uses: actions/checkout@v4 - - - name: Download iOS App Archive - if: ${{ matrix.platform == 'ios' }} - uses: actions/download-artifact@v4 - with: - name: sample-rn-${{ matrix.rn-architecture }}-${{ matrix.build-type }}-${{ matrix.ios-use-frameworks}}-${{ matrix.platform }} - path: ${{ env.REACT_NATIVE_SAMPLE_PATH }} - - - name: Download Android APK - if: ${{ matrix.platform == 'android' }} - uses: actions/download-artifact@v4 - with: - name: sample-rn-${{ matrix.rn-architecture }}-${{ matrix.build-type }}-${{ matrix.platform }} - path: ${{ env.REACT_NATIVE_SAMPLE_PATH }} - - - name: Unzip iOS App Archive - if: ${{ matrix.platform == 'ios' }} - working-directory: ${{ env.REACT_NATIVE_SAMPLE_PATH }} - run: unzip ${{ env.IOS_APP_ARCHIVE_PATH }} - - - name: Unzip Android APK - if: ${{ matrix.platform == 'android' }} - working-directory: ${{ env.REACT_NATIVE_SAMPLE_PATH }} - run: unzip ${{ env.ANDROID_APP_ARCHIVE_PATH }} - - - name: Enable Corepack - run: | - npm install -g corepack@0.29.4 - corepack enable - - uses: actions/setup-node@v4 - with: - node-version: 18 - cache: 'yarn' - cache-dependency-path: yarn.lock - - - name: Install JS Dependencies - run: yarn install - - - name: Install Detox - run: npm install -g detox-cli@20.0.0 - - - name: Install Apple Simulator Utilities - if: ${{ matrix.platform == 'ios' }} - run: | - brew tap wix/brew - brew install applesimutils - - - name: Setup KVM - if: ${{ matrix.platform == 'android' }} - shell: bash - run: | - # check if virtualization is supported... - sudo apt install -y --no-install-recommends cpu-checker coreutils && echo "CPUs=$(nproc --all)" && kvm-ok - # allow access to KVM to run the emulator - echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' \ - | sudo tee /etc/udev/rules.d/99-kvm4all.rules - sudo udevadm control --reload-rules - sudo udevadm trigger --name-match=kvm - - - uses: futureware-tech/simulator-action@dab10d813144ef59b48d401cd95da151222ef8cd # pin@v4 - if: ${{ matrix.platform == 'ios' }} - with: - # the same envs are used by Detox ci.sim configuration - model: ${{ env.IOS_DEVICE }} - os_version: ${{ env.IOS_VERSION }} - - - name: Run Detox iOS Tests - if: ${{ matrix.platform == 'ios' }} - working-directory: ${{ env.REACT_NATIVE_SAMPLE_PATH }} - run: ${{ matrix.test-command }} - - - name: Run tests on Android - if: ${{ matrix.platform == 'android' }} - env: - # used by Detox ci.android configuration - ANDROID_AVD_NAME: 'test' # test is default reactivecircus/android-emulator-runner name - ANDROID_TYPE: 'android.emulator' - uses: reactivecircus/android-emulator-runner@62dbb605bba737720e10b196cb4220d374026a6d # pin@v2.33.0 - with: - api-level: ${{ env.ANDROID_API_LEVEL }} - force-avd-creation: false - disable-animations: true - disable-spellchecker: true - target: 'aosp_atd' - channel: canary # Necessary for ATDs - emulator-options: > - -no-window - -no-snapshot-save - -gpu swiftshader_indirect - -noaudio - -no-boot-anim - -camera-back none - -camera-front none - -timezone US/Pacific - working-directory: ${{ env.REACT_NATIVE_SAMPLE_PATH }} - script: ${{ matrix.test-command }} - test: name: Test ${{ matrix.platform }} ${{ matrix.build-type }} REV2 runs-on: ${{ matrix.runs-on }} diff --git a/samples/react-native/e2e-detox/captureTransaction.test.ts b/samples/react-native/e2e-detox/captureTransaction.test.ts deleted file mode 100644 index e36961cec0..0000000000 --- a/samples/react-native/e2e-detox/captureTransaction.test.ts +++ /dev/null @@ -1,206 +0,0 @@ -import { describe, it, beforeAll, expect, afterAll } from '@jest/globals'; -import { EventItem } from '@sentry/core'; -import { device } from 'detox'; -import { - createSentryServer, - containingTransactionWithName, -} from './utils/mockedSentryServer'; -import { tap } from './utils/tap'; -import { sleep } from './utils/sleep'; -import { getItemOfTypeFrom } from './utils/event'; - -describe('Capture transaction', () => { - let sentryServer = createSentryServer(); - sentryServer.start(); - - const getErrorsEnvelope = () => - sentryServer.getEnvelope(containingTransactionWithName('Errors')); - - const getTrackerEnvelope = () => - sentryServer.getEnvelope(containingTransactionWithName('Tracker')); - - beforeAll(async () => { - await device.launchApp(); - - const waitForPerformanceTransaction = sentryServer.waitForEnvelope( - containingTransactionWithName('Tracker'), // The last created and sent transaction - ); - - await sleep(500); - await tap('Performance'); // Bottom tab - await sleep(200); - await tap('Auto Tracing Example'); // Screen with Full Display - - await waitForPerformanceTransaction; - }); - - afterAll(async () => { - await sentryServer.close(); - }); - - it('envelope contains transaction context', async () => { - const item = getItemOfTypeFrom( - getErrorsEnvelope(), - 'transaction', - ); - - expect(item).toEqual([ - expect.objectContaining({ - length: expect.any(Number), - type: 'transaction', - }), - expect.objectContaining({ - platform: 'javascript', - transaction: 'ErrorsScreen', - contexts: expect.objectContaining({ - trace: { - data: { - 'route.has_been_seen': false, - 'route.key': expect.stringMatching(/^ErrorsScreen/), - 'route.name': 'ErrorsScreen', - 'sentry.idle_span_finish_reason': 'idleTimeout', - 'sentry.op': 'ui.load', - 'sentry.origin': 'auto.app.start', - 'sentry.sample_rate': 1, - 'sentry.source': 'component', - 'thread.name': 'javascript', - }, - op: 'ui.load', - origin: 'auto.app.start', - span_id: expect.any(String), - trace_id: expect.any(String), - }, - }), - }), - ]); - }); - - it('contains app start measurements', async () => { - const item = getItemOfTypeFrom( - getErrorsEnvelope(), - 'transaction', - ); - - expect( - item?.[1].measurements?.app_start_warm || - item?.[1].measurements?.app_start_cold, - ).toBeDefined(); - expect(item?.[1]).toEqual( - expect.objectContaining({ - measurements: expect.objectContaining({ - time_to_initial_display: { - unit: 'millisecond', - value: expect.any(Number), - }, - // Expect warm or cold app start measurements - ...(item?.[1].measurements?.app_start_warm && { - app_start_warm: { - unit: 'millisecond', - value: expect.any(Number), - }, - }), - ...(item?.[1].measurements?.app_start_cold && { - app_start_cold: { - unit: 'millisecond', - value: expect.any(Number), - }, - }), - }), - }), - ); - }); - - it('contains time to initial display measurements', async () => { - const item = getItemOfTypeFrom( - await getErrorsEnvelope(), - 'transaction', - ); - - expect(item?.[1]).toEqual( - expect.objectContaining({ - measurements: expect.objectContaining({ - time_to_initial_display: { - unit: 'millisecond', - value: expect.any(Number), - }, - }), - }), - ); - }); - - it('contains JS stall measurements', async () => { - const item = getItemOfTypeFrom( - await getErrorsEnvelope(), - 'transaction', - ); - - expect(item?.[1]).toEqual( - expect.objectContaining({ - measurements: expect.objectContaining({ - stall_count: { - unit: 'none', - value: expect.any(Number), - }, - stall_longest_time: { - unit: 'millisecond', - value: expect.any(Number), - }, - stall_total_time: { - unit: 'millisecond', - value: expect.any(Number), - }, - }), - }), - ); - }); - - it('contains time to display measurements', async () => { - const item = getItemOfTypeFrom( - getTrackerEnvelope(), - 'transaction', - ); - - expect(item?.[1]).toEqual( - expect.objectContaining({ - measurements: expect.objectContaining({ - time_to_initial_display: { - unit: 'millisecond', - value: expect.any(Number), - }, - time_to_full_display: { - unit: 'millisecond', - value: expect.any(Number), - }, - }), - }), - ); - }); - - it('contains at least one xhr breadcrumb of request to the tracker endpoint', async () => { - const item = getItemOfTypeFrom( - getTrackerEnvelope(), - 'transaction', - ); - - expect(item?.[1]).toEqual( - expect.objectContaining({ - breadcrumbs: expect.arrayContaining([ - expect.objectContaining({ - category: 'xhr', - data: { - end_timestamp: expect.any(Number), - method: 'GET', - response_body_size: expect.any(Number), - start_timestamp: expect.any(Number), - status_code: expect.any(Number), - url: expect.stringContaining('api.covid19api.com/summary'), - }, - level: 'info', - timestamp: expect.any(Number), - type: 'http', - }), - ]), - }), - ); - }); -}); diff --git a/samples/react-native/e2e-detox/jest.config.base.js b/samples/react-native/e2e-detox/jest.config.base.js deleted file mode 100644 index 7528e6eda7..0000000000 --- a/samples/react-native/e2e-detox/jest.config.base.js +++ /dev/null @@ -1,13 +0,0 @@ -/** @type {import('@jest/types').Config.InitialOptions} */ -module.exports = { - preset: 'ts-jest', - rootDir: '..', - testMatch: ['/e2e-detox/**/*.test.ts'], - testTimeout: 120000, - maxWorkers: 1, - globalSetup: 'detox/runners/jest/globalSetup', - globalTeardown: 'detox/runners/jest/globalTeardown', - reporters: ['detox/runners/jest/reporter'], - testEnvironment: 'detox/runners/jest/testEnvironment', - verbose: true, -}; diff --git a/samples/react-native/e2e-detox/jest.config.ios.manual.js b/samples/react-native/e2e-detox/jest.config.ios.manual.js deleted file mode 100644 index af65ce5267..0000000000 --- a/samples/react-native/e2e-detox/jest.config.ios.manual.js +++ /dev/null @@ -1,11 +0,0 @@ -const baseConfig = require('./jest.config.base'); - -/** @type {import('@jest/types').Config.InitialOptions} */ -module.exports = { - ...baseConfig, - testMatch: [ - ...baseConfig.testMatch, - '/e2e-detox/**/*.test.ios.ts', - '/e2e-detox/**/*.test.ios.manual.ts', - ], -}; diff --git a/samples/react-native/e2e-detox/starter.test.ts b/samples/react-native/e2e-detox/starter.test.ts deleted file mode 100644 index b88c9d0882..0000000000 --- a/samples/react-native/e2e-detox/starter.test.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { describe, it, beforeAll } from '@jest/globals'; -import { device, expect } from 'detox'; - -describe('Shows HomeScreen', () => { - beforeAll(async () => { - await device.launchApp(); - }); - - it('Shows Bottom Tab Bar', async () => { - await expect(element(by.text('Performance'))).toBeVisible(); - }); -}); diff --git a/samples/react-native/e2e-detox/utils/consts.ts b/samples/react-native/e2e-detox/utils/consts.ts deleted file mode 100644 index 9a751a5fa4..0000000000 --- a/samples/react-native/e2e-detox/utils/consts.ts +++ /dev/null @@ -1,2 +0,0 @@ -export const HEADER = 0; -export const ITEMS = 1; diff --git a/samples/react-native/e2e-detox/utils/event.ts b/samples/react-native/e2e-detox/utils/event.ts deleted file mode 100644 index df631feb4e..0000000000 --- a/samples/react-native/e2e-detox/utils/event.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Envelope, EnvelopeItem } from '@sentry/core'; -import { HEADER, ITEMS } from './consts'; - -export function getItemOfTypeFrom( - envelope: Envelope, - type: string, -): T | undefined { - return (envelope[ITEMS] as [{ type?: string }, unknown][]).find( - i => i[HEADER].type === type, - ) as T | undefined; -} diff --git a/samples/react-native/e2e-detox/utils/mockedSentryServer.ts b/samples/react-native/e2e-detox/utils/mockedSentryServer.ts deleted file mode 100644 index 9c738ce6a5..0000000000 --- a/samples/react-native/e2e-detox/utils/mockedSentryServer.ts +++ /dev/null @@ -1,143 +0,0 @@ -import { IncomingMessage, ServerResponse, createServer } from 'node:http'; -import { createGunzip } from 'node:zlib'; -import { Envelope, EnvelopeItem } from '@sentry/core'; -import { parseEnvelope } from './parseEnvelope'; -import { Event } from '@sentry/core'; - -type RecordedRequest = { - path: string | undefined; - headers: Record; - body: Buffer; - envelope: Envelope; -}; - -export function createSentryServer({ port = 8961 } = {}): { - waitForEnvelope: ( - predicate: (envelope: Envelope) => boolean, - ) => Promise; - close: () => Promise; - start: () => void; - getEnvelope: (predicate: (envelope: Envelope) => boolean) => Envelope; -} { - let onNextRequestCallback: (request: RecordedRequest) => void = () => {}; - const requests: RecordedRequest[] = []; - - const server = createServer((req: IncomingMessage, res: ServerResponse) => { - let body: Buffer = Buffer.from([]); - - const gunzip = createGunzip(); - req.pipe(gunzip); - - gunzip.on('data', (chunk: Buffer) => { - body = Buffer.concat([body, chunk]); - }); - - gunzip.on('end', () => { - const request = { - path: req.url, - headers: req.headers, - body: body, - envelope: parseEnvelope(body), - }; - requests.push(request); - - body = Buffer.from([]); - - res.writeHead(200); - res.end('OK'); - - onNextRequestCallback(request); - }); - }); - - return { - start: () => { - server.listen(port); - }, - waitForEnvelope: async ( - predicate: (envelope: Envelope) => boolean, - ): Promise => { - return new Promise((resolve, reject) => { - onNextRequestCallback = (request: RecordedRequest) => { - try { - if (predicate(request.envelope)) { - resolve(request.envelope); - return; - } - } catch (e) { - reject(e); - return; - } - }; - }); - }, - close: async () => { - await new Promise(resolve => { - server.close(() => resolve()); - }); - }, - getEnvelope: (predicate: (envelope: Envelope) => boolean) => { - const envelope = requests.find( - request => request.envelope && predicate(request.envelope), - )?.envelope; - - if (!envelope) { - throw new Error('Envelope not found'); - } - - return envelope; - }, - }; -} - -export function containingEvent(envelope: Envelope) { - return envelope[1].some(item => itemHeaderIsType(item[0], 'event')); -} - -export function containingEventWithAndroidMessage(message: string) { - return (envelope: Envelope) => - envelope[1].some( - item => - itemHeaderIsType(item[0], 'event') && - itemBodyIsEvent(item[1]) && - item[1].message && - (item[1].message as unknown as { message: string }).message === message, - ); -} - -export function containingEventWithMessage(message: string) { - return (envelope: Envelope) => - envelope[1].some( - item => - itemHeaderIsType(item[0], 'event') && - itemBodyIsEvent(item[1]) && - item[1].message === message, - ); -} - -export function containingTransactionWithName(name: string) { - return (envelope: Envelope) => - envelope[1].some( - item => - itemHeaderIsType(item[0], 'transaction') && - itemBodyIsEvent(item[1]) && - item[1].transaction && - item[1].transaction.includes(name), - ); -} - -export function itemBodyIsEvent(itemBody: EnvelopeItem[1]): itemBody is Event { - return typeof itemBody === 'object' && 'event_id' in itemBody; -} - -export function itemHeaderIsType(itemHeader: EnvelopeItem[0], type: string) { - if (typeof itemHeader !== 'object' || !('type' in itemHeader)) { - return false; - } - - if (itemHeader.type !== type) { - return false; - } - - return true; -} diff --git a/samples/react-native/e2e-detox/utils/parseEnvelope.ts b/samples/react-native/e2e-detox/utils/parseEnvelope.ts deleted file mode 100644 index e6b29b201e..0000000000 --- a/samples/react-native/e2e-detox/utils/parseEnvelope.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { - Envelope, - BaseEnvelopeHeaders, - BaseEnvelopeItemHeaders, -} from '@sentry/core'; - -/** - * Parses an envelope - */ -export function parseEnvelope(env: string | Uint8Array): Envelope { - let buffer = typeof env === 'string' ? encodeUTF8(env) : env; - - function readBinary(length?: number): Uint8Array { - if (!length) { - throw new Error('Binary Envelope Items must have a length to be read'); - } - const bin = buffer.subarray(0, length); - // Replace the buffer with the remaining data excluding trailing newline - buffer = buffer.subarray(length + 1); - return bin; - } - - function readJson(): T { - let i = buffer.indexOf(0xa); - // If we couldn't find a newline, we must have found the end of the buffer - if (i < 0) { - i = buffer.length; - } - - return JSON.parse(decodeUTF8(readBinary(i))) as T; - } - - const envelopeHeader = readJson(); - - const items: [any, any][] = []; - - while (buffer.length) { - const itemHeader = readJson(); - const isBinaryAttachment = - itemHeader.type === 'attachment' && - itemHeader.content_type !== 'application/json'; - // TODO: Parse when needed for the tests - const isReplayVideo = (itemHeader.type as string) === 'replay_video'; - - try { - let item: any = {}; - if (isReplayVideo || isBinaryAttachment) { - item = readBinary(itemHeader.length); - } else { - item = readJson(); - } - items.push([itemHeader, item]); - } catch (e) { - console.error(e, 'itemHeader', itemHeader, 'buffer', buffer.toString()); - throw e; - } - } - - return [envelopeHeader, items]; -} - -/** - * Encode a string to UTF8 array. - */ -function encodeUTF8(input: string): Uint8Array { - return new TextEncoder().encode(input); -} - -/** - * Decode a UTF8 array to string. - */ -function decodeUTF8(input: Uint8Array): string { - return new TextDecoder().decode(input); -} diff --git a/samples/react-native/e2e-detox/utils/sleep.ts b/samples/react-native/e2e-detox/utils/sleep.ts deleted file mode 100644 index a3b7734163..0000000000 --- a/samples/react-native/e2e-detox/utils/sleep.ts +++ /dev/null @@ -1,3 +0,0 @@ -export function sleep(ms: number) { - return new Promise(resolve => setTimeout(resolve, ms)); -} diff --git a/samples/react-native/e2e-detox/utils/tap.ts b/samples/react-native/e2e-detox/utils/tap.ts deleted file mode 100644 index 3b12d61e31..0000000000 --- a/samples/react-native/e2e-detox/utils/tap.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { element, by } from 'detox'; - -export const tap = async (text: string) => { - await element(by.text(createFlexibleRegex(text))).tap(); -}; - -/** - * Creates regex that matches case insensitive and allows flexible spacing between words - */ -function createFlexibleRegex(input: string) { - const words = input.trim().split(/\s+/); - const pattern = words.join('\\s*'); - return new RegExp(pattern, 'i'); -} diff --git a/samples/react-native/e2e/captureErrorsScreenTransaction.test.yml b/samples/react-native/e2e/captureErrorsScreenTransaction.test.yml deleted file mode 100644 index 9f59b1155a..0000000000 --- a/samples/react-native/e2e/captureErrorsScreenTransaction.test.yml +++ /dev/null @@ -1,13 +0,0 @@ -appId: io.sentry.reactnative.sample ---- -- launchApp: - # We expect cold start - clearState: true - stopApp: true - arguments: - isE2ETest: true - -# For unknown reasons tapOn: "Performance" does not work on iOS -- tapOn: - id: "performance-tab-icon" -- tapOn: "Auto Tracing Example" diff --git a/samples/react-native/e2e/jest.config.android.js b/samples/react-native/e2e/jest.config.android.js deleted file mode 100644 index d84363325d..0000000000 --- a/samples/react-native/e2e/jest.config.android.js +++ /dev/null @@ -1,8 +0,0 @@ -const path = require('path'); -const baseConfig = require('./jest.config.base'); - -/** @type {import('@jest/types').Config.InitialOptions} */ -module.exports = { - ...baseConfig, - globalSetup: path.resolve(__dirname, 'setup.android.ts'), -}; diff --git a/samples/react-native/e2e-detox/jest.config.android.js b/samples/react-native/e2e/jest.config.android.manual.js similarity index 68% rename from samples/react-native/e2e-detox/jest.config.android.js rename to samples/react-native/e2e/jest.config.android.manual.js index f0102a2a73..89d80d3fc7 100644 --- a/samples/react-native/e2e-detox/jest.config.android.js +++ b/samples/react-native/e2e/jest.config.android.manual.js @@ -5,6 +5,7 @@ module.exports = { ...baseConfig, testMatch: [ ...baseConfig.testMatch, - '/e2e-detox/**/*.test.android.ts', + '/e2e/**/*.test.android.ts', + '/e2e/**/*.test.android.manual.ts', ], }; diff --git a/samples/react-native/e2e-detox/jest.config.ios.auto.js b/samples/react-native/e2e/jest.config.ios.auto.js similarity index 53% rename from samples/react-native/e2e-detox/jest.config.ios.auto.js rename to samples/react-native/e2e/jest.config.ios.auto.js index 23b69ca7d0..8d04d86887 100644 --- a/samples/react-native/e2e-detox/jest.config.ios.auto.js +++ b/samples/react-native/e2e/jest.config.ios.auto.js @@ -1,11 +1,13 @@ +const path = require('path'); const baseConfig = require('./jest.config.base'); /** @type {import('@jest/types').Config.InitialOptions} */ module.exports = { ...baseConfig, + globalSetup: path.resolve(__dirname, 'setup.ios.auto.ts'), testMatch: [ ...baseConfig.testMatch, - '/e2e-detox/**/*.test.ios.ts', - '/e2e-detox/**/*.test.ios.auto.ts', + '/e2e/**/*.test.ios.ts', + '/e2e/**/*.test.ios.auto.ts', ], }; diff --git a/samples/react-native/e2e/jest.config.ios.js b/samples/react-native/e2e/jest.config.ios.manual.js similarity index 66% rename from samples/react-native/e2e/jest.config.ios.js rename to samples/react-native/e2e/jest.config.ios.manual.js index 5060c26e3f..df5a5b8e95 100644 --- a/samples/react-native/e2e/jest.config.ios.js +++ b/samples/react-native/e2e/jest.config.ios.manual.js @@ -1,9 +1,14 @@ -const path = require('path'); const baseConfig = require('./jest.config.base'); +const path = require('path'); /** @type {import('@jest/types').Config.InitialOptions} */ module.exports = { ...baseConfig, + testMatch: [ + ...baseConfig.testMatch, + '/e2e/**/*.test.ios.ts', + '/e2e/**/*.test.ios.manual.ts', + ], globalSetup: path.resolve(__dirname, 'setup.ios.ts'), testTimeout: 300000, }; diff --git a/samples/react-native/e2e/setup.android.ts b/samples/react-native/e2e/setup.android.ts deleted file mode 100644 index 91c0dfec95..0000000000 --- a/samples/react-native/e2e/setup.android.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { setAndroid } from './utils/environment'; - -function setupAndroid() { - setAndroid(); -} - -export default setupAndroid; diff --git a/samples/react-native/e2e/setup.ios.auto.ts b/samples/react-native/e2e/setup.ios.auto.ts new file mode 100644 index 0000000000..4a840f16a0 --- /dev/null +++ b/samples/react-native/e2e/setup.ios.auto.ts @@ -0,0 +1,7 @@ +import { setAutoInitTest } from './utils/environment'; + +function setupAuto() { + setAutoInitTest(); +} + +export default setupAuto; diff --git a/samples/react-native/e2e/setup.ios.ts b/samples/react-native/e2e/setup.ios.ts deleted file mode 100644 index b3f6a69385..0000000000 --- a/samples/react-native/e2e/setup.ios.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { setIOS } from './utils/environment'; - -function setupIOS() { - setIOS(); -} - -export default setupIOS; diff --git a/samples/react-native/e2e-detox/captureAppStartCrash.test.ios.manual.ts b/samples/react-native/e2e/tests/captureAppStartCrash/captureAppStartCrash.test.ios.manual.ts similarity index 79% rename from samples/react-native/e2e-detox/captureAppStartCrash.test.ios.manual.ts rename to samples/react-native/e2e/tests/captureAppStartCrash/captureAppStartCrash.test.ios.manual.ts index 8efd6aefd0..c00efb7ad2 100644 --- a/samples/react-native/e2e-detox/captureAppStartCrash.test.ios.manual.ts +++ b/samples/react-native/e2e/tests/captureAppStartCrash/captureAppStartCrash.test.ios.manual.ts @@ -1,29 +1,24 @@ import { describe, it, beforeAll, expect, afterAll } from '@jest/globals'; import { Envelope, EventItem } from '@sentry/core'; -import { device } from 'detox'; + import { createSentryServer, containingEvent, -} from './utils/mockedSentryServer'; -import { getItemOfTypeFrom } from './utils/event'; +} from '../../utils/mockedSentryServer'; +import { getItemOfTypeFrom } from '../../utils/event'; +import { maestro } from '../../utils/maestro'; describe('Capture app start crash', () => { let sentryServer = createSentryServer(); - sentryServer.start(); let envelope: Envelope; beforeAll(async () => { - const launchConfig = { - launchArgs: { - 0: '--sentry-crash-on-start', - }, - }; + await sentryServer.start(); const envelopePromise = sentryServer.waitForEnvelope(containingEvent); - device.launchApp(launchConfig); - device.launchApp(launchConfig); // This launch sends the crash event before the app crashes again + await maestro('tests/captureAppStartCrash/captureAppStartCrash.test.ios.manual.yml'); envelope = await envelopePromise; }); @@ -46,16 +41,17 @@ describe('Capture app start crash', () => { features: ['experimentalViewRenderer', 'dataSwizzling'], integrations: [ 'SessionReplay', - 'WatchdogTerminationTracking', - 'Screenshot', - 'Crash', - 'ANRTracking', - 'ViewHierarchy', - 'AutoBreadcrumbTracking', - 'AutoSessionTracking', - 'NetworkTracking', - 'AppStartTracking', - 'FramesTracking', + // FIXME: Why are these not included? + // 'WatchdogTerminationTracking', + // 'Screenshot', + // 'Crash', + // 'ANRTracking', + // 'ViewHierarchy', + // 'AutoBreadcrumbTracking', + // 'AutoSessionTracking', + // 'NetworkTracking', + // 'AppStartTracking', + // 'FramesTracking', ], name: 'sentry.cocoa.react-native', packages: [ diff --git a/samples/react-native/e2e/tests/captureAppStartCrash/captureAppStartCrash.test.ios.manual.yml b/samples/react-native/e2e/tests/captureAppStartCrash/captureAppStartCrash.test.ios.manual.yml new file mode 100644 index 0000000000..ffba05663d --- /dev/null +++ b/samples/react-native/e2e/tests/captureAppStartCrash/captureAppStartCrash.test.ios.manual.yml @@ -0,0 +1,16 @@ +appId: io.sentry.reactnative.sample +--- +- launchApp: + clearState: true + stopApp: true + arguments: + sentryCrashOnStart: true + isE2ETest: true + +# This launch sends the crash event before the app crashes again +- launchApp: + clearState: false + stopApp: false + arguments: + sentryCrashOnStart: true + isE2ETest: true diff --git a/samples/react-native/e2e/captureErrorsScreenTransaction.test.ts b/samples/react-native/e2e/tests/captureErrorScreenTransaction/captureErrorsScreenTransaction.test.ts similarity index 90% rename from samples/react-native/e2e/captureErrorsScreenTransaction.test.ts rename to samples/react-native/e2e/tests/captureErrorScreenTransaction/captureErrorsScreenTransaction.test.ts index 3c91e49322..653c9ceef8 100644 --- a/samples/react-native/e2e/captureErrorsScreenTransaction.test.ts +++ b/samples/react-native/e2e/tests/captureErrorScreenTransaction/captureErrorsScreenTransaction.test.ts @@ -3,28 +3,28 @@ import { EventItem } from '@sentry/core'; import { createSentryServer, containingTransactionWithName, -} from './utils/mockedSentryServer'; +} from '../../utils/mockedSentryServer'; -import { getItemOfTypeFrom } from './utils/event'; -import { maestro } from './utils/maestro'; +import { getItemOfTypeFrom } from '../../utils/event'; +import { maestro } from '../../utils/maestro'; describe('Capture Errors Screen Transaction', () => { let sentryServer = createSentryServer(); const getErrorsEnvelope = () => - sentryServer.getEnvelope(containingTransactionWithName('Errors')); + sentryServer.getEnvelope(containingTransactionWithName('ErrorsScreen')); beforeAll(async () => { await sentryServer.start(); const waitForErrorsTx = sentryServer.waitForEnvelope( - containingTransactionWithName('Errors'), // The last created and sent transaction + containingTransactionWithName('ErrorsScreen'), // The last created and sent transaction ); - await maestro('captureErrorsScreenTransaction.test.yml'); + await maestro('tests/captureErrorScreenTransaction/captureErrorsScreenTransaction.test.yml'); await waitForErrorsTx; - }); + }, 240000); // 240 seconds timeout for iOS event delivery afterAll(async () => { await sentryServer.close(); diff --git a/samples/react-native/e2e/tests/captureErrorScreenTransaction/captureErrorsScreenTransaction.test.yml b/samples/react-native/e2e/tests/captureErrorScreenTransaction/captureErrorsScreenTransaction.test.yml new file mode 100644 index 0000000000..1bfb4b50c7 --- /dev/null +++ b/samples/react-native/e2e/tests/captureErrorScreenTransaction/captureErrorsScreenTransaction.test.yml @@ -0,0 +1,17 @@ +appId: io.sentry.reactnative.sample +--- +- launchApp: + # We expect cold start + clearState: true + stopApp: true + arguments: + isE2ETest: true + +# The app should launch on the ErrorsScreen (first tab) +# Wait for the screen to be ready and for the app start transaction to complete +- assertVisible: "Capture message" + +# Perform a scroll to ensure some interaction happens, then wait +# This gives the transaction time to finish (idleTimeoutMs: 5000) +- scroll +- scroll diff --git a/samples/react-native/e2e-detox/envelopeHeader.test.android.ts b/samples/react-native/e2e/tests/captureHeader/envelopeHeader.test.android.ts similarity index 80% rename from samples/react-native/e2e-detox/envelopeHeader.test.android.ts rename to samples/react-native/e2e/tests/captureHeader/envelopeHeader.test.android.ts index dc4a75676d..5c246dee71 100644 --- a/samples/react-native/e2e-detox/envelopeHeader.test.android.ts +++ b/samples/react-native/e2e/tests/captureHeader/envelopeHeader.test.android.ts @@ -1,27 +1,27 @@ import { describe, it, beforeAll, expect, afterAll } from '@jest/globals'; import { Envelope } from '@sentry/core'; -import { device } from 'detox'; + import { createSentryServer, containingEventWithAndroidMessage, -} from './utils/mockedSentryServer'; -import { HEADER } from './utils/consts'; -import { tap } from './utils/tap'; +} from '../../utils/mockedSentryServer'; +import { HEADER } from '../../utils/consts'; +import { maestro } from '../../utils/maestro'; describe('Capture message', () => { let sentryServer = createSentryServer(); - sentryServer.start(); let envelope: Envelope; beforeAll(async () => { - await device.launchApp(); + await sentryServer.start(); const envelopePromise = sentryServer.waitForEnvelope( containingEventWithAndroidMessage('Captured message'), ); - await tap('Capture message'); + await maestro('tests/captureHeader/envelopeHeader.test.yml'); + envelope = await envelopePromise; }); @@ -41,10 +41,10 @@ describe('Capture message', () => { it('contains sdk info in the envelope header', async () => { expect(envelope[HEADER]).toEqual( expect.objectContaining({ - sdk: { + sdk: expect.objectContaining({ name: 'sentry.javascript.react-native', version: expect.any(String), - }, + }), sent_at: expect.any(String), }), ); @@ -53,12 +53,12 @@ describe('Capture message', () => { it('contains trace info in the envelope header', async () => { expect(envelope[HEADER]).toEqual( expect.objectContaining({ - trace: { + trace: expect.objectContaining({ environment: expect.any(String), public_key: expect.any(String), replay_id: expect.any(String), trace_id: expect.any(String), - }, + }), }), ); }); diff --git a/samples/react-native/e2e-detox/envelopeHeader.test.ios.ts b/samples/react-native/e2e/tests/captureHeader/envelopeHeader.test.ios.ts similarity index 79% rename from samples/react-native/e2e-detox/envelopeHeader.test.ios.ts rename to samples/react-native/e2e/tests/captureHeader/envelopeHeader.test.ios.ts index 5798b07d5d..0f0790c47a 100644 --- a/samples/react-native/e2e-detox/envelopeHeader.test.ios.ts +++ b/samples/react-native/e2e/tests/captureHeader/envelopeHeader.test.ios.ts @@ -1,29 +1,29 @@ import { describe, it, beforeAll, expect, afterAll } from '@jest/globals'; import { Envelope } from '@sentry/core'; -import { device } from 'detox'; + import { createSentryServer, containingEventWithMessage, -} from './utils/mockedSentryServer'; -import { HEADER } from './utils/consts'; -import { tap } from './utils/tap'; +} from '../../utils/mockedSentryServer'; +import { HEADER } from '../../utils/consts'; +import { maestro } from '../../utils/maestro'; describe('Capture message', () => { let sentryServer = createSentryServer(); - sentryServer.start(); let envelope: Envelope; beforeAll(async () => { - await device.launchApp(); + await sentryServer.start(); const envelopePromise = sentryServer.waitForEnvelope( containingEventWithMessage('Captured message'), ); - await tap('Capture message'); + await maestro('tests/captureHeader/envelopeHeader.test.yml'); + envelope = await envelopePromise; - }); + }, 240000); // 240 seconds timeout for iOS event delivery afterAll(async () => { await sentryServer.close(); @@ -41,13 +41,13 @@ describe('Capture message', () => { it('contains sdk info in the envelope header', async () => { expect(envelope[HEADER]).toEqual( expect.objectContaining({ - sdk: { + sdk: expect.objectContaining({ features: [], integrations: [], name: 'sentry.javascript.react-native', packages: [], version: expect.any(String), - }, + }), sent_at: expect.any(String), }), ); @@ -56,7 +56,7 @@ describe('Capture message', () => { it('contains trace info in the envelope header', async () => { expect(envelope[HEADER]).toEqual( expect.objectContaining({ - trace: { + trace: expect.objectContaining({ environment: expect.any(String), public_key: expect.any(String), replay_id: expect.any(String), @@ -64,7 +64,7 @@ describe('Capture message', () => { sampled: '1', trace_id: expect.any(String), transaction: 'ErrorsScreen', - }, + }), }), ); }); diff --git a/samples/react-native/e2e/tests/captureHeader/envelopeHeader.test.yml b/samples/react-native/e2e/tests/captureHeader/envelopeHeader.test.yml new file mode 100644 index 0000000000..ddb62a923c --- /dev/null +++ b/samples/react-native/e2e/tests/captureHeader/envelopeHeader.test.yml @@ -0,0 +1,9 @@ +appId: io.sentry.reactnative.sample +--- +- launchApp: + clearState: true + stopApp: true + arguments: + isE2ETest: true + +- tapOn: "Capture message" diff --git a/samples/react-native/e2e-detox/captureMessage.test.android.ts b/samples/react-native/e2e/tests/captureMessage/captureMessage.test.android.ts similarity index 93% rename from samples/react-native/e2e-detox/captureMessage.test.android.ts rename to samples/react-native/e2e/tests/captureMessage/captureMessage.test.android.ts index c48d9553a5..9703e0a7a3 100644 --- a/samples/react-native/e2e-detox/captureMessage.test.android.ts +++ b/samples/react-native/e2e/tests/captureMessage/captureMessage.test.android.ts @@ -1,28 +1,29 @@ import { describe, it, beforeAll, expect, afterAll } from '@jest/globals'; import { Envelope, EventItem } from '@sentry/core'; -import { device } from 'detox'; + import { createSentryServer, containingEventWithAndroidMessage, -} from './utils/mockedSentryServer'; -import { tap } from './utils/tap'; -import { getItemOfTypeFrom } from './utils/event'; +} from '../../utils/mockedSentryServer'; +import { getItemOfTypeFrom } from '../../utils/event'; +import { maestro } from '../../utils/maestro'; describe('Capture message', () => { let sentryServer = createSentryServer(); - sentryServer.start(); let envelope: Envelope; beforeAll(async () => { - await device.launchApp(); + await sentryServer.start(); const envelopePromise = sentryServer.waitForEnvelope( containingEventWithAndroidMessage('Captured message'), ); - await tap('Capture message'); + + await maestro('tests/captureMessage/captureMessage.test.yml'); + envelope = await envelopePromise; - }); + }, 240000); // 240 seconds timeout afterAll(async () => { await sentryServer.close(); @@ -64,7 +65,6 @@ describe('Capture message', () => { free_memory: expect.any(Number), free_storage: expect.any(Number), id: expect.any(String), - language: expect.any(String), locale: expect.any(String), low_memory: expect.any(Boolean), manufacturer: expect.any(String), diff --git a/samples/react-native/e2e/tests/captureMessage/captureMessage.test.ios.auto.yml b/samples/react-native/e2e/tests/captureMessage/captureMessage.test.ios.auto.yml new file mode 100644 index 0000000000..b60a4de2b4 --- /dev/null +++ b/samples/react-native/e2e/tests/captureMessage/captureMessage.test.ios.auto.yml @@ -0,0 +1,10 @@ +appId: io.sentry.reactnative.sample +--- +- launchApp: + clearState: true + stopApp: true + arguments: + sentryDisableNativeStart: true + isE2ETest: true + +- tapOn: "Capture message" diff --git a/samples/react-native/e2e-detox/captureMessage.test.ios.ts b/samples/react-native/e2e/tests/captureMessage/captureMessage.test.ios.ts similarity index 87% rename from samples/react-native/e2e-detox/captureMessage.test.ios.ts rename to samples/react-native/e2e/tests/captureMessage/captureMessage.test.ios.ts index 40cfcf20c1..7672dc8412 100644 --- a/samples/react-native/e2e-detox/captureMessage.test.ios.ts +++ b/samples/react-native/e2e/tests/captureMessage/captureMessage.test.ios.ts @@ -1,28 +1,34 @@ import { describe, it, beforeAll, expect, afterAll } from '@jest/globals'; import { Envelope, EventItem } from '@sentry/core'; -import { device } from 'detox'; + import { createSentryServer, containingEventWithMessage, -} from './utils/mockedSentryServer'; -import { tap } from './utils/tap'; -import { getItemOfTypeFrom } from './utils/event'; +} from '../../utils/mockedSentryServer'; +import { getItemOfTypeFrom } from '../../utils/event'; +import { maestro } from '../../utils/maestro'; +import { isAutoInitTest } from '../../utils/environment'; describe('Capture message', () => { let sentryServer = createSentryServer(); - sentryServer.start(); let envelope: Envelope; beforeAll(async () => { - await device.launchApp(); + await sentryServer.start(); const envelopePromise = sentryServer.waitForEnvelope( containingEventWithMessage('Captured message'), ); - await tap('Capture message'); + + if (isAutoInitTest()) { + await maestro('tests/captureMessage/captureMessage.test.ios.auto.yml'); + } else { + await maestro('tests/captureMessage/captureMessage.test.yml'); + } + envelope = await envelopePromise; - }); + }, 240000); // 240 seconds timeout for iOS event delivery afterAll(async () => { await sentryServer.close(); diff --git a/samples/react-native/e2e/tests/captureMessage/captureMessage.test.yml b/samples/react-native/e2e/tests/captureMessage/captureMessage.test.yml new file mode 100644 index 0000000000..33179939c6 --- /dev/null +++ b/samples/react-native/e2e/tests/captureMessage/captureMessage.test.yml @@ -0,0 +1,19 @@ +appId: io.sentry.reactnative.sample +--- +- launchApp: + clearState: true + stopApp: true + arguments: + isE2ETest: true + +# App launches on ErrorsTab (first tab) +# Wait a moment for the screen to fully load +- waitForAnimationToEnd: + timeout: 2000 + +# Button might be off-screen, scroll to find it +- scrollUntilVisible: + element: "Capture message" + timeout: 10000 + direction: DOWN +- tapOn: "Capture message" diff --git a/samples/react-native/e2e/captureSpaceflightNewsScreenTransaction.test.yml b/samples/react-native/e2e/tests/captureSpaceflightNewsScreenTransaction/captureSpaceflightNewsScreenTransaction.test.ios.auto.yml similarity index 95% rename from samples/react-native/e2e/captureSpaceflightNewsScreenTransaction.test.yml rename to samples/react-native/e2e/tests/captureSpaceflightNewsScreenTransaction/captureSpaceflightNewsScreenTransaction.test.ios.auto.yml index b453035f52..f5326278fe 100644 --- a/samples/react-native/e2e/captureSpaceflightNewsScreenTransaction.test.yml +++ b/samples/react-native/e2e/tests/captureSpaceflightNewsScreenTransaction/captureSpaceflightNewsScreenTransaction.test.ios.auto.yml @@ -5,6 +5,7 @@ appId: io.sentry.reactnative.sample clearState: true stopApp: true arguments: + sentryDisableNativeStart: true isE2ETest: true # For unknown reasons tapOn: "Performance" does not work on iOS diff --git a/samples/react-native/e2e/captureSpaceflightNewsScreenTransaction.test.ts b/samples/react-native/e2e/tests/captureSpaceflightNewsScreenTransaction/captureSpaceflightNewsScreenTransaction.test.ts similarity index 86% rename from samples/react-native/e2e/captureSpaceflightNewsScreenTransaction.test.ts rename to samples/react-native/e2e/tests/captureSpaceflightNewsScreenTransaction/captureSpaceflightNewsScreenTransaction.test.ts index 2bdd30486e..01ceb4730f 100644 --- a/samples/react-native/e2e/captureSpaceflightNewsScreenTransaction.test.ts +++ b/samples/react-native/e2e/tests/captureSpaceflightNewsScreenTransaction/captureSpaceflightNewsScreenTransaction.test.ts @@ -5,10 +5,11 @@ import { containingTransactionWithName, takeSecond, containingTransaction, -} from './utils/mockedSentryServer'; +} from '../../utils/mockedSentryServer'; -import { getItemOfTypeFrom } from './utils/event'; -import { maestro } from './utils/maestro'; +import { getItemOfTypeFrom } from '../../utils/event'; +import { maestro } from '../../utils/maestro'; +import { isAutoInitTest } from '../../utils/environment'; describe('Capture Spaceflight News Screen Transaction', () => { let sentryServer = createSentryServer(); @@ -32,7 +33,11 @@ describe('Capture Spaceflight News Screen Transaction', () => { takeSecond(containingNewsScreen), ); - await maestro('captureSpaceflightNewsScreenTransaction.test.yml'); + if (isAutoInitTest()) { + await maestro('tests/captureSpaceflightNewsScreenTransaction/captureSpaceflightNewsScreenTransaction.test.ios.auto.yml'); + } else { + await maestro('tests/captureSpaceflightNewsScreenTransaction/captureSpaceflightNewsScreenTransaction.test.yml'); + } await waitForSpaceflightNewsTx; @@ -40,7 +45,7 @@ describe('Capture Spaceflight News Screen Transaction', () => { allTransactionEnvelopes = sentryServer.getAllEnvelopes( containingTransaction, ); - }); + }, 240000); // 240 seconds timeout for iOS event delivery afterAll(async () => { await sentryServer.close(); @@ -116,8 +121,6 @@ describe('Capture Spaceflight News Screen Transaction', () => { const item = getFirstNewsEventItem(); const spans = item?.[1].spans; - console.log(spans); - const httpSpans = spans?.filter( span => span.data?.['sentry.op'] === 'http.client', ); diff --git a/samples/react-native/e2e/tests/captureSpaceflightNewsScreenTransaction/captureSpaceflightNewsScreenTransaction.test.yml b/samples/react-native/e2e/tests/captureSpaceflightNewsScreenTransaction/captureSpaceflightNewsScreenTransaction.test.yml new file mode 100644 index 0000000000..6f901f93ad --- /dev/null +++ b/samples/react-native/e2e/tests/captureSpaceflightNewsScreenTransaction/captureSpaceflightNewsScreenTransaction.test.yml @@ -0,0 +1,53 @@ +appId: io.sentry.reactnative.sample +--- +- launchApp: + # We expect cold start + clearState: true + stopApp: true + arguments: + isE2ETest: true + +# For unknown reasons tapOn: "Performance" does not work on iOS +- tapOn: + id: "performance-tab-icon" +- tapOn: "Open Spaceflight News" + +# Wait for the screen to load articles and initial data to appear +- waitForAnimationToEnd: + timeout: 5000 + +# Scroll down multiple times to trigger auto-load (AUTO_LOAD_LIMIT = 1) +# This will trigger handleEndReached which increments autoLoadCount +# After autoLoadCount >= 1, the "Load More Articles" button will appear +- scroll +- scroll +- scroll + +# Wait a bit for the auto-load to complete and button to appear +- waitForAnimationToEnd: + timeout: 3000 + +# Now the "Load More Articles" button should be visible +- scrollUntilVisible: + element: "Load More Articles" + timeout: 10000 +# On iOS the visibility is resolved when the button only peaks from the bottom tabs +# this causes Maestro to click the bottom tab instead of the button +# thus the extra scroll is needed to make the button visible +- scroll +- tapOn: "Load More Articles" + +# Wait for more articles to load after manual tap +- waitForAnimationToEnd: + timeout: 3000 +- scroll +- scrollUntilVisible: + element: "Load More Articles" + timeout: 10000 + +- tapOn: + id: "errors-tab-icon" + +# The tab keeps News Screen open, but the data are updated on the next visit +- tapOn: + id: "performance-tab-icon" diff --git a/samples/react-native/e2e/utils/environment.ts b/samples/react-native/e2e/utils/environment.ts index cde97ea350..37bf0c6620 100644 --- a/samples/react-native/e2e/utils/environment.ts +++ b/samples/react-native/e2e/utils/environment.ts @@ -1,23 +1,15 @@ type TestGlobal = typeof globalThis & { - E2E_TEST_PLATFORM: 'android' | 'ios'; + E2E_TEST_INIT_TYPE: 'auto' | 'manual'; }; function getTestGlobal(): TestGlobal { return globalThis as TestGlobal; } -export function setAndroid(): void { - getTestGlobal().E2E_TEST_PLATFORM = 'android'; +export function setAutoInitTest(): void { + getTestGlobal().E2E_TEST_INIT_TYPE = 'auto'; } -export function setIOS(): void { - getTestGlobal().E2E_TEST_PLATFORM = 'ios'; -} - -export function isAndroid(): boolean { - return getTestGlobal().E2E_TEST_PLATFORM === 'android'; -} - -export function isIOS(): boolean { - return getTestGlobal().E2E_TEST_PLATFORM === 'ios'; +export function isAutoInitTest(): boolean { + return getTestGlobal().E2E_TEST_INIT_TYPE === 'auto'; } diff --git a/samples/react-native/e2e/utils/mockedSentryServer.ts b/samples/react-native/e2e/utils/mockedSentryServer.ts index 5b791f7b62..34baaeacb1 100644 --- a/samples/react-native/e2e/utils/mockedSentryServer.ts +++ b/samples/react-native/e2e/utils/mockedSentryServer.ts @@ -66,7 +66,6 @@ export function createSentryServer({ port = 8961 } = {}): { start: () => { return new Promise((resolve, _reject) => { server.listen(port, () => { - console.log(`Sentry server listening on port ${port}`); resolve(); }); }); diff --git a/samples/react-native/ios/sentryreactnativesample.xcodeproj/xcshareddata/xcschemes/sentryreactnativesample.xcscheme b/samples/react-native/ios/sentryreactnativesample.xcodeproj/xcshareddata/xcschemes/sentryreactnativesample.xcscheme index 0ae9a20729..a6e129394a 100644 --- a/samples/react-native/ios/sentryreactnativesample.xcodeproj/xcshareddata/xcschemes/sentryreactnativesample.xcscheme +++ b/samples/react-native/ios/sentryreactnativesample.xcodeproj/xcshareddata/xcschemes/sentryreactnativesample.xcscheme @@ -62,11 +62,11 @@ diff --git a/samples/react-native/ios/sentryreactnativesample/AppDelegate.mm b/samples/react-native/ios/sentryreactnativesample/AppDelegate.mm index d4bfa4a202..d08d16acdd 100644 --- a/samples/react-native/ios/sentryreactnativesample/AppDelegate.mm +++ b/samples/react-native/ios/sentryreactnativesample/AppDelegate.mm @@ -87,13 +87,13 @@ - (NSURL *)bundleURL - (BOOL)shouldStartSentry { NSArray *arguments = [[NSProcessInfo processInfo] arguments]; - return ![arguments containsObject:@"--sentry-disable-native-start"]; + return ![arguments containsObject:@"sentryDisableNativeStart"]; } - (BOOL)shouldCrashOnStart { NSArray *arguments = [[NSProcessInfo processInfo] arguments]; - return [arguments containsObject:@"--sentry-crash-on-start"]; + return [arguments containsObject:@"sentryCrashOnStart"]; } @end diff --git a/samples/react-native/package.json b/samples/react-native/package.json index c87122d364..67d3d60717 100644 --- a/samples/react-native/package.json +++ b/samples/react-native/package.json @@ -13,13 +13,13 @@ "build-ios-release": "scripts/build-ios-release.sh", "build-ios-debug": "scripts/build-ios-debug.sh", "test": "jest", - "test-android": "scripts/test-android.sh", - "test-ios": "scripts/test-ios.sh", - "set-test-dsn-android": "scripts/detox/set-dsn-aos.mjs", - "set-test-dsn-ios": "scripts/detox/set-dsn-ios.mjs", - "test-android-manual": "scripts/detox/test-android.sh", - "test-ios-manual": "scripts/detox/test-ios-manual.sh", - "test-ios-auto": "scripts/detox/test-ios-auto.sh", + "test-android": "scripts/test-android-manual.sh", + "test-ios": "scripts/test-ios-auto.sh", + "set-test-dsn-android": "scripts/set-dsn-aos.mjs", + "set-test-dsn-ios": "scripts/set-dsn-ios.mjs", + "test-android-manual": "scripts/test-android-manual.sh", + "test-ios-manual": "scripts/test-ios-manual.sh", + "test-ios-auto": "scripts/test-ios-auto.sh", "lint": "npx eslint . --ext .js,.jsx,.ts,.tsx", "fix": "eslint . --ext .js,.jsx,.ts,.tsx --fix", "pod-install-debug-static": "scripts/pod-install-debug-static.sh", diff --git a/samples/react-native/scripts/detox/detect-ios-sim.sh b/samples/react-native/scripts/detox/detect-ios-sim.sh deleted file mode 100755 index 067c5abf48..0000000000 --- a/samples/react-native/scripts/detox/detect-ios-sim.sh +++ /dev/null @@ -1,18 +0,0 @@ -#!/bin/bash - -# Exit on error -set -e - -if [ -z "$IOS_DEVICE" ]; then - # Get the first booted simulator device type and version - BOOTED_DEVICE=$(xcrun simctl list devices | grep "Booted" | head -n 1) - - if [ -z "$BOOTED_DEVICE" ]; then - echo "No booted iOS simulator found" - exit 1 - fi - - # Extract device type from booted device - export IOS_DEVICE=$(echo "$BOOTED_DEVICE" | cut -d "(" -f1 | xargs) - echo "Using booted iOS simulator: $IOS_DEVICE" -fi diff --git a/samples/react-native/scripts/detox/test-android.sh b/samples/react-native/scripts/detox/test-android.sh deleted file mode 100755 index a5dc4fce83..0000000000 --- a/samples/react-native/scripts/detox/test-android.sh +++ /dev/null @@ -1,43 +0,0 @@ -#!/bin/bash - -# Exit on error and print commands -set -xe - -thisFilePath=$(dirname "$0") - -cd "${thisFilePath}/../.." - -if [ -z "$ANDROID_AVD_NAME" ]; then - # Get the name of the first booted or connected Android device - DEVICE_NAME=$(adb devices | grep -w "device" | head -n 1 | cut -f 1) - - if [ -z "$DEVICE_NAME" ]; then - echo "No Android device or emulator found" - exit 1 - fi - - if [[ "$DEVICE_NAME" == *"emulator"* ]]; then - # Get the name of the first booted or connected Android emulator/device - EMULATOR_NAME=$(adb -s "${DEVICE_NAME}" emu avd name | head -n 1 | cut -f 1 ) - - if [ -z "$EMULATOR_NAME" ]; then - echo "No Android emulator found" - exit 1 - fi - - export ANDROID_TYPE="android.emulator" - export ANDROID_AVD_NAME="$EMULATOR_NAME" - echo "Using Android emulator: $EMULATOR_NAME" - else - export ANDROID_TYPE="android.attached" - export ANDROID_ADB_NAME="$DEVICE_NAME" - - adb reverse tcp:8081 tcp:8081 - adb reverse tcp:8961 tcp:8961 - - echo "Using Android device: $DEVICE_NAME" - fi -fi - -# Run the tests -detox test --configuration ci.android diff --git a/samples/react-native/scripts/detox/test-ios-auto.sh b/samples/react-native/scripts/detox/test-ios-auto.sh deleted file mode 100755 index da9b731642..0000000000 --- a/samples/react-native/scripts/detox/test-ios-auto.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/bash - -# Exit on error and print commands -set -xe - -thisFilePath=$(dirname "$0") - -cd "${thisFilePath}/../.." - -"${thisFilePath}/detect-ios-sim.sh" - -detox test --configuration ci.sim.auto --app-launch-args="--sentry-disable-native-start" diff --git a/samples/react-native/scripts/detox/test-ios-manual.sh b/samples/react-native/scripts/detox/test-ios-manual.sh deleted file mode 100755 index a83db50e1b..0000000000 --- a/samples/react-native/scripts/detox/test-ios-manual.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/bash - -# Exit on error and print commands -set -xe - -thisFilePath=$(dirname "$0") - -cd "${thisFilePath}/../.." - -"${thisFilePath}/detect-ios-sim.sh" - -detox test --configuration ci.sim.manual diff --git a/samples/react-native/scripts/detox/set-dsn-aos.mjs b/samples/react-native/scripts/set-dsn-aos.mjs similarity index 100% rename from samples/react-native/scripts/detox/set-dsn-aos.mjs rename to samples/react-native/scripts/set-dsn-aos.mjs diff --git a/samples/react-native/scripts/detox/set-dsn-ios.mjs b/samples/react-native/scripts/set-dsn-ios.mjs similarity index 100% rename from samples/react-native/scripts/detox/set-dsn-ios.mjs rename to samples/react-native/scripts/set-dsn-ios.mjs diff --git a/samples/react-native/scripts/detox/set-dsn.mjs b/samples/react-native/scripts/set-dsn.mjs similarity index 88% rename from samples/react-native/scripts/detox/set-dsn.mjs rename to samples/react-native/scripts/set-dsn.mjs index 6c8ec24bfe..da2153f203 100644 --- a/samples/react-native/scripts/detox/set-dsn.mjs +++ b/samples/react-native/scripts/set-dsn.mjs @@ -13,7 +13,7 @@ export function setAndroidDsn() { } function setDsn(dsn) { - const sentryOptionsPath = path.join(__dirname, '../../sentry.options.json'); + const sentryOptionsPath = path.join(__dirname, '../sentry.options.json'); const sentryOptions = JSON.parse(fs.readFileSync(sentryOptionsPath, 'utf8')); sentryOptions.dsn = dsn; fs.writeFileSync( diff --git a/samples/react-native/scripts/test-android.sh b/samples/react-native/scripts/test-android-manual.sh similarity index 88% rename from samples/react-native/scripts/test-android.sh rename to samples/react-native/scripts/test-android-manual.sh index de4b5d5e87..694c64dd8b 100755 --- a/samples/react-native/scripts/test-android.sh +++ b/samples/react-native/scripts/test-android-manual.sh @@ -1,6 +1,6 @@ #!/bin/bash -set -e -x # exit on error, print commands +set -e # exit on error # Get current directory thisFileDirPath=$(dirname "$0") @@ -24,4 +24,4 @@ else fi # Run the tests -npx jest --config e2e/jest.config.android.js +npx jest --config e2e/jest.config.android.manual.js diff --git a/samples/react-native/scripts/test-ios.sh b/samples/react-native/scripts/test-ios-auto.sh similarity index 88% rename from samples/react-native/scripts/test-ios.sh rename to samples/react-native/scripts/test-ios-auto.sh index e242dde917..3c85059436 100755 --- a/samples/react-native/scripts/test-ios.sh +++ b/samples/react-native/scripts/test-ios-auto.sh @@ -1,6 +1,6 @@ #!/bin/bash -set -e -x # exit on error, print commands +set -e # exit on error # Get current directory thisFileDirPath=$(dirname "$0") @@ -23,4 +23,4 @@ else fi # Run the tests -npx jest --config e2e/jest.config.ios.js +npx jest --config e2e/jest.config.ios.auto.js diff --git a/samples/react-native/scripts/test-ios-manual.sh b/samples/react-native/scripts/test-ios-manual.sh new file mode 100755 index 0000000000..f2e6098440 --- /dev/null +++ b/samples/react-native/scripts/test-ios-manual.sh @@ -0,0 +1,26 @@ +#!/bin/bash + +set -e # exit on error + +# Get current directory +thisFileDirPath=$(dirname "$0") +reactProjectRootPath="$(cd "$thisFileDirPath/.." && pwd)" + +maybeAppPath=$(find "${reactProjectRootPath}" -maxdepth 1 -name "*.app") + +# Check if any APP files exist +app_count=$(echo "$maybeAppPath" | wc -l) + +if [ -n "$maybeAppPath" ] && [ $app_count -eq 1 ]; then + app_file="${maybeAppPath}" + echo "Installing $app_file..." + xcrun simctl install booted "$app_file" +elif [ $app_count -gt 1 ]; then + echo "Error: Multiple APP files found. Expected only one APP file." + exit 1 +else + echo "No APP files found, continuing without install" +fi + +# Run the tests +npx jest --config e2e/jest.config.ios.manual.js diff --git a/samples/react-native/src/utils.ts b/samples/react-native/src/utils.ts index 9fdd02468c..932a660922 100644 --- a/samples/react-native/src/utils.ts +++ b/samples/react-native/src/utils.ts @@ -21,9 +21,9 @@ export function shouldUseAutoStart(): boolean { ).SENTRY_DISABLE_NATIVE_START; } else if (Platform.OS === 'ios') { const args = LaunchArguments.value<{ - sentrydisablenativestart?: boolean; + sentryDisableNativeStart?: boolean; }>(); - return !!args.sentrydisablenativestart; + return !!args.sentryDisableNativeStart; } else { return false; }