#import "ThinkingAnalyticsSDKPrivate.h" #import "TDAutoTrackManager.h" #import "TDCalibratedTimeWithNTP.h" #import "TDConfig.h" #import "TDPublicConfig.h" #import "TDFile.h" #import "TDNetwork.h" #if !__has_feature(objc_arc) #error The ThinkingSDK library must be compiled with ARC enabled #endif @interface TDPresetProperties (ThinkingAnalytics) - (instancetype)initWithDictionary:(NSDictionary *)dict; - (void)updateValuesWithDictionary:(NSDictionary *)dict; @end @interface ThinkingAnalyticsSDK () @property (atomic, strong) TDNetwork *network; @property (atomic, strong) TDAutoTrackManager *autoTrackManager; @property (strong,nonatomic) TDFile *file; @end @implementation ThinkingAnalyticsSDK static NSMutableDictionary *instances; static NSString *defaultProjectAppid; static BOOL isWifi; static BOOL isWwan; static TDCalibratedTime *calibratedTime; static dispatch_queue_t serialQueue; static dispatch_queue_t networkQueue; + (nullable ThinkingAnalyticsSDK *)sharedInstance { if (instances.count == 0) { TDLogError(@"sharedInstance called before creating a Thinking instance"); return nil; } return instances[defaultProjectAppid]; } + (ThinkingAnalyticsSDK *)sharedInstanceWithAppid:(NSString *)appid { if (instances[appid]) { return instances[appid]; } else { TDLogError(@"sharedInstanceWithAppid called before creating a Thinking instance"); return nil; } } + (ThinkingAnalyticsSDK *)startWithAppId:(NSString *)appId withUrl:(NSString *)url withConfig:(TDConfig *)config { if (instances[appId]) { return instances[appId]; } else if (![url isKindOfClass:[NSString class]] || url.length == 0) { return nil; } return [[self alloc] initWithAppkey:appId withServerURL:url withConfig:config]; } + (ThinkingAnalyticsSDK *)startWithAppId:(NSString *)appId withUrl:(NSString *)url { return [ThinkingAnalyticsSDK startWithAppId:appId withUrl:url withConfig:nil]; } + (ThinkingAnalyticsSDK *)startWithConfig:(nullable TDConfig *)config { return [ThinkingAnalyticsSDK startWithAppId:config.appid withUrl:config.configureURL withConfig:config]; } - (instancetype)init:(NSString *)appID { if (self = [super init]) { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ instances = [NSMutableDictionary dictionary]; defaultProjectAppid = appID; }); } return self; } + (void)initialize { static dispatch_once_t ThinkingOnceToken; dispatch_once(&ThinkingOnceToken, ^{ NSString *queuelabel = [NSString stringWithFormat:@"cn.thinkingdata.%p", (void *)self]; serialQueue = dispatch_queue_create([queuelabel UTF8String], DISPATCH_QUEUE_SERIAL); NSString *networkLabel = [queuelabel stringByAppendingString:@".network"]; networkQueue = dispatch_queue_create([networkLabel UTF8String], DISPATCH_QUEUE_SERIAL); }); } + (dispatch_queue_t)serialQueue { return serialQueue; } + (dispatch_queue_t)networkQueue { return networkQueue; } - (instancetype)initLight:(NSString *)appid withServerURL:(NSString *)serverURL withConfig:(TDConfig *)config { if (self = [self init]) { serverURL = [self checkServerURL:serverURL]; _appid = appid; _isEnabled = YES; _serverURL = serverURL; _config = [config copy]; _config.configureURL = serverURL; self.trackTimer = [NSMutableDictionary dictionary]; _timeFormatter = [[NSDateFormatter alloc] init]; _timeFormatter.dateFormat = kDefaultTimeFormat; _timeFormatter.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US"]; _timeFormatter.calendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSCalendarIdentifierGregorian]; _timeFormatter.timeZone = config.defaultTimeZone; self.file = [[TDFile alloc] initWithAppid:appid]; NSString *keyPattern = @"^[a-zA-Z][a-zA-Z\\d_]{0,49}$"; self.regexKey = [NSPredicate predicateWithFormat:@"SELF MATCHES %@", keyPattern]; self.dataQueue = [TDSqliteDataQueue sharedInstanceWithAppid:appid]; if (self.dataQueue == nil) { TDLogError(@"SqliteException: init SqliteDataQueue failed"); } _network = [[TDNetwork alloc] init]; _network.debugMode = config.debugMode; _network.appid = appid; _network.sessionDidReceiveAuthenticationChallenge = config.securityPolicy.sessionDidReceiveAuthenticationChallenge; if (config.debugMode == ThinkingAnalyticsDebugOnly || config.debugMode == ThinkingAnalyticsDebug) { _network.serverDebugURL = [NSURL URLWithString:[NSString stringWithFormat:@"%@/data_debug", serverURL]]; } _network.securityPolicy = config.securityPolicy; } return self; } - (instancetype)initWithAppkey:(NSString *)appid withServerURL:(NSString *)serverURL withConfig:(TDConfig *)config { if (self = [self init:appid]) { serverURL = [self checkServerURL:serverURL]; self.serverURL = serverURL; self.appid = appid; if (!config) { config = TDConfig.defaultTDConfig; } _config = [config copy]; _config.appid = appid; _config.configureURL = serverURL; self.file = [[TDFile alloc] initWithAppid:appid]; [self retrievePersistedData]; //次序不能调整 [_config updateConfig]; self.trackTimer = [NSMutableDictionary dictionary]; _timeFormatter = [[NSDateFormatter alloc] init]; _timeFormatter.dateFormat = kDefaultTimeFormat; _timeFormatter.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US"]; _timeFormatter.calendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSCalendarIdentifierGregorian]; _timeFormatter.timeZone = config.defaultTimeZone; _applicationWillResignActive = NO; _ignoredViewControllers = [[NSMutableSet alloc] init]; _ignoredViewTypeList = [[NSMutableSet alloc] init]; self.taskId = UIBackgroundTaskInvalid; NSString *keyPattern = @"^[a-zA-Z][a-zA-Z\\d_]{0,49}$"; self.regexKey = [NSPredicate predicateWithFormat:@"SELF MATCHES %@", keyPattern]; NSString *keyAutoTrackPattern = @"^([a-zA-Z][a-zA-Z\\d_]{0,49}|\\#(resume_from_background|app_crashed_reason|screen_name|referrer|title|url|element_id|element_type|element_content|element_position))$"; self.regexAutoTrackKey = [NSPredicate predicateWithFormat:@"SELF MATCHES %@", keyAutoTrackPattern]; self.dataQueue = [TDSqliteDataQueue sharedInstanceWithAppid:appid]; if (self.dataQueue == nil) { TDLogError(@"SqliteException: init SqliteDataQueue failed"); } [self setNetRadioListeners]; self.autoTrackManager = [TDAutoTrackManager sharedManager]; _network = [[TDNetwork alloc] init]; _network.debugMode = config.debugMode; _network.appid = appid; _network.sessionDidReceiveAuthenticationChallenge = config.securityPolicy.sessionDidReceiveAuthenticationChallenge; if (config.debugMode == ThinkingAnalyticsDebugOnly || config.debugMode == ThinkingAnalyticsDebug) { _network.serverDebugURL = [NSURL URLWithString:[NSString stringWithFormat:@"%@/data_debug",serverURL]]; } _network.serverURL = [NSURL URLWithString:[NSString stringWithFormat:@"%@/sync",serverURL]]; _network.securityPolicy = config.securityPolicy; [self sceneSupportSetting]; #ifdef __IPHONE_13_0 if (@available(iOS 13.0, *)) { if (!_isEnableSceneSupport) { [self launchedIntoBackground:config.launchOptions]; } else if (config.launchOptions && [config.launchOptions objectForKey:UIApplicationLaunchOptionsLocationKey]) { _relaunchInBackGround = YES; } else { _relaunchInBackGround = NO; } } #else [self launchedIntoBackground:config.launchOptions]; #endif [self startFlushTimer]; [self setApplicationListeners]; instances[appid] = self; TDLogInfo(@"Thinking Analytics SDK %@ instance initialized successfully with mode: %@, APP ID: %@, server url: %@, device ID: %@", [TDDeviceInfo libVersion], [self modeEnumToString:config.debugMode], appid, serverURL, [self getDeviceId]); } return self; } - (void)launchedIntoBackground:(NSDictionary *)launchOptions { td_dispatch_main_sync_safe(^{ if (launchOptions && [launchOptions objectForKey:UIApplicationLaunchOptionsLocationKey]) { UIApplicationState applicationState = [UIApplication sharedApplication].applicationState; if (applicationState == UIApplicationStateBackground) { self->_relaunchInBackGround = YES; } } }); } - (NSString *)description { return [NSString stringWithFormat:@"", (void *)self, self.appid, self.serverURL]; } + (UIApplication *)sharedUIApplication { if ([[UIApplication class] respondsToSelector:@selector(sharedApplication)]) { return [[UIApplication class] performSelector:@selector(sharedApplication)]; } return nil; } #pragma mark - EnableTracking - (void)enableTracking:(BOOL)enabled { self.isEnabled = enabled; dispatch_async(serialQueue, ^{ [self.file archiveIsEnabled:self.isEnabled]; }); } - (BOOL)hasDisabled { return !_isEnabled || _isOptOut; } - (void)optOutTracking { TDLogDebug(@"%@ optOutTracking...", self); [self doOptOutTracking]; } - (void)doOptOutTracking { self.isOptOut = YES; @synchronized (self.trackTimer) { [self.trackTimer removeAllObjects]; } @synchronized (self.superProperty) { self.superProperty = [NSDictionary new]; } @synchronized (self.identifyId) { self.identifyId = [TDDeviceInfo sharedManager].uniqueId; } @synchronized (self.accountId) { self.accountId = nil; } dispatch_async(serialQueue, ^{ @synchronized (instances) { [self.dataQueue deleteAll:self.appid]; } [self.file archiveAccountID:nil]; [self.file archiveIdentifyId:nil]; [self.file archiveSuperProperties:nil]; [self.file archiveOptOut:YES]; }); } - (void)optOutTrackingAndDeleteUser { TDLogDebug(@"%@ optOutTrackingAndDeleteUser...", self); TDEventModel *eventData = [[TDEventModel alloc] initWithEventName:nil eventType:TD_EVENT_TYPE_USER_DEL]; eventData.persist = NO; [self tdInternalTrack:eventData]; [self doOptOutTracking]; } - (void)optInTracking { TDLogDebug(@"%@ optInTracking...", self); self.isOptOut = NO; [self.file archiveOptOut:NO]; } #pragma mark - LightInstance - (ThinkingAnalyticsSDK *)createLightInstance { ThinkingAnalyticsSDK *lightInstance = [[LightThinkingAnalyticsSDK alloc] initWithAPPID:self.appid withServerURL:self.serverURL withConfig:self.config]; lightInstance.identifyId = [TDDeviceInfo sharedManager].uniqueId; lightInstance.relaunchInBackGround = self.relaunchInBackGround; lightInstance.isEnableSceneSupport = self.isEnableSceneSupport; return lightInstance; } #pragma mark - Persistence - (void)retrievePersistedData { self.accountId = [self.file unarchiveAccountID]; self.superProperty = [self.file unarchiveSuperProperties]; self.identifyId = [self.file unarchiveIdentifyID]; self.isEnabled = [self.file unarchiveEnabled]; self.isOptOut = [self.file unarchiveOptOut]; self.config.uploadSize = [self.file unarchiveUploadSize]; self.config.uploadInterval = [self.file unarchiveUploadInterval]; if (self.identifyId.length == 0) { self.identifyId = [TDDeviceInfo sharedManager].uniqueId; } // 兼容老版本 if (self.accountId.length == 0) { self.accountId = [self.file unarchiveAccountID]; [self.file archiveAccountID:self.accountId]; [self.file deleteOldLoginId]; } } - (NSInteger)saveEventsData:(NSDictionary *)data { NSMutableDictionary *event = [[NSMutableDictionary alloc] initWithDictionary:data]; NSInteger count; @synchronized (instances) { count = [self.dataQueue addObject:event withAppid:self.appid]; } return count; } - (void)deleteAll { dispatch_async(serialQueue, ^{ @synchronized (instances) { [self.dataQueue deleteAll:self.appid]; } }); } #pragma mark - UIApplication Events - (void)setApplicationListeners { NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter]; [notificationCenter addObserver:self selector:@selector(applicationWillEnterForeground:) name:UIApplicationWillEnterForegroundNotification object:nil]; [notificationCenter addObserver:self selector:@selector(applicationDidBecomeActive:) name:UIApplicationDidBecomeActiveNotification object:nil]; [notificationCenter addObserver:self selector:@selector(applicationWillResignActive:) name:UIApplicationWillResignActiveNotification object:nil]; [notificationCenter addObserver:self selector:@selector(applicationDidEnterBackground:) name:UIApplicationDidEnterBackgroundNotification object:nil]; [notificationCenter addObserver:self selector:@selector(applicationWillTerminate:) name:UIApplicationWillTerminateNotification object:nil]; } - (void)setNetRadioListeners { if ((_reachability = SCNetworkReachabilityCreateWithName(NULL,"thinkingdata.cn")) != NULL) { SCNetworkReachabilityFlags flags; BOOL didRetrieveFlags = SCNetworkReachabilityGetFlags(_reachability, &flags); if (didRetrieveFlags) { isWifi = (flags & kSCNetworkReachabilityFlagsReachable) && !(flags & kSCNetworkReachabilityFlagsIsWWAN); isWwan = (flags & kSCNetworkReachabilityFlagsIsWWAN); } SCNetworkReachabilityContext context = {0, (__bridge void *)self, NULL, NULL, NULL}; if (SCNetworkReachabilitySetCallback(_reachability, ThinkingReachabilityCallback, &context)) { if (!SCNetworkReachabilitySetDispatchQueue(_reachability, serialQueue)) { SCNetworkReachabilitySetCallback(_reachability, NULL, NULL); } } } } - (void)applicationWillEnterForeground:(NSNotification *)notification { TDLogDebug(@"%@ application will enter foreground", self); if (UIApplication.sharedApplication.applicationState == UIApplicationStateBackground) { _relaunchInBackGround = NO; _appRelaunched = YES; dispatch_async(serialQueue, ^{ if (self.taskId != UIBackgroundTaskInvalid) { [[ThinkingAnalyticsSDK sharedUIApplication] endBackgroundTask:self.taskId]; self.taskId = UIBackgroundTaskInvalid; } }); } } - (void)applicationDidEnterBackground:(NSNotification *)notification { TDLogDebug(@"%@ application did enter background", self); _relaunchInBackGround = NO; _applicationWillResignActive = NO; __block UIBackgroundTaskIdentifier backgroundTask = [[ThinkingAnalyticsSDK sharedUIApplication] beginBackgroundTaskWithExpirationHandler:^{ [[ThinkingAnalyticsSDK sharedUIApplication] endBackgroundTask:backgroundTask]; self.taskId = UIBackgroundTaskInvalid; }]; self.taskId = backgroundTask; dispatch_group_t bgGroup = dispatch_group_create(); dispatch_group_enter(bgGroup); dispatch_async(serialQueue, ^{ NSNumber *currentTimeStamp = @([[NSDate date] timeIntervalSince1970]); @synchronized (self.trackTimer) { NSArray *keys = [self.trackTimer allKeys]; for (NSString *key in keys) { if ([key isEqualToString:TD_APP_END_EVENT]) { continue; } NSMutableDictionary *eventTimer = [[NSMutableDictionary alloc] initWithDictionary:self.trackTimer[key]]; if (eventTimer) { NSNumber *eventBegin = [eventTimer valueForKey:TD_EVENT_START]; NSNumber *eventDuration = [eventTimer valueForKey:TD_EVENT_DURATION]; double usedTime; if (eventDuration) { usedTime = [currentTimeStamp doubleValue] - [eventBegin doubleValue] + [eventDuration doubleValue]; } else { usedTime = [currentTimeStamp doubleValue] - [eventBegin doubleValue]; } [eventTimer setObject:[NSNumber numberWithDouble:usedTime] forKey:TD_EVENT_DURATION]; self.trackTimer[key] = eventTimer; } } } dispatch_group_leave(bgGroup); }); if (_config.autoTrackEventType & ThinkingAnalyticsEventTypeAppEnd) { NSString *screenName = NSStringFromClass([[TDAutoTrackManager topPresentedViewController] class]); screenName = (screenName == nil) ? @"" : screenName; [self autotrack:TD_APP_END_EVENT properties:@{TD_EVENT_PROPERTY_SCREEN_NAME: screenName} withTime:nil]; } dispatch_group_enter(bgGroup); [self syncWithCompletion:^{ dispatch_group_leave(bgGroup); }]; dispatch_group_notify(bgGroup, dispatch_get_main_queue(), ^{ if (self.taskId != UIBackgroundTaskInvalid) { [[ThinkingAnalyticsSDK sharedUIApplication] endBackgroundTask:self.taskId]; self.taskId = UIBackgroundTaskInvalid; } }); } - (void)applicationWillTerminate:(UIApplication *)application { } - (void)applicationWillResignActive:(NSNotification *)notification { TDLogDebug(@"%@ application will resign active", self); _applicationWillResignActive = YES; [self stopFlushTimer]; } - (void)applicationDidBecomeActive:(NSNotification *)notification { TDLogDebug(@"%@ application did become active", self); [self startFlushTimer]; if (_applicationWillResignActive) { _applicationWillResignActive = NO; return; } _applicationWillResignActive = NO; dispatch_async(serialQueue, ^{ NSNumber *currentTime = @([[NSDate date] timeIntervalSince1970]); @synchronized (self.trackTimer) { NSArray *keys = [self.trackTimer allKeys]; for (NSString *key in keys) { NSMutableDictionary *eventTimer = [[NSMutableDictionary alloc] initWithDictionary:self.trackTimer[key]]; if (eventTimer) { [eventTimer setValue:currentTime forKey:TD_EVENT_START]; self.trackTimer[key] = eventTimer; } } } }); if (_appRelaunched) { if (_config.autoTrackEventType & ThinkingAnalyticsEventTypeAppStart) { [self autotrack:TD_APP_START_EVENT properties:@{TD_RESUME_FROM_BACKGROUND:@(_appRelaunched)} withTime:nil]; } if (_config.autoTrackEventType & ThinkingAnalyticsEventTypeAppEnd) { [self timeEvent:TD_APP_END_EVENT]; } } } - (void)sceneSupportSetting { NSDictionary *sceneManifest = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"UIApplicationSceneManifest"]; if (sceneManifest) { NSDictionary *sceneConfig = sceneManifest[@"UISceneConfigurations"]; if (sceneConfig.count > 0) { _isEnableSceneSupport = YES; } else { _isEnableSceneSupport = NO; } } else { _isEnableSceneSupport = NO; } } - (void)setNetworkType:(ThinkingAnalyticsNetworkType)type { if ([self hasDisabled]) return; [self.config setNetworkType:type]; } - (ThinkingNetworkType)convertNetworkType:(NSString *)networkType { if ([@"NULL" isEqualToString:networkType]) { return ThinkingNetworkTypeALL; } else if ([@"WIFI" isEqualToString:networkType]) { return ThinkingNetworkTypeWIFI; } else if ([@"2G" isEqualToString:networkType]) { return ThinkingNetworkType2G; } else if ([@"3G" isEqualToString:networkType]) { return ThinkingNetworkType3G; } else if ([@"4G" isEqualToString:networkType]) { return ThinkingNetworkType4G; }else if([@"5G"isEqualToString:networkType]) { return ThinkingNetworkType5G; } return ThinkingNetworkTypeNONE; } static void ThinkingReachabilityCallback(SCNetworkReachabilityRef target, SCNetworkReachabilityFlags flags, void *info) { ThinkingAnalyticsSDK *thinking = (__bridge ThinkingAnalyticsSDK *)info; if (thinking && [thinking isKindOfClass:[ThinkingAnalyticsSDK class]]) { [thinking reachabilityChanged:flags]; } } - (void)reachabilityChanged:(SCNetworkReachabilityFlags)flags { isWifi = (flags & kSCNetworkReachabilityFlagsReachable) && !(flags & kSCNetworkReachabilityFlagsIsWWAN); isWwan = (flags & kSCNetworkReachabilityFlagsIsWWAN); } + (NSString *)currentRadio { NSString *networkType = @"NULL"; @try { static CTTelephonyNetworkInfo *info = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ info = [[CTTelephonyNetworkInfo alloc] init]; }); NSString *currentRadio = nil; #ifdef __IPHONE_12_0 if (@available(iOS 12.0, *)) { NSDictionary *serviceCurrentRadio = [info serviceCurrentRadioAccessTechnology]; if ([serviceCurrentRadio isKindOfClass:[NSDictionary class]] && serviceCurrentRadio.allValues.count>0) { currentRadio = serviceCurrentRadio.allValues[0]; } } #endif if (currentRadio == nil && [info.currentRadioAccessTechnology isKindOfClass:[NSString class]]) { currentRadio = info.currentRadioAccessTechnology; } if ([currentRadio isEqualToString:CTRadioAccessTechnologyLTE]) { networkType = @"4G"; } else if ([currentRadio isEqualToString:CTRadioAccessTechnologyeHRPD] || [currentRadio isEqualToString:CTRadioAccessTechnologyCDMAEVDORevB] || [currentRadio isEqualToString:CTRadioAccessTechnologyCDMAEVDORevA] || [currentRadio isEqualToString:CTRadioAccessTechnologyCDMAEVDORev0] || [currentRadio isEqualToString:CTRadioAccessTechnologyCDMA1x] || [currentRadio isEqualToString:CTRadioAccessTechnologyHSUPA] || [currentRadio isEqualToString:CTRadioAccessTechnologyHSDPA] || [currentRadio isEqualToString:CTRadioAccessTechnologyWCDMA]) { networkType = @"3G"; } else if ([currentRadio isEqualToString:CTRadioAccessTechnologyEdge] || [currentRadio isEqualToString:CTRadioAccessTechnologyGPRS]) { networkType = @"2G"; } #ifdef __IPHONE_14_1 else if (@available(iOS 14.1, *)) { if ([currentRadio isKindOfClass:[NSString class]]) { if([currentRadio isEqualToString:CTRadioAccessTechnologyNRNSA] || [currentRadio isEqualToString:CTRadioAccessTechnologyNR]) { networkType = @"5G"; } } } #endif } @catch (NSException *exception) { TDLogError(@"%@: %@", self, exception); } return networkType; } + (NSString *)getNetWorkStates { if (isWifi) { return @"WIFI"; } else if (isWwan) { return [self currentRadio]; } else { return @"NULL"; } } #pragma mark - Public - (void)track:(NSString *)event { if ([self hasDisabled]) return; [self track:event properties:nil]; } - (void)track:(NSString *)event properties:(NSDictionary *)propertiesDict { if ([self hasDisabled]) return; propertiesDict = [self processParameters:propertiesDict withType:TD_EVENT_TYPE_TRACK withEventName:event withAutoTrack:NO withH5:NO]; TDEventModel *eventData = [[TDEventModel alloc] initWithEventName:event]; eventData.properties = [propertiesDict copy]; eventData.timeValueType = TDTimeValueTypeNone; [self tdInternalTrack:eventData]; } // deprecated 使用 track:properties:time:timeZone: 方法传入 - (void)track:(NSString *)event properties:(NSDictionary *)propertiesDict time:(NSDate *)time { if ([self hasDisabled]) return; propertiesDict = [self processParameters:propertiesDict withType:TD_EVENT_TYPE_TRACK withEventName:event withAutoTrack:NO withH5:NO]; TDEventModel *eventData = [[TDEventModel alloc] initWithEventName:event]; eventData.properties = [propertiesDict copy]; eventData.timeString = [_timeFormatter stringFromDate:time]; eventData.timeValueType = TDTimeValueTypeTimeOnly; [self tdInternalTrack:eventData]; } - (void)track:(NSString *)event properties:(nullable NSDictionary *)properties time:(NSDate *)time timeZone:(NSTimeZone *)timeZone { if ([self hasDisabled]) return; if (timeZone == nil) { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" [self track:event properties:properties time:time]; #pragma clang diagnostic pop return; } properties = [self processParameters:properties withType:TD_EVENT_TYPE_TRACK withEventName:event withAutoTrack:NO withH5:NO]; TDEventModel *eventData = [[TDEventModel alloc] initWithEventName:event]; eventData.properties = [properties copy]; NSDateFormatter *timeFormatter = [[NSDateFormatter alloc] init]; timeFormatter.dateFormat = kDefaultTimeFormat; timeFormatter.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US"]; timeFormatter.calendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSCalendarIdentifierGregorian]; timeFormatter.timeZone = timeZone; eventData.timeString = [timeFormatter stringFromDate:time]; eventData.zoneOffset = [self getTimezoneOffset:time timeZone:timeZone]; eventData.timeValueType = TDTimeValueTypeAll; [self tdInternalTrack:eventData]; } - (void)trackWithEventModel:(TDEventModel *)eventModel { NSDictionary *dic = eventModel.properties; eventModel.properties = [self processParameters:dic withType:eventModel.eventType withEventName:eventModel.eventName withAutoTrack:NO withH5:NO]; [self tdInternalTrack:eventModel]; } #pragma mark - Private - (NSString *)checkServerURL:(NSString *)urlString { urlString = [urlString stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; NSURL *url = [NSURL URLWithString:urlString]; NSString *scheme = [url scheme]; NSString *host = [url host]; NSNumber *port = [url port]; if (scheme && scheme.length>0 && host && host.length>0) { urlString = [NSString stringWithFormat:@"%@://%@", scheme, host]; if (port && [port stringValue]) { urlString = [urlString stringByAppendingFormat:@":%@", [port stringValue]]; } } return urlString; } - (void)h5track:(NSString *)eventName extraID:(NSString *)extraID properties:(NSDictionary *)propertieDict type:(NSString *)type time:(NSString *)time { if ([self hasDisabled]) return; propertieDict = [self processParameters:propertieDict withType:type withEventName:eventName withAutoTrack:NO withH5:YES]; TDEventModel *eventData; if (extraID.length > 0) { if ([type isEqualToString:TD_EVENT_TYPE_TRACK]) { eventData = [[TDEventModel alloc] initWithEventName:eventName eventType:TD_EVENT_TYPE_TRACK_FIRST]; } else { eventData = [[TDEventModel alloc] initWithEventName:eventName eventType:type]; } eventData.extraID = extraID; } else { eventData = [[TDEventModel alloc] initWithEventName:eventName]; } eventData.properties = [propertieDict copy]; if ([propertieDict objectForKey:@"#zone_offset"]) { eventData.zoneOffset = [[propertieDict objectForKey:@"#zone_offset"] doubleValue]; eventData.timeValueType = TDTimeValueTypeAll; } else { eventData.timeValueType = TDTimeValueTypeTimeOnly; } eventData.timeString = time; [self tdInternalTrack:eventData]; } - (void)autotrack:(NSString *)event properties:(NSDictionary *)propertieDict withTime:(NSDate *)time { if ([self hasDisabled]) return; NSMutableDictionary *properties = [NSMutableDictionary dictionary]; [properties addEntriesFromDictionary:propertieDict]; NSDictionary *superProperty = [NSDictionary dictionary]; superProperty = [self processParameters:superProperty withType:TD_EVENT_TYPE_TRACK withEventName:event withAutoTrack:YES withH5:NO]; [properties addEntriesFromDictionary:superProperty]; TDEventModel *eventData = [[TDEventModel alloc] initWithEventName:event]; eventData.properties = [properties copy]; eventData.timeString = [_timeFormatter stringFromDate:time]; eventData.timeValueType = TDTimeValueTypeNone; [self tdInternalTrack:eventData]; } - (double)getTimezoneOffset:(NSDate *)date timeZone:(NSTimeZone *)timeZone { NSTimeZone *tz = timeZone ? timeZone : [NSTimeZone localTimeZone]; NSInteger sourceGMTOffset = [tz secondsFromGMTForDate:date]; return (double)sourceGMTOffset/3600; } - (void)track:(NSString *)event withProperties:(NSDictionary *)properties withType:(NSString *)type { [self track:event withProperties:properties withType:type withTime:nil]; } - (void)track:(NSString *)event withProperties:(NSDictionary *)properties withType:(NSString *)type withTime:(NSDate *)time { if ([self hasDisabled]) return; properties = [self processParameters:properties withType:type withEventName:event withAutoTrack:NO withH5:NO]; TDEventModel *eventData = [[TDEventModel alloc] initWithEventName:event eventType:type]; eventData.properties = [properties copy]; if (time) { eventData.timeString = [_timeFormatter stringFromDate:time]; eventData.timeValueType = TDTimeValueTypeTimeOnly; } else { eventData.timeValueType = TDTimeValueTypeNone; } [self tdInternalTrack:eventData]; } + (BOOL)isTrackEvent:(NSString *)eventType { return [TD_EVENT_TYPE_TRACK isEqualToString:eventType] || [TD_EVENT_TYPE_TRACK_FIRST isEqualToString:eventType] || [TD_EVENT_TYPE_TRACK_UPDATE isEqualToString:eventType] || [TD_EVENT_TYPE_TRACK_OVERWRITE isEqualToString:eventType] ; } #pragma mark - User - (void)user_add:(NSString *)propertyName andPropertyValue:(NSNumber *)propertyValue { [self user_add:propertyName andPropertyValue:propertyValue withTime:nil]; } - (void)user_add:(NSString *)propertyName andPropertyValue:(NSNumber *)propertyValue withTime:(NSDate *)time { if (propertyName && propertyValue) { [self track:nil withProperties:@{propertyName:propertyValue} withType:TD_EVENT_TYPE_USER_ADD withTime:time]; } } - (void)user_add:(NSDictionary *)properties { [self user_add:properties withTime:nil]; } - (void)user_add:(NSDictionary *)properties withTime:(NSDate *)time { if ([self hasDisabled]) return; [self track:nil withProperties:properties withType:TD_EVENT_TYPE_USER_ADD withTime:time]; } - (void)user_setOnce:(NSDictionary *)properties { [self user_setOnce:properties withTime:nil]; } - (void)user_setOnce:(NSDictionary *)properties withTime:(NSDate *)time { [self track:nil withProperties:properties withType:TD_EVENT_TYPE_USER_SETONCE withTime:time]; } - (void)user_set:(NSDictionary *)properties { [self user_set:properties withTime:nil]; } - (void)user_set:(NSDictionary *)properties withTime:(NSDate *)time { [self track:nil withProperties:properties withType:TD_EVENT_TYPE_USER_SET withTime:time]; } - (void)user_unset:(NSString *)propertyName { [self user_unset:propertyName withTime:nil]; } - (void)user_unset:(NSString *)propertyName withTime:(NSDate *)time { if ([propertyName isKindOfClass:[NSString class]] && propertyName.length > 0) { NSDictionary *properties = @{propertyName: @0}; [self track:nil withProperties:properties withType:TD_EVENT_TYPE_USER_UNSET withTime:time]; } } - (void)user_delete { [self user_delete:nil]; } - (void)user_delete:(NSDate *)time { [self track:nil withProperties:nil withType:TD_EVENT_TYPE_USER_DEL withTime:time]; } - (void)user_append:(NSDictionary *)properties { [self user_append:properties withTime:nil]; } - (void)user_append:(NSDictionary *)properties withTime:(NSDate *)time { [self track:nil withProperties:properties withType:TD_EVENT_TYPE_USER_APPEND withTime:time]; } + (void)setCustomerLibInfoWithLibName:(NSString *)libName libVersion:(NSString *)libVersion { if (libName.length > 0) { [TDDeviceInfo sharedManager].libName = libName; } if (libVersion.length > 0) { [TDDeviceInfo sharedManager].libVersion = libVersion; } [[TDDeviceInfo sharedManager] updateAutomaticData]; } - (NSString *)getDistinctId { return [self.identifyId copy]; } + (NSString *)getSDKVersion { return TDPublicConfig.version; } - (NSString *)getDeviceId { return [TDDeviceInfo sharedManager].deviceId; } - (void)registerDynamicSuperProperties:(NSDictionary *(^)(void)) dynamicSuperProperties { if ([self hasDisabled]) return; self.dynamicSuperProperties = dynamicSuperProperties; } - (void)setSuperProperties:(NSDictionary *)properties { if ([self hasDisabled]) return; if (properties == nil) { return; } properties = [properties copy]; if ([TDLogging sharedInstance].loggingLevel != TDLoggingLevelNone && ![self checkEventProperties:properties withEventType:nil haveAutoTrackEvents:NO]) { TDLogError(@"%@ propertieDict error.", properties); return; } @synchronized (self.superProperty) { NSMutableDictionary *tmp = [NSMutableDictionary dictionaryWithDictionary:self.superProperty]; [tmp addEntriesFromDictionary:[properties copy]]; self.superProperty = [NSDictionary dictionaryWithDictionary:tmp]; } dispatch_async(serialQueue, ^{ [self.file archiveSuperProperties:self.superProperty]; }); } - (void)unsetSuperProperty:(NSString *)propertyKey { if ([self hasDisabled]) return; if (![propertyKey isKindOfClass:[NSString class]] || propertyKey.length == 0) return; @synchronized (self.superProperty) { NSMutableDictionary *tmp = [NSMutableDictionary dictionaryWithDictionary:self.superProperty]; tmp[propertyKey] = nil; self.superProperty = [NSDictionary dictionaryWithDictionary:tmp]; } dispatch_async(serialQueue, ^{ [self.file archiveSuperProperties:self.superProperty]; }); } - (void)clearSuperProperties { if ([self hasDisabled]) return; @synchronized (self.superProperty) { self.superProperty = @{}; } dispatch_async(serialQueue, ^{ [self.file archiveSuperProperties:self.superProperty]; }); } - (NSDictionary *)currentSuperProperties { return [self.superProperty copy]; } - (TDPresetProperties *)getPresetProperties { NSString *bundleId = [TDDeviceInfo bundleId]; NSString *networkType = [self.class getNetWorkStates]; double offset = [self getTimezoneOffset:[NSDate date] timeZone:_config.defaultTimeZone]; NSDictionary *autoDic = [[TDDeviceInfo sharedManager] collectAutomaticProperties]; NSMutableDictionary *presetDic = [NSMutableDictionary new]; [presetDic setObject:bundleId?:@"" forKey:@"#bundle_id"]; [presetDic setObject:autoDic[@"#carrier"]?:@"" forKey:@"#carrier"]; [presetDic setObject:autoDic[@"#device_id"]?:@"" forKey:@"#device_id"]; [presetDic setObject:autoDic[@"#device_model"]?:@"" forKey:@"#device_model"]; [presetDic setObject:autoDic[@"#manufacturer"]?:@"" forKey:@"#manufacturer"]; [presetDic setObject:networkType?:@"" forKey:@"#network_type"]; [presetDic setObject:autoDic[@"#os"]?:@"" forKey:@"#os"]; [presetDic setObject:autoDic[@"#os_version"]?:@"" forKey:@"#os_version"]; [presetDic setObject:autoDic[@"#screen_height"]?:@(0) forKey:@"#screen_height"]; [presetDic setObject:autoDic[@"#screen_width"]?:@(0) forKey:@"#screen_width"]; [presetDic setObject:autoDic[@"#system_language"]?:@"" forKey:@"#system_language"]; [presetDic setObject:@(offset)?:@(0) forKey:@"#zone_offset"]; static TDPresetProperties *presetProperties = nil; if (presetProperties == nil) { presetProperties = [[TDPresetProperties alloc] initWithDictionary:presetDic]; } else { [presetProperties updateValuesWithDictionary:presetDic]; } return presetProperties; } - (void)identify:(NSString *)distinctId { if ([self hasDisabled]) return; if (![distinctId isKindOfClass:[NSString class]] || distinctId.length == 0) { TDLogError(@"identify cannot null"); return; } @synchronized (self.identifyId) { self.identifyId = distinctId; } dispatch_async(serialQueue, ^{ [self.file archiveIdentifyId:distinctId]; }); } - (void)login:(NSString *)accountId { if ([self hasDisabled]) return; if (![accountId isKindOfClass:[NSString class]] || accountId.length == 0) { TDLogError(@"accountId invald", accountId); return; } @synchronized (self.accountId) { self.accountId = accountId; } dispatch_async(serialQueue, ^{ [self.file archiveAccountID:accountId]; }); } - (void)logout { if ([self hasDisabled]) return; @synchronized (self.accountId) { self.accountId = nil; } dispatch_async(serialQueue, ^{ [self.file archiveAccountID:nil]; }); } - (void)timeEvent:(NSString *)event { if ([self hasDisabled]) return; if (![event isKindOfClass:[NSString class]] || event.length == 0 || ![self isValidName:event isAutoTrack:NO]) { NSString *errMsg = [NSString stringWithFormat:@"timeEvent parameter[%@] is not valid", event]; TDLogError(errMsg); return; } NSNumber *eventBegin = @([[NSDate date] timeIntervalSince1970]); @synchronized (self.trackTimer) { self.trackTimer[event] = @{TD_EVENT_START:eventBegin, TD_EVENT_DURATION:[NSNumber numberWithDouble:0]}; }; } - (BOOL)isValidName:(NSString *)name isAutoTrack:(BOOL)isAutoTrack { @try { if (!isAutoTrack) { return [self.regexKey evaluateWithObject:name]; } else { return [self.regexAutoTrackKey evaluateWithObject:name]; } } @catch (NSException *exception) { TDLogError(@"%@: %@", self, exception); return YES; } } - (BOOL)checkEventProperties:(NSDictionary *)properties withEventType:(NSString *)eventType haveAutoTrackEvents:(BOOL)haveAutoTrackEvents { if (![properties isKindOfClass:[NSDictionary class]]) { return NO; } __block BOOL failed = NO; [properties enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) { if (![key isKindOfClass:[NSString class]]) { NSString *errMsg = [NSString stringWithFormat:@"Property name is not valid. The property KEY must be NSString. got: %@ %@", [key class], key]; TDLogError(errMsg); failed = YES; } if (![self isValidName:key isAutoTrack:haveAutoTrackEvents]) { NSString *errMsg = [NSString stringWithFormat:@"Property name[%@] is not valid. The property KEY must be string that starts with English letter, and contains letter, number, and '_'. The max length of the property KEY is 50.", key]; TDLogError(errMsg); failed = YES; } if (![obj isKindOfClass:[NSString class]] && ![obj isKindOfClass:[NSNumber class]] && ![obj isKindOfClass:[NSDate class]] && ![obj isKindOfClass:[NSArray class]]) { NSString *errMsg = [NSString stringWithFormat:@"Property value must be type NSString, NSNumber, NSDate or NSArray. got: %@ %@. ", [obj class], obj]; TDLogError(errMsg); failed = YES; } if (eventType.length > 0 && [eventType isEqualToString:TD_EVENT_TYPE_USER_ADD]) { if (![obj isKindOfClass:[NSNumber class]]) { NSString *errMsg = [NSString stringWithFormat:@"user_add value must be NSNumber. got: %@ %@. ", [obj class], obj]; TDLogError(errMsg); failed = YES; } } if (eventType.length > 0 && [eventType isEqualToString:TD_EVENT_TYPE_USER_APPEND]) { if (![obj isKindOfClass:[NSArray class]]) { NSString *errMsg = [NSString stringWithFormat:@"user_append value must be NSArray. got: %@ %@. ", [obj class], obj]; TDLogError(errMsg); failed = YES; } } if ([obj isKindOfClass:[NSNumber class]]) { if ([obj doubleValue] > 9999999999999.999 || [obj doubleValue] < -9999999999999.999) { NSString *errMsg = [NSString stringWithFormat:@"The number value [%@] is invalid.", obj]; TDLogError(errMsg); failed = YES; } } }]; if (failed) { return NO; } return YES; } - (void)clickFromH5:(NSString *)data { NSData *jsonData = [data dataUsingEncoding:NSUTF8StringEncoding]; NSError *err; NSDictionary *eventDict = [NSJSONSerialization JSONObjectWithData:jsonData options:NSJSONReadingMutableContainers error:&err]; NSString *appid = [eventDict[@"#app_id"] isKindOfClass:[NSString class]] ? eventDict[@"#app_id"] : self.appid; id dataArr = eventDict[@"data"]; if (!err && [dataArr isKindOfClass:[NSArray class]]) { NSDictionary *dataInfo = [dataArr objectAtIndex:0]; if (dataInfo != nil) { NSString *type = [dataInfo objectForKey:@"#type"]; NSString *event_name = [dataInfo objectForKey:@"#event_name"]; NSString *time = [dataInfo objectForKey:@"#time"]; NSDictionary *properties = [dataInfo objectForKey:@"properties"]; NSString *extraID; if ([type isEqualToString:TD_EVENT_TYPE_TRACK]) { extraID = [dataInfo objectForKey:@"#first_check_id"]; } else { extraID = [dataInfo objectForKey:@"#event_id"]; } NSMutableDictionary *dic = [properties mutableCopy]; [dic removeObjectForKey:@"#account_id"]; [dic removeObjectForKey:@"#distinct_id"]; [dic removeObjectForKey:@"#device_id"]; [dic removeObjectForKey:@"#lib"]; [dic removeObjectForKey:@"#lib_version"]; [dic removeObjectForKey:@"#screen_height"]; [dic removeObjectForKey:@"#screen_width"]; ThinkingAnalyticsSDK *instance = [ThinkingAnalyticsSDK sharedInstanceWithAppid:appid]; if (instance) { dispatch_async(serialQueue, ^{ [instance h5track:event_name extraID:extraID properties:dic type:type time:time]; }); } else { dispatch_async(serialQueue, ^{ [self h5track:event_name extraID:extraID properties:dic type:type time:time]; }); } } } } - (void)tdInternalTrack:(TDEventModel *)eventData { if ([self hasDisabled]) return; if (_relaunchInBackGround && !_config.trackRelaunchedInBackgroundEvents) { return; } NSDictionary *propertiesDict = eventData.properties; NSMutableDictionary *properties = [NSMutableDictionary dictionary]; NSString *timeString; NSDate *nowDate = [NSDate date]; NSTimeInterval systemUptime = [[NSProcessInfo processInfo] systemUptime]; double offset = 0; if (eventData.timeValueType == TDTimeValueTypeNone) { timeString = [_timeFormatter stringFromDate:[NSDate date]]; offset = [self getTimezoneOffset:[NSDate date] timeZone:_config.defaultTimeZone]; } else { timeString = eventData.timeString; offset = eventData.zoneOffset; } //增加duration NSDictionary *eventTimer; @synchronized (self.trackTimer) { eventTimer = self.trackTimer[eventData.eventName]; if (eventTimer) { [self.trackTimer removeObjectForKey:eventData.eventName]; } } if (eventTimer) { NSNumber *eventBegin = [eventTimer valueForKey:TD_EVENT_START]; NSNumber *eventDuration = [eventTimer valueForKey:TD_EVENT_DURATION]; double usedTime; NSNumber *currentTimeStamp = @([[NSDate date] timeIntervalSince1970]); if (eventDuration) { usedTime = [currentTimeStamp doubleValue] - [eventBegin doubleValue] + [eventDuration doubleValue]; } else { usedTime = [currentTimeStamp doubleValue] - [eventBegin doubleValue]; } if (usedTime > 0) { properties[@"#duration"] = @([[NSString stringWithFormat:@"%.3f", usedTime] floatValue]); } } if ([ThinkingAnalyticsSDK isTrackEvent:eventData.eventType]) { properties[@"#app_version"] = [TDDeviceInfo sharedManager].appVersion; properties[@"#bundle_id"] = [TDDeviceInfo bundleId]; properties[@"#network_type"] = [[self class] getNetWorkStates]; if (_relaunchInBackGround) { properties[@"#relaunched_in_background"] = @YES; } if (eventData.timeValueType != TDTimeValueTypeTimeOnly) { properties[@"#zone_offset"] = @(offset); } [properties addEntriesFromDictionary:[TDDeviceInfo sharedManager].automaticData]; } [properties addEntriesFromDictionary:propertiesDict]; NSMutableDictionary *dataDic = [NSMutableDictionary dictionary]; dataDic[@"#time"] = timeString; dataDic[@"#uuid"] = [[NSUUID UUID] UUIDString]; if ([eventData.eventType isEqualToString:TD_EVENT_TYPE_TRACK_FIRST]) { /** 首次事件的eventType也是track, 但是会有#first_check_id, 所以初始化的时候首次事件的eventType是 track_first, 用来判断是否需要extraID */ dataDic[@"#type"] = TD_EVENT_TYPE_TRACK; } else { dataDic[@"#type"] = eventData.eventType; } if (self.identifyId.length > 0) { dataDic[@"#distinct_id"] = self.identifyId; } if (properties) { dataDic[@"properties"] = [NSDictionary dictionaryWithDictionary:properties]; } if (eventData.eventName.length > 0) { dataDic[@"#event_name"] = eventData.eventName; } if (eventData.extraID.length > 0) { if ([eventData.eventType isEqualToString:TD_EVENT_TYPE_TRACK_FIRST]) { dataDic[@"#first_check_id"] = eventData.extraID; } else if ([eventData.eventType isEqualToString:TD_EVENT_TYPE_TRACK_UPDATE] || [eventData.eventType isEqualToString:TD_EVENT_TYPE_TRACK_OVERWRITE]) { dataDic[@"#event_id"] = eventData.extraID; } } if (self.accountId.length > 0) { dataDic[@"#account_id"] = self.accountId; } if ([self.config.disableEvents containsObject:eventData.eventName]) { TDLogDebug(@"disabled data:%@", dataDic); return; } if (eventData.persist) { dispatch_async(serialQueue, ^{ NSDictionary *finalDic = dataDic; if (eventData.timeValueType == TDTimeValueTypeNone && calibratedTime && !calibratedTime.stopCalibrate) { finalDic = [self calibratedTime:dataDic withDate:nowDate withSystemDate:systemUptime withEventData:eventData]; } NSInteger count = 0; if (self.config.debugMode == ThinkingAnalyticsDebugOnly || self.config.debugMode == ThinkingAnalyticsDebug) { TDLogDebug(@"queueing debug data:%@", finalDic); [self flushDebugEvent:finalDic]; @synchronized (instances) { count = [self.dataQueue sqliteCountForAppid:self.appid]; } } else { TDLogDebug(@"queueing data:%@", finalDic); // NSError *parseError; // NSData *jsonData = [NSJSONSerialization dataWithJSONObject:finalDic options:NSJSONWritingPrettyPrinted error:&parseError]; // NSString * str = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; // TDLogDebug(@"queueing data:%@", str); count = [self saveEventsData:finalDic]; } if (count >= [self.config.uploadSize integerValue]) { [self flush]; } }); } else { TDLogDebug(@"queueing data flush immediately:%@", dataDic); dispatch_async(serialQueue, ^{ [self flushImmediately:dataDic]; }); } } - (NSDictionary *)calibratedTime:(NSDictionary *)dataDic withDate:(NSDate *)date withSystemDate:(NSTimeInterval)systemUptime withEventData:(TDEventModel *)eventData { NSMutableDictionary *calibratedData = [NSMutableDictionary dictionaryWithDictionary:dataDic]; NSTimeInterval outTime = systemUptime - calibratedTime.systemUptime; NSDate *serverDate = [NSDate dateWithTimeIntervalSince1970:(calibratedTime.serverTime + outTime)]; if (calibratedTime.stopCalibrate) { return dataDic; } NSString *timeString = [_timeFormatter stringFromDate:serverDate]; double offset = [self getTimezoneOffset:serverDate timeZone:_config.defaultTimeZone]; calibratedData[@"#time"] = timeString; NSMutableDictionary *properties = [NSMutableDictionary dictionaryWithDictionary:[calibratedData objectForKey:@"properties"]]; if ([eventData.eventType isEqualToString:TD_EVENT_TYPE_TRACK] && eventData.timeValueType != TDTimeValueTypeTimeOnly) { properties[@"#zone_offset"] = @(offset); } calibratedData[@"properties"] = properties; return calibratedData; } - (void)flushImmediately:(NSDictionary *)dataDic { [self dispatchOnNetworkQueue:^{ [self.network flushEvents:@[dataDic]]; }]; } - (NSDictionary *)processParameters:(NSDictionary *)propertiesDict withType:(NSString *)eventType withEventName:(NSString *)eventName withAutoTrack:(BOOL)autotrack withH5:(BOOL)isH5 { BOOL isTrackEvent = [ThinkingAnalyticsSDK isTrackEvent:eventType]; NSMutableDictionary *properties = [NSMutableDictionary dictionary]; if (isTrackEvent) { [properties addEntriesFromDictionary:self.superProperty]; NSDictionary *dynamicSuperPropertiesDict = self.dynamicSuperProperties?[self.dynamicSuperProperties() copy]:nil; if (dynamicSuperPropertiesDict && [dynamicSuperPropertiesDict isKindOfClass:[NSDictionary class]]) { [properties addEntriesFromDictionary:dynamicSuperPropertiesDict]; } } if (propertiesDict) { if ([propertiesDict isKindOfClass:[NSDictionary class]]) { [properties addEntriesFromDictionary:propertiesDict]; } else { TDLogDebug(@"The property must be NSDictionary. got: %@ %@", [propertiesDict class], propertiesDict); } } if (isTrackEvent && !isH5) { if (![eventName isKindOfClass:[NSString class]] || eventName.length == 0) { NSString *errMsg = [NSString stringWithFormat:@"Event name is invalid. Event name must be NSString. got: %@ %@", [eventName class], eventName]; TDLogError(errMsg); } if (![self isValidName:eventName isAutoTrack:NO]) { NSString *errMsg = [NSString stringWithFormat:@"Event name[ %@ ] is invalid. Event name must be string that starts with English letter, and contains letter, number, and '_'. The max length of the event name is 50.", eventName]; TDLogError(@"%@", errMsg); } } if (properties && !isH5 && [TDLogging sharedInstance].loggingLevel != TDLoggingLevelNone && ![self checkEventProperties:properties withEventType:eventType haveAutoTrackEvents:autotrack]) { NSString *errMsg = [NSString stringWithFormat:@"%@ The data contains invalid key or value.", properties]; TDLogError(errMsg); } if (properties) { NSMutableDictionary *propertiesDic = [NSMutableDictionary dictionaryWithDictionary:properties]; for (NSString *key in [properties keyEnumerator]) { if ([properties[key] isKindOfClass:[NSDate class]]) { NSString *dateStr = [_timeFormatter stringFromDate:(NSDate *)properties[key]]; propertiesDic[key] = dateStr; } else if ([properties[key] isKindOfClass:[NSArray class]]) { NSMutableArray *arrayItem = [properties[key] mutableCopy]; for (int i = 0; i < arrayItem.count ; i++) { if ([arrayItem[i] isKindOfClass:[NSDate class]]) { NSString *dateStr = [_timeFormatter stringFromDate:(NSDate *)arrayItem[i]]; arrayItem[i] = dateStr; } } propertiesDic[key] = arrayItem; } } return [propertiesDic copy]; } return nil; } - (void)flush { [self syncWithCompletion:nil]; } - (void)flushDebugEvent:(NSDictionary *)data { [self dispatchOnNetworkQueue:^{ [self _syncDebug:data]; }]; } - (void)syncWithCompletion:(void (^)(void))handler { [self dispatchOnNetworkQueue:^{ [self _sync]; if (handler) { dispatch_async(dispatch_get_main_queue(), handler); } }]; } - (NSString*)modeEnumToString:(ThinkingAnalyticsDebugMode)enumVal { NSArray *modeEnumArray = [[NSArray alloc] initWithObjects:kModeEnumArray]; return [modeEnumArray objectAtIndex:enumVal]; } - (void)_syncDebug:(NSDictionary *)record { if (self.config.debugMode == ThinkingAnalyticsDebug || self.config.debugMode == ThinkingAnalyticsDebugOnly) { int debugResult = [self.network flushDebugEvents:record withAppid:self.appid]; if (debugResult == -1) { // 降级处理 if (self.config.debugMode == ThinkingAnalyticsDebug) { dispatch_async(serialQueue, ^{ [self saveEventsData:record]; }); self.config.debugMode = ThinkingAnalyticsDebugOff; self.network.debugMode = ThinkingAnalyticsDebugOff; } else if (self.config.debugMode == ThinkingAnalyticsDebugOnly) { TDLogDebug(@"The data will be discarded due to this device is not allowed to debug:%@", record); } } else if (debugResult == -2) { TDLogDebug(@"Exception occurred when sending message to Server:%@", record); if (self.config.debugMode == ThinkingAnalyticsDebug) { // 网络异常 dispatch_async(serialQueue, ^{ [self saveEventsData:record]; }); } } } else { //防止并发事件未降级 NSInteger count = [self saveEventsData:record]; if (count >= [self.config.uploadSize integerValue]) { [self flush]; } } } - (void)_sync { NSString *networkType = [[self class] getNetWorkStates]; if (!([self convertNetworkType:networkType] & self.config.networkTypePolicy)) { return; } dispatch_async(serialQueue, ^{ NSArray *recordArray; @synchronized (instances) { recordArray = [self.dataQueue getFirstRecords:kBatchSize withAppid:self.appid]; } BOOL flushSucc = YES; while (recordArray.count > 0 && flushSucc) { NSUInteger sendSize = recordArray.count; flushSucc = [self.network flushEvents:recordArray]; if (flushSucc) { @synchronized (instances) { BOOL ret = [self.dataQueue removeFirstRecords:sendSize withAppid:self.appid]; if (!ret) { break; } recordArray = [self.dataQueue getFirstRecords:kBatchSize withAppid:self.appid]; } } else { break; } } }); } - (void)dispatchOnNetworkQueue:(void (^)(void))dispatchBlock { dispatch_async(serialQueue, ^{ dispatch_async(networkQueue, dispatchBlock); }); } #pragma mark - Flush control - (void)startFlushTimer { [self stopFlushTimer]; dispatch_async(dispatch_get_main_queue(), ^{ if (self.config.uploadInterval > 0) { self.timer = [NSTimer scheduledTimerWithTimeInterval:[self.config.uploadInterval integerValue] target:self selector:@selector(flush) userInfo:nil repeats:YES]; } }); } - (void)stopFlushTimer { dispatch_async(dispatch_get_main_queue(), ^{ if (self.timer) { [self.timer invalidate]; self.timer = nil; } }); } #pragma mark - Autotracking - (void)enableAutoTrack:(ThinkingAnalyticsAutoTrackEventType)eventType { if ([self hasDisabled]) return; _config.autoTrackEventType = eventType; if ([TDDeviceInfo sharedManager].isFirstOpen && (_config.autoTrackEventType & ThinkingAnalyticsEventTypeAppInstall)) { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ [self autotrack:TD_APP_INSTALL_EVENT properties:nil withTime:nil]; [self flush]; }); } if (_config.autoTrackEventType & ThinkingAnalyticsEventTypeAppEnd) { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ [self timeEvent:TD_APP_END_EVENT]; }); } if (_config.autoTrackEventType & ThinkingAnalyticsEventTypeAppStart) { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ NSString *eventName = _relaunchInBackGround?TD_APP_START_BACKGROUND_EVENT:TD_APP_START_EVENT; #ifdef __IPHONE_13_0 if (@available(iOS 13.0, *)) { if (_isEnableSceneSupport) { eventName = TD_APP_START_EVENT; } } #endif [self autotrack:eventName properties:@{TD_RESUME_FROM_BACKGROUND:@(_appRelaunched)} withTime:nil]; [self flush]; }); } [_autoTrackManager trackWithAppid:self.appid withOption:eventType]; if (_config.autoTrackEventType & ThinkingAnalyticsEventTypeAppViewCrash) { [self trackCrash]; } } - (void)ignoreViewType:(Class)aClass { if ([self hasDisabled]) return; dispatch_async(serialQueue, ^{ [self->_ignoredViewTypeList addObject:aClass]; }); } - (BOOL)isViewTypeIgnored:(Class)aClass { return [_ignoredViewTypeList containsObject:aClass]; } - (BOOL)isViewControllerIgnored:(UIViewController *)viewController { if (viewController == nil) { return false; } NSString *screenName = NSStringFromClass([viewController class]); if (_ignoredViewControllers != nil && _ignoredViewControllers.count > 0) { if ([_ignoredViewControllers containsObject:screenName]) { return true; } } return false; } - (BOOL)isAutoTrackEventTypeIgnored:(ThinkingAnalyticsAutoTrackEventType)eventType { return !(_config.autoTrackEventType & eventType); } - (void)ignoreAutoTrackViewControllers:(NSArray *)controllers { if ([self hasDisabled]) return; if (controllers == nil || controllers.count == 0) { return; } dispatch_async(serialQueue, ^{ [self->_ignoredViewControllers addObjectsFromArray:controllers]; }); } #pragma mark - H5 tracking - (BOOL)showUpWebView:(id)webView WithRequest:(NSURLRequest *)request { if (webView == nil || request == nil || ![request isKindOfClass:NSURLRequest.class]) { TDLogInfo(@"showUpWebView request error"); return NO; } NSString *urlStr = request.URL.absoluteString; if (!urlStr) { return NO; } if ([urlStr rangeOfString:TA_JS_TRACK_SCHEME].length == 0) { return NO; } NSString *query = [[request URL] query]; NSArray *queryItem = [query componentsSeparatedByString:@"="]; if (queryItem.count != 2) return YES; NSString *queryValue = [queryItem lastObject]; if ([urlStr rangeOfString:TA_JS_TRACK_SCHEME].length > 0) { if ([self hasDisabled]) return YES; NSString *eventData = [queryValue stringByRemovingPercentEncoding]; if (eventData.length > 0) [self clickFromH5:eventData]; } return YES; } - (void)wkWebViewGetUserAgent:(void (^)(NSString *))completion { self.wkWebView = [[WKWebView alloc] initWithFrame:CGRectZero]; [self.wkWebView evaluateJavaScript:@"navigator.userAgent" completionHandler:^(id __nullable userAgent, NSError * __nullable error) { completion(userAgent); }]; } - (void)addWebViewUserAgent { if ([self hasDisabled]) return; void (^setUserAgent)(NSString *userAgent) = ^void (NSString *userAgent) { if ([userAgent rangeOfString:@"td-sdk-ios"].location == NSNotFound) { userAgent = [userAgent stringByAppendingString:@" /td-sdk-ios"]; NSDictionary *userAgentDic = [[NSDictionary alloc] initWithObjectsAndKeys:userAgent, @"UserAgent", nil]; [[NSUserDefaults standardUserDefaults] registerDefaults:userAgentDic]; [[NSUserDefaults standardUserDefaults] synchronize]; } }; dispatch_block_t getUABlock = ^() { [self wkWebViewGetUserAgent:^(NSString *userAgent) { setUserAgent(userAgent); }]; }; td_dispatch_main_sync_safe(getUABlock); } #pragma mark - Logging + (void)setLogLevel:(TDLoggingLevel)level { [TDLogging sharedInstance].loggingLevel = level; } #pragma mark - Crash tracking -(void)trackCrash { [[ThinkingExceptionHandler sharedHandler] addThinkingInstance:self]; } #pragma mark - Calibrate time + (void)calibrateTime:(NSTimeInterval)timestamp { calibratedTime = [TDCalibratedTime sharedInstance]; [[TDCalibratedTime sharedInstance] recalibrationWithTimeInterval:timestamp/1000.]; } + (void)calibrateTimeWithNtp:(NSString *)ntpServer { if ([ntpServer isKindOfClass:[NSString class]] && ntpServer.length > 0) { calibratedTime = [TDCalibratedTimeWithNTP sharedInstance]; [[TDCalibratedTimeWithNTP sharedInstance] recalibrationWithNtps:@[ntpServer]]; } } // for UNITY - (NSString *)getTimeString:(NSDate *)date { return [_timeFormatter stringFromDate:date]; } @end @implementation UIView (ThinkingAnalytics) - (NSString *)thinkingAnalyticsViewID { return objc_getAssociatedObject(self, &TD_AUTOTRACK_VIEW_ID); } - (void)setThinkingAnalyticsViewID:(NSString *)thinkingAnalyticsViewID { objc_setAssociatedObject(self, &TD_AUTOTRACK_VIEW_ID, thinkingAnalyticsViewID, OBJC_ASSOCIATION_COPY_NONATOMIC); } - (BOOL)thinkingAnalyticsIgnoreView { return [objc_getAssociatedObject(self, &TD_AUTOTRACK_VIEW_IGNORE) boolValue]; } - (void)setThinkingAnalyticsIgnoreView:(BOOL)thinkingAnalyticsIgnoreView { objc_setAssociatedObject(self, &TD_AUTOTRACK_VIEW_IGNORE, [NSNumber numberWithBool:thinkingAnalyticsIgnoreView], OBJC_ASSOCIATION_ASSIGN); } - (NSDictionary *)thinkingAnalyticsIgnoreViewWithAppid { return objc_getAssociatedObject(self, &TD_AUTOTRACK_VIEW_IGNORE_APPID); } - (void)setThinkingAnalyticsIgnoreViewWithAppid:(NSDictionary *)thinkingAnalyticsViewProperties { objc_setAssociatedObject(self, &TD_AUTOTRACK_VIEW_IGNORE_APPID, thinkingAnalyticsViewProperties, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } - (NSDictionary *)thinkingAnalyticsViewIDWithAppid { return objc_getAssociatedObject(self, &TD_AUTOTRACK_VIEW_ID_APPID); } - (void)setThinkingAnalyticsViewIDWithAppid:(NSDictionary *)thinkingAnalyticsViewProperties { objc_setAssociatedObject(self, &TD_AUTOTRACK_VIEW_ID_APPID, thinkingAnalyticsViewProperties, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } - (NSDictionary *)thinkingAnalyticsViewProperties { return objc_getAssociatedObject(self, &TD_AUTOTRACK_VIEW_PROPERTIES); } - (void)setThinkingAnalyticsViewProperties:(NSDictionary *)thinkingAnalyticsViewProperties { objc_setAssociatedObject(self, &TD_AUTOTRACK_VIEW_PROPERTIES, thinkingAnalyticsViewProperties, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } - (NSDictionary *)thinkingAnalyticsViewPropertiesWithAppid { return objc_getAssociatedObject(self, &TD_AUTOTRACK_VIEW_PROPERTIES_APPID); } - (void)setThinkingAnalyticsViewPropertiesWithAppid:(NSDictionary *)thinkingAnalyticsViewProperties { objc_setAssociatedObject(self, &TD_AUTOTRACK_VIEW_PROPERTIES_APPID, thinkingAnalyticsViewProperties, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } - (id)thinkingAnalyticsDelegate { return objc_getAssociatedObject(self, &TD_AUTOTRACK_VIEW_DELEGATE); } - (void)setThinkingAnalyticsDelegate:(id)thinkingAnalyticsDelegate { objc_setAssociatedObject(self, &TD_AUTOTRACK_VIEW_DELEGATE, thinkingAnalyticsDelegate, OBJC_ASSOCIATION_ASSIGN); } @end