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