/* * Copyright 2019 Google * * 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 #include #import #import #import #import #import "Interop/CoreDiagnostics/Public/FIRCoreDiagnosticsData.h" #import "Interop/CoreDiagnostics/Public/FIRCoreDiagnosticsInterop.h" #import #import #import #import "Firebase/CoreDiagnostics/FIRCDLibrary/Protogen/nanopb/firebasecore.nanopb.h" /** The logger service string to use when printing to the console. */ static GULLoggerService kFIRCoreDiagnostics = @"[FirebaseCoreDiagnostics/FIRCoreDiagnostics]"; #ifdef FIREBASE_BUILD_ZIP_FILE static BOOL kUsingZipFile = YES; #else // FIREBASE_BUILD_ZIP_FILE static BOOL kUsingZipFile = NO; #endif // FIREBASE_BUILD_ZIP_FILE #if SWIFT_PACKAGE #define kDeploymentType logs_proto_mobilesdk_ios_ICoreConfiguration_DeploymentType_SPM #elif FIREBASE_BUILD_CARTHAGE #define kDeploymentType logs_proto_mobilesdk_ios_ICoreConfiguration_DeploymentType_CARTHAGE #elif FIREBASE_BUILD_ZIP_FILE #define kDeploymentType logs_proto_mobilesdk_ios_ICoreConfiguration_DeploymentType_ZIP_FILE #else #define kDeploymentType logs_proto_mobilesdk_ios_ICoreConfiguration_DeploymentType_COCOAPODS #endif static NSString *const kFIRServiceMLModelInterpreter = @"MLModelInterpreter"; static NSString *const kFIRServiceAdMob = @"AdMob"; static NSString *const kFIRServiceAuth = @"Auth"; static NSString *const kFIRServiceAuthUI = @"AuthUI"; static NSString *const kFIRServiceCrash = @"Crash"; static NSString *const kFIRServiceDatabase = @"Database"; static NSString *const kFIRServiceDynamicLinks = @"DynamicLinks"; static NSString *const kFIRServiceFirestore = @"Firestore"; static NSString *const kFIRServiceFunctions = @"Functions"; static NSString *const kFIRServiceIAM = @"InAppMessaging"; static NSString *const kFIRServiceInstanceID = @"InstanceID"; static NSString *const kFIRServiceInvites = @"Invites"; static NSString *const kFIRServiceMessaging = @"Messaging"; static NSString *const kFIRServiceMeasurement = @"Measurement"; static NSString *const kFIRServicePerformance = @"Performance"; static NSString *const kFIRServiceRemoteConfig = @"RemoteConfig"; static NSString *const kFIRServiceStorage = @"Storage"; static NSString *const kGGLServiceAnalytics = @"Analytics"; static NSString *const kGGLServiceSignIn = @"SignIn"; static NSString *const kFIRAppDiagnosticsConfigurationTypeKey = @"FIRAppDiagnosticsConfigurationTypeKey"; static NSString *const kFIRAppDiagnosticsFIRAppKey = @"FIRAppDiagnosticsFIRAppKey"; static NSString *const kFIRAppDiagnosticsSDKNameKey = @"FIRAppDiagnosticsSDKNameKey"; static NSString *const kFIRAppDiagnosticsSDKVersionKey = @"FIRAppDiagnosticsSDKVersionKey"; static NSString *const kFIRCoreDiagnosticsHeartbeatTag = @"FIRCoreDiagnostics"; /** * The file name to the recent heartbeat date. */ NSString *const kFIRCoreDiagnosticsHeartbeatDateFileName = @"FIREBASE_DIAGNOSTICS_HEARTBEAT_DATE"; /** * @note This should implement the GDTCOREventDataObject protocol, but can't because of * weak-linking. */ @interface FIRCoreDiagnosticsLog : NSObject /** The config that will be converted to proto bytes. */ @property(nonatomic) logs_proto_mobilesdk_ios_ICoreConfiguration config; @end @implementation FIRCoreDiagnosticsLog - (instancetype)initWithConfig:(logs_proto_mobilesdk_ios_ICoreConfiguration)config { self = [super init]; if (self) { _config = config; } return self; } // Provided and required by the GDTCOREventDataObject protocol. - (NSData *)transportBytes { pb_ostream_t sizestream = PB_OSTREAM_SIZING; // Encode 1 time to determine the size. if (!pb_encode(&sizestream, logs_proto_mobilesdk_ios_ICoreConfiguration_fields, &_config)) { GDTCORLogError(GDTCORMCETransportBytesError, @"Error in nanopb encoding for size: %s", PB_GET_ERROR(&sizestream)); } // Encode a 2nd time to actually get the bytes from it. size_t bufferSize = sizestream.bytes_written; CFMutableDataRef dataRef = CFDataCreateMutable(CFAllocatorGetDefault(), bufferSize); CFDataSetLength(dataRef, bufferSize); pb_ostream_t ostream = pb_ostream_from_buffer((void *)CFDataGetBytePtr(dataRef), bufferSize); if (!pb_encode(&ostream, logs_proto_mobilesdk_ios_ICoreConfiguration_fields, &_config)) { GDTCORLogError(GDTCORMCETransportBytesError, @"Error in nanopb encoding for bytes: %s", PB_GET_ERROR(&ostream)); } CFDataSetLength(dataRef, ostream.bytes_written); return CFBridgingRelease(dataRef); } - (void)dealloc { pb_release(logs_proto_mobilesdk_ios_ICoreConfiguration_fields, &_config); } @end NS_ASSUME_NONNULL_BEGIN /** This class produces a protobuf containing diagnostics and usage data to be logged. */ @interface FIRCoreDiagnostics : NSObject /** The queue on which all diagnostics collection will occur. */ @property(nonatomic, readonly) dispatch_queue_t diagnosticsQueue; /** The transport object used to send data. */ @property(nonatomic, readonly) GDTCORTransport *transport; /** The storage to store the date of the last sent heartbeat. */ @property(nonatomic, readonly) GULHeartbeatDateStorage *heartbeatDateStorage; @end NS_ASSUME_NONNULL_END @implementation FIRCoreDiagnostics + (instancetype)sharedInstance { static FIRCoreDiagnostics *sharedInstance; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ sharedInstance = [[FIRCoreDiagnostics alloc] init]; }); return sharedInstance; } - (instancetype)init { GDTCORTransport *transport = [[GDTCORTransport alloc] initWithMappingID:@"137" transformers:nil target:kGDTCORTargetFLL]; GULHeartbeatDateStorage *dateStorage = [[GULHeartbeatDateStorage alloc] initWithFileName:kFIRCoreDiagnosticsHeartbeatDateFileName]; return [self initWithTransport:transport heartbeatDateStorage:dateStorage]; } /** Initializer for unit tests. * * @param transport A `GDTCORTransport` instance which that be used to send event. * @param heartbeatDateStorage An instanse of date storage to track heartbeat sending. * @return Returns the initialized `FIRCoreDiagnostics` instance. */ - (instancetype)initWithTransport:(GDTCORTransport *)transport heartbeatDateStorage:(GULHeartbeatDateStorage *)heartbeatDateStorage { self = [super init]; if (self) { _diagnosticsQueue = dispatch_queue_create("com.google.FIRCoreDiagnostics", DISPATCH_QUEUE_SERIAL); _transport = transport; _heartbeatDateStorage = heartbeatDateStorage; } return self; } #pragma mark - nanopb helper functions /** Callocs a pb_bytes_array and copies the given NSString's bytes into the bytes array. * * @note Memory needs to be free manually, through pb_free or pb_release. * @param string The string to encode as pb_bytes. */ pb_bytes_array_t *FIREncodeString(NSString *string) { NSData *stringBytes = [string dataUsingEncoding:NSUTF8StringEncoding]; return FIREncodeData(stringBytes); } /** Callocs a pb_bytes_array and copies the given NSData bytes into the bytes array. * * @note Memory needs to be free manually, through pb_free or pb_release. * @param data The data to copy into the new bytes array. */ pb_bytes_array_t *FIREncodeData(NSData *data) { pb_bytes_array_t *pbBytesArray = calloc(1, PB_BYTES_ARRAY_T_ALLOCSIZE(data.length)); if (pbBytesArray != NULL) { [data getBytes:pbBytesArray->bytes length:data.length]; pbBytesArray->size = (pb_size_t)data.length; } return pbBytesArray; } /** Maps a service string to the representative nanopb enum. * * @param serviceString The SDK service string to convert. * @return The representative nanopb enum. */ logs_proto_mobilesdk_ios_ICoreConfiguration_ServiceType FIRMapFromServiceStringToTypeEnum( NSString *serviceString) { static NSDictionary *serviceStringToTypeEnum; if (serviceStringToTypeEnum == nil) { serviceStringToTypeEnum = @{ kFIRServiceAdMob : @(logs_proto_mobilesdk_ios_ICoreConfiguration_ServiceType_ADMOB), kFIRServiceMessaging : @(logs_proto_mobilesdk_ios_ICoreConfiguration_ServiceType_MESSAGING), kFIRServiceMeasurement : @(logs_proto_mobilesdk_ios_ICoreConfiguration_ServiceType_MEASUREMENT), kFIRServiceRemoteConfig : @(logs_proto_mobilesdk_ios_ICoreConfiguration_ServiceType_REMOTE_CONFIG), kFIRServiceDatabase : @(logs_proto_mobilesdk_ios_ICoreConfiguration_ServiceType_DATABASE), kFIRServiceDynamicLinks : @(logs_proto_mobilesdk_ios_ICoreConfiguration_ServiceType_DYNAMIC_LINKS), kFIRServiceAuth : @(logs_proto_mobilesdk_ios_ICoreConfiguration_ServiceType_AUTH), kFIRServiceAuthUI : @(logs_proto_mobilesdk_ios_ICoreConfiguration_ServiceType_AUTH_UI), kFIRServiceFirestore : @(logs_proto_mobilesdk_ios_ICoreConfiguration_ServiceType_FIRESTORE), kFIRServiceFunctions : @(logs_proto_mobilesdk_ios_ICoreConfiguration_ServiceType_FUNCTIONS), kFIRServicePerformance : @(logs_proto_mobilesdk_ios_ICoreConfiguration_ServiceType_PERFORMANCE), kFIRServiceStorage : @(logs_proto_mobilesdk_ios_ICoreConfiguration_ServiceType_STORAGE), kFIRServiceMLModelInterpreter : @(logs_proto_mobilesdk_ios_ICoreConfiguration_ServiceType_ML_MODEL_INTERPRETER), kGGLServiceAnalytics : @(logs_proto_mobilesdk_ios_ICoreConfiguration_ServiceType_ANALYTICS), kGGLServiceSignIn : @(logs_proto_mobilesdk_ios_ICoreConfiguration_ServiceType_SIGN_IN), kFIRServiceIAM : @(logs_proto_mobilesdk_ios_ICoreConfiguration_ServiceType_IN_APP_MESSAGING), }; } if (serviceStringToTypeEnum[serviceString] != nil) { return (int32_t)serviceStringToTypeEnum[serviceString].longLongValue; } return logs_proto_mobilesdk_ios_ICoreConfiguration_ServiceType_UNKNOWN_SDK_SERVICE; } #pragma mark - Proto population functions /** Populates the given proto with data related to an SDK logDiagnostics call from the * diagnosticObjects dictionary. * * @param config The proto to populate * @param diagnosticObjects The dictionary of diagnostics objects. */ void FIRPopulateProtoWithInfoFromUserInfoParams(logs_proto_mobilesdk_ios_ICoreConfiguration *config, NSDictionary *diagnosticObjects) { NSNumber *configurationType = diagnosticObjects[kFIRCDConfigurationTypeKey]; if (configurationType != nil) { switch (configurationType.integerValue) { case logs_proto_mobilesdk_ios_ICoreConfiguration_ConfigurationType_CORE: config->configuration_type = logs_proto_mobilesdk_ios_ICoreConfiguration_ConfigurationType_CORE; config->has_configuration_type = 1; break; case logs_proto_mobilesdk_ios_ICoreConfiguration_ConfigurationType_SDK: config->configuration_type = logs_proto_mobilesdk_ios_ICoreConfiguration_ConfigurationType_SDK; config->has_configuration_type = 1; break; default: break; } } NSString *sdkName = diagnosticObjects[kFIRCDSdkNameKey]; if (sdkName) { config->sdk_name = FIRMapFromServiceStringToTypeEnum(sdkName); config->has_sdk_name = 1; } NSString *version = diagnosticObjects[kFIRCDSdkVersionKey]; if (version) { config->sdk_version = FIREncodeString(version); } } /** Populates the given proto with data from the calling FIRApp using the given * diagnosticObjects dictionary. * * @param config The proto to populate * @param diagnosticObjects The dictionary of diagnostics objects. */ void FIRPopulateProtoWithCommonInfoFromApp(logs_proto_mobilesdk_ios_ICoreConfiguration *config, NSDictionary *diagnosticObjects) { config->pod_name = logs_proto_mobilesdk_ios_ICoreConfiguration_PodName_FIREBASE; config->has_pod_name = 1; if (!diagnosticObjects[kFIRCDllAppsCountKey]) { GDTCORLogError(GDTCORMCEGeneralError, @"%@", @"App count is a required value in the data dict."); } config->app_count = (int32_t)[diagnosticObjects[kFIRCDllAppsCountKey] integerValue]; config->has_app_count = 1; NSString *googleAppID = diagnosticObjects[kFIRCDGoogleAppIDKey]; if (googleAppID.length) { config->app_id = FIREncodeString(googleAppID); } NSString *bundleID = diagnosticObjects[kFIRCDBundleIDKey]; if (bundleID.length) { config->bundle_id = FIREncodeString(bundleID); } NSString *firebaseUserAgent = diagnosticObjects[kFIRCDFirebaseUserAgentKey]; if (firebaseUserAgent.length) { config->platform_info = FIREncodeString(firebaseUserAgent); } NSNumber *usingOptionsFromDefaultPlist = diagnosticObjects[kFIRCDUsingOptionsFromDefaultPlistKey]; if (usingOptionsFromDefaultPlist != nil) { config->use_default_app = [usingOptionsFromDefaultPlist boolValue]; config->has_use_default_app = 1; } NSString *libraryVersionID = diagnosticObjects[kFIRCDLibraryVersionIDKey]; if (libraryVersionID) { config->icore_version = FIREncodeString(libraryVersionID); } NSString *deviceModel = [GULAppEnvironmentUtil deviceModel]; if (deviceModel.length) { config->device_model = FIREncodeString(deviceModel); } NSString *osVersion = [GULAppEnvironmentUtil systemVersion]; if (osVersion.length) { config->os_version = FIREncodeString(osVersion); } config->using_zip_file = kUsingZipFile; config->has_using_zip_file = 1; config->deployment_type = kDeploymentType; config->has_deployment_type = 1; config->deployed_in_app_store = [GULAppEnvironmentUtil isFromAppStore]; config->has_deployed_in_app_store = 1; } /** Populates the given proto with installed services data. * * @param config The proto to populate */ void FIRPopulateProtoWithInstalledServices(logs_proto_mobilesdk_ios_ICoreConfiguration *config) { NSMutableArray *sdkServiceInstalledArray = [NSMutableArray array]; // AdMob if (NSClassFromString(@"GADBannerView") != nil) { [sdkServiceInstalledArray addObject:@(FIRMapFromServiceStringToTypeEnum(kFIRServiceAdMob))]; } // CloudMessaging if (NSClassFromString(@"FIRMessaging") != nil) { [sdkServiceInstalledArray addObject:@(FIRMapFromServiceStringToTypeEnum(kFIRServiceMessaging))]; } // RemoteConfig if (NSClassFromString(@"FIRRemoteConfig") != nil) { [sdkServiceInstalledArray addObject:@(FIRMapFromServiceStringToTypeEnum(kFIRServiceRemoteConfig))]; } // Measurement/Analtyics if (NSClassFromString(@"FIRAnalytics") != nil) { [sdkServiceInstalledArray addObject:@(FIRMapFromServiceStringToTypeEnum(kFIRServiceMeasurement))]; } // ML Model Interpreter if (NSClassFromString(@"FIRCustomModelInterpreter") != nil) { [sdkServiceInstalledArray addObject:@(FIRMapFromServiceStringToTypeEnum(kFIRServiceMLModelInterpreter))]; } // Database if (NSClassFromString(@"FIRDatabase") != nil) { [sdkServiceInstalledArray addObject:@(FIRMapFromServiceStringToTypeEnum(kFIRServiceDatabase))]; } // DynamicDeepLink if (NSClassFromString(@"FIRDynamicLinks") != nil) { [sdkServiceInstalledArray addObject:@(FIRMapFromServiceStringToTypeEnum(kFIRServiceDynamicLinks))]; } // Auth if (NSClassFromString(@"FIRAuth") != nil) { [sdkServiceInstalledArray addObject:@(FIRMapFromServiceStringToTypeEnum(kFIRServiceAuth))]; } // AuthUI if (NSClassFromString(@"FUIAuth") != nil) { [sdkServiceInstalledArray addObject:@(FIRMapFromServiceStringToTypeEnum(kFIRServiceAuthUI))]; } // Firestore if (NSClassFromString(@"FIRFirestore") != nil) { [sdkServiceInstalledArray addObject:@(FIRMapFromServiceStringToTypeEnum(kFIRServiceFirestore))]; } // Functions if (NSClassFromString(@"FIRFunctions") != nil) { [sdkServiceInstalledArray addObject:@(FIRMapFromServiceStringToTypeEnum(kFIRServiceFunctions))]; } // Performance if (NSClassFromString(@"FIRPerformance") != nil) { [sdkServiceInstalledArray addObject:@(FIRMapFromServiceStringToTypeEnum(kFIRServicePerformance))]; } // Storage if (NSClassFromString(@"FIRStorage") != nil) { [sdkServiceInstalledArray addObject:@(FIRMapFromServiceStringToTypeEnum(kFIRServiceStorage))]; } // SignIn via Google pod if (NSClassFromString(@"GIDSignIn") != nil && NSClassFromString(@"GGLContext") != nil) { [sdkServiceInstalledArray addObject:@(FIRMapFromServiceStringToTypeEnum(kGGLServiceSignIn))]; } // Analytics via Google pod if (NSClassFromString(@"GAI") != nil && NSClassFromString(@"GGLContext") != nil) { [sdkServiceInstalledArray addObject:@(FIRMapFromServiceStringToTypeEnum(kGGLServiceAnalytics))]; } // In-App Messaging if (NSClassFromString(@"FIRInAppMessaging") != nil) { [sdkServiceInstalledArray addObject:@(FIRMapFromServiceStringToTypeEnum(kFIRServiceIAM))]; } logs_proto_mobilesdk_ios_ICoreConfiguration_ServiceType *servicesInstalled = calloc(sdkServiceInstalledArray.count, sizeof(logs_proto_mobilesdk_ios_ICoreConfiguration_ServiceType)); if (servicesInstalled == NULL) { return; } for (NSUInteger i = 0; i < sdkServiceInstalledArray.count; i++) { NSNumber *typeEnum = sdkServiceInstalledArray[i]; logs_proto_mobilesdk_ios_ICoreConfiguration_ServiceType serviceType = (int32_t)typeEnum.integerValue; servicesInstalled[i] = serviceType; } config->sdk_service_installed = servicesInstalled; config->sdk_service_installed_count = (int32_t)sdkServiceInstalledArray.count; } /** Populates the proto with Info.plist values. * * @param config The proto to populate. */ void FIRPopulateProtoWithInfoPlistValues(logs_proto_mobilesdk_ios_ICoreConfiguration *config) { NSDictionary *info = [[NSBundle mainBundle] infoDictionary]; NSString *xcodeVersion = info[@"DTXcodeBuild"] ?: @""; NSString *sdkVersion = info[@"DTSDKBuild"] ?: @""; NSString *combinedVersions = [NSString stringWithFormat:@"%@-%@", xcodeVersion, sdkVersion]; config->apple_framework_version = FIREncodeString(combinedVersions); NSString *minVersion = info[@"MinimumOSVersion"]; if (minVersion) { config->min_supported_ios_version = FIREncodeString(minVersion); } // Apps can turn off swizzling in the Info.plist, check if they've explicitly set the value and // report it. It's enabled by default. NSNumber *appDelegateSwizzledNum = info[@"FirebaseAppDelegateProxyEnabled"]; BOOL appDelegateSwizzled = YES; if ([appDelegateSwizzledNum isKindOfClass:[NSNumber class]]) { appDelegateSwizzled = [appDelegateSwizzledNum boolValue]; } config->swizzling_enabled = appDelegateSwizzled; config->has_swizzling_enabled = 1; } #pragma mark - FIRCoreDiagnosticsInterop + (void)sendDiagnosticsData:(nonnull id)diagnosticsData { FIRCoreDiagnostics *diagnostics = [FIRCoreDiagnostics sharedInstance]; [diagnostics sendDiagnosticsData:diagnosticsData]; } - (void)sendDiagnosticsData:(nonnull id)diagnosticsData { dispatch_async(self.diagnosticsQueue, ^{ NSDictionary *diagnosticObjects = diagnosticsData.diagnosticObjects; NSNumber *isDataCollectionDefaultEnabled = diagnosticObjects[kFIRCDIsDataCollectionDefaultEnabledKey]; if (isDataCollectionDefaultEnabled && ![isDataCollectionDefaultEnabled boolValue]) { return; } // Create the proto. logs_proto_mobilesdk_ios_ICoreConfiguration icore_config = logs_proto_mobilesdk_ios_ICoreConfiguration_init_default; icore_config.using_gdt = 1; icore_config.has_using_gdt = 1; // Populate the proto with information. FIRPopulateProtoWithInfoFromUserInfoParams(&icore_config, diagnosticObjects); FIRPopulateProtoWithCommonInfoFromApp(&icore_config, diagnosticObjects); FIRPopulateProtoWithInstalledServices(&icore_config); FIRPopulateProtoWithInfoPlistValues(&icore_config); [self setHeartbeatFlagIfNeededToConfig:&icore_config]; // This log object is capable of converting the proto to bytes. FIRCoreDiagnosticsLog *log = [[FIRCoreDiagnosticsLog alloc] initWithConfig:icore_config]; // Send the log as a telemetry event. GDTCOREvent *event = [self.transport eventForTransport]; event.dataObject = (id)log; [self.transport sendTelemetryEvent:event]; }); } #pragma mark - Heartbeat - (void)setHeartbeatFlagIfNeededToConfig:(logs_proto_mobilesdk_ios_ICoreConfiguration *)config { // Check if need to send a heartbeat. NSDate *currentDate = [NSDate date]; NSDate *lastCheckin = [self.heartbeatDateStorage heartbeatDateForTag:kFIRCoreDiagnosticsHeartbeatTag]; if (lastCheckin) { // Ensure the previous checkin was on a different date in the past. if ([self isDate:currentDate inSameDayOrBeforeThan:lastCheckin]) { return; } } // Update heartbeat sent date. [self.heartbeatDateStorage setHearbeatDate:currentDate forTag:kFIRCoreDiagnosticsHeartbeatTag]; // Set the flag. config->sdk_name = logs_proto_mobilesdk_ios_ICoreConfiguration_ServiceType_ICORE; config->has_sdk_name = 1; } - (BOOL)isDate:(NSDate *)date1 inSameDayOrBeforeThan:(NSDate *)date2 { return [[NSCalendar currentCalendar] isDate:date1 inSameDayAsDate:date2] || [date1 compare:date2] == NSOrderedAscending; } @end