mirror of
https://github.com/firebase/firebase-ios-sdk
synced 2026-11-09 22:36:01 +05:45
447 lines
16 KiB
Objective-C
447 lines
16 KiB
Objective-C
// Copyright 2020 Google LLC
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
#import "FirebasePerformance/Sources/Instrumentation/FPRNetworkTrace.h"
|
|
#import "FirebasePerformance/Sources/Instrumentation/FPRNetworkTrace+Private.h"
|
|
|
|
#import "FirebasePerformance/Sources/AppActivity/FPRSessionManager.h"
|
|
#import "FirebasePerformance/Sources/Common/FPRConstants.h"
|
|
#import "FirebasePerformance/Sources/Common/FPRDiagnostics.h"
|
|
#import "FirebasePerformance/Sources/Configurations/FPRConfigurations.h"
|
|
#import "FirebasePerformance/Sources/FPRClient.h"
|
|
#import "FirebasePerformance/Sources/FPRConsoleLogger.h"
|
|
#import "FirebasePerformance/Sources/FPRDataUtils.h"
|
|
#import "FirebasePerformance/Sources/FPRURLFilter.h"
|
|
#import "FirebasePerformance/Sources/Gauges/FPRGaugeManager.h"
|
|
|
|
#import <GoogleUtilities/GULObjectSwizzler.h>
|
|
|
|
NSString *const kFPRNetworkTracePropertyName = @"fpr_networkTrace";
|
|
|
|
@interface FPRNetworkTrace ()
|
|
|
|
@property(nonatomic, readwrite) NSURLRequest *URLRequest;
|
|
|
|
@property(nonatomic, readwrite, nullable) NSError *responseError;
|
|
|
|
/** State to know if the trace has started. */
|
|
@property(nonatomic) BOOL traceStarted;
|
|
|
|
/** State to know if the trace has completed. */
|
|
@property(nonatomic) BOOL traceCompleted;
|
|
|
|
/** Background activity tracker to know the background state of the trace. */
|
|
@property(nonatomic) FPRTraceBackgroundActivityTracker *backgroundActivityTracker;
|
|
|
|
/** Custom attribute managed internally. */
|
|
@property(nonatomic) NSMutableDictionary<NSString *, NSString *> *customAttributes;
|
|
|
|
/** @brief Serial queue to manage the updation of session Ids. */
|
|
@property(nonatomic, readwrite) dispatch_queue_t sessionIdSerialQueue;
|
|
|
|
/** Updates the current trace with the current session details. */
|
|
- (void)updateTraceWithCurrentSession;
|
|
|
|
@end
|
|
|
|
@implementation FPRNetworkTrace {
|
|
/**
|
|
* @brief Object containing different states of the network request. Stores the information about
|
|
* the state of a network request (defined in FPRNetworkTraceCheckpointState) and the time at
|
|
* which the event happened.
|
|
*/
|
|
NSMutableDictionary<NSString *, NSNumber *> *_states;
|
|
}
|
|
|
|
- (nullable instancetype)initWithURLRequest:(NSURLRequest *)URLRequest {
|
|
if (URLRequest.URL == nil) {
|
|
FPRLogError(kFPRNetworkTraceInvalidInputs, @"Invalid URL. URL is nil.");
|
|
return nil;
|
|
}
|
|
|
|
// Fail early instead of creating a trace here.
|
|
// IMPORTANT: Order is important here. This check needs to be done before looking up on remote
|
|
// config. Reference bug: b/141861005.
|
|
if (![[FPRURLFilter sharedInstance] shouldInstrumentURL:URLRequest.URL.absoluteString]) {
|
|
return nil;
|
|
}
|
|
|
|
BOOL tracingEnabled = [FPRConfigurations sharedInstance].isDataCollectionEnabled;
|
|
if (!tracingEnabled) {
|
|
FPRLogInfo(kFPRTraceDisabled, @"Trace feature is disabled.");
|
|
return nil;
|
|
}
|
|
|
|
BOOL sdkEnabled = [[FPRConfigurations sharedInstance] sdkEnabled];
|
|
if (!sdkEnabled) {
|
|
FPRLogInfo(kFPRTraceDisabled, @"Dropping event since Performance SDK is disabled.");
|
|
return nil;
|
|
}
|
|
|
|
NSString *trimmedURLString = [FPRNetworkTrace stringByTrimmingURLString:URLRequest];
|
|
if (!trimmedURLString || trimmedURLString.length <= 0) {
|
|
FPRLogInfo(kFPRNetworkTraceURLLengthExceeds, @"URL length outside limits, returning nil.");
|
|
return nil;
|
|
}
|
|
|
|
if (![URLRequest.URL.absoluteString isEqualToString:trimmedURLString]) {
|
|
FPRLogInfo(kFPRNetworkTraceURLLengthTruncation,
|
|
@"URL length exceeds limits, truncating recorded URL - %@.", trimmedURLString);
|
|
}
|
|
|
|
self = [super init];
|
|
if (self) {
|
|
_URLRequest = URLRequest;
|
|
_trimmedURLString = trimmedURLString;
|
|
_states = [[NSMutableDictionary<NSString *, NSNumber *> alloc] init];
|
|
_hasValidResponseCode = NO;
|
|
_customAttributes = [[NSMutableDictionary<NSString *, NSString *> alloc] init];
|
|
_syncQueue =
|
|
dispatch_queue_create("com.google.perf.networkTrace.metric", DISPATCH_QUEUE_SERIAL);
|
|
_sessionIdSerialQueue =
|
|
dispatch_queue_create("com.google.perf.sessionIds.networkTrace", DISPATCH_QUEUE_SERIAL);
|
|
_activeSessions = [[NSMutableArray<FPRSessionDetails *> alloc] init];
|
|
if (![FPRNetworkTrace isCompleteAndValidTrimmedURLString:_trimmedURLString
|
|
URLRequest:_URLRequest]) {
|
|
return nil;
|
|
};
|
|
}
|
|
return self;
|
|
}
|
|
|
|
- (instancetype)init {
|
|
FPRAssert(NO, @"Not a designated initializer.");
|
|
return nil;
|
|
}
|
|
|
|
- (void)dealloc {
|
|
// Safety net to ensure the notifications are not received anymore.
|
|
FPRSessionManager *sessionManager = [FPRSessionManager sharedInstance];
|
|
[sessionManager.sessionNotificationCenter removeObserver:self
|
|
name:kFPRSessionIdUpdatedNotification
|
|
object:sessionManager];
|
|
}
|
|
|
|
- (NSString *)description {
|
|
return [NSString stringWithFormat:@"Request: %@", _URLRequest];
|
|
}
|
|
|
|
- (void)updateTraceWithCurrentSession {
|
|
if (self.traceStarted && !self.traceCompleted) {
|
|
dispatch_async(self.sessionIdSerialQueue, ^{
|
|
FPRSessionManager *sessionManager = [FPRSessionManager sharedInstance];
|
|
FPRSessionDetails *sessionDetails = sessionManager.sessionDetails;
|
|
if (sessionDetails) {
|
|
[self.activeSessions addObject:sessionDetails];
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
- (NSArray<FPRSessionDetails *> *)sessions {
|
|
__block NSArray<FPRSessionDetails *> *sessionInfos = nil;
|
|
dispatch_sync(self.sessionIdSerialQueue, ^{
|
|
sessionInfos = [self.activeSessions copy];
|
|
});
|
|
return sessionInfos;
|
|
}
|
|
|
|
- (NSDictionary<NSString *, NSNumber *> *)checkpointStates {
|
|
__block NSDictionary<NSString *, NSNumber *> *copiedStates;
|
|
dispatch_sync(self.syncQueue, ^{
|
|
copiedStates = [_states copy];
|
|
});
|
|
return copiedStates;
|
|
}
|
|
|
|
- (void)checkpointState:(FPRNetworkTraceCheckpointState)state {
|
|
if (!self.traceCompleted && self.traceStarted) {
|
|
NSString *stateKey = @(state).stringValue;
|
|
if (stateKey) {
|
|
dispatch_sync(self.syncQueue, ^{
|
|
NSNumber *existingState = _states[stateKey];
|
|
|
|
if (existingState == nil) {
|
|
double intervalSinceEpoch = [[NSDate date] timeIntervalSince1970];
|
|
[_states setObject:@(intervalSinceEpoch) forKey:stateKey];
|
|
}
|
|
});
|
|
} else {
|
|
FPRAssert(NO, @"stateKey wasn't created for checkpoint state %ld", (long)state);
|
|
}
|
|
}
|
|
}
|
|
|
|
- (void)start {
|
|
if (!self.traceCompleted) {
|
|
[[FPRGaugeManager sharedInstance] collectAllGauges];
|
|
self.traceStarted = YES;
|
|
self.backgroundActivityTracker = [[FPRTraceBackgroundActivityTracker alloc] init];
|
|
[self checkpointState:FPRNetworkTraceCheckpointStateInitiated];
|
|
|
|
FPRSessionManager *sessionManager = [FPRSessionManager sharedInstance];
|
|
[self updateTraceWithCurrentSession];
|
|
[sessionManager.sessionNotificationCenter addObserver:self
|
|
selector:@selector(updateTraceWithCurrentSession)
|
|
name:kFPRSessionIdUpdatedNotification
|
|
object:sessionManager];
|
|
}
|
|
}
|
|
|
|
- (FPRTraceState)backgroundTraceState {
|
|
FPRTraceBackgroundActivityTracker *backgroundActivityTracker = self.backgroundActivityTracker;
|
|
if (backgroundActivityTracker) {
|
|
return backgroundActivityTracker.traceBackgroundState;
|
|
}
|
|
|
|
return FPRTraceStateUnknown;
|
|
}
|
|
|
|
- (NSTimeInterval)startTimeSinceEpoch {
|
|
NSString *stateKey =
|
|
[NSString stringWithFormat:@"%lu", (unsigned long)FPRNetworkTraceCheckpointStateInitiated];
|
|
__block NSTimeInterval timeSinceEpoch;
|
|
dispatch_sync(self.syncQueue, ^{
|
|
timeSinceEpoch = [[_states objectForKey:stateKey] doubleValue];
|
|
});
|
|
return timeSinceEpoch;
|
|
}
|
|
|
|
#pragma mark - Overrides
|
|
|
|
- (void)setResponseCode:(int32_t)responseCode {
|
|
_responseCode = responseCode;
|
|
if (responseCode != 0) {
|
|
_hasValidResponseCode = YES;
|
|
}
|
|
}
|
|
|
|
#pragma mark - FPRNetworkResponseHandler methods
|
|
|
|
- (void)didCompleteRequestWithResponse:(NSURLResponse *)response error:(NSError *)error {
|
|
if (!self.traceCompleted && self.traceStarted) {
|
|
// Extract needed fields for the trace object.
|
|
if ([response isKindOfClass:[NSHTTPURLResponse class]]) {
|
|
NSHTTPURLResponse *HTTPResponse = (NSHTTPURLResponse *)response;
|
|
self.responseCode = (int32_t)HTTPResponse.statusCode;
|
|
}
|
|
self.responseError = error;
|
|
self.responseContentType = response.MIMEType;
|
|
[self checkpointState:FPRNetworkTraceCheckpointStateResponseCompleted];
|
|
|
|
// Send the network trace for logging.
|
|
[[FPRGaugeManager sharedInstance] collectAllGauges];
|
|
[[FPRClient sharedInstance] logNetworkTrace:self];
|
|
|
|
self.traceCompleted = YES;
|
|
}
|
|
|
|
FPRSessionManager *sessionManager = [FPRSessionManager sharedInstance];
|
|
[sessionManager.sessionNotificationCenter removeObserver:self
|
|
name:kFPRSessionIdUpdatedNotification
|
|
object:sessionManager];
|
|
}
|
|
|
|
- (void)didUploadFileWithURL:(NSURL *)URL {
|
|
NSNumber *value = nil;
|
|
NSError *error = nil;
|
|
|
|
if ([URL getResourceValue:&value forKey:NSURLFileSizeKey error:&error]) {
|
|
if (error) {
|
|
FPRLogNotice(kFPRNetworkTraceFileError, @"Unable to determine the size of file.");
|
|
} else {
|
|
self.requestSize = value.unsignedIntegerValue;
|
|
}
|
|
}
|
|
}
|
|
|
|
- (void)didReceiveData:(NSData *)data {
|
|
self.responseSize = data.length;
|
|
}
|
|
|
|
- (void)didReceiveFileURL:(NSURL *)URL {
|
|
NSNumber *value = nil;
|
|
NSError *error = nil;
|
|
|
|
if ([URL getResourceValue:&value forKey:NSURLFileSizeKey error:&error]) {
|
|
if (error) {
|
|
FPRLogNotice(kFPRNetworkTraceFileError, @"Unable to determine the size of file.");
|
|
} else {
|
|
self.responseSize = value.unsignedIntegerValue;
|
|
}
|
|
}
|
|
}
|
|
|
|
- (NSTimeInterval)timeIntervalBetweenCheckpointState:(FPRNetworkTraceCheckpointState)startState
|
|
andState:(FPRNetworkTraceCheckpointState)endState {
|
|
__block NSNumber *startStateTime;
|
|
__block NSNumber *endStateTime;
|
|
dispatch_sync(self.syncQueue, ^{
|
|
startStateTime = [_states objectForKey:[@(startState) stringValue]];
|
|
endStateTime = [_states objectForKey:[@(endState) stringValue]];
|
|
});
|
|
// Fail fast. If any of the times do not exist, return 0.
|
|
if (startStateTime == nil || endStateTime == nil) {
|
|
return 0;
|
|
}
|
|
|
|
NSTimeInterval timeDiff = (endStateTime.doubleValue - startStateTime.doubleValue);
|
|
return timeDiff;
|
|
}
|
|
|
|
/** Trims and validates the URL string of a given NSURLRequest.
|
|
*
|
|
* @param URLRequest The NSURLRequest containing the URL string to trim.
|
|
* @return The trimmed string.
|
|
*/
|
|
+ (NSString *)stringByTrimmingURLString:(NSURLRequest *)URLRequest {
|
|
NSURLComponents *components = [NSURLComponents componentsWithURL:URLRequest.URL
|
|
resolvingAgainstBaseURL:NO];
|
|
components.query = nil;
|
|
components.fragment = nil;
|
|
components.user = nil;
|
|
components.password = nil;
|
|
NSURL *trimmedURL = [components URL];
|
|
NSString *truncatedURLString = FPRTruncatedURLString(trimmedURL.absoluteString);
|
|
|
|
NSURL *truncatedURL = [NSURL URLWithString:truncatedURLString];
|
|
if (!truncatedURL || truncatedURL.host == nil) {
|
|
return nil;
|
|
}
|
|
return truncatedURLString;
|
|
}
|
|
|
|
/** Validates the trace object by checking that it's http or https, and not a denied URL.
|
|
*
|
|
* @param trimmedURLString A trimmed URL string from the URLRequest.
|
|
* @param URLRequest The NSURLRequest that this trace will operate on.
|
|
* @return YES if the trace object is valid, NO otherwise.
|
|
*/
|
|
+ (BOOL)isCompleteAndValidTrimmedURLString:(NSString *)trimmedURLString
|
|
URLRequest:(NSURLRequest *)URLRequest {
|
|
if (![[FPRURLFilter sharedInstance] shouldInstrumentURL:trimmedURLString]) {
|
|
return NO;
|
|
}
|
|
|
|
// Check the URL begins with http or https.
|
|
NSURLComponents *components = [NSURLComponents componentsWithURL:URLRequest.URL
|
|
resolvingAgainstBaseURL:NO];
|
|
NSString *scheme = components.scheme;
|
|
if (!scheme || !([scheme caseInsensitiveCompare:@"HTTP"] == NSOrderedSame ||
|
|
[scheme caseInsensitiveCompare:@"HTTPS"] == NSOrderedSame)) {
|
|
FPRLogError(kFPRNetworkTraceInvalidInputs, @"Invalid URL - %@, returning nil.", URLRequest.URL);
|
|
return NO;
|
|
}
|
|
|
|
return YES;
|
|
}
|
|
|
|
#pragma mark - Custom attributes related methods
|
|
|
|
- (NSDictionary<NSString *, NSString *> *)attributes {
|
|
return [self.customAttributes copy];
|
|
}
|
|
|
|
- (void)setValue:(NSString *)value forAttribute:(nonnull NSString *)attribute {
|
|
BOOL canAddAttribute = YES;
|
|
if (self.traceCompleted) {
|
|
FPRLogError(kFPRTraceAlreadyStopped,
|
|
@"Failed to set attribute %@ because network request %@ has already stopped.",
|
|
attribute, self.URLRequest.URL);
|
|
canAddAttribute = NO;
|
|
}
|
|
|
|
NSString *validatedName = FPRReservableAttributeName(attribute);
|
|
NSString *validatedValue = FPRValidatedAttributeValue(value);
|
|
|
|
if (validatedName == nil) {
|
|
FPRLogError(kFPRAttributeNoName,
|
|
@"Failed to initialize because of a nil or zero length attribute name.");
|
|
canAddAttribute = NO;
|
|
}
|
|
|
|
if (validatedValue == nil) {
|
|
FPRLogError(kFPRAttributeNoValue,
|
|
@"Failed to initialize because of a nil or zero length attribute value.");
|
|
canAddAttribute = NO;
|
|
}
|
|
|
|
if (self.customAttributes.allKeys.count >= kFPRMaxGlobalCustomAttributesCount) {
|
|
FPRLogError(kFPRMaxAttributesReached,
|
|
@"Only %d attributes allowed. Already reached maximum attribute count.",
|
|
kFPRMaxGlobalCustomAttributesCount);
|
|
canAddAttribute = NO;
|
|
}
|
|
|
|
if (canAddAttribute) {
|
|
// Ensure concurrency during update of attributes.
|
|
dispatch_sync(self.syncQueue, ^{
|
|
self.customAttributes[validatedName] = validatedValue;
|
|
FPRLogDebug(kFPRClientMetricLogged, @"Setting attribute %@ to %@ on network request %@",
|
|
validatedName, validatedValue, self.URLRequest.URL);
|
|
});
|
|
}
|
|
}
|
|
|
|
- (NSString *)valueForAttribute:(NSString *)attribute {
|
|
// TODO(b/175053654): Should this be happening on the serial queue for thread safety?
|
|
return self.customAttributes[attribute];
|
|
}
|
|
|
|
- (void)removeAttribute:(NSString *)attribute {
|
|
if (self.traceCompleted) {
|
|
FPRLogError(kFPRTraceAlreadyStopped,
|
|
@"Failed to remove attribute %@ because network request %@ has already stopped.",
|
|
attribute, self.URLRequest.URL);
|
|
return;
|
|
}
|
|
|
|
[self.customAttributes removeObjectForKey:attribute];
|
|
}
|
|
|
|
#pragma mark - Class methods related to object association.
|
|
|
|
+ (void)addNetworkTrace:(FPRNetworkTrace *)networkTrace toObject:(id)object {
|
|
if (object != nil && networkTrace != nil) {
|
|
[GULObjectSwizzler setAssociatedObject:object
|
|
key:kFPRNetworkTracePropertyName
|
|
value:networkTrace
|
|
association:GUL_ASSOCIATION_RETAIN_NONATOMIC];
|
|
}
|
|
}
|
|
|
|
+ (FPRNetworkTrace *)networkTraceFromObject:(id)object {
|
|
FPRNetworkTrace *networkTrace = nil;
|
|
if (object != nil) {
|
|
id traceObject = [GULObjectSwizzler getAssociatedObject:object
|
|
key:kFPRNetworkTracePropertyName];
|
|
if ([traceObject isKindOfClass:[FPRNetworkTrace class]]) {
|
|
networkTrace = (FPRNetworkTrace *)traceObject;
|
|
}
|
|
}
|
|
|
|
return networkTrace;
|
|
}
|
|
|
|
+ (void)removeNetworkTraceFromObject:(id)object {
|
|
if (object != nil) {
|
|
[GULObjectSwizzler setAssociatedObject:object
|
|
key:kFPRNetworkTracePropertyName
|
|
value:nil
|
|
association:GUL_ASSOCIATION_RETAIN_NONATOMIC];
|
|
}
|
|
}
|
|
|
|
@end
|