miduo_client/Assets/Plugins/iOS/ThinkingSDK/Source/main/ThinkingAnalyticsSDK.m

1755 lines
66 KiB
Objective-C

#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:@"<ThinkingAnalyticsSDK: %p - appid: %@ serverUrl: %@>", (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<NSString *, NSArray *> *)properties {
[self user_append:properties withTime:nil];
}
- (void)user_append:(NSDictionary<NSString *, NSArray *> *)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<NSString *, id> *(^)(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<NSString *, id> *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<NSString *,id> *)processParameters:(NSDictionary<NSString *,id> *)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<NSString *, id> *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