diff --git a/CHANGELOG.md b/CHANGELOG.md index 618b86cdf0..3668ec549e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ ### Fixes +- Fix iOS UI profiling options being silently ignored ([#6012](https://github.com/getsentry/sentry-react-native/pull/6012)) - Check `captureReplay` return value in iOS bridge to avoid linking error events to uncaptured replays ([#6008](https://github.com/getsentry/sentry-react-native/pull/6008)) ### Dependencies diff --git a/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryTests.m b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryTests.m index f626df8355..1aeb6015cc 100644 --- a/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryTests.m +++ b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryTests.m @@ -6,6 +6,7 @@ #import #import #import +#import #import #import @import Sentry; @@ -1373,6 +1374,103 @@ - (void)testStartBeforeBreadcrumbsCallbackDoesNotFiltersOutNonDevServerOrDsnRequ XCTAssertEqual(breadcrumb, result); } +#if SENTRY_TARGET_PROFILING_SUPPORTED +// Regression test for the v8.0.0 bug where the init path (RNSentryStart) did not +// handle `_experiments.profilingOptions`, silently dropping iOS UI profiling config. +// This pins the full entry point used by `initNativeSdk` in RNSentry.mm. +- (void)testStartWithDictionaryInstallsConfigureProfilingFromExperimentsProfilingOptions +{ + NSError *error = nil; + + NSDictionary *_Nonnull mockedReactNativeDictionary = @{ + @"dsn" : @"https://abcd@efgh.ingest.sentry.io/123456", + @"_experiments" : @ { + @"profilingOptions" : @ { + @"profileSessionSampleRate" : @1.0, + @"lifecycle" : @"trace", + @"startOnAppStart" : @YES, + }, + }, + }; + [RNSentryStart startWithOptions:mockedReactNativeDictionary error:&error]; + SentryOptions *actualOptions = PrivateSentrySDKOnly.options; + + XCTAssertNotNil(actualOptions, @"Did not create sentry options"); + XCTAssertNil(error, @"Should not pass no error"); + XCTAssertNotNil(actualOptions.configureProfiling, + @"configureProfiling must be installed after startWithOptions when profilingOptions is " + @"present"); + + SentryProfileOptions *probe = [[SentryProfileOptions alloc] init]; + actualOptions.configureProfiling(probe); + XCTAssertEqual(probe.sessionSampleRate, 1.0f); + XCTAssertEqual(probe.lifecycle, SentryProfileLifecycleTrace); + XCTAssertTrue(probe.profileAppStarts); +} + +- (void)testStartCreateOptionsWithDictionaryProfilingOptionsInstallsConfigureProfiling +{ + NSError *error = nil; + + NSDictionary *_Nonnull mockedReactNativeDictionary = @{ + @"dsn" : @"https://abcd@efgh.ingest.sentry.io/123456", + @"_experiments" : @ { + @"profilingOptions" : @ { + @"profileSessionSampleRate" : @1.0, + @"lifecycle" : @"trace", + @"startOnAppStart" : @YES, + }, + }, + }; + SentryOptions *actualOptions = + [RNSentryStart createOptionsWithDictionary:mockedReactNativeDictionary error:&error]; + + XCTAssertNotNil(actualOptions, @"Did not create sentry options"); + XCTAssertNil(error, @"Should not pass no error"); + XCTAssertNotNil(actualOptions.configureProfiling, + @"configureProfiling callback should be installed when profilingOptions is present"); + + SentryProfileOptions *probe = [[SentryProfileOptions alloc] init]; + actualOptions.configureProfiling(probe); + XCTAssertEqual(probe.sessionSampleRate, 1.0f); + XCTAssertEqual(probe.lifecycle, SentryProfileLifecycleTrace); + XCTAssertTrue(probe.profileAppStarts); +} + +- (void)testStartCreateOptionsWithDictionaryProfilingOptionsMissingDoesNotInstallConfigureProfiling +{ + NSError *error = nil; + + NSDictionary *_Nonnull mockedReactNativeDictionary = @{ + @"dsn" : @"https://abcd@efgh.ingest.sentry.io/123456", + }; + SentryOptions *actualOptions = + [RNSentryStart createOptionsWithDictionary:mockedReactNativeDictionary error:&error]; + + XCTAssertNotNil(actualOptions, @"Did not create sentry options"); + XCTAssertNil(error, @"Should not pass no error"); + XCTAssertNil(actualOptions.configureProfiling, + @"configureProfiling callback should not be installed without profilingOptions"); +} + +- (void)testStartCreateOptionsWithDictionaryEmptyExperimentsDoesNotInstallConfigureProfiling +{ + NSError *error = nil; + + NSDictionary *_Nonnull mockedReactNativeDictionary = @{ + @"dsn" : @"https://abcd@efgh.ingest.sentry.io/123456", + @"_experiments" : @ { }, + }; + SentryOptions *actualOptions = + [RNSentryStart createOptionsWithDictionary:mockedReactNativeDictionary error:&error]; + + XCTAssertNotNil(actualOptions, @"Did not create sentry options"); + XCTAssertNil(error, @"Should not pass no error"); + XCTAssertNil(actualOptions.configureProfiling, + @"configureProfiling callback should not be installed when profilingOptions is absent"); +} +#endif + - (void)testStartEventFromSentryCocoaReactNativeHasOriginAndEnvironmentTags { SentryEvent *testEvent = [[SentryEvent alloc] init]; diff --git a/packages/core/ios/RNSentryStart.m b/packages/core/ios/RNSentryStart.m index fcc60857e9..4d07f8816b 100644 --- a/packages/core/ios/RNSentryStart.m +++ b/packages/core/ios/RNSentryStart.m @@ -140,6 +140,16 @@ + (SentryOptions *_Nullable)createOptionsWithDictionary:(NSDictionary *_Nonnull) } } + // Configure iOS UI Profiling from _experiments.profilingOptions + NSDictionary *experiments = mutableOptions[@"_experiments"]; + if (experiments != nil && [experiments isKindOfClass:[NSDictionary class]]) { + NSDictionary *profilingOptions = experiments[@"profilingOptions"]; + if (profilingOptions != nil && [profilingOptions isKindOfClass:[NSDictionary class]]) { + [RNSentryExperimentalOptions configureProfilingWithOptions:profilingOptions + sentryOptions:sentryOptions]; + } + } + // Set strict trace continuation options if ([mutableOptions valueForKey:@"strictTraceContinuation"] != nil) { sentryOptions.strictTraceContinuation =