FPRNetworkTrace.m 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499
  1. // Copyright 2020 Google LLC
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. #import "FirebasePerformance/Sources/Instrumentation/FPRNetworkTrace.h"
  15. #import "FirebasePerformance/Sources/Instrumentation/FPRNetworkTrace+Private.h"
  16. #import "FirebasePerformance/Sources/AppActivity/FPRSessionManager.h"
  17. #import "FirebasePerformance/Sources/Common/FPRConstants.h"
  18. #import "FirebasePerformance/Sources/Common/FPRDiagnostics.h"
  19. #import "FirebasePerformance/Sources/Configurations/FPRConfigurations.h"
  20. #import "FirebasePerformance/Sources/FIRPerformance_Private.h"
  21. #import "FirebasePerformance/Sources/FPRClient.h"
  22. #import "FirebasePerformance/Sources/FPRConsoleLogger.h"
  23. #import "FirebasePerformance/Sources/FPRDataUtils.h"
  24. #import "FirebasePerformance/Sources/FPRURLFilter.h"
  25. #import "FirebasePerformance/Sources/Gauges/FPRGaugeManager.h"
  26. #import <GoogleUtilities/GULObjectSwizzler.h>
  27. NSString *const kFPRNetworkTracePropertyName = @"fpr_networkTrace";
  28. @interface FPRNetworkTrace ()
  29. @property(nonatomic, readwrite) NSURLRequest *URLRequest;
  30. @property(nonatomic, readwrite, nullable) NSError *responseError;
  31. /** State to know if the trace has started. */
  32. @property(nonatomic) BOOL traceStarted;
  33. /** State to know if the trace has completed. */
  34. @property(nonatomic) BOOL traceCompleted;
  35. /** Background activity tracker to know the background state of the trace. */
  36. @property(nonatomic) FPRTraceBackgroundActivityTracker *backgroundActivityTracker;
  37. /** Custom attribute managed internally. */
  38. @property(nonatomic) NSMutableDictionary<NSString *, NSString *> *customAttributes;
  39. /** @brief Serial queue to manage the updation of session Ids. */
  40. @property(nonatomic, readwrite) dispatch_queue_t sessionIdSerialQueue;
  41. /**
  42. * Updates the current trace with the current session details.
  43. * @param sessionDetails Updated session details of the currently active session.
  44. */
  45. - (void)updateTraceWithCurrentSession:(FPRSessionDetails *)sessionDetails;
  46. @end
  47. @implementation FPRNetworkTrace {
  48. /**
  49. * @brief Object containing different states of the network request. Stores the information about
  50. * the state of a network request (defined in FPRNetworkTraceCheckpointState) and the time at
  51. * which the event happened.
  52. */
  53. NSMutableDictionary<NSString *, NSNumber *> *_states;
  54. }
  55. - (nullable instancetype)initWithURLRequest:(NSURLRequest *)URLRequest {
  56. if (URLRequest.URL == nil) {
  57. FPRLogError(kFPRNetworkTraceInvalidInputs, @"Invalid URL. URL is nil.");
  58. return nil;
  59. }
  60. // Fail early instead of creating a trace here.
  61. // IMPORTANT: Order is important here. This check needs to be done before looking up on remote
  62. // config. Reference bug: b/141861005.
  63. if (![[FPRURLFilter sharedInstance] shouldInstrumentURL:URLRequest.URL.absoluteString]) {
  64. return nil;
  65. }
  66. BOOL tracingEnabled = [FPRConfigurations sharedInstance].isDataCollectionEnabled;
  67. if (!tracingEnabled) {
  68. FPRLogInfo(kFPRTraceDisabled, @"Trace feature is disabled.");
  69. return nil;
  70. }
  71. BOOL sdkEnabled = [[FPRConfigurations sharedInstance] sdkEnabled];
  72. if (!sdkEnabled) {
  73. FPRLogInfo(kFPRTraceDisabled, @"Dropping event since Performance SDK is disabled.");
  74. return nil;
  75. }
  76. NSString *trimmedURLString = [FPRNetworkTrace stringByTrimmingURLString:URLRequest];
  77. if (!trimmedURLString || trimmedURLString.length <= 0) {
  78. FPRLogWarning(kFPRNetworkTraceURLLengthExceeds, @"URL length outside limits, returning nil.");
  79. return nil;
  80. }
  81. if (![URLRequest.URL.absoluteString isEqualToString:trimmedURLString]) {
  82. FPRLogInfo(kFPRNetworkTraceURLLengthTruncation,
  83. @"URL length exceeds limits, truncating recorded URL - %@.", trimmedURLString);
  84. }
  85. self = [super init];
  86. if (self) {
  87. _URLRequest = URLRequest;
  88. _trimmedURLString = trimmedURLString;
  89. _states = [[NSMutableDictionary<NSString *, NSNumber *> alloc] init];
  90. _hasValidResponseCode = NO;
  91. _customAttributes = [[NSMutableDictionary<NSString *, NSString *> alloc] init];
  92. _syncQueue =
  93. dispatch_queue_create("com.google.perf.networkTrace.metric", DISPATCH_QUEUE_SERIAL);
  94. _sessionIdSerialQueue =
  95. dispatch_queue_create("com.google.perf.sessionIds.networkTrace", DISPATCH_QUEUE_SERIAL);
  96. _activeSessions = [[NSMutableArray<FPRSessionDetails *> alloc] init];
  97. if (![FPRNetworkTrace isCompleteAndValidTrimmedURLString:_trimmedURLString
  98. URLRequest:_URLRequest]) {
  99. return nil;
  100. };
  101. }
  102. return self;
  103. }
  104. - (instancetype)init {
  105. FPRAssert(NO, @"Not a designated initializer.");
  106. return nil;
  107. }
  108. - (void)dealloc {
  109. // Safety net to ensure the notifications are not received anymore.
  110. FPRSessionManager *sessionManager = [FPRSessionManager sharedInstance];
  111. [sessionManager.sessionNotificationCenter removeObserver:self
  112. name:kFPRSessionIdUpdatedNotification
  113. object:sessionManager];
  114. }
  115. - (NSString *)description {
  116. return [NSString stringWithFormat:@"Request: %@", _URLRequest];
  117. }
  118. - (void)sessionChanged:(NSNotification *)notification {
  119. if (self.traceStarted && !self.traceCompleted) {
  120. NSDictionary<NSString *, FPRSessionDetails *> *userInfo = notification.userInfo;
  121. FPRSessionDetails *sessionDetails = [userInfo valueForKey:kFPRSessionIdNotificationKey];
  122. if (sessionDetails) {
  123. [self updateTraceWithCurrentSession:sessionDetails];
  124. }
  125. }
  126. }
  127. - (void)updateTraceWithCurrentSession:(FPRSessionDetails *)sessionDetails {
  128. if (sessionDetails != nil) {
  129. dispatch_sync(self.sessionIdSerialQueue, ^{
  130. [self.activeSessions addObject:sessionDetails];
  131. });
  132. }
  133. }
  134. - (NSArray<FPRSessionDetails *> *)sessions {
  135. __block NSArray<FPRSessionDetails *> *sessionInfos = nil;
  136. dispatch_sync(self.sessionIdSerialQueue, ^{
  137. sessionInfos = [self.activeSessions copy];
  138. });
  139. return sessionInfos;
  140. }
  141. - (NSDictionary<NSString *, NSNumber *> *)checkpointStates {
  142. __block NSDictionary<NSString *, NSNumber *> *copiedStates;
  143. dispatch_sync(self.syncQueue, ^{
  144. copiedStates = [_states copy];
  145. });
  146. return copiedStates;
  147. }
  148. - (void)checkpointState:(FPRNetworkTraceCheckpointState)state {
  149. if (!self.traceCompleted && self.traceStarted) {
  150. NSString *stateKey = @(state).stringValue;
  151. if (stateKey) {
  152. dispatch_sync(self.syncQueue, ^{
  153. NSNumber *existingState = _states[stateKey];
  154. if (existingState == nil) {
  155. double intervalSinceEpoch = [[NSDate date] timeIntervalSince1970];
  156. [_states setObject:@(intervalSinceEpoch) forKey:stateKey];
  157. }
  158. });
  159. } else {
  160. FPRAssert(NO, @"stateKey wasn't created for checkpoint state %ld", (long)state);
  161. }
  162. }
  163. }
  164. - (void)start {
  165. if (!self.traceCompleted) {
  166. [[FPRGaugeManager sharedInstance] collectAllGauges];
  167. self.traceStarted = YES;
  168. self.backgroundActivityTracker = [[FPRTraceBackgroundActivityTracker alloc] init];
  169. [self checkpointState:FPRNetworkTraceCheckpointStateInitiated];
  170. FPRSessionManager *sessionManager = [FPRSessionManager sharedInstance];
  171. [self updateTraceWithCurrentSession:[sessionManager.sessionDetails copy]];
  172. [sessionManager.sessionNotificationCenter addObserver:self
  173. selector:@selector(sessionChanged:)
  174. name:kFPRSessionIdUpdatedNotification
  175. object:sessionManager];
  176. // Send session id to crashlytics
  177. NSString *infoString = [NSString stringWithFormat:@"Request started - %@",
  178. self.URLRequest.URL.host];
  179. // Send session id to crashlytics
  180. NSDictionary *crashlyticsTraceBreadcrumb = @{
  181. @"source" : @"FirebasePerformance",
  182. @"info" : infoString,
  183. };
  184. NSError *error;
  185. NSData *crashlyticsSessionJsonBreadcrumb =
  186. [NSJSONSerialization dataWithJSONObject:crashlyticsTraceBreadcrumb options:NSJSONWritingPrettyPrinted error:&error];
  187. if (!crashlyticsSessionJsonBreadcrumb) {
  188. NSLog(@"Got an error: %@", error);
  189. } else {
  190. NSString *jsonString = [[NSString alloc] initWithData:crashlyticsSessionJsonBreadcrumb
  191. encoding:NSUTF8StringEncoding];
  192. // Getting the FirePerf shared instance here. Is there a better way to do that internally?
  193. [[FIRPerformance sharedInstance].crashlytics log:jsonString];
  194. }
  195. }
  196. }
  197. - (FPRTraceState)backgroundTraceState {
  198. FPRTraceBackgroundActivityTracker *backgroundActivityTracker = self.backgroundActivityTracker;
  199. if (backgroundActivityTracker) {
  200. return backgroundActivityTracker.traceBackgroundState;
  201. }
  202. return FPRTraceStateUnknown;
  203. }
  204. - (NSTimeInterval)startTimeSinceEpoch {
  205. NSString *stateKey =
  206. [NSString stringWithFormat:@"%lu", (unsigned long)FPRNetworkTraceCheckpointStateInitiated];
  207. __block NSTimeInterval timeSinceEpoch;
  208. dispatch_sync(self.syncQueue, ^{
  209. timeSinceEpoch = [[_states objectForKey:stateKey] doubleValue];
  210. });
  211. return timeSinceEpoch;
  212. }
  213. #pragma mark - Overrides
  214. - (void)setResponseCode:(int32_t)responseCode {
  215. _responseCode = responseCode;
  216. if (responseCode != 0) {
  217. _hasValidResponseCode = YES;
  218. }
  219. }
  220. #pragma mark - FPRNetworkResponseHandler methods
  221. - (void)didCompleteRequestWithResponse:(NSURLResponse *)response error:(NSError *)error {
  222. if (!self.traceCompleted && self.traceStarted) {
  223. // Extract needed fields for the trace object.
  224. if ([response isKindOfClass:[NSHTTPURLResponse class]]) {
  225. NSHTTPURLResponse *HTTPResponse = (NSHTTPURLResponse *)response;
  226. self.responseCode = (int32_t)HTTPResponse.statusCode;
  227. }
  228. self.responseError = error;
  229. self.responseContentType = response.MIMEType;
  230. [self checkpointState:FPRNetworkTraceCheckpointStateResponseCompleted];
  231. // Send the network trace for logging.
  232. [[FPRGaugeManager sharedInstance] collectAllGauges];
  233. [[FPRClient sharedInstance] logNetworkTrace:self];
  234. self.traceCompleted = YES;
  235. NSString *infoString = [NSString stringWithFormat:@"Request complete - %@, response code - %d",
  236. self.URLRequest.URL.host, self.responseCode];
  237. // Send session id to crashlytics
  238. NSDictionary *crashlyticsTraceBreadcrumb = @{
  239. @"source" : @"FirebasePerformance",
  240. @"info" : infoString,
  241. };
  242. NSError *error;
  243. NSData *crashlyticsSessionJsonBreadcrumb =
  244. [NSJSONSerialization dataWithJSONObject:crashlyticsTraceBreadcrumb options:NSJSONWritingPrettyPrinted error:&error];
  245. if (!crashlyticsSessionJsonBreadcrumb) {
  246. NSLog(@"Got an error: %@", error);
  247. } else {
  248. NSString *jsonString = [[NSString alloc] initWithData:crashlyticsSessionJsonBreadcrumb
  249. encoding:NSUTF8StringEncoding];
  250. // Getting the FirePerf shared instance here. Is there a better way to do that internally?
  251. [[FIRPerformance sharedInstance].crashlytics log:jsonString];
  252. }
  253. }
  254. FPRSessionManager *sessionManager = [FPRSessionManager sharedInstance];
  255. [sessionManager.sessionNotificationCenter removeObserver:self
  256. name:kFPRSessionIdUpdatedNotification
  257. object:sessionManager];
  258. }
  259. - (void)didUploadFileWithURL:(NSURL *)URL {
  260. NSNumber *value = nil;
  261. NSError *error = nil;
  262. if ([URL getResourceValue:&value forKey:NSURLFileSizeKey error:&error]) {
  263. if (error) {
  264. FPRLogNotice(kFPRNetworkTraceFileError, @"Unable to determine the size of file.");
  265. } else {
  266. self.requestSize = value.unsignedIntegerValue;
  267. }
  268. }
  269. }
  270. - (void)didReceiveData:(NSData *)data {
  271. self.responseSize = data.length;
  272. }
  273. - (void)didReceiveFileURL:(NSURL *)URL {
  274. NSNumber *value = nil;
  275. NSError *error = nil;
  276. if ([URL getResourceValue:&value forKey:NSURLFileSizeKey error:&error]) {
  277. if (error) {
  278. FPRLogNotice(kFPRNetworkTraceFileError, @"Unable to determine the size of file.");
  279. } else {
  280. self.responseSize = value.unsignedIntegerValue;
  281. }
  282. }
  283. }
  284. - (NSTimeInterval)timeIntervalBetweenCheckpointState:(FPRNetworkTraceCheckpointState)startState
  285. andState:(FPRNetworkTraceCheckpointState)endState {
  286. __block NSNumber *startStateTime;
  287. __block NSNumber *endStateTime;
  288. dispatch_sync(self.syncQueue, ^{
  289. startStateTime = [_states objectForKey:[@(startState) stringValue]];
  290. endStateTime = [_states objectForKey:[@(endState) stringValue]];
  291. });
  292. // Fail fast. If any of the times do not exist, return 0.
  293. if (startStateTime == nil || endStateTime == nil) {
  294. return 0;
  295. }
  296. NSTimeInterval timeDiff = (endStateTime.doubleValue - startStateTime.doubleValue);
  297. return timeDiff;
  298. }
  299. /** Trims and validates the URL string of a given NSURLRequest.
  300. *
  301. * @param URLRequest The NSURLRequest containing the URL string to trim.
  302. * @return The trimmed string.
  303. */
  304. + (NSString *)stringByTrimmingURLString:(NSURLRequest *)URLRequest {
  305. NSURLComponents *components = [NSURLComponents componentsWithURL:URLRequest.URL
  306. resolvingAgainstBaseURL:NO];
  307. components.query = nil;
  308. components.fragment = nil;
  309. components.user = nil;
  310. components.password = nil;
  311. NSURL *trimmedURL = [components URL];
  312. NSString *truncatedURLString = FPRTruncatedURLString(trimmedURL.absoluteString);
  313. NSURL *truncatedURL = [NSURL URLWithString:truncatedURLString];
  314. if (!truncatedURL || truncatedURL.host == nil) {
  315. return nil;
  316. }
  317. return truncatedURLString;
  318. }
  319. /** Validates the trace object by checking that it's http or https, and not a denied URL.
  320. *
  321. * @param trimmedURLString A trimmed URL string from the URLRequest.
  322. * @param URLRequest The NSURLRequest that this trace will operate on.
  323. * @return YES if the trace object is valid, NO otherwise.
  324. */
  325. + (BOOL)isCompleteAndValidTrimmedURLString:(NSString *)trimmedURLString
  326. URLRequest:(NSURLRequest *)URLRequest {
  327. if (![[FPRURLFilter sharedInstance] shouldInstrumentURL:trimmedURLString]) {
  328. return NO;
  329. }
  330. // Check the URL begins with http or https.
  331. NSURLComponents *components = [NSURLComponents componentsWithURL:URLRequest.URL
  332. resolvingAgainstBaseURL:NO];
  333. NSString *scheme = components.scheme;
  334. if (!scheme || !([scheme caseInsensitiveCompare:@"HTTP"] == NSOrderedSame ||
  335. [scheme caseInsensitiveCompare:@"HTTPS"] == NSOrderedSame)) {
  336. FPRLogError(kFPRNetworkTraceInvalidInputs, @"Invalid URL - %@, returning nil.", URLRequest.URL);
  337. return NO;
  338. }
  339. return YES;
  340. }
  341. #pragma mark - Custom attributes related methods
  342. - (NSDictionary<NSString *, NSString *> *)attributes {
  343. return [self.customAttributes copy];
  344. }
  345. - (void)setValue:(NSString *)value forAttribute:(nonnull NSString *)attribute {
  346. BOOL canAddAttribute = YES;
  347. if (self.traceCompleted) {
  348. FPRLogError(kFPRTraceAlreadyStopped,
  349. @"Failed to set attribute %@ because network request %@ has already stopped.",
  350. attribute, self.URLRequest.URL);
  351. canAddAttribute = NO;
  352. }
  353. NSString *validatedName = FPRReservableAttributeName(attribute);
  354. NSString *validatedValue = FPRValidatedAttributeValue(value);
  355. if (validatedName == nil) {
  356. FPRLogError(kFPRAttributeNoName,
  357. @"Failed to initialize because of a nil or zero length attribute name.");
  358. canAddAttribute = NO;
  359. }
  360. if (validatedValue == nil) {
  361. FPRLogError(kFPRAttributeNoValue,
  362. @"Failed to initialize because of a nil or zero length attribute value.");
  363. canAddAttribute = NO;
  364. }
  365. if (self.customAttributes.allKeys.count >= kFPRMaxGlobalCustomAttributesCount) {
  366. FPRLogError(kFPRMaxAttributesReached,
  367. @"Only %d attributes allowed. Already reached maximum attribute count.",
  368. kFPRMaxGlobalCustomAttributesCount);
  369. canAddAttribute = NO;
  370. }
  371. if (canAddAttribute) {
  372. // Ensure concurrency during update of attributes.
  373. dispatch_sync(self.syncQueue, ^{
  374. self.customAttributes[validatedName] = validatedValue;
  375. FPRLogDebug(kFPRClientMetricLogged, @"Setting attribute %@ to %@ on network request %@",
  376. validatedName, validatedValue, self.URLRequest.URL);
  377. });
  378. }
  379. }
  380. - (NSString *)valueForAttribute:(NSString *)attribute {
  381. // TODO(b/175053654): Should this be happening on the serial queue for thread safety?
  382. return self.customAttributes[attribute];
  383. }
  384. - (void)removeAttribute:(NSString *)attribute {
  385. if (self.traceCompleted) {
  386. FPRLogError(kFPRTraceAlreadyStopped,
  387. @"Failed to remove attribute %@ because network request %@ has already stopped.",
  388. attribute, self.URLRequest.URL);
  389. return;
  390. }
  391. [self.customAttributes removeObjectForKey:attribute];
  392. }
  393. #pragma mark - Class methods related to object association.
  394. + (void)addNetworkTrace:(FPRNetworkTrace *)networkTrace toObject:(id)object {
  395. if (object != nil && networkTrace != nil) {
  396. [GULObjectSwizzler setAssociatedObject:object
  397. key:kFPRNetworkTracePropertyName
  398. value:networkTrace
  399. association:GUL_ASSOCIATION_RETAIN_NONATOMIC];
  400. }
  401. }
  402. + (FPRNetworkTrace *)networkTraceFromObject:(id)object {
  403. FPRNetworkTrace *networkTrace = nil;
  404. if (object != nil) {
  405. id traceObject = [GULObjectSwizzler getAssociatedObject:object
  406. key:kFPRNetworkTracePropertyName];
  407. if ([traceObject isKindOfClass:[FPRNetworkTrace class]]) {
  408. networkTrace = (FPRNetworkTrace *)traceObject;
  409. }
  410. }
  411. return networkTrace;
  412. }
  413. + (void)removeNetworkTraceFromObject:(id)object {
  414. if (object != nil) {
  415. [GULObjectSwizzler setAssociatedObject:object
  416. key:kFPRNetworkTracePropertyName
  417. value:nil
  418. association:GUL_ASSOCIATION_RETAIN_NONATOMIC];
  419. }
  420. }
  421. - (BOOL)isValid {
  422. return _hasValidResponseCode;
  423. }
  424. @end