From ff03cfe3c931282c7abe50d8a595f66877932b54 Mon Sep 17 00:00:00 2001 From: Andrew McKnight Date: Tue, 26 Jul 2022 15:10:04 -0800 Subject: [PATCH 01/16] meta: add armcknight as a codeowner (#2008) --- .github/CODEOWNERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 8a8dd7578b8..4e7b38ec05f 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1 +1 @@ -* @philipphofmann @brustolin +* @philipphofmann @brustolin @armcknight From e6c1fb873a24856394b95e318ddadbe54753047b Mon Sep 17 00:00:00 2001 From: Bruno Garcia Date: Wed, 27 Jul 2022 04:58:46 -0400 Subject: [PATCH 02/16] fix log message option name (#2006) Co-authored-by: Sentry Github Bot --- Sources/Sentry/SentryAppStartTrackingIntegration.m | 6 +++--- Sources/Sentry/SentryFramesTrackingIntegration.m | 7 ++++--- Sources/Sentry/SentryPerformanceTrackingIntegration.m | 2 +- .../Sentry/include/SentryPerformanceTrackingIntegration.h | 2 +- .../SentryAppStartTrackingIntegrationTests.swift | 2 +- .../SentryFramesTrackingIntegrationTests.swift | 2 +- 6 files changed, 11 insertions(+), 10 deletions(-) diff --git a/Sources/Sentry/SentryAppStartTrackingIntegration.m b/Sources/Sentry/SentryAppStartTrackingIntegration.m index e60a31f17c3..f79f90a770f 100644 --- a/Sources/Sentry/SentryAppStartTrackingIntegration.m +++ b/Sources/Sentry/SentryAppStartTrackingIntegration.m @@ -60,9 +60,9 @@ - (BOOL)shouldBeEnabled:(SentryOptions *)options } if (!options.enableAutoPerformanceTracking) { - [SentryLog - logWithMessage:@"AutoUIPerformanceTracking disabled. Will not track app start up time." - andLevel:kSentryLevelDebug]; + [SentryLog logWithMessage: + @"enableAutoPerformanceTracking disabled. Will not track app start up time." + andLevel:kSentryLevelDebug]; return NO; } diff --git a/Sources/Sentry/SentryFramesTrackingIntegration.m b/Sources/Sentry/SentryFramesTrackingIntegration.m index 929d4e8625c..3af5d5da9b4 100644 --- a/Sources/Sentry/SentryFramesTrackingIntegration.m +++ b/Sources/Sentry/SentryFramesTrackingIntegration.m @@ -46,9 +46,10 @@ - (BOOL)shouldBeDisabled:(SentryOptions *)options } if (!options.enableAutoPerformanceTracking) { - [SentryLog logWithMessage: - @"AutoUIPerformanceTracking disabled. Will not track slow and frozen frames." - andLevel:kSentryLevelDebug]; + [SentryLog + logWithMessage: + @"enableAutoPerformanceTracking disabled. Will not track slow and frozen frames." + andLevel:kSentryLevelDebug]; return YES; } diff --git a/Sources/Sentry/SentryPerformanceTrackingIntegration.m b/Sources/Sentry/SentryPerformanceTrackingIntegration.m index 4d8decbc5bf..aee2dd20d44 100644 --- a/Sources/Sentry/SentryPerformanceTrackingIntegration.m +++ b/Sources/Sentry/SentryPerformanceTrackingIntegration.m @@ -52,7 +52,7 @@ - (void)installWithOptions:(SentryOptions *)options - (BOOL)shouldBeDisabled:(SentryOptions *)options { if (!options.enableAutoPerformanceTracking) { - [SentryLog logWithMessage:@"AutoUIPerformanceTracking disabled. Will not start " + [SentryLog logWithMessage:@"enableAutoPerformanceTracking disabled. Will not start " @"SentryPerformanceTrackingIntegration." andLevel:kSentryLevelDebug]; return YES; diff --git a/Sources/Sentry/include/SentryPerformanceTrackingIntegration.h b/Sources/Sentry/include/SentryPerformanceTrackingIntegration.h index 7bd2bb86855..68a64de12e6 100644 --- a/Sources/Sentry/include/SentryPerformanceTrackingIntegration.h +++ b/Sources/Sentry/include/SentryPerformanceTrackingIntegration.h @@ -7,7 +7,7 @@ NS_ASSUME_NONNULL_BEGIN * Integration to setup automatic performance tracking. * * Automatic UI performance setup can be avoided by setting - * enableAutoUIPerformanceTracking to NO + * enableAutoPerformanceTracking to NO * in SentryOptions during SentrySDK initialization. */ @interface SentryPerformanceTrackingIntegration : NSObject diff --git a/Tests/SentryTests/Integrations/Performance/AppStartTracking/SentryAppStartTrackingIntegrationTests.swift b/Tests/SentryTests/Integrations/Performance/AppStartTracking/SentryAppStartTrackingIntegrationTests.swift index 08466ad6d28..4ebc77236db 100644 --- a/Tests/SentryTests/Integrations/Performance/AppStartTracking/SentryAppStartTrackingIntegrationTests.swift +++ b/Tests/SentryTests/Integrations/Performance/AppStartTracking/SentryAppStartTrackingIntegrationTests.swift @@ -77,7 +77,7 @@ class SentryAppStartTrackingIntegrationTests: XCTestCase { XCTAssertNil(SentrySDK.getAppStartMeasurement()) } - func testAutoUIPerformanceTrackingDisabled_DoesNotUpdatesAppState() { + func testAutoPerformanceTrackingDisabled_DoesNotUpdatesAppState() { let options = fixture.options options.enableAutoPerformanceTracking = false sut.install(with: options) diff --git a/Tests/SentryTests/Integrations/Performance/FramesTracking/SentryFramesTrackingIntegrationTests.swift b/Tests/SentryTests/Integrations/Performance/FramesTracking/SentryFramesTrackingIntegrationTests.swift index 45eed208096..29c48de4e66 100644 --- a/Tests/SentryTests/Integrations/Performance/FramesTracking/SentryFramesTrackingIntegrationTests.swift +++ b/Tests/SentryTests/Integrations/Performance/FramesTracking/SentryFramesTrackingIntegrationTests.swift @@ -53,7 +53,7 @@ class SentryFramesTrackingIntegrationTests: XCTestCase { XCTAssertNil(Dynamic(sut).tracker.asObject) } - func testAutoUIPerformanceTrackingDisabled_DoesNotMeasureFrames() { + func testAutoPerformanceTrackingDisabled_DoesNotMeasureFrames() { let options = fixture.options options.tracesSampleRate = 0.1 options.enableAutoPerformanceTracking = false From 643eab833d298c5a69e9ff5021c4cf0f085300a2 Mon Sep 17 00:00:00 2001 From: Indragie Karunaratne Date: Wed, 27 Jul 2022 10:00:40 -0700 Subject: [PATCH 03/16] feat: Add sampling configuration for profiling (#2004) Adds two new configuration options, profilesSampleRate and profilesSampler to mirror tracesSampleRate and tracesSampler, except for profiling data. There are no breaking changes -- enableProfiling is now deprecated, but will continue to work for existing clients that use it (this is equivalent to setting profilesSampleRate to 1.0) --- CHANGELOG.md | 6 + .../TrendingMovies/Utilities/Tracer.swift | 2 +- Samples/iOS-Swift/iOS-Swift/AppDelegate.swift | 2 +- .../iOS-SwiftUI/iOS-SwiftUI/SwiftUIApp.swift | 2 +- Samples/iOS15-SwiftUI/iOS15-SwiftUI/App.swift | 2 +- Sentry.xcodeproj/project.pbxproj | 8 + Sources/Sentry/Public/SentryOptions.h | 36 ++++- Sources/Sentry/SentryHub.m | 23 ++- Sources/Sentry/SentryOptions.m | 58 ++++++- Sources/Sentry/SentryProfilesSampler.h | 49 ++++++ Sources/Sentry/SentryProfilesSampler.m | 90 +++++++++++ Sources/Sentry/SentryTracer.m | 45 ++++-- .../Sentry/include/SentryOptions+Private.h | 7 + Sources/Sentry/include/SentryTracer.h | 22 ++- .../Performance/SentryTracerObjCTests.m | 1 + .../Performance/SentryTracerTests.swift | 4 +- Tests/SentryTests/SentryHubTests.swift | 94 +++++++++--- Tests/SentryTests/SentryOptionsTest.m | 142 +++++++++++++++++- scripts/add-sentry-to-alamofire.patch | 2 +- scripts/add-sentry-to-homekit.patch | 2 +- scripts/add-sentry-to-vlc.patch | 2 +- scripts/set-device-tests-environment.patch | 2 +- 22 files changed, 546 insertions(+), 55 deletions(-) create mode 100644 Sources/Sentry/SentryProfilesSampler.h create mode 100644 Sources/Sentry/SentryProfilesSampler.m diff --git a/CHANGELOG.md b/CHANGELOG.md index 233251d3219..11fa83fbe0c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +### Features + +- Add sampling configuration for profiling (#2004) + ## 7.22.0 ### Features diff --git a/Samples/TrendingMovies/TrendingMovies/Utilities/Tracer.swift b/Samples/TrendingMovies/TrendingMovies/Utilities/Tracer.swift index 60d30155d5f..cc42441f7e3 100644 --- a/Samples/TrendingMovies/TrendingMovies/Utilities/Tracer.swift +++ b/Samples/TrendingMovies/TrendingMovies/Utilities/Tracer.swift @@ -35,7 +35,7 @@ extension Tracer { options.tracesSampleRate = 1.0 options.enableFileIOTracking = true options.enableCoreDataTracking = true - options.enableProfiling = true + options.profilesSampleRate = 1.0 options.attachScreenshot = true options.enableUserInteractionTracing = true } diff --git a/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift b/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift index 5eddb4dbfe5..df568d46dcf 100644 --- a/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift +++ b/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift @@ -25,7 +25,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { options.sessionTrackingIntervalMillis = 5_000 options.enableFileIOTracking = true options.enableCoreDataTracking = true - options.enableProfiling = true + options.profilesSampleRate = 1.0 options.attachScreenshot = true if !ProcessInfo.processInfo.arguments.contains("--io.sentry.test.benchmarking") { diff --git a/Samples/iOS-SwiftUI/iOS-SwiftUI/SwiftUIApp.swift b/Samples/iOS-SwiftUI/iOS-SwiftUI/SwiftUIApp.swift index b71e5a9de2b..a22e0f9d72a 100644 --- a/Samples/iOS-SwiftUI/iOS-SwiftUI/SwiftUIApp.swift +++ b/Samples/iOS-SwiftUI/iOS-SwiftUI/SwiftUIApp.swift @@ -11,7 +11,7 @@ struct SwiftUIApp: App { // Sampling 100% - In Production you probably want to adjust this options.tracesSampleRate = 1.0 options.enableFileIOTracking = true - options.enableProfiling = true + options.profilesSampleRate = 1.0 options.enableUserInteractionTracing = true } } diff --git a/Samples/iOS15-SwiftUI/iOS15-SwiftUI/App.swift b/Samples/iOS15-SwiftUI/iOS15-SwiftUI/App.swift index b71e5a9de2b..a22e0f9d72a 100644 --- a/Samples/iOS15-SwiftUI/iOS15-SwiftUI/App.swift +++ b/Samples/iOS15-SwiftUI/iOS15-SwiftUI/App.swift @@ -11,7 +11,7 @@ struct SwiftUIApp: App { // Sampling 100% - In Production you probably want to adjust this options.tracesSampleRate = 1.0 options.enableFileIOTracking = true - options.enableProfiling = true + options.profilesSampleRate = 1.0 options.enableUserInteractionTracing = true } } diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj index c99b50976f4..0208e4b63a4 100644 --- a/Sentry.xcodeproj/project.pbxproj +++ b/Sentry.xcodeproj/project.pbxproj @@ -7,6 +7,8 @@ objects = { /* Begin PBXBuildFile section */ + 0356A570288B4612008BF593 /* SentryProfilesSampler.h in Headers */ = {isa = PBXBuildFile; fileRef = 0356A56E288B4612008BF593 /* SentryProfilesSampler.h */; }; + 0356A571288B4612008BF593 /* SentryProfilesSampler.m in Sources */ = {isa = PBXBuildFile; fileRef = 0356A56F288B4612008BF593 /* SentryProfilesSampler.m */; }; 035E73C827D56757005EEB11 /* SentryBacktraceTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 035E73C727D56757005EEB11 /* SentryBacktraceTests.mm */; }; 035E73CA27D57398005EEB11 /* SentryThreadHandleTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 035E73C927D57398005EEB11 /* SentryThreadHandleTests.mm */; }; 035E73CC27D575B3005EEB11 /* SentrySamplingProfilerTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 035E73CB27D575B3005EEB11 /* SentrySamplingProfilerTests.mm */; }; @@ -680,6 +682,8 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 0356A56E288B4612008BF593 /* SentryProfilesSampler.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryProfilesSampler.h; path = Sources/Sentry/SentryProfilesSampler.h; sourceTree = SOURCE_ROOT; }; + 0356A56F288B4612008BF593 /* SentryProfilesSampler.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = SentryProfilesSampler.m; path = Sources/Sentry/SentryProfilesSampler.m; sourceTree = SOURCE_ROOT; }; 035E73C727D56757005EEB11 /* SentryBacktraceTests.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = SentryBacktraceTests.mm; sourceTree = ""; }; 035E73C927D57398005EEB11 /* SentryThreadHandleTests.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = SentryThreadHandleTests.mm; sourceTree = ""; }; 035E73CB27D575B3005EEB11 /* SentrySamplingProfilerTests.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = SentrySamplingProfilerTests.mm; sourceTree = ""; }; @@ -2592,6 +2596,8 @@ 03F84D1927DD414C008FE43F /* SentryThreadState.hpp */, 03BCC38927E1BF49003232C7 /* SentryTime.h */, 03BCC38B27E1C01A003232C7 /* SentryTime.mm */, + 0356A56E288B4612008BF593 /* SentryProfilesSampler.h */, + 0356A56F288B4612008BF593 /* SentryProfilesSampler.m */, ); path = Profiling; sourceTree = ""; @@ -2812,6 +2818,7 @@ 7BE3C77B2446111500A38442 /* SentryRateLimitParser.h in Headers */, 7D0637032382B34300B30749 /* SentryScope.h in Headers */, 03F84D2727DD414C008FE43F /* SentryMachLogging.hpp in Headers */, + 0356A570288B4612008BF593 /* SentryProfilesSampler.h in Headers */, 63295AF51EF3C7DB002D4490 /* NSDictionary+SentrySanitize.h in Headers */, 8E4A037825F6F52100000D77 /* SentrySampleDecision.h in Headers */, 63FE717920DA4C1100CDBAE8 /* SentryCrashReportStore.h in Headers */, @@ -3290,6 +3297,7 @@ D85852BA27EDDC5900C6D8AE /* SentryUIApplication.m in Sources */, 7B4E375F258231FC00059C93 /* SentryAttachment.m in Sources */, 636085141ED47BE600E8599E /* SentryFileManager.m in Sources */, + 0356A571288B4612008BF593 /* SentryProfilesSampler.m in Sources */, 63FE710B20DA4C1000CDBAE8 /* SentryCrashMach.c in Sources */, 63FE707720DA4C1000CDBAE8 /* Container+SentryDeepSearch.m in Sources */, 63FE71A020DA4C1100CDBAE8 /* SentryCrashInstallation.m in Sources */, diff --git a/Sources/Sentry/Public/SentryOptions.h b/Sources/Sentry/Public/SentryOptions.h index 9a0168248e9..1cdf2b4a6a4 100644 --- a/Sources/Sentry/Public/SentryOptions.h +++ b/Sources/Sentry/Public/SentryOptions.h @@ -308,11 +308,45 @@ NS_SWIFT_NAME(Options) #if SENTRY_TARGET_PROFILING_SUPPORTED /** + * This feature is experimental. Profiling is not supported on watchOS or tvOS. + * + * Indicates the percentage profiles being sampled out of the sampled transactions. + * + * The default is 0. The value needs to be >= 0.0 and <= 1.0. When setting a value out of range + * the SDK sets it to the default of 0. + * + * This property is dependent on `tracesSampleRate` -- if `tracesSampleRate` is 0 (default), + * no profiles will be collected no matter what this property is set to. This property is + * used to undersample profiles *relative to* `tracesSampleRate`. + */ +@property (nullable, nonatomic, strong) NSNumber *profilesSampleRate; + +/** + * This feature is experimental. Profiling is not supported on watchOS or tvOS. + * + * A callback to a user defined profiles sampler function. This is similar to setting + * `profilesSampleRate`, but instead of a static value, the callback function will be called to + * determine the sample rate. + */ +@property (nullable, nonatomic) SentryTracesSamplerCallback profilesSampler; + +/** + * If profiling should be enabled or not. Returns YES if either a profilesSampleRate > 0 and + * <=1 or a profilesSampler is set otherwise NO. + */ +@property (nonatomic, assign, readonly) BOOL isProfilingEnabled; + +/** + * DEPRECATED: Use `profilesSampleRate` instead. Setting `enableProfiling` to YES is the equivalent + * of setting `profilesSampleRate` to `1.0`. If `profilesSampleRate` is set, it will take precedence + * over this setting. + * * Whether to enable the sampling profiler. Default is NO. * @note This is a beta feature that is currently not available to all Sentry customers. This * feature is not supported on watchOS or tvOS. */ -@property (nonatomic, assign) BOOL enableProfiling; +@property (nonatomic, assign) BOOL enableProfiling DEPRECATED_MSG_ATTRIBUTE( + "This property will be removed in a future version of the SDK"); #endif /** diff --git a/Sources/Sentry/SentryHub.m b/Sources/Sentry/SentryHub.m index bac7814a31e..5f4953132b9 100644 --- a/Sources/Sentry/SentryHub.m +++ b/Sources/Sentry/SentryHub.m @@ -10,6 +10,7 @@ #import "SentryFileManager.h" #import "SentryId.h" #import "SentryLog.h" +#import "SentryProfilesSampler.h" #import "SentrySDK+Private.h" #import "SentrySamplingContext.h" #import "SentryScope.h" @@ -27,7 +28,8 @@ @property (nullable, nonatomic, strong) SentryClient *client; @property (nullable, nonatomic, strong) SentryScope *scope; @property (nonatomic, strong) SentryCrashWrapper *crashWrapper; -@property (nonatomic, strong) SentryTracesSampler *sampler; +@property (nonatomic, strong) SentryTracesSampler *tracesSampler; +@property (nonatomic, strong) SentryProfilesSampler *profilesSampler; @property (nonatomic, strong) id currentDateProvider; @end @@ -45,7 +47,12 @@ - (instancetype)initWithClient:(nullable SentryClient *)client _sessionLock = [[NSObject alloc] init]; _installedIntegrations = [[NSMutableArray alloc] init]; _crashWrapper = [SentryCrashWrapper sharedInstance]; - _sampler = [[SentryTracesSampler alloc] initWithOptions:client.options]; + _tracesSampler = [[SentryTracesSampler alloc] initWithOptions:client.options]; +#if SENTRY_TARGET_PROFILING_SUPPORTED + if (client.options.isProfilingEnabled) { + _profilesSampler = [[SentryProfilesSampler alloc] initWithOptions:client.options]; + } +#endif _currentDateProvider = [SentryDefaultCurrentDateProvider sharedInstance]; } return self; @@ -341,12 +348,16 @@ - (SentryId *)captureEvent:(SentryEvent *)event [[SentrySamplingContext alloc] initWithTransactionContext:transactionContext customSamplingContext:customSamplingContext]; - SentryTracesSamplerDecision *samplerDecision = [_sampler sample:samplingContext]; + SentryTracesSamplerDecision *samplerDecision = [_tracesSampler sample:samplingContext]; transactionContext.sampled = samplerDecision.decision; transactionContext.sampleRate = samplerDecision.sampleRate; + SentryProfilesSamplerDecision *profilesSamplerDecision = + [_profilesSampler sample:samplingContext tracesSamplerDecision:samplerDecision]; + id tracer = [[SentryTracer alloc] initWithTransactionContext:transactionContext hub:self + profilesSamplerDecision:profilesSamplerDecision waitForChildren:waitForChildren]; if (bindToScope) @@ -365,12 +376,16 @@ - (SentryTracer *)startTransactionWithContext:(SentryTransactionContext *)transa [[SentrySamplingContext alloc] initWithTransactionContext:transactionContext customSamplingContext:customSamplingContext]; - SentryTracesSamplerDecision *samplerDecision = [_sampler sample:samplingContext]; + SentryTracesSamplerDecision *samplerDecision = [_tracesSampler sample:samplingContext]; transactionContext.sampled = samplerDecision.decision; transactionContext.sampleRate = samplerDecision.sampleRate; + SentryProfilesSamplerDecision *profilesSamplerDecision = + [_profilesSampler sample:samplingContext tracesSamplerDecision:samplerDecision]; + SentryTracer *tracer = [[SentryTracer alloc] initWithTransactionContext:transactionContext hub:self + profilesSamplerDecision:profilesSamplerDecision idleTimeout:idleTimeout dispatchQueueWrapper:dispatchQueueWrapper]; if (bindToScope) diff --git a/Sources/Sentry/SentryOptions.m b/Sources/Sentry/SentryOptions.m index b728c17b3bb..978deca8f42 100644 --- a/Sources/Sentry/SentryOptions.m +++ b/Sources/Sentry/SentryOptions.m @@ -12,7 +12,10 @@ @property (nullable, nonatomic, copy, readonly) NSNumber *defaultSampleRate; @property (nullable, nonatomic, copy, readonly) NSNumber *defaultTracesSampleRate; @property (nonatomic, strong) NSMutableSet *disabledIntegrations; - +#if SENTRY_TARGET_PROFILING_SUPPORTED +@property (nullable, nonatomic, copy, readonly) NSNumber *defaultProfilesSampleRate; +@property (nonatomic, assign) BOOL enableProfiling_DEPRECATED_TEST_ONLY; +#endif @end @implementation SentryOptions @@ -68,11 +71,13 @@ - (instancetype)init self.enableNetworkBreadcrumbs = YES; _defaultTracesSampleRate = nil; self.tracesSampleRate = _defaultTracesSampleRate; - self.enableCoreDataTracking = NO; - _enableSwizzling = YES; #if SENTRY_TARGET_PROFILING_SUPPORTED - self.enableProfiling = NO; + _enableProfiling = NO; + _defaultProfilesSampleRate = nil; + self.profilesSampleRate = _defaultProfilesSampleRate; #endif + self.enableCoreDataTracking = NO; + _enableSwizzling = YES; self.sendClientReports = YES; // Use the name of the bundle’s executable file as inAppInclude, so SentryInAppLogic @@ -289,6 +294,14 @@ - (BOOL)validateOptions:(NSDictionary *)options block:^(BOOL value) { self->_enableCoreDataTracking = value; }]; #if SENTRY_TARGET_PROFILING_SUPPORTED + if ([options[@"profilesSampleRate"] isKindOfClass:[NSNumber class]]) { + self.profilesSampleRate = options[@"profilesSampleRate"]; + } + + if ([self isBlock:options[@"profilesSampler"]]) { + self.profilesSampler = options[@"profilesSampler"]; + } + [self setBool:options[@"enableProfiling"] block:^(BOOL value) { self->_enableProfiling = value; }]; #endif @@ -380,6 +393,43 @@ - (BOOL)isTracingEnabled || _tracesSampler != nil; } +#if SENTRY_TARGET_PROFILING_SUPPORTED +- (BOOL)isValidProfilesSampleRate:(NSNumber *)profilesSampleRate +{ + return [self isValidTracesSampleRate:profilesSampleRate]; +} + +- (void)setProfilesSampleRate:(NSNumber *)profilesSampleRate +{ + if (profilesSampleRate == nil) { + _profilesSampleRate = nil; + } else if ([self isValidProfilesSampleRate:profilesSampleRate]) { + _profilesSampleRate = profilesSampleRate; + } else { + _profilesSampleRate = _defaultProfilesSampleRate; + } +} + +- (BOOL)isProfilingEnabled +{ + return (_profilesSampleRate != nil && [_profilesSampleRate doubleValue] > 0) + || _profilesSampler != nil || _enableProfiling; +} + +# pragma clang diagnostic push +# pragma clang diagnostic ignored "-Wdeprecated-declarations" +- (void)setEnableProfiling_DEPRECATED_TEST_ONLY:(BOOL)enableProfiling_DEPRECATED_TEST_ONLY +{ + self.enableProfiling = enableProfiling_DEPRECATED_TEST_ONLY; +} + +- (BOOL)enableProfiling_DEPRECATED_TEST_ONLY +{ + return self.enableProfiling; +} +# pragma clang diagnostic pop +#endif + /** * Checks if the passed in block is actually of type block. We can't check if the block matches a * specific block without some complex objc runtime method calls and therefore we only check if its diff --git a/Sources/Sentry/SentryProfilesSampler.h b/Sources/Sentry/SentryProfilesSampler.h new file mode 100644 index 00000000000..81a89c7edbe --- /dev/null +++ b/Sources/Sentry/SentryProfilesSampler.h @@ -0,0 +1,49 @@ +#import "SentryRandom.h" +#import "SentrySampleDecision.h" +#import + +NS_ASSUME_NONNULL_BEGIN + +@class SentryOptions, SentrySamplingContext, SentryTracesSamplerDecision; + +@interface SentryProfilesSamplerDecision : NSObject + +@property (nonatomic, readonly) SentrySampleDecision decision; + +@property (nullable, nonatomic, strong, readonly) NSNumber *sampleRate; + +- (instancetype)initWithDecision:(SentrySampleDecision)decision + forSampleRate:(nullable NSNumber *)sampleRate; + +@end + +@interface SentryProfilesSampler : NSObject + +/** + * A random number generator + */ +@property (nonatomic, strong) id random; + +/** + * Init a ProfilesSampler with given options and random generator. + * @param options Sentry options with sampling configuration + * @param random A random number generator + */ +- (instancetype)initWithOptions:(SentryOptions *)options random:(id)random; + +/** + * Init a ProfilesSampler with given options and a default Random generator. + * @param options Sentry options with sampling configuration + */ +- (instancetype)initWithOptions:(SentryOptions *)options; + +/** + * Determines whether a profile should be sampled based on the context, options, and + * whether the trace corresponding to the profile was sampled. + */ +- (SentryProfilesSamplerDecision *)sample:(SentrySamplingContext *)context + tracesSamplerDecision:(SentryTracesSamplerDecision *)tracesSamplerDecision; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Sources/Sentry/SentryProfilesSampler.m b/Sources/Sentry/SentryProfilesSampler.m new file mode 100644 index 00000000000..d43cbef6cc5 --- /dev/null +++ b/Sources/Sentry/SentryProfilesSampler.m @@ -0,0 +1,90 @@ +#import "SentryProfilesSampler.h" +#import "SentryDependencyContainer.h" +#import "SentryOptions+Private.h" +#import "SentryTracesSampler.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation SentryProfilesSamplerDecision + +- (instancetype)initWithDecision:(SentrySampleDecision)decision + forSampleRate:(nullable NSNumber *)sampleRate +{ + if (self = [super init]) { + _decision = decision; + _sampleRate = sampleRate; + } + return self; +} + +@end + +@implementation SentryProfilesSampler { + SentryOptions *_options; +} + +- (instancetype)initWithOptions:(SentryOptions *)options random:(id)random +{ + if (self = [super init]) { + _options = options; + self.random = random; + } + return self; +} + +- (instancetype)initWithOptions:(SentryOptions *)options +{ + return [self initWithOptions:options random:[SentryDependencyContainer sharedInstance].random]; +} + +- (SentryProfilesSamplerDecision *)sample:(SentrySamplingContext *)context + tracesSamplerDecision:(SentryTracesSamplerDecision *)tracesSamplerDecision +{ + // Profiles are always undersampled with respect to traces. If the trace is not sampled, + // the profile will not be either. If the trace is sampled, we can proceed to checking + // whether the associated profile should be sampled. +#if SENTRY_TARGET_PROFILING_SUPPORTED + if (tracesSamplerDecision.decision == kSentrySampleDecisionYes) { + if (_options.profilesSampler != nil) { + NSNumber *callbackDecision = _options.profilesSampler(context); + if (callbackDecision != nil) { + if (![_options isValidProfilesSampleRate:callbackDecision]) { + callbackDecision = _options.defaultProfilesSampleRate; + } + } + if (callbackDecision != nil) { + return [self calcSample:callbackDecision.doubleValue]; + } + } + + if (_options.profilesSampleRate != nil) { + return [self calcSample:_options.profilesSampleRate.doubleValue]; + } + + // Backward compatibility for clients that are still using the enableProfiling option. +# pragma clang diagnostic push +# pragma clang diagnostic ignored "-Wdeprecated-declarations" + if (_options.enableProfiling) { + return [[SentryProfilesSamplerDecision alloc] initWithDecision:kSentrySampleDecisionYes + forSampleRate:@1.0]; + } +# pragma clang diagnostic pop + } +#endif + + return [[SentryProfilesSamplerDecision alloc] initWithDecision:kSentrySampleDecisionNo + forSampleRate:nil]; +} + +- (SentryProfilesSamplerDecision *)calcSample:(double)rate +{ + double r = [self.random nextNumber]; + SentrySampleDecision decision = r <= rate ? kSentrySampleDecisionYes : kSentrySampleDecisionNo; + return + [[SentryProfilesSamplerDecision alloc] initWithDecision:decision + forSampleRate:[NSNumber numberWithDouble:rate]]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Sources/Sentry/SentryTracer.m b/Sources/Sentry/SentryTracer.m index 5f549941bf6..7db554226e5 100644 --- a/Sources/Sentry/SentryTracer.m +++ b/Sources/Sentry/SentryTracer.m @@ -8,6 +8,7 @@ #import "SentryHub+Private.h" #import "SentryLog.h" #import "SentryProfiler.h" +#import "SentryProfilesSampler.h" #import "SentryProfilingConditionals.h" #import "SentrySDK+Private.h" #import "SentryScope.h" @@ -50,6 +51,7 @@ @implementation SentryTracer { BOOL _waitForChildren; SentryTraceContext *_traceContext; + SentryProfilesSamplerDecision *_profilesSamplerDecision; NSMutableDictionary *_tags; NSMutableDictionary *_data; dispatch_block_t _idleTimeoutBlock; @@ -86,7 +88,10 @@ + (void)initialize - (instancetype)initWithTransactionContext:(SentryTransactionContext *)transactionContext hub:(nullable SentryHub *)hub { - return [self initWithTransactionContext:transactionContext hub:hub waitForChildren:NO]; + return [self initWithTransactionContext:transactionContext + hub:hub + profilesSamplerDecision:nil + waitForChildren:NO]; } - (instancetype)initWithTransactionContext:(SentryTransactionContext *)transactionContext @@ -95,6 +100,7 @@ - (instancetype)initWithTransactionContext:(SentryTransactionContext *)transacti { return [self initWithTransactionContext:transactionContext hub:hub + profilesSamplerDecision:nil waitForChildren:waitForChildren idleTimeout:0.0 dispatchQueueWrapper:nil]; @@ -102,22 +108,40 @@ - (instancetype)initWithTransactionContext:(SentryTransactionContext *)transacti - (instancetype)initWithTransactionContext:(SentryTransactionContext *)transactionContext hub:(nullable SentryHub *)hub + profilesSamplerDecision: + (nullable SentryProfilesSamplerDecision *)profilesSamplerDecision + waitForChildren:(BOOL)waitForChildren +{ + return [self initWithTransactionContext:transactionContext + hub:hub + profilesSamplerDecision:profilesSamplerDecision + waitForChildren:waitForChildren + idleTimeout:0.0 + dispatchQueueWrapper:nil]; +} + +- (instancetype)initWithTransactionContext:(SentryTransactionContext *)transactionContext + hub:(nullable SentryHub *)hub + profilesSamplerDecision: + (nullable SentryProfilesSamplerDecision *)profilesSamplerDecision idleTimeout:(NSTimeInterval)idleTimeout dispatchQueueWrapper:(SentryDispatchQueueWrapper *)dispatchQueueWrapper { return [self initWithTransactionContext:transactionContext hub:hub + profilesSamplerDecision:profilesSamplerDecision waitForChildren:YES idleTimeout:idleTimeout dispatchQueueWrapper:dispatchQueueWrapper]; } -- (instancetype)initWithTransactionContext:(SentryTransactionContext *)transactionContext - hub:(nullable SentryHub *)hub - waitForChildren:(BOOL)waitForChildren - idleTimeout:(NSTimeInterval)idleTimeout - dispatchQueueWrapper: - (nullable SentryDispatchQueueWrapper *)dispatchQueueWrapper +- (instancetype) + initWithTransactionContext:(SentryTransactionContext *)transactionContext + hub:(nullable SentryHub *)hub + profilesSamplerDecision:(nullable SentryProfilesSamplerDecision *)profilesSamplerDecision + waitForChildren:(BOOL)waitForChildren + idleTimeout:(NSTimeInterval)idleTimeout + dispatchQueueWrapper:(nullable SentryDispatchQueueWrapper *)dispatchQueueWrapper { if (self = [super init]) { self.rootSpan = [[SentrySpan alloc] initWithTransaction:self context:transactionContext]; @@ -125,6 +149,7 @@ - (instancetype)initWithTransactionContext:(SentryTransactionContext *)transacti _children = [[NSMutableArray alloc] init]; self.hub = hub; self.isWaitingForChildren = NO; + _profilesSamplerDecision = profilesSamplerDecision; _waitForChildren = waitForChildren; _tags = [[NSMutableDictionary alloc] init]; _data = [[NSMutableDictionary alloc] init]; @@ -150,7 +175,7 @@ - (instancetype)initWithTransactionContext:(SentryTransactionContext *)transacti } #endif // SENTRY_HAS_UIKIT #if SENTRY_TARGET_PROFILING_SUPPORTED - if ([_hub getClient].options.enableProfiling) { + if (_profilesSamplerDecision.decision == kSentrySampleDecisionYes) { [profilerLock lock]; if (profiler == nil) { profiler = [[SentryProfiler alloc] init]; @@ -431,7 +456,7 @@ - (void)finishInternal #if SENTRY_TARGET_PROFILING_SUPPORTED SentryScreenFrames *frameInfo; - if ([_hub getClient].options.enableProfiling) { + if (_profilesSamplerDecision.decision == kSentrySampleDecisionYes) { [SentryLog logWithMessage:@"Stopping profiler." andLevel:kSentryLevelDebug]; [profilerLock lock]; [profiler stop]; @@ -489,7 +514,7 @@ - (void)finishInternal NSMutableArray *additionalEnvelopeItems = [NSMutableArray array]; #if SENTRY_TARGET_PROFILING_SUPPORTED - if ([_hub getClient].options.enableProfiling) { + if (_profilesSamplerDecision.decision == kSentrySampleDecisionYes) { [profilerLock lock]; if (profiler != nil) { SentryEnvelopeItem *profile = [profiler buildEnvelopeItemForTransaction:transaction diff --git a/Sources/Sentry/include/SentryOptions+Private.h b/Sources/Sentry/include/SentryOptions+Private.h index 1c867c52377..01fcf17148d 100644 --- a/Sources/Sentry/include/SentryOptions+Private.h +++ b/Sources/Sentry/include/SentryOptions+Private.h @@ -7,10 +7,17 @@ SentryOptions (Private) @property (nullable, nonatomic, copy, readonly) NSNumber *defaultTracesSampleRate; +#if SENTRY_TARGET_PROFILING_SUPPORTED +@property (nullable, nonatomic, copy, readonly) NSNumber *defaultProfilesSampleRate; +@property (nonatomic, assign) BOOL enableProfiling_DEPRECATED_TEST_ONLY; +#endif + - (BOOL)isValidSampleRate:(NSNumber *)sampleRate; - (BOOL)isValidTracesSampleRate:(NSNumber *)tracesSampleRate; +- (BOOL)isValidProfilesSampleRate:(NSNumber *)profilesSampleRate; + @property (nonatomic, strong, readonly) NSSet *enabledIntegrations; - (void)removeEnabledIntegration:(NSString *)integration; diff --git a/Sources/Sentry/include/SentryTracer.h b/Sources/Sentry/include/SentryTracer.h index cb6735e0733..cc8428bf940 100644 --- a/Sources/Sentry/include/SentryTracer.h +++ b/Sources/Sentry/include/SentryTracer.h @@ -4,7 +4,7 @@ NS_ASSUME_NONNULL_BEGIN @class SentryHub, SentryTransactionContext, SentryTraceHeader, SentryTraceContext, - SentryDispatchQueueWrapper, SentryTracer; + SentryDispatchQueueWrapper, SentryTracer, SentryProfilesSamplerDecision; static NSTimeInterval const SentryTracerDefaultTimeout = 3.0; @@ -110,12 +110,32 @@ static NSTimeInterval const SentryTracerDefaultTimeout = 3.0; * * @param transactionContext Transaction context * @param hub A hub to bind this transaction + * @param profilesSamplerDecision Whether to sample a profile corresponding to this transaction + * @param waitForChildren Whether this tracer should wait all children to finish. + * + * @return SentryTracer + */ +- (instancetype)initWithTransactionContext:(SentryTransactionContext *)transactionContext + hub:(nullable SentryHub *)hub + profilesSamplerDecision: + (nullable SentryProfilesSamplerDecision *)profilesSamplerDecision + waitForChildren:(BOOL)waitForChildren; + +/** + * Init a SentryTracer with given transaction context, hub and whether the tracer should wait + * for all children to finish before it finishes. + * + * @param transactionContext Transaction context + * @param hub A hub to bind this transaction + * @param profilesSamplerDecision Whether to sample a profile corresponding to this transaction * @param idleTimeout The idle time to wait until to finish the transaction. * * @return SentryTracer */ - (instancetype)initWithTransactionContext:(SentryTransactionContext *)transactionContext hub:(nullable SentryHub *)hub + profilesSamplerDecision: + (nullable SentryProfilesSamplerDecision *)profilesSamplerDecision idleTimeout:(NSTimeInterval)idleTimeout dispatchQueueWrapper:(SentryDispatchQueueWrapper *)dispatchQueueWrapper; diff --git a/Tests/SentryTests/Performance/SentryTracerObjCTests.m b/Tests/SentryTests/Performance/SentryTracerObjCTests.m index a7549a863ad..65c9aa0d5e0 100644 --- a/Tests/SentryTests/Performance/SentryTracerObjCTests.m +++ b/Tests/SentryTests/Performance/SentryTracerObjCTests.m @@ -24,6 +24,7 @@ - (void)testSpanFinishesAfterTracerReleased_NoCrash_TracerIsNil [[SentryTransactionContext alloc] initWithOperation:@""]; SentryTracer *tracer = [[SentryTracer alloc] initWithTransactionContext:context hub:hub + profilesSamplerDecision:nil waitForChildren:YES]; [tracer finish]; child = [tracer startChildWithOperation:@"child"]; diff --git a/Tests/SentryTests/Performance/SentryTracerTests.swift b/Tests/SentryTests/Performance/SentryTracerTests.swift index 76c58472018..04b51e6e82d 100644 --- a/Tests/SentryTests/Performance/SentryTracerTests.swift +++ b/Tests/SentryTests/Performance/SentryTracerTests.swift @@ -733,7 +733,7 @@ class SentryTracerTests: XCTestCase { func testCapturesProfile_whenProfilingEnabled() { let scope = Scope() let options = Options() - options.enableProfiling = true + options.profilesSampleRate = 1.0 options.tracesSampleRate = 1.0 let client = TestClient(options: options)! let hub = TestHub(client: client, andScope: scope) @@ -748,7 +748,7 @@ class SentryTracerTests: XCTestCase { func testDoesNotCapturesProfile_whenProfilingDisabled() { let scope = Scope() let options = Options() - options.enableProfiling = false + options.profilesSampleRate = 0.0 options.tracesSampleRate = 1.0 let client = TestClient(options: options)! let hub = TestHub(client: client, andScope: scope) diff --git a/Tests/SentryTests/SentryHubTests.swift b/Tests/SentryTests/SentryHubTests.swift index b21a56dc294..7f8355438f8 100644 --- a/Tests/SentryTests/SentryHubTests.swift +++ b/Tests/SentryTests/SentryHubTests.swift @@ -268,13 +268,13 @@ class SentryHubTests: XCTestCase { func testStartTransactionNotSamplingUsingSampleRate() { testSampler(expected: .no) { options in - options.tracesSampler = { _ in return 0.49 } + options.tracesSampleRate = 0.49 } } func testStartTransactionSamplingUsingSampleRate() { testSampler(expected: .yes) { options in - options.tracesSampler = { _ in return 0.5 } + options.tracesSampleRate = 0.50 } } @@ -330,9 +330,9 @@ class SentryHubTests: XCTestCase { } #if os(iOS) || os(macOS) || targetEnvironment(macCatalyst) - func testStartTransaction_WhenProfilingEnabled_CapturesProfile() { + func testStartTransaction_ProfilingDataIsValid() { let options = fixture.options - options.enableProfiling = true + options.profilesSampleRate = 1.0 options.tracesSampler = {(_: SamplingContext) -> NSNumber in return 1 } @@ -371,21 +371,53 @@ class SentryHubTests: XCTestCase { } } - func testStartTransaction_WhenProfilingDisabled_DoesNotCaptureProfile() { - let options = fixture.options - options.enableProfiling = false - options.tracesSampler = {(_: SamplingContext) -> NSNumber in - return 1 + func testStartTransaction_NotSamplingProfileUsingEnableProfiling() { + testProfilesSampler(expected: .no) { options in + options.enableProfiling_DEPRECATED_TEST_ONLY = false } - let hub = fixture.getSut(options) - let span = hub.startTransaction(name: fixture.transactionName, operation: fixture.transactionOperation) - span.finish() - - guard let additionalEnvelopeItems = fixture.client.captureEventWithScopeInvocations.first?.additionalEnvelopeItems else { - XCTFail("Expected to capture at least 1 event") - return + } + + func testStartTransaction_SamplingProfileUsingEnableProfiling() { + testProfilesSampler(expected: .yes) { options in + options.enableProfiling_DEPRECATED_TEST_ONLY = true + } + } + + func testStartTransaction_NotSamplingProfileUsingSampleRate() { + testProfilesSampler(expected: .no) { options in + options.profilesSampleRate = 0.49 + } + } + + func testStartTransaction_SamplingProfileUsingSampleRate() { + testProfilesSampler(expected: .yes) { options in + options.profilesSampleRate = 0.5 + } + } + + func testStartTransaction_SamplingProfileUsingProfilesSampler() { + testProfilesSampler(expected: .yes) { options in + options.profilesSampler = { _ in return 0.51 } + } + } + + func testStartTransaction_WhenProfilesSampleRateAndProfilesSamplerNil() { + testProfilesSampler(expected: .no) { options in + options.profilesSampleRate = nil + options.profilesSampler = { _ in return nil } + } + } + + func testStartTransaction_WhenProfilesSamplerOutOfRange_TooBig() { + testProfilesSampler(expected: .no) { options in + options.profilesSampler = { _ in return 1.01 } + } + } + + func testStartTransaction_WhenProfilesSamplersOutOfRange_TooSmall() { + testProfilesSampler(expected: .no) { options in + options.profilesSampler = { _ in return -0.01 } } - XCTAssertEqual(0, additionalEnvelopeItems.count) } #endif @@ -855,10 +887,36 @@ class SentryHubTests: XCTestCase { options(fixture.options) let hub = fixture.getSut() - Dynamic(hub).sampler.random = fixture.random + Dynamic(hub).tracesSampler.random = fixture.random let span = hub.startTransaction(name: fixture.transactionName, operation: fixture.transactionOperation) XCTAssertEqual(expected, span.context.sampled) } + + private func testProfilesSampler(expected: SentrySampleDecision, options: (Options) -> Void) { + let fixtureOptions = fixture.options + fixtureOptions.tracesSampleRate = 1.0 + options(fixtureOptions) + + let hub = fixture.getSut() + Dynamic(hub).tracesSampler.random = TestRandom(value: 1.0) + Dynamic(hub).profilesSampler.random = TestRandom(value: 0.5) + + let span = hub.startTransaction(name: fixture.transactionName, operation: fixture.transactionOperation) + span.finish() + + guard let additionalEnvelopeItems = fixture.client.captureEventWithScopeInvocations.first?.additionalEnvelopeItems else { + XCTFail("Expected to capture at least 1 event") + return + } + switch expected { + case .undecided, .no: + XCTAssertEqual(0, additionalEnvelopeItems.count) + case .yes: + XCTAssertEqual(1, additionalEnvelopeItems.count) + @unknown default: + fatalError("Unexpected value for sample decision") + } + } } diff --git a/Tests/SentryTests/SentryOptionsTest.m b/Tests/SentryTests/SentryOptionsTest.m index 9ad4b9f427c..714108a57a7 100644 --- a/Tests/SentryTests/SentryOptionsTest.m +++ b/Tests/SentryTests/SentryOptionsTest.m @@ -543,7 +543,12 @@ - (void)assertDefaultValues:(SentryOptions *)options XCTAssertEqual(NO, options.enableFileIOTracking); XCTAssertEqual(YES, options.enableAutoBreadcrumbTracking); #if SENTRY_TARGET_PROFILING_SUPPORTED +# pragma clang diagnostic push +# pragma clang diagnostic ignored "-Wdeprecated-declarations" XCTAssertEqual(NO, options.enableProfiling); +# pragma clang diagnostic pop + XCTAssertNil(options.profilesSampleRate); + XCTAssertNil(options.profilesSampler); #endif #pragma clang diagnostic push @@ -731,13 +736,6 @@ - (void)testEnableSwizzling [self testBooleanField:@"enableSwizzling"]; } -#if SENTRY_TARGET_PROFILING_SUPPORTED -- (void)testEnableProfiling -{ - [self testBooleanField:@"enableProfiling" defaultValue:NO]; -} -#endif - - (void)testTracesSampleRate { SentryOptions *options = [self getValidOptions:@{ @"tracesSampleRate" : @0.1 }]; @@ -851,6 +849,136 @@ - (void)testIsTracingEnabled_TracesSamplerSet_IsEnabled XCTAssertTrue(options.isTracingEnabled); } +#if SENTRY_TARGET_PROFILING_SUPPORTED +- (void)testEnableProfiling +{ + [self testBooleanField:@"enableProfiling" defaultValue:NO]; +} + +- (void)testProfilesSampleRate +{ + SentryOptions *options = [self getValidOptions:@{ @"profilesSampleRate" : @0.1 }]; + + XCTAssertEqual(options.profilesSampleRate.doubleValue, 0.1); +} + +- (void)testDefaultProfilesSampleRate +{ + SentryOptions *options = [self getValidOptions:@{}]; + + XCTAssertNil(options.profilesSampleRate); +} + +- (void)testProfilesSampleRate_SetToNil +{ + SentryOptions *options = [[SentryOptions alloc] init]; + options.profilesSampleRate = nil; + XCTAssertNil(options.profilesSampleRate); +} + +- (void)testProfilesSampleRateLowerBound +{ + SentryOptions *options = [[SentryOptions alloc] init]; + options.profilesSampleRate = @0.5; + + NSNumber *lowerBound = @0; + options.profilesSampleRate = lowerBound; + XCTAssertEqual(lowerBound, options.profilesSampleRate); + + options.profilesSampleRate = @0.5; + + NSNumber *tooLow = @-0.01; + options.profilesSampleRate = tooLow; + XCTAssertNil(options.profilesSampleRate); +} + +- (void)testProfilesSampleRateUpperBound +{ + SentryOptions *options = [[SentryOptions alloc] init]; + options.profilesSampleRate = @0.5; + + NSNumber *lowerBound = @1; + options.profilesSampleRate = lowerBound; + XCTAssertEqual(lowerBound, options.profilesSampleRate); + + options.profilesSampleRate = @0.5; + + NSNumber *tooLow = @1.01; + options.profilesSampleRate = tooLow; + XCTAssertNil(options.profilesSampleRate); +} + +- (void)testIsProfilingEnabled_NothingSet_IsDisabled +{ + SentryOptions *options = [[SentryOptions alloc] init]; + XCTAssertFalse(options.isProfilingEnabled); +} + +- (void)testIsProfilingEnabled_ProfilesSampleRateSetToZero_IsDisabled +{ + SentryOptions *options = [[SentryOptions alloc] init]; + options.profilesSampleRate = @0.00; + XCTAssertFalse(options.isProfilingEnabled); +} + +- (void)testIsProfilingEnabled_ProfilesSampleRateSet_IsEnabled +{ + SentryOptions *options = [[SentryOptions alloc] init]; + options.profilesSampleRate = @0.01; + XCTAssertTrue(options.isProfilingEnabled); +} + +- (void)testIsProfilingEnabled_ProfilesSamplerSet_IsEnabled +{ + SentryOptions *options = [[SentryOptions alloc] init]; + options.profilesSampler = ^(SentrySamplingContext *context) { + XCTAssertNotNil(context); + return @0.0; + }; + XCTAssertTrue(options.isProfilingEnabled); +} + +- (void)testIsProfilingEnabled_EnableProfilingSet_IsEnabled +{ + SentryOptions *options = [[SentryOptions alloc] init]; +# pragma clang diagnostic push +# pragma clang diagnostic ignored "-Wdeprecated-declarations" + options.enableProfiling = YES; +# pragma clang diagnostic pop + XCTAssertTrue(options.isProfilingEnabled); +} + +- (double)profilesSamplerCallback:(NSDictionary *)context +{ + return 0.1; +} + +- (void)testProfilesSampler +{ + SentryTracesSamplerCallback sampler = ^(SentrySamplingContext *context) { + XCTAssertNotNil(context); + return @1.0; + }; + + SentryOptions *options = [self getValidOptions:@{ @"profilesSampler" : sampler }]; + + SentrySamplingContext *context = [[SentrySamplingContext alloc] init]; + XCTAssertEqual(options.profilesSampler(context), @1.0); +} + +- (void)testDefaultProfilesSampler +{ + SentryOptions *options = [self getValidOptions:@{}]; + XCTAssertNil(options.profilesSampler); +} + +- (void)testGarbageProfilesSampler_ReturnsNil +{ + SentryOptions *options = [self getValidOptions:@{ @"profilesSampler" : @"fault" }]; + XCTAssertNil(options.profilesSampler); +} +#endif + - (void)testInAppIncludes { NSArray *expected = @[ @"iOS-Swift", @"BusinessLogic" ]; diff --git a/scripts/add-sentry-to-alamofire.patch b/scripts/add-sentry-to-alamofire.patch index ae60f667f89..39dd9c92942 100644 --- a/scripts/add-sentry-to-alamofire.patch +++ b/scripts/add-sentry-to-alamofire.patch @@ -120,7 +120,7 @@ index 1eeafe7..f5f3dea 100644 + options.environment = "integration-tests" + options.tracesSampleRate = 1.0 + options.enableFileIOTracking = true -+ options.enableProfiling = true ++ options.profilesSampleRate = 1.0 + } + + SentryInitialized = true diff --git a/scripts/add-sentry-to-homekit.patch b/scripts/add-sentry-to-homekit.patch index 48dcfa82b53..8de5a08a625 100644 --- a/scripts/add-sentry-to-homekit.patch +++ b/scripts/add-sentry-to-homekit.patch @@ -32,7 +32,7 @@ index 8e0e35f4..3d34887d 100644 + options.environment = "integration-tests" + options.tracesSampleRate = 1.0 + options.enableFileIOTracking = true -+ options.enableProfiling = true ++ options.profilesSampleRate = 1.0 + } + if NSClassFromString("XCTest") != nil { diff --git a/scripts/add-sentry-to-vlc.patch b/scripts/add-sentry-to-vlc.patch index 565dd73b546..10811ff9f0b 100644 --- a/scripts/add-sentry-to-vlc.patch +++ b/scripts/add-sentry-to-vlc.patch @@ -33,7 +33,7 @@ index 45e05469..0050ffbc 100644 + options.environment = @"integration-tests"; + options.tracesSampleRate = @0; + options.enableFileIOTracking = YES; -+ options.enableProfiling = YES; ++ options.profilesSampleRate = @1.0; + }]; self.orientationLock = UIInterfaceOrientationMaskPortrait | UIInterfaceOrientationMaskLandscape; diff --git a/scripts/set-device-tests-environment.patch b/scripts/set-device-tests-environment.patch index dd47e9d1d91..86e84b4fc65 100644 --- a/scripts/set-device-tests-environment.patch +++ b/scripts/set-device-tests-environment.patch @@ -10,5 +10,5 @@ index 25b92eed..8934d90b 100644 + // The UI tests generate false OOMs + options.enableOutOfMemoryTracking = false options.enableCoreDataTracking = true - options.enableProfiling = true + options.profilesSampleRate = 1.0 options.attachScreenshot = true From ffe3af906ce083597893e32a03bf18c781e07912 Mon Sep 17 00:00:00 2001 From: Indragie Karunaratne Date: Wed, 27 Jul 2022 10:47:22 -0700 Subject: [PATCH 04/16] fix: Add more descriptive deprecation message for enableProfiling (#2011) * fix: Add more descriptive deprecation message for enableProfiling * Format code Co-authored-by: Sentry Github Bot --- Sources/Sentry/Public/SentryOptions.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Sources/Sentry/Public/SentryOptions.h b/Sources/Sentry/Public/SentryOptions.h index 1cdf2b4a6a4..7d818c69605 100644 --- a/Sources/Sentry/Public/SentryOptions.h +++ b/Sources/Sentry/Public/SentryOptions.h @@ -346,7 +346,8 @@ NS_SWIFT_NAME(Options) * feature is not supported on watchOS or tvOS. */ @property (nonatomic, assign) BOOL enableProfiling DEPRECATED_MSG_ATTRIBUTE( - "This property will be removed in a future version of the SDK"); + "Use profilesSampleRate or profilesSampler instead. This property will be removed in a future " + "version of the SDK"); #endif /** From 91cf82a906ef359eb1f2ecafa8599104bb357eca Mon Sep 17 00:00:00 2001 From: Andrew McKnight Date: Wed, 27 Jul 2022 16:03:21 -0800 Subject: [PATCH 05/16] ci: fix ui thread starvation in benchmarks (#2009) Maxing out CPU with a hot while loop also prevented the test runner app from being able to query the accessibility system to hit the stop button. This causes lots of retries and eventual failures or timeouts in other areas, and the tests to run a long time in general. Instead of having a button press stop the benchmark and contending for time on the main run loop, just remove the start/stop buttons and start when the view controller appears, and set a 15 minute delay to stop the benchmarks. Then the test runner queries the text field for results as usual. --- .../SentrySDKPerformanceBenchmarkTests.m | 14 +---- .../PerformanceViewController.swift | 59 +++++++------------ 2 files changed, 22 insertions(+), 51 deletions(-) diff --git a/Samples/iOS-Swift/PerformanceBenchmarks/SentrySDKPerformanceBenchmarkTests.m b/Samples/iOS-Swift/PerformanceBenchmarks/SentrySDKPerformanceBenchmarkTests.m index a0adad6856b..f8e6057b202 100644 --- a/Samples/iOS-Swift/PerformanceBenchmarks/SentrySDKPerformanceBenchmarkTests.m +++ b/Samples/iOS-Swift/PerformanceBenchmarks/SentrySDKPerformanceBenchmarkTests.m @@ -84,24 +84,12 @@ + (void)tearDown [app launch]; [app.buttons[@"Performance scenarios"] tap]; - XCUIElement *startButton = app.buttons[@"Start test"]; - if (![startButton waitForExistenceWithTimeout:5.0]) { - XCTFail(@"Couldn't find benchmark retrieval button."); - } - [startButton tap]; - - // after hitting the start button, the test app will do CPU intensive work until hitting the + // after navigating to the test, the test app will do CPU intensive work until hitting the // stop button. wait 15 seconds so that work can be done while the profiler does its thing, // and the benchmarking observation in the test app will record how much CPU time is used by // everything sleep(15); - XCUIElement *stopButton = app.buttons[@"Stop test"]; - if (![stopButton waitForExistenceWithTimeout:5.0]) { - XCTFail(@"Couldn't find benchmark retrieval button."); - } - [stopButton tap]; - XCUIElement *textField = app.textFields[@"io.sentry.benchmark.value-marshaling-text-field"]; if (![textField waitForExistenceWithTimeout:5.0]) { XCTFail(@"Couldn't find benchmark value marshaling text field."); diff --git a/Samples/iOS-Swift/iOS-Swift/ViewControllers/PerformanceViewController.swift b/Samples/iOS-Swift/iOS-Swift/ViewControllers/PerformanceViewController.swift index b96ace3e3c5..b4f81dd1a4a 100644 --- a/Samples/iOS-Swift/iOS-Swift/ViewControllers/PerformanceViewController.swift +++ b/Samples/iOS-Swift/iOS-Swift/ViewControllers/PerformanceViewController.swift @@ -2,49 +2,19 @@ import Sentry import UIKit class PerformanceViewController: UIViewController { - private let startTestButton = UIButton(type: .custom) - private let stopTestButton = UIButton(type: .custom) private let valueTextField = UITextField(frame: .zero) override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) - startTestButton.addTarget(self, action: #selector(startTest), for: .touchUpInside) - startTestButton.setTitle("Start test", for: .normal) - - stopTestButton.addTarget(self, action: #selector(stopTest), for: .touchUpInside) - stopTestButton.setTitle("Stop test", for: .normal) - - let buttons = [ - startTestButton, - stopTestButton - ] - buttons.forEach { - $0.setTitleColor(.black, for: .normal) - } valueTextField.accessibilityLabel = "io.sentry.benchmark.value-marshaling-text-field" - let stack = UIStackView(arrangedSubviews: buttons + [valueTextField]) - stack.axis = .vertical - stack.alignment = .center - stack.distribution = .fillProportionally - stack.translatesAutoresizingMaskIntoConstraints = false - view.addSubview(stack) + valueTextField.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(valueTextField) NSLayoutConstraint.activate([ - stack.leadingAnchor.constraint(equalTo: view.leadingAnchor), - stack.trailingAnchor.constraint(equalTo: view.trailingAnchor), - stack.bottomAnchor.constraint(lessThanOrEqualTo: view.bottomAnchor), - stack.centerYAnchor.constraint(equalTo: view.centerYAnchor) + valueTextField.centerYAnchor.constraint(equalTo: view.centerYAnchor), + valueTextField.widthAnchor.constraint(equalTo: view.widthAnchor, constant: -20) ]) - - if #available(iOS 11.0, *) { - NSLayoutConstraint.activate([ - stack.topAnchor.constraint(greaterThanOrEqualTo: view.safeAreaLayoutGuide.topAnchor) - ]) - } else { - NSLayoutConstraint.activate([ - stack.topAnchor.constraint(greaterThanOrEqualTo: view.topAnchor) - ]) - } + valueTextField.isHidden = true view.backgroundColor = .white } @@ -53,6 +23,11 @@ class PerformanceViewController: UIViewController { fatalError("init(coder:) has not been implemented") } + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + startTest() + } + override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) timer?.invalidate() @@ -68,8 +43,10 @@ class PerformanceViewController: UIViewController { private let iterations = 5_000_000 private let range = 1.. Double { +private extension PerformanceViewController { + func doWork(withNumber a: Double) -> Double { var b: Double if arc4random() % 2 == 0 { b = fmod(a, Double.random(in: range)) @@ -89,16 +66,20 @@ class PerformanceViewController: UIViewController { } } - @objc func startTest() { + func startTest() { SentrySDK.configureScope { $0.setTag(value: "performance-benchmark", key: "uitest-type") } transaction = SentrySDK.startTransaction(name: "io.sentry.benchmark.transaction", operation: "crunch-numbers") timer = Timer.scheduledTimer(timeInterval: interval, target: self, selector: #selector(doRandomWork), userInfo: nil, repeats: true) SentryBenchmarking.startBenchmark() + + DispatchQueue.main.asyncAfter(deadline: .now() + 15) { + self.stopTest() + } } - @objc func stopTest() { + func stopTest() { defer { timer?.invalidate() transaction?.finish() @@ -110,6 +91,8 @@ class PerformanceViewController: UIViewController { valueTextField.text = "nil" return } + + valueTextField.isHidden = false valueTextField.text = "\(value)" SentrySDK.configureScope { From 69e4cd5f850cea3c093d4cb415a8933ab4878c50 Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Thu, 28 Jul 2022 06:26:07 -0300 Subject: [PATCH 06/16] feat: Add transaction to baggage and trace headers (#1992) Added the transaction name to http baggage header and to envelope trace header. --- CHANGELOG.md | 1 + Sources/Sentry/SentryBaggage.m | 5 +++++ Sources/Sentry/SentryTraceContext.m | 8 ++++++++ Sources/Sentry/include/SentryBaggage.h | 1 + Sources/Sentry/include/SentryTraceContext.h | 6 ++++++ Tests/SentryTests/Helper/SentrySerializationTests.swift | 4 ++-- Tests/SentryTests/Protocol/SentryEnvelopeTests.swift | 2 +- Tests/SentryTests/Transaction/SentryBaggageTests.swift | 6 +++--- Tests/SentryTests/Transaction/SentryTraceStateTests.swift | 3 +++ 9 files changed, 30 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 11fa83fbe0c..0d3d753ed39 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ ### Features - Read free_memory when the event is captured, not only at SDK startup (#1962) +- Add transaction to baggage and trace headers (#1992) - Provide private access to SentryOptions for hybrid SDKs (#1991) ### Fixes diff --git a/Sources/Sentry/SentryBaggage.m b/Sources/Sentry/SentryBaggage.m index 947d5a69631..111f5e1ebb5 100644 --- a/Sources/Sentry/SentryBaggage.m +++ b/Sources/Sentry/SentryBaggage.m @@ -14,6 +14,7 @@ - (instancetype)initWithTraceId:(SentryId *)traceId publicKey:(NSString *)publicKey releaseName:(nullable NSString *)releaseName environment:(nullable NSString *)environment + transaction:(nullable NSString *)transaction userSegment:(nullable NSString *)userSegment sampleRate:(nullable NSString *)sampleRate { @@ -23,6 +24,7 @@ - (instancetype)initWithTraceId:(SentryId *)traceId _publicKey = publicKey; _releaseName = releaseName; _environment = environment; + _transaction = transaction; _userSegment = userSegment; _sampleRate = sampleRate; } @@ -49,6 +51,9 @@ - (NSString *)toHTTPHeaderWithOriginalBaggage:(NSDictionary *_Nullable)originalB if (_environment != nil) [information setValue:_environment forKey:@"sentry-environment"]; + if (_transaction != nil) + [information setValue:_transaction forKey:@"sentry-transaction"]; + if (_userSegment != nil) [information setValue:_userSegment forKey:@"sentry-user_segment"]; diff --git a/Sources/Sentry/SentryTraceContext.m b/Sources/Sentry/SentryTraceContext.m index 8acc9bbc1d6..8f9425a0097 100644 --- a/Sources/Sentry/SentryTraceContext.m +++ b/Sources/Sentry/SentryTraceContext.m @@ -17,6 +17,7 @@ - (instancetype)initWithTraceId:(SentryId *)traceId publicKey:(NSString *)publicKey releaseName:(nullable NSString *)releaseName environment:(nullable NSString *)environment + transaction:(nullable NSString *)transaction userSegment:(nullable NSString *)userSegment sampleRate:(nullable NSString *)sampleRate { @@ -25,6 +26,7 @@ - (instancetype)initWithTraceId:(SentryId *)traceId _publicKey = publicKey; _environment = environment; _releaseName = releaseName; + _transaction = transaction; _userSegment = userSegment; _sampleRate = sampleRate; } @@ -63,6 +65,7 @@ - (nullable instancetype)initWithTracer:(SentryTracer *)tracer publicKey:options.parsedDsn.url.user releaseName:options.releaseName environment:options.environment + transaction:tracer.name userSegment:userSegment sampleRate:sampleRate]; } @@ -87,6 +90,7 @@ - (nullable instancetype)initWithDict:(NSDictionary *)dictionary publicKey:publicKey releaseName:dictionary[@"release"] environment:dictionary[@"environment"] + transaction:dictionary[@"transaction"] userSegment:userSegment sampleRate:dictionary[@"sample_rate"]]; } @@ -97,6 +101,7 @@ - (SentryBaggage *)toBaggage publicKey:_publicKey releaseName:_releaseName environment:_environment + transaction:_transaction userSegment:_userSegment sampleRate:_sampleRate]; return result; @@ -113,6 +118,9 @@ - (SentryBaggage *)toBaggage if (_environment != nil) [result setValue:_environment forKey:@"environment"]; + if (_transaction != nil) + [result setValue:_transaction forKey:@"transaction"]; + if (_userSegment != nil) [result setValue:_userSegment forKey:@"user_segment"]; diff --git a/Sources/Sentry/include/SentryBaggage.h b/Sources/Sentry/include/SentryBaggage.h index 0ac6a1d7702..508752927fa 100644 --- a/Sources/Sentry/include/SentryBaggage.h +++ b/Sources/Sentry/include/SentryBaggage.h @@ -53,6 +53,7 @@ static NSString *const SENTRY_BAGGAGE_HEADER = @"baggage"; publicKey:(NSString *)publicKey releaseName:(nullable NSString *)releaseName environment:(nullable NSString *)environment + transaction:(nullable NSString *)transaction userSegment:(nullable NSString *)userSegment sampleRate:(nullable NSString *)sampleRate; diff --git a/Sources/Sentry/include/SentryTraceContext.h b/Sources/Sentry/include/SentryTraceContext.h index 6c976622f39..818ea29c696 100644 --- a/Sources/Sentry/include/SentryTraceContext.h +++ b/Sources/Sentry/include/SentryTraceContext.h @@ -29,6 +29,11 @@ NS_ASSUME_NONNULL_BEGIN */ @property (nullable, nonatomic, readonly) NSString *environment; +/** + * The transaction name set on the scope. + */ +@property (nullable, nonatomic, readonly) NSString *transaction; + /** * A subset of the scope's user context. */ @@ -46,6 +51,7 @@ NS_ASSUME_NONNULL_BEGIN publicKey:(NSString *)publicKey releaseName:(nullable NSString *)releaseName environment:(nullable NSString *)environment + transaction:(nullable NSString *)transaction userSegment:(nullable NSString *)userSegment sampleRate:(nullable NSString *)sampleRate; diff --git a/Tests/SentryTests/Helper/SentrySerializationTests.swift b/Tests/SentryTests/Helper/SentrySerializationTests.swift index 5ad8bdcaab0..6dbdea90f07 100644 --- a/Tests/SentryTests/Helper/SentrySerializationTests.swift +++ b/Tests/SentryTests/Helper/SentrySerializationTests.swift @@ -4,7 +4,7 @@ class SentrySerializationTests: XCTestCase { private class Fixture { static var invalidData = "hi".data(using: .utf8)! - static var traceContext = SentryTraceContext(trace: SentryId(), publicKey: "PUBLIC_KEY", releaseName: "RELEASE_NAME", environment: "TEST", userSegment: "some segment", sampleRate: "0.25") + static var traceContext = SentryTraceContext(trace: SentryId(), publicKey: "PUBLIC_KEY", releaseName: "RELEASE_NAME", environment: "TEST", transaction: "transaction", userSegment: "some segment", sampleRate: "0.25") } func testSentryEnvelopeSerializer_WithSingleEvent() { @@ -111,7 +111,7 @@ class SentrySerializationTests: XCTestCase { } func testSentryEnvelopeSerializer_TraceStateWithoutUser() { - let trace = SentryTraceContext(trace: SentryId(), publicKey: "PUBLIC_KEY", releaseName: "RELEASE_NAME", environment: "TEST", userSegment: nil, sampleRate: nil) + let trace = SentryTraceContext(trace: SentryId(), publicKey: "PUBLIC_KEY", releaseName: "RELEASE_NAME", environment: "TEST", transaction: "transaction", userSegment: nil, sampleRate: nil) let envelopeHeader = SentryEnvelopeHeader(id: nil, traceContext: trace) let envelope = SentryEnvelope(header: envelopeHeader, singleItem: createItemWithEmptyAttachment()) diff --git a/Tests/SentryTests/Protocol/SentryEnvelopeTests.swift b/Tests/SentryTests/Protocol/SentryEnvelopeTests.swift index 898aafbbd62..b60a20e2545 100644 --- a/Tests/SentryTests/Protocol/SentryEnvelopeTests.swift +++ b/Tests/SentryTests/Protocol/SentryEnvelopeTests.swift @@ -159,7 +159,7 @@ class SentryEnvelopeTests: XCTestCase { func testInitSentryEnvelopeHeader_SetIdAndTraceState() { let eventId = SentryId() - let traceContext = SentryTraceContext(trace: SentryId(), publicKey: "publicKey", releaseName: "releaseName", environment: "environment", userSegment: nil, sampleRate: nil) + let traceContext = SentryTraceContext(trace: SentryId(), publicKey: "publicKey", releaseName: "releaseName", environment: "environment", transaction: "transaction", userSegment: nil, sampleRate: nil) let envelopeHeader = SentryEnvelopeHeader(id: eventId, traceContext: traceContext) XCTAssertEqual(eventId, envelopeHeader.eventId) diff --git a/Tests/SentryTests/Transaction/SentryBaggageTests.swift b/Tests/SentryTests/Transaction/SentryBaggageTests.swift index 7bcc41cd9d0..4344129f123 100644 --- a/Tests/SentryTests/Transaction/SentryBaggageTests.swift +++ b/Tests/SentryTests/Transaction/SentryBaggageTests.swift @@ -5,9 +5,9 @@ import XCTest class SentryBaggageTests: XCTestCase { func test_baggageToHeader() { - let header = SentryBaggage(trace: SentryId.empty, publicKey: "publicKey", releaseName: "release name", environment: "teste", userSegment: "test user", sampleRate: "0.49").toHTTPHeader() + let header = SentryBaggage(trace: SentryId.empty, publicKey: "publicKey", releaseName: "release name", environment: "teste", transaction: "transaction", userSegment: "test user", sampleRate: "0.49").toHTTPHeader() - XCTAssertEqual(header, "sentry-environment=teste,sentry-public_key=publicKey,sentry-release=release%20name,sentry-sample_rate=0.49,sentry-trace_id=00000000000000000000000000000000,sentry-user_segment=test%20user") + XCTAssertEqual(header, "sentry-environment=teste,sentry-public_key=publicKey,sentry-release=release%20name,sentry-sample_rate=0.49,sentry-trace_id=00000000000000000000000000000000,sentry-transaction=transaction,sentry-user_segment=test%20user") } func test_baggageToHeader_AppendToOriginal() { @@ -17,7 +17,7 @@ class SentryBaggageTests: XCTestCase { } func test_baggageToHeader_onlyTrace_ignoreNils() { - let header = SentryBaggage(trace: SentryId.empty, publicKey: "publicKey", releaseName: nil, environment: nil, userSegment: nil, sampleRate: nil).toHTTPHeader() + let header = SentryBaggage(trace: SentryId.empty, publicKey: "publicKey", releaseName: nil, environment: nil, transaction: nil, userSegment: nil, sampleRate: nil).toHTTPHeader() XCTAssertEqual(header, "sentry-public_key=publicKey,sentry-trace_id=00000000000000000000000000000000") } diff --git a/Tests/SentryTests/Transaction/SentryTraceStateTests.swift b/Tests/SentryTests/Transaction/SentryTraceStateTests.swift index 71c911b046c..7ae33f62276 100644 --- a/Tests/SentryTests/Transaction/SentryTraceStateTests.swift +++ b/Tests/SentryTests/Transaction/SentryTraceStateTests.swift @@ -54,6 +54,7 @@ class SentryTraceContextTests: XCTestCase { publicKey: fixture.publicKey, releaseName: fixture.releaseName, environment: fixture.environment, + transaction: fixture.transactionName, userSegment: fixture.userSegment, sampleRate: fixture.sampleRate) @@ -92,6 +93,7 @@ class SentryTraceContextTests: XCTestCase { publicKey: fixture.publicKey, releaseName: fixture.releaseName, environment: fixture.environment, + transaction: fixture.transactionName, userSegment: fixture.userSegment, sampleRate: fixture.sampleRate) @@ -110,6 +112,7 @@ class SentryTraceContextTests: XCTestCase { XCTAssertEqual(traceContext.publicKey, fixture.publicKey) XCTAssertEqual(traceContext.releaseName, fixture.releaseName) XCTAssertEqual(traceContext.environment, fixture.environment) + XCTAssertEqual(traceContext.transaction, fixture.transactionName) XCTAssertEqual(traceContext.userSegment, fixture.userSegment) } From df42a82c5824105ee4162d233419a2bd19838e89 Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Thu, 28 Jul 2022 06:41:55 -0300 Subject: [PATCH 07/16] fix: Sauce labs iPhone 13 pro iOS version iPhone 13 pro iOS version 15.5 is no longer available in Saucelabs. --- .sauce/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.sauce/config.yml b/.sauce/config.yml index 97816c9a15d..2ee7b4fda37 100644 --- a/.sauce/config.yml +++ b/.sauce/config.yml @@ -21,7 +21,7 @@ suites: - name: "iPhone-Pro" devices: - name: "iPhone 13 Pro.*" - platformVersion: "15.5" + platformVersion: "15.6" - name: "iOS-14" devices: From c93cf767351f8e61029a3e06f3ae2821d41f98a3 Mon Sep 17 00:00:00 2001 From: Philipp Hofmann Date: Thu, 28 Jul 2022 14:11:28 +0200 Subject: [PATCH 08/16] Fix address sanitizer compilation error (#1996) Fix address sanitizer compilation error by using the constructor attribute instead of __DATA,__mod_init_func. Fixes GH-1990 --- .github/workflows/test.yml | 14 +++++++++++++- CHANGELOG.md | 1 + Sources/Sentry/SentrySysctl.m | 23 ++++++++++++++--------- fastlane/Fastfile | 3 ++- 4 files changed, 30 insertions(+), 11 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a2732912cb2..f53bc43284d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -214,6 +214,18 @@ jobs: # GitHub Actions sometimes fail to launch the UI tests. Therefore we retry - name: Run Fastlane - run: for i in {1..2}; do fastlane ui_tests_${{matrix.target}} device:"$iPhone 8 (12.4)" && break ; done + run: for i in {1..2}; do fastlane ui_tests_${{matrix.target}} device:"$iPhone 8 (12.4)" address_sanitizer:false && break ; done shell: sh + ui-tests-address-sanitizer: + name: UI Tests with Address Sanitizer + runs-on: macos-12 + + steps: + - uses: actions/checkout@v3 + - run: ./scripts/ci-select-xcode.sh 13.4.1 + + # GitHub Actions sometimes fail to launch the UI tests. Therefore we retry + - name: Run Fastlane + run: for i in {1..2}; do fastlane ui_tests_ios_swift device:"$iPhone 8 (15.5)" address_sanitizer:true && break ; done + shell: sh diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d3d753ed39..7a44b78886c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ - Remove Sentry keys from cached HTTP request headers (#1975) - Collect samples for idle threads in iOS profiler (#1978) +- Fix address sanitizer compilation error (#1996) - Don't override already-set timestamp when finishing Span (#1993) - Respect existing baggage header instead of overwriting it (#1995) diff --git a/Sources/Sentry/SentrySysctl.m b/Sources/Sentry/SentrySysctl.m index 6883a9616a5..feaf8ae25ea 100644 --- a/Sources/Sentry/SentrySysctl.m +++ b/Sources/Sentry/SentrySysctl.m @@ -6,18 +6,23 @@ static NSDate *moduleInitializationTimestamp; static NSDate *runtimeInit = nil; -void -sentryModuleInitializationHook(int argc, char **argv, char **envp) +/** + * + * Constructor priority must be bounded between 101 and 65535 inclusive, see + * https://gcc.gnu.org/onlinedocs/gcc-4.7.0/gcc/Function-Attributes.html and + * https://gcc.gnu.org/onlinedocs/gcc-4.7.0/gcc/C_002b_002b-Attributes.html#C_002b_002b-Attributes + * The constructor attribute causes the function to be called automatically before execution enters + * main(). The lower the priority number, the sooner the constructor runs, which means 100 runs + * before 101. As we want to be as close to main() as possible, we choose a high number. + * + * Previously, we used __DATA,__mod_init_func, which leads to compilation errors and runtime crashes + * when enabling the address sanitizer. + */ +__used __attribute__((constructor(60000))) static void +sentryModuleInitializationHook() { moduleInitializationTimestamp = [NSDate date]; } -/** - * Module initialization functions. The C++ compiler places static constructors here. For more info - * visit: - * https://github.com/aidansteele/osx-abi-macho-file-format-reference#table-2-the-sections-of-a__datasegment - */ -__attribute__((section("__DATA,__mod_init_func"))) typeof(sentryModuleInitializationHook) *__init - = sentryModuleInitializationHook; @implementation SentrySysctl diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 43143988621..bccc0687caf 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -185,7 +185,8 @@ platform :ios do run_tests( workspace: "Sentry.xcworkspace", scheme: "iOS-Swift", - device: options[:device] + device: options[:device], + address_sanitizer: options[:address_sanitizer] ) end From 2b2966423c776d5e70391c9691bcafd67d6d9292 Mon Sep 17 00:00:00 2001 From: Philipp Hofmann Date: Thu, 28 Jul 2022 14:36:09 +0200 Subject: [PATCH 09/16] Fix build failure in unit tests (#2019) --- .../Performance/Network/SentryNetworkTrackerTests.swift | 4 ++-- Tests/SentryTests/Transaction/SentryBaggageTests.swift | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Tests/SentryTests/Integrations/Performance/Network/SentryNetworkTrackerTests.swift b/Tests/SentryTests/Integrations/Performance/Network/SentryNetworkTrackerTests.swift index 30897e359a0..8cb2cd2a9c4 100644 --- a/Tests/SentryTests/Integrations/Performance/Network/SentryNetworkTrackerTests.swift +++ b/Tests/SentryTests/Integrations/Performance/Network/SentryNetworkTrackerTests.swift @@ -457,7 +457,7 @@ class SentryNetworkTrackerTests: XCTestCase { XCTAssertNotNil(headers?["sentry-trace"]) let decodedBaggage = SentrySerialization.decodeBaggage(headers?["baggage"] ?? "") - XCTAssertEqual(decodedBaggage.count, 4) + XCTAssertEqual(decodedBaggage.count, 5) } func test_AddTraceHeader_AppendOriginalBaggage() { @@ -469,7 +469,7 @@ class SentryNetworkTrackerTests: XCTestCase { XCTAssertNotNil(headers?["sentry-trace"]) let decodedBaggage = SentrySerialization.decodeBaggage(headers?["baggage"] ?? "") - XCTAssertEqual(decodedBaggage.count, 5) + XCTAssertEqual(decodedBaggage.count, 6) } func test_RemoveExistingTraceHeader_WhenNoSpan() { diff --git a/Tests/SentryTests/Transaction/SentryBaggageTests.swift b/Tests/SentryTests/Transaction/SentryBaggageTests.swift index 4344129f123..2b0fef74bf6 100644 --- a/Tests/SentryTests/Transaction/SentryBaggageTests.swift +++ b/Tests/SentryTests/Transaction/SentryBaggageTests.swift @@ -11,9 +11,9 @@ class SentryBaggageTests: XCTestCase { } func test_baggageToHeader_AppendToOriginal() { - let header = SentryBaggage(trace: SentryId.empty, publicKey: "publicKey", releaseName: "release name", environment: "teste", userSegment: "test user", sampleRate: "0.49").toHTTPHeader(withOriginalBaggage: ["a": "a", "sentry-trace_id": "to-be-overwritten"]) + let header = SentryBaggage(trace: SentryId.empty, publicKey: "publicKey", releaseName: "release name", environment: "teste", transaction: "transaction", userSegment: "test user", sampleRate: "0.49").toHTTPHeader(withOriginalBaggage: ["a": "a", "sentry-trace_id": "to-be-overwritten"]) - XCTAssertEqual(header, "a=a,sentry-environment=teste,sentry-public_key=publicKey,sentry-release=release%20name,sentry-sample_rate=0.49,sentry-trace_id=00000000000000000000000000000000,sentry-user_segment=test%20user") + XCTAssertEqual(header, "a=a,sentry-environment=teste,sentry-public_key=publicKey,sentry-release=release%20name,sentry-sample_rate=0.49,sentry-trace_id=00000000000000000000000000000000,sentry-transaction=transaction,sentry-user_segment=test%20user") } func test_baggageToHeader_onlyTrace_ignoreNils() { From 0aefd8d1c84044f5a3ae7f0b02846530356cb39d Mon Sep 17 00:00:00 2001 From: Philipp Hofmann Date: Thu, 28 Jul 2022 14:36:22 +0200 Subject: [PATCH 10/16] Disable flaky tesDefaultMaxEnvelopesConcurrent (#2018) --- Tests/SentryTests/Helper/SentryFileManagerTests.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Tests/SentryTests/Helper/SentryFileManagerTests.swift b/Tests/SentryTests/Helper/SentryFileManagerTests.swift index 06806be9bfc..0d62ecee96d 100644 --- a/Tests/SentryTests/Helper/SentryFileManagerTests.swift +++ b/Tests/SentryTests/Helper/SentryFileManagerTests.swift @@ -185,7 +185,10 @@ class SentryFileManagerTests: XCTestCase { XCTAssertEqual(expected, fixture.delegate.envelopeItemsDeleted.invocations) } - func testDefaultMaxEnvelopesConcurrent() { + /** + * Disabled because flaky. Fix with https://github.com/getsentry/sentry-cocoa/issues/2017. + */ + func tesDefaultMaxEnvelopesConcurrent() { let parallelTaskAmount = 5 let queue = DispatchQueue(label: "testDefaultMaxEnvelopesConcurrent", qos: .userInitiated, attributes: [.concurrent, .initiallyInactive]) From 3fc26dbae5843d741b1ffe317d26ce6548b2fff9 Mon Sep 17 00:00:00 2001 From: Indragie Karunaratne Date: Thu, 28 Jul 2022 05:47:17 -0700 Subject: [PATCH 11/16] fix: Use constant for max thread name size instead of magic number (#2012) Co-authored-by: Philipp Hofmann --- Sources/Sentry/SentryThreadHandle.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Sentry/SentryThreadHandle.cpp b/Sources/Sentry/SentryThreadHandle.cpp index 9e59c558b11..38f09851abb 100644 --- a/Sources/Sentry/SentryThreadHandle.cpp +++ b/Sources/Sentry/SentryThreadHandle.cpp @@ -117,7 +117,7 @@ namespace profiling { if (handle == nullptr) { return {}; } - char name[128]; + char name[MAXTHREADNAMESIZE]; if (SENTRY_PROF_LOG_ERROR_RETURN(pthread_getname_np(handle, name, sizeof(name))) == 0) { return std::string(name); } From 3b70b5b641236bbfade656ad414ccec0012b773e Mon Sep 17 00:00:00 2001 From: Indragie Karunaratne Date: Thu, 28 Jul 2022 08:29:16 -0700 Subject: [PATCH 12/16] fix: Remove logging that could occur while a thread is suspended (#2014) Remove some logging that could happen when threads fail to be suspended or resumed. Co-authored-by: Philipp Hofmann --- CHANGELOG.md | 4 ++++ Sources/Sentry/SentryThreadHandle.cpp | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a44b78886c..844278ff1e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ - Add sampling configuration for profiling (#2004) +### Fixes + +- Remove logging that could occur while a thread is suspended (#2014) + ## 7.22.0 ### Features diff --git a/Sources/Sentry/SentryThreadHandle.cpp b/Sources/Sentry/SentryThreadHandle.cpp index 38f09851abb..f4e6b0864ab 100644 --- a/Sources/Sentry/SentryThreadHandle.cpp +++ b/Sources/Sentry/SentryThreadHandle.cpp @@ -240,7 +240,7 @@ namespace profiling { if (handle_ == THREAD_NULL) { return false; } - return SENTRY_PROF_LOG_KERN_RETURN(thread_suspend(handle_)) == KERN_SUCCESS; + return thread_suspend(handle_) == KERN_SUCCESS; } bool @@ -249,7 +249,7 @@ namespace profiling { if (handle_ == THREAD_NULL) { return false; } - return SENTRY_PROF_LOG_KERN_RETURN(thread_resume(handle_)) == KERN_SUCCESS; + return thread_resume(handle_) == KERN_SUCCESS; } bool From 887b464ece17508ca7dba492c94a80bf8573682f Mon Sep 17 00:00:00 2001 From: Indragie Karunaratne Date: Thu, 28 Jul 2022 09:01:16 -0700 Subject: [PATCH 13/16] fix: Handle failure to read thread priority gracefully (#2015) Handle this failure gracefully and only log a thread priority if reading it was successful. Reading the thread priority can return -1 in failure cases and we don't want to store this in the profile payload if we're not able to read a legitimate value. This also fixes a flake with the testStartTransaction_ProfilingDataIsValid test which checked for a valid priority and failed about 20% of the time on macOS. Co-authored-by: Sentry Github Bot Co-authored-by: Philipp Hofmann --- CHANGELOG.md | 1 + Sources/Sentry/SentryProfiler.mm | 20 +++++++----- Sources/Sentry/SentryThreadMetadataCache.cpp | 32 +++++++++++--------- 3 files changed, 30 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 844278ff1e6..a1a920772e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ ### Fixes - Remove logging that could occur while a thread is suspended (#2014) +- Handle failure to read thread priority gracefully (#2015) ## 7.22.0 diff --git a/Sources/Sentry/SentryProfiler.mm b/Sources/Sentry/SentryProfiler.mm index 57145c2f059..db23aa49580 100644 --- a/Sources/Sentry/SentryProfiler.mm +++ b/Sources/Sentry/SentryProfiler.mm @@ -131,7 +131,8 @@ - (void)start _profile = [NSMutableDictionary dictionary]; const auto sampledProfile = [NSMutableDictionary dictionary]; const auto samples = [NSMutableArray *> array]; - const auto threadMetadata = [NSMutableDictionary dictionary]; + const auto threadMetadata = + [NSMutableDictionary dictionary]; const auto queueMetadata = [NSMutableDictionary dictionary]; sampledProfile[@"samples"] = samples; sampledProfile[@"thread_metadata"] = threadMetadata; @@ -152,18 +153,21 @@ - (void)start if (backtrace.queueMetadata.address != 0) { queueAddress = sentry_formatHexAddress(@(backtrace.queueMetadata.address)); } - if (threadMetadata[threadID] == nil) { - const auto metadata = [NSMutableDictionary dictionary]; - if (!backtrace.threadMetadata.name.empty()) { - metadata[@"name"] = - [NSString stringWithUTF8String:backtrace.threadMetadata.name.c_str()]; - } - metadata[@"priority"] = @(backtrace.threadMetadata.priority); + NSMutableDictionary *metadata = threadMetadata[threadID]; + if (metadata == nil) { + metadata = [NSMutableDictionary dictionary]; if (backtrace.threadMetadata.threadID == mainThreadID) { metadata[@"is_main_thread"] = @YES; } threadMetadata[threadID] = metadata; } + if (!backtrace.threadMetadata.name.empty() && metadata[@"name"] == nil) { + metadata[@"name"] = + [NSString stringWithUTF8String:backtrace.threadMetadata.name.c_str()]; + } + if (backtrace.threadMetadata.priority != -1 && metadata[@"priority"] == nil) { + metadata[@"priority"] = @(backtrace.threadMetadata.priority); + } if (queueAddress != nil && queueMetadata[queueAddress] == nil && backtrace.queueMetadata.label != nullptr) { queueMetadata[queueAddress] = @{ diff --git a/Sources/Sentry/SentryThreadMetadataCache.cpp b/Sources/Sentry/SentryThreadMetadataCache.cpp index 9e9677ce816..1546a8a33f8 100644 --- a/Sources/Sentry/SentryThreadMetadataCache.cpp +++ b/Sources/Sentry/SentryThreadMetadataCache.cpp @@ -36,23 +36,25 @@ namespace profiling { metadata.priority = thread.priority(); // If getting the priority fails (via pthread_getschedparam()), that - // means the rest of this is probably going to fail too. - if (metadata.priority != -1) { - auto threadName = thread.name(); - if (isSentryOwnedThreadName(threadName)) { - // Don't collect backtraces for Sentry-owned threads. - metadata.priority = 0; - metadata.threadID = 0; - threadMetadataCache_.push_back({ handle, metadata }); - return metadata; - } - if (threadName.size() > kMaxThreadNameLength) { - threadName.resize(kMaxThreadNameLength); - } - - metadata.name = threadName; + // means the rest of this is probably going to fail too. We also don't + // want to cache this result. + if (metadata.priority == -1) { + return metadata; } + auto threadName = thread.name(); + if (isSentryOwnedThreadName(threadName)) { + // Don't collect backtraces for Sentry-owned threads. + metadata.priority = 0; + metadata.threadID = 0; + threadMetadataCache_.push_back({ handle, metadata }); + return metadata; + } + if (threadName.size() > kMaxThreadNameLength) { + threadName.resize(kMaxThreadNameLength); + } + + metadata.name = threadName; threadMetadataCache_.push_back({ handle, metadata }); return metadata; } else { From 78890baa7c19641b5b2864e64619b4053c035343 Mon Sep 17 00:00:00 2001 From: Indragie Karunaratne Date: Thu, 28 Jul 2022 10:18:10 -0700 Subject: [PATCH 14/16] fix: Log empty samples instead of collecting stacks for idle threads (#2013) * fix: Log empty samples instead of collecting stacks for idle threads Fix test breakage in UI Test testSplitView fails on iOS 12 #2000 Revert fix: Disable broken LaunchUITests to unblock release #2003 Replace stacks for idle threads with an empty sample so that it doesn't render in the profile Collecting stacks for idle threads produces this kind of data, where we have a stack that just shows the thread waiting on a function like mach_msg_trap. Instead of collecting these stacks, we can just log an empty sample, which will prevent it from rendering in the flamegraph. Co-authored-by: Sentry Github Bot Co-authored-by: Philipp Hofmann --- CHANGELOG.md | 1 + .../iOS-SwiftUITests/LaunchUITests.swift | 228 +++++++++--------- Sources/Sentry/SentryBacktrace.cpp | 19 +- Tests/SentryTests/SentryHubTests.swift | 2 +- 4 files changed, 130 insertions(+), 120 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a1a920772e6..af10240417b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ ### Fixes +- Log empty samples instead of collecting stacks for idle threads (#2013) - Remove logging that could occur while a thread is suspended (#2014) - Handle failure to read thread priority gracefully (#2015) diff --git a/Samples/iOS-Swift/iOS-SwiftUITests/LaunchUITests.swift b/Samples/iOS-Swift/iOS-SwiftUITests/LaunchUITests.swift index f13a1c0eced..2e229082195 100644 --- a/Samples/iOS-Swift/iOS-SwiftUITests/LaunchUITests.swift +++ b/Samples/iOS-Swift/iOS-SwiftUITests/LaunchUITests.swift @@ -1,114 +1,114 @@ -//import XCTest -// -//class LaunchUITests: XCTestCase { -// -// private let app: XCUIApplication = XCUIApplication() -// -// override func setUp() { -// super.setUp() -// -// continueAfterFailure = false -// XCUIDevice.shared.orientation = .portrait -// app.launch() -// -// waitForExistenseOfMainScreen() -// checkSlowAndFrozenFrames() -// } -// -// override func tearDown() { -// app.terminate() -// super.tearDown() -// } -// -// func testBreadcrumbData() { -// let breadcrumbLabel = app.staticTexts["breadcrumbLabel"] -// breadcrumbLabel.waitForExistence("Breadcrumb label not found.") -// XCTAssertEqual(breadcrumbLabel.label, "{ category: ui.lifecycle, parentViewController: UINavigationController, beingPresented: false, window_isKeyWindow: true, is_window_rootViewController: false }") -// } -// -// func testLoremIpsum() { -// app.buttons["loremIpsumButton"].tap() -// app.textViews.firstMatch.waitForExistence("Lorem Ipsum not loaded.") -// } -// -// func testNavigationTransaction() { -// app.buttons["testNavigationTransactionButton"].tap() -// app.images.firstMatch.waitForExistence("Navigation transaction not loaded.") -// assertApp() -// } -// -// func testShowNib() { -// app.buttons["showNibButton"].tap() -// app.buttons["lonelyButton"].waitForExistence("Nib ViewController not loaded.") -// assertApp() -// } -// -// func testUiClickTransaction() { -// app.buttons["uiClickTransactionButton"].tap() -// } -// -// func testCaptureError() { -// app.buttons["Error"].tap() -// } -// -// func testCaptureException() { -// app.buttons["NSException"].tap() -// } -// -// func testShowTableView() { -// app.buttons["showTableViewButton"].tap() -// app.navigationBars.buttons.element(boundBy: 0).waitForExistence("TableView not loaded.") -// assertApp() -// } -// -// func testSplitView() { -// app.buttons["showSplitViewButton"].tap() -// -// let app = XCUIApplication() -// app.navigationBars["iOS_Swift.SecondarySplitView"].buttons["Root ViewController"].waitForExistence("SplitView not loaded.") -// -// // This validation is currently not working on iOS 10. -// if #available(iOS 11.0, *) { -// assertApp() -// } -// } -// -// private func waitForExistenseOfMainScreen() { -// app.buttons["captureMessageButton"].waitForExistence( "Home Screen doesn't exist.") -// } -// -// private func checkSlowAndFrozenFrames() { -// let frameStatsLabel = app.staticTexts["framesStatsLabel"] -// frameStatsLabel.waitForExistence("Frame statistics message not found.") -// -// let frameStatsAsStringArray = frameStatsLabel.label.components(separatedBy: CharacterSet.decimalDigits.inverted) -// let frameStats = frameStatsAsStringArray.filter { $0 != "" }.map { Int($0) } -// -// XCTAssertEqual(3, frameStats.count) -// guard let totalFrames = frameStats[0] else { XCTFail("No total frames found."); return } -// guard let slowFrames = frameStats[1] else { XCTFail("No slow frames found."); return } -// guard let frozenFrames = frameStats[1] else { XCTFail("No frozen frames found."); return } -// -// let slowFramesPercentage = Double(slowFrames) / Double(totalFrames) -// XCTAssertTrue(0.5 > slowFramesPercentage, "Too many slow frames.") -// -// let frozenFramesPercentage = Double(frozenFrames) / Double(totalFrames) -// XCTAssertTrue(0.5 > frozenFramesPercentage, "Too many frozen frames.") -// } -// -// private func assertApp() { -// let confirmation = app.staticTexts["ASSERT_MESSAGE"] -// let errorMessage = app.staticTexts["ASSERT_ERROR"] -// confirmation.waitForExistence("Assertion Message Not Found") -// -// XCTAssertTrue(confirmation.label == "ASSERT: SUCCESS", errorMessage.label) -// } -// -//} -// -//extension XCUIElement { -// -// func waitForExistence(_ message: String) { -// XCTAssertTrue(self.waitForExistence(timeout: TimeInterval(10)), message) -// } -//} +import XCTest + +class LaunchUITests: XCTestCase { + + private let app: XCUIApplication = XCUIApplication() + + override func setUp() { + super.setUp() + + continueAfterFailure = false + XCUIDevice.shared.orientation = .portrait + app.launch() + + waitForExistenseOfMainScreen() + checkSlowAndFrozenFrames() + } + + override func tearDown() { + app.terminate() + super.tearDown() + } + + func testBreadcrumbData() { + let breadcrumbLabel = app.staticTexts["breadcrumbLabel"] + breadcrumbLabel.waitForExistence("Breadcrumb label not found.") + XCTAssertEqual(breadcrumbLabel.label, "{ category: ui.lifecycle, parentViewController: UINavigationController, beingPresented: false, window_isKeyWindow: true, is_window_rootViewController: false }") + } + + func testLoremIpsum() { + app.buttons["loremIpsumButton"].tap() + app.textViews.firstMatch.waitForExistence("Lorem Ipsum not loaded.") + } + + func testNavigationTransaction() { + app.buttons["testNavigationTransactionButton"].tap() + app.images.firstMatch.waitForExistence("Navigation transaction not loaded.") + assertApp() + } + + func testShowNib() { + app.buttons["showNibButton"].tap() + app.buttons["lonelyButton"].waitForExistence("Nib ViewController not loaded.") + assertApp() + } + + func testUiClickTransaction() { + app.buttons["uiClickTransactionButton"].tap() + } + + func testCaptureError() { + app.buttons["Error"].tap() + } + + func testCaptureException() { + app.buttons["NSException"].tap() + } + + func testShowTableView() { + app.buttons["showTableViewButton"].tap() + app.navigationBars.buttons.element(boundBy: 0).waitForExistence("TableView not loaded.") + assertApp() + } + + func testSplitView() { + app.buttons["showSplitViewButton"].tap() + + let app = XCUIApplication() + app.navigationBars["iOS_Swift.SecondarySplitView"].buttons["Root ViewController"].waitForExistence("SplitView not loaded.") + + // This validation is currently not working on iOS 10. + if #available(iOS 11.0, *) { + assertApp() + } + } + + private func waitForExistenseOfMainScreen() { + app.buttons["captureMessageButton"].waitForExistence( "Home Screen doesn't exist.") + } + + private func checkSlowAndFrozenFrames() { + let frameStatsLabel = app.staticTexts["framesStatsLabel"] + frameStatsLabel.waitForExistence("Frame statistics message not found.") + + let frameStatsAsStringArray = frameStatsLabel.label.components(separatedBy: CharacterSet.decimalDigits.inverted) + let frameStats = frameStatsAsStringArray.filter { $0 != "" }.map { Int($0) } + + XCTAssertEqual(3, frameStats.count) + guard let totalFrames = frameStats[0] else { XCTFail("No total frames found."); return } + guard let slowFrames = frameStats[1] else { XCTFail("No slow frames found."); return } + guard let frozenFrames = frameStats[1] else { XCTFail("No frozen frames found."); return } + + let slowFramesPercentage = Double(slowFrames) / Double(totalFrames) + XCTAssertTrue(0.5 > slowFramesPercentage, "Too many slow frames.") + + let frozenFramesPercentage = Double(frozenFrames) / Double(totalFrames) + XCTAssertTrue(0.5 > frozenFramesPercentage, "Too many frozen frames.") + } + + private func assertApp() { + let confirmation = app.staticTexts["ASSERT_MESSAGE"] + let errorMessage = app.staticTexts["ASSERT_ERROR"] + confirmation.waitForExistence("Assertion Message Not Found") + + XCTAssertTrue(confirmation.label == "ASSERT: SUCCESS", errorMessage.label) + } + +} + +extension XCUIElement { + + func waitForExistence(_ message: String) { + XCTAssertTrue(self.waitForExistence(timeout: TimeInterval(10)), message) + } +} diff --git a/Sources/Sentry/SentryBacktrace.cpp b/Sources/Sentry/SentryBacktrace.cpp index 6c791bc42d9..401285c7d49 100644 --- a/Sources/Sentry/SentryBacktrace.cpp +++ b/Sources/Sentry/SentryBacktrace.cpp @@ -108,22 +108,31 @@ namespace profiling { const auto pair = ThreadHandle::allExcludingCurrent(); for (const auto &thread : pair.first) { Backtrace bt; + // This one is probably safe to call while the thread is suspended, but + // being conservative here in case the platform time functions take any + // locks that we're not aware of. + bt.absoluteTimestamp = getAbsoluteTime(); + + // Log an empty stack for an idle thread, we don't need to walk the stack. + if (thread->isIdle()) { + bt.threadMetadata.threadID = thread->tid(); + bt.threadMetadata.priority = -1; + f(bt); + continue; + } + auto metadata = cache->metadataForThread(*thread); if (metadata.threadID == 0) { continue; } else { bt.threadMetadata = std::move(metadata); } + // This function calls `pthread_from_mach_thread_np`, which takes a lock, // so we must read the value before suspending the thread to avoid risking // a deadlock. See the comment below. const auto stackBounds = thread->stackBounds(); - // This one is probably safe to call while the thread is suspended, but - // being conservative here in case the platform time functions take any - // locks that we're not aware of. - bt.absoluteTimestamp = getAbsoluteTime(); - // ############################################ // DEADLOCK WARNING: It is not safe to call any functions that acquire a // lock between here and `thread->resume()` -- this may cause a deadlock. diff --git a/Tests/SentryTests/SentryHubTests.swift b/Tests/SentryTests/SentryHubTests.swift index 7f8355438f8..722384996e3 100644 --- a/Tests/SentryTests/SentryHubTests.swift +++ b/Tests/SentryTests/SentryHubTests.swift @@ -870,7 +870,7 @@ class SentryHubTests: XCTestCase { let queueMetadata = sampledProfile["queue_metadata"] as! [String: Any] XCTAssertFalse(threadMetadata.isEmpty) - XCTAssertGreaterThan(threadMetadata.first?.value["priority"] as! Int, 0) + XCTAssertFalse(threadMetadata.values.compactMap { $0["priority"] }.filter { ($0 as! Int) > 0 }.isEmpty) XCTAssertFalse(threadMetadata.values.filter { $0["is_main_thread"] as? Bool == true }.isEmpty) XCTAssertFalse(queueMetadata.isEmpty) XCTAssertFalse(((queueMetadata.first?.value as! [String: Any])["label"] as! String).isEmpty) From c60ef389b6025db3e15310d29662e6fcf76feb5a Mon Sep 17 00:00:00 2001 From: Indragie Karunaratne Date: Thu, 28 Jul 2022 11:01:23 -0700 Subject: [PATCH 15/16] fix: Bad merge on CHANGELOG.md (#2020) --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index af10240417b..904c5195d36 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,26 +5,26 @@ ### Features - Add sampling configuration for profiling (#2004) +- Add transaction to baggage and trace headers (#1992) ### Fixes - Log empty samples instead of collecting stacks for idle threads (#2013) - Remove logging that could occur while a thread is suspended (#2014) - Handle failure to read thread priority gracefully (#2015) +- Fix address sanitizer compilation error (#1996) ## 7.22.0 ### Features - Read free_memory when the event is captured, not only at SDK startup (#1962) -- Add transaction to baggage and trace headers (#1992) - Provide private access to SentryOptions for hybrid SDKs (#1991) ### Fixes - Remove Sentry keys from cached HTTP request headers (#1975) - Collect samples for idle threads in iOS profiler (#1978) -- Fix address sanitizer compilation error (#1996) - Don't override already-set timestamp when finishing Span (#1993) - Respect existing baggage header instead of overwriting it (#1995) From 5bc31fd9bb476539387de7c6ff32acfaf2407157 Mon Sep 17 00:00:00 2001 From: getsentry-bot Date: Thu, 28 Jul 2022 18:02:12 +0000 Subject: [PATCH 16/16] release: 7.23.0 --- CHANGELOG.md | 2 +- Samples/iOS-Swift/iOS-Swift.xcodeproj/project.pbxproj | 8 ++++---- Sentry.podspec | 2 +- Sources/Configuration/Sentry.xcconfig | 2 +- Sources/Sentry/SentryMeta.m | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 904c5195d36..c6813be58dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## Unreleased +## 7.23.0 ### Features diff --git a/Samples/iOS-Swift/iOS-Swift.xcodeproj/project.pbxproj b/Samples/iOS-Swift/iOS-Swift.xcodeproj/project.pbxproj index 8712acdd822..e12e64d52eb 100644 --- a/Samples/iOS-Swift/iOS-Swift.xcodeproj/project.pbxproj +++ b/Samples/iOS-Swift/iOS-Swift.xcodeproj/project.pbxproj @@ -1074,7 +1074,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 7.22.0; + MARKETING_VERSION = 7.23.0; PRODUCT_BUNDLE_IDENTIFIER = "io.sentry.sample.iOS-Swift"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = "match Development io.sentry.sample.iOS-Swift"; @@ -1103,7 +1103,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 7.22.0; + MARKETING_VERSION = 7.23.0; PRODUCT_BUNDLE_IDENTIFIER = "io.sentry.sample.iOS-Swift"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = "match AppStore io.sentry.sample.iOS-Swift"; @@ -1305,7 +1305,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 7.22.0; + MARKETING_VERSION = 7.23.0; PRODUCT_BUNDLE_IDENTIFIER = "io.sentry.sample.iOS-Swift.Clip"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = "match Development io.sentry.sample.iOS-Swift.Clip"; @@ -1341,7 +1341,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 7.22.0; + MARKETING_VERSION = 7.23.0; PRODUCT_BUNDLE_IDENTIFIER = "io.sentry.sample.iOS-Swift.Clip"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = "match AppStore io.sentry.sample.iOS-Swift.Clip"; diff --git a/Sentry.podspec b/Sentry.podspec index 1266c742743..4f3502479c7 100644 --- a/Sentry.podspec +++ b/Sentry.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "Sentry" - s.version = "7.22.0" + s.version = "7.23.0" s.summary = "Sentry client for cocoa" s.homepage = "https://github.com/getsentry/sentry-cocoa" s.license = "mit" diff --git a/Sources/Configuration/Sentry.xcconfig b/Sources/Configuration/Sentry.xcconfig index edd042fa0c4..d6e4309a483 100644 --- a/Sources/Configuration/Sentry.xcconfig +++ b/Sources/Configuration/Sentry.xcconfig @@ -29,7 +29,7 @@ MACH_O_TYPE = mh_dylib FRAMEWORK_VERSION = A PRODUCT_NAME = Sentry -CURRENT_PROJECT_VERSION = 7.22.0 +CURRENT_PROJECT_VERSION = 7.23.0 INFOPLIST_FILE = Sources/Sentry/Info.plist PRODUCT_BUNDLE_IDENTIFIER = io.sentry.Sentry ALWAYS_SEARCH_USER_PATHS = NO diff --git a/Sources/Sentry/SentryMeta.m b/Sources/Sentry/SentryMeta.m index 284e06dda83..0cc4e0abf02 100644 --- a/Sources/Sentry/SentryMeta.m +++ b/Sources/Sentry/SentryMeta.m @@ -5,7 +5,7 @@ @implementation SentryMeta // Don't remove the static keyword. If you do the compiler adds the constant name to the global // symbol table and it might clash with other constants. When keeping the static keyword the // compiler replaces all occurrences with the value. -static NSString *versionString = @"7.22.0"; +static NSString *versionString = @"7.23.0"; static NSString *sdkName = @"sentry.cocoa"; + (NSString *)versionString