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) SentryRequest *_Nullable request; + /** * Init an SentryEvent will set all needed fields by default * @return SentryEvent diff --git a/Sources/Sentry/Public/SentryOptions.h b/Sources/Sentry/Public/SentryOptions.h index 37229734120..a99ef040610 100644 --- a/Sources/Sentry/Public/SentryOptions.h +++ b/Sources/Sentry/Public/SentryOptions.h @@ -414,6 +414,11 @@ NS_SWIFT_NAME(Options) */ @property (nonatomic, retain) NSArray *tracePropagationTargets; +/** + * When enabled, the SDK captures HTTP Client errors. Default value is NO. + */ +@property (nonatomic, assign) BOOL enableCaptureFailedRequests; + @end NS_ASSUME_NONNULL_END diff --git a/Sources/Sentry/Public/SentryStacktrace.h b/Sources/Sentry/Public/SentryStacktrace.h index e8c67d83f41..43e0e762d1a 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) NSNumber *_Nullable 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/SentryNetworkTracker.m b/Sources/Sentry/SentryNetworkTracker.m index b3ef917b83c..9a8a9d4fde9 100644 --- a/Sources/Sentry/SentryNetworkTracker.m +++ b/Sources/Sentry/SentryNetworkTracker.m @@ -1,11 +1,19 @@ #import "SentryNetworkTracker.h" #import "SentryBaggage.h" #import "SentryBreadcrumb.h" +#import "SentryClient+Private.h" +#import "SentryEvent.h" +#import "SentryException.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 +24,7 @@ @property (nonatomic, assign) BOOL isNetworkTrackingEnabled; @property (nonatomic, assign) BOOL isNetworkBreadcrumbEnabled; +@property (nonatomic, assign) BOOL isCaptureFailedRequests; @end @@ -34,6 +43,7 @@ - (instancetype)init if (self = [super init]) { _isNetworkTrackingEnabled = NO; _isNetworkBreadcrumbEnabled = NO; + _isCaptureFailedRequests = NO; } return self; } @@ -52,11 +62,19 @@ - (void)enableNetworkBreadcrumbs } } +- (void)enableCaptureFailedRequests +{ + @synchronized(self) { + _isCaptureFailedRequests = YES; + } +} + - (void)disable { @synchronized(self) { _isNetworkBreadcrumbEnabled = NO; _isNetworkTrackingEnabled = NO; + _isCaptureFailedRequests = NO; } } @@ -206,7 +224,8 @@ - (void)urlSessionTaskResume:(NSURLSessionTask *)sessionTask - (void)urlSessionTask:(NSURLSessionTask *)sessionTask setState:(NSURLSessionTaskState)newState { - if (!self.isNetworkTrackingEnabled && !self.isNetworkBreadcrumbEnabled) { + if (!self.isNetworkTrackingEnabled && !self.isNetworkBreadcrumbEnabled + && !self.isCaptureFailedRequests) { return; } @@ -239,6 +258,8 @@ - (void)urlSessionTask:(NSURLSessionTask *)sessionTask setState:(NSURLSessionTas } if (sessionTask.state == NSURLSessionTaskStateRunning) { + [self captureEvent:sessionTask]; + [self addBreadcrumbForSessionTask:sessionTask]; NSInteger responseStatusCode = [self urlResponseStatusCode:sessionTask.response]; @@ -265,6 +286,85 @@ - (void)urlSessionTask:(NSURLSessionTask *)sessionTask setState:(NSURLSessionTas SENTRY_LOG_DEBUG(@"SentryNetworkTracker finished HTTP span for sessionTask"); } +- (void)captureEvent:(NSURLSessionTask *)sessionTask +{ + NSInteger responseStatusCode = [self urlResponseStatusCode:sessionTask.response]; + + // TODO: check the string contains and regex + if (!self.isCaptureFailedRequests) { + return; + } + + // TODO: check the range + if (responseStatusCode == 201) { + return; + } + + NSString *message = [NSString + stringWithFormat:@"HTTP Client Error with status code: %li", (long)(responseStatusCode)]; + + SentryEvent *event = [[SentryEvent alloc] initWithLevel:kSentryLevelError]; + + SentryThreadInspector *threadInspector = SentrySDK.currentHub.getClient.threadInspector; + NSArray *threads = [threadInspector getCurrentThreadsWithStackTrace]; + + SentryException *sentryException = [[SentryException alloc] initWithValue:message + type:@"HTTP-ClientError"]; + sentryException.mechanism = [[SentryMechanism alloc] initWithType:@"SentryNetworkTrackingIntegration"]; + + if (threads.count > 0) { + SentryStacktrace *sentryStacktrace = [threads[0] stacktrace]; + sentryStacktrace.snapshot = @(YES); + + sentryException.stacktrace = sentryStacktrace; + // TODO: do I need this? + // [threads enumerateObjectsUsingBlock:^(SentryThread *_Nonnull obj, NSUInteger idx, + // BOOL *_Nonnull stop) { obj.current = [NSNumber numberWithBool:idx == 0]; }]; + } + + SentryRequest *request = [[SentryRequest alloc] init]; + + NSURLRequest *myRequest = (NSURLRequest *)sessionTask.currentRequest; + + NSURL *url = [[sessionTask currentRequest] URL]; + request.url = url.absoluteString; + + request.fragment = url.fragment; + request.queryString = url.query; + request.method = myRequest.HTTPMethod; + if (sessionTask.countOfBytesSent != 0) { + 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; + + NSHTTPURLResponse *myResponse = (NSHTTPURLResponse *)sessionTask.response; + + NSMutableDictionary *context = [[NSMutableDictionary alloc] init];; + NSMutableDictionary *response = [[NSMutableDictionary alloc] init]; + + [response setValue:[NSNumber numberWithLongLong:responseStatusCode] forKey:@"status_code"]; + if (nil != myResponse.allHeaderFields) { + NSDictionary *headers = myResponse.allHeaderFields.copy; + [response setValue:headers forKey:@"headers"]; + [response setValue:headers[@"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]; +} + - (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..8ee387b9ca1 100644 --- a/Sources/Sentry/SentryOptions.m +++ b/Sources/Sentry/SentryOptions.m @@ -60,6 +60,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; @@ -270,6 +271,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; }]; diff --git a/Sources/Sentry/SentryRequest.h b/Sources/Sentry/SentryRequest.h new file mode 100644 index 00000000000..9178f289e15 --- /dev/null +++ b/Sources/Sentry/SentryRequest.h @@ -0,0 +1,50 @@ +#import "SentryDefines.h" +#import "SentrySerializable.h" + +NS_ASSUME_NONNULL_BEGIN + +NS_SWIFT_NAME(Request) +@interface SentryRequest : NSObject + +// TODO: data, env + +/** + * Optional: HTTP response body size. + */ +@property (atomic, copy) NSNumber *_Nullable bodySize; + +/** + * Optional: The cookie values. + */ +@property (atomic, copy) NSString *_Nullable cookies; + +/** + * Optional: A dictionary of submitted headers. + */ +@property (nonatomic, strong) NSDictionary *_Nullable headers; + +/** + * Optional: The fragment of the request URL. + */ +@property (atomic, copy) NSString *_Nullable fragment; + +/** + * Optional: HTTP request method. + */ +@property (atomic, copy) NSString *_Nullable method; + +/** + * Optional: The query string component of the URL. + */ +@property (atomic, copy) NSString *_Nullable queryString; + +/** + * Optional: The URL of the request if available. + */ +@property (atomic, copy) NSString *_Nullable url; + +- (instancetype)init; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Sources/Sentry/SentryRequest.m b/Sources/Sentry/SentryRequest.m new file mode 100644 index 00000000000..42edb8ce533 --- /dev/null +++ b/Sources/Sentry/SentryRequest.m @@ -0,0 +1,51 @@ +#import "SentryRequest.h" +#import "NSDictionary+SentrySanitize.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation SentryRequest + +- (instancetype)init +{ + return [super init]; +} + +- (NSDictionary *)serialize +{ + NSMutableDictionary *serializedData = [[NSMutableDictionary alloc] init]; + + @synchronized(self) { + [serializedData setValue:self.bodySize forKey:@"body_size"]; + [serializedData setValue:self.cookies forKey:@"cookies"]; + [serializedData setValue:self.fragment forKey:@"fragment"]; + [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; +} + +- (id)copyWithZone:(nullable NSZone *)zone +{ + SentryRequest *copy = [[SentryRequest allocWithZone:zone] init]; + + @synchronized(self) { + if (copy != nil) { + copy.bodySize = self.bodySize; + copy.cookies = self.cookies; + copy.fragment = self.fragment; + copy.method = self.method; + copy.queryString = self.queryString; + copy.url = self.url; + copy.headers = self.headers.copy; + } + } + + return copy; +} + +@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/SentryNetworkTracker.h b/Sources/Sentry/include/SentryNetworkTracker.h index c1758d5bcd5..204a80015d7 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; +- (void)enableCaptureFailedRequests; - (BOOL)addHeadersForRequestWithURL:(NSURL *)URL; - (void)disable; @property (nonatomic, readonly) BOOL isNetworkTrackingEnabled; @property (nonatomic, readonly) BOOL isNetworkBreadcrumbEnabled; +@property (nonatomic, readonly) BOOL isCaptureFailedRequests; @end