From 11d3565d4cfceadfca574c182e4723372b7abd97 Mon Sep 17 00:00:00 2001 From: Philipp Hofmann Date: Thu, 5 Jan 2023 12:31:20 +0100 Subject: [PATCH] fix: Readd initWithDict for Hybrid SDKs Add SentryOptions:initWithDict only for Hybrid SDKs again. Fixes GH-2547 --- Sources/Sentry/Public/SentryOptions.h | 3 - Sources/Sentry/SentryOptions.m | 236 +++++- .../HybridPublic/PrivateSentrySDKOnly.h | 12 + Tests/SentryTests/Networking/SentryDsnTests.m | 17 +- .../SentryTransportInitializerTests.swift | 2 +- Tests/SentryTests/SentryClientTests.swift | 4 +- Tests/SentryTests/SentryOptionsTest.m | 791 +++++++++++++++++- Tests/SentryTests/SentryTests.m | 7 +- 8 files changed, 1047 insertions(+), 25 deletions(-) diff --git a/Sources/Sentry/Public/SentryOptions.h b/Sources/Sentry/Public/SentryOptions.h index 6e46ae8620c..1875d8322bc 100644 --- a/Sources/Sentry/Public/SentryOptions.h +++ b/Sources/Sentry/Public/SentryOptions.h @@ -8,9 +8,6 @@ NS_ASSUME_NONNULL_BEGIN NS_SWIFT_NAME(Options) @interface SentryOptions : NSObject -- (_Nullable instancetype)initWithDsn:(NSString *)dsn - didFailWithError:(NSError *_Nullable *_Nullable)error; - /** * The DSN tells the SDK where to send the events to. If this value is not provided, the SDK will * not send any events. diff --git a/Sources/Sentry/SentryOptions.m b/Sources/Sentry/SentryOptions.m index a2066b4fc52..e15a19f1119 100644 --- a/Sources/Sentry/SentryOptions.m +++ b/Sources/Sentry/SentryOptions.m @@ -155,14 +155,14 @@ - (instancetype)init return self; } -- (_Nullable instancetype)initWithDsn:(NSString *)dsn - didFailWithError:(NSError *_Nullable *_Nullable)error +- (_Nullable instancetype)initWithDict:(NSDictionary *)options + didFailWithError:(NSError *_Nullable *_Nullable)error { if (self = [self init]) { - self.parsedDsn = [[SentryDsn alloc] initWithString:dsn didFailWithError:error]; - self.dsn = dsn; - - if (error != nil && *error != nil) { + if (![self validateOptions:options didFailWithError:error]) { + [SentryLog + logWithMessage:[NSString stringWithFormat:@"Failed to initialize: %@", *error] + andLevel:kSentryLevelError]; return nil; } } @@ -215,6 +215,230 @@ - (void)setDsn:(NSString *)dsn } } +/** + * Populates all `SentryOptions` values from `options` dict using fallbacks/defaults if needed. + */ +- (BOOL)validateOptions:(NSDictionary *)options + didFailWithError:(NSError *_Nullable *_Nullable)error +{ + NSPredicate *isNSString = [NSPredicate predicateWithBlock:^BOOL( + id object, NSDictionary *bindings) { return [object isKindOfClass:[NSString class]]; }]; + + [self setBool:options[@"debug"] block:^(BOOL value) { self->_debug = value; }]; + + if ([options[@"diagnosticLevel"] isKindOfClass:[NSString class]]) { + for (SentryLevel level = 0; level <= kSentryLevelFatal; level++) { + if ([nameForSentryLevel(level) isEqualToString:options[@"diagnosticLevel"]]) { + self.diagnosticLevel = level; + break; + } + } + } + + NSString *dsn = @""; + if (nil != options[@"dsn"] && [options[@"dsn"] isKindOfClass:[NSString class]]) { + dsn = options[@"dsn"]; + } + + self.parsedDsn = [[SentryDsn alloc] initWithString:dsn didFailWithError:error]; + + if ([options[@"release"] isKindOfClass:[NSString class]]) { + self.releaseName = options[@"release"]; + } + + if ([options[@"environment"] isKindOfClass:[NSString class]]) { + self.environment = options[@"environment"]; + } + + if ([options[@"dist"] isKindOfClass:[NSString class]]) { + self.dist = options[@"dist"]; + } + + [self setBool:options[@"enabled"] block:^(BOOL value) { self->_enabled = value; }]; + + if ([options[@"shutdownTimeInterval"] isKindOfClass:[NSNumber class]]) { + self.shutdownTimeInterval = [options[@"shutdownTimeInterval"] doubleValue]; + } + + [self setBool:options[@"enableCrashHandler"] + block:^(BOOL value) { self->_enableCrashHandler = value; }]; + + if ([options[@"maxBreadcrumbs"] isKindOfClass:[NSNumber class]]) { + self.maxBreadcrumbs = [options[@"maxBreadcrumbs"] unsignedIntValue]; + } + + [self setBool:options[@"enableNetworkBreadcrumbs"] + block:^(BOOL value) { self->_enableNetworkBreadcrumbs = value; }]; + + if ([options[@"maxCacheItems"] isKindOfClass:[NSNumber class]]) { + self.maxCacheItems = [options[@"maxCacheItems"] unsignedIntValue]; + } + + if ([self isBlock:options[@"beforeSend"]]) { + self.beforeSend = options[@"beforeSend"]; + } + + if ([self isBlock:options[@"beforeBreadcrumb"]]) { + self.beforeBreadcrumb = options[@"beforeBreadcrumb"]; + } + + if ([self isBlock:options[@"onCrashedLastRun"]]) { + self.onCrashedLastRun = options[@"onCrashedLastRun"]; + } + + if ([options[@"integrations"] isKindOfClass:[NSArray class]]) { + self.integrations = [options[@"integrations"] filteredArrayUsingPredicate:isNSString]; + } + + if ([options[@"sampleRate"] isKindOfClass:[NSNumber class]]) { + self.sampleRate = options[@"sampleRate"]; + } + + [self setBool:options[@"enableAutoSessionTracking"] + block:^(BOOL value) { self->_enableAutoSessionTracking = value; }]; + + [self setBool:options[@"enableWatchdogTerminationTracking"] + block:^(BOOL value) { self->_enableWatchdogTerminationTracking = value; }]; + + if ([options[@"sessionTrackingIntervalMillis"] isKindOfClass:[NSNumber class]]) { + self.sessionTrackingIntervalMillis = + [options[@"sessionTrackingIntervalMillis"] unsignedIntValue]; + } + + [self setBool:options[@"attachStacktrace"] + block:^(BOOL value) { self->_attachStacktrace = value; }]; + + [self setBool:options[@"stitchAsyncCode"] + block:^(BOOL value) { self->_stitchAsyncCode = value; }]; + + if ([options[@"maxAttachmentSize"] isKindOfClass:[NSNumber class]]) { + self.maxAttachmentSize = [options[@"maxAttachmentSize"] unsignedIntValue]; + } + + [self setBool:options[@"sendDefaultPii"] + block:^(BOOL value) { self->_sendDefaultPii = value; }]; + + [self setBool:options[@"enableAutoPerformanceTracing"] + block:^(BOOL value) { self->_enableAutoPerformanceTracing = value; }]; + + [self setBool:options[@"enableCaptureFailedRequests"] + block:^(BOOL value) { self->_enableCaptureFailedRequests = value; }]; + +#if SENTRY_HAS_UIKIT + [self setBool:options[@"enableUIViewControllerTracing"] + block:^(BOOL value) { self->_enableUIViewControllerTracing = value; }]; + + [self setBool:options[@"attachScreenshot"] + block:^(BOOL value) { self->_attachScreenshot = value; }]; + + [self setBool:options[@"attachViewHierarchy"] + block:^(BOOL value) { self->_attachViewHierarchy = value; }]; + + [self setBool:options[@"enableUserInteractionTracing"] + block:^(BOOL value) { self->_enableUserInteractionTracing = value; }]; + + if ([options[@"idleTimeout"] isKindOfClass:[NSNumber class]]) { + self.idleTimeout = [options[@"idleTimeout"] doubleValue]; + } + + [self setBool:options[@"enablePreWarmedAppStartTracing"] + block:^(BOOL value) { self->_enablePreWarmedAppStartTracing = value; }]; +#endif + + [self setBool:options[@"enableAppHangTracking"] + block:^(BOOL value) { self->_enableAppHangTracking = value; }]; + + if ([options[@"appHangTimeoutInterval"] isKindOfClass:[NSNumber class]]) { + self.appHangTimeoutInterval = [options[@"appHangTimeoutInterval"] doubleValue]; + } + + [self setBool:options[@"enableNetworkTracking"] + block:^(BOOL value) { self->_enableNetworkTracking = value; }]; + + [self setBool:options[@"enableFileIOTracing"] + block:^(BOOL value) { self->_enableFileIOTracing = value; }]; + + if ([options[@"tracesSampleRate"] isKindOfClass:[NSNumber class]]) { + self.tracesSampleRate = options[@"tracesSampleRate"]; + } + + if ([self isBlock:options[@"tracesSampler"]]) { + self.tracesSampler = options[@"tracesSampler"]; + } + + if ([options[@"inAppIncludes"] isKindOfClass:[NSArray class]]) { + NSArray *inAppIncludes = + [options[@"inAppIncludes"] filteredArrayUsingPredicate:isNSString]; + _inAppIncludes = [_inAppIncludes arrayByAddingObjectsFromArray:inAppIncludes]; + } + + if ([options[@"inAppExcludes"] isKindOfClass:[NSArray class]]) { + _inAppExcludes = [options[@"inAppExcludes"] filteredArrayUsingPredicate:isNSString]; + } + + if ([options[@"urlSessionDelegate"] conformsToProtocol:@protocol(NSURLSessionDelegate)]) { + self.urlSessionDelegate = options[@"urlSessionDelegate"]; + } + + [self setBool:options[@"enableSwizzling"] + block:^(BOOL value) { self->_enableSwizzling = value; }]; + + [self setBool:options[@"enableCoreDataTracing"] + block:^(BOOL value) { self->_enableCoreDataTracing = 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 + + [self setBool:options[@"sendClientReports"] + block:^(BOOL value) { self->_sendClientReports = value; }]; + + [self setBool:options[@"enableAutoBreadcrumbTracking"] + block:^(BOOL value) { self->_enableAutoBreadcrumbTracking = value; }]; + + if ([options[@"tracePropagationTargets"] isKindOfClass:[NSArray class]]) { + self.tracePropagationTargets = options[@"tracePropagationTargets"]; + } + + if ([options[@"failedRequestStatusCodes"] isKindOfClass:[NSArray class]]) { + self.failedRequestStatusCodes = options[@"failedRequestStatusCodes"]; + } + + if ([options[@"failedRequestTargets"] isKindOfClass:[NSArray class]]) { + self.failedRequestTargets = options[@"failedRequestTargets"]; + } + +#if SENTRY_HAS_METRIC_KIT + if (@available(iOS 14.0, macOS 12.0, macCatalyst 14.0, *)) { + [self setBool:options[@"enableMetricKit"] + block:^(BOOL value) { self->_enableMetricKit = value; }]; + } +#endif + + if (nil != error && nil != *error) { + return NO; + } else { + return YES; + } +} + +- (void)setBool:(id)value block:(void (^)(BOOL))block +{ + // Entries in the dictionary can be NSNull. Especially, on React-Native, this can happen. + if (value != nil && ![value isEqual:[NSNull null]]) { + block([value boolValue]); + } +} + - (void)addInAppInclude:(NSString *)inAppInclude { _inAppIncludes = [self.inAppIncludes arrayByAddingObject:inAppInclude]; diff --git a/Sources/Sentry/include/HybridPublic/PrivateSentrySDKOnly.h b/Sources/Sentry/include/HybridPublic/PrivateSentrySDKOnly.h index c09e5d000a9..b028098e28a 100644 --- a/Sources/Sentry/include/HybridPublic/PrivateSentrySDKOnly.h +++ b/Sources/Sentry/include/HybridPublic/PrivateSentrySDKOnly.h @@ -1,6 +1,7 @@ #import "PrivatesHeader.h" #import "SentryAppStartMeasurement.h" #import "SentryEnvelopeItemType.h" +#import "SentryOptions.h" #import "SentryScreenFrames.h" @class SentryEnvelope, SentryDebugMeta, SentryAppStartMeasurement, SentryScreenFrames, @@ -99,4 +100,15 @@ typedef void (^SentryOnAppStartMeasurementAvailable)( @end +@interface +SentryOptions (HybridSDKs) + +/** + * Init SentryOptions with a dictionary. Needed by hybrid SDKs. + */ +- (_Nullable instancetype)initWithDict:(NSDictionary *)options + didFailWithError:(NSError *_Nullable *_Nullable)error; + +@end + NS_ASSUME_NONNULL_END diff --git a/Tests/SentryTests/Networking/SentryDsnTests.m b/Tests/SentryTests/Networking/SentryDsnTests.m index 0d6fed5e14b..43597954668 100644 --- a/Tests/SentryTests/Networking/SentryDsnTests.m +++ b/Tests/SentryTests/Networking/SentryDsnTests.m @@ -1,3 +1,4 @@ +#import "PrivateSentrySDKOnly.h" #import "SentryDsn.h" #import "SentryError.h" #import "SentryMeta.h" @@ -14,8 +15,8 @@ @implementation SentryDsnTests - (void)testMissingUsernamePassword { NSError *error = nil; - SentryOptions *options = [[SentryOptions alloc] initWithDsn:@"https://sentry.io" - didFailWithError:&error]; + SentryOptions *options = [[SentryOptions alloc] initWithDict:@{ @"dsn" : @"https://sentry.io" } + didFailWithError:&error]; XCTAssertEqual(kSentryErrorInvalidDsnError, error.code); XCTAssertNil(options); } @@ -62,8 +63,8 @@ - (void)testDsnHeaderUsername - (void)testMissingScheme { NSError *error = nil; - SentryOptions *options = [[SentryOptions alloc] initWithDsn:@"sentry.io" - didFailWithError:&error]; + SentryOptions *options = [[SentryOptions alloc] initWithDict:@{ @"dsn" : @"https://sentry.io" } + didFailWithError:&error]; XCTAssertEqual(kSentryErrorInvalidDsnError, error.code); XCTAssertNil(options); } @@ -71,8 +72,8 @@ - (void)testMissingScheme - (void)testMissingHost { NSError *error = nil; - SentryOptions *options = [[SentryOptions alloc] initWithDsn:@"http:///1" - didFailWithError:&error]; + SentryOptions *options = [[SentryOptions alloc] initWithDict:@{ @"dsn" : @"http:///1" } + didFailWithError:&error]; XCTAssertEqual(kSentryErrorInvalidDsnError, error.code); XCTAssertNil(options); } @@ -80,8 +81,8 @@ - (void)testMissingHost - (void)testUnsupportedProtocol { NSError *error = nil; - SentryOptions *options = [[SentryOptions alloc] initWithDsn:@"ftp://sentry.io/1" - didFailWithError:&error]; + SentryOptions *options = [[SentryOptions alloc] initWithDict:@{ @"dsn" : @"ftp://sentry.io/1" } + didFailWithError:&error]; XCTAssertEqual(kSentryErrorInvalidDsnError, error.code); XCTAssertNil(options); } diff --git a/Tests/SentryTests/Networking/SentryTransportInitializerTests.swift b/Tests/SentryTests/Networking/SentryTransportInitializerTests.swift index fc9ce5159b7..44a27ff3a03 100644 --- a/Tests/SentryTests/Networking/SentryTransportInitializerTests.swift +++ b/Tests/SentryTests/Networking/SentryTransportInitializerTests.swift @@ -16,7 +16,7 @@ class SentryTransportInitializerTests: XCTestCase { } func testDefault() throws { - let options = try Options(dsn: SentryTransportInitializerTests.dsnAsString) + let options = try Options(dict: ["dsn": SentryTransportInitializerTests.dsnAsString]) let result = TransportInitializer.initTransport(options, sentryFileManager: fileManager) diff --git a/Tests/SentryTests/SentryClientTests.swift b/Tests/SentryTests/SentryClientTests.swift index 81847615fb2..3a7506ac416 100644 --- a/Tests/SentryTests/SentryClientTests.swift +++ b/Tests/SentryTests/SentryClientTests.swift @@ -64,7 +64,9 @@ class SentryClientTest: XCTestCase { func getSut(configureOptions: (Options) -> Void = { _ in }) -> SentryClient { var client: SentryClient! do { - let options = try Options(dsn: SentryClientTest.dsn) + let options = try Options(dict: [ + "dsn": SentryClientTest.dsn + ]) configureOptions(options) client = SentryClient( diff --git a/Tests/SentryTests/SentryOptionsTest.m b/Tests/SentryTests/SentryOptionsTest.m index e4d80a98e3c..4707be10092 100644 --- a/Tests/SentryTests/SentryOptionsTest.m +++ b/Tests/SentryTests/SentryOptionsTest.m @@ -1,4 +1,5 @@ #import "SentryOptions.h" +#import "PrivateSentrySDKOnly.h" #import "SentryError.h" #import "SentrySDK.h" #import "SentryTests-Swift.h" @@ -13,22 +14,381 @@ @implementation SentryOptionsTest - (void)testEmptyDsn { NSError *error = nil; - SentryOptions *options = [[SentryOptions alloc] initWithDsn:@"" didFailWithError:&error]; + SentryOptions *options = [[SentryOptions alloc] initWithDict:@{} didFailWithError:&error]; XCTAssertNil(options.parsedDsn); XCTAssertEqual(NO, options.debug); XCTAssertEqual(kSentryErrorInvalidDsnError, error.code); + + [self assertDsnNil:options andError:error]; +} + +- (void)testInvalidDsnBoolean +{ + NSError *error = nil; + SentryOptions *options = [[SentryOptions alloc] initWithDict:@{ @"dsn" : @YES } + didFailWithError:&error]; + + [self assertDsnNil:options andError:error]; +} + +- (void)assertDsnNil:(SentryOptions *)options andError:(NSError *)error +{ + XCTAssertNil(options.parsedDsn); + XCTAssertEqual(NO, options.debug); + XCTAssertEqual(kSentryErrorInvalidDsnError, error.code); } - (void)testInvalidDsn { NSError *error = nil; - SentryOptions *options = [[SentryOptions alloc] initWithDsn:@"https://sentry.io" - didFailWithError:&error]; + SentryOptions *options = [[SentryOptions alloc] initWithDict:@{ @"dsn" : @"https://sentry.io" } + didFailWithError:&error]; XCTAssertEqual(kSentryErrorInvalidDsnError, error.code); XCTAssertNil(options); } +- (void)testRelease +{ + SentryOptions *options = [self getValidOptions:@{ @"release" : @"abc" }]; + XCTAssertEqualObjects(options.releaseName, @"abc"); +} + +- (void)testSetEmptyRelease +{ + SentryOptions *options = [self getValidOptions:@{ @"release" : @"" }]; + XCTAssertEqualObjects(options.releaseName, @""); +} + +- (void)testSetReleaseToNonString +{ + SentryOptions *options = [self getValidOptions:@{ @"release" : @2 }]; + XCTAssertEqualObjects(options.releaseName, [self buildDefaultReleaseName]); +} + +- (void)testNoReleaseSetUsesDefault +{ + SentryOptions *options = [self getValidOptions:@{}]; + XCTAssertEqualObjects(options.releaseName, [self buildDefaultReleaseName]); +} + +- (NSString *)buildDefaultReleaseName +{ + NSDictionary *infoDict = [[NSBundle mainBundle] infoDictionary]; + return [NSString stringWithFormat:@"%@@%@+%@", infoDict[@"CFBundleIdentifier"], + infoDict[@"CFBundleShortVersionString"], infoDict[@"CFBundleVersion"]]; +} + +- (void)testEnvironment +{ + SentryOptions *options = [self getValidOptions:@{}]; + XCTAssertEqual(options.environment, kSentryDefaultEnvironment); + + options = [self getValidOptions:@{ @"environment" : @"xxx" }]; + XCTAssertEqualObjects(options.environment, @"xxx"); +} + +- (void)testDist +{ + SentryOptions *options = [self getValidOptions:@{}]; + XCTAssertNil(options.dist); + + options = [self getValidOptions:@{ @"dist" : @"hhh" }]; + XCTAssertEqualObjects(options.dist, @"hhh"); +} + +- (void)testValidDebug +{ + [self testDebugWith:@YES expected:YES]; + [self testDebugWith:@"YES" expected:YES]; + [self testDebugWith:@(YES) expected:YES]; +} + +- (void)testInvalidDebug +{ + [self testDebugWith:@"Invalid" expected:NO]; + [self testDebugWith:@NO expected:NO]; + [self testDebugWith:@(NO) expected:NO]; +} + +- (void)testDebugWith:(NSObject *)debugValue expected:(BOOL)expectedDebugValue +{ + NSError *error = nil; + SentryOptions *options = [[SentryOptions alloc] initWithDict:@{ + @"dsn" : @"https://username:password@sentry.io/1", + @"debug" : debugValue + } + didFailWithError:&error]; + + XCTAssertNil(error); + XCTAssertEqual(expectedDebugValue, options.debug); +} + +- (void)testValidDiagnosticLevel +{ + [self testDiagnosticlevelWith:@"none" expected:kSentryLevelNone]; + [self testDiagnosticlevelWith:@"debug" expected:kSentryLevelDebug]; + [self testDiagnosticlevelWith:@"info" expected:kSentryLevelInfo]; + [self testDiagnosticlevelWith:@"warning" expected:kSentryLevelWarning]; + [self testDiagnosticlevelWith:@"error" expected:kSentryLevelError]; + [self testDiagnosticlevelWith:@"fatal" expected:kSentryLevelFatal]; +} + +- (void)testInvalidDiagnosticLevel +{ + [self testDiagnosticlevelWith:@"fatala" expected:kSentryLevelDebug]; + [self testDiagnosticlevelWith:@(YES) expected:kSentryLevelDebug]; +} + +- (void)testDiagnosticlevelWith:(NSObject *)level expected:(SentryLevel)expected +{ + SentryOptions *options = [self getValidOptions:@{ @"diagnosticLevel" : level }]; + + XCTAssertEqual(expected, options.diagnosticLevel); +} + +- (void)testValidEnabled +{ + [self testEnabledWith:@YES expected:YES]; + [self testEnabledWith:@"YES" expected:YES]; + [self testEnabledWith:@(YES) expected:YES]; +} + +- (void)testInvalidEnabled +{ + [self testEnabledWith:@"Invalid" expected:NO]; + [self testEnabledWith:@NO expected:NO]; + [self testEnabledWith:@(NO) expected:NO]; +} + +- (void)testEnabledWith:(NSObject *)enabledValue expected:(BOOL)expectedValue +{ + SentryOptions *options = [self getValidOptions:@{ @"enabled" : enabledValue }]; + + XCTAssertEqual(expectedValue, options.enabled); +} + +- (void)testMaxBreadcrumbs +{ + NSNumber *maxBreadcrumbs = @20; + + SentryOptions *options = [self getValidOptions:@{ @"maxBreadcrumbs" : maxBreadcrumbs }]; + + XCTAssertEqual([maxBreadcrumbs unsignedIntValue], options.maxBreadcrumbs); +} + +- (void)testEnableNetworkBreadcrumbs +{ + [self testBooleanField:@"enableNetworkBreadcrumbs"]; +} + +- (void)testEnableAutoBreadcrumbTracking +{ + [self testBooleanField:@"enableAutoBreadcrumbTracking"]; +} + +- (void)testEnableCoreDataTracking +{ + [self testBooleanField:@"enableCoreDataTracing" defaultValue:NO]; +} + +- (void)testSendClientReports +{ + [self testBooleanField:@"sendClientReports" defaultValue:YES]; +} + +- (void)testDefaultMaxBreadcrumbs +{ + SentryOptions *options = [self getValidOptions:@{}]; + + XCTAssertEqual([@100 unsignedIntValue], options.maxBreadcrumbs); +} + +- (void)testMaxBreadcrumbsGarbage +{ + SentryOptions *options = [self getValidOptions:@{ @"maxBreadcrumbs" : self }]; + + XCTAssertEqual(100, options.maxBreadcrumbs); +} + +- (void)testMaxCacheItems +{ + NSNumber *maxCacheItems = @20; + + SentryOptions *options = [self getValidOptions:@{ @"maxCacheItems" : maxCacheItems }]; + + XCTAssertEqual([maxCacheItems unsignedIntValue], options.maxCacheItems); +} + +- (void)testMaxCacheItemsGarbage +{ + SentryOptions *options = [self getValidOptions:@{ @"maxCacheItems" : self }]; + + XCTAssertEqual(30, options.maxCacheItems); +} + +- (void)testDefaultMaxCacheItems +{ + SentryOptions *options = [self getValidOptions:@{}]; + + XCTAssertEqual([@30 unsignedIntValue], options.maxCacheItems); +} + +- (void)testBeforeSend +{ + SentryBeforeSendEventCallback callback = ^(SentryEvent *event) { return event; }; + SentryOptions *options = [self getValidOptions:@{ @"beforeSend" : callback }]; + + XCTAssertEqual(callback, options.beforeSend); +} + +- (void)testDefaultBeforeSend +{ + SentryOptions *options = [self getValidOptions:@{}]; + + XCTAssertNil(options.beforeSend); +} + +- (void)testGarbageBeforeSend_ReturnsNil +{ + SentryOptions *options = [self getValidOptions:@{ @"beforeSend" : @"fault" }]; + + XCTAssertNil(options.beforeSend); +} + +- (void)testNSNullBeforeSend_ReturnsNil +{ + SentryOptions *options = [self getValidOptions:@{ @"beforeSend" : [NSNull null] }]; + + XCTAssertFalse([options.beforeSend isEqual:[NSNull null]]); +} + +- (void)testBeforeBreadcrumb +{ + SentryBeforeBreadcrumbCallback callback + = ^(SentryBreadcrumb *breadcrumb) { return breadcrumb; }; + SentryOptions *options = [self getValidOptions:@{ @"beforeBreadcrumb" : callback }]; + + XCTAssertEqual(callback, options.beforeBreadcrumb); +} + +- (void)testDefaultBeforeBreadcrumb +{ + SentryOptions *options = [self getValidOptions:@{}]; + + XCTAssertNil(options.beforeBreadcrumb); +} + +- (void)testTracePropagationTargets +{ + SentryOptions *options = + [self getValidOptions:@{ @"tracePropagationTargets" : @[ @"localhost" ] }]; + + XCTAssertEqual(options.tracePropagationTargets.count, 1); + XCTAssertEqual(options.tracePropagationTargets[0], @"localhost"); +} + +- (void)testTracePropagationTargetsInvalidInstanceDoesntCrash +{ + SentryOptions *options = [self getValidOptions:@{ @"tracePropagationTargets" : @[ @YES ] }]; + + XCTAssertEqual(options.tracePropagationTargets.count, 1); + XCTAssertEqual(options.tracePropagationTargets[0], @YES); +} + +- (void)testFailedRequestTargets +{ + SentryOptions *options = + [self getValidOptions:@{ @"failedRequestTargets" : @[ @"localhost" ] }]; + + XCTAssertEqual(options.failedRequestTargets.count, 1); + XCTAssertEqual(options.failedRequestTargets[0], @"localhost"); +} + +- (void)testFailedRequestTargetsInvalidInstanceDoesntCrash +{ + SentryOptions *options = [self getValidOptions:@{ @"failedRequestTargets" : @[ @YES ] }]; + + XCTAssertEqual(options.failedRequestTargets.count, 1); + XCTAssertEqual(options.failedRequestTargets[0], @YES); +} + +- (void)testEnableCaptureFailedRequests +{ + [self testBooleanField:@"enableCaptureFailedRequests" defaultValue:YES]; +} + +- (void)testFailedRequestStatusCodes +{ + SentryHttpStatusCodeRange *httpStatusCodeRange = + [[SentryHttpStatusCodeRange alloc] initWithMin:400 max:599]; + SentryOptions *options = + [self getValidOptions:@{ @"failedRequestStatusCodes" : @[ httpStatusCodeRange ] }]; + + XCTAssertEqual(options.failedRequestStatusCodes.count, 1); + XCTAssertEqual(options.failedRequestStatusCodes[0].min, 400); + XCTAssertEqual(options.failedRequestStatusCodes[0].max, 599); +} + +- (void)testGarbageBeforeBreadcrumb_ReturnsNil +{ + SentryOptions *options = [self getValidOptions:@{ @"beforeBreadcrumb" : @"fault" }]; + + XCTAssertEqual(nil, options.beforeBreadcrumb); +} + +- (void)testOnCrashedLastRun +{ + __block BOOL onCrashedLastRunCalled = NO; + SentryOnCrashedLastRunCallback callback = ^(SentryEvent *event) { + onCrashedLastRunCalled = YES; + XCTAssertNotNil(event); + }; + SentryOptions *options = [self getValidOptions:@{ @"onCrashedLastRun" : callback }]; + + options.onCrashedLastRun([[SentryEvent alloc] init]); + + XCTAssertEqual(callback, options.onCrashedLastRun); + XCTAssertTrue(onCrashedLastRunCalled); +} + +- (void)testDefaultOnCrashedLastRun +{ + SentryOptions *options = [self getValidOptions:@{}]; + + XCTAssertNil(options.onCrashedLastRun); +} + +- (void)testGarbageOnCrashedLastRun_ReturnsNil +{ + SentryOptions *options = [self getValidOptions:@{ @"onCrashedLastRun" : @"fault" }]; + + XCTAssertNil(options.onCrashedLastRun); +} + +- (void)testIntegrations +{ + NSArray *integrations = @[ @"integration1", @"integration2" ]; + SentryOptions *options = [self getValidOptions:@{ @"integrations" : integrations }]; + + [self assertArrayEquals:integrations actual:options.integrations]; +} + +- (void)testDefaultIntegrations +{ + SentryOptions *options = [self getValidOptions:@{}]; + + XCTAssertTrue([[SentryOptions defaultIntegrations] isEqualToArray:options.integrations], + @"Default integrations are not set correctly"); +} + +- (void)testSampleRateWithDict +{ + NSNumber *sampleRate = @0.1; + SentryOptions *options = [self getValidOptions:@{ @"sampleRate" : sampleRate }]; + XCTAssertEqual(sampleRate, options.sampleRate); +} + - (void)testSampleRate_SetToNil { SentryOptions *options = [[SentryOptions alloc] init]; @@ -68,6 +428,54 @@ - (void)testSampleRateUpperBound XCTAssertEqual(@1, options.sampleRate); } +- (void)testSampleRateNotSet +{ + SentryOptions *options = [self getValidOptions:@{}]; + + XCTAssertEqual(@1, options.sampleRate); +} + +- (void)testEnableAutoSessionTracking +{ + [self testBooleanField:@"enableAutoSessionTracking"]; +} + +- (void)testEnableWatchdogTerminationTracking +{ + [self testBooleanField:@"enableWatchdogTerminationTracking"]; +} + +- (void)testSessionTrackingIntervalMillis +{ + NSNumber *sessionTracking = @2000; + SentryOptions *options = + [self getValidOptions:@{ @"sessionTrackingIntervalMillis" : sessionTracking }]; + + XCTAssertEqual([sessionTracking unsignedIntValue], options.sessionTrackingIntervalMillis); +} + +- (void)testDefaultSessionTrackingIntervalMillis +{ + SentryOptions *options = [self getValidOptions:@{}]; + + XCTAssertEqual([@30000 unsignedIntValue], options.sessionTrackingIntervalMillis); +} + +- (void)testAttachStackTrace +{ + [self testBooleanField:@"attachStacktrace"]; +} + +- (void)testStitchAsyncCodeDisabledPerDefault +{ + [self testBooleanField:@"stitchAsyncCode" defaultValue:NO]; +} + +- (void)testEnableIOTracking +{ + [self testBooleanField:@"enableFileIOTracing" defaultValue:YES]; +} + - (void)testEmptyConstructorSetsDefaultValues { SentryOptions *options = [[SentryOptions alloc] init]; @@ -75,6 +483,57 @@ - (void)testEmptyConstructorSetsDefaultValues [self assertDefaultValues:options]; } +- (void)testNSNull_SetsDefaultValue +{ + SentryOptions *options = [[SentryOptions alloc] initWithDict:@{ + @"dsn" : [NSNull null], + @"enabled" : [NSNull null], + @"debug" : [NSNull null], + @"diagnosticLevel" : [NSNull null], + @"release" : [NSNull null], + @"environment" : [NSNull null], + @"dist" : [NSNull null], + @"maxBreadcrumbs" : [NSNull null], + @"enableNetworkBreadcrumbs" : [NSNull null], + @"maxCacheItems" : [NSNull null], + @"beforeSend" : [NSNull null], + @"beforeBreadcrumb" : [NSNull null], + @"onCrashedLastRun" : [NSNull null], + @"integrations" : [NSNull null], + @"sampleRate" : [NSNull null], + @"enableAutoSessionTracking" : [NSNull null], + @"enableOutOfMemoryTracking" : [NSNull null], + @"sessionTrackingIntervalMillis" : [NSNull null], + @"attachStacktrace" : [NSNull null], + @"stitchAsyncCode" : [NSNull null], + @"maxAttachmentSize" : [NSNull null], + @"sendDefaultPii" : [NSNull null], + @"enableAutoPerformanceTracing" : [NSNull null], +#if SENTRY_HAS_UIKIT + @"enableUIViewControllerTracing" : [NSNull null], + @"attachScreenshot" : [NSNull null], +#endif + @"enableAppHangTracking" : [NSNull null], + @"appHangTimeoutInterval" : [NSNull null], + @"enableNetworkTracking" : [NSNull null], + @"enableAutoBreadcrumbTracking" : [NSNull null], + @"tracesSampleRate" : [NSNull null], + @"tracesSampler" : [NSNull null], + @"inAppIncludes" : [NSNull null], + @"inAppExcludes" : [NSNull null], + @"urlSessionDelegate" : [NSNull null], + @"enableSwizzling" : [NSNull null], + @"enableIOTracking" : [NSNull null], + @"sdk" : [NSNull null], + @"enableCaptureFailedRequests" : [NSNull null], + @"failedRequestStatusCodes" : [NSNull null], + } + didFailWithError:nil]; + + XCTAssertNotNil(options.parsedDsn); + [self assertDefaultValues:options]; +} + - (void)assertDefaultValues:(SentryOptions *)options { XCTAssertEqual(YES, options.enabled); @@ -203,6 +662,121 @@ - (void)testInvalidDsnViaEnvironment } #endif +- (void)testMaxAttachmentSize +{ + NSNumber *maxAttachmentSize = @21; + SentryOptions *options = [self getValidOptions:@{ @"maxAttachmentSize" : maxAttachmentSize }]; + + XCTAssertEqual([maxAttachmentSize unsignedIntValue], options.maxAttachmentSize); +} + +- (void)testDefaultMaxAttachmentSize +{ + SentryOptions *options = [self getValidOptions:@{}]; + + XCTAssertEqual(20 * 1024 * 1024, options.maxAttachmentSize); +} + +- (void)testSendDefaultPii +{ + [self testBooleanField:@"sendDefaultPii" defaultValue:NO]; +} + +- (void)testEnableAutoPerformanceTracing +{ + [self testBooleanField:@"enableAutoPerformanceTracing"]; +} + +#if SENTRY_HAS_UIKIT +- (void)testEnableUIViewControllerTracing +{ + [self testBooleanField:@"enableUIViewControllerTracing"]; +} + +- (void)testAttachScreenshot +{ + [self testBooleanField:@"attachScreenshot" defaultValue:NO]; +} + +- (void)testEnableUserInteractionTracing +{ + [self testBooleanField:@"enableUserInteractionTracing" defaultValue:YES]; +} + +- (void)testEnableFileIOTracing +{ + [self testBooleanField:@"enableFileIOTracing" defaultValue:YES]; +} + +# if SENTRY_HAS_METRIC_KIT + +- (void)testEnableMetricKit +{ + if (@available(iOS 14.0, macOS 12.0, macCatalyst 14.0, *)) { + [self testBooleanField:@"enableMetricKit" defaultValue:NO]; + } +} +# endif + +- (void)testShutdownTimeInterval +{ + NSNumber *shutdownTimeInterval = @2.1; + SentryOptions *options = + [self getValidOptions:@{ @"shutdownTimeInterval" : shutdownTimeInterval }]; + + XCTAssertEqual([shutdownTimeInterval doubleValue], options.shutdownTimeInterval); +} + +- (void)testIdleTimeout +{ + NSNumber *idleTimeout = @2.1; + SentryOptions *options = [self getValidOptions:@{ @"idleTimeout" : idleTimeout }]; + + XCTAssertEqual([idleTimeout doubleValue], options.idleTimeout); +} + +- (void)testEnablePreWarmedAppStartTracking +{ + [self testBooleanField:@"enablePreWarmedAppStartTracing" defaultValue:NO]; +} + +#endif + +- (void)testEnableAppHangTracking +{ + [self testBooleanField:@"enableAppHangTracking" defaultValue:NO]; +} + +- (void)testDefaultAppHangsTimeout +{ + SentryOptions *options = [self getValidOptions:@{}]; + XCTAssertEqual(2, options.appHangTimeoutInterval); +} + +- (void)testEnableNetworkTracking +{ + [self testBooleanField:@"enableNetworkTracking"]; +} + +- (void)testEnableSwizzling +{ + [self testBooleanField:@"enableSwizzling"]; +} + +- (void)testTracesSampleRate +{ + SentryOptions *options = [self getValidOptions:@{ @"tracesSampleRate" : @0.1 }]; + + XCTAssertEqual(options.tracesSampleRate.doubleValue, 0.1); +} + +- (void)testDefaultTracesSampleRate +{ + SentryOptions *options = [self getValidOptions:@{}]; + + XCTAssertNil(options.tracesSampleRate); +} + - (void)testTracesSampleRate_SetToNil { SentryOptions *options = [[SentryOptions alloc] init]; @@ -242,6 +816,36 @@ - (void)testTracesSampleRateUpperBound XCTAssertNil(options.tracesSampleRate); } +- (double)tracesSamplerCallback:(NSDictionary *)context +{ + return 0.1; +} + +- (void)testTracesSampler +{ + SentryTracesSamplerCallback sampler = ^(SentrySamplingContext *context) { + XCTAssertNotNil(context); + return @1.0; + }; + + SentryOptions *options = [self getValidOptions:@{ @"tracesSampler" : sampler }]; + + SentrySamplingContext *context = [[SentrySamplingContext alloc] init]; + XCTAssertEqual(options.tracesSampler(context), @1.0); +} + +- (void)testDefaultTracesSampler +{ + SentryOptions *options = [self getValidOptions:@{}]; + XCTAssertNil(options.tracesSampler); +} + +- (void)testGarbageTracesSampler_ReturnsNil +{ + SentryOptions *options = [self getValidOptions:@{ @"tracesSampler" : @"fault" }]; + XCTAssertNil(options.tracesSampler); +} + - (void)testIsTracingEnabled_NothingSet_IsDisabled { SentryOptions *options = [[SentryOptions alloc] init]; @@ -273,6 +877,25 @@ - (void)testIsTracingEnabled_TracesSamplerSet_IsEnabled } #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]; @@ -351,8 +974,170 @@ - (void)testIsProfilingEnabled_EnableProfilingSet_IsEnabled # 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" ]; + NSArray *inAppIncludes = @[ @"iOS-Swift", @"BusinessLogic", @1 ]; + SentryOptions *options = [self getValidOptions:@{ @"inAppIncludes" : inAppIncludes }]; + + NSString *bundleExecutable = [self getBundleExecutable]; + if (nil != bundleExecutable) { + expected = [expected arrayByAddingObject:bundleExecutable]; + } + + [self assertArrayEquals:expected actual:options.inAppIncludes]; +} + +- (void)testAddInAppIncludes +{ + SentryOptions *options = [self getValidOptions:@{}]; + [options addInAppInclude:@"App"]; + + NSArray *expected = @[ @"App" ]; + NSString *bundleExecutable = [self getBundleExecutable]; + if (nil != bundleExecutable) { + expected = [expected arrayByAddingObject:bundleExecutable]; + } + + [self assertArrayEquals:expected actual:options.inAppIncludes]; +} + +- (NSString *)getBundleExecutable +{ + NSDictionary *infoDict = [[NSBundle mainBundle] infoDictionary]; + return infoDict[@"CFBundleExecutable"]; +} + +- (void)testDefaultInAppIncludes +{ + SentryOptions *options = [self getValidOptions:@{}]; + XCTAssertEqualObjects([self getDefaultInAppIncludes], options.inAppIncludes); +} + +- (void)testInAppExcludes +{ + NSArray *expected = @[ @"Sentry" ]; + NSArray *inAppExcludes = @[ @"Sentry", @2 ]; + + SentryOptions *options = [self getValidOptions:@{ @"inAppExcludes" : inAppExcludes }]; + + XCTAssertEqualObjects(expected, options.inAppExcludes); +} + +- (void)testAddInAppExcludes +{ + SentryOptions *options = [self getValidOptions:@{}]; + [options addInAppExclude:@"App"]; + XCTAssertEqualObjects(@[ @"App" ], options.inAppExcludes); +} + +- (void)testDefaultInAppExcludes +{ + SentryOptions *options = [self getValidOptions:@{}]; + XCTAssertEqualObjects(@[], options.inAppExcludes); +} + +- (SentryOptions *)getValidOptions:(NSDictionary *)dict +{ + NSError *error = nil; + + NSMutableDictionary *options = [[NSMutableDictionary alloc] init]; + options[@"dsn"] = @"https://username:password@sentry.io/1"; + + [options addEntriesFromDictionary:dict]; + + SentryOptions *sentryOptions = [[SentryOptions alloc] initWithDict:options + didFailWithError:&error]; + XCTAssertNil(error); + return sentryOptions; +} + +- (void)testUrlSessionDelegate +{ + id urlSessionDelegate = [[UrlSessionDelegateSpy alloc] init]; + + SentryOptions *options = [self getValidOptions:@{ @"urlSessionDelegate" : urlSessionDelegate }]; + + XCTAssertNotNil(options.urlSessionDelegate); +} + +- (void)assertArrayEquals:(NSArray *)expected actual:(NSArray *)actual +{ + XCTAssertEqualObjects([expected sortedArrayUsingSelector:@selector(compare:)], + [actual sortedArrayUsingSelector:@selector(compare:)]); +} + +- (void)testBooleanField:(NSString *)property +{ + [self testBooleanField:property defaultValue:YES]; +} + +- (void)testBooleanField:(NSString *)property defaultValue:(BOOL)defaultValue +{ + // Opposite of default + SentryOptions *options = [self getValidOptions:@{ property : @(!defaultValue) }]; + XCTAssertEqual(!defaultValue, [self getProperty:property of:options]); + + // Default + options = [self getValidOptions:@{}]; + XCTAssertEqual(defaultValue, [self getProperty:property of:options]); + + // Garbage + options = [self getValidOptions:@{ property : @"" }]; + XCTAssertEqual(NO, [self getProperty:property of:options]); +} + +- (BOOL)getProperty:(NSString *)property of:(SentryOptions *)options +{ + SEL selector = NSSelectorFromString(property); + NSAssert( + [options respondsToSelector:selector], @"Options doesn't have a property '%@'", property); + + NSInvocation *invocation = [NSInvocation + invocationWithMethodSignature:[[options class] + instanceMethodSignatureForSelector:selector]]; + [invocation setSelector:selector]; + [invocation setTarget:options]; + [invocation invoke]; + BOOL result; + [invocation getReturnValue:&result]; + + return result; +} + - (NSArray *)getDefaultInAppIncludes { NSDictionary *infoDict = [[NSBundle mainBundle] infoDictionary]; diff --git a/Tests/SentryTests/SentryTests.m b/Tests/SentryTests/SentryTests.m index b1907268b03..69452ab8f46 100644 --- a/Tests/SentryTests/SentryTests.m +++ b/Tests/SentryTests/SentryTests.m @@ -1,4 +1,5 @@ #import "NSDate+SentryExtras.h" +#import "PrivateSentrySDKOnly.h" #import "SentryBreadcrumbTracker.h" #import "SentryLevelMapper.h" #import "SentryMessage.h" @@ -45,9 +46,9 @@ - (void)testVersion - (void)testSharedClient { NSError *error = nil; - SentryOptions *options = - [[SentryOptions alloc] initWithDsn:@"https://username:password@app.getsentry.com/12345" - didFailWithError:&error]; + SentryOptions *options = [[SentryOptions alloc] + initWithDict:@{ @"dsn" : @"https://username:password@app.getsentry.com/12345" } + didFailWithError:&error]; SentryClient *client = [[SentryClient alloc] initWithOptions:options]; XCTAssertNil(error);