Source/TNLAttemptMetrics.m (438 lines of code) (raw):

// // TNLAttemptMetrics.m // TwitterNetworkLayer // // Created on 1/15/15. // Copyright © 2020 Twitter. All rights reserved. // #import "NSCoder+TNLAdditions.h" #import "NSURLSessionTaskMetrics+TNLAdditions.h" #import "TNL_Project.h" #import "TNLAttemptMetaData_Project.h" #import "TNLAttemptMetrics_Project.h" #import "TNLCommunicationAgent_Project.h" #import "TNLGlobalConfiguration.h" #import "TNLTiming.h" NS_ASSUME_NONNULL_BEGIN TNLStaticAssert(TNLAttemptCompleteDispositionCount == TNLAttemptTypeCount, ATTEMPT_TYPE_COUNT_DOESNT_MATCH_ATTEMPT_COMPLETE_DISPOSITION_COUNT); @implementation TNLAttemptMetrics { BOOL _final; } + (BOOL)supportsSecureCoding { return YES; } - (instancetype)init { [self doesNotRecognizeSelector:_cmd]; abort(); } - (instancetype)initWithType:(TNLAttemptType)type startDate:(NSDate *)startDate startMachTime:(uint64_t)startMachTime endDate:(nullable NSDate *)endDate endMachTime:(uint64_t)endMachTime metaData:(nullable TNLAttemptMetaData *)metaData URLRequest:(NSURLRequest *)request URLResponse:(nullable NSHTTPURLResponse *)response operationError:(nullable NSError *)error { int64_t attemptId = 0; arc4random_buf(&attemptId, sizeof(int64_t)); return [self initWithAttemptId:attemptId type:type startDate:startDate startMachTime:startMachTime endDate:endDate endMachTime:endMachTime metaData:metaData URLRequest:request URLResponse:response operationError:error]; } - (instancetype)initWithAttemptId:(int64_t)attemptId type:(TNLAttemptType)type startDate:(NSDate *)startDate startMachTime:(uint64_t)startMachTime endDate:(nullable NSDate *)endDate endMachTime:(uint64_t)endMachTime metaData:(nullable TNLAttemptMetaData *)metaData URLRequest:(NSURLRequest *)request URLResponse:(nullable NSHTTPURLResponse *)response operationError:(nullable NSError *)error { if (self = [super init]) { #if !TARGET_OS_WATCH _reachabilityStatus = TNLNetworkReachabilityUndetermined; _captivePortalStatus = TNLCaptivePortalStatusUndetermined; #endif _attemptId = attemptId; _attemptType = type; _startDate = startDate; _startMachTime = startMachTime; _endDate = endDate; _endMachTime = endMachTime; _metaData = metaData; _URLRequest = [request copy]; _URLResponse = response; _operationError = error; } return self; } - (nullable instancetype)initWithCoder:(NSCoder *)aDecoder { const int64_t attemptId = [aDecoder decodeInt64ForKey:@"attemptId"]; const TNLAttemptType type = [aDecoder decodeIntegerForKey:@"attemptType"]; NSDate *startDate = [aDecoder decodeObjectOfClass:[NSDate class] forKey:@"startDate"] ?: [NSDate dateWithTimeIntervalSince1970:0]; const uint64_t startMachTime = (uint64_t)[aDecoder decodeInt64ForKey:@"startMachTime"]; NSDate *endDate = [aDecoder decodeObjectOfClass:[NSDate class] forKey:@"endDate"]; const uint64_t endMachTime = (uint64_t)[aDecoder decodeInt64ForKey:@"endMachTime"]; TNLAttemptMetaData *metaData = [aDecoder decodeObjectOfClass:[TNLAttemptMetaData class] forKey:@"metaData"]; NSURLRequest *request = [aDecoder decodeObjectOfClass:[NSURLRequest class] forKey:@"URLRequest"]; NSHTTPURLResponse *response = [aDecoder decodeObjectOfClass:[NSHTTPURLResponse class] forKey:@"URLResponse"]; NSError *error = [aDecoder decodeObjectOfClass:[NSError class] forKey:@"operationError"]; self = [self initWithAttemptId:attemptId type:type startDate:startDate startMachTime:startMachTime endDate:endDate endMachTime:endMachTime metaData:metaData URLRequest:request URLResponse:response operationError:error]; if (self) { _final = YES; _APIErrors = [[aDecoder tnl_decodeArrayOfItemsOfClass:[NSError class] forKey:@"APIErrors"] copy]; _responseBodyParseError = [aDecoder decodeObjectOfClass:[NSError class] forKey:@"parseError"]; #if !TARGET_OS_WATCH NSNumber *number; number = [aDecoder decodeObjectOfClass:[NSNumber class] forKey:@"reachabilityStatus"]; _reachabilityStatus = (number) ? [number integerValue] : TNLNetworkReachabilityUndetermined; number = [aDecoder decodeObjectOfClass:[NSNumber class] forKey:@"reachabilityFlags"]; _reachabilityFlags = [number unsignedIntValue]; number = [aDecoder decodeObjectOfClass:[NSNumber class] forKey:@"captivePortalStatus"]; _captivePortalStatus = (number) ? [number integerValue] : TNLCaptivePortalStatusUndetermined; _WWANRadioAccessTechnology = [aDecoder decodeObjectOfClass:[NSString class] forKey:@"WWANRadioAccessTechnology"]; #endif #if TARGET_OS_IOS && !TARGET_OS_MACCATALYST _carrierInfo = TNLCarrierInfoFromDictionary([aDecoder decodeObjectOfClass:[NSDictionary class] forKey:@"carrierInfo"]); #endif } return self; } - (void)encodeWithCoder:(NSCoder *)aCoder { [aCoder encodeInt64:_attemptId forKey:@"attemptId"]; [aCoder encodeInteger:_attemptType forKey:@"attemptType"]; [aCoder encodeObject:_startDate forKey:@"startDate"]; [aCoder encodeInt64:(int64_t)_startMachTime forKey:@"startMachTime"]; [aCoder encodeObject:_endDate forKey:@"endDate"]; [aCoder encodeInt64:(int64_t)_endMachTime forKey:@"endMachTime"]; [aCoder encodeObject:_metaData forKey:@"metaData"]; [aCoder encodeObject:_URLRequest forKey:@"URLRequest"]; [aCoder encodeObject:_URLResponse forKey:@"URLResponse"]; [aCoder encodeObject:TNLErrorToSecureCodingError(_operationError) forKey:@"operationError"]; [aCoder encodeObject:TNLErrorToSecureCodingError(_responseBodyParseError) forKey:@"parseError"]; NSMutableArray<NSError *> *apiErrors = (_APIErrors) ? [[NSMutableArray alloc] initWithCapacity:_APIErrors.count] : nil; for (NSError *apiError in _APIErrors) { [apiErrors addObject:TNLErrorToSecureCodingError(apiError)]; } [aCoder encodeObject:[apiErrors copy] forKey:@"APIErrors"]; #if !TARGET_OS_WATCH [aCoder encodeObject:@(_reachabilityStatus) forKey:@"reachabilityStatus"]; [aCoder encodeObject:@(_reachabilityFlags) forKey:@"reachabilityFlags"]; [aCoder encodeObject:@(_captivePortalStatus) forKey:@"captivePortalStatus"]; [aCoder encodeObject:_WWANRadioAccessTechnology forKey:@"WWANRadioAccessTechnology"]; #endif #if TARGET_OS_IOS && !TARGET_OS_MACCATALYST [aCoder encodeObject:TNLCarrierInfoToDictionary(_carrierInfo) forKey:@"carrierInfo"]; #endif } - (void)finalizeMetrics { if (_final) { return; } _final = YES; [_metaData finalizeMetaData]; } #if !TARGET_OS_WATCH - (void)setCommunicationMetricsWithAgent:(nullable TNLCommunicationAgent *)agent { if (!agent) { return; } if (_reachabilityStatus != TNLNetworkReachabilityUndetermined) { return; } _reachabilityStatus = agent.currentReachabilityStatus; _reachabilityFlags = agent.currentReachabilityFlags; _WWANRadioAccessTechnology = [agent.currentWWANRadioAccessTechnology copy]; _carrierInfo = agent.currentCarrierInfo; _captivePortalStatus = agent.currentCaptivePortalStatus; } #endif - (NSTimeInterval)duration { const NSTimeInterval duration = [(_endDate ?: [NSDate date]) timeIntervalSinceDate:_startDate]; TNLAssertMessage(duration >= 0, @"-[%@ %@] is negative! %f", NSStringFromClass([self class]), NSStringFromSelector(_cmd), duration); return duration; } - (NSUInteger)hash { return (NSUInteger)_startDate.hash; } - (BOOL)isEqual:(id)object { if (self == object) { return YES; } TNLAttemptMetrics *other = object; if (![other isKindOfClass:[TNLAttemptMetrics class]]) { return NO; } if (self.attemptType != other.attemptType) { return NO; } if (fabs(self.duration - other.duration) > kTNLTimeEpsilon) { return NO; } if (self.metaData != other.metaData) { if (![self.metaData isEqual:other.metaData]) { return NO; } } if (!TNLSecureCodingErrorsAreEqual(self.operationError, other.operationError)) { return NO; } if (self.URLResponse != other.URLResponse) { if (self.URLResponse.statusCode != other.URLResponse.statusCode) { return NO; } if (![self.URLResponse.URL isEqual:other.URLResponse.URL]) { return NO; } } if (self.URLRequest != other.URLRequest) { if (![self.URLRequest isEqual:other.URLRequest]) { return NO; } } #if !TARGET_OS_WATCH if (self.reachabilityFlags != other.reachabilityFlags) { return NO; } if (self.reachabilityStatus != other.reachabilityStatus) { return NO; } if (self.WWANRadioAccessTechnology != other.WWANRadioAccessTechnology) { if (![self.WWANRadioAccessTechnology isEqualToString:other.WWANRadioAccessTechnology]) { return NO; } } #endif // !WATCH if (self.APIErrors != other.APIErrors) { if (self.APIErrors.count != other.APIErrors.count) { return NO; } // just compare error codes and domains for (NSUInteger i = 0; i < self.APIErrors.count; i++) { if (!TNLSecureCodingErrorsAreEqual(self.APIErrors[i], other.APIErrors[i])) { return NO; } } } if (!TNLSecureCodingErrorsAreEqual(self.responseBodyParseError, other.responseBodyParseError)) { return NO; } #if !TARGET_OS_WATCH id<TNLCarrierInfo> carrierInfo = self.carrierInfo; id<TNLCarrierInfo> otherCarrierInfo = other.carrierInfo; if (carrierInfo != otherCarrierInfo) { if (!carrierInfo || !otherCarrierInfo) { return NO; } if (carrierInfo.allowsVOIP != otherCarrierInfo.allowsVOIP) { return NO; } if (carrierInfo.mobileCountryCode != otherCarrierInfo.mobileCountryCode) { if (![carrierInfo.mobileCountryCode isEqualToString:otherCarrierInfo.mobileCountryCode]) { return NO; } } if (carrierInfo.mobileNetworkCode != otherCarrierInfo.mobileNetworkCode) { if (![carrierInfo.mobileNetworkCode isEqualToString:otherCarrierInfo.mobileNetworkCode]) { return NO; } } if (carrierInfo.isoCountryCode != otherCarrierInfo.isoCountryCode) { if (![carrierInfo.isoCountryCode isEqualToString:otherCarrierInfo.isoCountryCode]) { return NO; } } } #endif // !WATCH return YES; } - (void)setMetaData:(nullable TNLAttemptMetaData *)metaData { if (_final && _metaData) { return; } _metaData = metaData; } - (void)setEndDate:(nonnull NSDate *)endDate machTime:(uint64_t)time { if (_final && _endMachTime) { return; } _endMachTime = time; _endDate = endDate; #if !TARGET_OS_WATCH [self setCommunicationMetricsWithAgent:[TNLGlobalConfiguration sharedInstance].metricProvidingCommunicationAgent]; #endif } - (void)setOperationError:(nullable NSError *)error { if (_final && _operationError) { return; } _operationError = error; } - (void)setURLResponse:(nullable NSHTTPURLResponse *)response { if (_final && _URLResponse) { return; } _URLResponse = response; } - (void)setTaskTransactionMetrics:(nullable NSURLSessionTaskTransactionMetrics *)metrics { if (_final && _taskTransactionMetrics) { return; } _taskTransactionMetrics = metrics; } - (void)setResponseBodyParseError:(nullable NSError *)responseBodyParseError { if (_final) { return; } _responseBodyParseError = responseBodyParseError; } - (void)setAPIErrors:(nullable NSArray<NSError *> *)APIErrors { if (_final) { return; } TNLAssert(0 == APIErrors.count || [APIErrors[0] isKindOfClass:[NSError class]]); _APIErrors = [APIErrors copy]; } - (void)updateRequest:(NSURLRequest *)request { if (_final) { return; } TNLAssert(request); TNLAssert([_URLRequest.URL isEqual:request.URL]); _URLRequest = [request copy]; } #pragma mark NSCopying - (id)copyWithZone:(nullable NSZone *)zone { TNLAttemptMetaData *metaData = _metaData ? [[TNLAttemptMetaData allocWithZone:zone] initWithMetaDataDictionary:_metaData.metaDataDictionary] : nil; TNLAttemptMetrics *dupeSubmetric = [[TNLAttemptMetrics allocWithZone:zone] initWithAttemptId:_attemptId type:_attemptType startDate:_startDate startMachTime:_startMachTime endDate:_endDate endMachTime:_endMachTime metaData:metaData URLRequest:_URLRequest URLResponse:_URLResponse operationError:_operationError]; dupeSubmetric->_taskTransactionMetrics = _taskTransactionMetrics; dupeSubmetric->_APIErrors = _APIErrors; dupeSubmetric->_responseBodyParseError = _responseBodyParseError; #if !TARGET_OS_WATCH dupeSubmetric->_reachabilityStatus = _reachabilityStatus; dupeSubmetric->_reachabilityFlags = _reachabilityFlags; dupeSubmetric->_WWANRadioAccessTechnology = [_WWANRadioAccessTechnology copyWithZone:zone]; dupeSubmetric->_carrierInfo = _carrierInfo; dupeSubmetric->_captivePortalStatus = _captivePortalStatus; #endif dupeSubmetric->_final = NO; return dupeSubmetric; } #pragma mark Description - (NSDictionary *)dictionaryDescription:(BOOL)verbose { NSMutableDictionary *attemptDict = [NSMutableDictionary dictionary]; attemptDict[@"attemptInfo"] = @{ @"id" : @(_attemptId), @"type" : TNLAttemptTypeToString(_attemptType), @"duration" : @(self.duration) }; #define UP_D(d, key, val) \ ({ \ id value__ = (val); \ if (value__) { \ (d)[(key)] = value__; \ } \ }) NSMutableDictionary *other = [[NSMutableDictionary alloc] init]; attemptDict[@"other"] = other; UP_D(other, @"error", self.operationError.description); if (_URLResponse) { other[@"statusCode"] = @(_URLResponse.statusCode); } if (verbose) { UP_D(other, @"timeStart", TNLHTTPDateToString(self.startDate, TNLHTTPDateFormatAuto)); UP_D(other, @"timeEnd", TNLHTTPDateToString(self.endDate, TNLHTTPDateFormatAuto)); UP_D(other, @"apiErrors", self.APIErrors.firstObject.description); UP_D(other, @"parseErrors", self.responseBodyParseError.description); #if !TARGET_OS_WATCH UP_D(other, @"reachStatus", TNLNetworkReachabilityStatusToString(_reachabilityStatus)); UP_D(other, @"reachFlags", TNLDebugStringFromNetworkReachabilityFlags(_reachabilityFlags)); UP_D(other, @"radio", self.WWANRadioAccessTechnology); UP_D(other, @"captivePortalStatus", TNLCaptivePortalStatusToString(_captivePortalStatus)); UP_D(other, @"carrierInfo", self.carrierInfo ? TNLCarrierInfoToDictionaryDescription(self.carrierInfo) : nil); #endif if (_URLRequest) { attemptDict[@"zRequest"] = @{ @"method" : _URLRequest.HTTPMethod ?: @"GET", @"URL" : _URLRequest.URL.absoluteURL.absoluteString, @"headers" : _URLRequest.allHTTPHeaderFields ?: @{}, @"bodyLength" : _URLRequest.HTTPBody ? @(_URLRequest.HTTPBody.length) : [NSNull null], }; } if (_URLResponse) { attemptDict[@"zResponse"] = @{ @"statusCode" : @(_URLResponse.statusCode), @"URL" : _URLResponse.URL.absoluteString ?: [NSNull null], @"headers" : _URLResponse.allHeaderFields ?: @{} }; } if (_metaData) { attemptDict[@"metaData"] = _metaData.dictionaryDescription; } NSDictionary *taskMetrics = self.taskTransactionMetrics.tnl_dictionaryDescription; if (taskMetrics) { attemptDict[@"transactionMetrics"] = taskMetrics; } } #undef UP_D return attemptDict; } - (NSString *)description { NSMutableString *string = [NSMutableString string]; [string appendFormat:@"<%@ %p: type=%@, duration=%.2fs", NSStringFromClass([self class]), self, TNLAttemptTypeToString(self.attemptType), self.duration]; if (self.URLResponse) { [string appendFormat:@", HTTP=%ld", (long)self.URLResponse.statusCode]; } if (self.operationError) { [string appendFormat:@", error=%@.%ld", self.operationError.domain, (long)self.operationError.code]; } if (self.metaData) { [string appendFormat:@", metaData.class=%@", NSStringFromClass([self.metaData class])]; } [string appendString:@">"]; return string; } @end NS_ASSUME_NONNULL_END