diff --git a/CHANGELOG.md b/CHANGELOG.md index 17cb4fb1ff0..7d004a5c3a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Features - Profile concurrent transactions (#2227) +- HTTP Client errors (#2308) - Disable bitcode for Carthage distribution (#2341) ### Fixes diff --git a/Samples/TrendingMovies/TrendingMovies.xcodeproj/xcshareddata/xcschemes/ProfileDataGeneratorUITest.xcscheme b/Samples/TrendingMovies/TrendingMovies.xcodeproj/xcshareddata/xcschemes/ProfileDataGeneratorUITest.xcscheme index fc186453333..37c5bba9acc 100644 --- a/Samples/TrendingMovies/TrendingMovies.xcodeproj/xcshareddata/xcschemes/ProfileDataGeneratorUITest.xcscheme +++ b/Samples/TrendingMovies/TrendingMovies.xcodeproj/xcshareddata/xcschemes/ProfileDataGeneratorUITest.xcscheme @@ -1,6 +1,6 @@ @@ -159,6 +159,11 @@ NS_SWIFT_NAME(Event) */ @property (nonatomic, strong) NSArray *_Nullable breadcrumbs; +/** + * Set the Http request information. + */ +@property (nonatomic, strong, nullable) SentryRequest *request; + /** * Init an SentryEvent will set all needed fields by default * @return SentryEvent diff --git a/Sources/Sentry/Public/SentryHttpStatusCodeRange.h b/Sources/Sentry/Public/SentryHttpStatusCodeRange.h new file mode 100644 index 00000000000..38e6f53457b --- /dev/null +++ b/Sources/Sentry/Public/SentryHttpStatusCodeRange.h @@ -0,0 +1,37 @@ +#import "SentryDefines.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + * The Http status code range. + * The range is inclusive so the min and max is considered part of the range. + * + * Example for a range: 400 to 499, 500 to 599, 400 to 599. + * Example for a single status code 400, 500. + */ +NS_SWIFT_NAME(HttpStatusCodeRange) +@interface SentryHttpStatusCodeRange : NSObject +SENTRY_NO_INIT + +@property (nonatomic, readonly) NSInteger min; + +@property (nonatomic, readonly) NSInteger max; + +/** + * The Http status code min and max. + * The range is inclusive so the min and max is considered part of the range. + * + * Example for a range: 400 to 499, 500 to 599, 400 to 599. + */ +- (instancetype)initWithMin:(NSInteger)min max:(NSInteger)max; + +/** + * The Http status code. + * + * Example for a single status code 400, 500. + */ +- (instancetype)initWithStatusCode:(NSInteger)statusCode; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Sources/Sentry/Public/SentryOptions.h b/Sources/Sentry/Public/SentryOptions.h index 37229734120..efa1537d230 100644 --- a/Sources/Sentry/Public/SentryOptions.h +++ b/Sources/Sentry/Public/SentryOptions.h @@ -3,7 +3,7 @@ NS_ASSUME_NONNULL_BEGIN -@class SentryDsn, SentrySdkInfo, SentryMeasurementValue; +@class SentryDsn, SentrySdkInfo, SentryMeasurementValue, SentryHttpStatusCodeRange; NS_SWIFT_NAME(Options) @interface SentryOptions : NSObject @@ -414,6 +414,31 @@ NS_SWIFT_NAME(Options) */ @property (nonatomic, retain) NSArray *tracePropagationTargets; +/** + * When enabled, the SDK captures HTTP Client errors. Default value is NO. + * This feature requires enableSwizzling enabled as well, Default value is YES. + */ +@property (nonatomic, assign) BOOL enableCaptureFailedRequests; + +/** + * The SDK will only capture HTTP Client errors if the HTTP Response status code is within the + * defined range. + * + * Defaults to 500 - 599. + */ +@property (nonatomic, strong) NSArray *failedRequestStatusCodes; + +/** + * An array of hosts or regexes that determines if HTTP Client errors will be automatically + * captured. + * + * This array can contain instances of NSString which should match the URL (using `contains`), + * and instances of NSRegularExpression, which will be used to check the whole URL. + * + * The default value automatically captures HTTP Client errors of all outgoing requests. + */ +@property (nonatomic, strong) NSArray *failedRequestTargets; + @end NS_ASSUME_NONNULL_END diff --git a/Sources/Sentry/Public/SentryRequest.h b/Sources/Sentry/Public/SentryRequest.h new file mode 100644 index 00000000000..583b0b635cf --- /dev/null +++ b/Sources/Sentry/Public/SentryRequest.h @@ -0,0 +1,49 @@ +#import "SentryDefines.h" +#import "SentrySerializable.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface SentryRequest : NSObject + +// TODO: data, env + +/** + * Optional: HTTP response body size. + */ +@property (nonatomic, copy, nullable) NSNumber *bodySize; + +/** + * Optional: The cookie values. + */ +@property (nonatomic, copy, nullable) NSString *cookies; + +/** + * Optional: A dictionary of submitted headers. + */ +@property (nonatomic, strong, nullable) NSDictionary *headers; + +/** + * Optional: The fragment of the request URL. + */ +@property (nonatomic, copy, nullable) NSString *fragment; + +/** + * Optional: HTTP request method. + */ +@property (nonatomic, copy, nullable) NSString *method; + +/** + * Optional: The query string component of the URL. + */ +@property (nonatomic, copy, nullable) NSString *queryString; + +/** + * Optional: The URL of the request if available. + */ +@property (nonatomic, copy, nullable) NSString *url; + +- (instancetype)init; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Sources/Sentry/Public/SentryStacktrace.h b/Sources/Sentry/Public/SentryStacktrace.h index e8c67d83f41..663b22505d8 100644 --- a/Sources/Sentry/Public/SentryStacktrace.h +++ b/Sources/Sentry/Public/SentryStacktrace.h @@ -21,6 +21,11 @@ SENTRY_NO_INIT */ @property (nonatomic, strong) NSDictionary *registers; +/** + * Indicates that this stack trace is a snapshot triggered by an external signal. + */ +@property (nonatomic, copy, nullable) NSNumber *snapshot; + /** * Initialize a SentryStacktrace with frames and registers * @param frames NSArray diff --git a/Sources/Sentry/SentryEvent.m b/Sources/Sentry/SentryEvent.m index 243f9405c51..9c9416f0ff1 100644 --- a/Sources/Sentry/SentryEvent.m +++ b/Sources/Sentry/SentryEvent.m @@ -10,6 +10,7 @@ #import "SentryLevelMapper.h" #import "SentryMessage.h" #import "SentryMeta.h" +#import "SentryRequest.h" #import "SentryStacktrace.h" #import "SentryThread.h" #import "SentryUser.h" @@ -157,6 +158,10 @@ - (void)addSimpleProperties:(NSMutableDictionary *)serializedData forKey:@"start_timestamp"]; } } + + if (nil != self.request) { + [serializedData setValue:[self.request serialize] forKey:@"request"]; + } } - (NSArray *_Nullable)serializeBreadcrumbs diff --git a/Sources/Sentry/SentryHttpStatusCodeRange.m b/Sources/Sentry/SentryHttpStatusCodeRange.m new file mode 100644 index 00000000000..d095752cf60 --- /dev/null +++ b/Sources/Sentry/SentryHttpStatusCodeRange.m @@ -0,0 +1,32 @@ +#import "SentryHttpStatusCodeRange.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation SentryHttpStatusCodeRange + +- (instancetype)initWithMin:(NSInteger)min max:(NSInteger)max +{ + if (self = [super init]) { + _min = min; + _max = max; + } + return self; +} + +- (instancetype)initWithStatusCode:(NSInteger)statusCode +{ + if (self = [super init]) { + _min = statusCode; + _max = statusCode; + } + return self; +} + +- (BOOL)isInRange:(NSInteger)statusCode +{ + return statusCode >= _min && statusCode <= _max; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Sources/Sentry/SentryNetworkTracker.m b/Sources/Sentry/SentryNetworkTracker.m index b3ef917b83c..9cadda5f681 100644 --- a/Sources/Sentry/SentryNetworkTracker.m +++ b/Sources/Sentry/SentryNetworkTracker.m @@ -1,11 +1,21 @@ #import "SentryNetworkTracker.h" #import "SentryBaggage.h" #import "SentryBreadcrumb.h" +#import "SentryClient+Private.h" +#import "SentryEvent.h" +#import "SentryException.h" +#import "SentryHttpStatusCodeRange+Private.h" +#import "SentryHttpStatusCodeRange.h" #import "SentryHub+Private.h" #import "SentryLog.h" +#import "SentryMechanism.h" +#import "SentryRequest.h" #import "SentrySDK+Private.h" #import "SentryScope+Private.h" #import "SentrySerialization.h" +#import "SentryStacktrace.h" +#import "SentryThread.h" +#import "SentryThreadInspector.h" #import "SentryTraceContext.h" #import "SentryTraceHeader.h" #import "SentryTracer.h" @@ -16,6 +26,7 @@ @property (nonatomic, assign) BOOL isNetworkTrackingEnabled; @property (nonatomic, assign) BOOL isNetworkBreadcrumbEnabled; +@property (nonatomic, assign) BOOL isCaptureFailedRequestsEnabled; @end @@ -34,6 +45,7 @@ - (instancetype)init if (self = [super init]) { _isNetworkTrackingEnabled = NO; _isNetworkBreadcrumbEnabled = NO; + _isCaptureFailedRequestsEnabled = NO; } return self; } @@ -52,17 +64,25 @@ - (void)enableNetworkBreadcrumbs } } +- (void)enableCaptureFailedRequests +{ + @synchronized(self) { + _isCaptureFailedRequestsEnabled = YES; + } +} + - (void)disable { @synchronized(self) { _isNetworkBreadcrumbEnabled = NO; _isNetworkTrackingEnabled = NO; + _isCaptureFailedRequestsEnabled = NO; } } -- (BOOL)addHeadersForRequestWithURL:(NSURL *)URL +- (BOOL)isTargetMatch:(NSURL *)URL withTargets:(NSArray *)targets { - for (id targetCheck in SentrySDK.options.tracePropagationTargets) { + for (id targetCheck in targets) { if ([targetCheck isKindOfClass:[NSRegularExpression class]]) { NSString *string = URL.absoluteString; NSUInteger numberOfMatches = @@ -143,7 +163,8 @@ - (void)urlSessionTaskResume:(NSURLSessionTask *)sessionTask } if ([sessionTask currentRequest] && - [self addHeadersForRequestWithURL:sessionTask.currentRequest.URL]) { + [self isTargetMatch:sessionTask.currentRequest.URL + withTargets:SentrySDK.options.tracePropagationTargets]) { NSString *baggageHeader = @""; SentryTracer *tracer = [SentryTracer getTracer:span]; @@ -206,7 +227,8 @@ - (void)urlSessionTaskResume:(NSURLSessionTask *)sessionTask - (void)urlSessionTask:(NSURLSessionTask *)sessionTask setState:(NSURLSessionTaskState)newState { - if (!self.isNetworkTrackingEnabled && !self.isNetworkBreadcrumbEnabled) { + if (!self.isNetworkTrackingEnabled && !self.isNetworkBreadcrumbEnabled + && !self.isCaptureFailedRequestsEnabled) { return; } @@ -239,6 +261,8 @@ - (void)urlSessionTask:(NSURLSessionTask *)sessionTask setState:(NSURLSessionTas } if (sessionTask.state == NSURLSessionTaskStateRunning) { + [self captureFailedRequests:sessionTask]; + [self addBreadcrumbForSessionTask:sessionTask]; NSInteger responseStatusCode = [self urlResponseStatusCode:sessionTask.response]; @@ -265,6 +289,117 @@ - (void)urlSessionTask:(NSURLSessionTask *)sessionTask setState:(NSURLSessionTas SENTRY_LOG_DEBUG(@"SentryNetworkTracker finished HTTP span for sessionTask"); } +- (void)captureFailedRequests:(NSURLSessionTask *)sessionTask +{ + if (!self.isCaptureFailedRequestsEnabled) { + SENTRY_LOG_DEBUG( + @"captureFailedRequestsEnabled is disabled, not capturing HTTP Client errors."); + return; + } + + // if request or response are null, we can't raise the event + if (sessionTask.currentRequest == nil || sessionTask.response == nil) { + SENTRY_LOG_DEBUG(@"Request or Response are null, not capturing HTTP Client errors."); + return; + } + // some properties are only available if the response is of the NSHTTPURLResponse type + // bail if not + if (![sessionTask.response isKindOfClass:[NSHTTPURLResponse class]]) { + SENTRY_LOG_DEBUG(@"Response isn't a known type, not capturing HTTP Client errors."); + return; + } + NSHTTPURLResponse *myResponse = (NSHTTPURLResponse *)sessionTask.response; + NSURLRequest *myRequest = sessionTask.currentRequest; + NSNumber *responseStatusCode = @(myResponse.statusCode); + + if (![self containsStatusCode:myResponse.statusCode]) { + SENTRY_LOG_DEBUG(@"Response status code isn't within the allowed ranges, not capturing " + @"HTTP Client errors."); + return; + } + + if (![self isTargetMatch:myRequest.URL withTargets:SentrySDK.options.failedRequestTargets]) { + SENTRY_LOG_DEBUG( + @"Request url isn't within the request targets, not capturing HTTP Client errors."); + return; + } + + NSString *message = [NSString + stringWithFormat:@"HTTP Client Error with status code: %ld", (long)myResponse.statusCode]; + + SentryEvent *event = [[SentryEvent alloc] initWithLevel:kSentryLevelError]; + + SentryThreadInspector *threadInspector = SentrySDK.currentHub.getClient.threadInspector; + NSArray *threads = [threadInspector getCurrentThreads]; + + // sessionTask.error isn't used because it's not about network errors but rather + // requests that are considered failed depending on the HTTP status code + SentryException *sentryException = [[SentryException alloc] initWithValue:message + type:@"HTTPClientError"]; + sentryException.mechanism = [[SentryMechanism alloc] initWithType:@"HTTPClientError"]; + + for (SentryThread *thread in threads) { + if ([thread.current boolValue]) { + SentryStacktrace *sentryStacktrace = [thread stacktrace]; + sentryStacktrace.snapshot = @(YES); + + sentryException.stacktrace = sentryStacktrace; + + break; + } + } + + SentryRequest *request = [[SentryRequest alloc] init]; + + NSURL *url = [[sessionTask currentRequest] URL]; + + NSString *urlString = [NSString stringWithFormat:@"%@://%@%@", url.scheme, url.host, url.path]; + + request.url = urlString; + request.method = myRequest.HTTPMethod; + request.fragment = url.fragment; + request.queryString = url.query; + request.bodySize = [NSNumber numberWithLongLong:sessionTask.countOfBytesSent]; + if (nil != myRequest.allHTTPHeaderFields) { + NSDictionary *headers = myRequest.allHTTPHeaderFields.copy; + request.headers = headers; + request.cookies = headers[@"Cookie"]; + } + + event.exceptions = @[ sentryException ]; + event.request = request; + + NSMutableDictionary *context = [[NSMutableDictionary alloc] init]; + NSMutableDictionary *response = [[NSMutableDictionary alloc] init]; + + [response setValue:responseStatusCode forKey:@"status_code"]; + if (nil != myResponse.allHeaderFields) { + NSDictionary *headers = myResponse.allHeaderFields.copy; + [response setValue:headers forKey:@"headers"]; + [response setValue:headers[@"Set-Cookie"] forKey:@"cookies"]; + } + if (sessionTask.countOfBytesReceived != 0) { + [response setValue:[NSNumber numberWithLongLong:sessionTask.countOfBytesReceived] + forKey:@"body_size"]; + } + + context[@"response"] = response; + event.context = context; + + [SentrySDK captureEvent:event]; +} + +- (BOOL)containsStatusCode:(NSInteger)statusCode +{ + for (SentryHttpStatusCodeRange *range in SentrySDK.options.failedRequestStatusCodes) { + if ([range isInRange:statusCode]) { + return YES; + } + } + + return NO; +} + - (void)addBreadcrumbForSessionTask:(NSURLSessionTask *)sessionTask { if (!self.isNetworkBreadcrumbEnabled) { diff --git a/Sources/Sentry/SentryNetworkTrackingIntegration.m b/Sources/Sentry/SentryNetworkTrackingIntegration.m index ee006406e05..3f0a5ba5a70 100644 --- a/Sources/Sentry/SentryNetworkTrackingIntegration.m +++ b/Sources/Sentry/SentryNetworkTrackingIntegration.m @@ -25,7 +25,12 @@ - (BOOL)installWithOptions:(SentryOptions *)options [SentryNetworkTracker.sharedInstance enableNetworkBreadcrumbs]; } - if (shouldEnableNetworkTracking || options.enableNetworkBreadcrumbs) { + if (options.enableCaptureFailedRequests) { + [SentryNetworkTracker.sharedInstance enableCaptureFailedRequests]; + } + + if (shouldEnableNetworkTracking || options.enableNetworkBreadcrumbs + || options.enableCaptureFailedRequests) { [SentryNetworkTrackingIntegration swizzleURLSessionTask]; return YES; } else { diff --git a/Sources/Sentry/SentryOptions.m b/Sources/Sentry/SentryOptions.m index 8f9bc0b5036..b37bdb5369c 100644 --- a/Sources/Sentry/SentryOptions.m +++ b/Sources/Sentry/SentryOptions.m @@ -1,6 +1,7 @@ #import "SentryOptions.h" #import "SentryANRTracker.h" #import "SentryDsn.h" +#import "SentryHttpStatusCodeRange.h" #import "SentryLevelMapper.h" #import "SentryLog.h" #import "SentryMeta.h" @@ -12,6 +13,7 @@ @property (nullable, nonatomic, copy, readonly) NSNumber *defaultSampleRate; @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; @@ -60,6 +62,7 @@ - (instancetype)init self.maxAttachmentSize = 20 * 1024 * 1024; self.sendDefaultPii = NO; self.enableAutoPerformanceTracking = YES; + self.enableCaptureFailedRequests = NO; #if SENTRY_HAS_UIKIT self.enableUIViewControllerTracking = YES; self.attachScreenshot = NO; @@ -117,6 +120,12 @@ - (instancetype)init options:NSRegularExpressionCaseInsensitive error:NULL]; self.tracePropagationTargets = @[ everythingAllowedRegex ]; + self.failedRequestTargets = @[ everythingAllowedRegex ]; + + // defaults to 500 to 599 + SentryHttpStatusCodeRange *defaultHttpStatusCodeRange = + [[SentryHttpStatusCodeRange alloc] initWithMin:500 max:599]; + self.failedRequestStatusCodes = @[ defaultHttpStatusCodeRange ]; } return self; } @@ -148,6 +157,19 @@ - (void)setTracePropagationTargets:(NSArray *)tracePropagationTargets _tracePropagationTargets = tracePropagationTargets; } +- (void)setFailedRequestTargets:(NSArray *)failedRequestTargets +{ + for (id targetCheck in failedRequestTargets) { + if (![targetCheck isKindOfClass:[NSRegularExpression class]] + && ![targetCheck isKindOfClass:[NSString class]]) { + SENTRY_LOG_WARN(@"Only instances of NSString and NSRegularExpression are supported " + @"inside failedRequestTargets."); + } + } + + _failedRequestTargets = failedRequestTargets; +} + - (void)setIntegrations:(NSArray *)integrations { SENTRY_LOG_WARN( @@ -270,6 +292,9 @@ - (BOOL)validateOptions:(NSDictionary *)options [self setBool:options[@"enableAutoPerformanceTracking"] block:^(BOOL value) { self->_enableAutoPerformanceTracking = value; }]; + [self setBool:options[@"enableCaptureFailedRequests"] + block:^(BOOL value) { self->_enableCaptureFailedRequests = value; }]; + #if SENTRY_HAS_UIKIT [self setBool:options[@"enableUIViewControllerTracking"] block:^(BOOL value) { self->_enableUIViewControllerTracking = value; }]; @@ -352,6 +377,14 @@ - (BOOL)validateOptions:(NSDictionary *)options self.tracePropagationTargets = options[@"tracePropagationTargets"]; } + if ([options[@"failedRequestStatusCodes"] isKindOfClass:[NSArray class]]) { + self.failedRequestStatusCodes = options[@"failedRequestStatusCodes"]; + } + + if ([options[@"failedRequestTargets"] isKindOfClass:[NSArray class]]) { + self.failedRequestTargets = options[@"failedRequestTargets"]; + } + // SentrySdkInfo already expects a dictionary with {"sdk": {"name": ..., "value": ...}} // so we're passing the whole options object. // Note: we should remove this code once the hybrid SDKs move over to the new diff --git a/Sources/Sentry/SentryRequest.m b/Sources/Sentry/SentryRequest.m new file mode 100644 index 00000000000..87f588775c3 --- /dev/null +++ b/Sources/Sentry/SentryRequest.m @@ -0,0 +1,37 @@ +#import "SentryRequest.h" +#import "NSDictionary+SentrySanitize.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation SentryRequest + +- (instancetype)init +{ + self = [super init]; + return self; +} + +- (NSDictionary *)serialize +{ + NSMutableDictionary *serializedData = [[NSMutableDictionary alloc] init]; + + @synchronized(self) { + if (nil != self.bodySize && self.bodySize.intValue != 0) { + [serializedData setValue:self.bodySize forKey:@"body_size"]; + } + [serializedData setValue:self.cookies forKey:@"cookies"]; + [serializedData setValue:self.fragment forKey:@"fragment"]; + if (nil != self.headers) { + [serializedData setValue:[self.headers sentry_sanitize] forKey:@"headers"]; + } + [serializedData setValue:self.method forKey:@"method"]; + [serializedData setValue:self.queryString forKey:@"query_string"]; + [serializedData setValue:self.url forKey:@"url"]; + } + + return serializedData; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Sources/Sentry/SentryStacktrace.m b/Sources/Sentry/SentryStacktrace.m index 49e6211da91..ccf6a7d1232 100644 --- a/Sources/Sentry/SentryStacktrace.m +++ b/Sources/Sentry/SentryStacktrace.m @@ -55,6 +55,8 @@ - (void)fixDuplicateFrames if (self.registers.count > 0) { [serializedData setValue:self.registers forKey:@"registers"]; } + [serializedData setValue:self.snapshot forKey:@"snapshot"]; + return serializedData; } diff --git a/Sources/Sentry/include/SentryHttpStatusCodeRange+Private.h b/Sources/Sentry/include/SentryHttpStatusCodeRange+Private.h new file mode 100644 index 00000000000..755c1ff31a4 --- /dev/null +++ b/Sources/Sentry/include/SentryHttpStatusCodeRange+Private.h @@ -0,0 +1,13 @@ +#import "SentryDefines.h" +#import "SentryHttpStatusCodeRange.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface +SentryHttpStatusCodeRange (Private) + +- (BOOL)isInRange:(NSInteger)statusCode; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Sources/Sentry/include/SentryNetworkTracker.h b/Sources/Sentry/include/SentryNetworkTracker.h index c1758d5bcd5..4b48aa185c9 100644 --- a/Sources/Sentry/include/SentryNetworkTracker.h +++ b/Sources/Sentry/include/SentryNetworkTracker.h @@ -15,11 +15,13 @@ static NSString *const SENTRY_NETWORK_REQUEST_TRACKER_SPAN = @"SENTRY_NETWORK_RE - (void)urlSessionTask:(NSURLSessionTask *)sessionTask setState:(NSURLSessionTaskState)newState; - (void)enableNetworkTracking; - (void)enableNetworkBreadcrumbs; -- (BOOL)addHeadersForRequestWithURL:(NSURL *)URL; +- (void)enableCaptureFailedRequests; +- (BOOL)isTargetMatch:(NSURL *)URL withTargets:(NSArray *)targets; - (void)disable; @property (nonatomic, readonly) BOOL isNetworkTrackingEnabled; @property (nonatomic, readonly) BOOL isNetworkBreadcrumbEnabled; +@property (nonatomic, readonly) BOOL isCaptureFailedRequestsEnabled; @end diff --git a/Tests/SentryTests/Integrations/Performance/Network/SentryNetworkTrackerIntegrationTests.swift b/Tests/SentryTests/Integrations/Performance/Network/SentryNetworkTrackerIntegrationTests.swift index 7b864a3969e..9c7293c7463 100644 --- a/Tests/SentryTests/Integrations/Performance/Network/SentryNetworkTrackerIntegrationTests.swift +++ b/Tests/SentryTests/Integrations/Performance/Network/SentryNetworkTrackerIntegrationTests.swift @@ -7,6 +7,7 @@ class SentryNetworkTrackerIntegrationTests: XCTestCase { private static let dsnAsString = TestConstants.dsnAsString(username: "SentryNetworkTrackerIntegrationTests") private static let testBaggageURL = URL(string: "http://localhost:8080/echo-baggage-header")! private static let testTraceURL = URL(string: "http://localhost:8080/echo-sentry-trace")! + private static let clientErrorTraceURL = URL(string: "http://localhost:8080/http-client-error")! private static let transactionName = "TestTransaction" private static let transactionOperation = "Test" @@ -177,7 +178,7 @@ class SentryNetworkTrackerIntegrationTests: XCTestCase { XCTAssertEqual("200", networkSpan.tags["http.status_code"]) } - + func testGetRequest_CompareSentryTraceHeader() { startSDK() let transaction = SentrySDK.startTransaction(name: "Test Transaction", operation: "TEST", bindToScope: true) as! SentryTracer @@ -188,12 +189,12 @@ class SentryNetworkTrackerIntegrationTests: XCTestCase { response = String(data: data ?? Data(), encoding: .utf8) ?? "" expect.fulfill() } - + dataTask.resume() wait(for: [expect], timeout: 5) - + let children = Dynamic(transaction).children as [SentrySpan]? - + XCTAssertEqual(children?.count, 1) //Span was created in task resume swizzle. let networkSpan = children![0] @@ -201,6 +202,57 @@ class SentryNetworkTrackerIntegrationTests: XCTestCase { XCTAssertEqual(expectedTraceHeader, response) } + func testCaptureFailedRequestsDisabled_WhenSwizzlingDisabled() { + fixture.options.enableSwizzling = false + fixture.options.enableCaptureFailedRequests = true + startSDK() + + XCTAssertFalse(SentryNetworkTracker.sharedInstance.isCaptureFailedRequestsEnabled) + } + + func testCaptureFailedRequestsDisabled() { + startSDK() + + XCTAssertFalse(SentryNetworkTracker.sharedInstance.isCaptureFailedRequestsEnabled) + } + + func testCaptureFailedRequestsEnabled() { + fixture.options.enableCaptureFailedRequests = true + startSDK() + + XCTAssertTrue(SentryNetworkTracker.sharedInstance.isCaptureFailedRequestsEnabled) + } + + func testGetCaptureFailedRequestsEnabled() { + let expect = expectation(description: "Request completed") + + var sentryEvent: Event? + + fixture.options.enableCaptureFailedRequests = true + fixture.options.failedRequestStatusCodes = [ HttpStatusCodeRange(statusCode: 400) ] + fixture.options.beforeSend = { event in + sentryEvent = event + expect.fulfill() + return event + } + + startSDK() + + let session = URLSession(configuration: URLSessionConfiguration.default) + + let dataTask = session.dataTask(with: SentryNetworkTrackerIntegrationTests.clientErrorTraceURL) { (_, _, _) in } + + dataTask.resume() + wait(for: [expect], timeout: 5) + + XCTAssertNotNil(sentryEvent) + XCTAssertNotNil(sentryEvent!.request) + + let sentryResponse = sentryEvent!.context?["response"] + + XCTAssertEqual(sentryResponse?["status_code"] as? NSNumber, 400) + } + private func asserrtNetworkTrackerDisabled(configureOptions: (Options) -> Void) { configureOptions(fixture.options) diff --git a/Tests/SentryTests/Integrations/Performance/Network/SentryNetworkTrackerTests.swift b/Tests/SentryTests/Integrations/Performance/Network/SentryNetworkTrackerTests.swift index 5de8b153103..55ec9272697 100644 --- a/Tests/SentryTests/Integrations/Performance/Network/SentryNetworkTrackerTests.swift +++ b/Tests/SentryTests/Integrations/Performance/Network/SentryNetworkTrackerTests.swift @@ -15,18 +15,23 @@ class SentryNetworkTrackerTests: XCTestCase { let options: Options let scope: Scope let nsUrlRequest = NSURLRequest(url: SentryNetworkTrackerTests.testURL) + let client: TestClient! + let hub: TestHub! init() { options = Options() options.dsn = SentryNetworkTrackerTests.dsnAsString sentryTask = URLSessionDataTaskMock(request: URLRequest(url: URL(string: options.dsn!)!)) scope = Scope() + client = TestClient(options: options) + hub = TestHub(client: client, andScope: scope) } func getSut() -> SentryNetworkTracker { let result = SentryNetworkTracker.sharedInstance result.enableNetworkTracking() result.enableNetworkBreadcrumbs() + result.enableCaptureFailedRequests() return result } } @@ -36,7 +41,8 @@ class SentryNetworkTrackerTests: XCTestCase { override func setUp() { super.setUp() fixture = Fixture() - SentrySDK.setCurrentHub(TestHub(client: TestClient(options: fixture.options), andScope: fixture.scope)) + + SentrySDK.setCurrentHub(fixture.hub) CurrentDate.setCurrentDateProvider(fixture.dateProvider) } @@ -565,40 +571,155 @@ class SentryNetworkTrackerTests: XCTestCase { XCTAssertNil(task.currentRequest?.allHTTPHeaderFields?["sentry-trace"]) } - func testAddHeadersForRequestWithURL() { + func testIsTargetMatch() { // Default: all urls + let defaultRegex = try! NSRegularExpression(pattern: ".*") let sut = fixture.getSut() - XCTAssertTrue(sut.addHeadersForRequest(with: URL(string: "http://localhost")!)) - XCTAssertTrue(sut.addHeadersForRequest(with: URL(string: "http://www.example.com/api/projects")!)) + XCTAssertTrue(sut.isTargetMatch(URL(string: "http://localhost")!, withTargets: [ defaultRegex ])) + XCTAssertTrue(sut.isTargetMatch(URL(string: "http://www.example.com/api/projects")!, withTargets: [ defaultRegex ])) // Strings: hostname - fixture.options.tracePropagationTargets = ["localhost"] - XCTAssertTrue(sut.addHeadersForRequest(with: URL(string: "http://localhost")!)) - XCTAssertTrue(sut.addHeadersForRequest(with: URL(string: "http://localhost-but-not-really")!)) // works because of `contains` - XCTAssertFalse(sut.addHeadersForRequest(with: URL(string: "http://www.example.com/api/projects")!)) + XCTAssertTrue(sut.isTargetMatch(URL(string: "http://localhost")!, withTargets: ["localhost"])) + XCTAssertTrue(sut.isTargetMatch(URL(string: "http://localhost-but-not-really")!, withTargets: ["localhost"])) // works because of `contains` + XCTAssertFalse(sut.isTargetMatch(URL(string: "http://www.example.com/api/projects")!, withTargets: ["localhost"])) - fixture.options.tracePropagationTargets = ["www.example.com"] - XCTAssertFalse(sut.addHeadersForRequest(with: URL(string: "http://localhost")!)) - XCTAssertTrue(sut.addHeadersForRequest(with: URL(string: "http://www.example.com/api/projects")!)) - XCTAssertFalse(sut.addHeadersForRequest(with: URL(string: "http://api.example.com/api/projects")!)) - XCTAssertTrue(sut.addHeadersForRequest(with: URL(string: "http://www.example.com.evil.com/api/projects")!)) // works because of `contains` + XCTAssertFalse(sut.isTargetMatch(URL(string: "http://localhost")!, withTargets: ["www.example.com"])) + XCTAssertTrue(sut.isTargetMatch(URL(string: "http://www.example.com/api/projects")!, withTargets: ["www.example.com"])) + XCTAssertFalse(sut.isTargetMatch(URL(string: "http://api.example.com/api/projects")!, withTargets: ["www.example.com"])) + XCTAssertTrue(sut.isTargetMatch(URL(string: "http://www.example.com.evil.com/api/projects")!, withTargets: ["www.example.com"])) // works because of `contains` // Test regex let regex = try! NSRegularExpression(pattern: "http://www.example.com/api/.*") - fixture.options.tracePropagationTargets = [regex] - XCTAssertFalse(sut.addHeadersForRequest(with: URL(string: "http://localhost")!)) - XCTAssertFalse(sut.addHeadersForRequest(with: URL(string: "http://www.example.com/url")!)) - XCTAssertTrue(sut.addHeadersForRequest(with: URL(string: "http://www.example.com/api/projects")!)) + XCTAssertFalse(sut.isTargetMatch(URL(string: "http://localhost")!, withTargets: [regex])) + XCTAssertFalse(sut.isTargetMatch(URL(string: "http://www.example.com/url")!, withTargets: [regex])) + XCTAssertTrue(sut.isTargetMatch(URL(string: "http://www.example.com/api/projects")!, withTargets: [regex])) // Regex and string - fixture.options.tracePropagationTargets = ["localhost", regex] - XCTAssertTrue(sut.addHeadersForRequest(with: URL(string: "http://localhost")!)) - XCTAssertFalse(sut.addHeadersForRequest(with: URL(string: "http://www.example.com/url")!)) - XCTAssertTrue(sut.addHeadersForRequest(with: URL(string: "http://www.example.com/api/projects")!)) + XCTAssertTrue(sut.isTargetMatch(URL(string: "http://localhost")!, withTargets: ["localhost", regex])) + XCTAssertFalse(sut.isTargetMatch(URL(string: "http://www.example.com/url")!, withTargets: ["localhost", regex])) + XCTAssertTrue(sut.isTargetMatch(URL(string: "http://www.example.com/api/projects")!, withTargets: ["localhost", regex])) // String and integer (which isn't valid, make sure it doesn't crash) - fixture.options.tracePropagationTargets = ["localhost", 123] - XCTAssertTrue(sut.addHeadersForRequest(with: URL(string: "http://localhost")!)) + XCTAssertTrue(sut.isTargetMatch(URL(string: "http://localhost")!, withTargets: ["localhost", 123])) + } + + func testCaptureHTTPClientErrorRequest() { + let sut = fixture.getSut() + + let url = URL(string: "https://www.domain.com/api?query=myQuery#myFragment")! + var request = URLRequest(url: url) + request.httpMethod = "GET" + let headers = ["test": "test", "Cookie": "myCookie", "Set-Cookie": "myCookie"] + request.allHTTPHeaderFields = headers + + let task = URLSessionDataTaskMock(request: request) + task.setResponse(createResponse(code: 500)) + + sut.urlSessionTask(task, setState: .completed) + + fixture.hub.group.wait() + + guard let envelope = self.fixture.hub.capturedEventsWithScopes.first else { + XCTFail("Expected to capture 1 event") + return + } + let sentryRequest = envelope.event.request! + + XCTAssertEqual(sentryRequest.url, "https://www.domain.com/api") + XCTAssertEqual(sentryRequest.method, "GET") + XCTAssertEqual(sentryRequest.bodySize, 652) + XCTAssertEqual(sentryRequest.cookies, "myCookie") + XCTAssertEqual(sentryRequest.headers, headers) + XCTAssertEqual(sentryRequest.fragment, "myFragment") + XCTAssertEqual(sentryRequest.queryString, "query=myQuery") + } + + func testCaptureHTTPClientErrorResponse() { + let sut = fixture.getSut() + let task = createDataTask() + + let headers = ["test": "test", "Cookie": "myCookie", "Set-Cookie": "myCookie"] + let response = HTTPURLResponse( + url: SentryNetworkTrackerTests.testURL, + statusCode: 500, + httpVersion: "1.1", + headerFields: headers)! + task.setResponse(response) + + sut.urlSessionTask(task, setState: .completed) + + fixture.hub.group.wait() + + guard let envelope = self.fixture.hub.capturedEventsWithScopes.first else { + XCTFail("Expected to capture 1 event") + return + } + let sentryResponse = envelope.event.context?["response"] + + XCTAssertEqual(sentryResponse?["status_code"] as? NSNumber, 500) + XCTAssertEqual(sentryResponse?["headers"] as? [String: String], headers) + XCTAssertEqual(sentryResponse?["cookies"] as? String, "myCookie") + XCTAssertEqual(sentryResponse?["body_size"] as? NSNumber, 256) + } + + func testCaptureHTTPClientErrorException() { + let sut = fixture.getSut() + let task = createDataTask() + task.setResponse(createResponse(code: 500)) + + sut.urlSessionTask(task, setState: .completed) + + fixture.hub.group.wait() + + guard let envelope = self.fixture.hub.capturedEventsWithScopes.first else { + XCTFail("Expected to capture 1 event") + return + } + XCTAssertEqual(envelope.event.exceptions!.count, 1) + let exception = envelope.event.exceptions!.first! + + XCTAssertEqual(exception.type, "HTTPClientError") + XCTAssertEqual(exception.value, "HTTP Client Error with status code: 500") + + let stackTrace = exception.stacktrace! + XCTAssertTrue(stackTrace.snapshot!.boolValue) + XCTAssertNotNil(stackTrace.frames) + } + + func testDoesNotCaptureHTTPClientErrorIfDisabled() { + let sut = fixture.getSut() + sut.disable() + sut.enableNetworkTracking() + sut.enableNetworkBreadcrumbs() + + let task = createDataTask() + task.setResponse(createResponse(code: 500)) + + sut.urlSessionTask(task, setState: .completed) + + XCTAssertNil(fixture.hub.capturedEventsWithScopes.first) + } + + func testDoesNotCaptureHTTPClientErrorIfNotStatusCodeRange() { + let sut = fixture.getSut() + let task = createDataTask() + task.setResponse(createResponse(code: 200)) + + sut.urlSessionTask(task, setState: .completed) + + XCTAssertNil(fixture.hub.capturedEventsWithScopes.first) + } + + func testDoesNotCaptureHTTPClientErrorIfNotTarget() { + fixture.options.failedRequestTargets = ["www.example.com"] + + let sut = fixture.getSut() + let task = createDataTask() + task.setResponse(createResponse(code: 500)) + + sut.urlSessionTask(task, setState: .completed) + + XCTAssertNil(fixture.hub.capturedEventsWithScopes.first) } func setTaskState(_ task: URLSessionTaskMock, state: URLSessionTask.State) { diff --git a/Tests/SentryTests/Protocol/SentryEventTests.swift b/Tests/SentryTests/Protocol/SentryEventTests.swift index 00980fddffc..fea897230aa 100644 --- a/Tests/SentryTests/Protocol/SentryEventTests.swift +++ b/Tests/SentryTests/Protocol/SentryEventTests.swift @@ -41,6 +41,7 @@ class SentryEventTests: XCTestCase { XCTAssertNotNil(actual["user"] as? [String: Any]) XCTAssertEqual(TestData.event.modules, actual["modules"] as? [String: String]) XCTAssertNotNil(actual["stacktrace"] as? [String: Any]) + XCTAssertNotNil(actual["request"] as? [String: Any]) let crumbs = actual["breadcrumbs"] as? [[String: Any]] XCTAssertNotNil(crumbs) diff --git a/Tests/SentryTests/Protocol/SentryRequestTests.swift b/Tests/SentryTests/Protocol/SentryRequestTests.swift new file mode 100644 index 00000000000..afc7b0f38fd --- /dev/null +++ b/Tests/SentryTests/Protocol/SentryRequestTests.swift @@ -0,0 +1,36 @@ +import XCTest + +class SentryRequestTests: XCTestCase { + func testSerialize() { + let request = TestData.request + + let actual = request.serialize() + + XCTAssertEqual(request.url, actual["url"] as? String) + XCTAssertEqual(request.queryString, actual["query_string"] as? String) + XCTAssertEqual(request.fragment, actual["fragment"] as? String) + XCTAssertEqual(request.cookies, actual["cookies"] as? String) + XCTAssertEqual(request.method, actual["method"] as? String) + XCTAssertEqual(request.bodySize, actual["body_size"] as? NSNumber) + + XCTAssertEqual(request.headers, actual["headers"] as? Dictionary) + } + + func testNoHeaders() { + let request = TestData.request + request.headers = nil + + let actual = request.serialize() + + XCTAssertNil(actual["headers"]) + } + + func testNoBodySize() { + let request = TestData.request + request.bodySize = 0 + + let actual = request.serialize() + + XCTAssertNil(actual["body_size"]) + } +} diff --git a/Tests/SentryTests/Protocol/SentryStacktraceTests.swift b/Tests/SentryTests/Protocol/SentryStacktraceTests.swift index c288982fa10..f0f84452fa0 100644 --- a/Tests/SentryTests/Protocol/SentryStacktraceTests.swift +++ b/Tests/SentryTests/Protocol/SentryStacktraceTests.swift @@ -14,6 +14,7 @@ class SentryStacktraceTests: XCTestCase { let frames = actual["frames"] as? [Any] XCTAssertEqual(1, frames?.count) XCTAssertEqual(["register": "one"], actual["registers"] as? [String: String]) + XCTAssertEqual(stacktrace.snapshot, actual["snapshot"] as? NSNumber) } func testSerializeNoRegisters() { diff --git a/Tests/SentryTests/Protocol/TestData.swift b/Tests/SentryTests/Protocol/TestData.swift index 68cc36a0648..64177f526d2 100644 --- a/Tests/SentryTests/Protocol/TestData.swift +++ b/Tests/SentryTests/Protocol/TestData.swift @@ -50,6 +50,7 @@ class TestData { event.transaction = "transaction" event.type = "type" event.user = user + event.request = request return event } @@ -131,6 +132,7 @@ class TestData { static var stacktrace: Stacktrace { let stacktrace = Stacktrace(frames: [frame], registers: ["register": "one"]) + stacktrace.snapshot = true return stacktrace } @@ -219,6 +221,19 @@ class TestData { scope.setContext(value: TestData.context["context"]!, key: "context") } + static var request: SentryRequest { + let request = SentryRequest() + request.url = "https://sentry.io" + request.fragment = "fragment" + request.bodySize = 10 + request.queryString = "query" + request.cookies = "cookies" + request.method = "GET" + request.headers = ["header": "value"] + + return request + } + static func getAppStartMeasurement(type: SentryAppStartType, appStartTimestamp: Date = TestData.timestamp) -> SentryAppStartMeasurement { let appStartDuration = 0.5 let main = appStartTimestamp.addingTimeInterval(0.15) diff --git a/Tests/SentryTests/SentryHttpStatusCodeRangeTests.swift b/Tests/SentryTests/SentryHttpStatusCodeRangeTests.swift new file mode 100644 index 00000000000..1cb454f1fa0 --- /dev/null +++ b/Tests/SentryTests/SentryHttpStatusCodeRangeTests.swift @@ -0,0 +1,46 @@ +import XCTest + +class SentryHttpStatusCodeRangeTests: XCTestCase { + + func testWithinRange() { + let range = HttpStatusCodeRange(min: 500, max: 599) + + XCTAssertTrue(range.is(inRange: 550)) + } + + func testMinWithinRange() { + let range = HttpStatusCodeRange(min: 500, max: 599) + + XCTAssertTrue(range.is(inRange: 500)) + } + + func testLowerMinNotWithinRange() { + let range = HttpStatusCodeRange(min: 500, max: 599) + + XCTAssertFalse(range.is(inRange: 499)) + } + + func testMaxWithinRange() { + let range = HttpStatusCodeRange(min: 500, max: 599) + + XCTAssertTrue(range.is(inRange: 599)) + } + + func testHigherMaxNotWithinRange() { + let range = HttpStatusCodeRange(min: 500, max: 599) + + XCTAssertFalse(range.is(inRange: 600)) + } + + func testStatusCodeWithinRange() { + let range = HttpStatusCodeRange(statusCode: 500) + + XCTAssertTrue(range.is(inRange: 500)) + } + + func testStatusCodeNotWithinRange() { + let range = HttpStatusCodeRange(statusCode: 500) + + XCTAssertFalse(range.is(inRange: 200)) + } +} diff --git a/Tests/SentryTests/SentryOptionsTest.m b/Tests/SentryTests/SentryOptionsTest.m index dbb350c34a0..109dab8a7e7 100644 --- a/Tests/SentryTests/SentryOptionsTest.m +++ b/Tests/SentryTests/SentryOptionsTest.m @@ -292,6 +292,40 @@ - (void)testTracePropagationTargetsInvalidInstanceDoesntCrash 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:NO]; +} + +- (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" }]; @@ -486,7 +520,9 @@ - (void)testNSNull_SetsDefaultValue @"urlSessionDelegate" : [NSNull null], @"enableSwizzling" : [NSNull null], @"enableIOTracking" : [NSNull null], - @"sdk" : [NSNull null] + @"sdk" : [NSNull null], + @"enableCaptureFailedRequests" : [NSNull null], + @"failedRequestStatusCodes" : [NSNull null], } didFailWithError:nil]; @@ -534,8 +570,19 @@ - (void)assertDefaultValues:(SentryOptions *)options XCTAssertEqual(YES, options.enableSwizzling); XCTAssertEqual(NO, options.enableFileIOTracking); XCTAssertEqual(YES, options.enableAutoBreadcrumbTracking); - NSRegularExpression *regex = options.tracePropagationTargets[0]; - XCTAssertTrue([regex.pattern isEqualToString:@".*"]); + + NSRegularExpression *regexTrace = options.tracePropagationTargets[0]; + XCTAssertTrue([regexTrace.pattern isEqualToString:@".*"]); + + NSRegularExpression *regexRequests = options.failedRequestTargets[0]; + XCTAssertTrue([regexRequests.pattern isEqualToString:@".*"]); + + XCTAssertEqual(NO, options.enableCaptureFailedRequests); + + SentryHttpStatusCodeRange *range = options.failedRequestStatusCodes[0]; + XCTAssertEqual(500, range.min); + XCTAssertEqual(599, range.max); + #if SENTRY_TARGET_PROFILING_SUPPORTED # pragma clang diagnostic push # pragma clang diagnostic ignored "-Wdeprecated-declarations" diff --git a/Tests/SentryTests/SentryTests-Bridging-Header.h b/Tests/SentryTests/SentryTests-Bridging-Header.h index 57a91cc6017..d5b4b21e7ac 100644 --- a/Tests/SentryTests/SentryTests-Bridging-Header.h +++ b/Tests/SentryTests/SentryTests-Bridging-Header.h @@ -82,6 +82,7 @@ #import "SentryFramesTrackingIntegration.h" #import "SentryGlobalEventProcessor.h" #import "SentryHttpDateParser.h" +#import "SentryHttpStatusCodeRange+Private.h" #import "SentryHttpTransport.h" #import "SentryHub+Private.h" #import "SentryHub+TestInit.h" diff --git a/develop-docs/README.md b/develop-docs/README.md index b20ae26b187..d3ddb94d7f8 100644 --- a/develop-docs/README.md +++ b/develop-docs/README.md @@ -114,6 +114,13 @@ Related links: - https://github.com/getsentry/sentry-cocoa/pull/1751 +### Custom SentryHttpStatusCodeRange type instead of NSRange + +Date: October 24th 2022 +Contributors: @marandaneto, @brustolin and @philipphofmann + +We decided not to use the `NSRange` type for the `failedRequestStatusCodes` property of the `SentryNetworkTracker` class because it's not compatible with the specification, which requires the type to be a range of `from` -> `to` integers. The `NSRange` type is a range of `location` -> `length` integers. We decided to use a custom type instead of `NSRange` to avoid confusion. The custom type is called `SentryHttpStatusCodeRange`. + ### Manually installing iOS 12 simulators Date: October 21st 2022 diff --git a/test-server/Sources/App/routes.swift b/test-server/Sources/App/routes.swift index f2d7f33a318..e57bf07590a 100644 --- a/test-server/Sources/App/routes.swift +++ b/test-server/Sources/App/routes.swift @@ -22,4 +22,8 @@ func routes(_ app: Application) throws { return "(NO-HEADER)" } + + app.get("http-client-error") { _ -> String in + throw Abort(.badRequest) + } }