diff --git a/CHANGELOG.md b/CHANGELOG.md index 2abeb90664..d6b8b527af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,10 @@ - Warn Expo users at Metro startup when prebuilt native projects are missing Sentry configuration ([#5984](https://github.com/getsentry/sentry-react-native/pull/5984)) +### Fixes + +- Fix iOS UI profiling options being silently ignored ([#6012](https://github.com/getsentry/sentry-react-native/pull/6012)) + ### Dependencies - Bump JavaScript SDK from v10.48.0 to v10.49.0 ([#6011](https://github.com/getsentry/sentry-react-native/pull/6011)) diff --git a/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryTests.m b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryTests.m index 76c41d3c66..b09a2073d0 100644 --- a/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryTests.m +++ b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryTests.m @@ -5,6 +5,7 @@ #import #import #import +#import #import #import @import Sentry; @@ -1372,6 +1373,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 03ca9e2ccd..c7faf91dae 100644 --- a/packages/core/ios/RNSentryStart.m +++ b/packages/core/ios/RNSentryStart.m @@ -100,6 +100,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 =