From 894ebcdd9a0f74f98225cdfabcca7452e1c6f7c2 Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Mon, 29 Sep 2025 14:53:09 +0300 Subject: [PATCH 01/42] feat: session immediates --- Countly.m | 2 +- CountlyCommon.h | 2 ++ CountlyCommon.m | 11 +++++++++++ CountlyContentBuilderInternal.m | 3 ++- CountlyFeedbackWidget.m | 3 ++- CountlyFeedbacksInternal.m | 6 ++++-- CountlyHealthTracker.m | 3 ++- CountlyNotificationService.m | 1 + CountlyRemoteConfigInternal.m | 13 ++++++++----- CountlyServerConfig.m | 3 ++- 10 files changed, 35 insertions(+), 12 deletions(-) diff --git a/Countly.m b/Countly.m index 2db45b4a..4bcdef83 100644 --- a/Countly.m +++ b/Countly.m @@ -89,7 +89,7 @@ - (void)startWithConfig:(CountlyConfig *)config CountlyCommon.sharedInstance.shouldIgnoreTrustCheck = config.shouldIgnoreTrustCheck; CountlyCommon.sharedInstance.loggerDelegate = config.loggerDelegate; CountlyCommon.sharedInstance.internalLogLevel = config.internalLogLevel; - + config = [self checkAndFixInternalLimitsConfig:config]; if (config.disableSDKBehaviorSettingsUpdates) { diff --git a/CountlyCommon.h b/CountlyCommon.h index 42b4e1ad..3b72c049 100644 --- a/CountlyCommon.h +++ b/CountlyCommon.h @@ -125,6 +125,8 @@ void CountlyPrint(NSString *stringToPrint); - (NSURLSession *)URLSession; +- (NSURLSession *)ImmediateURLSession; + - (CGSize)getWindowSize; @end diff --git a/CountlyCommon.m b/CountlyCommon.m index 485268cd..ffd8c7ab 100644 --- a/CountlyCommon.m +++ b/CountlyCommon.m @@ -335,6 +335,17 @@ - (NSURLSession *)URLSession } } +- (NSURLSession *)ImmediateURLSession +{ + NSURLSessionConfiguration *immediateConfig = [NSURLSessionConfiguration defaultSessionConfiguration]; + if (CountlyConnectionManager.sharedInstance.URLSessionConfiguration) + { + immediateConfig.HTTPAdditionalHeaders = CountlyConnectionManager.sharedInstance.URLSessionConfiguration.HTTPAdditionalHeaders; + } + + return [NSURLSession sessionWithConfiguration:immediateConfig]; +} + - (CGSize)getWindowSize { #if (TARGET_OS_IOS) UIWindow *window = nil; diff --git a/CountlyContentBuilderInternal.m b/CountlyContentBuilderInternal.m index 6317d367..162f33df 100644 --- a/CountlyContentBuilderInternal.m +++ b/CountlyContentBuilderInternal.m @@ -157,7 +157,8 @@ - (void)fetchContents { [self setRequestQueueLockedThreadSafe:YES]; - NSURLSessionTask *dataTask = [[NSURLSession sharedSession] dataTaskWithRequest:[self fetchContentsRequest] completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { + NSURLSessionTask *dataTask = [[CountlyCommon.sharedInstance ImmediateURLSession] dataTaskWithRequest:[self fetchContentsRequest] completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { + // IMMEDIATE REQUEST to find them better in search if (error) { CLY_LOG_I(@"%s fetch content details failed: [%@]", __FUNCTION__, error); [self setRequestQueueLockedThreadSafe:NO]; diff --git a/CountlyFeedbackWidget.m b/CountlyFeedbackWidget.m index ee90867c..561b9b81 100644 --- a/CountlyFeedbackWidget.m +++ b/CountlyFeedbackWidget.m @@ -205,8 +205,9 @@ - (void)getWidgetData:(void (^)(NSDictionary * __nullable widgetData, NSError * return; } - NSURLSessionTask* task = [CountlyCommon.sharedInstance.URLSession dataTaskWithRequest:[self dataRequest] completionHandler:^(NSData* data, NSURLResponse* response, NSError* error) + NSURLSessionTask* task = [CountlyCommon.sharedInstance.ImmediateURLSession dataTaskWithRequest:[self dataRequest] completionHandler:^(NSData* data, NSURLResponse* response, NSError* error) { + // IMMEDIATE REQUEST to find them better in search NSDictionary *widgetData = nil; if (!error) diff --git a/CountlyFeedbacksInternal.m b/CountlyFeedbacksInternal.m index 71a9e46a..35a49fff 100644 --- a/CountlyFeedbacksInternal.m +++ b/CountlyFeedbacksInternal.m @@ -252,8 +252,9 @@ - (void)presentRatingWidgetWithID:(NSString *)widgetID closeButtonText:(NSString return; NSURLRequest* feedbackWidgetCheckRequest = [self widgetCheckURLRequest:widgetID]; - NSURLSessionTask* task = [CountlyCommon.sharedInstance.URLSession dataTaskWithRequest:feedbackWidgetCheckRequest completionHandler:^(NSData* data, NSURLResponse* response, NSError* error) + NSURLSessionTask* task = [CountlyCommon.sharedInstance.ImmediateURLSession dataTaskWithRequest:feedbackWidgetCheckRequest completionHandler:^(NSData* data, NSURLResponse* response, NSError* error) { + // IMMEDIATE REQUEST to find them better in search NSDictionary* widgetInfo = nil; if (!error) @@ -437,8 +438,9 @@ - (void)getFeedbackWidgets:(void (^)(NSArray *feedback return; } - NSURLSessionTask* task = [CountlyCommon.sharedInstance.URLSession dataTaskWithRequest:[self feedbacksRequest] completionHandler:^(NSData* data, NSURLResponse* response, NSError* error) + NSURLSessionTask* task = [CountlyCommon.sharedInstance.ImmediateURLSession dataTaskWithRequest:[self feedbacksRequest] completionHandler:^(NSData* data, NSURLResponse* response, NSError* error) { + // IMMEDIATE REQUEST to find them better in search NSDictionary *feedbacksResponse = nil; if (!error) diff --git a/CountlyHealthTracker.m b/CountlyHealthTracker.m index 79ef74ca..cfea795f 100644 --- a/CountlyHealthTracker.m +++ b/CountlyHealthTracker.m @@ -182,8 +182,9 @@ - (void)sendHealthCheck { CLY_LOG_D(@"%s health check status, healthCheckSent: [%d], healthCheckEnabled: [%d]", __FUNCTION__, _healthCheckSent, _healthCheckEnabled); } - NSURLSessionTask* task = [CountlyCommon.sharedInstance.URLSession dataTaskWithRequest:[self healthCheckRequest] completionHandler:^(NSData* data, NSURLResponse* response, NSError* error) + NSURLSessionTask* task = [CountlyCommon.sharedInstance.ImmediateURLSession dataTaskWithRequest:[self healthCheckRequest] completionHandler:^(NSData* data, NSURLResponse* response, NSError* error) { + // IMMEDIATE REQUEST to find them better in search if (error) { CLY_LOG_W(@"%s error while sending health checks error: [%@]", __FUNCTION__, error); diff --git a/CountlyNotificationService.m b/CountlyNotificationService.m index a42a08ad..bec63764 100644 --- a/CountlyNotificationService.m +++ b/CountlyNotificationService.m @@ -87,6 +87,7 @@ + (void)didReceiveNotificationRequest:(UNNotificationRequest *)request withConte [[NSURLSession.sharedSession downloadTaskWithURL:[NSURL URLWithString:attachmentURL] completionHandler:^(NSURL * location, NSURLResponse * response, NSError * error) { + // IMMEDIATE REQUEST to find them better in search, but uses NSURLSession.sharedSession as it should not be blocked by SDK configurations if (!error) { COUNTLY_EXT_LOG(@"Attachment download completed!"); diff --git a/CountlyRemoteConfigInternal.m b/CountlyRemoteConfigInternal.m index 5e0aee79..9bb12663 100644 --- a/CountlyRemoteConfigInternal.m +++ b/CountlyRemoteConfigInternal.m @@ -197,8 +197,9 @@ - (void)fetchRemoteConfigForKeys:(NSArray *)keys omitKeys:(NSArray *)omitKeys i return; NSURLRequest* request = [self remoteConfigRequestForKeys:keys omitKeys:omitKeys isLegacy:isLegacy]; - NSURLSessionTask* task = [CountlyCommon.sharedInstance.URLSession dataTaskWithRequest:request completionHandler:^(NSData* data, NSURLResponse* response, NSError* error) + NSURLSessionTask* task = [CountlyCommon.sharedInstance.ImmediateURLSession dataTaskWithRequest:request completionHandler:^(NSData* data, NSURLResponse* response, NSError* error) { + // IMMEDIATE REQUEST to find them better in search NSDictionary* remoteConfig = nil; if (!error) @@ -496,8 +497,9 @@ - (void)testingDownloadAllVariantsInternal:(void (^)(CLYRequestResult response, return; NSURLRequest* request = [self downloadVariantsRequest]; - NSURLSessionTask* task = [CountlyCommon.sharedInstance.URLSession dataTaskWithRequest:request completionHandler:^(NSData* data, NSURLResponse* response, NSError* error) + NSURLSessionTask* task = [CountlyCommon.sharedInstance.ImmediateURLSession dataTaskWithRequest:request completionHandler:^(NSData* data, NSURLResponse* response, NSError* error) { + // IMMEDIATE REQUEST to find them better in search NSMutableDictionary* variants = NSMutableDictionary.new; if (!error) @@ -607,8 +609,9 @@ - (void)testingEnrollIntoVariantInternal:(NSString *)key variantName:(NSString * } NSURLRequest* request = [self enrollInVarianRequestForKey:key variantName:variantName]; - NSURLSessionTask* task = [CountlyCommon.sharedInstance.URLSession dataTaskWithRequest:request completionHandler:^(NSData* data, NSURLResponse* response, NSError* error) + NSURLSessionTask* task = [CountlyCommon.sharedInstance.ImmediateURLSession dataTaskWithRequest:request completionHandler:^(NSData* data, NSURLResponse* response, NSError* error) { + // IMMEDIATE REQUEST to find them better in search NSDictionary* variants = nil; [self clearCachedRemoteConfig]; if (!error) @@ -721,9 +724,9 @@ - (void)testingDownloaExperimentInfoInternal:(void (^)(CLYRequestResult response return; NSURLRequest* request = [self downloadExperimentInfoRequest]; - NSURLSessionTask* task = [CountlyCommon.sharedInstance.URLSession dataTaskWithRequest:request completionHandler:^(NSData* data, NSURLResponse* response, NSError* error) + NSURLSessionTask* task = [CountlyCommon.sharedInstance.ImmediateURLSession dataTaskWithRequest:request completionHandler:^(NSData* data, NSURLResponse* response, NSError* error) { - + // IMMEDIATE REQUEST to find them better in search NSMutableDictionary * experiments = NSMutableDictionary.new; if (!error) diff --git a/CountlyServerConfig.m b/CountlyServerConfig.m index 85a86dbe..ac621d7f 100644 --- a/CountlyServerConfig.m +++ b/CountlyServerConfig.m @@ -446,7 +446,8 @@ - (void)fetchServerConfig:(CountlyConfig *)config } }; // Set default values - NSURLSessionTask *task = [CountlyCommon.sharedInstance.URLSession dataTaskWithRequest:[self serverConfigRequest] completionHandler:handler]; + NSURLSessionTask *task = [CountlyCommon.sharedInstance.ImmediateURLSession dataTaskWithRequest:[self serverConfigRequest] completionHandler:handler]; + // IMMEDIATE REQUEST to find them better in search [task resume]; } From 5c59aecd433bd9bbbbda588174566f59db0387de Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Mon, 29 Sep 2025 15:00:22 +0300 Subject: [PATCH 02/42] feat: add changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index acd3e599..33cbc00c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## XX.XX.XX +* Added ImmediateURLSession internal method for requests that bypass timeout configurations in time-sensitive operations. + ## 25.4.6 * Added the ability to record reserved events. * Changed default log level from "CLYInternalLogLevelDebug" to "CLYInternalLogLevelVerbose". From 465d46b3f4203d5eaf2fd73d45e23637b0d68b25 Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Mon, 29 Sep 2025 15:11:33 +0300 Subject: [PATCH 03/42] fix: changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 33cbc00c..54b7b750 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,5 @@ ## XX.XX.XX -* Added ImmediateURLSession internal method for requests that bypass timeout configurations in time-sensitive operations. +* Mitigated an issue where non-queued requests were affected from request timeout settings, not anymore. ## 25.4.6 * Added the ability to record reserved events. From 1d6027b27ab172be61cc73d50a5e069130596a6d Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Mon, 29 Sep 2025 15:13:00 +0300 Subject: [PATCH 04/42] fix: remove extra from changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 54b7b750..4c1016ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,5 @@ ## XX.XX.XX -* Mitigated an issue where non-queued requests were affected from request timeout settings, not anymore. +* Mitigated an issue where non-queued requests were affected from request timeout settings. ## 25.4.6 * Added the ability to record reserved events. From 96528b59161ee465a76d20f7f390fdad6cf4be26 Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Wed, 1 Oct 2025 17:31:44 +0300 Subject: [PATCH 05/42] refactor: screen size functions to acmm ios 26 --- CHANGELOG.md | 3 +++ CountlyCommon.m | 4 +++- CountlyDeviceInfo.m | 27 +++++++++++++++++++++++---- CountlyFeedbacksInternal.m | 3 ++- 4 files changed, 31 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index acd3e599..598a44a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## XX.XX.XX +* Updated resolution extraction to accommodate iOS 26 deprecations in UIScreen/UIScene APIs. + ## 25.4.6 * Added the ability to record reserved events. * Changed default log level from "CLYInternalLogLevelDebug" to "CLYInternalLogLevelVerbose". diff --git a/CountlyCommon.m b/CountlyCommon.m index 485268cd..5c151a06 100644 --- a/CountlyCommon.m +++ b/CountlyCommon.m @@ -338,6 +338,7 @@ - (NSURLSession *)URLSession - (CGSize)getWindowSize { #if (TARGET_OS_IOS) UIWindow *window = nil; + CGFloat screenScale; if (@available(iOS 13.0, *)) { for (UIScene *scene in [UIApplication sharedApplication].connectedScenes) { @@ -346,14 +347,15 @@ - (CGSize)getWindowSize { break; } } + screenScale = window.traitCollection.displayScale; } else { window = [[UIApplication sharedApplication].delegate window]; + screenScale = [UIScreen mainScreen].scale; } if (!window) return CGSizeZero; UIEdgeInsets safeArea = UIEdgeInsetsZero; - CGFloat screenScale = [UIScreen mainScreen].scale; if (@available(iOS 11.0, *)) { safeArea = window.safeAreaInsets; safeArea.left /= screenScale; diff --git a/CountlyDeviceInfo.m b/CountlyDeviceInfo.m index 7af25929..6ebc99f5 100644 --- a/CountlyDeviceInfo.m +++ b/CountlyDeviceInfo.m @@ -261,11 +261,29 @@ + (NSString *)carrier + (NSString *)resolution { - CGRect bounds; - CGFloat scale; + CGRect bounds = CGRectZero; + CGFloat scale = 0; #if (TARGET_OS_IOS || TARGET_OS_TV) - bounds = UIScreen.mainScreen.bounds; - scale = UIScreen.mainScreen.scale; + UIWindow *window = nil; +#if (TARGET_OS_IOS) + if (@available(iOS 13.0, *)) { +#elif (TARGET_OS_TV) + if (@available(tvOS 13.0, *)) { +#endif + for (UIScene *scene in [UIApplication sharedApplication].connectedScenes) { + if ([scene isKindOfClass:[UIWindowScene class]]) { + window = ((UIWindowScene *)scene).windows.firstObject; + break; + } + } + if(window){ + bounds = window.bounds; + scale = window.traitCollection.displayScale; + } + } else { + bounds = UIScreen.mainScreen.bounds; + scale = UIScreen.mainScreen.scale; + } #elif (TARGET_OS_WATCH) bounds = WKInterfaceDevice.currentDevice.screenBounds; scale = WKInterfaceDevice.currentDevice.screenScale; @@ -519,3 +537,4 @@ - (void)resetInstance } @end + diff --git a/CountlyFeedbacksInternal.m b/CountlyFeedbacksInternal.m index 71a9e46a..1f5e80bf 100644 --- a/CountlyFeedbacksInternal.m +++ b/CountlyFeedbacksInternal.m @@ -300,7 +300,8 @@ - (void)presentRatingWidgetInternal:(NSString *)widgetID closeButtonText:(NSStri { __block CLYInternalViewController* webVC = CLYInternalViewController.new; webVC.view.backgroundColor = UIColor.whiteColor; - webVC.view.bounds = UIScreen.mainScreen.bounds; + CGSize windowSize = [CountlyCommon.sharedInstance getWindowSize]; + webVC.view.bounds = CGRectMake(0, 0, windowSize.width, windowSize.height); webVC.modalPresentationStyle = UIModalPresentationCustom; WKWebView* webView = [WKWebView.alloc initWithFrame:webVC.view.bounds]; From 5ec8e6c4cdc9eeead6a41ad41aa819c932f2c4b2 Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray <57103426+arifBurakDemiray@users.noreply.github.com> Date: Thu, 2 Oct 2025 10:13:23 +0300 Subject: [PATCH 06/42] Simplify changelog entry for iOS 26 updates --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 598a44a6..2be25f0b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,5 @@ ## XX.XX.XX -* Updated resolution extraction to accommodate iOS 26 deprecations in UIScreen/UIScene APIs. +* Updated resolution extraction to accommodate iOS 26 deprecations. ## 25.4.6 * Added the ability to record reserved events. From 8b4400e80cac60cfed508dc8f2f71f4bfdc1c0ae Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Fri, 3 Oct 2025 08:40:28 +0300 Subject: [PATCH 07/42] feat: user properties interface --- Countly.h | 2 + Countly.m | 5 + CountlyCommon.m | 2 +- CountlyUserDetails.h | 4 + CountlyUserDetails.m | 356 +++++++++++++++++++++---------------------- 5 files changed, 186 insertions(+), 183 deletions(-) diff --git a/Countly.h b/Countly.h index 89043cb2..b5e84e9e 100644 --- a/Countly.h +++ b/Countly.h @@ -697,6 +697,8 @@ NS_ASSUME_NONNULL_BEGIN * @discussion Feedback widget interface for developer to interact with SDK. */ - (CountlyFeedbacks *) feedback; + +- (CountlyUserDetails *) userProfile; #endif diff --git a/Countly.m b/Countly.m index 2db45b4a..9033d236 100644 --- a/Countly.m +++ b/Countly.m @@ -1484,4 +1484,9 @@ - (CountlyRemoteConfig *) remoteConfig { return CountlyRemoteConfig.sharedInstance; } +- (CountlyUserDetails *) userProfile +{ + return CountlyUserDetails.sharedInstance; +} + @end diff --git a/CountlyCommon.m b/CountlyCommon.m index 485268cd..e78ece2a 100644 --- a/CountlyCommon.m +++ b/CountlyCommon.m @@ -712,7 +712,7 @@ - (NSMutableDictionary *) cly_filterSupportedDataTypes if ([value isKindOfClass:[NSNumber class]] || [value isKindOfClass:[NSString class]] || - ([value isKindOfClass:[NSArray class]] && (value = [value cly_filterSupportedDataTypes]))) { + ([value isKindOfClass:[NSArray class]] && (value = [(NSArray *)value cly_filterSupportedDataTypes]))) { [filteredDictionary setObject:value forKey:key]; } else { CLY_LOG_W(@"%s, Removed invalid type for key %@: %@", __FUNCTION__, key, [value class]); diff --git a/CountlyUserDetails.h b/CountlyUserDetails.h index 35f6ef32..2d4d28f8 100644 --- a/CountlyUserDetails.h +++ b/CountlyUserDetails.h @@ -324,6 +324,10 @@ extern NSString* const kCountlyLocalPicturePath; */ - (void)save; +- (void)setProperties:(NSDictionary *)data; +- (void)setProperty:(NSString *)key value:(id)value; + + - (BOOL) hasUnsyncedChanges; NS_ASSUME_NONNULL_END diff --git a/CountlyUserDetails.m b/CountlyUserDetails.m index 6652f16a..54c17a78 100644 --- a/CountlyUserDetails.m +++ b/CountlyUserDetails.m @@ -7,7 +7,11 @@ #import "CountlyCommon.h" @interface CountlyUserDetails () -@property (nonatomic) NSMutableDictionary* modifications; +@property (nonatomic) NSMutableDictionary* predefined; +@property (nonatomic) NSMutableDictionary* customMods; +@property (nonatomic) NSMutableDictionary* customProperties; + +- (BOOL)isValidDataType:(id) value; @end NSString* const kCountlyLocalPicturePath = @"kCountlyLocalPicturePath"; @@ -31,6 +35,19 @@ @interface CountlyUserDetails () NSString* const kCountlyUDKeyModifierAddToSet = @"$addToSet"; NSString* const kCountlyUDKeyModifierPull = @"$pull"; +static NSString* const kCountlyUDNamedFields[] = { + kCountlyUDKeyName, + kCountlyUDKeyUsername, + kCountlyUDKeyEmail, + kCountlyUDKeyOrganization, + kCountlyUDKeyPhone, + kCountlyUDKeyGender, + kCountlyUDKeyPicture, + kCountlyUDKeyBirthyear +}; + +static const NSUInteger kCountlyUDNamedFieldsCount = sizeof(kCountlyUDNamedFields) / sizeof(kCountlyUDNamedFields[0]); + @implementation CountlyUserDetails + (instancetype)sharedInstance @@ -45,7 +62,9 @@ - (instancetype)init { if (self = [super init]) { - self.modifications = NSMutableDictionary.new; + self.predefined = NSMutableDictionary.new; + self.customMods = NSMutableDictionary.new; + self.customProperties = NSMutableDictionary.new; } return self; @@ -90,14 +109,24 @@ - (NSString *)serializedUserDetails if (self.birthYear) userDictionary[kCountlyUDKeyBirthyear] = self.birthYear; + NSMutableDictionary* customAll = NSMutableDictionary.new; + if ([self.custom isKindOfClass:NSDictionary.class]) { NSDictionary* customTruncated = [((NSDictionary *)self.custom) cly_truncated:@"User details custom dictionary"]; - self.custom = [customTruncated cly_limited:@"User details custom dictionary"]; + [customAll addEntriesFromDictionary:[customTruncated cly_limited:@"User details custom dictionary"]]; + } + + if(self.customProperties.count > 0){ + [customAll addEntriesFromDictionary:[self.customProperties cly_limited:@"User details custom dictionary"]]; + } + + if(self.customMods.count > 0){ + [customAll addEntriesFromDictionary:self.customMods]; } - if (self.custom) - userDictionary[kCountlyUDKeyCustom] = self.custom; + if (customAll.count > 0) + userDictionary[kCountlyUDKeyCustom] = customAll; if (userDictionary.allKeys.count) return [userDictionary cly_JSONify]; @@ -118,7 +147,9 @@ - (void)clearUserDetails self.birthYear = nil; self.custom = nil; - [self.modifications removeAllObjects]; + [self.predefined removeAllObjects]; + [self.customMods removeAllObjects]; + [self.customProperties removeAllObjects]; } - (BOOL)hasUnsyncedChanges @@ -144,7 +175,7 @@ - (BOOL)hasUnsyncedChanges } }]; - return userDetailsChanged || self.modifications.count > 0; + return userDetailsChanged || self.predefined.count > 0 || self.customProperties.count > 0 || self.customMods.count > 0; } @@ -154,62 +185,55 @@ - (BOOL)hasUnsyncedChanges - (void)set:(NSString *)key value:(NSString *)value { CLY_LOG_I(@"%s %@ %@", __FUNCTION__, key, value); - - self.modifications[key] = value.copy; + [self setProperty:key value:value]; } - (void)set:(NSString *)key numberValue:(NSNumber *)value { CLY_LOG_I(@"%s %@ %@", __FUNCTION__, key, value); - - self.modifications[key] = value.copy; + [self setProperty:key value:value]; } - (void)set:(NSString *)key boolValue:(BOOL)value { CLY_LOG_I(@"%s %@ %d", __FUNCTION__, key, value); - - self.modifications[key] = @(value); + [self setProperty:key value:@(value)]; } - (void)setOnce:(NSString *)key value:(NSString *)value { CLY_LOG_I(@"%s %@ %@", __FUNCTION__, key, value); - - if (!value) - { - CLY_LOG_W(@"%s call will be ignored as value is nil!", __FUNCTION__); - return; - } - - self.modifications[key] = @{kCountlyUDKeyModifierSetOnce: value.copy}; + [self doModification:kCountlyUDKeyModifierSetOnce key:key value:value]; } - (void)setOnce:(NSString *)key numberValue:(NSNumber *)value { CLY_LOG_I(@"%s %@ %@", __FUNCTION__, key, value); - - if (!value) - { - CLY_LOG_W(@"%s call will be ignored as value is nil!", __FUNCTION__); - return; - } - - self.modifications[key] = @{kCountlyUDKeyModifierSetOnce: value.copy}; + [self doModification:kCountlyUDKeyModifierSetOnce key:key value:value]; } - (void)setOnce:(NSString *)key boolValue:(BOOL)value; { CLY_LOG_I(@"%s %@ %d", __FUNCTION__, key, value); - - self.modifications[key] = @{kCountlyUDKeyModifierSetOnce: @(value)}; + [self doModification:kCountlyUDKeyModifierSetOnce key:key value:@(value)]; } - (void)unSet:(NSString *)key { CLY_LOG_I(@"%s %@", __FUNCTION__, key); - - self.modifications[key] = NSNull.null; + if(key != nil){ + if(self.customProperties[key]) { + self.customProperties[key] = NSNull.null; + } + + if(self.customMods[key]) { + self.customMods[key] = NSNull.null; + } + + if(self.predefined[key]) { + self.predefined[key] = NSNull.null; + } + } } - (void)increment:(NSString *)key @@ -222,189 +246,95 @@ - (void)increment:(NSString *)key - (void)incrementBy:(NSString *)key value:(NSNumber *)value { CLY_LOG_I(@"%s %@ %@", __FUNCTION__, key, value); - - if (!value) - { - CLY_LOG_W(@"%s call will be ignored as value is nil!", __FUNCTION__); - return; - } - - self.modifications[key] = @{kCountlyUDKeyModifierIncrement: value}; + [self doModification:kCountlyUDKeyModifierIncrement key:key value:value]; } - (void)multiply:(NSString *)key value:(NSNumber *)value { CLY_LOG_I(@"%s %@ %@", __FUNCTION__, key, value); - - if (!value) - { - CLY_LOG_W(@"%s call will be ignored as value is nil!", __FUNCTION__); - return; - } - - self.modifications[key] = @{kCountlyUDKeyModifierMultiply: value}; + [self doModification:kCountlyUDKeyModifierMultiply key:key value:value]; } - (void)max:(NSString *)key value:(NSNumber *)value { CLY_LOG_I(@"%s %@ %@", __FUNCTION__, key, value); - - if (!value) - { - CLY_LOG_W(@"%s call will be ignored as value is nil!", __FUNCTION__); - return; - } - - self.modifications[key] = @{kCountlyUDKeyModifierMax: value}; + [self doModification:kCountlyUDKeyModifierMax key:key value:value]; } - (void)min:(NSString *)key value:(NSNumber *)value { CLY_LOG_I(@"%s %@ %@", __FUNCTION__, key, value); - - if (!value) - { - CLY_LOG_W(@"%s call will be ignored as value is nil!", __FUNCTION__); - return; - } - - self.modifications[key] = @{kCountlyUDKeyModifierMin: value}; -} + [self doModification:kCountlyUDKeyModifierMin key:key value:value];} - (void)push:(NSString *)key value:(NSString *)value { CLY_LOG_I(@"%s %@ %@", __FUNCTION__, key, value); - - if (!value) - { - CLY_LOG_W(@"%s call will be ignored as value is nil!", __FUNCTION__); - return; - } - - self.modifications[key] = @{kCountlyUDKeyModifierPush: value.copy}; + [self doModification:kCountlyUDKeyModifierPush key:key value:value]; } - (void)push:(NSString *)key numberValue:(NSNumber *)value; { - if (!value) - { - CLY_LOG_W(@"%s call will be ignored as value is nil!", __FUNCTION__); - return; - } - - self.modifications[key] = @{kCountlyUDKeyModifierPush: value.copy}; + [self doModification:kCountlyUDKeyModifierPush key:key value:value]; } - (void)push:(NSString *)key boolValue:(BOOL)value { CLY_LOG_I(@"%s %@ %d", __FUNCTION__, key, value); - - self.modifications[key] = @{kCountlyUDKeyModifierPush: @(value)}; + [self doModification:kCountlyUDKeyModifierPush key:key value:@(value)]; } - (void)push:(NSString *)key values:(NSArray *)value { CLY_LOG_I(@"%s %@ %@", __FUNCTION__, key, value); - - if (!value) - { - CLY_LOG_W(@"%s call will be ignored as value is nil!", __FUNCTION__); - return; - } - - self.modifications[key] = @{kCountlyUDKeyModifierPush: value.copy}; + [self doModification:kCountlyUDKeyModifierPush key:key value:value]; } - (void)pushUnique:(NSString *)key value:(NSString *)value { CLY_LOG_I(@"%s %@ %@", __FUNCTION__, key, value); - - if (!value) - { - CLY_LOG_W(@"%s call will be ignored as value is nil!", __FUNCTION__); - return; - } - - self.modifications[key] = @{kCountlyUDKeyModifierAddToSet: value.copy}; + [self doModification:kCountlyUDKeyModifierAddToSet key:key value:value]; } - (void)pushUnique:(NSString *)key numberValue:(NSNumber *)value { CLY_LOG_I(@"%s %@ %@", __FUNCTION__, key, value); - - if (!value) - { - CLY_LOG_W(@"%s call will be ignored as value is nil!", __FUNCTION__); - return; - } - - self.modifications[key] = @{kCountlyUDKeyModifierAddToSet: value.copy}; + [self doModification:kCountlyUDKeyModifierAddToSet key:key value:value]; } - (void)pushUnique:(NSString *)key boolValue:(BOOL)value { CLY_LOG_I(@"%s %@ %d", __FUNCTION__, key, value); - - self.modifications[key] = @{kCountlyUDKeyModifierAddToSet: @(value)}; + [self doModification:kCountlyUDKeyModifierAddToSet key:key value:@(value)]; } - (void)pushUnique:(NSString *)key values:(NSArray *)value { CLY_LOG_I(@"%s %@ %@", __FUNCTION__, key, value); - - if (!value) - { - CLY_LOG_W(@"%s call will be ignored as value is nil!", __FUNCTION__); - return; - } - - self.modifications[key] = @{kCountlyUDKeyModifierAddToSet: value.copy}; + [self doModification:kCountlyUDKeyModifierAddToSet key:key value:value]; } - (void)pull:(NSString *)key value:(NSString *)value { CLY_LOG_I(@"%s %@ %@", __FUNCTION__, key, value); - - if (!value) - { - CLY_LOG_W(@"%s call will be ignored as value is nil!", __FUNCTION__); - return; - } - - self.modifications[key] = @{kCountlyUDKeyModifierPull: value.copy}; + [self doModification:kCountlyUDKeyModifierPull key:key value:value]; } - (void)pull:(NSString *)key numberValue:(NSNumber *)value { CLY_LOG_I(@"%s %@ %@", __FUNCTION__, key, value); - - if (!value) - { - CLY_LOG_W(@"%s call will be ignored as value is nil!", __FUNCTION__); - return; - } - - self.modifications[key] = @{kCountlyUDKeyModifierPull: value.copy}; + [self doModification:kCountlyUDKeyModifierPull key:key value:value]; } - (void)pull:(NSString *)key boolValue:(BOOL)value { CLY_LOG_I(@"%s %@ %d", __FUNCTION__, key, value); - - self.modifications[key] = @{kCountlyUDKeyModifierPull: @(value)}; + [self doModification:kCountlyUDKeyModifierPull key:key value:@(value)]; } - (void)pull:(NSString *)key values:(NSArray *)value { CLY_LOG_I(@"%s %@ %@", __FUNCTION__, key, value); - - if (!value) - { - CLY_LOG_W(@"%s call will be ignored as value is nil!", __FUNCTION__); - return; - } - - self.modifications[key] = @{kCountlyUDKeyModifierPull: value.copy}; + [self doModification:kCountlyUDKeyModifierPull key:key value:value]; } - (void)save @@ -430,55 +360,117 @@ - (void)save if (self.pictureLocalPath && !self.pictureURL) [CountlyConnectionManager.sharedInstance sendUserDetails:[@{kCountlyLocalPicturePath: self.pictureLocalPath} cly_JSONify]]; - if (self.modifications.count) - { - [CountlyConnectionManager.sharedInstance sendUserDetails:[@{kCountlyUDKeyCustom: [self truncateModifications]} cly_JSONify]]; - } - [self clearUserDetails]; } +- (void)doModification:(NSString *)mod key:(NSString *)key value:(id)value { + if (value == nil) + { + CLY_LOG_W(@"%s call will be ignored as value is nil!", __FUNCTION__); + return; + } + + // If the value is NSString, apply truncation rules + if ([value isKindOfClass:[NSString class]]) { + value = [[value description] cly_truncatedValue:@"[CountlyUserDetails] doModification"]; + } + + NSString* truncatedKey = [[key description] cly_truncatedKey:@"[CountlyUserDetails] setPropertiesInternal"]; + if (![mod isEqualToString:@"$pull"] && + ![mod isEqualToString:@"$push"] && + ![mod isEqualToString:@"$addToSet"]) { + self.customMods[truncatedKey] = @{mod: value};; + } else { + if(self.customMods[truncatedKey] && self.customMods[truncatedKey][mod]){ + NSMutableArray *array = [self.customMods[key][mod] mutableCopy]; + [array addObject:value]; + self.customMods[truncatedKey] = @{mod: array}; + } else { + self.customMods[truncatedKey] = @{mod: @[value]}; + } + } + +} -- (NSDictionary *)truncateModifications -{ - NSMutableDictionary* truncatedDict = self.modifications.mutableCopy; - [self.modifications enumerateKeysAndObjectsUsingBlock:^(NSString * key, id obj, BOOL * stop) - { - NSString* truncatedKey = [key cly_truncatedKey:@"User details modifications key"]; - if (![truncatedKey isEqualToString:key]) - { - truncatedDict[truncatedKey] = obj; - [truncatedDict removeObjectForKey:key]; +/** + * This mainly performs the filtering of provided values. + * This single call is used for both predefined properties and custom user properties. + * + * @param data Dictionary of user properties + */ +- (void)setPropertiesInternal:(NSDictionary *)data { + if (data.count == 0) { + NSLog(@"[ModuleUserProfile] setPropertiesInternal, no data was provided"); + return; + } + + for (NSString *key in data) { + id value = data[key]; + + if (value == nil || value == [NSNull null]) { + NSLog(@"[ModuleUserProfile] setPropertiesInternal, provided value for key [%@] is 'null'", key); + continue; } - if ([obj isKindOfClass:NSString.class]) - { - NSString* truncatedValue = [obj cly_truncatedValue:@"User details modifications value"]; - if (![truncatedValue isEqualToString:obj]) - { - truncatedDict[truncatedKey] = truncatedValue; - } + if ([value isKindOfClass:[NSString class]]) { + value = [[value description] cly_truncatedValue:@"[CountlyUserDetails] setPropertiesInternal"]; } - else if ([obj isKindOfClass:NSDictionary.class]) - { - NSMutableDictionary* truncatedValueDict = ((NSDictionary *)obj).mutableCopy; - [(NSDictionary *)obj enumerateKeysAndObjectsUsingBlock:^(NSString * key, id value, BOOL * stop) - { - if ([value isKindOfClass:NSString.class]) - { - NSString* truncatedValue = [value cly_truncatedValue:@"User details modifications value"]; - if (![truncatedValue isEqualToString:value]) - { - truncatedValueDict[key] = truncatedValue; - truncatedDict[truncatedKey] = truncatedValueDict; - } - } - }]; + + BOOL isNamed = NO; + + for (NSUInteger i = 0; i < kCountlyUDNamedFieldsCount; i++) { + if ([kCountlyUDNamedFields[i] isEqualToString:key]) { + isNamed = YES; + self.predefined[key] = [value description]; + break; + } } - }]; + // Handle custom fields + if (!isNamed) { + NSString* truncatedKey = [[key description] cly_truncatedKey:@"[CountlyUserDetails] setPropertiesInternal"]; + if ([self isValidDataType:value]) { + self.customProperties[truncatedKey] = value; + } else { + NSLog(@"[ModuleUserProfile] setPropertiesInternal, provided an unsupported type for key: [%@], value: [%@], type: [%@], omitting call", + key, value, NSStringFromClass([value class])); + } + } + } +} + + +- (BOOL)isValidDataType:(id) value { + if ([value isKindOfClass:[NSNumber class]] || + [value isKindOfClass:[NSString class]] || + ([value isKindOfClass:[NSArray class]] && (value = [(NSArray *)value cly_filterSupportedDataTypes]))) { + return YES; + } + return NO; +} + +// Set a single user property. It can be either a custom one or one of the predefined ones. +- (void)setProperty:(NSString *)key value:(id)value { + NSLog(@"[UserProfile] Calling 'setProperty'"); + + NSMutableDictionary *data = [NSMutableDictionary dictionary]; + if (key != nil && value != nil) { + data[key] = value; + } - return truncatedDict.copy; + [self setPropertiesInternal:data]; } +// Provide a map of user properties to set. +// Those can be either custom user properties or predefined user properties +- (void)setProperties:(NSDictionary *)data { + NSLog(@"[UserProfile] Calling 'setProperties'"); + + if (data == nil) { + NSLog(@"[UserProfile] Provided data can not be 'null'"); + return; + } + + [self setPropertiesInternal:data]; +} @end From 3435cf82722ec419452bda9143dde28ca776e80c Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Fri, 3 Oct 2025 09:00:02 +0300 Subject: [PATCH 08/42] fix: logs --- CountlyUserDetails.m | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/CountlyUserDetails.m b/CountlyUserDetails.m index 54c17a78..b367f132 100644 --- a/CountlyUserDetails.m +++ b/CountlyUserDetails.m @@ -274,6 +274,7 @@ - (void)push:(NSString *)key value:(NSString *)value - (void)push:(NSString *)key numberValue:(NSNumber *)value; { + CLY_LOG_I(@"%s %@ %@", __FUNCTION__, key, value); [self doModification:kCountlyUDKeyModifierPush key:key value:value]; } @@ -369,13 +370,14 @@ - (void)doModification:(NSString *)mod key:(NSString *)key value:(id)value { CLY_LOG_W(@"%s call will be ignored as value is nil!", __FUNCTION__); return; } + NSString* truncatedLog = [NSString stringWithFormat:@"%s",__FUNCTION__]; // If the value is NSString, apply truncation rules if ([value isKindOfClass:[NSString class]]) { - value = [[value description] cly_truncatedValue:@"[CountlyUserDetails] doModification"]; + value = [[value description] cly_truncatedValue:truncatedLog]; } - NSString* truncatedKey = [[key description] cly_truncatedKey:@"[CountlyUserDetails] setPropertiesInternal"]; + NSString* truncatedKey = [[key description] cly_truncatedKey:truncatedLog]; if (![mod isEqualToString:@"$pull"] && ![mod isEqualToString:@"$push"] && ![mod isEqualToString:@"$addToSet"]) { @@ -400,7 +402,7 @@ - (void)doModification:(NSString *)mod key:(NSString *)key value:(id)value { */ - (void)setPropertiesInternal:(NSDictionary *)data { if (data.count == 0) { - NSLog(@"[ModuleUserProfile] setPropertiesInternal, no data was provided"); + CLY_LOG_I(@"%s no data was provided", __FUNCTION__); return; } @@ -408,12 +410,13 @@ - (void)setPropertiesInternal:(NSDictionary *)data { id value = data[key]; if (value == nil || value == [NSNull null]) { - NSLog(@"[ModuleUserProfile] setPropertiesInternal, provided value for key [%@] is 'null'", key); + CLY_LOG_W(@"%s provided value for key [%@] is 'null'", __FUNCTION__, key); continue; } - + + NSString* truncatedLog = [NSString stringWithFormat:@"%s",__FUNCTION__]; if ([value isKindOfClass:[NSString class]]) { - value = [[value description] cly_truncatedValue:@"[CountlyUserDetails] setPropertiesInternal"]; + value = [[value description] cly_truncatedValue:truncatedLog]; } BOOL isNamed = NO; @@ -428,11 +431,11 @@ - (void)setPropertiesInternal:(NSDictionary *)data { // Handle custom fields if (!isNamed) { - NSString* truncatedKey = [[key description] cly_truncatedKey:@"[CountlyUserDetails] setPropertiesInternal"]; + NSString* truncatedKey = [[key description] cly_truncatedKey:truncatedLog]; if ([self isValidDataType:value]) { self.customProperties[truncatedKey] = value; } else { - NSLog(@"[ModuleUserProfile] setPropertiesInternal, provided an unsupported type for key: [%@], value: [%@], type: [%@], omitting call", + CLY_LOG_D(@"%s provided an unsupported type for key: [%@], value: [%@], type: [%@], omitting call",__FUNCTION__, key, value, NSStringFromClass([value class])); } } From b4c60710fd29076655d4ef718125789a970662fd Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Fri, 3 Oct 2025 09:13:34 +0300 Subject: [PATCH 09/42] refactor: unnecessary event packing --- CountlyConnectionManager.m | 9 +-------- CountlyUserDetails.m | 2 +- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/CountlyConnectionManager.m b/CountlyConnectionManager.m index b43a117c..0b63c857 100644 --- a/CountlyConnectionManager.m +++ b/CountlyConnectionManager.m @@ -557,14 +557,7 @@ - (void)endSession - (void)sendEventsWithSaveIfNeeded { - if([Countly.user hasUnsyncedChanges]) - { - [Countly.user save]; - } - else - { - [self sendEventsInternal]; - } + [Countly.user save]; // this also sends the events } - (void)sendEvents diff --git a/CountlyUserDetails.m b/CountlyUserDetails.m index b367f132..01193e9e 100644 --- a/CountlyUserDetails.m +++ b/CountlyUserDetails.m @@ -128,7 +128,7 @@ - (NSString *)serializedUserDetails if (customAll.count > 0) userDictionary[kCountlyUDKeyCustom] = customAll; - if (userDictionary.allKeys.count) + if (userDictionary.count > 0) return [userDictionary cly_JSONify]; return nil; From 4c3dd454e34c74a1b98122ff67258c2be306d0f6 Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Fri, 3 Oct 2025 09:18:03 +0300 Subject: [PATCH 10/42] refactor: unnecessary event packing --- CountlyConnectionManager.m | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/CountlyConnectionManager.m b/CountlyConnectionManager.m index 0b63c857..f2efda27 100644 --- a/CountlyConnectionManager.m +++ b/CountlyConnectionManager.m @@ -557,7 +557,14 @@ - (void)endSession - (void)sendEventsWithSaveIfNeeded { - [Countly.user save]; // this also sends the events + if([Countly.user hasUnsyncedChanges]) + { + [Countly.user save]; + } + else + { + [self sendEventsInternal]; + } } - (void)sendEvents From f11db93f5395addf7510d1af99572fefeed6b562 Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Fri, 3 Oct 2025 12:46:55 +0300 Subject: [PATCH 11/42] fix: array existances be a single k-v --- CountlyUserDetails.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CountlyUserDetails.m b/CountlyUserDetails.m index 01193e9e..787a8f47 100644 --- a/CountlyUserDetails.m +++ b/CountlyUserDetails.m @@ -388,7 +388,7 @@ - (void)doModification:(NSString *)mod key:(NSString *)key value:(id)value { [array addObject:value]; self.customMods[truncatedKey] = @{mod: array}; } else { - self.customMods[truncatedKey] = @{mod: @[value]}; + self.customMods[truncatedKey] = @{mod: value}; } } From 2c27448cda41f5a45d82005e7ed20bb6fbe2fdbe Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Wed, 1 Apr 2026 16:40:05 +0300 Subject: [PATCH 12/42] feat: fix post flutter tests --- Countly.m | 2053 ++++++++++++++++++++--------------------- CountlyCommon.h | 114 ++- CountlyCommon.m | 871 ++++++++--------- CountlyServerConfig.m | 1294 +++++++++++++------------- 4 files changed, 2191 insertions(+), 2141 deletions(-) diff --git a/Countly.m b/Countly.m index d07ef6fd..a018a0c6 100644 --- a/Countly.m +++ b/Countly.m @@ -6,20 +6,19 @@ #import "CountlyCommon.h" -@interface Countly () -{ - NSTimer* timer; - BOOL isSuspended; +@interface Countly () { + NSTimer *timer; + BOOL isSuspended; } @end long long appLoadStartTime; // It holds the event id of previous recorded custom event. -static NSString* previousEventID; +static NSString *previousEventID; // It holds the event name of previous recorded custom event. -static NSString* previousEventName; +static NSString *previousEventName; #if __has_include() -#import + #import static os_unfair_lock previousEventLock = OS_UNFAIR_LOCK_INIT; #endif @implementation Countly @@ -28,1587 +27,1583 @@ @implementation Countly + (void)load { - [super load]; - - appLoadStartTime = floor(NSDate.date.timeIntervalSince1970 * 1000); + [super load]; + + appLoadStartTime = floor(NSDate.date.timeIntervalSince1970 * 1000); } -static Countly *s_sharedCountly = nil; +static Countly *s_sharedCountly = nil; static dispatch_once_t onceToken; + (instancetype)sharedInstance { - dispatch_once(&onceToken, ^{s_sharedCountly = self.new;}); - return s_sharedCountly; + dispatch_once(&onceToken, ^{ + s_sharedCountly = self.new; + }); + return s_sharedCountly; } -- (void)resetInstance { - CLY_LOG_I(@"%s resetting the instance", __FUNCTION__); - // Invalidate timer to avoid callbacks to a deallocated instance between tests. - if (timer) { - [timer invalidate]; - timer = nil; - } - // Remove all notification observers to avoid duplicate registrations after re-init in tests. - [NSNotificationCenter.defaultCenter removeObserver:self]; - isSuspended = NO; - onceToken = 0; - s_sharedCountly = nil; - } +- (void)resetInstance +{ + CLY_LOG_I(@"%s resetting the instance", __FUNCTION__); + // Invalidate timer to avoid callbacks to a deallocated instance between tests. + if (timer) + { + [timer invalidate]; + timer = nil; + } + // Remove all notification observers to avoid duplicate registrations after re-init in tests. + [NSNotificationCenter.defaultCenter removeObserver:self]; + isSuspended = NO; + onceToken = 0; + s_sharedCountly = nil; +} - (instancetype)init { - if (self = [super init]) - { -#if (TARGET_OS_IOS || TARGET_OS_VISION || TARGET_OS_TV ) - [NSNotificationCenter.defaultCenter addObserver:self - selector:@selector(applicationDidEnterBackground:) - name:UIApplicationDidEnterBackgroundNotification - object:nil]; - [NSNotificationCenter.defaultCenter addObserver:self - selector:@selector(applicationWillEnterForeground:) - name:UIApplicationWillEnterForegroundNotification - object:nil]; - [NSNotificationCenter.defaultCenter addObserver:self - selector:@selector(applicationWillTerminate:) - name:UIApplicationWillTerminateNotification - object:nil]; - - [NSNotificationCenter.defaultCenter addObserver:self - selector:@selector(applicationDidBecomeActive:) name:UIApplicationDidBecomeActiveNotification - object:nil]; - [NSNotificationCenter.defaultCenter addObserver:self - selector:@selector(applicationWillResignActive:) name:UIApplicationWillResignActiveNotification - object:nil]; + if (self = [super init]) + { +#if (TARGET_OS_IOS || TARGET_OS_VISION || TARGET_OS_TV) + [NSNotificationCenter.defaultCenter addObserver:self selector:@selector(applicationDidEnterBackground:) name:UIApplicationDidEnterBackgroundNotification object:nil]; + [NSNotificationCenter.defaultCenter addObserver:self selector:@selector(applicationWillEnterForeground:) name:UIApplicationWillEnterForegroundNotification object:nil]; + [NSNotificationCenter.defaultCenter addObserver:self selector:@selector(applicationWillTerminate:) name:UIApplicationWillTerminateNotification object:nil]; + + [NSNotificationCenter.defaultCenter addObserver:self selector:@selector(applicationDidBecomeActive:) name:UIApplicationDidBecomeActiveNotification object:nil]; + [NSNotificationCenter.defaultCenter addObserver:self selector:@selector(applicationWillResignActive:) name:UIApplicationWillResignActiveNotification object:nil]; #elif (TARGET_OS_OSX) - [NSNotificationCenter.defaultCenter addObserver:self - selector:@selector(applicationWillTerminate:) - name:NSApplicationWillTerminateNotification - object:nil]; + [NSNotificationCenter.defaultCenter addObserver:self selector:@selector(applicationWillTerminate:) name:NSApplicationWillTerminateNotification object:nil]; #endif - } - - return self; + } + + return self; } - (void)startWithConfig:(CountlyConfig *)config { - if (CountlyCommon.sharedInstance.hasStarted_) - return; - - CountlyCommon.sharedInstance.hasStarted = YES; - CountlyCommon.sharedInstance.enableDebug = config.enableDebug; - CountlyCommon.sharedInstance.shouldIgnoreTrustCheck = config.shouldIgnoreTrustCheck; - CountlyCommon.sharedInstance.loggerDelegate = config.loggerDelegate; - CountlyCommon.sharedInstance.internalLogLevel = config.internalLogLevel; - - config = [self checkAndFixInternalLimitsConfig:config]; - - if (config.disableSDKBehaviorSettingsUpdates) { - [CountlyServerConfig.sharedInstance disableSDKBehaviourSettings]; - } - [CountlyServerConfig.sharedInstance retrieveServerConfigFromStorage:config]; - - CountlyCommon.sharedInstance.maxKeyLength = config.sdkInternalLimits.getMaxKeyLength; - CountlyCommon.sharedInstance.maxValueLength = config.sdkInternalLimits.getMaxValueSize; - CountlyCommon.sharedInstance.maxSegmentationValues = config.sdkInternalLimits.getMaxSegmentationValues; - - // For backward compatibility, deprecated values are only set incase new values are not provided using sdkInternalLimits interface - if(CountlyCommon.sharedInstance.maxKeyLength == kCountlyMaxKeyLength && config.maxKeyLength != kCountlyMaxKeyLength) { - CountlyCommon.sharedInstance.maxKeyLength = config.maxKeyLength; - CLY_LOG_I(@"%s deprecated maxKeyLength provided, maxKeyLength: [%lu]", __FUNCTION__, (unsigned long)config.maxKeyLength); - } - if(CountlyCommon.sharedInstance.maxValueLength == kCountlyMaxValueSize && config.maxValueLength != kCountlyMaxValueSize) { - CountlyCommon.sharedInstance.maxValueLength = config.maxValueLength; - CLY_LOG_I(@"%s deprecated maxValueLength provided, maxValueLength: [%lu]", __FUNCTION__, (unsigned long)config.maxValueLength); - } - if(CountlyCommon.sharedInstance.maxSegmentationValues == kCountlyMaxSegmentationValues && config.maxSegmentationValues != kCountlyMaxSegmentationValues) { - CountlyCommon.sharedInstance.maxSegmentationValues = config.maxSegmentationValues; - CLY_LOG_I(@"%s deprecated maxSegmentationValues provided, maxSegmentationValues: [%lu]", __FUNCTION__, (unsigned long)config.maxSegmentationValues); - } - - CountlyConsentManager.sharedInstance.requiresConsent = config.requiresConsent; - - if (!config.appKey.length || [config.appKey isEqualToString:@"YOUR_APP_KEY"]) - [NSException raise:@"CountlyAppKeyNotSetException" format:@"appKey property on CountlyConfig object is not set"]; - - if (!config.host.length || [config.host isEqualToString:@"https://YOUR_COUNTLY_SERVER"]) - [NSException raise:@"CountlyHostNotSetException" format:@"host property on CountlyConfig object is not set"]; - - CLY_LOG_I(@"%s initializing, appKey: [%@], serverUrl: [%@], sdkName: [%@], sdkVersion: [%@], device: [%@], osName: [%@], osVersion: [%@], defaultSDKName: [%@], defaultSDKVersion: [%@]", - __FUNCTION__, config.appKey, config.host, CountlyCommon.sharedInstance.SDKName, CountlyCommon.sharedInstance.SDKVersion, CountlyDeviceInfo.device, - CountlyDeviceInfo.osName, CountlyDeviceInfo.osVersion,kCountlySDKName, kCountlySDKVersion); - - - if (!CountlyDeviceInfo.sharedInstance.deviceID || config.resetStoredDeviceID) - { - [self storeCustomDeviceIDState:config.deviceID]; - - [CountlyDeviceInfo.sharedInstance initializeDeviceID:config.deviceID]; - } - - CountlyConnectionManager.sharedInstance.appKey = config.appKey; - CountlyConnectionManager.sharedInstance.host = config.host; - CountlyConnectionManager.sharedInstance.alwaysUsePOST = config.alwaysUsePOST; - CountlyConnectionManager.sharedInstance.pinnedCertificates = config.pinnedCertificates; - CountlyConnectionManager.sharedInstance.secretSalt = config.secretSalt; - CountlyConnectionManager.sharedInstance.URLSessionConfiguration = config.URLSessionConfiguration; - - CountlyPersistency.sharedInstance.eventSendThreshold = config.eventSendThreshold; - CountlyPersistency.sharedInstance.requestDropAgeHours = config.requestDropAgeHours; - CountlyPersistency.sharedInstance.storedRequestsLimit = MAX(1, config.storedRequestsLimit); - - CountlyCommon.sharedInstance.manualSessionHandling = config.manualSessionHandling; - CountlyCommon.sharedInstance.enableManualSessionControlHybridMode = config.enableManualSessionControlHybridMode; - - CountlyCommon.sharedInstance.attributionID = config.attributionID; - - NSDictionary* customMetricsTruncated = [config.customMetrics cly_truncated:@"Custom metric"]; - CountlyDeviceInfo.sharedInstance.customMetrics = [customMetricsTruncated cly_limited:@"Custom metric"]; - - [Countly.user save]; - // If something added related to server config, make sure to check CountlyServerConfig.notifySdkConfigChange - [CountlyServerConfig.sharedInstance fetchServerConfig:config]; - + if (CountlyCommon.sharedInstance.hasStarted_) + return; + + CountlyCommon.sharedInstance.hasStarted = YES; + CountlyCommon.sharedInstance.enableDebug = config.enableDebug; + CountlyCommon.sharedInstance.shouldIgnoreTrustCheck = config.shouldIgnoreTrustCheck; + CountlyCommon.sharedInstance.loggerDelegate = config.loggerDelegate; + CountlyCommon.sharedInstance.internalLogLevel = config.internalLogLevel; + + config = [self checkAndFixInternalLimitsConfig:config]; + + if (config.disableSDKBehaviorSettingsUpdates) + { + [CountlyServerConfig.sharedInstance disableSDKBehaviourSettings]; + } + [CountlyServerConfig.sharedInstance retrieveServerConfigFromStorage:config]; + + CountlyCommon.sharedInstance.maxKeyLength = config.sdkInternalLimits.getMaxKeyLength; + CountlyCommon.sharedInstance.maxValueLength = config.sdkInternalLimits.getMaxValueSize; + CountlyCommon.sharedInstance.maxSegmentationValues = config.sdkInternalLimits.getMaxSegmentationValues; + + // For backward compatibility, deprecated values are only set incase new values are not provided using sdkInternalLimits interface + if (CountlyCommon.sharedInstance.maxKeyLength == kCountlyMaxKeyLength && config.maxKeyLength != kCountlyMaxKeyLength) + { + CountlyCommon.sharedInstance.maxKeyLength = config.maxKeyLength; + CLY_LOG_I(@"%s deprecated maxKeyLength provided, maxKeyLength: [%lu]", __FUNCTION__, (unsigned long)config.maxKeyLength); + } + if (CountlyCommon.sharedInstance.maxValueLength == kCountlyMaxValueSize && config.maxValueLength != kCountlyMaxValueSize) + { + CountlyCommon.sharedInstance.maxValueLength = config.maxValueLength; + CLY_LOG_I(@"%s deprecated maxValueLength provided, maxValueLength: [%lu]", __FUNCTION__, (unsigned long)config.maxValueLength); + } + if (CountlyCommon.sharedInstance.maxSegmentationValues == kCountlyMaxSegmentationValues && config.maxSegmentationValues != kCountlyMaxSegmentationValues) + { + CountlyCommon.sharedInstance.maxSegmentationValues = config.maxSegmentationValues; + CLY_LOG_I(@"%s deprecated maxSegmentationValues provided, maxSegmentationValues: [%lu]", __FUNCTION__, (unsigned long)config.maxSegmentationValues); + } + + CountlyConsentManager.sharedInstance.requiresConsent = config.requiresConsent; + + if (!config.appKey.length || [config.appKey isEqualToString:@"YOUR_APP_KEY"]) + [NSException raise:@"CountlyAppKeyNotSetException" format:@"appKey property on CountlyConfig object is not set"]; + + if (!config.host.length || [config.host isEqualToString:@"https://YOUR_COUNTLY_SERVER"]) + [NSException raise:@"CountlyHostNotSetException" format:@"host property on CountlyConfig object is not set"]; + + CLY_LOG_I(@"%s initializing, appKey: [%@], serverUrl: [%@], sdkName: [%@], sdkVersion: [%@], device: [%@], osName: [%@], osVersion: [%@], defaultSDKName: [%@], defaultSDKVersion: [%@]", __FUNCTION__, config.appKey, config.host, CountlyCommon.sharedInstance.SDKName, + CountlyCommon.sharedInstance.SDKVersion, CountlyDeviceInfo.device, CountlyDeviceInfo.osName, CountlyDeviceInfo.osVersion, kCountlySDKName, kCountlySDKVersion); + + if (!CountlyDeviceInfo.sharedInstance.deviceID || config.resetStoredDeviceID) + { + [self storeCustomDeviceIDState:config.deviceID]; + + [CountlyDeviceInfo.sharedInstance initializeDeviceID:config.deviceID]; + } + + CountlyConnectionManager.sharedInstance.appKey = config.appKey; + CountlyConnectionManager.sharedInstance.host = config.host; + CountlyConnectionManager.sharedInstance.alwaysUsePOST = config.alwaysUsePOST; + CountlyConnectionManager.sharedInstance.pinnedCertificates = config.pinnedCertificates; + CountlyConnectionManager.sharedInstance.secretSalt = config.secretSalt; + CountlyConnectionManager.sharedInstance.URLSessionConfiguration = config.URLSessionConfiguration; + + CountlyPersistency.sharedInstance.eventSendThreshold = config.eventSendThreshold; + CountlyPersistency.sharedInstance.requestDropAgeHours = config.requestDropAgeHours; + CountlyPersistency.sharedInstance.storedRequestsLimit = MAX(1, config.storedRequestsLimit); + + CountlyCommon.sharedInstance.manualSessionHandling = config.manualSessionHandling; + CountlyCommon.sharedInstance.enableManualSessionControlHybridMode = config.enableManualSessionControlHybridMode; + + CountlyCommon.sharedInstance.attributionID = config.attributionID; + + NSDictionary *customMetricsTruncated = [config.customMetrics cly_truncated:@"Custom metric"]; + CountlyDeviceInfo.sharedInstance.customMetrics = [customMetricsTruncated cly_limited:@"Custom metric"]; + + [Countly.user save]; + // If something added related to server config, make sure to check CountlyServerConfig.notifySdkConfigChange + [CountlyServerConfig.sharedInstance fetchServerConfig:config]; + #if (TARGET_OS_IOS) - CountlyFeedbacksInternal.sharedInstance.message = config.starRatingMessage; - CountlyFeedbacksInternal.sharedInstance.sessionCount = config.starRatingSessionCount; - CountlyFeedbacksInternal.sharedInstance.disableAskingForEachAppVersion = config.starRatingDisableAskingForEachAppVersion; - CountlyFeedbacksInternal.sharedInstance.ratingCompletionForAutoAsk = config.starRatingCompletion; - [CountlyFeedbacksInternal.sharedInstance checkForStarRatingAutoAsk]; + CountlyFeedbacksInternal.sharedInstance.message = config.starRatingMessage; + CountlyFeedbacksInternal.sharedInstance.sessionCount = config.starRatingSessionCount; + CountlyFeedbacksInternal.sharedInstance.disableAskingForEachAppVersion = config.starRatingDisableAskingForEachAppVersion; + CountlyFeedbacksInternal.sharedInstance.ratingCompletionForAutoAsk = config.starRatingCompletion; + [CountlyFeedbacksInternal.sharedInstance checkForStarRatingAutoAsk]; #endif - - if(config.disableLocation) - { - [CountlyLocationManager.sharedInstance disableLocation]; - } - else - { - [CountlyLocationManager.sharedInstance updateLocation:config.location city:config.city ISOCountryCode:config.ISOCountryCode IP:config.IP]; - } - - if (!CountlyCommon.sharedInstance.manualSessionHandling) - [CountlyConnectionManager.sharedInstance beginSession]; - else - [CountlyCommon.sharedInstance recordOrientation]; - - //NOTE: If there is no consent for sessions, location info and attribution should be sent separately, as they cannot be sent with begin_session request. -#if (TARGET_OS_IOS || TARGET_OS_VISION || TARGET_OS_OSX ) -#ifndef COUNTLY_EXCLUDE_PUSHNOTIFICATIONS - if ([config.features containsObject:CLYPushNotifications]) - { - CountlyPushNotifications.sharedInstance.isEnabledOnInitialConfig = YES; - CountlyPushNotifications.sharedInstance.pushTestMode = config.pushTestMode; - CountlyPushNotifications.sharedInstance.sendPushTokenAlways = config.sendPushTokenAlways; - CountlyPushNotifications.sharedInstance.doNotShowAlertForNotifications = config.doNotShowAlertForNotifications; - CountlyPushNotifications.sharedInstance.launchNotification = config.launchNotification; - [CountlyPushNotifications.sharedInstance startPushNotifications]; - } -#endif + if (config.disableLocation) + { + [CountlyLocationManager.sharedInstance disableLocation]; + } + else + { + [CountlyLocationManager.sharedInstance updateLocation:config.location city:config.city ISOCountryCode:config.ISOCountryCode IP:config.IP]; + } + + if (!CountlyCommon.sharedInstance.manualSessionHandling) + [CountlyConnectionManager.sharedInstance beginSession]; + else + [CountlyCommon.sharedInstance recordOrientation]; + + // NOTE: If there is no consent for sessions, location info and attribution should be sent separately, as they cannot be sent with begin_session request. + +#if (TARGET_OS_IOS || TARGET_OS_VISION || TARGET_OS_OSX) + #ifndef COUNTLY_EXCLUDE_PUSHNOTIFICATIONS + if ([config.features containsObject:CLYPushNotifications]) + { + CountlyPushNotifications.sharedInstance.isEnabledOnInitialConfig = YES; + CountlyPushNotifications.sharedInstance.pushTestMode = config.pushTestMode; + CountlyPushNotifications.sharedInstance.sendPushTokenAlways = config.sendPushTokenAlways; + CountlyPushNotifications.sharedInstance.doNotShowAlertForNotifications = config.doNotShowAlertForNotifications; + CountlyPushNotifications.sharedInstance.launchNotification = config.launchNotification; + [CountlyPushNotifications.sharedInstance startPushNotifications]; + } + #endif #endif - - if(config.crashes.crashFilterCallback) { - [CountlyCrashReporter.sharedInstance setCrashFilterCallback:config.crashes.crashFilterCallback]; - } - - CountlyCrashReporter.sharedInstance.crashSegmentation = config.crashSegmentation; - CountlyCrashReporter.sharedInstance.crashLogLimit = config.sdkInternalLimits.getMaxBreadcrumbCount; - // For backward compatibility, deprecated values are only set incase new values are not provided using sdkInternalLimits interface - if(CountlyCrashReporter.sharedInstance.crashLogLimit == kCountlyMaxBreadcrumbCount && config.crashLogLimit != kCountlyMaxBreadcrumbCount) { - CountlyCrashReporter.sharedInstance.crashLogLimit = MAX(1, config.crashLogLimit); - CLY_LOG_W(@"%s deprecated maxBreadcrumbCount provided, maxBreadcrumbCount: [%lu]", __FUNCTION__, (unsigned long)config.crashLogLimit); - } - CountlyCrashReporter.sharedInstance.crashFilter = config.crashFilter; - CountlyCrashReporter.sharedInstance.shouldUsePLCrashReporter = config.shouldUsePLCrashReporter; - CountlyCrashReporter.sharedInstance.shouldUseMachSignalHandler = config.shouldUseMachSignalHandler; - CountlyCrashReporter.sharedInstance.crashOccuredOnPreviousSessionCallback = config.crashOccuredOnPreviousSessionCallback; - CountlyCrashReporter.sharedInstance.shouldSendCrashReportCallback = config.shouldSendCrashReportCallback; - if ([config.features containsObject:CLYCrashReporting]) + + if (config.crashes.crashFilterCallback) + { + [CountlyCrashReporter.sharedInstance setCrashFilterCallback:config.crashes.crashFilterCallback]; + } + + CountlyCrashReporter.sharedInstance.crashSegmentation = config.crashSegmentation; + CountlyCrashReporter.sharedInstance.crashLogLimit = config.sdkInternalLimits.getMaxBreadcrumbCount; + // For backward compatibility, deprecated values are only set incase new values are not provided using sdkInternalLimits interface + if (CountlyCrashReporter.sharedInstance.crashLogLimit == kCountlyMaxBreadcrumbCount && config.crashLogLimit != kCountlyMaxBreadcrumbCount) + { + CountlyCrashReporter.sharedInstance.crashLogLimit = MAX(1, config.crashLogLimit); + CLY_LOG_W(@"%s deprecated maxBreadcrumbCount provided, maxBreadcrumbCount: [%lu]", __FUNCTION__, (unsigned long)config.crashLogLimit); + } + CountlyCrashReporter.sharedInstance.crashFilter = config.crashFilter; + CountlyCrashReporter.sharedInstance.shouldUsePLCrashReporter = config.shouldUsePLCrashReporter; + CountlyCrashReporter.sharedInstance.shouldUseMachSignalHandler = config.shouldUseMachSignalHandler; + CountlyCrashReporter.sharedInstance.crashOccuredOnPreviousSessionCallback = config.crashOccuredOnPreviousSessionCallback; + CountlyCrashReporter.sharedInstance.shouldSendCrashReportCallback = config.shouldSendCrashReportCallback; + if ([config.features containsObject:CLYCrashReporting]) + { + CountlyCrashReporter.sharedInstance.isEnabledOnInitialConfig = YES; + if (CountlyServerConfig.sharedInstance.crashReportingEnabled) { - CountlyCrashReporter.sharedInstance.isEnabledOnInitialConfig = YES; - if (CountlyServerConfig.sharedInstance.crashReportingEnabled) - { - [CountlyCrashReporter.sharedInstance startCrashReporting]; - } + [CountlyCrashReporter.sharedInstance startCrashReporting]; } + } -#if (TARGET_OS_IOS || TARGET_OS_TV ) - if (config.enableAutomaticViewTracking || [config.features containsObject:CLYAutoViewTracking]) +#if (TARGET_OS_IOS || TARGET_OS_TV) + if (config.enableAutomaticViewTracking || [config.features containsObject:CLYAutoViewTracking]) + { + // Print deprecation flag for feature + CountlyViewTrackingInternal.sharedInstance.isEnabledOnInitialConfig = YES; + if (CountlyServerConfig.sharedInstance.viewTrackingEnabled) { - // Print deprecation flag for feature - CountlyViewTrackingInternal.sharedInstance.isEnabledOnInitialConfig = YES; - if (CountlyServerConfig.sharedInstance.viewTrackingEnabled) - { - [CountlyViewTrackingInternal.sharedInstance startAutoViewTracking]; - } - } - if (config.automaticViewTrackingExclusionList) { - [CountlyViewTrackingInternal.sharedInstance addAutoViewTrackingExclutionList:config.automaticViewTrackingExclusionList]; + [CountlyViewTrackingInternal.sharedInstance startAutoViewTracking]; } + } + if (config.automaticViewTrackingExclusionList) + { + [CountlyViewTrackingInternal.sharedInstance addAutoViewTrackingExclutionList:config.automaticViewTrackingExclusionList]; + } #endif - - if(config.disableViewRestartForManualRecording){ - CountlyViewTrackingInternal.sharedInstance.isManualViewRestartActive = NO; - } - - if(config.experimental.enablePreviousNameRecording) { - CountlyViewTrackingInternal.sharedInstance.enablePreviousNameRecording = YES; - } - if(config.experimental.enableVisibiltyTracking) { - CountlyCommon.sharedInstance.enableVisibiltyTracking = YES; - } - if (config.globalViewSegmentation) { - [CountlyViewTrackingInternal.sharedInstance setGlobalViewSegmentation:config.globalViewSegmentation]; - } - timer = [NSTimer timerWithTimeInterval:config.updateSessionPeriod target:self selector:@selector(onTimer:) userInfo:nil repeats:YES]; - [NSRunLoop.mainRunLoop addTimer:timer forMode:NSRunLoopCommonModes]; - - CountlyRemoteConfigInternal.sharedInstance.isRCAutomaticTriggersEnabled = config.enableRemoteConfigAutomaticTriggers || config.enableRemoteConfig; - CountlyRemoteConfigInternal.sharedInstance.isRCValueCachingEnabled = config.enableRemoteConfigValueCaching; - CountlyRemoteConfigInternal.sharedInstance.remoteConfigCompletionHandler = config.remoteConfigCompletionHandler; - if (config.getRemoteConfigGlobalCallbacks) { - CountlyRemoteConfigInternal.sharedInstance.remoteConfigGlobalCallbacks = config.getRemoteConfigGlobalCallbacks; - } - if (config.enrollABOnRCDownload) { - CountlyRemoteConfigInternal.sharedInstance.enrollABOnRCDownload = config.enrollABOnRCDownload; - } - [CountlyRemoteConfigInternal.sharedInstance downloadRemoteConfigAutomatically]; - if (config.apm.getAppStartTimestampOverride) { - appLoadStartTime = config.apm.getAppStartTimestampOverride; - } + + if (config.disableViewRestartForManualRecording) + { + CountlyViewTrackingInternal.sharedInstance.isManualViewRestartActive = NO; + } + + if (config.experimental.enablePreviousNameRecording) + { + CountlyViewTrackingInternal.sharedInstance.enablePreviousNameRecording = YES; + } + if (config.experimental.enableVisibiltyTracking) + { + CountlyCommon.sharedInstance.enableVisibiltyTracking = YES; + } + if (config.globalViewSegmentation) + { + [CountlyViewTrackingInternal.sharedInstance setGlobalViewSegmentation:config.globalViewSegmentation]; + } + timer = [NSTimer timerWithTimeInterval:config.updateSessionPeriod target:self selector:@selector(onTimer:) userInfo:nil repeats:YES]; + [NSRunLoop.mainRunLoop addTimer:timer forMode:NSRunLoopCommonModes]; + + CountlyRemoteConfigInternal.sharedInstance.isRCAutomaticTriggersEnabled = config.enableRemoteConfigAutomaticTriggers || config.enableRemoteConfig; + CountlyRemoteConfigInternal.sharedInstance.isRCValueCachingEnabled = config.enableRemoteConfigValueCaching; + CountlyRemoteConfigInternal.sharedInstance.remoteConfigCompletionHandler = config.remoteConfigCompletionHandler; + if (config.getRemoteConfigGlobalCallbacks) + { + CountlyRemoteConfigInternal.sharedInstance.remoteConfigGlobalCallbacks = config.getRemoteConfigGlobalCallbacks; + } + if (config.enrollABOnRCDownload) + { + CountlyRemoteConfigInternal.sharedInstance.enrollABOnRCDownload = config.enrollABOnRCDownload; + } + [CountlyRemoteConfigInternal.sharedInstance downloadRemoteConfigAutomatically]; + if (config.apm.getAppStartTimestampOverride) + { + appLoadStartTime = config.apm.getAppStartTimestampOverride; + } #if (TARGET_OS_IOS) - if(config.content.getGlobalContentCallback) { - CountlyContentBuilderInternal.sharedInstance.contentCallback = config.content.getGlobalContentCallback; - } - if(config.content.getZoneTimerInterval){ - CountlyContentBuilderInternal.sharedInstance.zoneTimerInterval = config.content.getZoneTimerInterval; - } - if(config.content.getWebViewDisplayOption){ - CountlyContentBuilderInternal.sharedInstance.webViewDisplayOption = config.content.getWebViewDisplayOption; - } + if (config.content.getGlobalContentCallback) + { + CountlyContentBuilderInternal.sharedInstance.contentCallback = config.content.getGlobalContentCallback; + } + if (config.content.getZoneTimerInterval) + { + CountlyContentBuilderInternal.sharedInstance.zoneTimerInterval = config.content.getZoneTimerInterval; + } + if (config.content.getWebViewDisplayOption) + { + CountlyContentBuilderInternal.sharedInstance.webViewDisplayOption = config.content.getWebViewDisplayOption; + } #endif - - [CountlyPerformanceMonitoring.sharedInstance startWithConfig:config.apm]; - - CountlyCommon.sharedInstance.enableOrientationTracking = config.enableOrientationTracking; - [CountlyCommon.sharedInstance observeDeviceOrientationChanges]; - - [CountlyConnectionManager.sharedInstance proceedOnQueue]; - - //TODO: Should move at the top after checking the the edge cases of current implementation - if (config.enableAllConsents) - [self giveAllConsents]; - else if (config.consents) - [self giveConsentForFeatures:config.consents]; - else if (config.requiresConsent) - [CountlyConsentManager.sharedInstance sendConsents]; - - if (!CountlyConsentManager.sharedInstance.consentForSessions) - { - //Send an empty location if location is disabled or location consent is not given, without checking for location consent. - if (!CountlyConsentManager.sharedInstance.consentForLocation || CountlyLocationManager.sharedInstance.isLocationInfoDisabled) - { - [CountlyConnectionManager.sharedInstance sendLocationInfo]; - } - else - { - [CountlyLocationManager.sharedInstance sendLocationInfo]; - } - [CountlyConnectionManager.sharedInstance sendAttribution]; - } - - - if (config.campaignType && config.campaignData) - [self recordDirectAttributionWithCampaignType:config.campaignType andCampaignData:config.campaignData]; - - if (config.indirectAttribution) - [self recordIndirectAttribution:config.indirectAttribution]; - - [CountlyHealthTracker.sharedInstance sendHealthCheck]; -} -- (CountlyConfig *) checkAndFixInternalLimitsConfig:(CountlyConfig *)config -{ - if (config.sdkInternalLimits.getMaxKeyLength == 0) { - [config.sdkInternalLimits setMaxKeyLength:kCountlyMaxKeyLength]; - CLY_LOG_W(@"%s Ignoring provided value of %lu for 'maxKeyLength' because it's less than 1", __FUNCTION__, (unsigned long)config.sdkInternalLimits.getMaxKeyLength); - } - else if(config.sdkInternalLimits.getMaxKeyLength != kCountlyMaxKeyLength) - { - CLY_LOG_I(@"%s provided 'maxKeyLength' override:[ %lu ]", __FUNCTION__, (unsigned long)config.sdkInternalLimits.getMaxKeyLength); - } - - if (config.sdkInternalLimits.getMaxValueSize == 0) { - [config.sdkInternalLimits setMaxValueSize:kCountlyMaxValueSize]; - CLY_LOG_W(@"%s Ignoring provided value of %lu for 'maxValueSize' because it's less than 1", __FUNCTION__, (unsigned long)config.sdkInternalLimits.getMaxValueSize); - } - else if(config.sdkInternalLimits.getMaxValueSize != kCountlyMaxValueSize) - { - CLY_LOG_I(@"%s provided 'maxValueSize' override:[ %lu ]", __FUNCTION__, (unsigned long)config.sdkInternalLimits.getMaxValueSize); - } - - if (config.sdkInternalLimits.getMaxSegmentationValues == 0) { - [config.sdkInternalLimits setMaxSegmentationValues:kCountlyMaxSegmentationValues]; - CLY_LOG_W(@"%s Ignoring provided value of %lu for 'maxSegmentationValues' because it's less than 1", __FUNCTION__, (unsigned long)config.sdkInternalLimits.getMaxSegmentationValues); - } - else if(config.sdkInternalLimits.getMaxSegmentationValues != kCountlyMaxSegmentationValues) - { - CLY_LOG_I(@"%s provided 'maxSegmentationValues' override:[ %lu ]", __FUNCTION__, (unsigned long)config.sdkInternalLimits.getMaxSegmentationValues); - } - - if (config.sdkInternalLimits.getMaxBreadcrumbCount == 0) { - [config.sdkInternalLimits setMaxBreadcrumbCount:kCountlyMaxBreadcrumbCount]; - CLY_LOG_W(@"%s Ignoring provided value of %lu for 'maxBreadcrumbCount' because it's less than 1", __FUNCTION__, (unsigned long)config.sdkInternalLimits.getMaxBreadcrumbCount); - } - else if(config.sdkInternalLimits.getMaxBreadcrumbCount != kCountlyMaxBreadcrumbCount) - { - CLY_LOG_I(@"%s provided 'maxBreadcrumbCount' override:[ %lu ]", __FUNCTION__, (unsigned long)config.sdkInternalLimits.getMaxBreadcrumbCount); - } - - if(config.sdkInternalLimits.getMaxStackTraceLineLength != kCountlyMaxStackTraceLinesPerThread) + [CountlyPerformanceMonitoring.sharedInstance startWithConfig:config.apm]; + + CountlyCommon.sharedInstance.enableOrientationTracking = config.enableOrientationTracking; + [CountlyCommon.sharedInstance observeDeviceOrientationChanges]; + + [CountlyConnectionManager.sharedInstance proceedOnQueue]; + + // TODO: Should move at the top after checking the the edge cases of current implementation + if (config.enableAllConsents) + [self giveAllConsents]; + else if (config.consents) + [self giveConsentForFeatures:config.consents]; + else if (config.requiresConsent) + [CountlyConsentManager.sharedInstance sendConsents]; + + if (!CountlyConsentManager.sharedInstance.consentForSessions) + { + // Send an empty location if location is disabled or location consent is not given, without checking for location consent. + if (!CountlyConsentManager.sharedInstance.consentForLocation || CountlyLocationManager.sharedInstance.isLocationInfoDisabled) { - CLY_LOG_W(@"%s 'maxStackTraceLineLength' is currently a placeholder and doesn't actively utilize the set values.", __FUNCTION__); + [CountlyConnectionManager.sharedInstance sendLocationInfo]; } - - if(config.sdkInternalLimits.getMaxStackTraceLinesPerThread != kCountlyMaxStackTraceLineLength) + else { - CLY_LOG_I(@"%s 'maxStackTraceLinesPerThread' is currently a placeholder and doesn't actively utilize the set values.", __FUNCTION__); + [CountlyLocationManager.sharedInstance sendLocationInfo]; } - return config; + [CountlyConnectionManager.sharedInstance sendAttribution]; + } + + if (config.campaignType && config.campaignData) + [self recordDirectAttributionWithCampaignType:config.campaignType andCampaignData:config.campaignData]; + + if (config.indirectAttribution) + [self recordIndirectAttribution:config.indirectAttribution]; + + [CountlyHealthTracker.sharedInstance sendHealthCheck]; + + CountlyCommon.sharedInstance.hasFinishedInit = YES; +} + +- (CountlyConfig *)checkAndFixInternalLimitsConfig:(CountlyConfig *)config +{ + if (config.sdkInternalLimits.getMaxKeyLength == 0) + { + [config.sdkInternalLimits setMaxKeyLength:kCountlyMaxKeyLength]; + CLY_LOG_W(@"%s Ignoring provided value of %lu for 'maxKeyLength' because it's less than 1", __FUNCTION__, (unsigned long)config.sdkInternalLimits.getMaxKeyLength); + } + else if (config.sdkInternalLimits.getMaxKeyLength != kCountlyMaxKeyLength) + { + CLY_LOG_I(@"%s provided 'maxKeyLength' override:[ %lu ]", __FUNCTION__, (unsigned long)config.sdkInternalLimits.getMaxKeyLength); + } + + if (config.sdkInternalLimits.getMaxValueSize == 0) + { + [config.sdkInternalLimits setMaxValueSize:kCountlyMaxValueSize]; + CLY_LOG_W(@"%s Ignoring provided value of %lu for 'maxValueSize' because it's less than 1", __FUNCTION__, (unsigned long)config.sdkInternalLimits.getMaxValueSize); + } + else if (config.sdkInternalLimits.getMaxValueSize != kCountlyMaxValueSize) + { + CLY_LOG_I(@"%s provided 'maxValueSize' override:[ %lu ]", __FUNCTION__, (unsigned long)config.sdkInternalLimits.getMaxValueSize); + } + + if (config.sdkInternalLimits.getMaxSegmentationValues == 0) + { + [config.sdkInternalLimits setMaxSegmentationValues:kCountlyMaxSegmentationValues]; + CLY_LOG_W(@"%s Ignoring provided value of %lu for 'maxSegmentationValues' because it's less than 1", __FUNCTION__, (unsigned long)config.sdkInternalLimits.getMaxSegmentationValues); + } + else if (config.sdkInternalLimits.getMaxSegmentationValues != kCountlyMaxSegmentationValues) + { + CLY_LOG_I(@"%s provided 'maxSegmentationValues' override:[ %lu ]", __FUNCTION__, (unsigned long)config.sdkInternalLimits.getMaxSegmentationValues); + } + + if (config.sdkInternalLimits.getMaxBreadcrumbCount == 0) + { + [config.sdkInternalLimits setMaxBreadcrumbCount:kCountlyMaxBreadcrumbCount]; + CLY_LOG_W(@"%s Ignoring provided value of %lu for 'maxBreadcrumbCount' because it's less than 1", __FUNCTION__, (unsigned long)config.sdkInternalLimits.getMaxBreadcrumbCount); + } + else if (config.sdkInternalLimits.getMaxBreadcrumbCount != kCountlyMaxBreadcrumbCount) + { + CLY_LOG_I(@"%s provided 'maxBreadcrumbCount' override:[ %lu ]", __FUNCTION__, (unsigned long)config.sdkInternalLimits.getMaxBreadcrumbCount); + } + + if (config.sdkInternalLimits.getMaxStackTraceLineLength != kCountlyMaxStackTraceLinesPerThread) + { + CLY_LOG_W(@"%s 'maxStackTraceLineLength' is currently a placeholder and doesn't actively utilize the set values.", __FUNCTION__); + } + + if (config.sdkInternalLimits.getMaxStackTraceLinesPerThread != kCountlyMaxStackTraceLineLength) + { + CLY_LOG_I(@"%s 'maxStackTraceLinesPerThread' is currently a placeholder and doesn't actively utilize the set values.", __FUNCTION__); + } + return config; } #pragma mark - - (void)onTimer:(NSTimer *)timer { - CLY_LOG_D(@"%s tick is happening sending events, manualSessions: [%d], hybridSessions: [%d], isSuspended: [%d]", __FUNCTION__, CountlyCommon.sharedInstance.manualSessionHandling, CountlyCommon.sharedInstance.enableManualSessionControlHybridMode, isSuspended); - if (isSuspended) - return; - - if (!CountlyCommon.sharedInstance.manualSessionHandling) - { - [CountlyConnectionManager.sharedInstance updateSession]; - } - // this condtion is called only when both manual session handling and hybrid mode is enabled. - else if (CountlyCommon.sharedInstance.enableManualSessionControlHybridMode) - { - [CountlyConnectionManager.sharedInstance updateSession]; - } - - [CountlyConnectionManager.sharedInstance sendEventsWithSaveIfNeeded]; + CLY_LOG_D(@"%s tick is happening sending events, manualSessions: [%d], hybridSessions: [%d], isSuspended: [%d]", __FUNCTION__, CountlyCommon.sharedInstance.manualSessionHandling, CountlyCommon.sharedInstance.enableManualSessionControlHybridMode, isSuspended); + if (isSuspended) + return; + + if (!CountlyCommon.sharedInstance.manualSessionHandling) + { + [CountlyConnectionManager.sharedInstance updateSession]; + } + // this condtion is called only when both manual session handling and hybrid mode is enabled. + else if (CountlyCommon.sharedInstance.enableManualSessionControlHybridMode) + { + [CountlyConnectionManager.sharedInstance updateSession]; + } + + [CountlyConnectionManager.sharedInstance sendEventsWithSaveIfNeeded]; } - (void)suspend { #if (TARGET_OS_WATCH) - CLY_LOG_I(@"%s", __FUNCTION__); + CLY_LOG_I(@"%s", __FUNCTION__); #endif - - if (!CountlyCommon.sharedInstance.hasStarted) - return; - - if (isSuspended) - return; - - CLY_LOG_D(@"%s sending events, saving the state, manualSessions: [%d]", __FUNCTION__, CountlyCommon.sharedInstance.manualSessionHandling); - - isSuspended = YES; - - [CountlyViewTrackingInternal.sharedInstance applicationDidEnterBackground]; - - [CountlyConnectionManager.sharedInstance sendEventsWithSaveIfNeeded]; - - if (!CountlyCommon.sharedInstance.manualSessionHandling) - [CountlyConnectionManager.sharedInstance endSession]; - - [CountlyPersistency.sharedInstance saveToFile]; + + if (!CountlyCommon.sharedInstance.hasStarted) + return; + + if (isSuspended) + return; + + CLY_LOG_D(@"%s sending events, saving the state, manualSessions: [%d]", __FUNCTION__, CountlyCommon.sharedInstance.manualSessionHandling); + + isSuspended = YES; + + [CountlyViewTrackingInternal.sharedInstance applicationDidEnterBackground]; + + [CountlyConnectionManager.sharedInstance sendEventsWithSaveIfNeeded]; + + if (!CountlyCommon.sharedInstance.manualSessionHandling) + [CountlyConnectionManager.sharedInstance endSession]; + + [CountlyPersistency.sharedInstance saveToFile]; } - (void)resume { #if (TARGET_OS_WATCH) - CLY_LOG_I(@"%s", __FUNCTION__); + CLY_LOG_I(@"%s", __FUNCTION__); #endif - - if (!CountlyCommon.sharedInstance.hasStarted) - return; - + + if (!CountlyCommon.sharedInstance.hasStarted) + return; + #if (TARGET_OS_WATCH) - //NOTE: Skip first time to prevent double begin session because of applicationDidBecomeActive call on launch of watchOS apps - static BOOL isFirstCall = YES; - - if (isFirstCall) - { - isFirstCall = NO; - return; - } + // NOTE: Skip first time to prevent double begin session because of applicationDidBecomeActive call on launch of watchOS apps + static BOOL isFirstCall = YES; + + if (isFirstCall) + { + isFirstCall = NO; + return; + } #endif - - CLY_LOG_D(@"%s manualSessions: [%d]", __FUNCTION__, CountlyCommon.sharedInstance.manualSessionHandling); - - if (!CountlyCommon.sharedInstance.manualSessionHandling) - [CountlyConnectionManager.sharedInstance beginSession]; - - [CountlyViewTrackingInternal.sharedInstance applicationWillEnterForeground]; - - isSuspended = NO; + + CLY_LOG_D(@"%s manualSessions: [%d]", __FUNCTION__, CountlyCommon.sharedInstance.manualSessionHandling); + + if (!CountlyCommon.sharedInstance.manualSessionHandling) + [CountlyConnectionManager.sharedInstance beginSession]; + + [CountlyViewTrackingInternal.sharedInstance applicationWillEnterForeground]; + + isSuspended = NO; } - (void)applicationDidBecomeActive:(NSNotification *)notification { - CLY_LOG_D(@"%s app enters foreground", __FUNCTION__); + CLY_LOG_D(@"%s app enters foreground", __FUNCTION__); [CountlyServerConfig.sharedInstance fetchServerConfigIfTimeIsUp]; - [self resume]; + [self resume]; } - (void)applicationWillResignActive:(NSNotification *)notification { - CLY_LOG_D(@"%s, app enters background", __FUNCTION__); - [CountlyHealthTracker.sharedInstance saveState]; + CLY_LOG_D(@"%s, app enters background", __FUNCTION__); + [CountlyHealthTracker.sharedInstance saveState]; } - (void)applicationDidEnterBackground:(NSNotification *)notification { - CLY_LOG_D(@"%s, app did enter background.", __FUNCTION__); - [CountlyHealthTracker.sharedInstance saveState]; - [self suspend]; + CLY_LOG_D(@"%s, app did enter background.", __FUNCTION__); + [CountlyHealthTracker.sharedInstance saveState]; + [self suspend]; } - (void)applicationWillEnterForeground:(NSNotification *)notification { - CLY_LOG_D(@"%s, app will enter foreground.", __FUNCTION__); + CLY_LOG_D(@"%s, app will enter foreground.", __FUNCTION__); } - (void)applicationWillTerminate:(NSNotification *)notification { - CLY_LOG_D(@"%s, app will terminate.", __FUNCTION__); - - [CountlyHealthTracker.sharedInstance saveState]; - - CountlyConnectionManager.sharedInstance.isTerminating = YES; - - [CountlyViewTrackingInternal.sharedInstance applicationWillTerminate]; - - [CountlyConnectionManager.sharedInstance sendEventsWithSaveIfNeeded]; - - [CountlyPerformanceMonitoring.sharedInstance endBackgroundTrace]; - - [CountlyPersistency.sharedInstance saveToFileSync]; -} + CLY_LOG_D(@"%s, app will terminate.", __FUNCTION__); + + [CountlyHealthTracker.sharedInstance saveState]; + + CountlyConnectionManager.sharedInstance.isTerminating = YES; + [CountlyViewTrackingInternal.sharedInstance applicationWillTerminate]; + + [CountlyConnectionManager.sharedInstance sendEventsWithSaveIfNeeded]; + + [CountlyPerformanceMonitoring.sharedInstance endBackgroundTrace]; + + [CountlyPersistency.sharedInstance saveToFileSync]; +} - (void)dealloc { - [NSNotificationCenter.defaultCenter removeObserver:self]; - - if (timer) - { - [timer invalidate]; - timer = nil; - } -} + [NSNotificationCenter.defaultCenter removeObserver:self]; + if (timer) + { + [timer invalidate]; + timer = nil; + } +} #pragma mark - Override Configuration - (void)setNewHost:(NSString *)newHost { - CLY_LOG_I(@"%s %@", __FUNCTION__, newHost); - - if (!newHost.length) - { - CLY_LOG_W(@"%s new host is invalid!", __FUNCTION__); - return; - } - - CountlyConnectionManager.sharedInstance.host = newHost; + CLY_LOG_I(@"%s %@", __FUNCTION__, newHost); + + if (!newHost.length) + { + CLY_LOG_W(@"%s new host is invalid!", __FUNCTION__); + return; + } + + CountlyConnectionManager.sharedInstance.host = newHost; } - (void)setNewURLSessionConfiguration:(NSURLSessionConfiguration *)newURLSessionConfiguration { - CLY_LOG_I(@"%s %@", __FUNCTION__, newURLSessionConfiguration); - - CountlyConnectionManager.sharedInstance.URLSessionConfiguration = newURLSessionConfiguration; + CLY_LOG_I(@"%s %@", __FUNCTION__, newURLSessionConfiguration); + + CountlyConnectionManager.sharedInstance.URLSessionConfiguration = newURLSessionConfiguration; } - (void)setNewAppKey:(NSString *)newAppKey { - CLY_LOG_I(@"%s %@", __FUNCTION__, newAppKey); - - if (!newAppKey.length) - { - CLY_LOG_W(@"%s new app key is invalid!", __FUNCTION__); - return; - } - - [self suspend]; - - [CountlyPerformanceMonitoring.sharedInstance clearAllCustomTraces]; - - CountlyConnectionManager.sharedInstance.appKey = newAppKey; - - [self resume]; -} + CLY_LOG_I(@"%s %@", __FUNCTION__, newAppKey); + + if (!newAppKey.length) + { + CLY_LOG_W(@"%s new app key is invalid!", __FUNCTION__); + return; + } + [self suspend]; + [CountlyPerformanceMonitoring.sharedInstance clearAllCustomTraces]; + + CountlyConnectionManager.sharedInstance.appKey = newAppKey; + + [self resume]; +} #pragma mark - Queue Operations -- (void)recordMetrics:(NSDictionary * _Nullable)metricsOverride +- (void)recordMetrics:(NSDictionary *_Nullable)metricsOverride { - CLY_LOG_I(@"%s %@", __FUNCTION__, metricsOverride); - [CountlyConnectionManager.sharedInstance recordMetrics:metricsOverride]; + CLY_LOG_I(@"%s %@", __FUNCTION__, metricsOverride); + [CountlyConnectionManager.sharedInstance recordMetrics:metricsOverride]; } - (void)flushQueues { - CLY_LOG_I(@"%s", __FUNCTION__); - - [CountlyPersistency.sharedInstance flushEvents]; - [CountlyPersistency.sharedInstance flushQueue]; + CLY_LOG_I(@"%s", __FUNCTION__); + + [CountlyPersistency.sharedInstance flushEvents]; + [CountlyPersistency.sharedInstance flushQueue]; } - (void)replaceAllAppKeysInQueueWithCurrentAppKey { - CLY_LOG_I(@"%s", __FUNCTION__); - - [CountlyPersistency.sharedInstance replaceAllAppKeysInQueueWithCurrentAppKey]; + CLY_LOG_I(@"%s", __FUNCTION__); + + [CountlyPersistency.sharedInstance replaceAllAppKeysInQueueWithCurrentAppKey]; } - (void)removeDifferentAppKeysFromQueue { - CLY_LOG_I(@"%s", __FUNCTION__); - - [CountlyPersistency.sharedInstance removeDifferentAppKeysFromQueue]; + CLY_LOG_I(@"%s", __FUNCTION__); + + [CountlyPersistency.sharedInstance removeDifferentAppKeysFromQueue]; } -- (void)addDirectRequest:(NSDictionary * _Nullable)requestParameters +- (void)addDirectRequest:(NSDictionary *_Nullable)requestParameters { - CLY_LOG_I(@"%s %@", __FUNCTION__, requestParameters); - - [CountlyConnectionManager.sharedInstance addDirectRequest:requestParameters]; -} + CLY_LOG_I(@"%s %@", __FUNCTION__, requestParameters); -- (void)addCustomNetworkRequestHeaders:(NSDictionary *_Nullable)customHeaderValues { - CLY_LOG_I(@"%s %@", __FUNCTION__, customHeaderValues); - - // Ignore nil or empty dictionary - if (customHeaderValues == nil || customHeaderValues.count == 0) { - return; - } - - [CountlyConnectionManager.sharedInstance addCustomNetworkRequestHeaders:customHeaderValues]; + [CountlyConnectionManager.sharedInstance addDirectRequest:requestParameters]; } +- (void)addCustomNetworkRequestHeaders:(NSDictionary *_Nullable)customHeaderValues +{ + CLY_LOG_I(@"%s %@", __FUNCTION__, customHeaderValues); + + // Ignore nil or empty dictionary + if (customHeaderValues == nil || customHeaderValues.count == 0) + { + return; + } + + [CountlyConnectionManager.sharedInstance addCustomNetworkRequestHeaders:customHeaderValues]; +} #pragma mark - Sessions - (void)beginSession { - CLY_LOG_I(@"%s", __FUNCTION__); - - if (CountlyCommon.sharedInstance.manualSessionHandling) - [CountlyConnectionManager.sharedInstance beginSession]; + CLY_LOG_I(@"%s", __FUNCTION__); + + if (CountlyCommon.sharedInstance.manualSessionHandling) + [CountlyConnectionManager.sharedInstance beginSession]; } - (void)updateSession { - CLY_LOG_I(@"%s", __FUNCTION__); - - if (CountlyCommon.sharedInstance.manualSessionHandling) - [CountlyConnectionManager.sharedInstance updateSession]; + CLY_LOG_I(@"%s", __FUNCTION__); + + if (CountlyCommon.sharedInstance.manualSessionHandling) + [CountlyConnectionManager.sharedInstance updateSession]; } - (void)endSession { - CLY_LOG_I(@"%s", __FUNCTION__); - - if (CountlyCommon.sharedInstance.manualSessionHandling) - { - [CountlyConnectionManager.sharedInstance sendEventsWithSaveIfNeeded]; - [CountlyConnectionManager.sharedInstance endSession]; - } -} - - + CLY_LOG_I(@"%s", __FUNCTION__); + if (CountlyCommon.sharedInstance.manualSessionHandling) + { + [CountlyConnectionManager.sharedInstance sendEventsWithSaveIfNeeded]; + [CountlyConnectionManager.sharedInstance endSession]; + } +} #pragma mark - Device ID - (NSString *)deviceID { - CLY_LOG_I(@"%s", __FUNCTION__); - - return CountlyDeviceInfo.sharedInstance.deviceID.cly_URLEscaped; + CLY_LOG_I(@"%s", __FUNCTION__); + + return CountlyDeviceInfo.sharedInstance.deviceID.cly_URLEscaped; } - (CLYDeviceIDType)deviceIDType { - CLY_LOG_I(@"%s", __FUNCTION__); - - if (CountlyDeviceInfo.sharedInstance.isDeviceIDTemporary) - return CLYDeviceIDTypeTemporary; - - if ([CountlyPersistency.sharedInstance retrieveIsCustomDeviceID]) - return CLYDeviceIDTypeCustom; + CLY_LOG_I(@"%s", __FUNCTION__); + + if (CountlyDeviceInfo.sharedInstance.isDeviceIDTemporary) + return CLYDeviceIDTypeTemporary; -#if (TARGET_OS_IOS || TARGET_OS_VISION || TARGET_OS_TV ) - return CLYDeviceIDTypeIDFV; + if ([CountlyPersistency.sharedInstance retrieveIsCustomDeviceID]) + return CLYDeviceIDTypeCustom; + +#if (TARGET_OS_IOS || TARGET_OS_VISION || TARGET_OS_TV) + return CLYDeviceIDTypeIDFV; #else - return CLYDeviceIDTypeNSUUID; + return CLYDeviceIDTypeNSUUID; #endif } - (void)setID:(NSString *)deviceID; { - if (deviceID == nil || !deviceID.length) - { - CLY_LOG_W(@"%s Passing `nil` or empty string as devie ID is not allowed.", __FUNCTION__); - return; - } - - CLYDeviceIDType deviceIDType = [Countly.sharedInstance deviceIDType]; - if([deviceIDType isEqualToString:CLYDeviceIDTypeCustom]) - { - [Countly.sharedInstance setIDInternal:deviceID onServer: NO]; - } - else - { - [Countly.sharedInstance setIDInternal:deviceID onServer: YES]; - } + if (deviceID == nil || !deviceID.length) + { + CLY_LOG_W(@"%s Passing `nil` or empty string as devie ID is not allowed.", __FUNCTION__); + return; + } + + CLYDeviceIDType deviceIDType = [Countly.sharedInstance deviceIDType]; + if ([deviceIDType isEqualToString:CLYDeviceIDTypeCustom]) + { + [Countly.sharedInstance setIDInternal:deviceID onServer:NO]; + } + else + { + [Countly.sharedInstance setIDInternal:deviceID onServer:YES]; + } } -- (void)changeDeviceIDWithMerge:(NSString * _Nullable)deviceID { - CLY_LOG_I(@"%s", __FUNCTION__); - [self setIDInternal:deviceID onServer:YES]; +- (void)changeDeviceIDWithMerge:(NSString *_Nullable)deviceID +{ + CLY_LOG_I(@"%s", __FUNCTION__); + [self setIDInternal:deviceID onServer:YES]; } -- (void)changeDeviceIDWithoutMerge:(NSString * _Nullable)deviceID { - CLY_LOG_I(@"%s", __FUNCTION__); - [self setIDInternal:deviceID onServer:NO]; +- (void)changeDeviceIDWithoutMerge:(NSString *_Nullable)deviceID +{ + CLY_LOG_I(@"%s", __FUNCTION__); + [self setIDInternal:deviceID onServer:NO]; } - (void)enableTemporaryDeviceIDMode { - CLY_LOG_I(@"%s", __FUNCTION__); - [Countly.sharedInstance setIDInternal:CLYTemporaryDeviceID onServer:NO]; + CLY_LOG_I(@"%s", __FUNCTION__); + [Countly.sharedInstance setIDInternal:CLYTemporaryDeviceID onServer:NO]; } - (void)setNewDeviceID:(NSString *)deviceID onServer:(BOOL)onServer { - [Countly.sharedInstance setIDInternal:deviceID onServer:onServer]; + [Countly.sharedInstance setIDInternal:deviceID onServer:onServer]; } - (void)setIDInternal:(NSString *)deviceID onServer:(BOOL)onServer { - CLY_LOG_I(@"%s deviceID: [%@], onServer: [%d]", __FUNCTION__, deviceID, onServer); - if (!CountlyCommon.sharedInstance.hasStarted) - return; + CLY_LOG_I(@"%s deviceID: [%@], onServer: [%d]", __FUNCTION__, deviceID, onServer); + if (!CountlyCommon.sharedInstance.hasStarted) + return; - if (!deviceID.length) - { - CLY_LOG_W(@"%s Passing `CLYDefaultDeviceID` or `nil` or empty string as devie ID is deprecated, and will not be allowed in the future.", __FUNCTION__); - } - - [self storeCustomDeviceIDState:deviceID]; + if (!deviceID.length) + { + CLY_LOG_W(@"%s Passing `CLYDefaultDeviceID` or `nil` or empty string as devie ID is deprecated, and will not be allowed in the future.", __FUNCTION__); + } - deviceID = [CountlyDeviceInfo.sharedInstance ensafeDeviceID:deviceID]; + [self storeCustomDeviceIDState:deviceID]; - if ([deviceID isEqualToString:CountlyDeviceInfo.sharedInstance.deviceID]) - { - CLY_LOG_W(@"%s Attempted to set the same device ID again. So, setting new device ID is aborted.", __FUNCTION__); - return; - } + deviceID = [CountlyDeviceInfo.sharedInstance ensafeDeviceID:deviceID]; - if (CountlyDeviceInfo.sharedInstance.isDeviceIDTemporary) - { - CLY_LOG_I(@"%s Going out of CLYTemporaryDeviceID mode and switching back to normal mode.", __FUNCTION__); + if ([deviceID isEqualToString:CountlyDeviceInfo.sharedInstance.deviceID]) + { + CLY_LOG_W(@"%s Attempted to set the same device ID again. So, setting new device ID is aborted.", __FUNCTION__); + return; + } - [CountlyDeviceInfo.sharedInstance initializeDeviceID:deviceID]; - - [CountlyPersistency.sharedInstance replaceAllTemporaryDeviceIDsInQueueWithDeviceID:deviceID]; + if (CountlyDeviceInfo.sharedInstance.isDeviceIDTemporary) + { + CLY_LOG_I(@"%s Going out of CLYTemporaryDeviceID mode and switching back to normal mode.", __FUNCTION__); - [CountlyConnectionManager.sharedInstance proceedOnQueue]; + [CountlyDeviceInfo.sharedInstance initializeDeviceID:deviceID]; - [CountlyRemoteConfigInternal.sharedInstance downloadRemoteConfigAutomatically]; - - [CountlyHealthTracker.sharedInstance sendHealthCheck]; + [CountlyPersistency.sharedInstance replaceAllTemporaryDeviceIDsInQueueWithDeviceID:deviceID]; - return; - } + [CountlyConnectionManager.sharedInstance proceedOnQueue]; - if ([deviceID isEqualToString:CLYTemporaryDeviceID] && onServer) - { - CLY_LOG_W(@"%s Attempted to set device ID as CLYTemporaryDeviceID with onServer option. So, onServer value is overridden as NO.", __FUNCTION__); - onServer = NO; - } + [CountlyRemoteConfigInternal.sharedInstance downloadRemoteConfigAutomatically]; - if (onServer) - { - NSString* oldDeviceID = CountlyDeviceInfo.sharedInstance.deviceID; + [CountlyHealthTracker.sharedInstance sendHealthCheck]; - [CountlyDeviceInfo.sharedInstance initializeDeviceID:deviceID]; + return; + } - [CountlyConnectionManager.sharedInstance sendOldDeviceID:oldDeviceID]; - } - else - { - [self suspend]; + if ([deviceID isEqualToString:CLYTemporaryDeviceID] && onServer) + { + CLY_LOG_W(@"%s Attempted to set device ID as CLYTemporaryDeviceID with onServer option. So, onServer value is overridden as NO.", __FUNCTION__); + onServer = NO; + } - [CountlyDeviceInfo.sharedInstance initializeDeviceID:deviceID]; + if (onServer) + { + NSString *oldDeviceID = CountlyDeviceInfo.sharedInstance.deviceID; - [CountlyConsentManager.sharedInstance cancelConsentForAllFeaturesWithoutSendingConsentsRequest]; + [CountlyDeviceInfo.sharedInstance initializeDeviceID:deviceID]; - [self resume]; + [CountlyConnectionManager.sharedInstance sendOldDeviceID:oldDeviceID]; + } + else + { + [self suspend]; - [CountlyPersistency.sharedInstance clearAllTimedEvents]; - } + [CountlyDeviceInfo.sharedInstance initializeDeviceID:deviceID]; - - [CountlyRemoteConfigInternal.sharedInstance clearCachedRemoteConfig]; - - if (![deviceID isEqualToString:CLYTemporaryDeviceID] ) - { - [CountlyRemoteConfigInternal.sharedInstance downloadRemoteConfigAutomatically]; - } + [CountlyConsentManager.sharedInstance cancelConsentForAllFeaturesWithoutSendingConsentsRequest]; + + [self resume]; + + [CountlyPersistency.sharedInstance clearAllTimedEvents]; + } + + [CountlyRemoteConfigInternal.sharedInstance clearCachedRemoteConfig]; + + if (![deviceID isEqualToString:CLYTemporaryDeviceID]) + { + [CountlyRemoteConfigInternal.sharedInstance downloadRemoteConfigAutomatically]; + } } - (void)storeCustomDeviceIDState:(NSString *)deviceID { - BOOL isCustomDeviceID = deviceID.length && ![deviceID isEqualToString:CLYTemporaryDeviceID]; - [CountlyPersistency.sharedInstance storeIsCustomDeviceID:isCustomDeviceID]; + BOOL isCustomDeviceID = deviceID.length && ![deviceID isEqualToString:CLYTemporaryDeviceID]; + [CountlyPersistency.sharedInstance storeIsCustomDeviceID:isCustomDeviceID]; } #pragma mark - Consents - (void)giveConsentForFeature:(NSString *)featureName { - CLY_LOG_I(@"%s featureName: [%@]", __FUNCTION__, featureName); + CLY_LOG_I(@"%s featureName: [%@]", __FUNCTION__, featureName); - if (!featureName.length) - return; + if (!featureName.length) + return; - [CountlyConsentManager.sharedInstance giveConsentForFeatures:@[featureName]]; + [CountlyConsentManager.sharedInstance giveConsentForFeatures:@[ featureName ]]; } - (void)giveConsentForFeatures:(NSArray *)features { - CLY_LOG_I(@"%s features: [%@]", __FUNCTION__, features); - [CountlyConsentManager.sharedInstance giveConsentForFeatures:features]; + CLY_LOG_I(@"%s features: [%@]", __FUNCTION__, features); + [CountlyConsentManager.sharedInstance giveConsentForFeatures:features]; } - (void)giveConsentForAllFeatures { - CLY_LOG_I(@"%s", __FUNCTION__); - [CountlyConsentManager.sharedInstance giveAllConsents]; + CLY_LOG_I(@"%s", __FUNCTION__); + [CountlyConsentManager.sharedInstance giveAllConsents]; } - (void)giveAllConsents { - CLY_LOG_I(@"%s", __FUNCTION__); - [CountlyConsentManager.sharedInstance giveAllConsents]; + CLY_LOG_I(@"%s", __FUNCTION__); + [CountlyConsentManager.sharedInstance giveAllConsents]; } - (void)cancelConsentForFeature:(NSString *)featureName { - CLY_LOG_I(@"%s featureName: [%@]", __FUNCTION__, featureName); + CLY_LOG_I(@"%s featureName: [%@]", __FUNCTION__, featureName); - if (!featureName.length) - return; + if (!featureName.length) + return; - [CountlyConsentManager.sharedInstance cancelConsentForFeatures:@[featureName]]; + [CountlyConsentManager.sharedInstance cancelConsentForFeatures:@[ featureName ]]; } - (void)cancelConsentForFeatures:(NSArray *)features { - CLY_LOG_I(@"%s features: [%@]", __FUNCTION__, features); - [CountlyConsentManager.sharedInstance cancelConsentForFeatures:features]; + CLY_LOG_I(@"%s features: [%@]", __FUNCTION__, features); + [CountlyConsentManager.sharedInstance cancelConsentForFeatures:features]; } - (void)cancelConsentForAllFeatures { - CLY_LOG_I(@"%s", __FUNCTION__); - [CountlyConsentManager.sharedInstance cancelConsentForAllFeatures]; + CLY_LOG_I(@"%s", __FUNCTION__); + [CountlyConsentManager.sharedInstance cancelConsentForAllFeatures]; } - - #pragma mark - Events - (void)recordEvent:(NSString *)key { - [self recordEvent:key segmentation:nil count:1 sum:0 duration:0]; + [self recordEvent:key segmentation:nil count:1 sum:0 duration:0]; } - (void)recordEvent:(NSString *)key count:(NSUInteger)count { - [self recordEvent:key segmentation:nil count:count sum:0 duration:0]; + [self recordEvent:key segmentation:nil count:count sum:0 duration:0]; } - (void)recordEvent:(NSString *)key sum:(double)sum { - [self recordEvent:key segmentation:nil count:1 sum:sum duration:0]; + [self recordEvent:key segmentation:nil count:1 sum:sum duration:0]; } - (void)recordEvent:(NSString *)key duration:(NSTimeInterval)duration { - [self recordEvent:key segmentation:nil count:1 sum:0 duration:duration]; + [self recordEvent:key segmentation:nil count:1 sum:0 duration:duration]; } - (void)recordEvent:(NSString *)key count:(NSUInteger)count sum:(double)sum { - [self recordEvent:key segmentation:nil count:count sum:sum duration:0]; + [self recordEvent:key segmentation:nil count:count sum:sum duration:0]; } - (void)recordEvent:(NSString *)key segmentation:(NSDictionary *)segmentation { - [self recordEvent:key segmentation:segmentation count:1 sum:0 duration:0]; + [self recordEvent:key segmentation:segmentation count:1 sum:0 duration:0]; } - (void)recordEvent:(NSString *)key segmentation:(NSDictionary *)segmentation count:(NSUInteger)count { - [self recordEvent:key segmentation:segmentation count:count sum:0 duration:0]; + [self recordEvent:key segmentation:segmentation count:count sum:0 duration:0]; } - (void)recordEvent:(NSString *)key segmentation:(NSDictionary *)segmentation count:(NSUInteger)count sum:(double)sum { - [self recordEvent:key segmentation:segmentation count:count sum:sum duration:0]; + [self recordEvent:key segmentation:segmentation count:count sum:sum duration:0]; } - (void)recordEvent:(NSString *)key segmentation:(NSDictionary *)segmentation count:(NSUInteger)count sum:(double)sum duration:(NSTimeInterval)duration { - CLY_LOG_I(@"%s key: [%@], segmentation: [%@], count: [%lu], sum: [%f], duration: [%f]", __FUNCTION__, key, segmentation, (unsigned long)count, sum, duration); + CLY_LOG_I(@"%s key: [%@], segmentation: [%@], count: [%lu], sum: [%f], duration: [%f]", __FUNCTION__, key, segmentation, (unsigned long)count, sum, duration); - NSNumber* isReservedEvent = [self isReservedEvent:key]; + NSNumber *isReservedEvent = [self isReservedEvent:key]; - if (isReservedEvent) - { - CLY_LOG_V(@"%s, A reserved event detected: %@", __FUNCTION__, key); - if (!isReservedEvent.boolValue) - { - CLY_LOG_W(@"%s, No consent given for the reserved event! Event will not be recorded.", __FUNCTION__); - return; - } - CLY_LOG_V(@"%s, Specific consent given for the reserved event! So, it will be recorded.", __FUNCTION__); - } else if (!CountlyConsentManager.sharedInstance.consentForEvents) { - CLY_LOG_W(@"%s, Consent for events not given! Event will not be recorded.", __FUNCTION__); - return; - } - - if (!CountlyServerConfig.sharedInstance.customEventTrackingEnabled) + if (isReservedEvent) + { + CLY_LOG_V(@"%s, A reserved event detected: %@", __FUNCTION__, key); + if (!isReservedEvent.boolValue) { - CLY_LOG_D(@"%s, aborted: Custom Event Tracking is disabled from server config!", __FUNCTION__); - return; + CLY_LOG_W(@"%s, No consent given for the reserved event! Event will not be recorded.", __FUNCTION__); + return; } + CLY_LOG_V(@"%s, Specific consent given for the reserved event! So, it will be recorded.", __FUNCTION__); + } + else if (!CountlyConsentManager.sharedInstance.consentForEvents) + { + CLY_LOG_W(@"%s, Consent for events not given! Event will not be recorded.", __FUNCTION__); + return; + } - if (![CountlyServerConfig.sharedInstance shouldRecordEvent:key]) - { - CLY_LOG_D(@"%s, aborted: Event '%@' is filtered by server config event filter!", __FUNCTION__, key); - return; - } - - // Apply global segmentation filter (sb/sw) and event-specific segmentation filter (esb/esw) - NSDictionary* filtered = [CountlyServerConfig.sharedInstance filterSegmentation:segmentation eventKey:key]; - filtered = [filtered cly_truncated:@"Event segmentation"]; - segmentation = [filtered cly_limited:@"Event segmentation"]; + if (!CountlyServerConfig.sharedInstance.customEventTrackingEnabled) + { + CLY_LOG_D(@"%s, aborted: Custom Event Tracking is disabled from server config!", __FUNCTION__); + return; + } + + if (![CountlyServerConfig.sharedInstance shouldRecordEvent:key]) + { + CLY_LOG_D(@"%s, aborted: Event '%@' is filtered by server config event filter!", __FUNCTION__, key); + return; + } - [self recordEvent:key segmentation:segmentation count:count sum:sum duration:duration ID:nil timestamp:CountlyCommon.sharedInstance.uniqueTimestamp]; + // Apply global segmentation filter (sb/sw) and event-specific segmentation filter (esb/esw) + NSDictionary *filtered = [CountlyServerConfig.sharedInstance filterSegmentation:segmentation eventKey:key]; + filtered = [filtered cly_truncated:@"Event segmentation"]; + segmentation = [filtered cly_limited:@"Event segmentation"]; + + [self recordEvent:key segmentation:segmentation count:count sum:sum duration:duration ID:nil timestamp:CountlyCommon.sharedInstance.uniqueTimestamp]; } #pragma mark - - (void)recordReservedEvent:(NSString *)key segmentation:(NSDictionary *)segmentation { - [self recordEvent:key segmentation:segmentation count:1 sum:0 duration:0 ID:nil timestamp:CountlyCommon.sharedInstance.uniqueTimestamp]; + [self recordEvent:key segmentation:segmentation count:1 sum:0 duration:0 ID:nil timestamp:CountlyCommon.sharedInstance.uniqueTimestamp]; } - (void)recordReservedEvent:(NSString *)key segmentation:(NSDictionary *)segmentation ID:(NSString *)ID { - [self recordEvent:key segmentation:segmentation count:1 sum:0 duration:0 ID:ID timestamp:CountlyCommon.sharedInstance.uniqueTimestamp]; + [self recordEvent:key segmentation:segmentation count:1 sum:0 duration:0 ID:ID timestamp:CountlyCommon.sharedInstance.uniqueTimestamp]; } - (void)recordReservedEvent:(NSString *)key segmentation:(NSDictionary *)segmentation count:(NSUInteger)count sum:(double)sum duration:(NSTimeInterval)duration ID:(NSString *)ID timestamp:(NSTimeInterval)timestamp { - [self recordEvent:key segmentation:segmentation count:count sum:sum duration:duration ID:ID timestamp:timestamp]; + [self recordEvent:key segmentation:segmentation count:count sum:sum duration:duration ID:ID timestamp:timestamp]; } #pragma mark - - (void)recordEvent:(NSString *)key segmentation:(NSDictionary *)segmentation count:(NSUInteger)count sum:(double)sum duration:(NSTimeInterval)duration ID:(NSString *)ID timestamp:(NSTimeInterval)timestamp { - if (key.length == 0) { - CLY_LOG_D(@"%s omitting the call, key is empty", __FUNCTION__); - return; - } - - CountlyEvent *event = CountlyEvent.new; - event.ID = ID; - if (!event.ID.length) - { - event.ID = CountlyCommon.sharedInstance.randomEventID; - } - - if ([key isEqualToString:kCountlyReservedEventView]) + if (key.length == 0) + { + CLY_LOG_D(@"%s omitting the call, key is empty", __FUNCTION__); + return; + } + + CountlyEvent *event = CountlyEvent.new; + event.ID = ID; + if (!event.ID.length) + { + event.ID = CountlyCommon.sharedInstance.randomEventID; + } + + if ([key isEqualToString:kCountlyReservedEventView]) + { + event.PVID = CountlyViewTrackingInternal.sharedInstance.previousViewID ?: @""; + } + else + { + event.CVID = CountlyViewTrackingInternal.sharedInstance.currentViewID ?: @""; + } + + // Check if the event is a reserved event + BOOL isReservedEvent = [self isReservedEvent:key]; + + NSMutableDictionary *filteredSegmentations = segmentation.cly_filterSupportedDataTypes; + if (filteredSegmentations == nil) + filteredSegmentations = NSMutableDictionary.new; + + event.count = MAX(count, 1); + event.sum = sum; + event.timestamp = timestamp; + event.hourOfDay = CountlyCommon.sharedInstance.hourOfDay; + event.dayOfWeek = CountlyCommon.sharedInstance.dayOfWeek; + event.duration = duration; + + if (!isReservedEvent) + { + CLY_LOG_V(@"%s will add event id and name properties because it is not a reserved event ", __FUNCTION__); + key = [key cly_truncatedKey:@"Event key"]; + NSString *capturedPreviousID = nil; + NSString *capturedPreviousName = nil; +#if __has_include() + os_unfair_lock_lock(&previousEventLock); +#endif + capturedPreviousID = previousEventID; + previousEventID = event.ID; // update chain + if (CountlyViewTrackingInternal.sharedInstance.enablePreviousNameRecording) { - event.PVID = CountlyViewTrackingInternal.sharedInstance.previousViewID ?: @""; + capturedPreviousName = previousEventName; + previousEventName = key; } - else + event.PEID = capturedPreviousID ?: @""; + if (CountlyViewTrackingInternal.sharedInstance.enablePreviousNameRecording) { - event.CVID = CountlyViewTrackingInternal.sharedInstance.currentViewID ?: @""; + filteredSegmentations[kCountlyPreviousEventName] = capturedPreviousName ?: @""; + filteredSegmentations[kCountlyCurrentView] = CountlyViewTrackingInternal.sharedInstance.currentViewName ?: @""; } - - // Check if the event is a reserved event - BOOL isReservedEvent = [self isReservedEvent:key]; - - NSMutableDictionary *filteredSegmentations = segmentation.cly_filterSupportedDataTypes; - if(filteredSegmentations == nil) - filteredSegmentations = NSMutableDictionary.new; - - event.count = MAX(count, 1); - event.sum = sum; - event.timestamp = timestamp; - event.hourOfDay = CountlyCommon.sharedInstance.hourOfDay; - event.dayOfWeek = CountlyCommon.sharedInstance.dayOfWeek; - event.duration = duration; - - if (!isReservedEvent) + event.key = key; + event.segmentation = [self processSegmentation:filteredSegmentations eventKey:key]; + id callback = nil; + if ([CountlyServerConfig.sharedInstance isJourneyTriggerEvent:key]) { - CLY_LOG_V(@"%s will add event id and name properties because it is not a reserved event ", __FUNCTION__); - key = [key cly_truncatedKey:@"Event key"]; - NSString* capturedPreviousID = nil; - NSString* capturedPreviousName = nil; -#if __has_include() - os_unfair_lock_lock(&previousEventLock); + callback = ^(NSString *response, BOOL success) { + if (success) + { +#if (TARGET_OS_IOS) + dispatch_async(dispatch_get_main_queue(), ^{ + [CountlyContentBuilderInternal.sharedInstance refreshContentZoneJTE]; + }); #endif - capturedPreviousID = previousEventID; - previousEventID = event.ID; // update chain - if(CountlyViewTrackingInternal.sharedInstance.enablePreviousNameRecording) { - capturedPreviousName = previousEventName; - previousEventName = key; - } - event.PEID = capturedPreviousID ?: @""; - if(CountlyViewTrackingInternal.sharedInstance.enablePreviousNameRecording) { - filteredSegmentations[kCountlyPreviousEventName] = capturedPreviousName ?: @""; - filteredSegmentations[kCountlyCurrentView] = CountlyViewTrackingInternal.sharedInstance.currentViewName ?: @""; } - event.key = key; - event.segmentation = [self processSegmentation:filteredSegmentations eventKey:key]; - id callback = nil; - if ([CountlyServerConfig.sharedInstance isJourneyTriggerEvent:key]){ - callback = ^(NSString *response, BOOL success) { - if (success) - { - #if (TARGET_OS_IOS) - dispatch_async(dispatch_get_main_queue(), ^{ - [CountlyContentBuilderInternal.sharedInstance refreshContentZoneJTE]; - }); - #endif - } - }; - } - [CountlyPersistency.sharedInstance recordEvent:event callback:callback]; + }; + } + [CountlyPersistency.sharedInstance recordEvent:event callback:callback]; #if __has_include() - os_unfair_lock_unlock(&previousEventLock); + os_unfair_lock_unlock(&previousEventLock); #endif - } - else - { - event.key = key; - event.segmentation = [self processSegmentation:filteredSegmentations eventKey:key]; - [CountlyPersistency.sharedInstance recordEvent:event]; - } + } + else + { + event.key = key; + event.segmentation = [self processSegmentation:filteredSegmentations eventKey:key]; + [CountlyPersistency.sharedInstance recordEvent:event]; + } } -- (NSDictionary *)processSegmentation:(NSMutableDictionary *)segmentation eventKey:(NSString *)eventKey { - BOOL isViewEvent = [eventKey isEqualToString:kCountlyReservedEventView]; - - // Add previous view name if enabled and the event is a view event - if (isViewEvent && CountlyViewTrackingInternal.sharedInstance.enablePreviousNameRecording) { - segmentation[kCountlyPreviousView] = CountlyViewTrackingInternal.sharedInstance.previousViewName ?: @""; - } - - // Add visibility tracking information if enabled - if (CountlyCommon.sharedInstance.enableVisibiltyTracking) { - BOOL isViewStart = [segmentation[kCountlyVTKeyVisit] isEqual:@1]; - - // Add visibility if it's not a view event or it's a view start event - if (!isViewEvent || isViewStart) { - segmentation[kCountlyVisibility] = @([self isAppInForeground] ? 1 : 0); - } +- (NSDictionary *)processSegmentation:(NSMutableDictionary *)segmentation eventKey:(NSString *)eventKey +{ + BOOL isViewEvent = [eventKey isEqualToString:kCountlyReservedEventView]; + + // Add previous view name if enabled and the event is a view event + if (isViewEvent && CountlyViewTrackingInternal.sharedInstance.enablePreviousNameRecording) + { + segmentation[kCountlyPreviousView] = CountlyViewTrackingInternal.sharedInstance.previousViewName ?: @""; + } + + // Add visibility tracking information if enabled + if (CountlyCommon.sharedInstance.enableVisibiltyTracking) + { + BOOL isViewStart = [segmentation[kCountlyVTKeyVisit] isEqual:@1]; + + // Add visibility if it's not a view event or it's a view start event + if (!isViewEvent || isViewStart) + { + segmentation[kCountlyVisibility] = @([self isAppInForeground] ? 1 : 0); } - - // Return segmentation dictionary if not empty, otherwise return nil - return segmentation.count > 0 ? segmentation : nil; + } + + // Return segmentation dictionary if not empty, otherwise return nil + return segmentation.count > 0 ? segmentation : nil; } -- (BOOL)isAppInForeground { +- (BOOL)isAppInForeground +{ #if TARGET_OS_IOS || TARGET_OS_TV - UIApplicationState state = [UIApplication sharedApplication].applicationState; - return state == UIApplicationStateActive; + UIApplicationState state = [UIApplication sharedApplication].applicationState; + return state == UIApplicationStateActive; #elif TARGET_OS_OSX - NSApplication *app = [NSApplication sharedApplication]; - return app.isActive; + NSApplication *app = [NSApplication sharedApplication]; + return app.isActive; #elif TARGET_OS_WATCH - WKExtension *extension = [WKExtension sharedExtension]; - return extension.applicationState == WKApplicationStateActive; + WKExtension *extension = [WKExtension sharedExtension]; + return extension.applicationState == WKApplicationStateActive; #else - return NO; + return NO; #endif } /// This checks that given key requires any extra consent - (NSNumber *)isReservedEvent:(NSString *)key { - NSDictionary * reservedEvents = - @{ - kCountlyReservedEventOrientation: @(CountlyConsentManager.sharedInstance.consentForUserDetails), - kCountlyReservedEventStarRating: @(CountlyConsentManager.sharedInstance.consentForFeedback), - kCountlyReservedEventSurvey: @(CountlyConsentManager.sharedInstance.consentForFeedback), - kCountlyReservedEventNPS: @(CountlyConsentManager.sharedInstance.consentForFeedback), - kCountlyReservedEventPushAction: @(CountlyConsentManager.sharedInstance.consentForPushNotifications), - kCountlyReservedEventView: @(CountlyConsentManager.sharedInstance.consentForViewTracking), - }; + NSDictionary *reservedEvents = @{ + kCountlyReservedEventOrientation : @(CountlyConsentManager.sharedInstance.consentForUserDetails), + kCountlyReservedEventStarRating : @(CountlyConsentManager.sharedInstance.consentForFeedback), + kCountlyReservedEventSurvey : @(CountlyConsentManager.sharedInstance.consentForFeedback), + kCountlyReservedEventNPS : @(CountlyConsentManager.sharedInstance.consentForFeedback), + kCountlyReservedEventPushAction : @(CountlyConsentManager.sharedInstance.consentForPushNotifications), + kCountlyReservedEventView : @(CountlyConsentManager.sharedInstance.consentForViewTracking), + }; - NSNumber* aReservedEvent = reservedEvents[key]; - return aReservedEvent; + NSNumber *aReservedEvent = reservedEvents[key]; + return aReservedEvent; } #pragma mark - - (void)startEvent:(NSString *)key { - CLY_LOG_I(@"%s key: [%@]", __FUNCTION__, key); + CLY_LOG_I(@"%s key: [%@]", __FUNCTION__, key); - if (!CountlyConsentManager.sharedInstance.consentForEvents) - return; + if (!CountlyConsentManager.sharedInstance.consentForEvents) + return; - CountlyEvent *event = CountlyEvent.new; - event.key = key; - event.timestamp = CountlyCommon.sharedInstance.uniqueTimestamp; + CountlyEvent *event = CountlyEvent.new; + event.key = key; + event.timestamp = CountlyCommon.sharedInstance.uniqueTimestamp; - [CountlyPersistency.sharedInstance recordTimedEvent:event]; + [CountlyPersistency.sharedInstance recordTimedEvent:event]; } - (void)endEvent:(NSString *)key { - [self endEvent:key segmentation:nil count:1 sum:0]; + [self endEvent:key segmentation:nil count:1 sum:0]; } - (void)endEvent:(NSString *)key segmentation:(NSDictionary *)segmentation count:(NSUInteger)count sum:(double)sum { - CLY_LOG_I(@"%s key: [%@], segmentation: [%@], count: [%lu], sum: [%f]", __FUNCTION__, key, segmentation, (unsigned long)count, sum); + CLY_LOG_I(@"%s key: [%@], segmentation: [%@], count: [%lu], sum: [%f]", __FUNCTION__, key, segmentation, (unsigned long)count, sum); - if (!CountlyConsentManager.sharedInstance.consentForEvents) - return; + if (!CountlyConsentManager.sharedInstance.consentForEvents) + return; - CountlyEvent *event = [CountlyPersistency.sharedInstance timedEventForKey:key]; + CountlyEvent *event = [CountlyPersistency.sharedInstance timedEventForKey:key]; - if (!event) - { - CLY_LOG_W(@"%s Event with key '%@' not started yet or cancelled/ended before!", __FUNCTION__, key); - return; - } + if (!event) + { + CLY_LOG_W(@"%s Event with key '%@' not started yet or cancelled/ended before!", __FUNCTION__, key); + return; + } - NSTimeInterval duration = NSDate.date.timeIntervalSince1970 - event.timestamp; - [self recordEvent:key segmentation:segmentation count:count sum:sum duration:duration]; + NSTimeInterval duration = NSDate.date.timeIntervalSince1970 - event.timestamp; + [self recordEvent:key segmentation:segmentation count:count sum:sum duration:duration]; } - (void)cancelEvent:(NSString *)key { - CLY_LOG_I(@"%s key: [%@]", __FUNCTION__, key); + CLY_LOG_I(@"%s key: [%@]", __FUNCTION__, key); - if (!CountlyConsentManager.sharedInstance.consentForEvents) - return; + if (!CountlyConsentManager.sharedInstance.consentForEvents) + return; - CountlyEvent *event = [CountlyPersistency.sharedInstance timedEventForKey:key]; + CountlyEvent *event = [CountlyPersistency.sharedInstance timedEventForKey:key]; - if (!event) - { - CLY_LOG_W(@"%s Event with key '%@' not started yet or cancelled/ended before!", __FUNCTION__, key); - return; - } + if (!event) + { + CLY_LOG_W(@"%s Event with key '%@' not started yet or cancelled/ended before!", __FUNCTION__, key); + return; + } - CLY_LOG_D(@"%s Event with key '%@' cancelled!", __FUNCTION__, key); + CLY_LOG_D(@"%s Event with key '%@' cancelled!", __FUNCTION__, key); } - #pragma mark - Push Notifications -#if (TARGET_OS_IOS || TARGET_OS_VISION || TARGET_OS_OSX ) -#ifndef COUNTLY_EXCLUDE_PUSHNOTIFICATIONS +#if (TARGET_OS_IOS || TARGET_OS_VISION || TARGET_OS_OSX) + #ifndef COUNTLY_EXCLUDE_PUSHNOTIFICATIONS - (void)askForNotificationPermission { - CLY_LOG_I(@"%s", __FUNCTION__); - [CountlyPushNotifications.sharedInstance askForNotificationPermissionWithOptions:0 completionHandler:nil]; + CLY_LOG_I(@"%s", __FUNCTION__); + [CountlyPushNotifications.sharedInstance askForNotificationPermissionWithOptions:0 completionHandler:nil]; } -- (void)askForNotificationPermissionWithOptions:(UNAuthorizationOptions)options completionHandler:(void (^)(BOOL granted, NSError * error))completionHandler; +- (void)askForNotificationPermissionWithOptions:(UNAuthorizationOptions)options completionHandler:(void (^)(BOOL granted, NSError *error))completionHandler; { - CLY_LOG_I(@"%s options: [%lu], completionHandler: [%@]", __FUNCTION__, (unsigned long)options, completionHandler); - [CountlyPushNotifications.sharedInstance askForNotificationPermissionWithOptions:options completionHandler:completionHandler]; + CLY_LOG_I(@"%s options: [%lu], completionHandler: [%@]", __FUNCTION__, (unsigned long)options, completionHandler); + [CountlyPushNotifications.sharedInstance askForNotificationPermissionWithOptions:options completionHandler:completionHandler]; } - (void)recordActionForNotification:(NSDictionary *)userInfo clickedButtonIndex:(NSInteger)buttonIndex; { - CLY_LOG_I(@"%s userInfo: [%@], buttonIndex: [%ld]", __FUNCTION__, userInfo, (long)buttonIndex); - [CountlyPushNotifications.sharedInstance recordActionForNotification:userInfo clickedButtonIndex:buttonIndex]; + CLY_LOG_I(@"%s userInfo: [%@], buttonIndex: [%ld]", __FUNCTION__, userInfo, (long)buttonIndex); + [CountlyPushNotifications.sharedInstance recordActionForNotification:userInfo clickedButtonIndex:buttonIndex]; } - (void)recordPushNotificationToken { - CLY_LOG_I(@"%s", __FUNCTION__); - [CountlyPushNotifications.sharedInstance sendToken]; + CLY_LOG_I(@"%s", __FUNCTION__); + [CountlyPushNotifications.sharedInstance sendToken]; } - (void)clearPushNotificationToken { - CLY_LOG_I(@"%s", __FUNCTION__); - [CountlyPushNotifications.sharedInstance clearToken]; + CLY_LOG_I(@"%s", __FUNCTION__); + [CountlyPushNotifications.sharedInstance clearToken]; } + #endif #endif -#endif - - #pragma mark - Location -- (void)recordLocation:(CLLocationCoordinate2D)location city:(NSString * _Nullable)city ISOCountryCode:(NSString * _Nullable)ISOCountryCode IP:(NSString * _Nullable)IP +- (void)recordLocation:(CLLocationCoordinate2D)location city:(NSString *_Nullable)city ISOCountryCode:(NSString *_Nullable)ISOCountryCode IP:(NSString *_Nullable)IP { - CLY_LOG_I(@"%s lat: [%f], long: [%f], city: [%@], country: [%@], ip: [%@]", __FUNCTION__, location.latitude, location.longitude, city, ISOCountryCode, IP); - [CountlyLocationManager.sharedInstance recordLocation:location city:city ISOCountryCode:ISOCountryCode IP:IP]; + CLY_LOG_I(@"%s lat: [%f], long: [%f], city: [%@], country: [%@], ip: [%@]", __FUNCTION__, location.latitude, location.longitude, city, ISOCountryCode, IP); + [CountlyLocationManager.sharedInstance recordLocation:location city:city ISOCountryCode:ISOCountryCode IP:IP]; } - (void)disableLocationInfo { - CLY_LOG_I(@"%s", __FUNCTION__); - [CountlyLocationManager.sharedInstance disableLocationInfo]; + CLY_LOG_I(@"%s", __FUNCTION__); + [CountlyLocationManager.sharedInstance disableLocationInfo]; } - - #pragma mark - Crash Reporting - (void)recordException:(NSException *)exception { - CLY_LOG_I(@"%s exception: [%@]", __FUNCTION__, exception); - [CountlyCrashReporter.sharedInstance recordException:exception isFatal:NO stackTrace:nil segmentation:nil]; + CLY_LOG_I(@"%s exception: [%@]", __FUNCTION__, exception); + [CountlyCrashReporter.sharedInstance recordException:exception isFatal:NO stackTrace:nil segmentation:nil]; } - (void)recordException:(NSException *)exception isFatal:(BOOL)isFatal { - CLY_LOG_I(@"%s exception: [%@], isFatal: [%d]", __FUNCTION__, exception, isFatal); - [CountlyCrashReporter.sharedInstance recordException:exception isFatal:isFatal stackTrace:nil segmentation:nil]; + CLY_LOG_I(@"%s exception: [%@], isFatal: [%d]", __FUNCTION__, exception, isFatal); + [CountlyCrashReporter.sharedInstance recordException:exception isFatal:isFatal stackTrace:nil segmentation:nil]; } - (void)recordException:(NSException *)exception isFatal:(BOOL)isFatal stackTrace:(NSArray *)stackTrace segmentation:(NSDictionary *)segmentation { - CLY_LOG_I(@"%s exception: [%@], isFatal: [%d], stackTrace: [%@], segmentation: [%@]", __FUNCTION__, exception, isFatal, stackTrace, segmentation); - [CountlyCrashReporter.sharedInstance recordException:exception isFatal:isFatal stackTrace:stackTrace segmentation:segmentation]; + CLY_LOG_I(@"%s exception: [%@], isFatal: [%d], stackTrace: [%@], segmentation: [%@]", __FUNCTION__, exception, isFatal, stackTrace, segmentation); + [CountlyCrashReporter.sharedInstance recordException:exception isFatal:isFatal stackTrace:stackTrace segmentation:segmentation]; } -- (void)recordError:(NSString *)errorName stackTrace:(NSArray * _Nullable)stackTrace +- (void)recordError:(NSString *)errorName stackTrace:(NSArray *_Nullable)stackTrace { - CLY_LOG_I(@"%s errorName: [%@], stackTrace: [%@]", __FUNCTION__, errorName, stackTrace); - [CountlyCrashReporter.sharedInstance recordError:errorName isFatal:NO stackTrace:stackTrace segmentation:nil]; + CLY_LOG_I(@"%s errorName: [%@], stackTrace: [%@]", __FUNCTION__, errorName, stackTrace); + [CountlyCrashReporter.sharedInstance recordError:errorName isFatal:NO stackTrace:stackTrace segmentation:nil]; } -- (void)recordError:(NSString *)errorName isFatal:(BOOL)isFatal stackTrace:(NSArray * _Nullable)stackTrace segmentation:(NSDictionary *)segmentation +- (void)recordError:(NSString *)errorName isFatal:(BOOL)isFatal stackTrace:(NSArray *_Nullable)stackTrace segmentation:(NSDictionary *)segmentation { - CLY_LOG_I(@"%s errorName: [%@], isFatal: [%d], stackTrace: [%@], segmentation: [%@]", __FUNCTION__, errorName, isFatal, stackTrace, segmentation); - [CountlyCrashReporter.sharedInstance recordError:errorName isFatal:isFatal stackTrace:stackTrace segmentation:segmentation]; + CLY_LOG_I(@"%s errorName: [%@], isFatal: [%d], stackTrace: [%@], segmentation: [%@]", __FUNCTION__, errorName, isFatal, stackTrace, segmentation); + [CountlyCrashReporter.sharedInstance recordError:errorName isFatal:isFatal stackTrace:stackTrace segmentation:segmentation]; } - (void)recordHandledException:(NSException *)exception { - CLY_LOG_I(@"%s exception: [%@]", __FUNCTION__, exception); - [CountlyCrashReporter.sharedInstance recordException:exception isFatal:NO stackTrace:nil segmentation:nil]; + CLY_LOG_I(@"%s exception: [%@]", __FUNCTION__, exception); + [CountlyCrashReporter.sharedInstance recordException:exception isFatal:NO stackTrace:nil segmentation:nil]; } - (void)recordHandledException:(NSException *)exception withStackTrace:(NSArray *)stackTrace { - CLY_LOG_I(@"%s exception: [%@], stackTrace: [%@]", __FUNCTION__, exception, stackTrace); - [CountlyCrashReporter.sharedInstance recordException:exception isFatal:NO stackTrace:stackTrace segmentation:nil]; + CLY_LOG_I(@"%s exception: [%@], stackTrace: [%@]", __FUNCTION__, exception, stackTrace); + [CountlyCrashReporter.sharedInstance recordException:exception isFatal:NO stackTrace:stackTrace segmentation:nil]; } -- (void)recordUnhandledException:(NSException *)exception withStackTrace:(NSArray * _Nullable)stackTrace +- (void)recordUnhandledException:(NSException *)exception withStackTrace:(NSArray *_Nullable)stackTrace { - CLY_LOG_I(@"%s exception: [%@], stackTrace: [%@]", __FUNCTION__, exception, stackTrace); - [CountlyCrashReporter.sharedInstance recordException:exception isFatal:YES stackTrace:stackTrace segmentation:nil]; + CLY_LOG_I(@"%s exception: [%@], stackTrace: [%@]", __FUNCTION__, exception, stackTrace); + [CountlyCrashReporter.sharedInstance recordException:exception isFatal:YES stackTrace:stackTrace segmentation:nil]; } - (void)recordCrashLog:(NSString *)log { - CLY_LOG_I(@"%s log: [%@]", __FUNCTION__, log); - [CountlyCrashReporter.sharedInstance log:log]; + CLY_LOG_I(@"%s log: [%@]", __FUNCTION__, log); + [CountlyCrashReporter.sharedInstance log:log]; } - (void)clearCrashLogs { - CLY_LOG_I(@"%s", __FUNCTION__); - [CountlyCrashReporter.sharedInstance clearCrashLogs]; + CLY_LOG_I(@"%s", __FUNCTION__); + [CountlyCrashReporter.sharedInstance clearCrashLogs]; } - (void)crashLog:(NSString *)format, ... { - } #pragma mark - View Tracking - (void)recordView:(NSString *)viewName; { - CLY_LOG_I(@"%s viewName: [%@]", __FUNCTION__, viewName); - [CountlyViewTrackingInternal.sharedInstance startAutoStoppedView:viewName segmentation:nil]; + CLY_LOG_I(@"%s viewName: [%@]", __FUNCTION__, viewName); + [CountlyViewTrackingInternal.sharedInstance startAutoStoppedView:viewName segmentation:nil]; } - (void)recordView:(NSString *)viewName segmentation:(NSDictionary *)segmentation { - CLY_LOG_I(@"%s viewName: [%@], segmentation: [%@]", __FUNCTION__, viewName, segmentation); + CLY_LOG_I(@"%s viewName: [%@], segmentation: [%@]", __FUNCTION__, viewName, segmentation); - [CountlyViewTrackingInternal.sharedInstance startAutoStoppedView:viewName segmentation:segmentation]; + [CountlyViewTrackingInternal.sharedInstance startAutoStoppedView:viewName segmentation:segmentation]; } -#if (TARGET_OS_IOS || TARGET_OS_VISION || TARGET_OS_TV ) +#if (TARGET_OS_IOS || TARGET_OS_VISION || TARGET_OS_TV) - (void)addExceptionForAutoViewTracking:(NSString *)exception { - CLY_LOG_I(@"%s exception: [%@]", __FUNCTION__, exception); + CLY_LOG_I(@"%s exception: [%@]", __FUNCTION__, exception); - [CountlyViewTrackingInternal.sharedInstance addExceptionForAutoViewTracking:exception.copy]; + [CountlyViewTrackingInternal.sharedInstance addExceptionForAutoViewTracking:exception.copy]; } - (void)removeExceptionForAutoViewTracking:(NSString *)exception { - CLY_LOG_I(@"%s exception: [%@]", __FUNCTION__, exception); + CLY_LOG_I(@"%s exception: [%@]", __FUNCTION__, exception); - [CountlyViewTrackingInternal.sharedInstance removeExceptionForAutoViewTracking:exception.copy]; + [CountlyViewTrackingInternal.sharedInstance removeExceptionForAutoViewTracking:exception.copy]; } - (void)setIsAutoViewTrackingActive:(BOOL)isAutoViewTrackingActive { - CLY_LOG_I(@"%s isAutoViewTrackingActive: [%d]", __FUNCTION__, isAutoViewTrackingActive); + CLY_LOG_I(@"%s isAutoViewTrackingActive: [%d]", __FUNCTION__, isAutoViewTrackingActive); - CountlyViewTrackingInternal.sharedInstance.isAutoViewTrackingActive = isAutoViewTrackingActive; + CountlyViewTrackingInternal.sharedInstance.isAutoViewTrackingActive = isAutoViewTrackingActive; } - (BOOL)isAutoViewTrackingActive { - CLY_LOG_I(@"%s", __FUNCTION__); - return CountlyViewTrackingInternal.sharedInstance.isAutoViewTrackingActive; + CLY_LOG_I(@"%s", __FUNCTION__); + return CountlyViewTrackingInternal.sharedInstance.isAutoViewTrackingActive; } #endif #pragma mark - Star Rating #if (TARGET_OS_IOS) -- (void)askForStarRating:(void(^)(NSInteger rating))completion +- (void)askForStarRating:(void (^)(NSInteger rating))completion { - CLY_LOG_I(@"%s completion: [%@]", __FUNCTION__, completion); - [CountlyFeedbacksInternal.sharedInstance showDialog:completion]; + CLY_LOG_I(@"%s completion: [%@]", __FUNCTION__, completion); + [CountlyFeedbacksInternal.sharedInstance showDialog:completion]; } -- (void)presentFeedbackWidgetWithID:(NSString *)widgetID completionHandler:(void (^)(NSError * error))completionHandler +- (void)presentFeedbackWidgetWithID:(NSString *)widgetID completionHandler:(void (^)(NSError *error))completionHandler { - CLY_LOG_I(@"%s widgetID: [%@], completionHandler: [%@]", __FUNCTION__, widgetID, completionHandler); - - [self presentRatingWidgetWithID:widgetID closeButtonText:nil completionHandler:completionHandler]; + CLY_LOG_I(@"%s widgetID: [%@], completionHandler: [%@]", __FUNCTION__, widgetID, completionHandler); + + [self presentRatingWidgetWithID:widgetID closeButtonText:nil completionHandler:completionHandler]; } -- (void)presentRatingWidgetWithID:(NSString *)widgetID completionHandler:(void (^)(NSError * error))completionHandler +- (void)presentRatingWidgetWithID:(NSString *)widgetID completionHandler:(void (^)(NSError *error))completionHandler { - CLY_LOG_I(@"%s widgetID: [%@], completionHandler: [%@]", __FUNCTION__, widgetID, completionHandler); - [self presentRatingWidgetWithID:widgetID closeButtonText:nil completionHandler:completionHandler]; + CLY_LOG_I(@"%s widgetID: [%@], completionHandler: [%@]", __FUNCTION__, widgetID, completionHandler); + [self presentRatingWidgetWithID:widgetID closeButtonText:nil completionHandler:completionHandler]; } -- (void)presentRatingWidgetWithID:(NSString *)widgetID closeButtonText:(NSString * _Nullable)closeButtonText completionHandler:(void (^)(NSError * __nullable error))completionHandler +- (void)presentRatingWidgetWithID:(NSString *)widgetID closeButtonText:(NSString *_Nullable)closeButtonText completionHandler:(void (^)(NSError *__nullable error))completionHandler { - - CLY_LOG_I(@"%s widgetID: [%@], closeButtonText: [%@], completionHandler: [%@]", __FUNCTION__, widgetID, closeButtonText, completionHandler); - - [CountlyFeedbacksInternal.sharedInstance presentRatingWidgetWithID:widgetID closeButtonText:closeButtonText completionHandler:completionHandler]; + + CLY_LOG_I(@"%s widgetID: [%@], closeButtonText: [%@], completionHandler: [%@]", __FUNCTION__, widgetID, closeButtonText, completionHandler); + + [CountlyFeedbacksInternal.sharedInstance presentRatingWidgetWithID:widgetID closeButtonText:closeButtonText completionHandler:completionHandler]; } -- (void)recordRatingWidgetWithID:(NSString *)widgetID rating:(NSInteger)rating email:(NSString * _Nullable)email comment:(NSString * _Nullable)comment userCanBeContacted:(BOOL)userCanBeContacted +- (void)recordRatingWidgetWithID:(NSString *)widgetID rating:(NSInteger)rating email:(NSString *_Nullable)email comment:(NSString *_Nullable)comment userCanBeContacted:(BOOL)userCanBeContacted { - CLY_LOG_I(@"%s widgetID: [%@], rating: [%ld], email: [%@], comment: [%@], userCanBeContacted: [%d]", __FUNCTION__, widgetID, (long)rating, email, comment, userCanBeContacted); + CLY_LOG_I(@"%s widgetID: [%@], rating: [%ld], email: [%@], comment: [%@], userCanBeContacted: [%d]", __FUNCTION__, widgetID, (long)rating, email, comment, userCanBeContacted); - [CountlyFeedbacksInternal.sharedInstance recordRatingWidgetWithID:widgetID rating:rating email:email comment:comment userCanBeContacted:userCanBeContacted]; + [CountlyFeedbacksInternal.sharedInstance recordRatingWidgetWithID:widgetID rating:rating email:email comment:comment userCanBeContacted:userCanBeContacted]; } -- (void)getFeedbackWidgets:(void (^)(NSArray *feedbackWidgets, NSError * error))completionHandler +- (void)getFeedbackWidgets:(void (^)(NSArray *feedbackWidgets, NSError *error))completionHandler { - CLY_LOG_I(@"%s completionHandler: [%@]", __FUNCTION__, completionHandler); - [CountlyFeedbacksInternal.sharedInstance getFeedbackWidgets:completionHandler]; + CLY_LOG_I(@"%s completionHandler: [%@]", __FUNCTION__, completionHandler); + [CountlyFeedbacksInternal.sharedInstance getFeedbackWidgets:completionHandler]; } #endif #pragma mark - Attribution - (void)recordAttributionID:(NSString *)attributionID { - CLY_LOG_I(@"%s attributionID: [%@]", __FUNCTION__, attributionID); + CLY_LOG_I(@"%s attributionID: [%@]", __FUNCTION__, attributionID); - if (!CountlyConsentManager.sharedInstance.consentForAttribution) - return; + if (!CountlyConsentManager.sharedInstance.consentForAttribution) + return; - CountlyCommon.sharedInstance.attributionID = attributionID; + CountlyCommon.sharedInstance.attributionID = attributionID; - [CountlyConnectionManager.sharedInstance sendAttribution]; + [CountlyConnectionManager.sharedInstance sendAttribution]; } - (void)recordDirectAttributionWithCampaignType:(NSString *)campaignType andCampaignData:(NSString *)campaignData { - CLY_LOG_I(@"%s campaignType: [%@], campaignData: [%@]", __FUNCTION__, campaignType, campaignData); - - if (!CountlyConsentManager.sharedInstance.consentForAttribution) - return; - - if (!campaignType.length) - { - CLY_LOG_E(@"%s campaignType must be non-zero length valid string. Method execution will be aborted!", __FUNCTION__); - return; - } - - if (!campaignData.length) - { - CLY_LOG_E(@"%s campaignData must be non-zero length valid string. Method execution will be aborted!", __FUNCTION__); - return; - } - - if ([campaignType isEqualToString:@"_special_test"]) - { - [CountlyConnectionManager.sharedInstance sendAttributionData:campaignData]; - return; - } - - if (![campaignType isEqualToString:@"countly"]) - { - CLY_LOG_W(@"%s Recording direct attribution with a type other than 'countly' is currently not supported. Method execution will be aborted!", __FUNCTION__); - return; - } - - NSError* error = nil; - NSDictionary* campaignDataDictionary = [NSJSONSerialization JSONObjectWithData:[campaignData cly_dataUTF8] options:0 error:&error]; - if (error) - { - CLY_LOG_E(@"%s Campaign data is not in expected format. Method execution will be aborted!", __FUNCTION__); - return; - } - - NSString* campaignID = campaignDataDictionary[@"cid"]; - if (!campaignID.length) - { - CLY_LOG_E(@"%s Campaign ID must be non-zero length valid string. Method execution will be aborted!", __FUNCTION__); - return; - } - - NSString* campaignUserID = campaignDataDictionary[@"cuid"]; - if (!campaignUserID.length) - { - CLY_LOG_W(@"%s Campaign User ID must be non-zero length valid string. It will be ignored!", __FUNCTION__); - } - - [CountlyConnectionManager.sharedInstance sendDirectAttributionWithCampaignID:campaignID andCampaignUserID:campaignUserID]; + CLY_LOG_I(@"%s campaignType: [%@], campaignData: [%@]", __FUNCTION__, campaignType, campaignData); + + if (!CountlyConsentManager.sharedInstance.consentForAttribution) + return; + + if (!campaignType.length) + { + CLY_LOG_E(@"%s campaignType must be non-zero length valid string. Method execution will be aborted!", __FUNCTION__); + return; + } + + if (!campaignData.length) + { + CLY_LOG_E(@"%s campaignData must be non-zero length valid string. Method execution will be aborted!", __FUNCTION__); + return; + } + + if ([campaignType isEqualToString:@"_special_test"]) + { + [CountlyConnectionManager.sharedInstance sendAttributionData:campaignData]; + return; + } + + if (![campaignType isEqualToString:@"countly"]) + { + CLY_LOG_W(@"%s Recording direct attribution with a type other than 'countly' is currently not supported. Method execution will be aborted!", __FUNCTION__); + return; + } + + NSError *error = nil; + NSDictionary *campaignDataDictionary = [NSJSONSerialization JSONObjectWithData:[campaignData cly_dataUTF8] options:0 error:&error]; + if (error) + { + CLY_LOG_E(@"%s Campaign data is not in expected format. Method execution will be aborted!", __FUNCTION__); + return; + } + + NSString *campaignID = campaignDataDictionary[@"cid"]; + if (!campaignID.length) + { + CLY_LOG_E(@"%s Campaign ID must be non-zero length valid string. Method execution will be aborted!", __FUNCTION__); + return; + } + + NSString *campaignUserID = campaignDataDictionary[@"cuid"]; + if (!campaignUserID.length) + { + CLY_LOG_W(@"%s Campaign User ID must be non-zero length valid string. It will be ignored!", __FUNCTION__); + } + + [CountlyConnectionManager.sharedInstance sendDirectAttributionWithCampaignID:campaignID andCampaignUserID:campaignUserID]; } - (void)recordIndirectAttribution:(NSDictionary *)attribution { - CLY_LOG_I(@"%s attribution: [%@]", __FUNCTION__, attribution); + CLY_LOG_I(@"%s attribution: [%@]", __FUNCTION__, attribution); - if (!CountlyConsentManager.sharedInstance.consentForAttribution) - return; + if (!CountlyConsentManager.sharedInstance.consentForAttribution) + return; - NSMutableDictionary* filtered = attribution.mutableCopy; - [attribution enumerateKeysAndObjectsUsingBlock:^(NSString * key, NSString * value, BOOL * stop) - { - if (!value.length) - [filtered removeObjectForKey:key]; - }]; + NSMutableDictionary *filtered = attribution.mutableCopy; + [attribution enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSString *value, BOOL *stop) { + if (!value.length) + [filtered removeObjectForKey:key]; + }]; - NSDictionary* truncated = [filtered cly_truncated:@"Indirect attribution"]; - NSDictionary* limited = [truncated cly_limited:@"Indirect attribution"]; + NSDictionary *truncated = [filtered cly_truncated:@"Indirect attribution"]; + NSDictionary *limited = [truncated cly_limited:@"Indirect attribution"]; - [CountlyConnectionManager.sharedInstance sendIndirectAttribution:limited]; + [CountlyConnectionManager.sharedInstance sendIndirectAttribution:limited]; } #pragma mark - Remote Config - (id)remoteConfigValueForKey:(NSString *)key { - CLY_LOG_I(@"%s key: [%@]", __FUNCTION__, key); - return [CountlyRemoteConfigInternal.sharedInstance remoteConfigValueForKey:key]; + CLY_LOG_I(@"%s key: [%@]", __FUNCTION__, key); + return [CountlyRemoteConfigInternal.sharedInstance remoteConfigValueForKey:key]; } -- (void)updateRemoteConfigWithCompletionHandler:(void (^)(NSError * error))completionHandler +- (void)updateRemoteConfigWithCompletionHandler:(void (^)(NSError *error))completionHandler { - CLY_LOG_I(@"%s completionHandler: [%@]", __FUNCTION__, completionHandler); - [CountlyRemoteConfigInternal.sharedInstance updateRemoteConfigForKeys:nil omitKeys:nil completionHandler:completionHandler]; + CLY_LOG_I(@"%s completionHandler: [%@]", __FUNCTION__, completionHandler); + [CountlyRemoteConfigInternal.sharedInstance updateRemoteConfigForKeys:nil omitKeys:nil completionHandler:completionHandler]; } -- (void)updateRemoteConfigOnlyForKeys:(NSArray *)keys completionHandler:(void (^)(NSError * error))completionHandler +- (void)updateRemoteConfigOnlyForKeys:(NSArray *)keys completionHandler:(void (^)(NSError *error))completionHandler { - CLY_LOG_I(@"%s keys: [%@], completionHandler: [%@]", __FUNCTION__, keys, completionHandler); - [CountlyRemoteConfigInternal.sharedInstance updateRemoteConfigForKeys:keys omitKeys:nil completionHandler:completionHandler]; + CLY_LOG_I(@"%s keys: [%@], completionHandler: [%@]", __FUNCTION__, keys, completionHandler); + [CountlyRemoteConfigInternal.sharedInstance updateRemoteConfigForKeys:keys omitKeys:nil completionHandler:completionHandler]; } -- (void)updateRemoteConfigExceptForKeys:(NSArray *)omitKeys completionHandler:(void (^)(NSError * error))completionHandler +- (void)updateRemoteConfigExceptForKeys:(NSArray *)omitKeys completionHandler:(void (^)(NSError *error))completionHandler { - CLY_LOG_I(@"%s omitKeys: [%@], completionHandler: [%@]", __FUNCTION__, omitKeys, completionHandler); - [CountlyRemoteConfigInternal.sharedInstance updateRemoteConfigForKeys:nil omitKeys:omitKeys completionHandler:completionHandler]; + CLY_LOG_I(@"%s omitKeys: [%@], completionHandler: [%@]", __FUNCTION__, omitKeys, completionHandler); + [CountlyRemoteConfigInternal.sharedInstance updateRemoteConfigForKeys:nil omitKeys:omitKeys completionHandler:completionHandler]; } #pragma mark - Performance Monitoring - (void)recordNetworkTrace:(NSString *)traceName requestPayloadSize:(NSInteger)requestPayloadSize responsePayloadSize:(NSInteger)responsePayloadSize responseStatusCode:(NSInteger)responseStatusCode startTime:(long long)startTime endTime:(long long)endTime { - CLY_LOG_I(@"%s traceName: [%@], requestPayloadSize: [%ld], responsePayloadSize: [%ld], responseStatusCode: [%ld], startTime: [%lld], endTime: [%lld]", __FUNCTION__, traceName, (long)requestPayloadSize, (long)responsePayloadSize, (long)responseStatusCode, startTime, endTime); + CLY_LOG_I(@"%s traceName: [%@], requestPayloadSize: [%ld], responsePayloadSize: [%ld], responseStatusCode: [%ld], startTime: [%lld], endTime: [%lld]", __FUNCTION__, traceName, (long)requestPayloadSize, (long)responsePayloadSize, (long)responseStatusCode, startTime, endTime); - [CountlyPerformanceMonitoring.sharedInstance recordNetworkTrace:traceName requestPayloadSize:requestPayloadSize responsePayloadSize:responsePayloadSize responseStatusCode:responseStatusCode startTime:startTime endTime:endTime]; + [CountlyPerformanceMonitoring.sharedInstance recordNetworkTrace:traceName requestPayloadSize:requestPayloadSize responsePayloadSize:responsePayloadSize responseStatusCode:responseStatusCode startTime:startTime endTime:endTime]; } - (void)startCustomTrace:(NSString *)traceName { - CLY_LOG_I(@"%s traceName: [%@]", __FUNCTION__, traceName); - [CountlyPerformanceMonitoring.sharedInstance startCustomTrace:traceName]; + CLY_LOG_I(@"%s traceName: [%@]", __FUNCTION__, traceName); + [CountlyPerformanceMonitoring.sharedInstance startCustomTrace:traceName]; } -- (void)endCustomTrace:(NSString *)traceName metrics:(NSDictionary * _Nullable)metrics +- (void)endCustomTrace:(NSString *)traceName metrics:(NSDictionary *_Nullable)metrics { - CLY_LOG_I(@"%s traceName: [%@], metrics: [%@]", __FUNCTION__, traceName, metrics); - [CountlyPerformanceMonitoring.sharedInstance endCustomTrace:traceName metrics:metrics]; + CLY_LOG_I(@"%s traceName: [%@], metrics: [%@]", __FUNCTION__, traceName, metrics); + [CountlyPerformanceMonitoring.sharedInstance endCustomTrace:traceName metrics:metrics]; } - (void)cancelCustomTrace:(NSString *)traceName { - CLY_LOG_I(@"%s traceName: [%@]", __FUNCTION__, traceName); - [CountlyPerformanceMonitoring.sharedInstance cancelCustomTrace:traceName]; + CLY_LOG_I(@"%s traceName: [%@]", __FUNCTION__, traceName); + [CountlyPerformanceMonitoring.sharedInstance cancelCustomTrace:traceName]; } - (void)clearAllCustomTraces { - CLY_LOG_I(@"%s", __FUNCTION__); - [CountlyPerformanceMonitoring.sharedInstance clearAllCustomTraces]; + CLY_LOG_I(@"%s", __FUNCTION__); + [CountlyPerformanceMonitoring.sharedInstance clearAllCustomTraces]; } - (void)appLoadingFinished { - CLY_LOG_I(@"%s", __FUNCTION__); + CLY_LOG_I(@"%s", __FUNCTION__); - long long appLoadEndTime = floor(NSDate.date.timeIntervalSince1970 * 1000); + long long appLoadEndTime = floor(NSDate.date.timeIntervalSince1970 * 1000); - [CountlyPerformanceMonitoring.sharedInstance recordAppStartDurationTraceWithStartTime:appLoadStartTime endTime:appLoadEndTime]; + [CountlyPerformanceMonitoring.sharedInstance recordAppStartDurationTraceWithStartTime:appLoadStartTime endTime:appLoadEndTime]; } - (void)halt { - CLY_LOG_I(@"%s", __FUNCTION__); - [self halt:true]; + CLY_LOG_I(@"%s", __FUNCTION__); + [self halt:true]; } -- (void)halt:(BOOL) clearStorage +- (void)halt:(BOOL)clearStorage { - CLY_LOG_I(@"%s clearStorage: [%d]", __FUNCTION__, clearStorage); + CLY_LOG_I(@"%s clearStorage: [%d]", __FUNCTION__, clearStorage); - // Reset view tracking state BEFORE halt — sharedInstance() returns nil after halt. - // Use KVC to clear internal state directly since stopAllViews checks consent - // and may be a no-op if previous test required consent. - if (CountlyViewTrackingInternal.sharedInstance) - { - CountlyViewTrackingInternal* viewTracking = CountlyViewTrackingInternal.sharedInstance; - [viewTracking setValue:NSMutableDictionary.new forKey:@"viewDataDictionary"]; - [viewTracking setValue:nil forKey:@"currentViewID"]; - [viewTracking setValue:nil forKey:@"currentViewName"]; - [viewTracking setValue:nil forKey:@"previousViewID"]; - [viewTracking setValue:nil forKey:@"previousViewName"]; - [viewTracking setValue:@NO forKey:@"isAutoViewTrackingActive"]; - [viewTracking resetFirstView]; - } + // Reset view tracking state BEFORE halt — sharedInstance() returns nil after halt. + // Use KVC to clear internal state directly since stopAllViews checks consent + // and may be a no-op if previous test required consent. + if (CountlyViewTrackingInternal.sharedInstance) + { + CountlyViewTrackingInternal *viewTracking = CountlyViewTrackingInternal.sharedInstance; + [viewTracking setValue:NSMutableDictionary.new forKey:@"viewDataDictionary"]; + [viewTracking setValue:nil forKey:@"currentViewID"]; + [viewTracking setValue:nil forKey:@"currentViewName"]; + [viewTracking setValue:nil forKey:@"previousViewID"]; + [viewTracking setValue:nil forKey:@"previousViewName"]; + [viewTracking setValue:@NO forKey:@"isAutoViewTrackingActive"]; + [viewTracking resetFirstView]; + } - // Reset health tracker state - [CountlyHealthTracker.sharedInstance resetInstance]; + // Reset health tracker state + [CountlyHealthTracker.sharedInstance resetInstance]; - // Clear crash logs (safe operation - just clears array or deletes file) - if (CountlyCrashReporter.sharedInstance) - { - [CountlyCrashReporter.sharedInstance clearCrashLogs]; - } + // Clear crash logs (safe operation - just clears array or deletes file) + if (CountlyCrashReporter.sharedInstance) + { + [CountlyCrashReporter.sharedInstance clearCrashLogs]; + } - // Clear custom performance monitoring traces (safe operation - just clears dictionary) - if (CountlyPerformanceMonitoring.sharedInstance) + // Clear custom performance monitoring traces (safe operation - just clears dictionary) + if (CountlyPerformanceMonitoring.sharedInstance) + { + [CountlyPerformanceMonitoring.sharedInstance clearAllCustomTraces]; + } + + // Note: CountlyRemoteConfigInternal.clearAll is not called here because it triggers + // storeRemoteConfig which involves file I/O and can block during shutdown. + // Remote config state will persist across halt/start but is cleared via UserDefaults + // key removal below. + + [CountlyConsentManager.sharedInstance resetInstance]; + [CountlyPersistency.sharedInstance resetInstance:clearStorage]; + [CountlyDeviceInfo.sharedInstance resetInstance]; + [CountlyConnectionManager.sharedInstance resetInstance]; + [CountlyServerConfig.sharedInstance resetInstance]; + [CountlyUserDetails.sharedInstance clearUserDetails]; + [self resetInstance]; + [CountlyCommon.sharedInstance resetInstance]; + + if (clearStorage) + { + NSString *appDomain = [[NSBundle mainBundle] bundleIdentifier]; + [NSUserDefaults.standardUserDefaults removePersistentDomainForName:appDomain]; + + // halt(true) calls removePersistentDomainForName which doesn't work in xctest + // environment (different bundle ID), so clear SDK keys manually + NSArray *sdkKeys = @[ + @"kCountlyServerConfigPersistencyKey", @"kCountlyHealthCheckStatePersistencyKey", @"kCountlyQueuedRequestsPersistencyKey", @"kCountlyStartedEventsPersistencyKey", @"kCountlyStoredDeviceIDKey", @"kCountlyStoredNSUUIDKey", @"kCountlyStarRatingStatusKey", @"kCountlyRemoteConfigKey", + @"kCountlyIsCustomDeviceIDKey", @"kCountlyNotificationPermissionKey", @"kCountlyWatchParentDeviceIDKey" + ]; + for (NSString *key in sdkKeys) { - [CountlyPerformanceMonitoring.sharedInstance clearAllCustomTraces]; + [NSUserDefaults.standardUserDefaults removeObjectForKey:key]; } - // Note: CountlyRemoteConfigInternal.clearAll is not called here because it triggers - // storeRemoteConfig which involves file I/O and can block during shutdown. - // Remote config state will persist across halt/start but is cleared via UserDefaults - // key removal below. - - [CountlyConsentManager.sharedInstance resetInstance]; - [CountlyPersistency.sharedInstance resetInstance:clearStorage]; - [CountlyDeviceInfo.sharedInstance resetInstance]; - [CountlyConnectionManager.sharedInstance resetInstance]; - [CountlyServerConfig.sharedInstance resetInstance]; - [CountlyUserDetails.sharedInstance clearUserDetails]; - [self resetInstance]; - [CountlyCommon.sharedInstance resetInstance]; - - if(clearStorage) - { - NSString *appDomain = [[NSBundle mainBundle] bundleIdentifier]; - [NSUserDefaults.standardUserDefaults removePersistentDomainForName:appDomain]; - - // halt(true) calls removePersistentDomainForName which doesn't work in xctest - // environment (different bundle ID), so clear SDK keys manually - NSArray* sdkKeys = @[ - @"kCountlyServerConfigPersistencyKey", - @"kCountlyHealthCheckStatePersistencyKey", - @"kCountlyQueuedRequestsPersistencyKey", - @"kCountlyStartedEventsPersistencyKey", - @"kCountlyStoredDeviceIDKey", - @"kCountlyStoredNSUUIDKey", - @"kCountlyStarRatingStatusKey", - @"kCountlyRemoteConfigKey", - @"kCountlyIsCustomDeviceIDKey", - @"kCountlyNotificationPermissionKey", - @"kCountlyWatchParentDeviceIDKey" - ]; - for (NSString* key in sdkKeys) - { - [NSUserDefaults.standardUserDefaults removeObjectForKey:key]; - } - - // Single synchronize after all UserDefaults modifications - [NSUserDefaults.standardUserDefaults synchronize]; - } + // Single synchronize after all UserDefaults modifications + [NSUserDefaults.standardUserDefaults synchronize]; + } } - (void)attemptToSendStoredRequests { - CLY_LOG_I(@"%s", __FUNCTION__); - [CountlyConnectionManager.sharedInstance attemptToSendStoredRequests]; + CLY_LOG_I(@"%s", __FUNCTION__); + [CountlyConnectionManager.sharedInstance attemptToSendStoredRequests]; } #pragma mark - Interfaces #if (TARGET_OS_IOS) -- (CountlyContentBuilder *) content +- (CountlyContentBuilder *)content { - return CountlyContentBuilder.sharedInstance; + return CountlyContentBuilder.sharedInstance; } -- (CountlyFeedbacks *) feedback +- (CountlyFeedbacks *)feedback { - return CountlyFeedbacks.sharedInstance; + return CountlyFeedbacks.sharedInstance; } #endif -- (CountlyViewTracking *) views +- (CountlyViewTracking *)views { - return CountlyViewTracking.sharedInstance; + return CountlyViewTracking.sharedInstance; } + (CountlyUserDetails *)user { - return CountlyUserDetails.sharedInstance; + return CountlyUserDetails.sharedInstance; } -- (CountlyRemoteConfig *) remoteConfig { - return CountlyRemoteConfig.sharedInstance; +- (CountlyRemoteConfig *)remoteConfig +{ + return CountlyRemoteConfig.sharedInstance; } @end diff --git a/CountlyCommon.h b/CountlyCommon.h index 42b4e1ad..3026caa7 100644 --- a/CountlyCommon.h +++ b/CountlyCommon.h @@ -4,34 +4,34 @@ // // Please visit www.count.ly for more information. -#import #import "Countly.h" -#import "CountlyServerConfig.h" -#import "CountlyPersistency.h" +#import "CountlyConfig.h" #import "CountlyConnectionManager.h" -#import "CountlyEvent.h" -#import "CountlyUserDetails.h" -#import "CountlyDeviceInfo.h" +#import "CountlyConsentManager.h" +#import "CountlyContentBuilderInternal.h" +#import "CountlyCrashData.h" #import "CountlyCrashReporter.h" -#import "CountlyConfig.h" -#import "CountlyViewTrackingInternal.h" -#import "CountlyFeedbacksInternal.h" +#import "CountlyDeviceInfo.h" +#import "CountlyEvent.h" +#import "CountlyExperimentalConfig.h" #import "CountlyFeedbackWidget.h" -#import "CountlyPushNotifications.h" -#import "CountlyNotificationService.h" -#import "CountlyConsentManager.h" +#import "CountlyFeedbacksInternal.h" +#import "CountlyHealthTracker.h" #import "CountlyLocationManager.h" -#import "CountlyRemoteConfigInternal.h" +#import "CountlyNotificationService.h" #import "CountlyPerformanceMonitoring.h" +#import "CountlyPersistency.h" +#import "CountlyPushNotifications.h" #import "CountlyRCData.h" -#import "CountlyViewData.h" #import "CountlyRemoteConfig.h" +#import "CountlyRemoteConfigInternal.h" +#import "CountlyServerConfig.h" +#import "CountlyUserDetails.h" +#import "CountlyViewData.h" #import "CountlyViewTracking.h" +#import "CountlyViewTrackingInternal.h" #import "Resettable.h" -#import "CountlyCrashData.h" -#import "CountlyContentBuilderInternal.h" -#import "CountlyExperimentalConfig.h" -#import "CountlyHealthTracker.h" +#import #define CLY_LOG_E(fmt, ...) CountlyInternalLog(CLYInternalLogLevelError, fmt, ##__VA_ARGS__) #define CLY_LOG_W(fmt, ...) CountlyInternalLog(CLYInternalLogLevelWarning, fmt, ##__VA_ARGS__) @@ -40,62 +40,57 @@ #define CLY_LOG_V(fmt, ...) CountlyInternalLog(CLYInternalLogLevelVerbose, fmt, ##__VA_ARGS__) #if (TARGET_OS_IOS) -#import -#import + #import + #import #endif #if (TARGET_OS_WATCH) -#import -#import "WatchConnectivity/WatchConnectivity.h" + #import "WatchConnectivity/WatchConnectivity.h" + #import #endif #if (TARGET_OS_TV) -#import + #import #endif #import NS_ASSUME_NONNULL_BEGIN -extern NSString* const kCountlyErrorDomain; +extern NSString *const kCountlyErrorDomain; -extern NSString* const kCountlyReservedEventOrientation; -extern NSString* const kCountlyVisibility; +extern NSString *const kCountlyReservedEventOrientation; +extern NSString *const kCountlyVisibility; -NS_ERROR_ENUM(kCountlyErrorDomain) -{ - CLYErrorFeedbackWidgetNotAvailable = 10001, - CLYErrorFeedbackWidgetNotTargetedForDevice = 10002, - CLYErrorRemoteConfigGeneralAPIError = 10011, - CLYErrorFeedbacksGeneralAPIError = 10012, - CLYErrorServerConfigGeneralAPIError = 10013, +NS_ERROR_ENUM(kCountlyErrorDomain){ + CLYErrorFeedbackWidgetNotAvailable = 10001, CLYErrorFeedbackWidgetNotTargetedForDevice = 10002, CLYErrorRemoteConfigGeneralAPIError = 10011, CLYErrorFeedbacksGeneralAPIError = 10012, CLYErrorServerConfigGeneralAPIError = 10013, }; -extern NSString* const kCountlySDKVersion; -extern NSString* const kCountlySDKName; +extern NSString *const kCountlySDKVersion; +extern NSString *const kCountlySDKName; @interface CountlyCommon : NSObject -@property (nonatomic, copy) NSString* SDKVersion; -@property (nonatomic, copy) NSString* SDKName; +@property(nonatomic, copy) NSString *SDKVersion; +@property(nonatomic, copy) NSString *SDKName; -@property (nonatomic) BOOL hasStarted; -@property (nonatomic) BOOL enableDebug; +@property(nonatomic) BOOL hasStarted; +@property(nonatomic) BOOL hasFinishedInit; +@property(nonatomic) BOOL enableDebug; -@property (nonatomic) BOOL shouldIgnoreTrustCheck; -@property (nonatomic, weak) id loggerDelegate; -@property (nonatomic) CLYInternalLogLevel internalLogLevel; -@property (nonatomic, copy) NSString* attributionID; -@property (nonatomic) BOOL manualSessionHandling; -@property (nonatomic) BOOL enableManualSessionControlHybridMode; -@property (nonatomic) BOOL enableOrientationTracking; +@property(nonatomic) BOOL shouldIgnoreTrustCheck; +@property(nonatomic, weak) id loggerDelegate; +@property(nonatomic) CLYInternalLogLevel internalLogLevel; +@property(nonatomic, copy) NSString *attributionID; +@property(nonatomic) BOOL manualSessionHandling; +@property(nonatomic) BOOL enableManualSessionControlHybridMode; +@property(nonatomic) BOOL enableOrientationTracking; -@property (nonatomic) BOOL enableVisibiltyTracking; +@property(nonatomic) BOOL enableVisibiltyTracking; - -@property (nonatomic) NSUInteger maxKeyLength; -@property (nonatomic) NSUInteger maxValueLength; -@property (nonatomic) NSUInteger maxSegmentationValues; +@property(nonatomic) NSUInteger maxKeyLength; +@property(nonatomic) NSUInteger maxValueLength; +@property(nonatomic) NSUInteger maxSegmentationValues; void CountlyInternalLog(CLYInternalLogLevel level, NSString *format, ...) NS_FORMAT_FUNCTION(2, 3); void CountlyPrint(NSString *stringToPrint); @@ -111,10 +106,10 @@ void CountlyPrint(NSString *stringToPrint); - (void)startBackgroundTask; - (void)finishBackgroundTask; -#if (TARGET_OS_IOS || TARGET_OS_VISION || TARGET_OS_TV ) +#if (TARGET_OS_IOS || TARGET_OS_VISION || TARGET_OS_TV) - (UIViewController *)topViewController; - (void)tryPresentingViewController:(UIViewController *)viewController; -- (void)tryPresentingViewController:(UIViewController *)viewController withCompletion:(void (^ __nullable) (void))completion; +- (void)tryPresentingViewController:(UIViewController *)viewController withCompletion:(void (^__nullable)(void))completion; #endif - (void)observeDeviceOrientationChanges; @@ -128,23 +123,22 @@ void CountlyPrint(NSString *stringToPrint); - (CGSize)getWindowSize; @end - #if (TARGET_OS_IOS) -@interface CLYInternalViewController : UIViewController -@property (nonatomic, weak) WKWebView* webView; +@interface CLYInternalViewController : UIViewController +@property(nonatomic, weak) WKWebView *webView; @end @interface CLYButton : UIButton -@property (nonatomic, copy) void (^onClick)(id sender); +@property(nonatomic, copy) void (^onClick)(id sender); + (CLYButton *)dismissAlertButton; -+ (CLYButton *)dismissAlertButton:(NSString * _Nullable)closeButtonText; ++ (CLYButton *)dismissAlertButton:(NSString *_Nullable)closeButtonText; - (void)positionToTopRight; - (void)positionToTopRightConsideringStatusBar; @end #endif -@interface CLYDelegateInterceptor : NSObject -@property (nonatomic, weak) id originalDelegate; +@interface CLYDelegateInterceptor : NSObject +@property(nonatomic, weak) id originalDelegate; @end @interface NSString (Countly) diff --git a/CountlyCommon.m b/CountlyCommon.m index 280d26fd..bd5f9636 100644 --- a/CountlyCommon.m +++ b/CountlyCommon.m @@ -7,186 +7,185 @@ #import "CountlyCommon.h" #include -NSString* const kCountlyReservedEventOrientation = @"[CLY]_orientation"; -NSString* const kCountlyOrientationKeyMode = @"mode"; +NSString *const kCountlyReservedEventOrientation = @"[CLY]_orientation"; +NSString *const kCountlyOrientationKeyMode = @"mode"; -NSString* const kCountlyVisibility = @"cly_v"; +NSString *const kCountlyVisibility = @"cly_v"; - -@interface CountlyCommon () -{ - NSCalendar* gregorianCalendar; - NSTimeInterval startTime; +@interface CountlyCommon () { + NSCalendar *gregorianCalendar; + NSTimeInterval startTime; } @property long long lastTimestamp; -#if (TARGET_OS_IOS || TARGET_OS_VISION ) -@property (nonatomic) NSString* lastInterfaceOrientation; +#if (TARGET_OS_IOS || TARGET_OS_VISION) +@property(nonatomic) NSString *lastInterfaceOrientation; #endif -#if (TARGET_OS_IOS || TARGET_OS_VISION || TARGET_OS_TV ) -@property (nonatomic) UIBackgroundTaskIdentifier bgTask; +#if (TARGET_OS_IOS || TARGET_OS_VISION || TARGET_OS_TV) +@property(nonatomic) UIBackgroundTaskIdentifier bgTask; #endif @end -NSString* const kCountlySDKVersion = @"26.1.1"; -NSString* const kCountlySDKName = @"objc-native-ios"; - -NSString* const kCountlyErrorDomain = @"ly.count.ErrorDomain"; +NSString *const kCountlySDKVersion = @"26.1.1"; +NSString *const kCountlySDKName = @"objc-native-ios"; -NSString* const kCountlyInternalLogPrefix = @"[Countly] "; +NSString *const kCountlyErrorDomain = @"ly.count.ErrorDomain"; +NSString *const kCountlyInternalLogPrefix = @"[Countly] "; @implementation CountlyCommon @synthesize lastTimestamp; -static CountlyCommon *s_sharedInstance = nil; +static CountlyCommon *s_sharedInstance = nil; static dispatch_once_t onceToken; + (instancetype)sharedInstance { - dispatch_once(&onceToken, ^{s_sharedInstance = self.new;}); - return s_sharedInstance; + dispatch_once(&onceToken, ^{ + s_sharedInstance = self.new; + }); + return s_sharedInstance; } - (instancetype)init { - if (self = [super init]) - { - gregorianCalendar = [NSCalendar.alloc initWithCalendarIdentifier:NSCalendarIdentifierGregorian]; - startTime = NSDate.date.timeIntervalSince1970; - - self.lastTimestamp = 0; - self.SDKVersion = kCountlySDKVersion; - self.SDKName = kCountlySDKName; - } + if (self = [super init]) + { + gregorianCalendar = [NSCalendar.alloc initWithCalendarIdentifier:NSCalendarIdentifierGregorian]; + startTime = NSDate.date.timeIntervalSince1970; - return self; + self.lastTimestamp = 0; + self.SDKVersion = kCountlySDKVersion; + self.SDKName = kCountlySDKName; + } + + return self; } -- (void)resetInstance { - CLY_LOG_I(@"%s", __FUNCTION__); -#if (TARGET_OS_IOS || TARGET_OS_VISION ) - [NSNotificationCenter.defaultCenter removeObserver:self name:UIDeviceOrientationDidChangeNotification object:nil]; +- (void)resetInstance +{ + CLY_LOG_I(@"%s", __FUNCTION__); +#if (TARGET_OS_IOS || TARGET_OS_VISION) + [NSNotificationCenter.defaultCenter removeObserver:self name:UIDeviceOrientationDidChangeNotification object:nil]; #endif - _hasStarted = false; - _maxKeyLength = kCountlyMaxKeyLength; - _maxValueLength = kCountlyMaxValueSize; - _maxSegmentationValues = kCountlyMaxSegmentationValues; - onceToken = 0; - s_sharedInstance = nil; - } - + _hasStarted = false; + _hasFinishedInit = false; + _maxKeyLength = kCountlyMaxKeyLength; + _maxValueLength = kCountlyMaxValueSize; + _maxSegmentationValues = kCountlyMaxSegmentationValues; + onceToken = 0; + s_sharedInstance = nil; +} - (BOOL)hasStarted { - if (!_hasStarted) - CountlyPrint(@"SDK should be started first!"); + if (!_hasStarted) + CountlyPrint(@"SDK should be started first!"); - return _hasStarted; + return _hasStarted; } -//NOTE: This is an equivalent of hasStarted, but without internal logging. +// NOTE: This is an equivalent of hasStarted, but without internal logging. - (BOOL)hasStarted_ { - return _hasStarted; + return _hasStarted; } void CountlyInternalLog(CLYInternalLogLevel level, NSString *format, ...) { - if (level == CLYInternalLogLevelError) { - [CountlyHealthTracker.sharedInstance logError]; - } else if(level == CLYInternalLogLevelWarning) { - [CountlyHealthTracker.sharedInstance logWarning]; - } - - if (!CountlyCommon.sharedInstance.enableDebug && !CountlyCommon.sharedInstance.loggerDelegate) - return; + if (level == CLYInternalLogLevelError) + { + [CountlyHealthTracker.sharedInstance logError]; + } + else if (level == CLYInternalLogLevelWarning) + { + [CountlyHealthTracker.sharedInstance logWarning]; + } + + if (!CountlyCommon.sharedInstance.enableDebug && !CountlyCommon.sharedInstance.loggerDelegate) + return; - if (level > CountlyCommon.sharedInstance.internalLogLevel) - return; + if (level > CountlyCommon.sharedInstance.internalLogLevel) + return; - va_list args; - va_start(args, format); + va_list args; + va_start(args, format); - NSString* logString = [NSString.alloc initWithFormat:format arguments:args]; + NSString *logString = [NSString.alloc initWithFormat:format arguments:args]; - NSArray *logLevelPrefixes = - @[ - @"None", - @"Error", - @"Warning", - @"Info", - @"Debug", - @"Verbose", - ]; + NSArray *logLevelPrefixes = @[ + @"None", + @"Error", + @"Warning", + @"Info", + @"Debug", + @"Verbose", + ]; - logString = [NSString stringWithFormat:@"[%@] %@", logLevelPrefixes[level], logString]; + logString = [NSString stringWithFormat:@"[%@] %@", logLevelPrefixes[level], logString]; #if DEBUG - if (CountlyCommon.sharedInstance.enableDebug) - CountlyPrint(logString); + if (CountlyCommon.sharedInstance.enableDebug) + CountlyPrint(logString); #endif - if ([CountlyCommon.sharedInstance.loggerDelegate respondsToSelector:@selector(internalLog:withLevel:)]) - { - NSString* logStringWithPrefix = [NSString stringWithFormat:@"%@%@", kCountlyInternalLogPrefix, logString]; - [CountlyCommon.sharedInstance.loggerDelegate internalLog:logStringWithPrefix withLevel:level]; - } + if ([CountlyCommon.sharedInstance.loggerDelegate respondsToSelector:@selector(internalLog:withLevel:)]) + { + NSString *logStringWithPrefix = [NSString stringWithFormat:@"%@%@", kCountlyInternalLogPrefix, logString]; + [CountlyCommon.sharedInstance.loggerDelegate internalLog:logStringWithPrefix withLevel:level]; + } - va_end(args); + va_end(args); } -void CountlyPrint(NSString *stringToPrint) -{ - NSLog(@"%@%@", kCountlyInternalLogPrefix, stringToPrint); -} +void CountlyPrint(NSString *stringToPrint) { NSLog(@"%@%@", kCountlyInternalLogPrefix, stringToPrint); } #pragma mark - Time/Date related methods - (NSInteger)hourOfDay { - NSDateComponents* components = [gregorianCalendar components:NSCalendarUnitHour fromDate:NSDate.date]; - return components.hour; + NSDateComponents *components = [gregorianCalendar components:NSCalendarUnitHour fromDate:NSDate.date]; + return components.hour; } - (NSInteger)dayOfWeek { - NSDateComponents* components = [gregorianCalendar components:NSCalendarUnitWeekday fromDate:NSDate.date]; - return components.weekday - 1; + NSDateComponents *components = [gregorianCalendar components:NSCalendarUnitWeekday fromDate:NSDate.date]; + return components.weekday - 1; } - (NSInteger)timeZone { - return NSTimeZone.systemTimeZone.secondsFromGMT / 60; + return NSTimeZone.systemTimeZone.secondsFromGMT / 60; } - (NSInteger)timeSinceLaunch { - return (int)NSDate.date.timeIntervalSince1970 - startTime; + return (int)NSDate.date.timeIntervalSince1970 - startTime; } - (NSTimeInterval)uniqueTimestamp { - long long now = floor(NSDate.date.timeIntervalSince1970 * 1000); + long long now = floor(NSDate.date.timeIntervalSince1970 * 1000); - if (now <= self.lastTimestamp) - self.lastTimestamp++; - else - self.lastTimestamp = now; + if (now <= self.lastTimestamp) + self.lastTimestamp++; + else + self.lastTimestamp = now; - return (NSTimeInterval)(self.lastTimestamp / 1000.0); + return (NSTimeInterval)(self.lastTimestamp / 1000.0); } - (NSString *)randomEventID { - const int size = 6; - void *randomBuffer = malloc(size); - arc4random_buf(randomBuffer, size); - NSData* randomData = [NSData dataWithBytesNoCopy:randomBuffer length:size freeWhenDone:YES]; - NSString* randomBase64 = [randomData base64EncodedStringWithOptions:0]; - NSTimeInterval timestamp = self.uniqueTimestamp; - NSString* randomEventID = [NSString stringWithFormat:@"%@%lld", randomBase64, (long long)(timestamp * 1000)]; - return randomEventID; + const int size = 6; + void *randomBuffer = malloc(size); + arc4random_buf(randomBuffer, size); + NSData *randomData = [NSData dataWithBytesNoCopy:randomBuffer length:size freeWhenDone:YES]; + NSString *randomBase64 = [randomData base64EncodedStringWithOptions:0]; + NSTimeInterval timestamp = self.uniqueTimestamp; + NSString *randomEventID = [NSString stringWithFormat:@"%@%lld", randomBase64, (long long)(timestamp * 1000)]; + return randomEventID; } #pragma mark - Orientation @@ -194,61 +193,62 @@ - (NSString *)randomEventID - (void)observeDeviceOrientationChanges { #if (TARGET_OS_IOS) - [NSNotificationCenter.defaultCenter addObserver:self selector:@selector(deviceOrientationDidChange:) name:UIDeviceOrientationDidChangeNotification object:nil]; + [NSNotificationCenter.defaultCenter addObserver:self selector:@selector(deviceOrientationDidChange:) name:UIDeviceOrientationDidChangeNotification object:nil]; #endif } - (void)deviceOrientationDidChange:(NSNotification *)notification { - if (!self.enableOrientationTracking) - return; + if (!self.enableOrientationTracking) + return; - //NOTE: Delay is needed for interface orientation change animation to complete. Otherwise old interface orientation value is returned. - [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(recordOrientation) object:nil]; - [self performSelector:@selector(recordOrientation) withObject:nil afterDelay:0.5]; + // NOTE: Delay is needed for interface orientation change animation to complete. Otherwise old interface orientation value is returned. + [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(recordOrientation) object:nil]; + [self performSelector:@selector(recordOrientation) withObject:nil afterDelay:0.5]; } - (void)recordOrientation { #if (TARGET_OS_IOS) - if (!self.enableOrientationTracking) - return; - - if ([UIApplication sharedApplication].applicationState == UIApplicationStateBackground) { - CLY_LOG_W(@"%s App is in the background, 'Record Orientation' will be ignored", __FUNCTION__); - return; - } -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wdeprecated-declarations" - UIInterfaceOrientation interfaceOrientation = UIApplication.sharedApplication.statusBarOrientation; -#pragma GCC diagnostic pop - - NSString* mode = nil; - if (UIInterfaceOrientationIsPortrait(interfaceOrientation)) - mode = @"portrait"; - else if (UIInterfaceOrientationIsLandscape(interfaceOrientation)) - mode = @"landscape"; - - if (!mode) - { - CLY_LOG_D(@"Interface orientation is not landscape or portrait."); - return; - } - - if ([mode isEqualToString:self.lastInterfaceOrientation]) - { - CLY_LOG_V(@"Interface orientation is still same: %@", self.lastInterfaceOrientation); - return; - } - - CLY_LOG_D(@"Interface orientation is now: %@", mode); - - if (!CountlyConsentManager.sharedInstance.consentForUserDetails) - return; - - self.lastInterfaceOrientation = mode; - - [Countly.sharedInstance recordReservedEvent:kCountlyReservedEventOrientation segmentation:@{kCountlyOrientationKeyMode: mode}]; + if (!self.enableOrientationTracking) + return; + + if ([UIApplication sharedApplication].applicationState == UIApplicationStateBackground) + { + CLY_LOG_W(@"%s App is in the background, 'Record Orientation' will be ignored", __FUNCTION__); + return; + } + #pragma GCC diagnostic push + #pragma GCC diagnostic ignored "-Wdeprecated-declarations" + UIInterfaceOrientation interfaceOrientation = UIApplication.sharedApplication.statusBarOrientation; + #pragma GCC diagnostic pop + + NSString *mode = nil; + if (UIInterfaceOrientationIsPortrait(interfaceOrientation)) + mode = @"portrait"; + else if (UIInterfaceOrientationIsLandscape(interfaceOrientation)) + mode = @"landscape"; + + if (!mode) + { + CLY_LOG_D(@"Interface orientation is not landscape or portrait."); + return; + } + + if ([mode isEqualToString:self.lastInterfaceOrientation]) + { + CLY_LOG_V(@"Interface orientation is still same: %@", self.lastInterfaceOrientation); + return; + } + + CLY_LOG_D(@"Interface orientation is now: %@", mode); + + if (!CountlyConsentManager.sharedInstance.consentForUserDetails) + return; + + self.lastInterfaceOrientation = mode; + + [Countly.sharedInstance recordReservedEvent:kCountlyReservedEventOrientation segmentation:@{kCountlyOrientationKeyMode : mode}]; #endif } @@ -257,389 +257,398 @@ - (void)recordOrientation - (void)startBackgroundTask { #if (TARGET_OS_IOS || TARGET_OS_VISION || TARGET_OS_TV) - if (self.bgTask != UIBackgroundTaskInvalid) - return; + if (self.bgTask != UIBackgroundTaskInvalid) + return; - self.bgTask = [UIApplication.sharedApplication beginBackgroundTaskWithExpirationHandler:^ - { - [UIApplication.sharedApplication endBackgroundTask:self.bgTask]; - self.bgTask = UIBackgroundTaskInvalid; - }]; + self.bgTask = [UIApplication.sharedApplication beginBackgroundTaskWithExpirationHandler:^{ + [UIApplication.sharedApplication endBackgroundTask:self.bgTask]; + self.bgTask = UIBackgroundTaskInvalid; + }]; #endif } - (void)finishBackgroundTask { #if (TARGET_OS_IOS || TARGET_OS_VISION || TARGET_OS_TV) - if (self.bgTask != UIBackgroundTaskInvalid && !CountlyConnectionManager.sharedInstance.connection) - { - [UIApplication.sharedApplication endBackgroundTask:self.bgTask]; - self.bgTask = UIBackgroundTaskInvalid; - } + if (self.bgTask != UIBackgroundTaskInvalid && !CountlyConnectionManager.sharedInstance.connection) + { + [UIApplication.sharedApplication endBackgroundTask:self.bgTask]; + self.bgTask = UIBackgroundTaskInvalid; + } #endif } #if (TARGET_OS_IOS || TARGET_OS_TV) - (UIViewController *)topViewController { -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wdeprecated-declarations" - UIViewController* topVC = UIApplication.sharedApplication.keyWindow.rootViewController; -#pragma GCC diagnostic pop - - while (YES) - { - if (topVC.presentedViewController) - topVC = topVC.presentedViewController; - else if ([topVC isKindOfClass:UINavigationController.class]) - topVC = ((UINavigationController *)topVC).topViewController; - else if ([topVC isKindOfClass:UITabBarController.class]) - topVC = ((UITabBarController *)topVC).selectedViewController; - else - break; - } + #pragma GCC diagnostic push + #pragma GCC diagnostic ignored "-Wdeprecated-declarations" + UIViewController *topVC = UIApplication.sharedApplication.keyWindow.rootViewController; + #pragma GCC diagnostic pop + + while (YES) + { + if (topVC.presentedViewController) + topVC = topVC.presentedViewController; + else if ([topVC isKindOfClass:UINavigationController.class]) + topVC = ((UINavigationController *)topVC).topViewController; + else if ([topVC isKindOfClass:UITabBarController.class]) + topVC = ((UITabBarController *)topVC).selectedViewController; + else + break; + } - return topVC; + return topVC; } - (void)tryPresentingViewController:(UIViewController *)viewController { - [self tryPresentingViewController:viewController withCompletion:nil]; + [self tryPresentingViewController:viewController withCompletion:nil]; } -- (void)tryPresentingViewController:(UIViewController *)viewController withCompletion:(void (^ __nullable) (void))completion +- (void)tryPresentingViewController:(UIViewController *)viewController withCompletion:(void (^__nullable)(void))completion { - UIViewController* topVC = self.topViewController; + UIViewController *topVC = self.topViewController; - if (topVC) - { - [topVC presentViewController:viewController animated:YES completion:^ - { - if (completion) - completion(); - }]; + if (topVC) + { + [topVC presentViewController:viewController + animated:YES + completion:^{ + if (completion) + completion(); + }]; - return; - } + return; + } - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^ - { - [self tryPresentingViewController:viewController]; - }); + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + [self tryPresentingViewController:viewController]; + }); } #endif - (NSURLSession *)URLSession { - if (CountlyConnectionManager.sharedInstance.URLSessionConfiguration) - { - return [NSURLSession sessionWithConfiguration:CountlyConnectionManager.sharedInstance.URLSessionConfiguration]; - } - else - { - return NSURLSession.sharedSession; - } + if (CountlyConnectionManager.sharedInstance.URLSessionConfiguration) + { + return [NSURLSession sessionWithConfiguration:CountlyConnectionManager.sharedInstance.URLSessionConfiguration]; + } + else + { + return NSURLSession.sharedSession; + } } #if (TARGET_OS_IOS) -- (bool) hasTopNotch:(UIEdgeInsets)safeArea +- (bool)hasTopNotch:(UIEdgeInsets)safeArea { - return safeArea.top >= 44; + return safeArea.top >= 44; } #endif -- (CGSize)getWindowSize{ +- (CGSize)getWindowSize +{ #if (TARGET_OS_IOS) - UIWindow *window = nil; - - if (@available(iOS 13.0, *)) { - for (UIScene *scene in UIApplication.sharedApplication.connectedScenes) { - if ([scene isKindOfClass:[UIWindowScene class]]) { - window = ((UIWindowScene *)scene).windows.firstObject; - break; - } - } - } else { - window = UIApplication.sharedApplication.delegate.window; + UIWindow *window = nil; + + if (@available(iOS 13.0, *)) + { + for (UIScene *scene in UIApplication.sharedApplication.connectedScenes) + { + if ([scene isKindOfClass:[UIWindowScene class]]) + { + window = ((UIWindowScene *)scene).windows.firstObject; + break; + } } + } + else + { + window = UIApplication.sharedApplication.delegate.window; + } - if (!window) return CGSizeZero; + if (!window) + return CGSizeZero; - CGSize size = window.bounds.size; + CGSize size = window.bounds.size; - if (@available(iOS 11.0, *)) { - UIEdgeInsets safeArea = window.safeAreaInsets; - if([self hasTopNotch:safeArea] || CountlyContentBuilderInternal.sharedInstance.webViewDisplayOption == SAFE_AREA){ - size.height -= (safeArea.top); // always respect notch - } - if(CountlyContentBuilderInternal.sharedInstance.webViewDisplayOption == SAFE_AREA){ - size.height -= safeArea.bottom; - } - size.width -= MAX(safeArea.left, safeArea.right); // regardles of given safe area, act for cutout + if (@available(iOS 11.0, *)) + { + UIEdgeInsets safeArea = window.safeAreaInsets; + if ([self hasTopNotch:safeArea] || CountlyContentBuilderInternal.sharedInstance.webViewDisplayOption == SAFE_AREA) + { + size.height -= (safeArea.top); // always respect notch + } + if (CountlyContentBuilderInternal.sharedInstance.webViewDisplayOption == SAFE_AREA) + { + size.height -= safeArea.bottom; } + size.width -= MAX(safeArea.left, safeArea.right); // regardles of given safe area, act for cutout + } - return size; + return size; #else - return CGSizeZero; + return CGSizeZero; #endif } - @end - #pragma mark - Internal ViewController #if (TARGET_OS_IOS) @implementation CLYInternalViewController : UIViewController - (void)viewWillLayoutSubviews { - [super viewWillLayoutSubviews]; + [super viewWillLayoutSubviews]; + + if (self.webView) + { + CGRect frame = CGRectInset(self.view.bounds, 20.0, 20.0); - if (self.webView) + UIEdgeInsets insets = UIEdgeInsetsZero; + if (@available(iOS 11.0, *)) { - CGRect frame = CGRectInset(self.view.bounds, 20.0, 20.0); - - UIEdgeInsets insets = UIEdgeInsetsZero; - if (@available(iOS 11.0, *)) - { - #pragma GCC diagnostic push - #pragma GCC diagnostic ignored "-Wdeprecated-declarations" - insets = UIApplication.sharedApplication.keyWindow.safeAreaInsets; - #pragma GCC diagnostic pop - } - - self.webView.navigationDelegate = self; - frame = UIEdgeInsetsInsetRect(frame, insets); - self.webView.frame = frame; + #pragma GCC diagnostic push + #pragma GCC diagnostic ignored "-Wdeprecated-declarations" + insets = UIApplication.sharedApplication.keyWindow.safeAreaInsets; + #pragma GCC diagnostic pop } -} -- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler { - NSString *url = navigationAction.request.URL.absoluteString; - if ([url containsString:@"cly_x_int=1"]) { - CLY_LOG_I(@"%s Opening url [%@] in external browser", __FUNCTION__, url); - [[UIApplication sharedApplication] openURL:navigationAction.request.URL options:@{} completionHandler:^(BOOL success) { - if (success) { - CLY_LOG_I(@"%s url [%@] opened in external browser", __FUNCTION__, url); - } - else { - CLY_LOG_I(@"%s unable to open url [%@] in external browser", __FUNCTION__, url); - } - }]; - decisionHandler(WKNavigationActionPolicyCancel); - return; - } - decisionHandler(WKNavigationActionPolicyAllow); + self.webView.navigationDelegate = self; + frame = UIEdgeInsetsInsetRect(frame, insets); + self.webView.frame = frame; + } +} + +- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler +{ + NSString *url = navigationAction.request.URL.absoluteString; + if ([url containsString:@"cly_x_int=1"]) + { + CLY_LOG_I(@"%s Opening url [%@] in external browser", __FUNCTION__, url); + [[UIApplication sharedApplication] openURL:navigationAction.request.URL + options:@{} + completionHandler:^(BOOL success) { + if (success) + { + CLY_LOG_I(@"%s url [%@] opened in external browser", __FUNCTION__, url); + } + else + { + CLY_LOG_I(@"%s unable to open url [%@] in external browser", __FUNCTION__, url); + } + }]; + decisionHandler(WKNavigationActionPolicyCancel); + return; + } + decisionHandler(WKNavigationActionPolicyAllow); } - (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(null_unspecified WKNavigation *)navigation { - CLY_LOG_I(@"%s Web view has start loading", __FUNCTION__); - + CLY_LOG_I(@"%s Web view has start loading", __FUNCTION__); } -- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation { - CLY_LOG_I(@"%s Web view has finished loading", __FUNCTION__); +- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation +{ + CLY_LOG_I(@"%s Web view has finished loading", __FUNCTION__); } - @end - @implementation CLYButton : UIButton -const CGFloat kCountlyDismissButtonSize = 30.0; -const CGFloat kCountlyDismissButtonMargin = 10.0; +const CGFloat kCountlyDismissButtonSize = 30.0; +const CGFloat kCountlyDismissButtonMargin = 10.0; const CGFloat kCountlyDismissButtonStandardStatusBarHeight = 20.0; - (instancetype)initWithFrame:(CGRect)frame { - if (self = [super initWithFrame:frame]) - { - [self addTarget:self action:@selector(touchUpInside:) forControlEvents:UIControlEventTouchUpInside]; - } + if (self = [super initWithFrame:frame]) + { + [self addTarget:self action:@selector(touchUpInside:) forControlEvents:UIControlEventTouchUpInside]; + } - return self; + return self; } - (void)touchUpInside:(id)sender { - if (self.onClick) - self.onClick(self); + if (self.onClick) + self.onClick(self); } -+ (CLYButton *)dismissAlertButton:(NSString * _Nullable)closeButtonText ++ (CLYButton *)dismissAlertButton:(NSString *_Nullable)closeButtonText { - if (!closeButtonText) { - closeButtonText = @"x"; - } - CLYButton* dismissButton = [CLYButton buttonWithType:UIButtonTypeCustom]; - dismissButton.frame = (CGRect){CGPointZero, kCountlyDismissButtonSize, kCountlyDismissButtonSize}; - [dismissButton setTitle:closeButtonText forState:UIControlStateNormal]; - [dismissButton setTitleColor:UIColor.whiteColor forState:UIControlStateNormal]; - dismissButton.backgroundColor = [UIColor.blackColor colorWithAlphaComponent:0.5]; - dismissButton.layer.cornerRadius = dismissButton.bounds.size.width * 0.5; - dismissButton.layer.borderColor = [UIColor.blackColor colorWithAlphaComponent:0.7].CGColor; - dismissButton.layer.borderWidth = 1.0; - dismissButton.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleBottomMargin; - - return dismissButton; + if (!closeButtonText) + { + closeButtonText = @"x"; + } + CLYButton *dismissButton = [CLYButton buttonWithType:UIButtonTypeCustom]; + dismissButton.frame = (CGRect){CGPointZero, kCountlyDismissButtonSize, kCountlyDismissButtonSize}; + [dismissButton setTitle:closeButtonText forState:UIControlStateNormal]; + [dismissButton setTitleColor:UIColor.whiteColor forState:UIControlStateNormal]; + dismissButton.backgroundColor = [UIColor.blackColor colorWithAlphaComponent:0.5]; + dismissButton.layer.cornerRadius = dismissButton.bounds.size.width * 0.5; + dismissButton.layer.borderColor = [UIColor.blackColor colorWithAlphaComponent:0.7].CGColor; + dismissButton.layer.borderWidth = 1.0; + dismissButton.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleBottomMargin; + + return dismissButton; } + (CLYButton *)dismissAlertButton { - return [CLYButton dismissAlertButton:nil]; + return [CLYButton dismissAlertButton:nil]; } - (void)positionToTopRight { - [self positionToTopRight:NO]; + [self positionToTopRight:NO]; } - (void)positionToTopRightConsideringStatusBar { - [self positionToTopRight:YES]; + [self positionToTopRight:YES]; } - (void)positionToTopRight:(BOOL)shouldConsiderStatusBar { - CGRect rect = self.frame; - rect.origin.x = self.superview.bounds.size.width - self.bounds.size.width - kCountlyDismissButtonMargin; - rect.origin.y = kCountlyDismissButtonMargin; + CGRect rect = self.frame; + rect.origin.x = self.superview.bounds.size.width - self.bounds.size.width - kCountlyDismissButtonMargin; + rect.origin.y = kCountlyDismissButtonMargin; - if (shouldConsiderStatusBar) + if (shouldConsiderStatusBar) + { + if (@available(iOS 11.0, *)) { - if (@available(iOS 11.0, *)) - { -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wdeprecated-declarations" - CGFloat top = UIApplication.sharedApplication.keyWindow.safeAreaInsets.top; -#pragma GCC diagnostic pop - - if (top) - { - rect.origin.y += top; - } - else - { - rect.origin.y += kCountlyDismissButtonStandardStatusBarHeight; - } - } - else - { - rect.origin.y += kCountlyDismissButtonStandardStatusBarHeight; - } + #pragma GCC diagnostic push + #pragma GCC diagnostic ignored "-Wdeprecated-declarations" + CGFloat top = UIApplication.sharedApplication.keyWindow.safeAreaInsets.top; + #pragma GCC diagnostic pop + + if (top) + { + rect.origin.y += top; + } + else + { + rect.origin.y += kCountlyDismissButtonStandardStatusBarHeight; + } } + else + { + rect.origin.y += kCountlyDismissButtonStandardStatusBarHeight; + } + } - self.frame = rect; + self.frame = rect; } @end #endif - #pragma mark - Proxy Object @implementation CLYDelegateInterceptor - (NSMethodSignature *)methodSignatureForSelector:(SEL)sel { - return [self.originalDelegate methodSignatureForSelector:sel]; + return [self.originalDelegate methodSignatureForSelector:sel]; } - (void)forwardInvocation:(NSInvocation *)invocation { - if ([self.originalDelegate respondsToSelector:invocation.selector]) - [invocation invokeWithTarget:self.originalDelegate]; - else - [super forwardInvocation:invocation]; + if ([self.originalDelegate respondsToSelector:invocation.selector]) + [invocation invokeWithTarget:self.originalDelegate]; + else + [super forwardInvocation:invocation]; } @end - - #pragma mark - Categories -NSString* CountlyJSONFromObject(id object) +NSString *CountlyJSONFromObject(id object) { - if (!object) - return nil; + if (!object) + return nil; - if (![NSJSONSerialization isValidJSONObject:object]) - { - CLY_LOG_W(@"Object is not valid for converting to JSON!"); - return nil; - } + if (![NSJSONSerialization isValidJSONObject:object]) + { + CLY_LOG_W(@"Object is not valid for converting to JSON!"); + return nil; + } - NSError *error = nil; - NSData *data = [NSJSONSerialization dataWithJSONObject:object options:0 error:&error]; - if (error) - { - CLY_LOG_W(@"%s, JSON can not be created error:[ %@ ]", __FUNCTION__, error); - } + NSError *error = nil; + NSData *data = [NSJSONSerialization dataWithJSONObject:object options:0 error:&error]; + if (error) + { + CLY_LOG_W(@"%s, JSON can not be created error:[ %@ ]", __FUNCTION__, error); + } - return [data cly_stringUTF8]; + return [data cly_stringUTF8]; } @implementation NSString (Countly) - (NSString *)cly_URLEscaped { - NSCharacterSet* charset = [NSCharacterSet characterSetWithCharactersInString:@"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._~"]; - return [self stringByAddingPercentEncodingWithAllowedCharacters:charset]; + NSCharacterSet *charset = [NSCharacterSet characterSetWithCharactersInString:@"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._~"]; + return [self stringByAddingPercentEncodingWithAllowedCharacters:charset]; } - (NSString *)cly_SHA256 { - const char* s = [self UTF8String]; - unsigned char digest[CC_SHA256_DIGEST_LENGTH]; - CC_SHA256(s, (CC_LONG)strlen(s), digest); + const char *s = [self UTF8String]; + unsigned char digest[CC_SHA256_DIGEST_LENGTH]; + CC_SHA256(s, (CC_LONG)strlen(s), digest); - NSMutableString* hash = NSMutableString.new; - for (int i = 0; i < CC_SHA256_DIGEST_LENGTH; i++) - [hash appendFormat:@"%02x", digest[i]]; + NSMutableString *hash = NSMutableString.new; + for (int i = 0; i < CC_SHA256_DIGEST_LENGTH; i++) + [hash appendFormat:@"%02x", digest[i]]; - return hash; + return hash; } - (NSData *)cly_dataUTF8 { - return [self dataUsingEncoding:NSUTF8StringEncoding]; + return [self dataUsingEncoding:NSUTF8StringEncoding]; } - (NSString *)cly_valueForQueryStringKey:(NSString *)key { - NSString* tempURLString = [@"http://example.com/path?" stringByAppendingString:self]; - NSURLComponents* URLComponents = [NSURLComponents componentsWithString:tempURLString]; - for (NSURLQueryItem* queryItem in URLComponents.queryItems) + NSString *tempURLString = [@"http://example.com/path?" stringByAppendingString:self]; + NSURLComponents *URLComponents = [NSURLComponents componentsWithString:tempURLString]; + for (NSURLQueryItem *queryItem in URLComponents.queryItems) + { + if ([queryItem.name isEqualToString:key]) { - if ([queryItem.name isEqualToString:key]) - { - return queryItem.value; - } + return queryItem.value; } + } - return nil; + return nil; } - (NSString *)cly_truncatedKey:(NSString *)explanation { - if (self.length > CountlyCommon.sharedInstance.maxKeyLength) - { - CLY_LOG_W(@"%@ length is more than the limit (%ld)! So, it will be truncated: %@.", explanation, (long)CountlyCommon.sharedInstance.maxKeyLength, self); - return [self substringToIndex:CountlyCommon.sharedInstance.maxKeyLength]; - } + if (self.length > CountlyCommon.sharedInstance.maxKeyLength) + { + CLY_LOG_W(@"%@ length is more than the limit (%ld)! So, it will be truncated: %@.", explanation, (long)CountlyCommon.sharedInstance.maxKeyLength, self); + return [self substringToIndex:CountlyCommon.sharedInstance.maxKeyLength]; + } - return self; + return self; } - (NSString *)cly_truncatedValue:(NSString *)explanation { - if (self.length > CountlyCommon.sharedInstance.maxValueLength) - { - CLY_LOG_W(@"%@ length is more than the limit (%ld)! So, it will be truncated: %@.", explanation, (long)CountlyCommon.sharedInstance.maxValueLength, self); - return [self substringToIndex:CountlyCommon.sharedInstance.maxValueLength]; - } + if (self.length > CountlyCommon.sharedInstance.maxValueLength) + { + CLY_LOG_W(@"%@ length is more than the limit (%ld)! So, it will be truncated: %@.", explanation, (long)CountlyCommon.sharedInstance.maxValueLength, self); + return [self substringToIndex:CountlyCommon.sharedInstance.maxValueLength]; + } - return self; + return self; } @end @@ -647,88 +656,94 @@ - (NSString *)cly_truncatedValue:(NSString *)explanation @implementation NSArray (Countly) - (NSString *)cly_JSONify { - return [CountlyJSONFromObject(self) cly_URLEscaped]; + return [CountlyJSONFromObject(self) cly_URLEscaped]; } -- (NSArray *) cly_filterSupportedDataTypes { - NSMutableArray *filteredArray = [NSMutableArray array]; - for (id obj in self) { - if ([obj isKindOfClass:[NSNumber class]] || [obj isKindOfClass:[NSString class]]) { - [filteredArray addObject:obj]; - } else { - CLY_LOG_W(@"%s, Removed invalid type from array: %@", __FUNCTION__, [obj class]); - } +- (NSArray *)cly_filterSupportedDataTypes +{ + NSMutableArray *filteredArray = [NSMutableArray array]; + for (id obj in self) + { + if ([obj isKindOfClass:[NSNumber class]] || [obj isKindOfClass:[NSString class]]) + { + [filteredArray addObject:obj]; + } + else + { + CLY_LOG_W(@"%s, Removed invalid type from array: %@", __FUNCTION__, [obj class]); } - return filteredArray.copy; + } + return filteredArray.copy; } @end @implementation NSDictionary (Countly) - (NSString *)cly_JSONify { - return [CountlyJSONFromObject(self) cly_URLEscaped]; + return [CountlyJSONFromObject(self) cly_URLEscaped]; } - (NSDictionary *)cly_truncated:(NSString *)explanation { - NSMutableDictionary* truncatedDict = self.mutableCopy; - [self enumerateKeysAndObjectsUsingBlock:^(NSString * key, id obj, BOOL * stop) + NSMutableDictionary *truncatedDict = self.mutableCopy; + [self enumerateKeysAndObjectsUsingBlock:^(NSString *key, id obj, BOOL *stop) { + NSString *truncatedKey = [key cly_truncatedKey:[explanation stringByAppendingString:@" key"]]; + if (![truncatedKey isEqualToString:key]) { - NSString* truncatedKey = [key cly_truncatedKey:[explanation stringByAppendingString:@" key"]]; - if (![truncatedKey isEqualToString:key]) - { - truncatedDict[truncatedKey] = obj; - [truncatedDict removeObjectForKey:key]; - } - - if ([obj isKindOfClass:NSString.class]) - { - NSString* truncatedValue = [obj cly_truncatedValue:[explanation stringByAppendingString:@" value"]]; - if (![truncatedValue isEqualToString:obj]) - { - truncatedDict[truncatedKey] = truncatedValue; - } - } - }]; - - return truncatedDict.copy; + truncatedDict[truncatedKey] = obj; + [truncatedDict removeObjectForKey:key]; + } + + if ([obj isKindOfClass:NSString.class]) + { + NSString *truncatedValue = [obj cly_truncatedValue:[explanation stringByAppendingString:@" value"]]; + if (![truncatedValue isEqualToString:obj]) + { + truncatedDict[truncatedKey] = truncatedValue; + } + } + }]; + + return truncatedDict.copy; } - (NSDictionary *)cly_limited:(NSString *)explanation { - NSArray* allKeys = self.allKeys; + NSArray *allKeys = self.allKeys; - if (allKeys.count <= CountlyCommon.sharedInstance.maxSegmentationValues) - return self; + if (allKeys.count <= CountlyCommon.sharedInstance.maxSegmentationValues) + return self; - NSMutableArray* excessKeys = allKeys.mutableCopy; - [excessKeys removeObjectsInRange:(NSRange){0, CountlyCommon.sharedInstance.maxSegmentationValues}]; + NSMutableArray *excessKeys = allKeys.mutableCopy; + [excessKeys removeObjectsInRange:(NSRange){0, CountlyCommon.sharedInstance.maxSegmentationValues}]; - CLY_LOG_W(@"%s, Number of key-value pairs in %@ is more than the limit (%ld)! So, some of them will be removed %@", __FUNCTION__, explanation, (long)CountlyCommon.sharedInstance.maxSegmentationValues, [excessKeys description]); + CLY_LOG_W(@"%s, Number of key-value pairs in %@ is more than the limit (%ld)! So, some of them will be removed %@", __FUNCTION__, explanation, (long)CountlyCommon.sharedInstance.maxSegmentationValues, [excessKeys description]); - NSMutableDictionary* limitedDict = self.mutableCopy; - [limitedDict removeObjectsForKeys:excessKeys]; + NSMutableDictionary *limitedDict = self.mutableCopy; + [limitedDict removeObjectsForKeys:excessKeys]; - return limitedDict.copy; + return limitedDict.copy; } -- (NSMutableDictionary *) cly_filterSupportedDataTypes +- (NSMutableDictionary *)cly_filterSupportedDataTypes { - NSMutableDictionary *filteredDictionary = [NSMutableDictionary dictionary]; - - for (NSString *key in self) { - id value = [self objectForKey:key]; - - if ([value isKindOfClass:[NSNumber class]] || - [value isKindOfClass:[NSString class]] || - ([value isKindOfClass:[NSArray class]] && (value = [value cly_filterSupportedDataTypes]))) { - [filteredDictionary setObject:value forKey:key]; - } else { - CLY_LOG_W(@"%s, Removed invalid type for key %@: %@", __FUNCTION__, key, [value class]); - } + NSMutableDictionary *filteredDictionary = [NSMutableDictionary dictionary]; + + for (NSString *key in self) + { + id value = [self objectForKey:key]; + + if ([value isKindOfClass:[NSNumber class]] || [value isKindOfClass:[NSString class]] || ([value isKindOfClass:[NSArray class]] && (value = [value cly_filterSupportedDataTypes]))) + { + [filteredDictionary setObject:value forKey:key]; + } + else + { + CLY_LOG_W(@"%s, Removed invalid type for key %@: %@", __FUNCTION__, key, [value class]); } - - return filteredDictionary.mutableCopy; + } + + return filteredDictionary.mutableCopy; } @end @@ -736,6 +751,6 @@ - (NSMutableDictionary *) cly_filterSupportedDataTypes @implementation NSData (Countly) - (NSString *)cly_stringUTF8 { - return [NSString.alloc initWithData:self encoding:NSUTF8StringEncoding]; + return [NSString.alloc initWithData:self encoding:NSUTF8StringEncoding]; } @end diff --git a/CountlyServerConfig.m b/CountlyServerConfig.m index cbf35b34..42ad6ed2 100644 --- a/CountlyServerConfig.m +++ b/CountlyServerConfig.m @@ -7,879 +7,925 @@ #import "CountlyCommon.h" @interface CountlyServerConfig () { - NSTimer *_requestTimer; -} -@property (nonatomic) BOOL trackingEnabled; -@property (nonatomic) BOOL networkingEnabled; -@property (nonatomic) BOOL crashReportingEnabled; -@property (nonatomic) BOOL loggingEnabled; -@property (nonatomic) BOOL customEventTrackingEnabled; -@property (nonatomic) BOOL viewTrackingEnabled; -@property (nonatomic) BOOL sessionTrackingEnabled; -@property (nonatomic) BOOL enterContentZone; -@property (nonatomic) BOOL consentRequired; -@property (nonatomic) BOOL locationTracking; -@property (nonatomic) BOOL refreshContentZone; -@property (nonatomic) BOOL backoffMechanism; - -@property (nonatomic) NSInteger limitKeyLength; -@property (nonatomic) NSInteger limitValueSize; -@property (nonatomic) NSInteger limitSegValues; -@property (nonatomic) NSInteger limitBreadcrumb; -@property (nonatomic) NSInteger limitTraceLine; -@property (nonatomic) NSInteger limitTraceLength; -@property (nonatomic) NSInteger sessionInterval; -@property (nonatomic) NSInteger eventQueueSize; -@property (nonatomic) NSInteger requestQueueSize; -@property (nonatomic) NSInteger contentZoneInterval; -@property (nonatomic) NSInteger dropOldRequestTime; -@property (nonatomic) NSInteger serverConfigUpdateInterval; -@property (nonatomic) NSInteger currentServerConfigUpdateInterval; - -@property (nonatomic) NSInteger bomAcceptedTimeoutSeconds; -@property (nonatomic) double bomRQPercentage; -@property (nonatomic) NSInteger bomRequestAge; -@property (nonatomic) NSInteger bomDuration; - -@property (nonatomic) NSInteger requestTimeoutDuration; - -@property (nonatomic) NSSet *eventFilterSet; -@property (nonatomic) BOOL eventFilterIsWhitelist; -@property (nonatomic) NSSet *userPropertyFilterSet; -@property (nonatomic) BOOL userPropertyFilterIsWhitelist; -@property (nonatomic) NSInteger userPropertyCacheLimit; -@property (nonatomic) NSSet *segmentationFilterSet; -@property (nonatomic) BOOL segmentationFilterIsWhitelist; -@property (nonatomic) NSDictionary *> *eventSegmentationFilterMap; -@property (nonatomic) BOOL eventSegmentationFilterIsWhitelist; -@property (nonatomic) NSSet *journeyTriggerEvents; - -@property (nonatomic) NSInteger version; -@property (nonatomic) long long timestamp; -@property (nonatomic) long long lastFetchTimestamp; -@property (nonatomic) BOOL serverConfigUpdatesDisabled; + NSTimer *_requestTimer; +} +@property(nonatomic) BOOL trackingEnabled; +@property(nonatomic) BOOL networkingEnabled; +@property(nonatomic) BOOL crashReportingEnabled; +@property(nonatomic) BOOL loggingEnabled; +@property(nonatomic) BOOL customEventTrackingEnabled; +@property(nonatomic) BOOL viewTrackingEnabled; +@property(nonatomic) BOOL sessionTrackingEnabled; +@property(nonatomic) BOOL enterContentZone; +@property(nonatomic) BOOL consentRequired; +@property(nonatomic) BOOL locationTracking; +@property(nonatomic) BOOL refreshContentZone; +@property(nonatomic) BOOL backoffMechanism; + +@property(nonatomic) NSInteger limitKeyLength; +@property(nonatomic) NSInteger limitValueSize; +@property(nonatomic) NSInteger limitSegValues; +@property(nonatomic) NSInteger limitBreadcrumb; +@property(nonatomic) NSInteger limitTraceLine; +@property(nonatomic) NSInteger limitTraceLength; +@property(nonatomic) NSInteger sessionInterval; +@property(nonatomic) NSInteger eventQueueSize; +@property(nonatomic) NSInteger requestQueueSize; +@property(nonatomic) NSInteger contentZoneInterval; +@property(nonatomic) NSInteger dropOldRequestTime; +@property(nonatomic) NSInteger serverConfigUpdateInterval; +@property(nonatomic) NSInteger currentServerConfigUpdateInterval; + +@property(nonatomic) NSInteger bomAcceptedTimeoutSeconds; +@property(nonatomic) double bomRQPercentage; +@property(nonatomic) NSInteger bomRequestAge; +@property(nonatomic) NSInteger bomDuration; + +@property(nonatomic) NSInteger requestTimeoutDuration; + +@property(nonatomic) NSSet *eventFilterSet; +@property(nonatomic) BOOL eventFilterIsWhitelist; +@property(nonatomic) NSSet *userPropertyFilterSet; +@property(nonatomic) BOOL userPropertyFilterIsWhitelist; +@property(nonatomic) NSInteger userPropertyCacheLimit; +@property(nonatomic) NSSet *segmentationFilterSet; +@property(nonatomic) BOOL segmentationFilterIsWhitelist; +@property(nonatomic) NSDictionary *> *eventSegmentationFilterMap; +@property(nonatomic) BOOL eventSegmentationFilterIsWhitelist; +@property(nonatomic) NSSet *journeyTriggerEvents; + +@property(nonatomic) NSInteger version; +@property(nonatomic) long long timestamp; +@property(nonatomic) long long lastFetchTimestamp; +@property(nonatomic) BOOL serverConfigUpdatesDisabled; @end NSString *const kCountlySCKeySC = @"sc"; -NSString *const kTracking = @"tracking"; -NSString *const kNetworking = @"networking"; +NSString *const kTracking = @"tracking"; +NSString *const kNetworking = @"networking"; // request keys -NSString *const kRTimestamp = @"t"; -NSString *const kRVersion = @"v"; -NSString *const kRConfig = @"c"; -NSString *const kRReqQueueSize = @"rqs"; -NSString *const kREventQueueSize = @"eqs"; -NSString *const kRLogging = @"log"; +NSString *const kRTimestamp = @"t"; +NSString *const kRVersion = @"v"; +NSString *const kRConfig = @"c"; +NSString *const kRReqQueueSize = @"rqs"; +NSString *const kREventQueueSize = @"eqs"; +NSString *const kRLogging = @"log"; NSString *const kRSessionUpdateInterval = @"sui"; -NSString *const kRSessionTracking = @"st"; -NSString *const kRViewTracking = @"vt"; -NSString *const kRLocationTracking = @"lt"; -NSString *const kRRefreshContentZone = @"rcz"; -NSString *const kRbackoffMechanism = @"bom"; - -NSString *const kRLimitKeyLength = @"lkl"; -NSString *const kRLimitValueSize = @"lvs"; -NSString *const kRLimitSegValues = @"lsv"; -NSString *const kRLimitBreadcrumb = @"lbc"; -NSString *const kRLimitTraceLine = @"ltlpt"; -NSString *const kRLimitTraceLength = @"ltl"; -NSString *const kRCustomEventTracking = @"cet"; -NSString *const kREnterContentZone = @"ecz"; -NSString *const kRContentZoneInterval = @"czi"; -NSString *const kRConsentRequired = @"cr"; -NSString *const kRDropOldRequestTime = @"dort"; -NSString *const kRCrashReporting = @"crt"; +NSString *const kRSessionTracking = @"st"; +NSString *const kRViewTracking = @"vt"; +NSString *const kRLocationTracking = @"lt"; +NSString *const kRRefreshContentZone = @"rcz"; +NSString *const kRbackoffMechanism = @"bom"; + +NSString *const kRLimitKeyLength = @"lkl"; +NSString *const kRLimitValueSize = @"lvs"; +NSString *const kRLimitSegValues = @"lsv"; +NSString *const kRLimitBreadcrumb = @"lbc"; +NSString *const kRLimitTraceLine = @"ltlpt"; +NSString *const kRLimitTraceLength = @"ltl"; +NSString *const kRCustomEventTracking = @"cet"; +NSString *const kREnterContentZone = @"ecz"; +NSString *const kRContentZoneInterval = @"czi"; +NSString *const kRConsentRequired = @"cr"; +NSString *const kRDropOldRequestTime = @"dort"; +NSString *const kRCrashReporting = @"crt"; NSString *const kRServerConfigUpdateInterval = @"scui"; -NSString *const kRBOMAcceptedTimeout = @"bom_at"; -NSString *const kRBOMRQPercentage = @"bom_rqp"; -NSString *const kRBOMRequestAge = @"bom_ra"; -NSString *const kRBOMDuration = @"bom_d"; - -NSString *const kREventBlacklist = @"eb"; -NSString *const kREventWhitelist = @"ew"; -NSString *const kRUserPropertyBlacklist = @"upb"; -NSString *const kRUserPropertyWhitelist = @"upw"; -NSString *const kRUserPropertyCacheLimit = @"upcl"; -NSString *const kRSegmentationBlacklist = @"sb"; -NSString *const kRSegmentationWhitelist = @"sw"; +NSString *const kRBOMAcceptedTimeout = @"bom_at"; +NSString *const kRBOMRQPercentage = @"bom_rqp"; +NSString *const kRBOMRequestAge = @"bom_ra"; +NSString *const kRBOMDuration = @"bom_d"; + +NSString *const kREventBlacklist = @"eb"; +NSString *const kREventWhitelist = @"ew"; +NSString *const kRUserPropertyBlacklist = @"upb"; +NSString *const kRUserPropertyWhitelist = @"upw"; +NSString *const kRUserPropertyCacheLimit = @"upcl"; +NSString *const kRSegmentationBlacklist = @"sb"; +NSString *const kRSegmentationWhitelist = @"sw"; NSString *const kREventSegmentationBlacklist = @"esb"; NSString *const kREventSegmentationWhitelist = @"esw"; -NSString *const kRJourneyTriggerEvents = @"jte"; +NSString *const kRJourneyTriggerEvents = @"jte"; static CountlyServerConfig *s_sharedInstance = nil; -static dispatch_once_t onceToken; +static dispatch_once_t onceToken; @implementation CountlyServerConfig + (instancetype)sharedInstance { - if (!CountlyCommon.sharedInstance.hasStarted) - return nil; + if (!CountlyCommon.sharedInstance.hasStarted) + return nil; - dispatch_once(&onceToken, ^{ - s_sharedInstance = self.new; - }); - return s_sharedInstance; + dispatch_once(&onceToken, ^{ + s_sharedInstance = self.new; + }); + return s_sharedInstance; } - (instancetype)init { - self = [super init]; - if (self) - { - _timestamp = 0; - _version = 0; - _currentServerConfigUpdateInterval = 4; - _requestTimer = nil; - _serverConfigUpdatesDisabled = NO; - _requestTimeoutDuration = 30; - [self setDefaultValues]; - } - return self; + self = [super init]; + if (self) + { + _timestamp = 0; + _version = 0; + _currentServerConfigUpdateInterval = 4; + _requestTimer = nil; + _serverConfigUpdatesDisabled = NO; + _requestTimeoutDuration = 30; + [self setDefaultValues]; + } + return self; } - (void)resetInstance { - CLY_LOG_I(@"%s", __FUNCTION__); - _timestamp = 0; - _version = 0; - _currentServerConfigUpdateInterval = 4; - _serverConfigUpdatesDisabled = NO; - _requestTimeoutDuration = 30; - _lastFetchTimestamp = 0; - if (_requestTimer) - { - [_requestTimer invalidate]; - _requestTimer = nil; - } - [self setDefaultValues]; - onceToken = 0; - s_sharedInstance = nil; + CLY_LOG_I(@"%s", __FUNCTION__); + _timestamp = 0; + _version = 0; + _currentServerConfigUpdateInterval = 4; + _serverConfigUpdatesDisabled = NO; + _requestTimeoutDuration = 30; + _lastFetchTimestamp = 0; + if (_requestTimer) + { + [_requestTimer invalidate]; + _requestTimer = nil; + } + [self setDefaultValues]; + onceToken = 0; + s_sharedInstance = nil; } - (void)retrieveServerConfigFromStorage:(CountlyConfig *)config { - NSMutableDictionary *persistentBehaviorSettings = [CountlyPersistency.sharedInstance retrieveServerConfig]; - if (persistentBehaviorSettings.count == 0 && config.sdkBehaviorSettings) + NSMutableDictionary *persistentBehaviorSettings = [CountlyPersistency.sharedInstance retrieveServerConfig]; + if (persistentBehaviorSettings.count == 0 && config.sdkBehaviorSettings) + { + NSError *error = nil; + id parsed = [NSJSONSerialization JSONObjectWithData:[config.sdkBehaviorSettings cly_dataUTF8] options:0 error:&error]; + + if ([parsed isKindOfClass:[NSDictionary class]]) { - NSError *error = nil; - id parsed = [NSJSONSerialization JSONObjectWithData:[config.sdkBehaviorSettings cly_dataUTF8] options:0 error:&error]; - - if ([parsed isKindOfClass:[NSDictionary class]]) { - persistentBehaviorSettings = [(NSDictionary *)parsed mutableCopy]; - [CountlyPersistency.sharedInstance storeServerConfig:persistentBehaviorSettings]; - } else { - CLY_LOG_W(@"%s, Failed to parse sdkBehaviorSettings or not a dictionary: %@", __FUNCTION__, error); - } + persistentBehaviorSettings = [(NSDictionary *)parsed mutableCopy]; + [CountlyPersistency.sharedInstance storeServerConfig:persistentBehaviorSettings]; } + else + { + CLY_LOG_W(@"%s, Failed to parse sdkBehaviorSettings or not a dictionary: %@", __FUNCTION__, error); + } + } - [self populateServerConfig:persistentBehaviorSettings withConfig:config]; + [self populateServerConfig:persistentBehaviorSettings withConfig:config]; } -- (void)mergeBehaviorSettings:(NSMutableDictionary *)baseConfig - withConfig:(NSDictionary *)newConfig +- (void)mergeBehaviorSettings:(NSMutableDictionary *)baseConfig withConfig:(NSDictionary *)newConfig { - // c, t and v paramters must exist - if(newConfig.count != 3 || !newConfig[kRConfig]) { - CLY_LOG_D(@"%s, missing entries for a behavior settings omitting", __FUNCTION__); - return; - } - - if (!newConfig[kRVersion] || !newConfig[kRTimestamp]) - { - CLY_LOG_D(@"%s, version or timestamp is missing in the behavioır settings omitting", __FUNCTION__); - return; - } - - if(!([newConfig[kRConfig] isKindOfClass:[NSDictionary class]]) || ((NSDictionary *)newConfig[kRConfig]).count == 0){ - CLY_LOG_D(@"%s, invalid behavior settings omitting", __FUNCTION__); - return; - } - - id timestamp = newConfig[kRTimestamp]; - if (timestamp) { - baseConfig[kRTimestamp] = timestamp; - } + // c, t and v paramters must exist + if (newConfig.count != 3 || !newConfig[kRConfig]) + { + CLY_LOG_D(@"%s, missing entries for a behavior settings omitting", __FUNCTION__); + return; + } - id version = newConfig[kRVersion]; - if (version) { - baseConfig[kRVersion] = version; - } - - NSDictionary *cBase = baseConfig[kRConfig] ?: NSMutableDictionary.new; - NSDictionary *cNew = newConfig[kRConfig]; + if (!newConfig[kRVersion] || !newConfig[kRTimestamp]) + { + CLY_LOG_D(@"%s, version or timestamp is missing in the behavioır settings omitting", __FUNCTION__); + return; + } - if ([cBase isKindOfClass:[NSDictionary class]] || [cNew isKindOfClass:[NSDictionary class]]) { - NSMutableDictionary *cMerged = [cBase mutableCopy]; + if (!([newConfig[kRConfig] isKindOfClass:[NSDictionary class]]) || ((NSDictionary *)newConfig[kRConfig]).count == 0) + { + CLY_LOG_D(@"%s, invalid behavior settings omitting", __FUNCTION__); + return; + } - [cNew enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) { - if (obj != nil && obj != [NSNull null]) { - cMerged[key] = obj; - } - }]; + id timestamp = newConfig[kRTimestamp]; + if (timestamp) + { + baseConfig[kRTimestamp] = timestamp; + } - [self removeConflictingFilterKeys:cMerged newConfig:cNew]; - baseConfig[kRConfig] = cMerged; - } + id version = newConfig[kRVersion]; + if (version) + { + baseConfig[kRVersion] = version; + } + + NSDictionary *cBase = baseConfig[kRConfig] ?: NSMutableDictionary.new; + NSDictionary *cNew = newConfig[kRConfig]; + + if ([cBase isKindOfClass:[NSDictionary class]] || [cNew isKindOfClass:[NSDictionary class]]) + { + NSMutableDictionary *cMerged = [cBase mutableCopy]; + + [cNew enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) { + if (obj != nil && obj != [NSNull null]) + { + cMerged[key] = obj; + } + }]; + + [self removeConflictingFilterKeys:cMerged newConfig:cNew]; + baseConfig[kRConfig] = cMerged; + } } - (void)setBoolProperty:(BOOL *)property fromDictionary:(NSDictionary *)dictionary key:(NSString *)key logString:(NSMutableString *)logString { - NSNumber *value = dictionary[key]; - if (value) - { - *property = value.boolValue; - [logString appendFormat:@"%@: %@, ", key, *property ? @"YES" : @"NO"]; - } + NSNumber *value = dictionary[key]; + if (value) + { + *property = value.boolValue; + [logString appendFormat:@"%@: %@, ", key, *property ? @"YES" : @"NO"]; + } } - (void)setIntegerProperty:(NSInteger *)property fromDictionary:(NSDictionary *)dictionary key:(NSString *)key logString:(NSMutableString *)logString { - NSNumber *value = dictionary[key]; - if (value) - { - *property = value.integerValue; - [logString appendFormat:@"%@: %ld, ", key, (long)*property]; - } + NSNumber *value = dictionary[key]; + if (value) + { + *property = value.integerValue; + [logString appendFormat:@"%@: %ld, ", key, (long)*property]; + } } - (void)setDoubleProperty:(double *)property fromDictionary:(NSDictionary *)dictionary key:(NSString *)key logString:(NSMutableString *)logString { - NSNumber *value = dictionary[key]; - if (value) - { - *property = value.doubleValue; - [logString appendFormat:@"%@: %lf, ", key, (double)*property]; - } + NSNumber *value = dictionary[key]; + if (value) + { + *property = value.doubleValue; + [logString appendFormat:@"%@: %lf, ", key, (double)*property]; + } } - (void)populateServerConfig:(NSDictionary *)serverConfig withConfig:(CountlyConfig *)config { - if(config.requestTimeoutDuration <= 0) { - config.requestTimeoutDuration = 1; - } - - _requestTimeoutDuration = config.requestTimeoutDuration; - - if (!serverConfig[kRConfig]) - { - CLY_LOG_D(@"%s, config key is missing in the server configuration omitting", __FUNCTION__); - return; - } - - NSDictionary *dictionary = serverConfig[kRConfig]; - - if (!serverConfig[kRVersion] || !serverConfig[kRTimestamp]) - { - CLY_LOG_D(@"%s, version or timestamp is missing in the server configuration omitting", __FUNCTION__); - return; - } - - _version = [serverConfig[kRVersion] integerValue]; - _timestamp = [serverConfig[kRTimestamp] longLongValue]; - - NSMutableString *logString = [NSMutableString stringWithString:@"Server Config: "]; - - [self setBoolProperty:&_trackingEnabled fromDictionary:dictionary key:kTracking logString:logString]; - [self setBoolProperty:&_networkingEnabled fromDictionary:dictionary key:kNetworking logString:logString]; - [self setIntegerProperty:&_sessionInterval fromDictionary:dictionary key:kRSessionUpdateInterval logString:logString]; - [self setIntegerProperty:&_requestQueueSize fromDictionary:dictionary key:kRReqQueueSize logString:logString]; - [self setIntegerProperty:&_eventQueueSize fromDictionary:dictionary key:kREventQueueSize logString:logString]; - [self setBoolProperty:&_crashReportingEnabled fromDictionary:dictionary key:kRCrashReporting logString:logString]; - [self setBoolProperty:&_sessionTrackingEnabled fromDictionary:dictionary key:kRSessionTracking logString:logString]; - [self setBoolProperty:&_loggingEnabled fromDictionary:dictionary key:kRLogging logString:logString]; - [self setIntegerProperty:&_limitKeyLength fromDictionary:dictionary key:kRLimitKeyLength logString:logString]; - [self setIntegerProperty:&_limitValueSize fromDictionary:dictionary key:kRLimitValueSize logString:logString]; - [self setIntegerProperty:&_limitSegValues fromDictionary:dictionary key:kRLimitSegValues logString:logString]; - [self setIntegerProperty:&_limitBreadcrumb fromDictionary:dictionary key:kRLimitBreadcrumb logString:logString]; - [self setIntegerProperty:&_limitTraceLine fromDictionary:dictionary key:kRLimitTraceLine logString:logString]; - [self setIntegerProperty:&_limitTraceLength fromDictionary:dictionary key:kRLimitTraceLength logString:logString]; - [self setBoolProperty:&_customEventTrackingEnabled fromDictionary:dictionary key:kRCustomEventTracking logString:logString]; - [self setBoolProperty:&_viewTrackingEnabled fromDictionary:dictionary key:kRViewTracking logString:logString]; - [self setBoolProperty:&_enterContentZone fromDictionary:dictionary key:kREnterContentZone logString:logString]; - [self setIntegerProperty:&_contentZoneInterval fromDictionary:dictionary key:kRContentZoneInterval logString:logString]; - [self setBoolProperty:&_consentRequired fromDictionary:dictionary key:kRConsentRequired logString:logString]; - [self setIntegerProperty:&_dropOldRequestTime fromDictionary:dictionary key:kRDropOldRequestTime logString:logString]; - [self setIntegerProperty:&_serverConfigUpdateInterval fromDictionary:dictionary key:kRServerConfigUpdateInterval logString:logString]; - [self setBoolProperty:&_locationTracking fromDictionary:dictionary key:kRLocationTracking logString:logString]; - [self setBoolProperty:&_refreshContentZone fromDictionary:dictionary key:kRRefreshContentZone logString:logString]; - [self setBoolProperty:&_backoffMechanism fromDictionary:dictionary key:kRbackoffMechanism logString:logString]; - [self setIntegerProperty:&_bomAcceptedTimeoutSeconds fromDictionary:dictionary key:kRBOMAcceptedTimeout logString:logString]; - [self setDoubleProperty:&_bomRQPercentage fromDictionary:dictionary key:kRBOMRQPercentage logString:logString]; - [self setIntegerProperty:&_bomRequestAge fromDictionary:dictionary key:kRBOMRequestAge logString:logString]; - [self setIntegerProperty:&_bomDuration fromDictionary:dictionary key:kRBOMDuration logString:logString]; - [self setIntegerProperty:&_userPropertyCacheLimit fromDictionary:dictionary key:kRUserPropertyCacheLimit logString:logString]; - - [self updateListingFilters:dictionary logString:logString]; - - if(![logString isEqualToString: @"Server Config: "]){ - // means new config gotten, if that is the case notify SDK - [self notifySdkConfigChange: config]; - } - - CLY_LOG_D(@"%s, version:[%li], timestamp:[%lli], %@", __FUNCTION__, _version, _timestamp, logString); + if (config.requestTimeoutDuration <= 0) + { + config.requestTimeoutDuration = 1; + } + + _requestTimeoutDuration = config.requestTimeoutDuration; + + if (!serverConfig[kRConfig]) + { + CLY_LOG_D(@"%s, config key is missing in the server configuration omitting", __FUNCTION__); + return; + } + + NSDictionary *dictionary = serverConfig[kRConfig]; + + if (!serverConfig[kRVersion] || !serverConfig[kRTimestamp]) + { + CLY_LOG_D(@"%s, version or timestamp is missing in the server configuration omitting", __FUNCTION__); + return; + } + + _version = [serverConfig[kRVersion] integerValue]; + _timestamp = [serverConfig[kRTimestamp] longLongValue]; + + NSMutableString *logString = [NSMutableString stringWithString:@"Server Config: "]; + + [self setBoolProperty:&_trackingEnabled fromDictionary:dictionary key:kTracking logString:logString]; + [self setBoolProperty:&_networkingEnabled fromDictionary:dictionary key:kNetworking logString:logString]; + [self setIntegerProperty:&_sessionInterval fromDictionary:dictionary key:kRSessionUpdateInterval logString:logString]; + [self setIntegerProperty:&_requestQueueSize fromDictionary:dictionary key:kRReqQueueSize logString:logString]; + [self setIntegerProperty:&_eventQueueSize fromDictionary:dictionary key:kREventQueueSize logString:logString]; + [self setBoolProperty:&_crashReportingEnabled fromDictionary:dictionary key:kRCrashReporting logString:logString]; + [self setBoolProperty:&_sessionTrackingEnabled fromDictionary:dictionary key:kRSessionTracking logString:logString]; + [self setBoolProperty:&_loggingEnabled fromDictionary:dictionary key:kRLogging logString:logString]; + [self setIntegerProperty:&_limitKeyLength fromDictionary:dictionary key:kRLimitKeyLength logString:logString]; + [self setIntegerProperty:&_limitValueSize fromDictionary:dictionary key:kRLimitValueSize logString:logString]; + [self setIntegerProperty:&_limitSegValues fromDictionary:dictionary key:kRLimitSegValues logString:logString]; + [self setIntegerProperty:&_limitBreadcrumb fromDictionary:dictionary key:kRLimitBreadcrumb logString:logString]; + [self setIntegerProperty:&_limitTraceLine fromDictionary:dictionary key:kRLimitTraceLine logString:logString]; + [self setIntegerProperty:&_limitTraceLength fromDictionary:dictionary key:kRLimitTraceLength logString:logString]; + [self setBoolProperty:&_customEventTrackingEnabled fromDictionary:dictionary key:kRCustomEventTracking logString:logString]; + [self setBoolProperty:&_viewTrackingEnabled fromDictionary:dictionary key:kRViewTracking logString:logString]; + [self setBoolProperty:&_enterContentZone fromDictionary:dictionary key:kREnterContentZone logString:logString]; + [self setIntegerProperty:&_contentZoneInterval fromDictionary:dictionary key:kRContentZoneInterval logString:logString]; + [self setBoolProperty:&_consentRequired fromDictionary:dictionary key:kRConsentRequired logString:logString]; + [self setIntegerProperty:&_dropOldRequestTime fromDictionary:dictionary key:kRDropOldRequestTime logString:logString]; + [self setIntegerProperty:&_serverConfigUpdateInterval fromDictionary:dictionary key:kRServerConfigUpdateInterval logString:logString]; + [self setBoolProperty:&_locationTracking fromDictionary:dictionary key:kRLocationTracking logString:logString]; + [self setBoolProperty:&_refreshContentZone fromDictionary:dictionary key:kRRefreshContentZone logString:logString]; + [self setBoolProperty:&_backoffMechanism fromDictionary:dictionary key:kRbackoffMechanism logString:logString]; + [self setIntegerProperty:&_bomAcceptedTimeoutSeconds fromDictionary:dictionary key:kRBOMAcceptedTimeout logString:logString]; + [self setDoubleProperty:&_bomRQPercentage fromDictionary:dictionary key:kRBOMRQPercentage logString:logString]; + [self setIntegerProperty:&_bomRequestAge fromDictionary:dictionary key:kRBOMRequestAge logString:logString]; + [self setIntegerProperty:&_bomDuration fromDictionary:dictionary key:kRBOMDuration logString:logString]; + [self setIntegerProperty:&_userPropertyCacheLimit fromDictionary:dictionary key:kRUserPropertyCacheLimit logString:logString]; + + [self updateListingFilters:dictionary logString:logString]; + + if (![logString isEqualToString:@"Server Config: "]) + { + // means new config gotten, if that is the case notify SDK + [self notifySdkConfigChange:config]; + } + + CLY_LOG_D(@"%s, version:[%li], timestamp:[%lli], %@", __FUNCTION__, _version, _timestamp, logString); } - (void)notifySdkConfigChange:(CountlyConfig *)config { - config.enableDebug = _loggingEnabled || config.enableDebug; - CountlyCommon.sharedInstance.enableDebug = config.enableDebug; - - // Limits could be moved to another function, but letting them stay here serves us a monopolized view of notify - if (config.maxKeyLength != kCountlyMaxKeyLength) - { - [config.sdkInternalLimits setMaxKeyLength:config.maxKeyLength]; - } - - if (config.maxValueLength != kCountlyMaxValueSize) - { - [config.sdkInternalLimits setMaxValueSize:config.maxValueLength]; - } - - if (config.maxSegmentationValues != kCountlyMaxSegmentationValues) - { - [config.sdkInternalLimits setMaxSegmentationValues:config.maxSegmentationValues]; - } - - if (config.crashLogLimit != kCountlyMaxBreadcrumbCount) + config.enableDebug = _loggingEnabled || config.enableDebug; + CountlyCommon.sharedInstance.enableDebug = config.enableDebug; + + // Limits could be moved to another function, but letting them stay here serves us a monopolized view of notify + if (config.maxKeyLength != kCountlyMaxKeyLength) + { + [config.sdkInternalLimits setMaxKeyLength:config.maxKeyLength]; + } + + if (config.maxValueLength != kCountlyMaxValueSize) + { + [config.sdkInternalLimits setMaxValueSize:config.maxValueLength]; + } + + if (config.maxSegmentationValues != kCountlyMaxSegmentationValues) + { + [config.sdkInternalLimits setMaxSegmentationValues:config.maxSegmentationValues]; + } + + if (config.crashLogLimit != kCountlyMaxBreadcrumbCount) + { + [config.sdkInternalLimits setMaxBreadcrumbCount:config.crashLogLimit]; + } + + [config.sdkInternalLimits setMaxKeyLength:_limitKeyLength ?: config.sdkInternalLimits.getMaxKeyLength]; + [config.sdkInternalLimits setMaxValueSize:_limitValueSize ?: config.sdkInternalLimits.getMaxValueSize]; + [config.sdkInternalLimits setMaxSegmentationValues:_limitSegValues ?: config.sdkInternalLimits.getMaxSegmentationValues]; + [config.sdkInternalLimits setMaxBreadcrumbCount:_limitBreadcrumb ?: config.sdkInternalLimits.getMaxBreadcrumbCount]; + [config.sdkInternalLimits setMaxStackTraceLineLength:_limitTraceLength ?: config.sdkInternalLimits.getMaxStackTraceLineLength]; + [config.sdkInternalLimits setMaxStackTraceLinesPerThread:_limitTraceLine ?: config.sdkInternalLimits.getMaxStackTraceLinesPerThread]; + + CountlyCommon.sharedInstance.maxKeyLength = config.sdkInternalLimits.getMaxKeyLength; + CountlyCommon.sharedInstance.maxValueLength = config.sdkInternalLimits.getMaxValueSize; + CountlyCommon.sharedInstance.maxSegmentationValues = config.sdkInternalLimits.getMaxSegmentationValues; + + config.eventSendThreshold = _eventQueueSize ?: config.eventSendThreshold; + config.requestDropAgeHours = _dropOldRequestTime ?: config.requestDropAgeHours; + config.storedRequestsLimit = _requestQueueSize ?: config.storedRequestsLimit; + CountlyPersistency.sharedInstance.eventSendThreshold = config.eventSendThreshold; + CountlyPersistency.sharedInstance.requestDropAgeHours = config.requestDropAgeHours; + CountlyPersistency.sharedInstance.storedRequestsLimit = MAX(1, config.storedRequestsLimit); + + config.updateSessionPeriod = _sessionInterval ?: config.updateSessionPeriod; + _sessionInterval = config.updateSessionPeriod; + + BOOL consentDidChange = !config.requiresConsent && _consentRequired; + config.requiresConsent = _consentRequired ?: config.requiresConsent; + CountlyConsentManager.sharedInstance.requiresConsent = config.requiresConsent; + if (consentDidChange && CountlyCommon.sharedInstance.hasFinishedInit) + { + [CountlyConsentManager.sharedInstance sendConsents]; + if (!CountlyConsentManager.sharedInstance.consentForLocation) { - [config.sdkInternalLimits setMaxBreadcrumbCount:config.crashLogLimit]; - } - - [config.sdkInternalLimits setMaxKeyLength:_limitKeyLength ?: config.sdkInternalLimits.getMaxKeyLength]; - [config.sdkInternalLimits setMaxValueSize:_limitValueSize ?: config.sdkInternalLimits.getMaxValueSize]; - [config.sdkInternalLimits setMaxSegmentationValues:_limitSegValues ?: config.sdkInternalLimits.getMaxSegmentationValues]; - [config.sdkInternalLimits setMaxBreadcrumbCount:_limitBreadcrumb ?: config.sdkInternalLimits.getMaxBreadcrumbCount]; - [config.sdkInternalLimits setMaxStackTraceLineLength:_limitTraceLength ?: config.sdkInternalLimits.getMaxStackTraceLineLength]; - [config.sdkInternalLimits setMaxStackTraceLinesPerThread:_limitTraceLine ?: config.sdkInternalLimits.getMaxStackTraceLinesPerThread]; - - CountlyCommon.sharedInstance.maxKeyLength = config.sdkInternalLimits.getMaxKeyLength; - CountlyCommon.sharedInstance.maxValueLength = config.sdkInternalLimits.getMaxValueSize; - CountlyCommon.sharedInstance.maxSegmentationValues = config.sdkInternalLimits.getMaxSegmentationValues; - - config.eventSendThreshold = _eventQueueSize ?: config.eventSendThreshold; - config.requestDropAgeHours = _dropOldRequestTime ?: config.requestDropAgeHours; - config.storedRequestsLimit = _requestQueueSize ?: config.storedRequestsLimit; - CountlyPersistency.sharedInstance.eventSendThreshold = config.eventSendThreshold; - CountlyPersistency.sharedInstance.requestDropAgeHours = config.requestDropAgeHours; - CountlyPersistency.sharedInstance.storedRequestsLimit = MAX(1, config.storedRequestsLimit); - - config.updateSessionPeriod = _sessionInterval ?: config.updateSessionPeriod; - _sessionInterval = config.updateSessionPeriod; - - BOOL consentDidChange = !config.requiresConsent && _consentRequired; - config.requiresConsent = _consentRequired ?: config.requiresConsent; - CountlyConsentManager.sharedInstance.requiresConsent = config.requiresConsent; - if(consentDidChange){ - [CountlyConsentManager.sharedInstance sendConsents]; - if (!CountlyConsentManager.sharedInstance.consentForLocation) - { - [CountlyConnectionManager.sharedInstance sendLocationInfo]; - } + [CountlyConnectionManager.sharedInstance sendLocationInfo]; } + } #if (TARGET_OS_IOS) - [config.content setZoneTimerInterval:_contentZoneInterval ?: config.content.getZoneTimerInterval]; - if (config.content.getZoneTimerInterval) - { - CountlyContentBuilderInternal.sharedInstance.zoneTimerInterval = config.content.getZoneTimerInterval; - } - if (!_enterContentZone) - { - dispatch_async(dispatch_get_main_queue(), ^{ - [CountlyContentBuilderInternal.sharedInstance exitContentZone]; - }); - } - else - { - dispatch_async(dispatch_get_main_queue(), ^{ - [CountlyContentBuilderInternal.sharedInstance exitContentZone]; - [CountlyContentBuilderInternal.sharedInstance enterContentZone:@[]]; - }); - } + [config.content setZoneTimerInterval:_contentZoneInterval ?: config.content.getZoneTimerInterval]; + if (config.content.getZoneTimerInterval) + { + CountlyContentBuilderInternal.sharedInstance.zoneTimerInterval = config.content.getZoneTimerInterval; + } + if (!_enterContentZone) + { + dispatch_async(dispatch_get_main_queue(), ^{ + [CountlyContentBuilderInternal.sharedInstance exitContentZone]; + }); + } + else + { + dispatch_async(dispatch_get_main_queue(), ^{ + [CountlyContentBuilderInternal.sharedInstance exitContentZone]; + [CountlyContentBuilderInternal.sharedInstance enterContentZone:@[]]; + }); + } #endif - CountlyCrashReporter.sharedInstance.crashLogLimit = config.sdkInternalLimits.getMaxBreadcrumbCount; + CountlyCrashReporter.sharedInstance.crashLogLimit = config.sdkInternalLimits.getMaxBreadcrumbCount; - if (_serverConfigUpdateInterval && _serverConfigUpdateInterval != _currentServerConfigUpdateInterval && _requestTimer) - { - _currentServerConfigUpdateInterval = _serverConfigUpdateInterval; - [_requestTimer invalidate]; - _requestTimer = nil; - _requestTimer = [NSTimer timerWithTimeInterval:_currentServerConfigUpdateInterval * 60 * 60 target:self selector:@selector(fetchServerConfigTimer:) userInfo:config repeats:YES]; - [NSRunLoop.mainRunLoop addTimer:_requestTimer forMode:NSRunLoopCommonModes]; - } + if (_serverConfigUpdateInterval && _serverConfigUpdateInterval != _currentServerConfigUpdateInterval && _requestTimer) + { + _currentServerConfigUpdateInterval = _serverConfigUpdateInterval; + [_requestTimer invalidate]; + _requestTimer = nil; + _requestTimer = [NSTimer timerWithTimeInterval:_currentServerConfigUpdateInterval * 60 * 60 target:self selector:@selector(fetchServerConfigTimer:) userInfo:config repeats:YES]; + [NSRunLoop.mainRunLoop addTimer:_requestTimer forMode:NSRunLoopCommonModes]; + } - if (!_locationTracking && !CountlyLocationManager.sharedInstance.isLocationInfoDisabled) - { - [CountlyLocationManager.sharedInstance disableLocation]; - [CountlyConnectionManager.sharedInstance sendLocationInfo]; - } - - if(_backoffMechanism && config.disableBackoffMechanism){ - _backoffMechanism = NO; - } + if (!_locationTracking && !CountlyLocationManager.sharedInstance.isLocationInfoDisabled) + { + [CountlyLocationManager.sharedInstance disableLocation]; + [CountlyConnectionManager.sharedInstance sendLocationInfo]; + } + + if (_backoffMechanism && config.disableBackoffMechanism) + { + _backoffMechanism = NO; + } } - (void)fetchServerConfigTimer:(NSTimer *)timer { - CountlyConfig *config = (CountlyConfig *)timer.userInfo; // Retrieve CountlyConfig from userInfo - if (config) - { - [self fetchServerConfig:config]; - } + CountlyConfig *config = (CountlyConfig *)timer.userInfo; // Retrieve CountlyConfig from userInfo + if (config) + { + [self fetchServerConfig:config]; + } } - (void)fetchServerConfigIfTimeIsUp { - if (_serverConfigUpdatesDisabled) { - return; - } - - if (_lastFetchTimestamp) - { - long long currentTime = NSDate.date.timeIntervalSince1970 * 1000; - long long timePassed = currentTime - _lastFetchTimestamp; + if (_serverConfigUpdatesDisabled) + { + return; + } - if (timePassed > _currentServerConfigUpdateInterval * 60 * 60 * 1000) - { - [self fetchServerConfig:CountlyConfig.new]; - } + if (_lastFetchTimestamp) + { + long long currentTime = NSDate.date.timeIntervalSince1970 * 1000; + long long timePassed = currentTime - _lastFetchTimestamp; + + if (timePassed > _currentServerConfigUpdateInterval * 60 * 60 * 1000) + { + [self fetchServerConfig:CountlyConfig.new]; } + } } - (void)fetchServerConfig:(CountlyConfig *)config { - CLY_LOG_D(@"%s, fetching sdk behavior settings", __FUNCTION__); - - if (_serverConfigUpdatesDisabled) { - CLY_LOG_D(@"%s, sdk behavior settings updates disabled, omitting fetch", __FUNCTION__); - return; - } - - if (CountlyDeviceInfo.sharedInstance.isDeviceIDTemporary) - return; + CLY_LOG_D(@"%s, fetching sdk behavior settings", __FUNCTION__); - _lastFetchTimestamp = NSDate.date.timeIntervalSince1970 * 1000; + if (_serverConfigUpdatesDisabled) + { + CLY_LOG_D(@"%s, sdk behavior settings updates disabled, omitting fetch", __FUNCTION__); + return; + } - if (!_requestTimer) - { - _requestTimer = [NSTimer timerWithTimeInterval:_currentServerConfigUpdateInterval * 60 * 60 target:self selector:@selector(fetchServerConfigTimer:) userInfo:config repeats:YES]; - [NSRunLoop.mainRunLoop addTimer:_requestTimer forMode:NSRunLoopCommonModes]; - } - - id handler = ^(NSData *data, NSURLResponse *response, NSError *error) { - NSDictionary *serverConfigResponse = nil; - if (!error) - { - serverConfigResponse = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error]; - CLY_LOG_D(@"Server Config Fetched: %@", serverConfigResponse.description); - } - - if (!error) - { - if (((NSHTTPURLResponse *)response).statusCode != 200) - { - NSMutableDictionary *serverConfig = serverConfigResponse.mutableCopy; - serverConfig[NSLocalizedDescriptionKey] = @"Server configuration general API error"; - error = [NSError errorWithDomain:kCountlyErrorDomain code:CLYErrorServerConfigGeneralAPIError userInfo:serverConfig]; - } - } - - if (error) - { - CLY_LOG_E(@"Error while fetching server configs: %@", error.description); - } - - if (serverConfigResponse[kRConfig] != nil) - { - NSMutableDictionary *persistentBehaviorSettings = [CountlyPersistency.sharedInstance retrieveServerConfig]; - [self mergeBehaviorSettings:persistentBehaviorSettings withConfig:serverConfigResponse]; - [CountlyPersistency.sharedInstance storeServerConfig:persistentBehaviorSettings]; - [self populateServerConfig:persistentBehaviorSettings withConfig:config]; - } - }; - // Set default values - NSURLSessionTask *task = [CountlyCommon.sharedInstance.URLSession dataTaskWithRequest:[self serverConfigRequest] completionHandler:handler]; - [task resume]; -} - -- (NSURLRequest *)serverConfigRequest -{ - NSString *queryString = [NSString stringWithFormat:@"%@=%@&%@=%@&%@=%@&%@=%@&%@=%@", kCountlyQSKeyMethod, kCountlySCKeySC, kCountlyQSKeyAppKey, CountlyConnectionManager.sharedInstance.appKey.cly_URLEscaped, kCountlyQSKeyDeviceID, CountlyDeviceInfo.sharedInstance.deviceID.cly_URLEscaped, - kCountlyQSKeySDKName, CountlyCommon.sharedInstance.SDKName, kCountlyQSKeySDKVersion, CountlyCommon.sharedInstance.SDKVersion]; + if (CountlyDeviceInfo.sharedInstance.isDeviceIDTemporary) + return; - queryString = [queryString stringByAppendingFormat:@"&%@=%@", kCountlyAppVersionKey, CountlyDeviceInfo.appVersion]; + _lastFetchTimestamp = NSDate.date.timeIntervalSince1970 * 1000; - queryString = [CountlyConnectionManager.sharedInstance appendChecksum:queryString]; + if (!_requestTimer) + { + _requestTimer = [NSTimer timerWithTimeInterval:_currentServerConfigUpdateInterval * 60 * 60 target:self selector:@selector(fetchServerConfigTimer:) userInfo:config repeats:YES]; + [NSRunLoop.mainRunLoop addTimer:_requestTimer forMode:NSRunLoopCommonModes]; + } - NSMutableString *URL = CountlyConnectionManager.sharedInstance.host.mutableCopy; - [URL appendString:kCountlyEndpointO]; - [URL appendString:kCountlyEndpointSDK]; + id handler = ^(NSData *data, NSURLResponse *response, NSError *error) { + NSDictionary *serverConfigResponse = nil; + if (!error) + { + serverConfigResponse = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error]; + CLY_LOG_D(@"Server Config Fetched: %@", serverConfigResponse.description); + } - if (queryString.length > kCountlyGETRequestMaxLength || CountlyConnectionManager.sharedInstance.alwaysUsePOST) + if (!error) { - NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:URL]]; - request.HTTPMethod = @"POST"; - request.HTTPBody = [queryString cly_dataUTF8]; - return request.copy; + if (((NSHTTPURLResponse *)response).statusCode != 200) + { + NSMutableDictionary *serverConfig = serverConfigResponse.mutableCopy; + serverConfig[NSLocalizedDescriptionKey] = @"Server configuration general API error"; + error = [NSError errorWithDomain:kCountlyErrorDomain code:CLYErrorServerConfigGeneralAPIError userInfo:serverConfig]; + } } - else + + if (error) { - [URL appendFormat:@"?%@", queryString]; - NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:URL]]; - return request; + CLY_LOG_E(@"Error while fetching server configs: %@", error.description); } - CLY_LOG_D(@"serverConfigRequest URL :%@", URL); -} - -- (void)setDefaultValues { - _trackingEnabled = YES; - _networkingEnabled = YES; - _crashReportingEnabled = YES; - _customEventTrackingEnabled = YES; - _enterContentZone = NO; - _locationTracking = YES; - _viewTrackingEnabled = YES; - _sessionTrackingEnabled = YES; - _loggingEnabled = NO; - _refreshContentZone = YES; - _backoffMechanism = YES; - _bomAcceptedTimeoutSeconds = 10; - _bomRQPercentage = 0.5; - _bomRequestAge = 24; - _bomDuration = 60; - - _eventQueueSize = 100; - _requestQueueSize = 1000; - _sessionInterval = 60; - _limitKeyLength = 128; - _limitValueSize = 256; - _limitSegValues = 100; - _limitBreadcrumb = 100; - _limitTraceLine = 200; - _limitTraceLength = 30; - _consentRequired = NO; - _dropOldRequestTime = 0; - _contentZoneInterval = 30; - - _eventFilterSet = [NSSet set]; - _eventFilterIsWhitelist = NO; - _userPropertyFilterSet = [NSSet set]; - _userPropertyFilterIsWhitelist = NO; - _userPropertyCacheLimit = 100; - _segmentationFilterSet = [NSSet set]; - _segmentationFilterIsWhitelist = NO; - _eventSegmentationFilterMap = @{}; - _eventSegmentationFilterIsWhitelist = NO; - _journeyTriggerEvents = [NSSet set]; + if (serverConfigResponse[kRConfig] != nil) + { + NSMutableDictionary *persistentBehaviorSettings = [CountlyPersistency.sharedInstance retrieveServerConfig]; + [self mergeBehaviorSettings:persistentBehaviorSettings withConfig:serverConfigResponse]; + [CountlyPersistency.sharedInstance storeServerConfig:persistentBehaviorSettings]; + [self populateServerConfig:persistentBehaviorSettings withConfig:config]; + } + }; + // Set default values + NSURLSessionTask *task = [CountlyCommon.sharedInstance.URLSession dataTaskWithRequest:[self serverConfigRequest] completionHandler:handler]; + [task resume]; } -- (void)disableSDKBehaviourSettings { - _serverConfigUpdatesDisabled = YES; +- (NSURLRequest *)serverConfigRequest +{ + NSString *queryString = [NSString stringWithFormat:@"%@=%@&%@=%@&%@=%@&%@=%@&%@=%@", kCountlyQSKeyMethod, kCountlySCKeySC, kCountlyQSKeyAppKey, CountlyConnectionManager.sharedInstance.appKey.cly_URLEscaped, kCountlyQSKeyDeviceID, CountlyDeviceInfo.sharedInstance.deviceID.cly_URLEscaped, + kCountlyQSKeySDKName, CountlyCommon.sharedInstance.SDKName, kCountlyQSKeySDKVersion, CountlyCommon.sharedInstance.SDKVersion]; + + queryString = [queryString stringByAppendingFormat:@"&%@=%@", kCountlyAppVersionKey, CountlyDeviceInfo.appVersion]; + + queryString = [CountlyConnectionManager.sharedInstance appendChecksum:queryString]; + + NSMutableString *URL = CountlyConnectionManager.sharedInstance.host.mutableCopy; + [URL appendString:kCountlyEndpointO]; + [URL appendString:kCountlyEndpointSDK]; + + if (queryString.length > kCountlyGETRequestMaxLength || CountlyConnectionManager.sharedInstance.alwaysUsePOST) + { + NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:URL]]; + request.HTTPMethod = @"POST"; + request.HTTPBody = [queryString cly_dataUTF8]; + return request.copy; + } + else + { + [URL appendFormat:@"?%@", queryString]; + NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:URL]]; + return request; + } + + CLY_LOG_D(@"serverConfigRequest URL :%@", URL); +} + +- (void)setDefaultValues +{ + _trackingEnabled = YES; + _networkingEnabled = YES; + _crashReportingEnabled = YES; + _customEventTrackingEnabled = YES; + _enterContentZone = NO; + _locationTracking = YES; + _viewTrackingEnabled = YES; + _sessionTrackingEnabled = YES; + _loggingEnabled = NO; + _refreshContentZone = YES; + _backoffMechanism = YES; + _bomAcceptedTimeoutSeconds = 10; + _bomRQPercentage = 0.5; + _bomRequestAge = 24; + _bomDuration = 60; + + _eventQueueSize = 0; + _requestQueueSize = 0; + _sessionInterval = 0; + _limitKeyLength = 0; + _limitValueSize = 0; + _limitSegValues = 0; + _limitBreadcrumb = 0; + _limitTraceLine = 0; + _limitTraceLength = 0; + _consentRequired = NO; + _dropOldRequestTime = 0; + _contentZoneInterval = 0; + + _eventFilterSet = [NSSet set]; + _eventFilterIsWhitelist = NO; + _userPropertyFilterSet = [NSSet set]; + _userPropertyFilterIsWhitelist = NO; + _userPropertyCacheLimit = 100; + _segmentationFilterSet = [NSSet set]; + _segmentationFilterIsWhitelist = NO; + _eventSegmentationFilterMap = @{}; + _eventSegmentationFilterIsWhitelist = NO; + _journeyTriggerEvents = [NSSet set]; +} + +- (void)disableSDKBehaviourSettings +{ + _serverConfigUpdatesDisabled = YES; } - (BOOL)trackingEnabled { - return _trackingEnabled; + return _trackingEnabled; } - (BOOL)networkingEnabled { - return _networkingEnabled; + return _networkingEnabled; } - (NSInteger)sessionInterval { - return _sessionInterval; + return _sessionInterval; } - (NSInteger)requestQueueSize { - return _requestQueueSize; + return _requestQueueSize; } - (NSInteger)eventQueueSize { - return _eventQueueSize; + return _eventQueueSize; } - (BOOL)crashReportingEnabled { - return _crashReportingEnabled; + return _crashReportingEnabled; } - (BOOL)sessionTrackingEnabled { - return _sessionTrackingEnabled; + return _sessionTrackingEnabled; } - (BOOL)loggingEnabled { - return _loggingEnabled; + return _loggingEnabled; } - (NSInteger)limitKeyLength { - return _limitKeyLength; + return _limitKeyLength; } - (NSInteger)limitValueSize { - return _limitValueSize; + return _limitValueSize; } - (NSInteger)limitSegValues { - return _limitSegValues; + return _limitSegValues; } - (NSInteger)limitBreadcrumb { - return _limitBreadcrumb; + return _limitBreadcrumb; } - (NSInteger)limitTraceLine { - return _limitTraceLine; + return _limitTraceLine; } - (NSInteger)limitTraceLength { - return _limitTraceLength; + return _limitTraceLength; } - (BOOL)customEventTrackingEnabled { - return _customEventTrackingEnabled; + return _customEventTrackingEnabled; } - (BOOL)viewTrackingEnabled { - return _viewTrackingEnabled; + return _viewTrackingEnabled; } - (BOOL)enterContentZone { - return _enterContentZone; + return _enterContentZone; } - (NSInteger)contentZoneInterval { - return _contentZoneInterval; + return _contentZoneInterval; } - (BOOL)consentRequired { - return _consentRequired; + return _consentRequired; } - (NSInteger)dropOldRequestTime { - return _dropOldRequestTime; + return _dropOldRequestTime; } - (BOOL)locationTrackingEnabled { - return _locationTracking; + return _locationTracking; } - (BOOL)refreshContentZoneEnabled { - return _refreshContentZone; + return _refreshContentZone; } - (BOOL)backoffMechanism { - return _backoffMechanism; + return _backoffMechanism; } - (NSInteger)bomAcceptedTimeoutSeconds { - return _bomAcceptedTimeoutSeconds; + return _bomAcceptedTimeoutSeconds; } - (double)bomRQPercentage { - return _bomRQPercentage; + return _bomRQPercentage; } - (NSInteger)bomRequestAge { - return _bomRequestAge; + return _bomRequestAge; } - (NSInteger)bomDuration { - return _bomDuration; + return _bomDuration; } - (NSInteger)requestTimeoutDuration { - return _requestTimeoutDuration; + return _requestTimeoutDuration; } - (NSInteger)userPropertyCacheLimit { - return _userPropertyCacheLimit; + return _userPropertyCacheLimit; } #pragma mark - Listing Filters - (void)removeConflictingFilterKeys:(NSMutableDictionary *)mergedConfig newConfig:(NSDictionary *)newConfig { - // Remove listing filter keys from stored config based on new config - // If new config has any whitelist key, remove all blacklist keys from stored config - // If new config has any blacklist key, remove all whitelist keys from stored config - NSArray *whitelistKeys = @[kREventWhitelist, kRSegmentationWhitelist, kREventSegmentationWhitelist, kRUserPropertyWhitelist]; - NSArray *blacklistKeys = @[kREventBlacklist, kRSegmentationBlacklist, kREventSegmentationBlacklist, kRUserPropertyBlacklist]; - - BOOL newHasWhitelist = NO; - BOOL newHasBlacklist = NO; - for (NSString *key in whitelistKeys) + // Remove listing filter keys from stored config based on new config + // If new config has any whitelist key, remove all blacklist keys from stored config + // If new config has any blacklist key, remove all whitelist keys from stored config + NSArray *whitelistKeys = @[ kREventWhitelist, kRSegmentationWhitelist, kREventSegmentationWhitelist, kRUserPropertyWhitelist ]; + NSArray *blacklistKeys = @[ kREventBlacklist, kRSegmentationBlacklist, kREventSegmentationBlacklist, kRUserPropertyBlacklist ]; + + BOOL newHasWhitelist = NO; + BOOL newHasBlacklist = NO; + for (NSString *key in whitelistKeys) + { + if (newConfig[key]) { - if (newConfig[key]) { newHasWhitelist = YES; break; } + newHasWhitelist = YES; + break; } - for (NSString *key in blacklistKeys) + } + for (NSString *key in blacklistKeys) + { + if (newConfig[key]) { - if (newConfig[key]) { newHasBlacklist = YES; break; } + newHasBlacklist = YES; + break; } + } - if (newHasWhitelist) + if (newHasWhitelist) + { + for (NSString *key in blacklistKeys) { - for (NSString *key in blacklistKeys) - { - [mergedConfig removeObjectForKey:key]; - } + [mergedConfig removeObjectForKey:key]; } - if (newHasBlacklist) + } + if (newHasBlacklist) + { + for (NSString *key in whitelistKeys) { - for (NSString *key in whitelistKeys) - { - [mergedConfig removeObjectForKey:key]; - } + [mergedConfig removeObjectForKey:key]; } + } } - (void)updateListingFilters:(NSDictionary *)dictionary logString:(NSMutableString *)logString { - // Event filter (eb/ew) - blacklist takes precedence - NSArray *eb = dictionary[kREventBlacklist]; - NSArray *ew = dictionary[kREventWhitelist]; - if ([eb isKindOfClass:NSArray.class]) { - _eventFilterSet = [NSSet setWithArray:eb]; - _eventFilterIsWhitelist = NO; - [logString appendFormat:@"%@: %@, ", kREventBlacklist, eb]; - } else if ([ew isKindOfClass:NSArray.class]) { - _eventFilterSet = [NSSet setWithArray:ew]; - _eventFilterIsWhitelist = YES; - [logString appendFormat:@"%@: %@, ", kREventWhitelist, ew]; - } - - // User property filter (upb/upw) - blacklist takes precedence - NSArray *upb = dictionary[kRUserPropertyBlacklist]; - NSArray *upw = dictionary[kRUserPropertyWhitelist]; - if ([upb isKindOfClass:NSArray.class]) { - _userPropertyFilterSet = [NSSet setWithArray:upb]; - _userPropertyFilterIsWhitelist = NO; - [logString appendFormat:@"%@: %@, ", kRUserPropertyBlacklist, upb]; - } else if ([upw isKindOfClass:NSArray.class]) { - _userPropertyFilterSet = [NSSet setWithArray:upw]; - _userPropertyFilterIsWhitelist = YES; - [logString appendFormat:@"%@: %@, ", kRUserPropertyWhitelist, upw]; - } - - // Segmentation filter (sb/sw) - blacklist takes precedence - NSArray *sb = dictionary[kRSegmentationBlacklist]; - NSArray *sw = dictionary[kRSegmentationWhitelist]; - if ([sb isKindOfClass:NSArray.class]) { - _segmentationFilterSet = [NSSet setWithArray:sb]; - _segmentationFilterIsWhitelist = NO; - [logString appendFormat:@"%@: %@, ", kRSegmentationBlacklist, sb]; - } else if ([sw isKindOfClass:NSArray.class]) { - _segmentationFilterSet = [NSSet setWithArray:sw]; - _segmentationFilterIsWhitelist = YES; - [logString appendFormat:@"%@: %@, ", kRSegmentationWhitelist, sw]; - } - - // Event segmentation filter (esb/esw) - blacklist takes precedence - NSDictionary *esb = dictionary[kREventSegmentationBlacklist]; - NSDictionary *esw = dictionary[kREventSegmentationWhitelist]; - if ([esb isKindOfClass:NSDictionary.class]) { - NSMutableDictionary *map = NSMutableDictionary.new; - [esb enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSArray *obj, BOOL *stop) { - if ([obj isKindOfClass:NSArray.class]) { - map[key] = [NSSet setWithArray:obj]; - } - }]; - _eventSegmentationFilterMap = map.copy; - _eventSegmentationFilterIsWhitelist = NO; - [logString appendFormat:@"%@: %@, ", kREventSegmentationBlacklist, esb]; - } else if ([esw isKindOfClass:NSDictionary.class]) { - NSMutableDictionary *map = NSMutableDictionary.new; - [esw enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSArray *obj, BOOL *stop) { - if ([obj isKindOfClass:NSArray.class]) { - map[key] = [NSSet setWithArray:obj]; - } - }]; - _eventSegmentationFilterMap = map.copy; - _eventSegmentationFilterIsWhitelist = YES; - [logString appendFormat:@"%@: %@, ", kREventSegmentationWhitelist, esw]; - } - - // Journey trigger events (jte) - NSArray *jte = dictionary[kRJourneyTriggerEvents]; - if ([jte isKindOfClass:NSArray.class]) { - _journeyTriggerEvents = [NSSet setWithArray:jte]; - [logString appendFormat:@"%@: %@, ", kRJourneyTriggerEvents, jte]; - } + // Event filter (eb/ew) - blacklist takes precedence + NSArray *eb = dictionary[kREventBlacklist]; + NSArray *ew = dictionary[kREventWhitelist]; + if ([eb isKindOfClass:NSArray.class]) + { + _eventFilterSet = [NSSet setWithArray:eb]; + _eventFilterIsWhitelist = NO; + [logString appendFormat:@"%@: %@, ", kREventBlacklist, eb]; + } + else if ([ew isKindOfClass:NSArray.class]) + { + _eventFilterSet = [NSSet setWithArray:ew]; + _eventFilterIsWhitelist = YES; + [logString appendFormat:@"%@: %@, ", kREventWhitelist, ew]; + } + + // User property filter (upb/upw) - blacklist takes precedence + NSArray *upb = dictionary[kRUserPropertyBlacklist]; + NSArray *upw = dictionary[kRUserPropertyWhitelist]; + if ([upb isKindOfClass:NSArray.class]) + { + _userPropertyFilterSet = [NSSet setWithArray:upb]; + _userPropertyFilterIsWhitelist = NO; + [logString appendFormat:@"%@: %@, ", kRUserPropertyBlacklist, upb]; + } + else if ([upw isKindOfClass:NSArray.class]) + { + _userPropertyFilterSet = [NSSet setWithArray:upw]; + _userPropertyFilterIsWhitelist = YES; + [logString appendFormat:@"%@: %@, ", kRUserPropertyWhitelist, upw]; + } + + // Segmentation filter (sb/sw) - blacklist takes precedence + NSArray *sb = dictionary[kRSegmentationBlacklist]; + NSArray *sw = dictionary[kRSegmentationWhitelist]; + if ([sb isKindOfClass:NSArray.class]) + { + _segmentationFilterSet = [NSSet setWithArray:sb]; + _segmentationFilterIsWhitelist = NO; + [logString appendFormat:@"%@: %@, ", kRSegmentationBlacklist, sb]; + } + else if ([sw isKindOfClass:NSArray.class]) + { + _segmentationFilterSet = [NSSet setWithArray:sw]; + _segmentationFilterIsWhitelist = YES; + [logString appendFormat:@"%@: %@, ", kRSegmentationWhitelist, sw]; + } + + // Event segmentation filter (esb/esw) - blacklist takes precedence + NSDictionary *esb = dictionary[kREventSegmentationBlacklist]; + NSDictionary *esw = dictionary[kREventSegmentationWhitelist]; + if ([esb isKindOfClass:NSDictionary.class]) + { + NSMutableDictionary *map = NSMutableDictionary.new; + [esb enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSArray *obj, BOOL *stop) { + if ([obj isKindOfClass:NSArray.class]) + { + map[key] = [NSSet setWithArray:obj]; + } + }]; + _eventSegmentationFilterMap = map.copy; + _eventSegmentationFilterIsWhitelist = NO; + [logString appendFormat:@"%@: %@, ", kREventSegmentationBlacklist, esb]; + } + else if ([esw isKindOfClass:NSDictionary.class]) + { + NSMutableDictionary *map = NSMutableDictionary.new; + [esw enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSArray *obj, BOOL *stop) { + if ([obj isKindOfClass:NSArray.class]) + { + map[key] = [NSSet setWithArray:obj]; + } + }]; + _eventSegmentationFilterMap = map.copy; + _eventSegmentationFilterIsWhitelist = YES; + [logString appendFormat:@"%@: %@, ", kREventSegmentationWhitelist, esw]; + } + + // Journey trigger events (jte) + NSArray *jte = dictionary[kRJourneyTriggerEvents]; + if ([jte isKindOfClass:NSArray.class]) + { + _journeyTriggerEvents = [NSSet setWithArray:jte]; + [logString appendFormat:@"%@: %@, ", kRJourneyTriggerEvents, jte]; + } } - (BOOL)shouldRecordEvent:(NSString *)eventKey { - if (_eventFilterSet.count == 0) return YES; - return _eventFilterIsWhitelist == [_eventFilterSet containsObject:eventKey]; + if (_eventFilterSet.count == 0) + return YES; + return _eventFilterIsWhitelist == [_eventFilterSet containsObject:eventKey]; } - (BOOL)shouldRecordUserProperty:(NSString *)propertyKey { - if (_userPropertyFilterSet.count == 0) return YES; - return _userPropertyFilterIsWhitelist == [_userPropertyFilterSet containsObject:propertyKey]; + if (_userPropertyFilterSet.count == 0) + return YES; + return _userPropertyFilterIsWhitelist == [_userPropertyFilterSet containsObject:propertyKey]; } - (NSDictionary *)filterSegmentation:(NSDictionary *)segmentation eventKey:(NSString *)eventKey { - if (!segmentation) { - return segmentation; - } + if (!segmentation) + { + return segmentation; + } - BOOL hasGlobalFilter = _segmentationFilterSet.count > 0; - NSSet *eventFilter = _eventSegmentationFilterMap[eventKey]; - BOOL hasEventFilter = eventFilter.count > 0; + BOOL hasGlobalFilter = _segmentationFilterSet.count > 0; + NSSet *eventFilter = _eventSegmentationFilterMap[eventKey]; + BOOL hasEventFilter = eventFilter.count > 0; - if (!hasGlobalFilter && !hasEventFilter) { - return segmentation; - } + if (!hasGlobalFilter && !hasEventFilter) + { + return segmentation; + } - NSMutableDictionary *result = [segmentation mutableCopy]; - for (NSString *key in segmentation.allKeys) { - if (hasGlobalFilter && _segmentationFilterIsWhitelist != [_segmentationFilterSet containsObject:key]) { - CLY_LOG_D(@"Filtering out segmentation key '%@' by global segmentation filter", key); - [result removeObjectForKey:key]; - } - else if (hasEventFilter && _eventSegmentationFilterIsWhitelist != [eventFilter containsObject:key]) { - CLY_LOG_D(@"Filtering out segmentation key '%@' for event '%@' by event segmentation filter", key, eventKey); - [result removeObjectForKey:key]; - } + NSMutableDictionary *result = [segmentation mutableCopy]; + for (NSString *key in segmentation.allKeys) + { + if (hasGlobalFilter && _segmentationFilterIsWhitelist != [_segmentationFilterSet containsObject:key]) + { + CLY_LOG_D(@"Filtering out segmentation key '%@' by global segmentation filter", key); + [result removeObjectForKey:key]; + } + else if (hasEventFilter && _eventSegmentationFilterIsWhitelist != [eventFilter containsObject:key]) + { + CLY_LOG_D(@"Filtering out segmentation key '%@' for event '%@' by event segmentation filter", key, eventKey); + [result removeObjectForKey:key]; } + } - return result.copy; + return result.copy; } - (BOOL)isJourneyTriggerEvent:(NSString *)eventKey { - return [_journeyTriggerEvents containsObject:eventKey]; + return [_journeyTriggerEvents containsObject:eventKey]; } @end From 2b6a52d0182d6e8ee0565b4a2cae5ebbbc2892af Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Mon, 6 Apr 2026 12:25:59 +0300 Subject: [PATCH 13/42] fix: support parallel runs --- CountlyConnectionManager.m | 2041 ++++++++++++++++++------------------ 1 file changed, 1033 insertions(+), 1008 deletions(-) diff --git a/CountlyConnectionManager.m b/CountlyConnectionManager.m index 4e9d5a45..4d43f84e 100644 --- a/CountlyConnectionManager.m +++ b/CountlyConnectionManager.m @@ -7,1416 +7,1441 @@ #import "CountlyCommon.h" #import -@interface CountlyConnectionManager () -{ - NSTimeInterval unsentSessionLength; - NSTimeInterval lastSessionStartTime; - BOOL isCrashing; - BOOL isSessionStarted; +@interface CountlyConnectionManager () { + NSTimeInterval unsentSessionLength; + NSTimeInterval lastSessionStartTime; + BOOL isCrashing; + BOOL isSessionStarted; } -@property (nonatomic) NSURLSession* URLSession; +@property(nonatomic) NSURLSession *URLSession; -@property (nonatomic, strong) NSDate *startTime; -@property (nonatomic, assign) atomic_bool backoff; -@property (nonatomic, strong) NSMutableDictionary *internalRequestCallbacks; -@property (nonatomic, strong) NSMutableArray *queueFlushRunnables; -@property (nonatomic) BOOL hasAnyRequestFailed; -@property (nonatomic, strong) dispatch_queue_t callbackQueue; // Serial queue for thread-safe callback/runnable access +@property(nonatomic, strong) NSDate *startTime; +@property(nonatomic, assign) atomic_bool backoff; +@property(nonatomic, assign) atomic_bool isProcessingQueue; +@property(nonatomic, strong) NSMutableDictionary *internalRequestCallbacks; +@property(nonatomic, strong) NSMutableArray *queueFlushRunnables; +@property(nonatomic) BOOL hasAnyRequestFailed; +@property(nonatomic, strong) dispatch_queue_t callbackQueue; // Serial queue for thread-safe callback/runnable access @end -NSString* const kCountlyQSKeyAppKey = @"app_key"; - -NSString* const kCountlyQSKeyDeviceID = @"device_id"; -NSString* const kCountlyQSKeyDeviceIDOld = @"old_device_id"; -NSString* const kCountlyQSKeyDeviceIDType = @"t"; - -NSString* const kCountlyQSKeyTimestamp = @"timestamp"; -NSString* const kCountlyQSKeyTimeZone = @"tz"; -NSString* const kCountlyQSKeyTimeHourOfDay = @"hour"; -NSString* const kCountlyQSKeyTimeDayOfWeek = @"dow"; - -NSString* const kCountlyQSKeySDKVersion = @"sdk_version"; -NSString* const kCountlyQSKeySDKName = @"sdk_name"; - -NSString* const kCountlyQSKeySessionBegin = @"begin_session"; -NSString* const kCountlyQSKeySessionDuration = @"session_duration"; -NSString* const kCountlyQSKeySessionEnd = @"end_session"; - -NSString* const kCountlyQSKeyPushTokenSession = @"token_session"; -NSString* const kCountlyQSKeyPushTokeniOS = @"ios_token"; -NSString* const kCountlyQSKeyPushTestMode = @"test_mode"; - -NSString* const kCountlyQSKeyLocation = @"location"; -NSString* const kCountlyQSKeyLocationCity = @"city"; -NSString* const kCountlyQSKeyLocationCountry = @"country_code"; -NSString* const kCountlyQSKeyLocationIP = @"ip_address"; - -NSString* const kCountlyQSKeyAttributionID = @"aid"; -NSString* const kCountlyQSKeyIDFA = @"idfa"; -NSString* const kCountlyQSKeyADID = @"adid"; -NSString* const kCountlyQSKeyCampaignID = @"campaign_id"; -NSString* const kCountlyQSKeyCampaignUser = @"campaign_user"; -NSString* const kCountlyQSKeyAttributionData = @"attribution_data"; - -NSString* const kCountlyQSKeyMetrics = @"metrics"; -NSString* const kCountlyQSKeyEvents = @"events"; -NSString* const kCountlyQSKeyUserDetails = @"user_details"; -NSString* const kCountlyQSKeyCrash = @"crash"; -NSString* const kCountlyQSKeyChecksum256 = @"checksum256"; -NSString* const kCountlyQSKeyConsent = @"consent"; -NSString* const kCountlyQSKeyAPM = @"apm"; -NSString* const kCountlyQSKeyRemainingRequest = @"rr"; - -NSString* const kCountlyQSKeyMethod = @"method"; - -NSString* const kCountlyRCKeyABOptIn = @"ab"; -NSString* const kCountlyRCKeyABOptOut = @"ab_opt_out"; -NSString* const kCountlyEndPointOverrideTag = @"&new_end_point="; -NSString* const kCountlyNewEndPoint = @"new_end_point"; -NSString* const kCountlyCallbackID = @"callback_id"; +NSString *const kCountlyQSKeyAppKey = @"app_key"; + +NSString *const kCountlyQSKeyDeviceID = @"device_id"; +NSString *const kCountlyQSKeyDeviceIDOld = @"old_device_id"; +NSString *const kCountlyQSKeyDeviceIDType = @"t"; + +NSString *const kCountlyQSKeyTimestamp = @"timestamp"; +NSString *const kCountlyQSKeyTimeZone = @"tz"; +NSString *const kCountlyQSKeyTimeHourOfDay = @"hour"; +NSString *const kCountlyQSKeyTimeDayOfWeek = @"dow"; + +NSString *const kCountlyQSKeySDKVersion = @"sdk_version"; +NSString *const kCountlyQSKeySDKName = @"sdk_name"; + +NSString *const kCountlyQSKeySessionBegin = @"begin_session"; +NSString *const kCountlyQSKeySessionDuration = @"session_duration"; +NSString *const kCountlyQSKeySessionEnd = @"end_session"; + +NSString *const kCountlyQSKeyPushTokenSession = @"token_session"; +NSString *const kCountlyQSKeyPushTokeniOS = @"ios_token"; +NSString *const kCountlyQSKeyPushTestMode = @"test_mode"; + +NSString *const kCountlyQSKeyLocation = @"location"; +NSString *const kCountlyQSKeyLocationCity = @"city"; +NSString *const kCountlyQSKeyLocationCountry = @"country_code"; +NSString *const kCountlyQSKeyLocationIP = @"ip_address"; + +NSString *const kCountlyQSKeyAttributionID = @"aid"; +NSString *const kCountlyQSKeyIDFA = @"idfa"; +NSString *const kCountlyQSKeyADID = @"adid"; +NSString *const kCountlyQSKeyCampaignID = @"campaign_id"; +NSString *const kCountlyQSKeyCampaignUser = @"campaign_user"; +NSString *const kCountlyQSKeyAttributionData = @"attribution_data"; + +NSString *const kCountlyQSKeyMetrics = @"metrics"; +NSString *const kCountlyQSKeyEvents = @"events"; +NSString *const kCountlyQSKeyUserDetails = @"user_details"; +NSString *const kCountlyQSKeyCrash = @"crash"; +NSString *const kCountlyQSKeyChecksum256 = @"checksum256"; +NSString *const kCountlyQSKeyConsent = @"consent"; +NSString *const kCountlyQSKeyAPM = @"apm"; +NSString *const kCountlyQSKeyRemainingRequest = @"rr"; + +NSString *const kCountlyQSKeyMethod = @"method"; + +NSString *const kCountlyRCKeyABOptIn = @"ab"; +NSString *const kCountlyRCKeyABOptOut = @"ab_opt_out"; +NSString *const kCountlyEndPointOverrideTag = @"&new_end_point="; +NSString *const kCountlyNewEndPoint = @"new_end_point"; +NSString *const kCountlyCallbackID = @"callback_id"; CLYAttributionKey const CLYAttributionKeyIDFA = kCountlyQSKeyIDFA; CLYAttributionKey const CLYAttributionKeyADID = kCountlyQSKeyADID; -NSString* const kCountlyUploadBoundary = @"0cae04a8b698d63ff6ea55d168993f21"; +NSString *const kCountlyUploadBoundary = @"0cae04a8b698d63ff6ea55d168993f21"; -NSString* const kCountlyEndpointI = @"/i"; //NOTE: input endpoint -NSString* const kCountlyEndpointO = @"/o"; //NOTE: output endpoint -NSString* const kCountlyEndpointSDK = @"/sdk"; -NSString* const kCountlyEndpointFeedback = @"/feedback"; -NSString* const kCountlyEndpointWidget = @"/widget"; -NSString* const kCountlyEndpointSurveys = @"/surveys"; +NSString *const kCountlyEndpointI = @"/i"; // NOTE: input endpoint +NSString *const kCountlyEndpointO = @"/o"; // NOTE: output endpoint +NSString *const kCountlyEndpointSDK = @"/sdk"; +NSString *const kCountlyEndpointFeedback = @"/feedback"; +NSString *const kCountlyEndpointWidget = @"/widget"; +NSString *const kCountlyEndpointSurveys = @"/surveys"; const NSInteger kCountlyGETRequestMaxLength = 2048; @implementation CountlyConnectionManager : NSObject static CountlyConnectionManager *s_sharedInstance = nil; -static dispatch_once_t onceToken; +static dispatch_once_t onceToken; + (instancetype)sharedInstance { - if (!CountlyCommon.sharedInstance.hasStarted) - return nil; - dispatch_once(&onceToken, ^{s_sharedInstance = self.new;}); - return s_sharedInstance; + if (!CountlyCommon.sharedInstance.hasStarted) + return nil; + dispatch_once(&onceToken, ^{ + s_sharedInstance = self.new; + }); + return s_sharedInstance; } - (instancetype)init { - if (self = [super init]) - { - unsentSessionLength = 0.0; - isSessionStarted = NO; - atomic_init(&_backoff, NO); - _internalRequestCallbacks = [NSMutableDictionary dictionary]; - _queueFlushRunnables = [NSMutableArray array]; - _hasAnyRequestFailed = NO; - _callbackQueue = dispatch_queue_create("ly.count.callbackQueue", DISPATCH_QUEUE_SERIAL); - } - - return self; + if (self = [super init]) + { + unsentSessionLength = 0.0; + isSessionStarted = NO; + atomic_init(&_backoff, NO); + atomic_init(&_isProcessingQueue, NO); + _internalRequestCallbacks = [NSMutableDictionary dictionary]; + _queueFlushRunnables = [NSMutableArray array]; + _hasAnyRequestFailed = NO; + _callbackQueue = dispatch_queue_create("ly.count.callbackQueue", DISPATCH_QUEUE_SERIAL); + } + + return self; } - -- (BOOL)isSessionStarted { - return isSessionStarted; +- (BOOL)isSessionStarted +{ + return isSessionStarted; } -- (void)resetInstance { - CLY_LOG_I(@"%s", __FUNCTION__); - onceToken = 0; - s_sharedInstance = nil; - isSessionStarted = NO; - dispatch_sync(_callbackQueue, ^{ - [self->_internalRequestCallbacks removeAllObjects]; - [self->_queueFlushRunnables removeAllObjects]; - }); - _hasAnyRequestFailed = NO; +- (void)resetInstance +{ + CLY_LOG_I(@"%s", __FUNCTION__); + onceToken = 0; + s_sharedInstance = nil; + isSessionStarted = NO; + dispatch_sync(_callbackQueue, ^{ + [self->_internalRequestCallbacks removeAllObjects]; + [self->_queueFlushRunnables removeAllObjects]; + }); + _hasAnyRequestFailed = NO; + atomic_store(&_isProcessingQueue, NO); } - (void)setHost:(NSString *)host { - if ([host hasSuffix:@"/"]) - { - CLY_LOG_W(@"Host has an extra \"/\" at the end! It will be removed by the SDK.\ + if ([host hasSuffix:@"/"]) + { + CLY_LOG_W(@"Host has an extra \"/\" at the end! It will be removed by the SDK.\ But please make sure you fix it to avoid this warning in the future."); - _host = [host substringToIndex:host.length - 1]; - } - else - { - _host = host; - } + _host = [host substringToIndex:host.length - 1]; + } + else + { + _host = host; + } } - (void)setURLSessionConfiguration:(NSURLSessionConfiguration *)URLSessionConfiguration { - if (URLSessionConfiguration != nil) - { - _URLSessionConfiguration = URLSessionConfiguration; - _URLSession = nil; - } + if (URLSessionConfiguration != nil) + { + _URLSessionConfiguration = URLSessionConfiguration; + _URLSession = nil; + } } -- (void)addCustomNetworkRequestHeaders:(NSDictionary *_Nullable)customHeaderValues { - if (_URLSessionConfiguration == nil) { - return; - } +- (void)addCustomNetworkRequestHeaders:(NSDictionary *_Nullable)customHeaderValues +{ + if (_URLSessionConfiguration == nil) + { + return; + } - // Start with current headers (or empty if nil) - NSMutableDictionary *updatedHeaders = [NSMutableDictionary dictionaryWithDictionary:_URLSessionConfiguration.HTTPAdditionalHeaders ?: @{}]; + // Start with current headers (or empty if nil) + NSMutableDictionary *updatedHeaders = [NSMutableDictionary dictionaryWithDictionary:_URLSessionConfiguration.HTTPAdditionalHeaders ?: @{}]; - // Enumerate and validate custom headers - [customHeaderValues enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSString *value, BOOL *stop) { - if (key == nil || key.length == 0) { - return; // Skip empty key - } - if (value == nil) { - return; // Skip nil value - } + // Enumerate and validate custom headers + [customHeaderValues enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSString *value, BOOL *stop) { + if (key == nil || key.length == 0) + { + return; // Skip empty key + } + if (value == nil) + { + return; // Skip nil value + } - // Add or override - updatedHeaders[key] = value; - }]; + // Add or override + updatedHeaders[key] = value; + }]; - // Apply updated headers - _URLSessionConfiguration.HTTPAdditionalHeaders = [updatedHeaders copy]; - _URLSession = nil; + // Apply updated headers + _URLSessionConfiguration.HTTPAdditionalHeaders = [updatedHeaders copy]; + _URLSession = nil; } - (void)proceedOnQueue { - CLY_LOG_D(@"Proceeding on queue..."); - - if (!CountlyServerConfig.sharedInstance.networkingEnabled) - { - CLY_LOG_D(@"Proceeding on queue is aborted: SDK Networking is disabled from server config!"); - return; - } - - if (self.connection) - { - CLY_LOG_D(@"Proceeding on queue is aborted: Already has a request in process!"); - return; - } - - if (isCrashing) - { - CLY_LOG_D(@"Proceeding on queue is aborted: Application is crashing!"); - return; - } - - if (self.isTerminating) - { - CLY_LOG_D(@"Proceeding on queue is aborted: Application is terminating!"); - return; - } - - if (CountlyPersistency.sharedInstance.isQueueBeingModified) - { - CLY_LOG_D(@"Proceeding on queue is aborted: Queue is being modified!"); - return; - } - - BOOL backoffFlag = atomic_load(&_backoff) ? YES : NO; - if (backoffFlag) { - CLY_LOG_I(@"%s, currently backed off, skipping proceeding the queue", __FUNCTION__); - return; - } - - if (!self.startTime) { - self.startTime = [NSDate date]; // Record start time only when it's not already recorded - self.hasAnyRequestFailed = NO; // Reset failure flag when starting queue processing - CLY_LOG_D(@"%s, Proceeding on queue started, queued request count %lu", __FUNCTION__, [CountlyPersistency.sharedInstance remainingRequestCount]); - } - - NSString* firstItemInQueue = [CountlyPersistency.sharedInstance firstItemInQueue]; - if (!firstItemInQueue) - { - // Calculate total time when the queue becomes empty - NSTimeInterval elapsedTime = -[self.startTime timeIntervalSinceNow]; - CLY_LOG_D(@"%s, Queue is empty. All requests are processed. Total time taken: %.2f seconds", __FUNCTION__, elapsedTime); - - // Execute and clear runnables only if all requests succeeded - if (!self.hasAnyRequestFailed) { - // Thread-safe copy and clear of runnables - __block NSArray *runnablesToExecute = nil; - dispatch_sync(_callbackQueue, ^{ - if (self->_queueFlushRunnables.count > 0) { - CLY_LOG_D(@"%s, All requests succeeded. Executing %lu queue flush runnables.", __FUNCTION__, (unsigned long)self->_queueFlushRunnables.count); - runnablesToExecute = [self->_queueFlushRunnables copy]; - [self->_queueFlushRunnables removeAllObjects]; - } - }); - - // Execute runnables outside the lock to prevent deadlocks - if (runnablesToExecute) { - for (CLYQueueFlushRunnable runnable in runnablesToExecute) { - runnable(); - } - CLY_LOG_D(@"%s, All queue flush runnables executed and removed.", __FUNCTION__); - } - } else { - CLY_LOG_D(@"%s, Some requests failed. Runnables will not be executed.", __FUNCTION__); - } + CLY_LOG_D(@"Proceeding on queue..."); - // Reset start time and failure flag for future queue processing - self.startTime = nil; - self.hasAnyRequestFailed = NO; - return; - } - - BOOL isOldRequest = [CountlyPersistency.sharedInstance isOldRequest:firstItemInQueue]; - if(isOldRequest) - { - [CountlyPersistency.sharedInstance removeFromQueue:firstItemInQueue]; - - [CountlyPersistency.sharedInstance saveToFile]; - - [self proceedOnQueue]; - - return; - } - + if (!CountlyServerConfig.sharedInstance.networkingEnabled) + { + CLY_LOG_D(@"Proceeding on queue is aborted: SDK Networking is disabled from server config!"); + return; + } - NSString* temporaryDeviceIDQueryString = [NSString stringWithFormat:@"&%@=%@", kCountlyQSKeyDeviceID, CLYTemporaryDeviceID]; - if ([firstItemInQueue containsString:temporaryDeviceIDQueryString]) - { - CLY_LOG_D(@"Proceeding on queue is aborted: Device ID in request is CLYTemporaryDeviceID!"); - return; - } + if (self.connection || atomic_exchange(&_isProcessingQueue, YES)) + { + CLY_LOG_D(@"Proceeding on queue is aborted: Already has a request in process!"); + return; + } - _URLSessionConfiguration.timeoutIntervalForRequest = [CountlyServerConfig.sharedInstance requestTimeoutDuration]; - _URLSessionConfiguration.timeoutIntervalForResource = [CountlyServerConfig.sharedInstance requestTimeoutDuration]; - NSString* queryString = firstItemInQueue; - NSString* endPoint = kCountlyEndpointI; - - NSString* overrideEndPoint = [self extractAndRemoveParameter:&queryString parameter: kCountlyNewEndPoint]; - if(overrideEndPoint) { - endPoint = overrideEndPoint; - } - - NSString* callbackID = [self extractAndRemoveParameter:&queryString parameter: kCountlyCallbackID]; - __block CLYRequestCallback requestCallback = nil; - if(callbackID){ - dispatch_sync(_callbackQueue, ^{ - requestCallback = self.internalRequestCallbacks[callbackID]; - }); - } - - - [CountlyCommon.sharedInstance startBackgroundTask]; + if (isCrashing) + { + CLY_LOG_D(@"Proceeding on queue is aborted: Application is crashing!"); + atomic_store(&_isProcessingQueue, NO); + return; + } - queryString = [self appendRemainingRequest:queryString]; - NSMutableData* pictureUploadData = [self pictureUploadDataForQueryString:queryString]; + if (self.isTerminating) + { + CLY_LOG_D(@"Proceeding on queue is aborted: Application is terminating!"); + atomic_store(&_isProcessingQueue, NO); + return; + } - if (!pictureUploadData) - { - queryString = [self appendChecksum:queryString]; - } + if (CountlyPersistency.sharedInstance.isQueueBeingModified) + { + CLY_LOG_D(@"Proceeding on queue is aborted: Queue is being modified!"); + atomic_store(&_isProcessingQueue, NO); + return; + } - NSString* serverInputEndpoint = [self.host stringByAppendingString:endPoint]; - NSMutableURLRequest* request; - - if (pictureUploadData) + BOOL backoffFlag = atomic_load(&_backoff) ? YES : NO; + if (backoffFlag) + { + CLY_LOG_I(@"%s, currently backed off, skipping proceeding the queue", __FUNCTION__); + atomic_store(&_isProcessingQueue, NO); + return; + } + + if (!self.startTime) + { + self.startTime = [NSDate date]; // Record start time only when it's not already recorded + self.hasAnyRequestFailed = NO; // Reset failure flag when starting queue processing + CLY_LOG_D(@"%s, Proceeding on queue started, queued request count %lu", __FUNCTION__, [CountlyPersistency.sharedInstance remainingRequestCount]); + } + + NSString *firstItemInQueue = [CountlyPersistency.sharedInstance firstItemInQueue]; + if (!firstItemInQueue) + { + // Calculate total time when the queue becomes empty + NSTimeInterval elapsedTime = -[self.startTime timeIntervalSinceNow]; + CLY_LOG_D(@"%s, Queue is empty. All requests are processed. Total time taken: %.2f seconds", __FUNCTION__, elapsedTime); + + // Execute and clear runnables only if all requests succeeded + if (!self.hasAnyRequestFailed) { - request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:serverInputEndpoint]]; - NSString *contentType = [@"multipart/form-data; boundary=" stringByAppendingString:kCountlyUploadBoundary]; - [request addValue:contentType forHTTPHeaderField: @"Content-Type"]; - - NSArray *query = [queryString componentsSeparatedByString:@"&"]; - NSEnumerator *e = [query objectEnumerator]; - NSString* kvString; - while (kvString = [e nextObject]) { - NSArray *kv = [kvString componentsSeparatedByString:@"="]; - [self addMultipart:pictureUploadData andKey:[kv[0] stringByRemovingPercentEncoding] andValue:[kv[1] stringByRemovingPercentEncoding]]; + // Thread-safe copy and clear of runnables + __block NSArray *runnablesToExecute = nil; + dispatch_sync(_callbackQueue, ^{ + if (self->_queueFlushRunnables.count > 0) + { + CLY_LOG_D(@"%s, All requests succeeded. Executing %lu queue flush runnables.", __FUNCTION__, (unsigned long)self->_queueFlushRunnables.count); + runnablesToExecute = [self->_queueFlushRunnables copy]; + [self->_queueFlushRunnables removeAllObjects]; } - - if (self.secretSalt) + }); + + // Execute runnables outside the lock to prevent deadlocks + if (runnablesToExecute) + { + for (CLYQueueFlushRunnable runnable in runnablesToExecute) { - NSString* checksum = [[[queryString stringByRemovingPercentEncoding] stringByAppendingString:self.secretSalt] cly_SHA256]; - [self addMultipart:pictureUploadData andKey:kCountlyQSKeyChecksum256 andValue:checksum]; + runnable(); } - - NSString* boundaryEnd = [NSString stringWithFormat:@"\r\n--%@--\r\n", kCountlyUploadBoundary]; - [pictureUploadData appendData:[boundaryEnd cly_dataUTF8]]; - request.HTTPMethod = @"POST"; - request.HTTPBody = pictureUploadData; + CLY_LOG_D(@"%s, All queue flush runnables executed and removed.", __FUNCTION__); + } } - else if (queryString.length > kCountlyGETRequestMaxLength || self.alwaysUsePOST) + else { - request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:serverInputEndpoint]]; - request.HTTPMethod = @"POST"; - request.HTTPBody = [queryString cly_dataUTF8]; + CLY_LOG_D(@"%s, Some requests failed. Runnables will not be executed.", __FUNCTION__); } - else + + // Reset start time and failure flag for future queue processing + self.startTime = nil; + self.hasAnyRequestFailed = NO; + atomic_store(&_isProcessingQueue, NO); + return; + } + + BOOL isOldRequest = [CountlyPersistency.sharedInstance isOldRequest:firstItemInQueue]; + if (isOldRequest) + { + [CountlyPersistency.sharedInstance removeFromQueue:firstItemInQueue]; + + [CountlyPersistency.sharedInstance saveToFile]; + + atomic_store(&_isProcessingQueue, NO); + [self proceedOnQueue]; + + return; + } + + NSString *temporaryDeviceIDQueryString = [NSString stringWithFormat:@"&%@=%@", kCountlyQSKeyDeviceID, CLYTemporaryDeviceID]; + if ([firstItemInQueue containsString:temporaryDeviceIDQueryString]) + { + CLY_LOG_D(@"Proceeding on queue is aborted: Device ID in request is CLYTemporaryDeviceID!"); + atomic_store(&_isProcessingQueue, NO); + return; + } + + _URLSessionConfiguration.timeoutIntervalForRequest = [CountlyServerConfig.sharedInstance requestTimeoutDuration]; + _URLSessionConfiguration.timeoutIntervalForResource = [CountlyServerConfig.sharedInstance requestTimeoutDuration]; + NSString *queryString = firstItemInQueue; + NSString *endPoint = kCountlyEndpointI; + + NSString *overrideEndPoint = [self extractAndRemoveParameter:&queryString parameter:kCountlyNewEndPoint]; + if (overrideEndPoint) + { + endPoint = overrideEndPoint; + } + + NSString *callbackID = [self extractAndRemoveParameter:&queryString parameter:kCountlyCallbackID]; + __block CLYRequestCallback requestCallback = nil; + if (callbackID) + { + dispatch_sync(_callbackQueue, ^{ + requestCallback = self.internalRequestCallbacks[callbackID]; + }); + } + + [CountlyCommon.sharedInstance startBackgroundTask]; + + queryString = [self appendRemainingRequest:queryString]; + NSMutableData *pictureUploadData = [self pictureUploadDataForQueryString:queryString]; + + if (!pictureUploadData) + { + queryString = [self appendChecksum:queryString]; + } + + NSString *serverInputEndpoint = [self.host stringByAppendingString:endPoint]; + NSMutableURLRequest *request; + + if (pictureUploadData) + { + request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:serverInputEndpoint]]; + NSString *contentType = [@"multipart/form-data; boundary=" stringByAppendingString:kCountlyUploadBoundary]; + [request addValue:contentType forHTTPHeaderField:@"Content-Type"]; + + NSArray *query = [queryString componentsSeparatedByString:@"&"]; + NSEnumerator *e = [query objectEnumerator]; + NSString *kvString; + while (kvString = [e nextObject]) { - NSString* fullRequestURL = [serverInputEndpoint stringByAppendingFormat:@"?%@", queryString]; - request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:fullRequestURL]]; + NSArray *kv = [kvString componentsSeparatedByString:@"="]; + [self addMultipart:pictureUploadData andKey:[kv[0] stringByRemovingPercentEncoding] andValue:[kv[1] stringByRemovingPercentEncoding]]; } - request.cachePolicy = NSURLRequestReloadIgnoringLocalCacheData; - NSDate *startTimeRequest = [NSDate date]; - self.connection = [self.URLSession dataTaskWithRequest:request completionHandler:^(NSData * data, NSURLResponse * response, NSError * error) + if (self.secretSalt) { - self.connection = nil; - NSDate *endTimeRequest = [NSDate date]; - long duration = (long)[endTimeRequest timeIntervalSinceDate:startTimeRequest]; - - CLY_LOG_V(@"Approximate received data size for request <%p> is %ld bytes.", (id)request, (long)data.length); - - if(response) { - NSInteger code = ((NSHTTPURLResponse*)response).statusCode; - CLY_LOG_V(@"%s, Response received from server with status code:[ %ld ] request:[ %@ ]", __FUNCTION__, (long)code, ((NSHTTPURLResponse*)response).URL); - } - + NSString *checksum = [[[queryString stringByRemovingPercentEncoding] stringByAppendingString:self.secretSalt] cly_SHA256]; + [self addMultipart:pictureUploadData andKey:kCountlyQSKeyChecksum256 andValue:checksum]; + } - if (!error) - { - if ([self isRequestSuccessful:response data:data]) - { - CLY_LOG_D(@"Request <%p> successfully completed.", request); - - if(requestCallback){ - requestCallback([response description], YES); - // Clean up callback after execution - if (callbackID) { - dispatch_sync(self->_callbackQueue, ^{ - [self.internalRequestCallbacks removeObjectForKey:callbackID]; - }); - } - } - - [CountlyPersistency.sharedInstance removeFromQueue:firstItemInQueue]; - - [CountlyPersistency.sharedInstance saveToFile]; - - if(CountlyServerConfig.sharedInstance.backoffMechanism && [self backoff:duration queryString:queryString]){ - CLY_LOG_D(@"%s, backed off dropping proceeding the queue", __FUNCTION__); - self.startTime = nil; - self.hasAnyRequestFailed = NO; // Reset on backoff - [self backoffCountdown]; - } else { - [self proceedOnQueue]; - - } - } - else - { - CLY_LOG_D(@"%s, request:[ <%p> ] failed! response:[ %@ ]", __FUNCTION__, request, [data cly_stringUTF8]); - - self.hasAnyRequestFailed = YES; // Mark that a request has failed - - if(requestCallback){ - requestCallback([data cly_stringUTF8], NO); - // Clean up callback after execution - if (callbackID) { - dispatch_sync(self->_callbackQueue, ^{ - [self.internalRequestCallbacks removeObjectForKey:callbackID]; - }); - } - } - - [CountlyHealthTracker.sharedInstance logFailedNetworkRequestWithStatusCode:((NSHTTPURLResponse*)response).statusCode errorResponse: [data cly_stringUTF8]]; - [CountlyHealthTracker.sharedInstance saveState]; - self.startTime = nil; - } - } - else - { - CLY_LOG_D(@"%s, request:[ <%p> ] failed! error:[ %@ ]", __FUNCTION__, request, error); - - self.hasAnyRequestFailed = YES; // Mark that a request has failed - - if(requestCallback){ - requestCallback([error description], NO); - // Clean up callback after execution - if (callbackID) { - dispatch_sync(self->_callbackQueue, ^{ - [self.internalRequestCallbacks removeObjectForKey:callbackID]; - }); - } - } + NSString *boundaryEnd = [NSString stringWithFormat:@"\r\n--%@--\r\n", kCountlyUploadBoundary]; + [pictureUploadData appendData:[boundaryEnd cly_dataUTF8]]; + request.HTTPMethod = @"POST"; + request.HTTPBody = pictureUploadData; + } + else if (queryString.length > kCountlyGETRequestMaxLength || self.alwaysUsePOST) + { + request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:serverInputEndpoint]]; + request.HTTPMethod = @"POST"; + request.HTTPBody = [queryString cly_dataUTF8]; + } + else + { + NSString *fullRequestURL = [serverInputEndpoint stringByAppendingFormat:@"?%@", queryString]; + request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:fullRequestURL]]; + } + + request.cachePolicy = NSURLRequestReloadIgnoringLocalCacheData; + NSDate *startTimeRequest = [NSDate date]; + self.connection = [self.URLSession dataTaskWithRequest:request + completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { + self.connection = nil; + atomic_store(&self->_isProcessingQueue, NO); + NSDate *endTimeRequest = [NSDate date]; + long duration = (long)[endTimeRequest timeIntervalSinceDate:startTimeRequest]; + + CLY_LOG_V(@"Approximate received data size for request <%p> is %ld bytes.", (id)request, (long)data.length); + + if (response) + { + NSInteger code = ((NSHTTPURLResponse *)response).statusCode; + CLY_LOG_V(@"%s, Response received from server with status code:[ %ld ] request:[ %@ ]", __FUNCTION__, (long)code, ((NSHTTPURLResponse *)response).URL); + } + + if (!error) + { + if ([self isRequestSuccessful:response data:data]) + { + CLY_LOG_D(@"Request <%p> successfully completed.", request); + + if (requestCallback) + { + requestCallback([response description], YES); + // Clean up callback after execution + if (callbackID) + { + dispatch_sync(self->_callbackQueue, ^{ + [self.internalRequestCallbacks removeObjectForKey:callbackID]; + }); + } + } + + [CountlyPersistency.sharedInstance removeFromQueue:firstItemInQueue]; + + [CountlyPersistency.sharedInstance saveToFile]; + + if (CountlyServerConfig.sharedInstance.backoffMechanism && [self backoff:duration queryString:queryString]) + { + CLY_LOG_D(@"%s, backed off dropping proceeding the queue", __FUNCTION__); + self.startTime = nil; + self.hasAnyRequestFailed = NO; // Reset on backoff + [self backoffCountdown]; + } + else + { + [self proceedOnQueue]; + } + } + else + { + CLY_LOG_D(@"%s, request:[ <%p> ] failed! response:[ %@ ]", __FUNCTION__, request, [data cly_stringUTF8]); + + self.hasAnyRequestFailed = YES; // Mark that a request has failed + + if (requestCallback) + { + requestCallback([data cly_stringUTF8], NO); + // Clean up callback after execution + if (callbackID) + { + dispatch_sync(self->_callbackQueue, ^{ + [self.internalRequestCallbacks removeObjectForKey:callbackID]; + }); + } + } + + [CountlyHealthTracker.sharedInstance logFailedNetworkRequestWithStatusCode:((NSHTTPURLResponse *)response).statusCode errorResponse:[data cly_stringUTF8]]; + [CountlyHealthTracker.sharedInstance saveState]; + self.startTime = nil; + } + } + else + { + CLY_LOG_D(@"%s, request:[ <%p> ] failed! error:[ %@ ]", __FUNCTION__, request, error); + + self.hasAnyRequestFailed = YES; // Mark that a request has failed + + if (requestCallback) + { + requestCallback([error description], NO); + // Clean up callback after execution + if (callbackID) + { + dispatch_sync(self->_callbackQueue, ^{ + [self.internalRequestCallbacks removeObjectForKey:callbackID]; + }); + } + } #if (TARGET_OS_WATCH) - [CountlyPersistency.sharedInstance saveToFile]; + [CountlyPersistency.sharedInstance saveToFile]; #endif - self.startTime = nil; - } - }]; + self.startTime = nil; + } + }]; - [self.connection resume]; + [self.connection resume]; - [self logRequest:request]; + [self logRequest:request]; } - (void)recordMetrics:(nullable NSDictionary *)metricsOverride { - CLY_LOG_I(@"%s", __FUNCTION__, metricsOverride); - if (!CountlyConsentManager.sharedInstance.consentForMetrics) + CLY_LOG_I(@"%s", __FUNCTION__, metricsOverride); + if (!CountlyConsentManager.sharedInstance.consentForMetrics) return; - - NSDictionary *defaultMetrics = [CountlyDeviceInfo metricsDictionary]; - if (!defaultMetrics) { - CLY_LOG_W(@"%s Default metrics is nil, aborting", __FUNCTION__); - return; - } - CLY_LOG_I(@"%s default metrics:[%@]", __FUNCTION__, defaultMetrics); - - NSDictionary *finalMetrics; - if (metricsOverride && metricsOverride.count > 0) { - NSMutableDictionary *mutableMetrics = [defaultMetrics mutableCopy]; - [mutableMetrics addEntriesFromDictionary:metricsOverride]; - finalMetrics = [mutableMetrics copy]; - } else { - finalMetrics = defaultMetrics; - } - CLY_LOG_I(@"%s final metrics:[%@]", __FUNCTION__, finalMetrics); - - NSString* queryString = [[self queryEssentials] stringByAppendingFormat:@"&%@=%@", - kCountlyQSKeyMetrics, [finalMetrics cly_JSONify]]; - - [CountlyPersistency.sharedInstance addToQueue:queryString]; - [self proceedOnQueue]; + + NSDictionary *defaultMetrics = [CountlyDeviceInfo metricsDictionary]; + if (!defaultMetrics) + { + CLY_LOG_W(@"%s Default metrics is nil, aborting", __FUNCTION__); + return; + } + CLY_LOG_I(@"%s default metrics:[%@]", __FUNCTION__, defaultMetrics); + + NSDictionary *finalMetrics; + if (metricsOverride && metricsOverride.count > 0) + { + NSMutableDictionary *mutableMetrics = [defaultMetrics mutableCopy]; + [mutableMetrics addEntriesFromDictionary:metricsOverride]; + finalMetrics = [mutableMetrics copy]; + } + else + { + finalMetrics = defaultMetrics; + } + CLY_LOG_I(@"%s final metrics:[%@]", __FUNCTION__, finalMetrics); + + NSString *queryString = [[self queryEssentials] stringByAppendingFormat:@"&%@=%@", kCountlyQSKeyMetrics, [finalMetrics cly_JSONify]]; + + [CountlyPersistency.sharedInstance addToQueue:queryString]; + [self proceedOnQueue]; } - (BOOL)backoff:(long)responseTimeSeconds queryString:(NSString *)queryString { - BOOL result = NO; - // Check if the current response time is within acceptable limits - if (responseTimeSeconds >= [CountlyServerConfig.sharedInstance bomAcceptedTimeoutSeconds]) { - // Check if the remaining request count is within acceptable limits - NSUInteger remainingRequests = [CountlyPersistency.sharedInstance remainingRequestCount]; - NSUInteger threshold = (NSUInteger)(CountlyPersistency.sharedInstance.storedRequestsLimit * [CountlyServerConfig.sharedInstance bomRQPercentage]); - - if (remainingRequests <= threshold) { - // Calculate the age of the current request - double requestTimestamp = [[queryString cly_valueForQueryStringKey:kCountlyQSKeyTimestamp] longLongValue] / 1000.0; - double requestAgeInSeconds = [NSDate date].timeIntervalSince1970 - requestTimestamp; - - if (requestAgeInSeconds <= [CountlyServerConfig.sharedInstance bomRequestAge] * 3600.0) { - // Server is too busy, back off - result = YES; - [CountlyHealthTracker.sharedInstance logBackoffRequest]; - } - } - } - - if (!result) { - [CountlyHealthTracker.sharedInstance logConsecutiveBackoffRequest]; + BOOL result = NO; + // Check if the current response time is within acceptable limits + if (responseTimeSeconds >= [CountlyServerConfig.sharedInstance bomAcceptedTimeoutSeconds]) + { + // Check if the remaining request count is within acceptable limits + NSUInteger remainingRequests = [CountlyPersistency.sharedInstance remainingRequestCount]; + NSUInteger threshold = (NSUInteger)(CountlyPersistency.sharedInstance.storedRequestsLimit * [CountlyServerConfig.sharedInstance bomRQPercentage]); + + if (remainingRequests <= threshold) + { + // Calculate the age of the current request + double requestTimestamp = [[queryString cly_valueForQueryStringKey:kCountlyQSKeyTimestamp] longLongValue] / 1000.0; + double requestAgeInSeconds = [NSDate date].timeIntervalSince1970 - requestTimestamp; + + if (requestAgeInSeconds <= [CountlyServerConfig.sharedInstance bomRequestAge] * 3600.0) + { + // Server is too busy, back off + result = YES; + [CountlyHealthTracker.sharedInstance logBackoffRequest]; + } } - - return result; + } + + if (!result) + { + [CountlyHealthTracker.sharedInstance logConsecutiveBackoffRequest]; + } + + return result; } - (void)backoffCountdown { - __weak typeof(self) weakSelf = self; - CLY_LOG_D(@"%s, backed off, countdown start for %f seconds", __FUNCTION__, [CountlyServerConfig.sharedInstance bomDuration]); - - atomic_store(&_backoff, YES); - dispatch_time_t delay = dispatch_time(DISPATCH_TIME_NOW, (int64_t)([CountlyServerConfig.sharedInstance bomDuration] * NSEC_PER_SEC)); - dispatch_after(delay, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - __strong typeof(weakSelf) strongSelf = weakSelf; - if (!strongSelf) return; - - - CLY_LOG_D(@"%s, countdown finished, running tick in background thread", __FUNCTION__); - atomic_store(&strongSelf->_backoff, NO); - [strongSelf proceedOnQueue]; - }); + __weak typeof(self) weakSelf = self; + CLY_LOG_D(@"%s, backed off, countdown start for %f seconds", __FUNCTION__, [CountlyServerConfig.sharedInstance bomDuration]); + + atomic_store(&_backoff, YES); + dispatch_time_t delay = dispatch_time(DISPATCH_TIME_NOW, (int64_t)([CountlyServerConfig.sharedInstance bomDuration] * NSEC_PER_SEC)); + dispatch_after(delay, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + __strong typeof(weakSelf) strongSelf = weakSelf; + if (!strongSelf) + return; + + CLY_LOG_D(@"%s, countdown finished, running tick in background thread", __FUNCTION__); + atomic_store(&strongSelf->_backoff, NO); + [strongSelf proceedOnQueue]; + }); } -- (NSString*)extractAndRemoveParameter:(NSString **)queryString parameter:(NSString*)parameter +- (NSString *)extractAndRemoveParameter:(NSString **)queryString parameter:(NSString *)parameter { - CLY_LOG_D(@"%s, Extracting parameter: %@", __FUNCTION__, parameter); - - if([*queryString containsString:parameter]) { - NSString* parameterExtracted = [*queryString cly_valueForQueryStringKey:parameter]; - if(parameterExtracted) { - NSString* stringToRemove = [NSString stringWithFormat:@"&%@=%@",parameter,parameterExtracted]; - *queryString = [*queryString stringByReplacingOccurrencesOfString:stringToRemove withString:@""]; - CLY_LOG_D(@"%s, Parameter extracted successfully: %@ = %@", __FUNCTION__, parameter, parameterExtracted); - return parameterExtracted; - } - CLY_LOG_D(@"%s, Parameter found but value extraction failed for: %@", __FUNCTION__, parameter); - } else { - CLY_LOG_D(@"%s, Parameter not found in query string: %@", __FUNCTION__, parameter); - } - return nil; + CLY_LOG_D(@"%s, Extracting parameter: %@", __FUNCTION__, parameter); + + if ([*queryString containsString:parameter]) + { + NSString *parameterExtracted = [*queryString cly_valueForQueryStringKey:parameter]; + if (parameterExtracted) + { + NSString *stringToRemove = [NSString stringWithFormat:@"&%@=%@", parameter, parameterExtracted]; + *queryString = [*queryString stringByReplacingOccurrencesOfString:stringToRemove withString:@""]; + CLY_LOG_D(@"%s, Parameter extracted successfully: %@ = %@", __FUNCTION__, parameter, parameterExtracted); + return parameterExtracted; + } + CLY_LOG_D(@"%s, Parameter found but value extraction failed for: %@", __FUNCTION__, parameter); + } + else + { + CLY_LOG_D(@"%s, Parameter not found in query string: %@", __FUNCTION__, parameter); + } + return nil; } - (void)logRequest:(NSURLRequest *)request { - NSString* bodyAsString = @""; - NSInteger sentSize = request.URL.absoluteString.length; + NSString *bodyAsString = @""; + NSInteger sentSize = request.URL.absoluteString.length; - if (request.HTTPBody) - { - bodyAsString = [request.HTTPBody cly_stringUTF8]; - if (!bodyAsString) - bodyAsString = @"Picture uploading..."; + if (request.HTTPBody) + { + bodyAsString = [request.HTTPBody cly_stringUTF8]; + if (!bodyAsString) + bodyAsString = @"Picture uploading..."; - sentSize += request.HTTPBody.length; - } + sentSize += request.HTTPBody.length; + } - CLY_LOG_D(@"%s, request:[ <%p> ] started. [%@] %@ %@", __FUNCTION__, (id)request, request.HTTPMethod, request.URL.absoluteString, bodyAsString); - CLY_LOG_V(@"Approximate sent data size for request <%p> is %ld bytes.", (id)request, (long)sentSize); + CLY_LOG_D(@"%s, request:[ <%p> ] started. [%@] %@ %@", __FUNCTION__, (id)request, request.HTTPMethod, request.URL.absoluteString, bodyAsString); + CLY_LOG_V(@"Approximate sent data size for request <%p> is %ld bytes.", (id)request, (long)sentSize); } -#pragma mark --- +#pragma mark--- - (void)beginSession { - if (!CountlyConsentManager.sharedInstance.consentForSessions) - return; - - if(!CountlyServerConfig.sharedInstance.sessionTrackingEnabled) - return; - - if (isSessionStarted) { - CLY_LOG_W(@"%s A session is already running, this 'beginSession' will be ignored", __FUNCTION__); - return; - } - + if (!CountlyConsentManager.sharedInstance.consentForSessions) + return; + + if (!CountlyServerConfig.sharedInstance.sessionTrackingEnabled) + return; + + if (isSessionStarted) + { + CLY_LOG_W(@"%s A session is already running, this 'beginSession' will be ignored", __FUNCTION__); + return; + } + #if TARGET_OS_IOS || TARGET_OS_TV - if (!CountlyCommon.sharedInstance.manualSessionHandling && [UIApplication sharedApplication].applicationState == UIApplicationStateBackground) { - CLY_LOG_W(@"%s App is in the background, 'beginSession' will be ignored", __FUNCTION__); - return; - } + if (!CountlyCommon.sharedInstance.manualSessionHandling && [UIApplication sharedApplication].applicationState == UIApplicationStateBackground) + { + CLY_LOG_W(@"%s App is in the background, 'beginSession' will be ignored", __FUNCTION__); + return; + } #elif TARGET_OS_OSX - if (!CountlyCommon.sharedInstance.manualSessionHandling && ![NSApplication sharedApplication].isActive) { - CLY_LOG_W(@"%s App is not active, 'beginSession' will be ignored", __FUNCTION__); - return; - } + if (!CountlyCommon.sharedInstance.manualSessionHandling && ![NSApplication sharedApplication].isActive) + { + CLY_LOG_W(@"%s App is not active, 'beginSession' will be ignored", __FUNCTION__); + return; + } #elif TARGET_OS_WATCH - if (!CountlyCommon.sharedInstance.manualSessionHandling && [WKExtension sharedExtension].applicationState == WKApplicationStateBackground) { - CLY_LOG_W(@"%s App is in the background, 'beginSession' will be ignored", __FUNCTION__); - return; - } + if (!CountlyCommon.sharedInstance.manualSessionHandling && [WKExtension sharedExtension].applicationState == WKApplicationStateBackground) + { + CLY_LOG_W(@"%s App is in the background, 'beginSession' will be ignored", __FUNCTION__); + return; + } #endif - if([Countly.user hasUnsyncedChanges]) - { - [Countly.user save]; - } + if ([Countly.user hasUnsyncedChanges]) + { + [Countly.user save]; + } - isSessionStarted = YES; - lastSessionStartTime = NSDate.date.timeIntervalSince1970; - unsentSessionLength = 0.0; + isSessionStarted = YES; + lastSessionStartTime = NSDate.date.timeIntervalSince1970; + unsentSessionLength = 0.0; - NSString* queryString = [[self queryEssentials] stringByAppendingFormat:@"&%@=%@&%@=%@", - kCountlyQSKeySessionBegin, @"1", - kCountlyQSKeyMetrics, [CountlyDeviceInfo metrics]]; + NSString *queryString = [[self queryEssentials] stringByAppendingFormat:@"&%@=%@&%@=%@", kCountlyQSKeySessionBegin, @"1", kCountlyQSKeyMetrics, [CountlyDeviceInfo metrics]]; - if(CountlyServerConfig.sharedInstance.locationTrackingEnabled) { - NSString* locationRelatedInfoQueryString = [self locationRelatedInfoQueryString]; - if (locationRelatedInfoQueryString) - queryString = [queryString stringByAppendingString:locationRelatedInfoQueryString]; - } + if (CountlyServerConfig.sharedInstance.locationTrackingEnabled) + { + NSString *locationRelatedInfoQueryString = [self locationRelatedInfoQueryString]; + if (locationRelatedInfoQueryString) + queryString = [queryString stringByAppendingString:locationRelatedInfoQueryString]; + } - NSString* attributionQueryString = [self attributionQueryString]; - if (attributionQueryString) - queryString = [queryString stringByAppendingString:attributionQueryString]; + NSString *attributionQueryString = [self attributionQueryString]; + if (attributionQueryString) + queryString = [queryString stringByAppendingString:attributionQueryString]; - [CountlyPersistency.sharedInstance addToQueue:queryString]; - - [CountlyCommon.sharedInstance recordOrientation]; - - [self proceedOnQueue]; + [CountlyPersistency.sharedInstance addToQueue:queryString]; + + [CountlyCommon.sharedInstance recordOrientation]; + + [self proceedOnQueue]; } - (void)updateSession { - if (!CountlyConsentManager.sharedInstance.consentForSessions) - return; - - if(!CountlyServerConfig.sharedInstance.sessionTrackingEnabled) - return; - - if (!isSessionStarted) { - CLY_LOG_W(@"%s No session is running, this 'updateSession' will be ignored", __FUNCTION__); - return; - } - - if([Countly.user hasUnsyncedChanges]) - { - [Countly.user save]; - } + if (!CountlyConsentManager.sharedInstance.consentForSessions) + return; - NSString* queryString = [[self queryEssentials] stringByAppendingFormat:@"&%@=%d", - kCountlyQSKeySessionDuration, (int)[self sessionLengthInSeconds]]; + if (!CountlyServerConfig.sharedInstance.sessionTrackingEnabled) + return; - [CountlyPersistency.sharedInstance addToQueue:queryString]; + if (!isSessionStarted) + { + CLY_LOG_W(@"%s No session is running, this 'updateSession' will be ignored", __FUNCTION__); + return; + } - [self proceedOnQueue]; + if ([Countly.user hasUnsyncedChanges]) + { + [Countly.user save]; + } + + NSString *queryString = [[self queryEssentials] stringByAppendingFormat:@"&%@=%d", kCountlyQSKeySessionDuration, (int)[self sessionLengthInSeconds]]; + + [CountlyPersistency.sharedInstance addToQueue:queryString]; + + [self proceedOnQueue]; } - (void)endSession { - if (!CountlyConsentManager.sharedInstance.consentForSessions) - return; - - if(!CountlyServerConfig.sharedInstance.sessionTrackingEnabled) - return; - - if (!isSessionStarted) { - CLY_LOG_W(@"%s No session is running, this 'endSession' will be ignored", __FUNCTION__); - return; - } + if (!CountlyConsentManager.sharedInstance.consentForSessions) + return; - isSessionStarted = NO; - NSString* queryString = [[self queryEssentials] stringByAppendingFormat:@"&%@=%@&%@=%d", - kCountlyQSKeySessionEnd, @"1", - kCountlyQSKeySessionDuration, (int)[self sessionLengthInSeconds]]; + if (!CountlyServerConfig.sharedInstance.sessionTrackingEnabled) + return; - [CountlyPersistency.sharedInstance addToQueue:queryString]; + if (!isSessionStarted) + { + CLY_LOG_W(@"%s No session is running, this 'endSession' will be ignored", __FUNCTION__); + return; + } - [self proceedOnQueue]; - - [CountlyViewTrackingInternal.sharedInstance resetFirstView]; + isSessionStarted = NO; + NSString *queryString = [[self queryEssentials] stringByAppendingFormat:@"&%@=%@&%@=%d", kCountlyQSKeySessionEnd, @"1", kCountlyQSKeySessionDuration, (int)[self sessionLengthInSeconds]]; + + [CountlyPersistency.sharedInstance addToQueue:queryString]; + + [self proceedOnQueue]; + + [CountlyViewTrackingInternal.sharedInstance resetFirstView]; } -#pragma mark --- +#pragma mark--- - (void)sendEventsWithSaveIfNeeded { - if([Countly.user hasUnsyncedChanges]) - { - [Countly.user save]; - } - else - { - [self sendEventsInternal]; - } + if ([Countly.user hasUnsyncedChanges]) + { + [Countly.user save]; + } + else + { + [self sendEventsInternal]; + } } - (void)sendEvents { - [self sendEventsInternal]; + [self sendEventsInternal]; } - (void)attemptToSendStoredRequests { - [self addEventsToQueue]; - [CountlyPersistency.sharedInstance saveToFileSync]; - [self proceedOnQueue]; + [self addEventsToQueue]; + [CountlyPersistency.sharedInstance saveToFileSync]; + [self proceedOnQueue]; } - (void)sendEventsInternal { - [self addEventsToQueue]; - [self proceedOnQueue]; + [self addEventsToQueue]; + [self proceedOnQueue]; } - (void)addEventsToQueue { - [self addEventsToQueue:nil]; + [self addEventsToQueue:nil]; } - (void)addEventsToQueue:(CLYRequestCallback)callback { - NSString* events = [CountlyPersistency.sharedInstance serializedRecordedEvents]; + NSString *events = [CountlyPersistency.sharedInstance serializedRecordedEvents]; - if (!events) - return; + if (!events) + return; - NSString* queryString = [[self queryEssentials] stringByAppendingFormat:@"&%@=%@", kCountlyQSKeyEvents, events]; - [self addToQueueWithCallback:queryString callback:callback]; + NSString *queryString = [[self queryEssentials] stringByAppendingFormat:@"&%@=%@", kCountlyQSKeyEvents, events]; + [self addToQueueWithCallback:queryString callback:callback]; } - (void)sendEventsWithCallback:(CLYRequestCallback)callback { - [self addEventsToQueue:callback]; - [self proceedOnQueue]; + [self addEventsToQueue:callback]; + [self proceedOnQueue]; } -#pragma mark --- +#pragma mark--- - (void)sendPushToken:(NSString *)token { #ifndef COUNTLY_EXCLUDE_PUSHNOTIFICATIONS - NSInteger testMode = 0; //NOTE: default is 0: Production - not test mode + NSInteger testMode = 0; // NOTE: default is 0: Production - not test mode - if ([CountlyPushNotifications.sharedInstance.pushTestMode isEqualToString:CLYPushTestModeDevelopment]) - testMode = 1; //NOTE: 1: Developement/Debug builds - standard test mode using Sandbox APNs - else if ([CountlyPushNotifications.sharedInstance.pushTestMode isEqualToString:CLYPushTestModeTestFlightOrAdHoc]) - testMode = 2; //NOTE: 2: TestFlight/AdHoc builds - special test mode using Production APNs + if ([CountlyPushNotifications.sharedInstance.pushTestMode isEqualToString:CLYPushTestModeDevelopment]) + testMode = 1; // NOTE: 1: Developement/Debug builds - standard test mode using Sandbox APNs + else if ([CountlyPushNotifications.sharedInstance.pushTestMode isEqualToString:CLYPushTestModeTestFlightOrAdHoc]) + testMode = 2; // NOTE: 2: TestFlight/AdHoc builds - special test mode using Production APNs - NSString* queryString = [[self queryEssentials] stringByAppendingFormat:@"&%@=%@&%@=%@&%@=%ld", - kCountlyQSKeyPushTokenSession, @"1", - kCountlyQSKeyPushTokeniOS, token, - kCountlyQSKeyPushTestMode, (long)testMode]; + NSString *queryString = [[self queryEssentials] stringByAppendingFormat:@"&%@=%@&%@=%@&%@=%ld", kCountlyQSKeyPushTokenSession, @"1", kCountlyQSKeyPushTokeniOS, token, kCountlyQSKeyPushTestMode, (long)testMode]; - [CountlyPersistency.sharedInstance addToQueue:queryString]; + [CountlyPersistency.sharedInstance addToQueue:queryString]; - [self proceedOnQueue]; + [self proceedOnQueue]; #endif } - (void)sendLocationInfo { - NSString* locationRelatedInfoQueryString = [self locationRelatedInfoQueryString]; + NSString *locationRelatedInfoQueryString = [self locationRelatedInfoQueryString]; - if (!locationRelatedInfoQueryString) - return; + if (!locationRelatedInfoQueryString) + return; - NSString* queryString = [[self queryEssentials] stringByAppendingString:locationRelatedInfoQueryString]; + NSString *queryString = [[self queryEssentials] stringByAppendingString:locationRelatedInfoQueryString]; - [CountlyPersistency.sharedInstance addToQueue:queryString]; + [CountlyPersistency.sharedInstance addToQueue:queryString]; - [self proceedOnQueue]; + [self proceedOnQueue]; } - (void)sendUserDetails:(NSString *)userDetails { - NSString* queryString = [[self queryEssentials] stringByAppendingFormat:@"&%@=%@", - kCountlyQSKeyUserDetails, userDetails]; + NSString *queryString = [[self queryEssentials] stringByAppendingFormat:@"&%@=%@", kCountlyQSKeyUserDetails, userDetails]; - [CountlyPersistency.sharedInstance addToQueue:queryString]; + [CountlyPersistency.sharedInstance addToQueue:queryString]; - [self proceedOnQueue]; + [self proceedOnQueue]; } - (void)sendCrashReport:(NSString *)report immediately:(BOOL)immediately; { - if (!CountlyServerConfig.sharedInstance.networkingEnabled) - { - CLY_LOG_D(@"'sendCrashReport' is aborted: SDK Networking is disabled from server config!"); - return; - } - - if (!report) - { - CLY_LOG_W(@"Crash report is nil. Converting to JSON may have failed due to custom objects in initial config's crashSegmentation property."); - return; - } + if (!CountlyServerConfig.sharedInstance.networkingEnabled) + { + CLY_LOG_D(@"'sendCrashReport' is aborted: SDK Networking is disabled from server config!"); + return; + } - NSString* queryString = [[self queryEssentials] stringByAppendingFormat:@"&%@=%@", - kCountlyQSKeyCrash, report]; + if (!report) + { + CLY_LOG_W(@"Crash report is nil. Converting to JSON may have failed due to custom objects in initial config's crashSegmentation property."); + return; + } - if (!immediately) - { - [CountlyPersistency.sharedInstance addToQueue:queryString]; - [self proceedOnQueue]; - return; - } + NSString *queryString = [[self queryEssentials] stringByAppendingFormat:@"&%@=%@", kCountlyQSKeyCrash, report]; - //NOTE: Prevent `event` and `end_session` requests from being started, after `sendEvents` and `endSession` calls below. - isCrashing = YES; + if (!immediately) + { + [CountlyPersistency.sharedInstance addToQueue:queryString]; + [self proceedOnQueue]; + return; + } - [self sendEventsWithSaveIfNeeded]; + // NOTE: Prevent `event` and `end_session` requests from being started, after `sendEvents` and `endSession` calls below. + isCrashing = YES; - if (!CountlyCommon.sharedInstance.manualSessionHandling) - [self endSession]; + [self sendEventsWithSaveIfNeeded]; - if (CountlyDeviceInfo.sharedInstance.isDeviceIDTemporary) - { - CLY_LOG_D(@"Device ID is set as CLYTemporaryDeviceID! Crash report stored to be sent later!"); + if (!CountlyCommon.sharedInstance.manualSessionHandling) + [self endSession]; - [CountlyPersistency.sharedInstance addToQueue:queryString]; - [CountlyPersistency.sharedInstance saveToFileSync]; - return; - } + if (CountlyDeviceInfo.sharedInstance.isDeviceIDTemporary) + { + CLY_LOG_D(@"Device ID is set as CLYTemporaryDeviceID! Crash report stored to be sent later!"); + [CountlyPersistency.sharedInstance addToQueue:queryString]; [CountlyPersistency.sharedInstance saveToFileSync]; + return; + } - queryString = [queryString stringByAppendingFormat:@"&%@=%@", - kCountlyAppVersionKey, CountlyDeviceInfo.appVersion]; - - NSString* serverInputEndpoint = [self.host stringByAppendingString:kCountlyEndpointI]; - NSMutableURLRequest* request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:serverInputEndpoint]]; - request.HTTPMethod = @"POST"; - request.HTTPBody = [[self appendChecksum:queryString] cly_dataUTF8]; + [CountlyPersistency.sharedInstance saveToFileSync]; - dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); + queryString = [queryString stringByAppendingFormat:@"&%@=%@", kCountlyAppVersionKey, CountlyDeviceInfo.appVersion]; - [[self.URLSession dataTaskWithRequest:request completionHandler:^(NSData* data, NSURLResponse* response, NSError* error) - { - if (error || ![self isRequestSuccessful:response data:data]) - { - CLY_LOG_D(@"%s, request: [ %p ] failed! %@: %@", __FUNCTION__, request, error ? @"Error" : @"Server reply", error ?: [data cly_stringUTF8]); - [CountlyPersistency.sharedInstance addToQueue:queryString]; - [CountlyPersistency.sharedInstance saveToFileSync]; - } - else - { - CLY_LOG_D(@"Request <%p> successfully completed.", request); - } + NSString *serverInputEndpoint = [self.host stringByAppendingString:kCountlyEndpointI]; + NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:serverInputEndpoint]]; + request.HTTPMethod = @"POST"; + request.HTTPBody = [[self appendChecksum:queryString] cly_dataUTF8]; + + dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); - dispatch_semaphore_signal(semaphore); + [[self.URLSession dataTaskWithRequest:request + completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { + if (error || ![self isRequestSuccessful:response data:data]) + { + CLY_LOG_D(@"%s, request: [ %p ] failed! %@: %@", __FUNCTION__, request, error ? @"Error" : @"Server reply", error ?: [data cly_stringUTF8]); + [CountlyPersistency.sharedInstance addToQueue:queryString]; + [CountlyPersistency.sharedInstance saveToFileSync]; + } + else + { + CLY_LOG_D(@"Request <%p> successfully completed.", request); + } - }] resume]; + dispatch_semaphore_signal(semaphore); + }] resume]; - [self logRequest:request]; + [self logRequest:request]; - dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); + dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); } - (void)sendOldDeviceID:(NSString *)oldDeviceID { - NSString* queryString = [[self queryEssentials] stringByAppendingFormat:@"&%@=%@", - kCountlyQSKeyDeviceIDOld, oldDeviceID.cly_URLEscaped]; + NSString *queryString = [[self queryEssentials] stringByAppendingFormat:@"&%@=%@", kCountlyQSKeyDeviceIDOld, oldDeviceID.cly_URLEscaped]; - [CountlyPersistency.sharedInstance addToQueue:queryString]; + [CountlyPersistency.sharedInstance addToQueue:queryString]; - [self proceedOnQueue]; + [self proceedOnQueue]; } - (void)sendAttribution { - NSString * attributionQueryString = [self attributionQueryString]; - if (!attributionQueryString) - return; + NSString *attributionQueryString = [self attributionQueryString]; + if (!attributionQueryString) + return; - NSString* queryString = [[self queryEssentials] stringByAppendingString:attributionQueryString]; + NSString *queryString = [[self queryEssentials] stringByAppendingString:attributionQueryString]; - [CountlyPersistency.sharedInstance addToQueue:queryString]; + [CountlyPersistency.sharedInstance addToQueue:queryString]; - [self proceedOnQueue]; + [self proceedOnQueue]; } - (void)sendDirectAttributionWithCampaignID:(NSString *)campaignID andCampaignUserID:(NSString *)campaignUserID { - NSMutableString* queryString = [self queryEssentials].mutableCopy; - [queryString appendFormat:@"&%@=%@", kCountlyQSKeyCampaignID, campaignID]; + NSMutableString *queryString = [self queryEssentials].mutableCopy; + [queryString appendFormat:@"&%@=%@", kCountlyQSKeyCampaignID, campaignID]; - if (campaignUserID.length) - { - [queryString appendFormat:@"&%@=%@", kCountlyQSKeyCampaignUser, campaignUserID]; - } + if (campaignUserID.length) + { + [queryString appendFormat:@"&%@=%@", kCountlyQSKeyCampaignUser, campaignUserID]; + } - [CountlyPersistency.sharedInstance addToQueue:queryString.copy]; + [CountlyPersistency.sharedInstance addToQueue:queryString.copy]; - [self proceedOnQueue]; + [self proceedOnQueue]; } - (void)sendAttributionData:(NSString *)attributionData { - NSMutableString* queryString = [self queryEssentials].mutableCopy; - [queryString appendFormat:@"&%@=%@", kCountlyQSKeyAttributionData, [attributionData cly_URLEscaped]]; + NSMutableString *queryString = [self queryEssentials].mutableCopy; + [queryString appendFormat:@"&%@=%@", kCountlyQSKeyAttributionData, [attributionData cly_URLEscaped]]; - [CountlyPersistency.sharedInstance addToQueue:queryString.copy]; + [CountlyPersistency.sharedInstance addToQueue:queryString.copy]; - [self proceedOnQueue]; + [self proceedOnQueue]; } - (void)sendIndirectAttribution:(NSDictionary *)attribution { - NSMutableString* queryString = [self queryEssentials].mutableCopy; - [queryString appendFormat:@"&%@=%@", kCountlyQSKeyAttributionID, [attribution cly_JSONify]]; + NSMutableString *queryString = [self queryEssentials].mutableCopy; + [queryString appendFormat:@"&%@=%@", kCountlyQSKeyAttributionID, [attribution cly_JSONify]]; - [CountlyPersistency.sharedInstance addToQueue:queryString.copy]; + [CountlyPersistency.sharedInstance addToQueue:queryString.copy]; - [self proceedOnQueue]; + [self proceedOnQueue]; } - (void)sendConsents:(NSString *)consents { - NSString* queryString = [[self queryEssentials] stringByAppendingFormat:@"&%@=%@", - kCountlyQSKeyConsent, consents]; + NSString *queryString = [[self queryEssentials] stringByAppendingFormat:@"&%@=%@", kCountlyQSKeyConsent, consents]; - [CountlyPersistency.sharedInstance addToQueue:queryString]; + [CountlyPersistency.sharedInstance addToQueue:queryString]; - [self proceedOnQueue]; + [self proceedOnQueue]; } - (void)sendPerformanceMonitoringTrace:(NSString *)trace { - NSString* queryString = [[self queryEssentials] stringByAppendingFormat:@"&%@=%@", - kCountlyQSKeyAPM, trace]; + NSString *queryString = [[self queryEssentials] stringByAppendingFormat:@"&%@=%@", kCountlyQSKeyAPM, trace]; - [CountlyPersistency.sharedInstance addToQueue:queryString]; + [CountlyPersistency.sharedInstance addToQueue:queryString]; - [self proceedOnQueue]; + [self proceedOnQueue]; } -#pragma mark --- +#pragma mark--- -- (void)sendEnrollABRequestForKeys:(NSArray*)keys +- (void)sendEnrollABRequestForKeys:(NSArray *)keys { - NSString* queryString = [[self queryEssentials] stringByAppendingFormat:@"&%@=%@", kCountlyQSKeyMethod, kCountlyRCKeyABOptIn]; - - if (keys) - { - queryString = [queryString stringByAppendingFormat:@"&%@=%@", kCountlyRCKeyKeys, [keys cly_JSONify]]; - } - - queryString = [queryString stringByAppendingFormat:@"%@%@%@", kCountlyEndPointOverrideTag, kCountlyEndpointO, kCountlyEndpointSDK]; - - [CountlyPersistency.sharedInstance addToQueue:queryString]; - - [self proceedOnQueue]; + NSString *queryString = [[self queryEssentials] stringByAppendingFormat:@"&%@=%@", kCountlyQSKeyMethod, kCountlyRCKeyABOptIn]; + + if (keys) + { + queryString = [queryString stringByAppendingFormat:@"&%@=%@", kCountlyRCKeyKeys, [keys cly_JSONify]]; + } + + queryString = [queryString stringByAppendingFormat:@"%@%@%@", kCountlyEndPointOverrideTag, kCountlyEndpointO, kCountlyEndpointSDK]; + + [CountlyPersistency.sharedInstance addToQueue:queryString]; + + [self proceedOnQueue]; } -- (void)sendExitABRequestForKeys:(NSArray*)keys +- (void)sendExitABRequestForKeys:(NSArray *)keys { - NSString* queryString = [[self queryEssentials] stringByAppendingFormat:@"&%@=%@", kCountlyQSKeyMethod, kCountlyRCKeyABOptOut]; - - if (keys) - { - queryString = [queryString stringByAppendingFormat:@"&%@=%@", kCountlyRCKeyKeys, [keys cly_JSONify]]; - } - - [CountlyPersistency.sharedInstance addToQueue:queryString]; - - [self proceedOnQueue]; + NSString *queryString = [[self queryEssentials] stringByAppendingFormat:@"&%@=%@", kCountlyQSKeyMethod, kCountlyRCKeyABOptOut]; + + if (keys) + { + queryString = [queryString stringByAppendingFormat:@"&%@=%@", kCountlyRCKeyKeys, [keys cly_JSONify]]; + } + + [CountlyPersistency.sharedInstance addToQueue:queryString]; + + [self proceedOnQueue]; } -#pragma mark --- +#pragma mark--- - (void)addDirectRequest:(NSDictionary *)requestParameters { - if (!CountlyConsentManager.sharedInstance.hasAnyConsent) - return; + if (!CountlyConsentManager.sharedInstance.hasAnyConsent) + return; - NSMutableDictionary* mutableRequestParameters = requestParameters.mutableCopy; + NSMutableDictionary *mutableRequestParameters = requestParameters.mutableCopy; - for (NSString * reservedKey in self.reservedQueryStringKeys) + for (NSString *reservedKey in self.reservedQueryStringKeys) + { + if (mutableRequestParameters[reservedKey]) { - if (mutableRequestParameters[reservedKey]) - { - CLY_LOG_W(@"A reserved query string key detected in direct request parameters and it will be removed: %@", reservedKey); - [mutableRequestParameters removeObjectForKey:reservedKey]; - } + CLY_LOG_W(@"A reserved query string key detected in direct request parameters and it will be removed: %@", reservedKey); + [mutableRequestParameters removeObjectForKey:reservedKey]; } - - mutableRequestParameters[@"dr"] = [NSNumber numberWithInt:1]; - NSMutableString* queryString = [self queryEssentials].mutableCopy; + } - [mutableRequestParameters enumerateKeysAndObjectsUsingBlock:^(NSString * key, NSString * value, BOOL * stop) - { - [queryString appendFormat:@"&%@=%@", key, value]; - }]; + mutableRequestParameters[@"dr"] = [NSNumber numberWithInt:1]; + NSMutableString *queryString = [self queryEssentials].mutableCopy; - [CountlyPersistency.sharedInstance addToQueue:queryString.copy]; + [mutableRequestParameters enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSString *value, BOOL *stop) { + [queryString appendFormat:@"&%@=%@", key, value]; + }]; - [self proceedOnQueue]; + [CountlyPersistency.sharedInstance addToQueue:queryString.copy]; + + [self proceedOnQueue]; } -#pragma mark --- +#pragma mark--- - (NSString *)queryEssentials { - return [NSString stringWithFormat:@"%@=%@&%@=%@&%@=%d&%@=%lld&%@=%d&%@=%d&%@=%d&%@=%@&%@=%@", - kCountlyQSKeyAppKey, self.appKey.cly_URLEscaped, - kCountlyQSKeyDeviceID, CountlyDeviceInfo.sharedInstance.deviceID.cly_URLEscaped, - kCountlyQSKeyDeviceIDType, (int)CountlyDeviceInfo.sharedInstance.deviceIDTypeValue, - kCountlyQSKeyTimestamp, (long long)(CountlyCommon.sharedInstance.uniqueTimestamp * 1000), - kCountlyQSKeyTimeHourOfDay, (int)CountlyCommon.sharedInstance.hourOfDay, - kCountlyQSKeyTimeDayOfWeek, (int)CountlyCommon.sharedInstance.dayOfWeek, - kCountlyQSKeyTimeZone, (int)CountlyCommon.sharedInstance.timeZone, - kCountlyQSKeySDKVersion, CountlyCommon.sharedInstance.SDKVersion, - kCountlyQSKeySDKName, CountlyCommon.sharedInstance.SDKName]; + return [NSString stringWithFormat:@"%@=%@&%@=%@&%@=%d&%@=%lld&%@=%d&%@=%d&%@=%d&%@=%@&%@=%@", kCountlyQSKeyAppKey, self.appKey.cly_URLEscaped, kCountlyQSKeyDeviceID, CountlyDeviceInfo.sharedInstance.deviceID.cly_URLEscaped, kCountlyQSKeyDeviceIDType, + (int)CountlyDeviceInfo.sharedInstance.deviceIDTypeValue, kCountlyQSKeyTimestamp, (long long)(CountlyCommon.sharedInstance.uniqueTimestamp * 1000), kCountlyQSKeyTimeHourOfDay, (int)CountlyCommon.sharedInstance.hourOfDay, kCountlyQSKeyTimeDayOfWeek, + (int)CountlyCommon.sharedInstance.dayOfWeek, kCountlyQSKeyTimeZone, (int)CountlyCommon.sharedInstance.timeZone, kCountlyQSKeySDKVersion, CountlyCommon.sharedInstance.SDKVersion, kCountlyQSKeySDKName, CountlyCommon.sharedInstance.SDKName]; } - - (NSArray *)reservedQueryStringKeys { - return - @[ - kCountlyQSKeyAppKey, - kCountlyQSKeyDeviceID, - kCountlyQSKeyDeviceIDType, - kCountlyQSKeyTimestamp, - kCountlyQSKeyTimeHourOfDay, - kCountlyQSKeyTimeDayOfWeek, - kCountlyQSKeyTimeZone, - kCountlyQSKeySDKVersion, - kCountlyQSKeySDKName, - kCountlyQSKeyDeviceID, - kCountlyQSKeyDeviceIDOld, - kCountlyQSKeyChecksum256, - ]; + return @[ + kCountlyQSKeyAppKey, + kCountlyQSKeyDeviceID, + kCountlyQSKeyDeviceIDType, + kCountlyQSKeyTimestamp, + kCountlyQSKeyTimeHourOfDay, + kCountlyQSKeyTimeDayOfWeek, + kCountlyQSKeyTimeZone, + kCountlyQSKeySDKVersion, + kCountlyQSKeySDKName, + kCountlyQSKeyDeviceID, + kCountlyQSKeyDeviceIDOld, + kCountlyQSKeyChecksum256, + ]; } - - (NSString *)locationRelatedInfoQueryString { - if (!CountlyConsentManager.sharedInstance.consentForLocation || CountlyLocationManager.sharedInstance.isLocationInfoDisabled) - { - //NOTE: Return empty string for location. This is a server requirement to disable IP based location inferring. - return [NSString stringWithFormat:@"&%@=%@", kCountlyQSKeyLocation, @""]; - } + if (!CountlyConsentManager.sharedInstance.consentForLocation || CountlyLocationManager.sharedInstance.isLocationInfoDisabled) + { + // NOTE: Return empty string for location. This is a server requirement to disable IP based location inferring. + return [NSString stringWithFormat:@"&%@=%@", kCountlyQSKeyLocation, @""]; + } - NSString* location = CountlyLocationManager.sharedInstance.location.cly_URLEscaped; - NSString* city = CountlyLocationManager.sharedInstance.city.cly_URLEscaped; - NSString* ISOCountryCode = CountlyLocationManager.sharedInstance.ISOCountryCode.cly_URLEscaped; - NSString* IP = CountlyLocationManager.sharedInstance.IP.cly_URLEscaped; + NSString *location = CountlyLocationManager.sharedInstance.location.cly_URLEscaped; + NSString *city = CountlyLocationManager.sharedInstance.city.cly_URLEscaped; + NSString *ISOCountryCode = CountlyLocationManager.sharedInstance.ISOCountryCode.cly_URLEscaped; + NSString *IP = CountlyLocationManager.sharedInstance.IP.cly_URLEscaped; - NSMutableString* locationInfoQueryString = NSMutableString.new; + NSMutableString *locationInfoQueryString = NSMutableString.new; - if (location) - [locationInfoQueryString appendFormat:@"&%@=%@", kCountlyQSKeyLocation, location]; + if (location) + [locationInfoQueryString appendFormat:@"&%@=%@", kCountlyQSKeyLocation, location]; - if (city) - [locationInfoQueryString appendFormat:@"&%@=%@", kCountlyQSKeyLocationCity, city]; + if (city) + [locationInfoQueryString appendFormat:@"&%@=%@", kCountlyQSKeyLocationCity, city]; - if (ISOCountryCode) - [locationInfoQueryString appendFormat:@"&%@=%@", kCountlyQSKeyLocationCountry, ISOCountryCode]; + if (ISOCountryCode) + [locationInfoQueryString appendFormat:@"&%@=%@", kCountlyQSKeyLocationCountry, ISOCountryCode]; - if (IP) - [locationInfoQueryString appendFormat:@"&%@=%@", kCountlyQSKeyLocationIP, IP]; + if (IP) + [locationInfoQueryString appendFormat:@"&%@=%@", kCountlyQSKeyLocationIP, IP]; - if (locationInfoQueryString.length) - return locationInfoQueryString.copy; + if (locationInfoQueryString.length) + return locationInfoQueryString.copy; - return nil; + return nil; } - (NSString *)attributionQueryString { - if (!CountlyConsentManager.sharedInstance.consentForAttribution) - return nil; + if (!CountlyConsentManager.sharedInstance.consentForAttribution) + return nil; - if (!CountlyCommon.sharedInstance.attributionID) - return nil; + if (!CountlyCommon.sharedInstance.attributionID) + return nil; - NSDictionary* attribution = @{kCountlyQSKeyIDFA: CountlyCommon.sharedInstance.attributionID}; + NSDictionary *attribution = @{kCountlyQSKeyIDFA : CountlyCommon.sharedInstance.attributionID}; - return [NSString stringWithFormat:@"&%@=%@", kCountlyQSKeyAttributionID, [attribution cly_JSONify]]; + return [NSString stringWithFormat:@"&%@=%@", kCountlyQSKeyAttributionID, [attribution cly_JSONify]]; } - (NSMutableData *)pictureUploadDataForQueryString:(NSString *)queryString { #if (TARGET_OS_IOS || TARGET_OS_VISION) - NSString* localPicturePath = nil; + NSString *localPicturePath = nil; - NSString* userDetails = [queryString cly_valueForQueryStringKey:kCountlyQSKeyUserDetails]; - NSString* unescapedUserDetails = [userDetails stringByRemovingPercentEncoding]; - if (!unescapedUserDetails) - return nil; + NSString *userDetails = [queryString cly_valueForQueryStringKey:kCountlyQSKeyUserDetails]; + NSString *unescapedUserDetails = [userDetails stringByRemovingPercentEncoding]; + if (!unescapedUserDetails) + return nil; - NSDictionary* pathDictionary = [NSJSONSerialization JSONObjectWithData:[unescapedUserDetails cly_dataUTF8] options:0 error:nil]; - localPicturePath = pathDictionary[kCountlyLocalPicturePath]; + NSDictionary *pathDictionary = [NSJSONSerialization JSONObjectWithData:[unescapedUserDetails cly_dataUTF8] options:0 error:nil]; + localPicturePath = pathDictionary[kCountlyLocalPicturePath]; - if (!localPicturePath.length) - return nil; + if (!localPicturePath.length) + return nil; - CLY_LOG_D(@"Local picture path successfully extracted from query string: %@", localPicturePath); + CLY_LOG_D(@"Local picture path successfully extracted from query string: %@", localPicturePath); - NSArray* allowedFileTypes = @[@"gif", @"png", @"jpg", @"jpeg"]; - NSString* fileExt = localPicturePath.pathExtension.lowercaseString; - NSInteger fileExtIndex = [allowedFileTypes indexOfObject:fileExt]; + NSArray *allowedFileTypes = @[ @"gif", @"png", @"jpg", @"jpeg" ]; + NSString *fileExt = localPicturePath.pathExtension.lowercaseString; + NSInteger fileExtIndex = [allowedFileTypes indexOfObject:fileExt]; - if (fileExtIndex == NSNotFound) - { - CLY_LOG_W(@"Unsupported file extension for picture upload: %@", fileExt); - return nil; - } + if (fileExtIndex == NSNotFound) + { + CLY_LOG_W(@"Unsupported file extension for picture upload: %@", fileExt); + return nil; + } - NSData* imageData = [NSData dataWithContentsOfURL:[NSURL URLWithString:localPicturePath]]; + NSData *imageData = [NSData dataWithContentsOfURL:[NSURL URLWithString:localPicturePath]]; - if (!imageData) - { - CLY_LOG_W(@"Local picture data can not be read!"); - return nil; - } + if (!imageData) + { + CLY_LOG_W(@"Local picture data can not be read!"); + return nil; + } - CLY_LOG_D(@"Local picture data read successfully."); + CLY_LOG_D(@"Local picture data read successfully."); - //NOTE: Overcome failing PNG file upload if data is directly read from disk - if (fileExtIndex == 1) - imageData = UIImagePNGRepresentation([UIImage imageWithData:imageData]); + // NOTE: Overcome failing PNG file upload if data is directly read from disk + if (fileExtIndex == 1) + imageData = UIImagePNGRepresentation([UIImage imageWithData:imageData]); - //NOTE: Remap content type from jpg to jpeg - if (fileExtIndex == 2) - fileExtIndex = 3; + // NOTE: Remap content type from jpg to jpeg + if (fileExtIndex == 2) + fileExtIndex = 3; - NSString* boundaryStart = [NSString stringWithFormat:@"--%@\r\n", kCountlyUploadBoundary]; - NSString* contentDisposition = [NSString stringWithFormat:@"Content-Disposition: form-data; name=\"pictureFile\"; filename=\"%@\"\r\n", localPicturePath.lastPathComponent]; - NSString* contentType = [NSString stringWithFormat:@"Content-Type: image/%@\r\n\r\n", allowedFileTypes[fileExtIndex]]; + NSString *boundaryStart = [NSString stringWithFormat:@"--%@\r\n", kCountlyUploadBoundary]; + NSString *contentDisposition = [NSString stringWithFormat:@"Content-Disposition: form-data; name=\"pictureFile\"; filename=\"%@\"\r\n", localPicturePath.lastPathComponent]; + NSString *contentType = [NSString stringWithFormat:@"Content-Type: image/%@\r\n\r\n", allowedFileTypes[fileExtIndex]]; - NSMutableData* uploadData = NSMutableData.new; - [uploadData appendData:[boundaryStart cly_dataUTF8]]; - [uploadData appendData:[contentDisposition cly_dataUTF8]]; - [uploadData appendData:[contentType cly_dataUTF8]]; - [uploadData appendData:imageData]; - return uploadData; + NSMutableData *uploadData = NSMutableData.new; + [uploadData appendData:[boundaryStart cly_dataUTF8]]; + [uploadData appendData:[contentDisposition cly_dataUTF8]]; + [uploadData appendData:[contentType cly_dataUTF8]]; + [uploadData appendData:imageData]; + return uploadData; #endif - return nil; + return nil; } - (void)addMultipart:(NSMutableData *)uploadData andKey:(NSString *)key andValue:(NSString *)value { - NSString* boundaryStart = [NSString stringWithFormat:@"\r\n--%@\r\n", kCountlyUploadBoundary]; - NSString* contentDisposition = [NSString stringWithFormat:@"Content-Disposition: form-data; name=\"%@\";\r\n\r\n", key]; + NSString *boundaryStart = [NSString stringWithFormat:@"\r\n--%@\r\n", kCountlyUploadBoundary]; + NSString *contentDisposition = [NSString stringWithFormat:@"Content-Disposition: form-data; name=\"%@\";\r\n\r\n", key]; - [uploadData appendData:[boundaryStart cly_dataUTF8]]; - [uploadData appendData:[contentDisposition cly_dataUTF8]]; - [uploadData appendData:[value cly_dataUTF8]]; + [uploadData appendData:[boundaryStart cly_dataUTF8]]; + [uploadData appendData:[contentDisposition cly_dataUTF8]]; + [uploadData appendData:[value cly_dataUTF8]]; } - (NSString *)appendChecksum:(NSString *)queryString { - if (self.secretSalt) - { - NSString* checksum = [[queryString stringByAppendingString:self.secretSalt] cly_SHA256]; - return [queryString stringByAppendingFormat:@"&%@=%@", kCountlyQSKeyChecksum256, checksum]; - } + if (self.secretSalt) + { + NSString *checksum = [[queryString stringByAppendingString:self.secretSalt] cly_SHA256]; + return [queryString stringByAppendingFormat:@"&%@=%@", kCountlyQSKeyChecksum256, checksum]; + } - return queryString; + return queryString; } - (NSString *)appendRemainingRequest:(NSString *)queryString { - NSUInteger rrCount = [CountlyPersistency.sharedInstance remainingRequestCount] - 1; - return [queryString stringByAppendingFormat:@"&%@=%lu", kCountlyQSKeyRemainingRequest, (unsigned long)rrCount]; - - return queryString; + NSUInteger rrCount = [CountlyPersistency.sharedInstance remainingRequestCount] - 1; + return [queryString stringByAppendingFormat:@"&%@=%lu", kCountlyQSKeyRemainingRequest, (unsigned long)rrCount]; + + return queryString; } -- (BOOL)isRequestSuccessful:(NSURLResponse *)response data:(NSData *)data +- (BOOL)isRequestSuccessful:(NSURLResponse *)response data:(NSData *)data { - if (!response) - return NO; + if (!response) + return NO; - NSInteger code = ((NSHTTPURLResponse*)response).statusCode; + NSInteger code = ((NSHTTPURLResponse *)response).statusCode; - if (code >= 200 && code < 300) - { - NSError* error = nil; - NSDictionary* serverReply = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error]; + if (code >= 200 && code < 300) + { + NSError *error = nil; + NSDictionary *serverReply = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error]; - if (error) - { - CLY_LOG_W(@"Server reply is not a valid JSON!"); - return NO; - } - - CLY_LOG_V(@"%s, response:[ %@ ] request:[ %@ ]", __FUNCTION__, serverReply, ((NSHTTPURLResponse*)response).URL); - - NSString* result = serverReply[@"result"]; - - if(result) - { - return YES; - } - - return NO; - + if (error) + { + CLY_LOG_W(@"Server reply is not a valid JSON!"); + return NO; } - else + + CLY_LOG_V(@"%s, response:[ %@ ] request:[ %@ ]", __FUNCTION__, serverReply, ((NSHTTPURLResponse *)response).URL); + + NSString *result = serverReply[@"result"]; + + if (result) { - CLY_LOG_V(@"HTTP status code is not 2XX series."); - return NO; + return YES; } + + return NO; + } + else + { + CLY_LOG_V(@"HTTP status code is not 2XX series."); + return NO; + } } - (NSInteger)sessionLengthInSeconds { - NSTimeInterval currentTime = NSDate.date.timeIntervalSince1970; - unsentSessionLength += (currentTime - lastSessionStartTime); - lastSessionStartTime = currentTime; - int sessionLengthInSeconds = (int)unsentSessionLength; - unsentSessionLength -= sessionLengthInSeconds; - return sessionLengthInSeconds; + NSTimeInterval currentTime = NSDate.date.timeIntervalSince1970; + unsentSessionLength += (currentTime - lastSessionStartTime); + lastSessionStartTime = currentTime; + int sessionLengthInSeconds = (int)unsentSessionLength; + unsentSessionLength -= sessionLengthInSeconds; + return sessionLengthInSeconds; } -#pragma mark --- +#pragma mark--- - (NSURLSession *)URLSession { - if (!_URLSession) + if (!_URLSession) + { + if (self.pinnedCertificates) { - if (self.pinnedCertificates) - { - CLY_LOG_D(@"%d pinned certificate(s) specified in config.", (int)self.pinnedCertificates.count); - _URLSession = [NSURLSession sessionWithConfiguration:self.URLSessionConfiguration delegate:self delegateQueue:nil]; - } - else - { - _URLSession = [NSURLSession sessionWithConfiguration:self.URLSessionConfiguration]; - } + CLY_LOG_D(@"%d pinned certificate(s) specified in config.", (int)self.pinnedCertificates.count); + _URLSession = [NSURLSession sessionWithConfiguration:self.URLSessionConfiguration delegate:self delegateQueue:nil]; + } + else + { + _URLSession = [NSURLSession sessionWithConfiguration:self.URLSessionConfiguration]; } + } - return _URLSession; + return _URLSession; } - (void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential *))completionHandler { - SecPolicyRef policy = SecPolicyCreateSSL(true, (__bridge CFStringRef)challenge.protectionSpace.host); - SecTrustRef serverTrust = challenge.protectionSpace.serverTrust; - SecKeyRef serverKey = NULL; + SecPolicyRef policy = SecPolicyCreateSSL(true, (__bridge CFStringRef)challenge.protectionSpace.host); + SecTrustRef serverTrust = challenge.protectionSpace.serverTrust; + SecKeyRef serverKey = NULL; + + if (@available(iOS 14.0, tvOS 14.0, macOS 11.0, watchOS 7.0, *)) + { + serverKey = SecTrustCopyKey(serverTrust); + } + else + { +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" + serverKey = SecTrustCopyPublicKey(serverTrust); +#pragma GCC diagnostic pop + } + + __block BOOL isLocalAndServerCertMatch = NO; + + for (NSString *certificate in self.pinnedCertificates) + { + NSString *localCertPath = [NSBundle.mainBundle pathForResource:certificate ofType:nil]; + + if (!localCertPath) + [NSException raise:@"CountlyCertificateNotFoundException" format:@"Bundled certificate can not be found for %@", certificate]; + + NSData *localCertData = [NSData dataWithContentsOfFile:localCertPath]; + SecCertificateRef localCert = SecCertificateCreateWithData(NULL, (__bridge CFDataRef)localCertData); + SecTrustRef localTrust = NULL; + SecTrustCreateWithCertificates(localCert, policy, &localTrust); + SecKeyRef localKey = NULL; if (@available(iOS 14.0, tvOS 14.0, macOS 11.0, watchOS 7.0, *)) { - serverKey = SecTrustCopyKey(serverTrust); + localKey = SecTrustCopyKey(localTrust); } else { #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wdeprecated-declarations" - serverKey = SecTrustCopyPublicKey(serverTrust); + localKey = SecTrustCopyPublicKey(localTrust); #pragma GCC diagnostic pop } - __block BOOL isLocalAndServerCertMatch = NO; + CFRelease(localCert); + CFRelease(localTrust); - for (NSString* certificate in self.pinnedCertificates) + if (serverKey != NULL && localKey != NULL && [(__bridge id)serverKey isEqual:(__bridge id)localKey]) { - NSString* localCertPath = [NSBundle.mainBundle pathForResource:certificate ofType:nil]; - - if (!localCertPath) - [NSException raise:@"CountlyCertificateNotFoundException" format:@"Bundled certificate can not be found for %@", certificate]; - - NSData* localCertData = [NSData dataWithContentsOfFile:localCertPath]; - SecCertificateRef localCert = SecCertificateCreateWithData(NULL, (__bridge CFDataRef)localCertData); - SecTrustRef localTrust = NULL; - SecTrustCreateWithCertificates(localCert, policy, &localTrust); - SecKeyRef localKey = NULL; - - if (@available(iOS 14.0, tvOS 14.0, macOS 11.0, watchOS 7.0, *)) - { - localKey = SecTrustCopyKey(localTrust); - } - else - { -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wdeprecated-declarations" - localKey = SecTrustCopyPublicKey(localTrust); -#pragma GCC diagnostic pop - } - - CFRelease(localCert); - CFRelease(localTrust); + CLY_LOG_D(@"Pinned certificate and server certificate match."); - if (serverKey != NULL && localKey != NULL && [(__bridge id)serverKey isEqual:(__bridge id)localKey]) - { - CLY_LOG_D(@"Pinned certificate and server certificate match."); + isLocalAndServerCertMatch = YES; + CFRelease(localKey); + break; + } - isLocalAndServerCertMatch = YES; - CFRelease(localKey); - break; - } + if (localKey) + CFRelease(localKey); + } - if (localKey) - CFRelease(localKey); - } - #if DEBUG - if (CountlyCommon.sharedInstance.shouldIgnoreTrustCheck) - { - CFDataRef exceptions = SecTrustCopyExceptions(serverTrust); - SecTrustSetExceptions(serverTrust, exceptions); - CFRelease(exceptions); - } + if (CountlyCommon.sharedInstance.shouldIgnoreTrustCheck) + { + CFDataRef exceptions = SecTrustCopyExceptions(serverTrust); + SecTrustSetExceptions(serverTrust, exceptions); + CFRelease(exceptions); + } #endif - - SecTrustResultType serverTrustResult; + + SecTrustResultType serverTrustResult; #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wdeprecated-declarations" - SecTrustEvaluate(serverTrust, &serverTrustResult); + SecTrustEvaluate(serverTrust, &serverTrustResult); #pragma GCC diagnostic pop - BOOL isServerCertValid = (serverTrustResult == kSecTrustResultUnspecified || serverTrustResult == kSecTrustResultProceed); + BOOL isServerCertValid = (serverTrustResult == kSecTrustResultUnspecified || serverTrustResult == kSecTrustResultProceed); - if (isLocalAndServerCertMatch && isServerCertValid) - { - CLY_LOG_D(@"Pinned certificate check is successful. Proceeding with request."); - completionHandler(NSURLSessionAuthChallengeUseCredential, [NSURLCredential credentialForTrust:serverTrust]); - } - else - { - if (!isLocalAndServerCertMatch) - CLY_LOG_W(@"Pinned certificate and server certificate does not match!"); + if (isLocalAndServerCertMatch && isServerCertValid) + { + CLY_LOG_D(@"Pinned certificate check is successful. Proceeding with request."); + completionHandler(NSURLSessionAuthChallengeUseCredential, [NSURLCredential credentialForTrust:serverTrust]); + } + else + { + if (!isLocalAndServerCertMatch) + CLY_LOG_W(@"Pinned certificate and server certificate does not match!"); - if (!isServerCertValid) - CLY_LOG_W(@"Server certificate is not valid! SecTrustEvaluate result is: %u", serverTrustResult); + if (!isServerCertValid) + CLY_LOG_W(@"Server certificate is not valid! SecTrustEvaluate result is: %u", serverTrustResult); - CLY_LOG_D(@"Pinned certificate check failed! Cancelling request."); - completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, NULL); - } + CLY_LOG_D(@"Pinned certificate check failed! Cancelling request."); + completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, NULL); + } - if (serverKey) - CFRelease(serverKey); + if (serverKey) + CFRelease(serverKey); - CFRelease(policy); + CFRelease(policy); } #pragma mark - Request Callbacks - (void)registerRequestCallback:(NSString *)callbackID callback:(CLYRequestCallback)callback { - if (!callbackID || callbackID.length == 0) - { - CLY_LOG_W(@"%s, Callback ID is nil or empty. Callback registration ignored.", __FUNCTION__); - return; - } + if (!callbackID || callbackID.length == 0) + { + CLY_LOG_W(@"%s, Callback ID is nil or empty. Callback registration ignored.", __FUNCTION__); + return; + } - if (!callback) - { - CLY_LOG_W(@"%s, Callback block is nil. Callback registration ignored.", __FUNCTION__); - return; - } + if (!callback) + { + CLY_LOG_W(@"%s, Callback block is nil. Callback registration ignored.", __FUNCTION__); + return; + } - dispatch_sync(_callbackQueue, ^{ - CLY_LOG_D(@"%s, Registering request callback with ID: %@", __FUNCTION__, callbackID); - self.internalRequestCallbacks[callbackID] = callback; - }); + dispatch_sync(_callbackQueue, ^{ + CLY_LOG_D(@"%s, Registering request callback with ID: %@", __FUNCTION__, callbackID); + self.internalRequestCallbacks[callbackID] = callback; + }); } - (void)removeRequestCallback:(NSString *)callbackID { - if (!callbackID || callbackID.length == 0) - { - CLY_LOG_W(@"%s, Callback ID is nil or empty. Callback removal ignored.", __FUNCTION__); - return; - } + if (!callbackID || callbackID.length == 0) + { + CLY_LOG_W(@"%s, Callback ID is nil or empty. Callback removal ignored.", __FUNCTION__); + return; + } - dispatch_sync(_callbackQueue, ^{ - CLY_LOG_D(@"%s, Removing request callback with ID: %@", __FUNCTION__, callbackID); - [self.internalRequestCallbacks removeObjectForKey:callbackID]; - }); + dispatch_sync(_callbackQueue, ^{ + CLY_LOG_D(@"%s, Removing request callback with ID: %@", __FUNCTION__, callbackID); + [self.internalRequestCallbacks removeObjectForKey:callbackID]; + }); } - (void)addQueueFlushRunnable:(CLYQueueFlushRunnable)runnable { - if (!runnable) - { - CLY_LOG_W(@"%s, Runnable is nil. Cannot add to queue flush runnables.", __FUNCTION__); - return; - } + if (!runnable) + { + CLY_LOG_W(@"%s, Runnable is nil. Cannot add to queue flush runnables.", __FUNCTION__); + return; + } - CLYQueueFlushRunnable runnableCopy = [runnable copy]; - dispatch_sync(_callbackQueue, ^{ - CLY_LOG_D(@"%s, Adding queue flush runnable. Total count: %lu", __FUNCTION__, (unsigned long)(self->_queueFlushRunnables.count + 1)); - [self->_queueFlushRunnables addObject:runnableCopy]; - }); + CLYQueueFlushRunnable runnableCopy = [runnable copy]; + dispatch_sync(_callbackQueue, ^{ + CLY_LOG_D(@"%s, Adding queue flush runnable. Total count: %lu", __FUNCTION__, (unsigned long)(self->_queueFlushRunnables.count + 1)); + [self->_queueFlushRunnables addObject:runnableCopy]; + }); } - (void)clearQueueFlushRunnables { - dispatch_sync(_callbackQueue, ^{ - CLY_LOG_D(@"%s, Clearing %lu queue flush runnables.", __FUNCTION__, (unsigned long)self->_queueFlushRunnables.count); - [self->_queueFlushRunnables removeAllObjects]; - }); + dispatch_sync(_callbackQueue, ^{ + CLY_LOG_D(@"%s, Clearing %lu queue flush runnables.", __FUNCTION__, (unsigned long)self->_queueFlushRunnables.count); + [self->_queueFlushRunnables removeAllObjects]; + }); } - (void)addToQueueWithCallback:(NSString *)queryString callback:(CLYRequestCallback)callback { - if (!queryString || queryString.length == 0) - { - CLY_LOG_W(@"%s, Query string is nil or empty. Cannot add to queue with callback.", __FUNCTION__); - return; - } + if (!queryString || queryString.length == 0) + { + CLY_LOG_W(@"%s, Query string is nil or empty. Cannot add to queue with callback.", __FUNCTION__); + return; + } - if (!callback) - { - [CountlyPersistency.sharedInstance addToQueue:queryString]; - return; - } + if (!callback) + { + [CountlyPersistency.sharedInstance addToQueue:queryString]; + return; + } - // Generate a unique callback ID - NSString* callbackID = [[NSUUID UUID] UUIDString]; - CLY_LOG_D(@"%s, Adding request to queue with callback ID: %@", __FUNCTION__, callbackID); + // Generate a unique callback ID + NSString *callbackID = [[NSUUID UUID] UUIDString]; + CLY_LOG_D(@"%s, Adding request to queue with callback ID: %@", __FUNCTION__, callbackID); - // Register the callback - [self registerRequestCallback:callbackID callback:callback]; + // Register the callback + [self registerRequestCallback:callbackID callback:callback]; - // Append callback_id parameter to query string - NSString* queryStringWithCallback = [queryString stringByAppendingFormat:@"&callback_id=%@", callbackID]; + // Append callback_id parameter to query string + NSString *queryStringWithCallback = [queryString stringByAppendingFormat:@"&callback_id=%@", callbackID]; - // Add to queue - [CountlyPersistency.sharedInstance addToQueue:queryStringWithCallback]; + // Add to queue + [CountlyPersistency.sharedInstance addToQueue:queryStringWithCallback]; } @end From e9deaf2d71b817d224d8332aed35fe47f8de033d Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Mon, 6 Apr 2026 12:26:15 +0300 Subject: [PATCH 14/42] fix: add validations --- CountlyServerConfig.m | 248 +++++++++++++++++++++++++++++++++--------- 1 file changed, 195 insertions(+), 53 deletions(-) diff --git a/CountlyServerConfig.m b/CountlyServerConfig.m index 42ad6ed2..b0d3a63e 100644 --- a/CountlyServerConfig.m +++ b/CountlyServerConfig.m @@ -170,7 +170,6 @@ - (void)retrieveServerConfigFromStorage:(CountlyConfig *)config if ([parsed isKindOfClass:[NSDictionary class]]) { persistentBehaviorSettings = [(NSDictionary *)parsed mutableCopy]; - [CountlyPersistency.sharedInstance storeServerConfig:persistentBehaviorSettings]; } else { @@ -179,6 +178,7 @@ - (void)retrieveServerConfigFromStorage:(CountlyConfig *)config } [self populateServerConfig:persistentBehaviorSettings withConfig:config]; + [CountlyPersistency.sharedInstance storeServerConfig:persistentBehaviorSettings]; } - (void)mergeBehaviorSettings:(NSMutableDictionary *)baseConfig withConfig:(NSDictionary *)newConfig @@ -233,37 +233,84 @@ - (void)mergeBehaviorSettings:(NSMutableDictionary *)baseConfig withConfig:(NSDi } } -- (void)setBoolProperty:(BOOL *)property fromDictionary:(NSDictionary *)dictionary key:(NSString *)key logString:(NSMutableString *)logString +- (void)setBoolProperty:(BOOL *)property fromDictionary:(NSMutableDictionary *)dictionary key:(NSString *)key logString:(NSMutableString *)logString { - NSNumber *value = dictionary[key]; - if (value) + id value = dictionary[key]; + if (!value) + return; + + if (CFGetTypeID((__bridge CFTypeRef)value) == CFBooleanGetTypeID()) { - *property = value.boolValue; + *property = [value boolValue]; [logString appendFormat:@"%@: %@, ", key, *property ? @"YES" : @"NO"]; } + else + { + CLY_LOG_W(@"%s, Invalid type for bool key '%@', removing", __FUNCTION__, key); + [dictionary removeObjectForKey:key]; + } +} + +- (void)setIntegerProperty:(NSInteger *)property fromDictionary:(NSMutableDictionary *)dictionary key:(NSString *)key logString:(NSMutableString *)logString +{ + [self setIntegerProperty:property fromDictionary:dictionary key:key minValue:1 logString:logString]; } -- (void)setIntegerProperty:(NSInteger *)property fromDictionary:(NSDictionary *)dictionary key:(NSString *)key logString:(NSMutableString *)logString +- (void)setIntegerProperty:(NSInteger *)property fromDictionary:(NSMutableDictionary *)dictionary key:(NSString *)key minValue:(NSInteger)minValue logString:(NSMutableString *)logString { - NSNumber *value = dictionary[key]; - if (value) + id value = dictionary[key]; + if (!value) + return; + + if ([value isKindOfClass:[NSNumber class]] && CFGetTypeID((__bridge CFTypeRef)value) != CFBooleanGetTypeID()) { - *property = value.integerValue; - [logString appendFormat:@"%@: %ld, ", key, (long)*property]; + NSInteger intVal = [value integerValue]; + if (intVal >= minValue) + { + *property = intVal; + [logString appendFormat:@"%@: %ld, ", key, (long)*property]; + } + else + { + CLY_LOG_W(@"%s, Invalid value (%ld) for integer key '%@' (min: %ld), removing", __FUNCTION__, (long)intVal, key, (long)minValue); + [dictionary removeObjectForKey:key]; + } + } + else + { + CLY_LOG_W(@"%s, Invalid type for integer key '%@', removing", __FUNCTION__, key); + [dictionary removeObjectForKey:key]; } } -- (void)setDoubleProperty:(double *)property fromDictionary:(NSDictionary *)dictionary key:(NSString *)key logString:(NSMutableString *)logString +- (void)setDoubleProperty:(double *)property fromDictionary:(NSMutableDictionary *)dictionary key:(NSString *)key logString:(NSMutableString *)logString { - NSNumber *value = dictionary[key]; - if (value) + id value = dictionary[key]; + if (!value) + return; + + if ([value isKindOfClass:[NSNumber class]] && CFGetTypeID((__bridge CFTypeRef)value) != CFBooleanGetTypeID()) { - *property = value.doubleValue; - [logString appendFormat:@"%@: %lf, ", key, (double)*property]; + double dblVal = [value doubleValue]; + if (dblVal > 0.0 && dblVal < 1.0) + { + *property = dblVal; + [logString appendFormat:@"%@: %lf, ", key, (double)*property]; + } + else + { + CLY_LOG_W(@"%s, Invalid value (%lf) for double key '%@', removing", __FUNCTION__, dblVal, key); + [dictionary removeObjectForKey:key]; + } + } + else + { + CLY_LOG_W(@"%s, Invalid type for double key '%@', removing", __FUNCTION__, key); + [dictionary removeObjectForKey:key]; } } -- (void)populateServerConfig:(NSDictionary *)serverConfig withConfig:(CountlyConfig *)config +- (void)populateServerConfig:(NSMutableDictionary *)serverConfig withConfig:(CountlyConfig *)config { if (config.requestTimeoutDuration <= 0) { @@ -278,7 +325,16 @@ - (void)populateServerConfig:(NSDictionary *)serverConfig withConfig:(CountlyCon return; } - NSDictionary *dictionary = serverConfig[kRConfig]; + NSMutableDictionary *dictionary; + if ([serverConfig[kRConfig] isKindOfClass:[NSDictionary class]]) + { + dictionary = [serverConfig[kRConfig] mutableCopy]; + } + else + { + CLY_LOG_D(@"%s, config is not a dictionary, omitting", __FUNCTION__); + return; + } if (!serverConfig[kRVersion] || !serverConfig[kRTimestamp]) { @@ -291,6 +347,58 @@ - (void)populateServerConfig:(NSDictionary *)serverConfig withConfig:(CountlyCon NSMutableString *logString = [NSMutableString stringWithString:@"Server Config: "]; + // Known keys set — used to remove unknown keys after validation + NSMutableSet *knownKeys = [NSMutableSet setWithArray:@[ + kTracking, + kNetworking, + kRSessionUpdateInterval, + kRReqQueueSize, + kREventQueueSize, + kRCrashReporting, + kRSessionTracking, + kRLogging, + kRLimitKeyLength, + kRLimitValueSize, + kRLimitSegValues, + kRLimitBreadcrumb, + kRLimitTraceLine, + kRLimitTraceLength, + kRCustomEventTracking, + kRViewTracking, + kREnterContentZone, + kRContentZoneInterval, + kRConsentRequired, + kRDropOldRequestTime, + kRServerConfigUpdateInterval, + kRLocationTracking, + kRRefreshContentZone, + kRbackoffMechanism, + kRBOMAcceptedTimeout, + kRBOMRQPercentage, + kRBOMRequestAge, + kRBOMDuration, + kRUserPropertyCacheLimit, + kREventBlacklist, + kREventWhitelist, + kRUserPropertyBlacklist, + kRUserPropertyWhitelist, + kRSegmentationBlacklist, + kRSegmentationWhitelist, + kREventSegmentationBlacklist, + kREventSegmentationWhitelist, + kRJourneyTriggerEvents + ]]; + + // Remove unknown keys + for (NSString *key in dictionary.allKeys) + { + if (![knownKeys containsObject:key]) + { + CLY_LOG_W(@"%s, Unknown config key '%@', removing", __FUNCTION__, key); + [dictionary removeObjectForKey:key]; + } + } + [self setBoolProperty:&_trackingEnabled fromDictionary:dictionary key:kTracking logString:logString]; [self setBoolProperty:&_networkingEnabled fromDictionary:dictionary key:kNetworking logString:logString]; [self setIntegerProperty:&_sessionInterval fromDictionary:dictionary key:kRSessionUpdateInterval logString:logString]; @@ -308,9 +416,9 @@ - (void)populateServerConfig:(NSDictionary *)serverConfig withConfig:(CountlyCon [self setBoolProperty:&_customEventTrackingEnabled fromDictionary:dictionary key:kRCustomEventTracking logString:logString]; [self setBoolProperty:&_viewTrackingEnabled fromDictionary:dictionary key:kRViewTracking logString:logString]; [self setBoolProperty:&_enterContentZone fromDictionary:dictionary key:kREnterContentZone logString:logString]; - [self setIntegerProperty:&_contentZoneInterval fromDictionary:dictionary key:kRContentZoneInterval logString:logString]; + [self setIntegerProperty:&_contentZoneInterval fromDictionary:dictionary key:kRContentZoneInterval minValue:16 logString:logString]; [self setBoolProperty:&_consentRequired fromDictionary:dictionary key:kRConsentRequired logString:logString]; - [self setIntegerProperty:&_dropOldRequestTime fromDictionary:dictionary key:kRDropOldRequestTime logString:logString]; + [self setIntegerProperty:&_dropOldRequestTime fromDictionary:dictionary key:kRDropOldRequestTime minValue:0 logString:logString]; [self setIntegerProperty:&_serverConfigUpdateInterval fromDictionary:dictionary key:kRServerConfigUpdateInterval logString:logString]; [self setBoolProperty:&_locationTracking fromDictionary:dictionary key:kRLocationTracking logString:logString]; [self setBoolProperty:&_refreshContentZone fromDictionary:dictionary key:kRRefreshContentZone logString:logString]; @@ -323,6 +431,9 @@ - (void)populateServerConfig:(NSDictionary *)serverConfig withConfig:(CountlyCon [self updateListingFilters:dictionary logString:logString]; + // Update the config dictionary with cleaned values + serverConfig[kRConfig] = dictionary; + if (![logString isEqualToString:@"Server Config: "]) { // means new config gotten, if that is the case notify SDK @@ -510,8 +621,8 @@ - (void)fetchServerConfig:(CountlyConfig *)config { NSMutableDictionary *persistentBehaviorSettings = [CountlyPersistency.sharedInstance retrieveServerConfig]; [self mergeBehaviorSettings:persistentBehaviorSettings withConfig:serverConfigResponse]; - [CountlyPersistency.sharedInstance storeServerConfig:persistentBehaviorSettings]; [self populateServerConfig:persistentBehaviorSettings withConfig:config]; + [CountlyPersistency.sharedInstance storeServerConfig:persistentBehaviorSettings]; } }; // Set default values @@ -745,48 +856,38 @@ - (NSInteger)userPropertyCacheLimit - (void)removeConflictingFilterKeys:(NSMutableDictionary *)mergedConfig newConfig:(NSDictionary *)newConfig { - // Remove listing filter keys from stored config based on new config - // If new config has any whitelist key, remove all blacklist keys from stored config - // If new config has any blacklist key, remove all whitelist keys from stored config - NSArray *whitelistKeys = @[ kREventWhitelist, kRSegmentationWhitelist, kREventSegmentationWhitelist, kRUserPropertyWhitelist ]; - NSArray *blacklistKeys = @[ kREventBlacklist, kRSegmentationBlacklist, kREventSegmentationBlacklist, kRUserPropertyBlacklist ]; - - BOOL newHasWhitelist = NO; - BOOL newHasBlacklist = NO; - for (NSString *key in whitelistKeys) + // Remove conflicting filter keys per category. + // Within each category, if new config provides a blacklist, remove stored whitelist (and vice versa). + NSArray *filterPairs = @[ + @[ kREventBlacklist, kREventWhitelist ], + @[ kRSegmentationBlacklist, kRSegmentationWhitelist ], + @[ kREventSegmentationBlacklist, kREventSegmentationWhitelist ], + @[ kRUserPropertyBlacklist, kRUserPropertyWhitelist ], + ]; + + for (NSArray *pair in filterPairs) { - if (newConfig[key]) - { - newHasWhitelist = YES; - break; - } - } - for (NSString *key in blacklistKeys) - { - if (newConfig[key]) - { - newHasBlacklist = YES; - break; - } - } + NSString *blacklistKey = pair[0]; + NSString *whitelistKey = pair[1]; - if (newHasWhitelist) - { - for (NSString *key in blacklistKeys) + // Only consider valid filter values (arrays/dicts) for conflict resolution + id blacklistVal = newConfig[blacklistKey]; + id whitelistVal = newConfig[whitelistKey]; + BOOL hasValidBlacklist = [blacklistVal isKindOfClass:NSArray.class] || [blacklistVal isKindOfClass:NSDictionary.class]; + BOOL hasValidWhitelist = [whitelistVal isKindOfClass:NSArray.class] || [whitelistVal isKindOfClass:NSDictionary.class]; + + if (hasValidBlacklist) { - [mergedConfig removeObjectForKey:key]; + [mergedConfig removeObjectForKey:whitelistKey]; } - } - if (newHasBlacklist) - { - for (NSString *key in whitelistKeys) + if (hasValidWhitelist) { - [mergedConfig removeObjectForKey:key]; + [mergedConfig removeObjectForKey:blacklistKey]; } } } -- (void)updateListingFilters:(NSDictionary *)dictionary logString:(NSMutableString *)logString +- (void)updateListingFilters:(NSMutableDictionary *)dictionary logString:(NSMutableString *)logString { // Event filter (eb/ew) - blacklist takes precedence NSArray *eb = dictionary[kREventBlacklist]; @@ -796,6 +897,8 @@ - (void)updateListingFilters:(NSDictionary *)dictionary logString:(NSMutableStri _eventFilterSet = [NSSet setWithArray:eb]; _eventFilterIsWhitelist = NO; [logString appendFormat:@"%@: %@, ", kREventBlacklist, eb]; + if (ew) + [dictionary removeObjectForKey:kREventWhitelist]; // blacklist takes precedence } else if ([ew isKindOfClass:NSArray.class]) { @@ -803,6 +906,13 @@ - (void)updateListingFilters:(NSDictionary *)dictionary logString:(NSMutableStri _eventFilterIsWhitelist = YES; [logString appendFormat:@"%@: %@, ", kREventWhitelist, ew]; } + else + { + if (eb) + [dictionary removeObjectForKey:kREventBlacklist]; + if (ew) + [dictionary removeObjectForKey:kREventWhitelist]; + } // User property filter (upb/upw) - blacklist takes precedence NSArray *upb = dictionary[kRUserPropertyBlacklist]; @@ -812,6 +922,8 @@ - (void)updateListingFilters:(NSDictionary *)dictionary logString:(NSMutableStri _userPropertyFilterSet = [NSSet setWithArray:upb]; _userPropertyFilterIsWhitelist = NO; [logString appendFormat:@"%@: %@, ", kRUserPropertyBlacklist, upb]; + if (upw) + [dictionary removeObjectForKey:kRUserPropertyWhitelist]; } else if ([upw isKindOfClass:NSArray.class]) { @@ -819,6 +931,13 @@ - (void)updateListingFilters:(NSDictionary *)dictionary logString:(NSMutableStri _userPropertyFilterIsWhitelist = YES; [logString appendFormat:@"%@: %@, ", kRUserPropertyWhitelist, upw]; } + else + { + if (upb) + [dictionary removeObjectForKey:kRUserPropertyBlacklist]; + if (upw) + [dictionary removeObjectForKey:kRUserPropertyWhitelist]; + } // Segmentation filter (sb/sw) - blacklist takes precedence NSArray *sb = dictionary[kRSegmentationBlacklist]; @@ -828,6 +947,8 @@ - (void)updateListingFilters:(NSDictionary *)dictionary logString:(NSMutableStri _segmentationFilterSet = [NSSet setWithArray:sb]; _segmentationFilterIsWhitelist = NO; [logString appendFormat:@"%@: %@, ", kRSegmentationBlacklist, sb]; + if (sw) + [dictionary removeObjectForKey:kRSegmentationWhitelist]; } else if ([sw isKindOfClass:NSArray.class]) { @@ -835,6 +956,13 @@ - (void)updateListingFilters:(NSDictionary *)dictionary logString:(NSMutableStri _segmentationFilterIsWhitelist = YES; [logString appendFormat:@"%@: %@, ", kRSegmentationWhitelist, sw]; } + else + { + if (sb) + [dictionary removeObjectForKey:kRSegmentationBlacklist]; + if (sw) + [dictionary removeObjectForKey:kRSegmentationWhitelist]; + } // Event segmentation filter (esb/esw) - blacklist takes precedence NSDictionary *esb = dictionary[kREventSegmentationBlacklist]; @@ -851,6 +979,8 @@ - (void)updateListingFilters:(NSDictionary *)dictionary logString:(NSMutableStri _eventSegmentationFilterMap = map.copy; _eventSegmentationFilterIsWhitelist = NO; [logString appendFormat:@"%@: %@, ", kREventSegmentationBlacklist, esb]; + if (esw) + [dictionary removeObjectForKey:kREventSegmentationWhitelist]; } else if ([esw isKindOfClass:NSDictionary.class]) { @@ -865,6 +995,13 @@ - (void)updateListingFilters:(NSDictionary *)dictionary logString:(NSMutableStri _eventSegmentationFilterIsWhitelist = YES; [logString appendFormat:@"%@: %@, ", kREventSegmentationWhitelist, esw]; } + else + { + if (esb) + [dictionary removeObjectForKey:kREventSegmentationBlacklist]; + if (esw) + [dictionary removeObjectForKey:kREventSegmentationWhitelist]; + } // Journey trigger events (jte) NSArray *jte = dictionary[kRJourneyTriggerEvents]; @@ -873,6 +1010,11 @@ - (void)updateListingFilters:(NSDictionary *)dictionary logString:(NSMutableStri _journeyTriggerEvents = [NSSet setWithArray:jte]; [logString appendFormat:@"%@: %@, ", kRJourneyTriggerEvents, jte]; } + else + { + if (jte) + [dictionary removeObjectForKey:kRJourneyTriggerEvents]; + } } - (BOOL)shouldRecordEvent:(NSString *)eventKey From 7c75387c458ee37cc762667ac94f4bccfeb416dd Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Mon, 6 Apr 2026 12:46:11 +0300 Subject: [PATCH 15/42] chore: revert format --- Countly.m | 2053 ++++++++++++++++++------------------ CountlyCommon.h | 115 +- CountlyCommon.m | 872 ++++++++------- CountlyConnectionManager.m | 2042 ++++++++++++++++++----------------- CountlyServerConfig.m | 1566 +++++++++++++-------------- 5 files changed, 3293 insertions(+), 3355 deletions(-) diff --git a/Countly.m b/Countly.m index a018a0c6..09d9dc2c 100644 --- a/Countly.m +++ b/Countly.m @@ -6,19 +6,20 @@ #import "CountlyCommon.h" -@interface Countly () { - NSTimer *timer; - BOOL isSuspended; +@interface Countly () +{ + NSTimer* timer; + BOOL isSuspended; } @end long long appLoadStartTime; // It holds the event id of previous recorded custom event. -static NSString *previousEventID; +static NSString* previousEventID; // It holds the event name of previous recorded custom event. -static NSString *previousEventName; +static NSString* previousEventName; #if __has_include() - #import +#import static os_unfair_lock previousEventLock = OS_UNFAIR_LOCK_INIT; #endif @implementation Countly @@ -27,1583 +28,1589 @@ @implementation Countly + (void)load { - [super load]; - - appLoadStartTime = floor(NSDate.date.timeIntervalSince1970 * 1000); + [super load]; + + appLoadStartTime = floor(NSDate.date.timeIntervalSince1970 * 1000); } -static Countly *s_sharedCountly = nil; +static Countly *s_sharedCountly = nil; static dispatch_once_t onceToken; + (instancetype)sharedInstance { - dispatch_once(&onceToken, ^{ - s_sharedCountly = self.new; - }); - return s_sharedCountly; + dispatch_once(&onceToken, ^{s_sharedCountly = self.new;}); + return s_sharedCountly; } -- (void)resetInstance -{ - CLY_LOG_I(@"%s resetting the instance", __FUNCTION__); - // Invalidate timer to avoid callbacks to a deallocated instance between tests. - if (timer) - { - [timer invalidate]; - timer = nil; - } - // Remove all notification observers to avoid duplicate registrations after re-init in tests. - [NSNotificationCenter.defaultCenter removeObserver:self]; - isSuspended = NO; - onceToken = 0; - s_sharedCountly = nil; -} +- (void)resetInstance { + CLY_LOG_I(@"%s resetting the instance", __FUNCTION__); + // Invalidate timer to avoid callbacks to a deallocated instance between tests. + if (timer) { + [timer invalidate]; + timer = nil; + } + // Remove all notification observers to avoid duplicate registrations after re-init in tests. + [NSNotificationCenter.defaultCenter removeObserver:self]; + isSuspended = NO; + onceToken = 0; + s_sharedCountly = nil; + } - (instancetype)init { - if (self = [super init]) - { -#if (TARGET_OS_IOS || TARGET_OS_VISION || TARGET_OS_TV) - [NSNotificationCenter.defaultCenter addObserver:self selector:@selector(applicationDidEnterBackground:) name:UIApplicationDidEnterBackgroundNotification object:nil]; - [NSNotificationCenter.defaultCenter addObserver:self selector:@selector(applicationWillEnterForeground:) name:UIApplicationWillEnterForegroundNotification object:nil]; - [NSNotificationCenter.defaultCenter addObserver:self selector:@selector(applicationWillTerminate:) name:UIApplicationWillTerminateNotification object:nil]; - - [NSNotificationCenter.defaultCenter addObserver:self selector:@selector(applicationDidBecomeActive:) name:UIApplicationDidBecomeActiveNotification object:nil]; - [NSNotificationCenter.defaultCenter addObserver:self selector:@selector(applicationWillResignActive:) name:UIApplicationWillResignActiveNotification object:nil]; + if (self = [super init]) + { +#if (TARGET_OS_IOS || TARGET_OS_VISION || TARGET_OS_TV ) + [NSNotificationCenter.defaultCenter addObserver:self + selector:@selector(applicationDidEnterBackground:) + name:UIApplicationDidEnterBackgroundNotification + object:nil]; + [NSNotificationCenter.defaultCenter addObserver:self + selector:@selector(applicationWillEnterForeground:) + name:UIApplicationWillEnterForegroundNotification + object:nil]; + [NSNotificationCenter.defaultCenter addObserver:self + selector:@selector(applicationWillTerminate:) + name:UIApplicationWillTerminateNotification + object:nil]; + + [NSNotificationCenter.defaultCenter addObserver:self + selector:@selector(applicationDidBecomeActive:) name:UIApplicationDidBecomeActiveNotification + object:nil]; + [NSNotificationCenter.defaultCenter addObserver:self + selector:@selector(applicationWillResignActive:) name:UIApplicationWillResignActiveNotification + object:nil]; #elif (TARGET_OS_OSX) - [NSNotificationCenter.defaultCenter addObserver:self selector:@selector(applicationWillTerminate:) name:NSApplicationWillTerminateNotification object:nil]; + [NSNotificationCenter.defaultCenter addObserver:self + selector:@selector(applicationWillTerminate:) + name:NSApplicationWillTerminateNotification + object:nil]; #endif - } - - return self; + } + + return self; } - (void)startWithConfig:(CountlyConfig *)config { - if (CountlyCommon.sharedInstance.hasStarted_) - return; - - CountlyCommon.sharedInstance.hasStarted = YES; - CountlyCommon.sharedInstance.enableDebug = config.enableDebug; - CountlyCommon.sharedInstance.shouldIgnoreTrustCheck = config.shouldIgnoreTrustCheck; - CountlyCommon.sharedInstance.loggerDelegate = config.loggerDelegate; - CountlyCommon.sharedInstance.internalLogLevel = config.internalLogLevel; - - config = [self checkAndFixInternalLimitsConfig:config]; - - if (config.disableSDKBehaviorSettingsUpdates) - { - [CountlyServerConfig.sharedInstance disableSDKBehaviourSettings]; - } - [CountlyServerConfig.sharedInstance retrieveServerConfigFromStorage:config]; - - CountlyCommon.sharedInstance.maxKeyLength = config.sdkInternalLimits.getMaxKeyLength; - CountlyCommon.sharedInstance.maxValueLength = config.sdkInternalLimits.getMaxValueSize; - CountlyCommon.sharedInstance.maxSegmentationValues = config.sdkInternalLimits.getMaxSegmentationValues; - - // For backward compatibility, deprecated values are only set incase new values are not provided using sdkInternalLimits interface - if (CountlyCommon.sharedInstance.maxKeyLength == kCountlyMaxKeyLength && config.maxKeyLength != kCountlyMaxKeyLength) - { - CountlyCommon.sharedInstance.maxKeyLength = config.maxKeyLength; - CLY_LOG_I(@"%s deprecated maxKeyLength provided, maxKeyLength: [%lu]", __FUNCTION__, (unsigned long)config.maxKeyLength); - } - if (CountlyCommon.sharedInstance.maxValueLength == kCountlyMaxValueSize && config.maxValueLength != kCountlyMaxValueSize) - { - CountlyCommon.sharedInstance.maxValueLength = config.maxValueLength; - CLY_LOG_I(@"%s deprecated maxValueLength provided, maxValueLength: [%lu]", __FUNCTION__, (unsigned long)config.maxValueLength); - } - if (CountlyCommon.sharedInstance.maxSegmentationValues == kCountlyMaxSegmentationValues && config.maxSegmentationValues != kCountlyMaxSegmentationValues) - { - CountlyCommon.sharedInstance.maxSegmentationValues = config.maxSegmentationValues; - CLY_LOG_I(@"%s deprecated maxSegmentationValues provided, maxSegmentationValues: [%lu]", __FUNCTION__, (unsigned long)config.maxSegmentationValues); - } - - CountlyConsentManager.sharedInstance.requiresConsent = config.requiresConsent; - - if (!config.appKey.length || [config.appKey isEqualToString:@"YOUR_APP_KEY"]) - [NSException raise:@"CountlyAppKeyNotSetException" format:@"appKey property on CountlyConfig object is not set"]; - - if (!config.host.length || [config.host isEqualToString:@"https://YOUR_COUNTLY_SERVER"]) - [NSException raise:@"CountlyHostNotSetException" format:@"host property on CountlyConfig object is not set"]; - - CLY_LOG_I(@"%s initializing, appKey: [%@], serverUrl: [%@], sdkName: [%@], sdkVersion: [%@], device: [%@], osName: [%@], osVersion: [%@], defaultSDKName: [%@], defaultSDKVersion: [%@]", __FUNCTION__, config.appKey, config.host, CountlyCommon.sharedInstance.SDKName, - CountlyCommon.sharedInstance.SDKVersion, CountlyDeviceInfo.device, CountlyDeviceInfo.osName, CountlyDeviceInfo.osVersion, kCountlySDKName, kCountlySDKVersion); - - if (!CountlyDeviceInfo.sharedInstance.deviceID || config.resetStoredDeviceID) - { - [self storeCustomDeviceIDState:config.deviceID]; - - [CountlyDeviceInfo.sharedInstance initializeDeviceID:config.deviceID]; - } - - CountlyConnectionManager.sharedInstance.appKey = config.appKey; - CountlyConnectionManager.sharedInstance.host = config.host; - CountlyConnectionManager.sharedInstance.alwaysUsePOST = config.alwaysUsePOST; - CountlyConnectionManager.sharedInstance.pinnedCertificates = config.pinnedCertificates; - CountlyConnectionManager.sharedInstance.secretSalt = config.secretSalt; - CountlyConnectionManager.sharedInstance.URLSessionConfiguration = config.URLSessionConfiguration; - - CountlyPersistency.sharedInstance.eventSendThreshold = config.eventSendThreshold; - CountlyPersistency.sharedInstance.requestDropAgeHours = config.requestDropAgeHours; - CountlyPersistency.sharedInstance.storedRequestsLimit = MAX(1, config.storedRequestsLimit); - - CountlyCommon.sharedInstance.manualSessionHandling = config.manualSessionHandling; - CountlyCommon.sharedInstance.enableManualSessionControlHybridMode = config.enableManualSessionControlHybridMode; - - CountlyCommon.sharedInstance.attributionID = config.attributionID; - - NSDictionary *customMetricsTruncated = [config.customMetrics cly_truncated:@"Custom metric"]; - CountlyDeviceInfo.sharedInstance.customMetrics = [customMetricsTruncated cly_limited:@"Custom metric"]; - - [Countly.user save]; - // If something added related to server config, make sure to check CountlyServerConfig.notifySdkConfigChange - [CountlyServerConfig.sharedInstance fetchServerConfig:config]; - + if (CountlyCommon.sharedInstance.hasStarted_) + return; + + CountlyCommon.sharedInstance.hasStarted = YES; + CountlyCommon.sharedInstance.enableDebug = config.enableDebug; + CountlyCommon.sharedInstance.shouldIgnoreTrustCheck = config.shouldIgnoreTrustCheck; + CountlyCommon.sharedInstance.loggerDelegate = config.loggerDelegate; + CountlyCommon.sharedInstance.internalLogLevel = config.internalLogLevel; + + config = [self checkAndFixInternalLimitsConfig:config]; + + if (config.disableSDKBehaviorSettingsUpdates) { + [CountlyServerConfig.sharedInstance disableSDKBehaviourSettings]; + } + [CountlyServerConfig.sharedInstance retrieveServerConfigFromStorage:config]; + + CountlyCommon.sharedInstance.maxKeyLength = config.sdkInternalLimits.getMaxKeyLength; + CountlyCommon.sharedInstance.maxValueLength = config.sdkInternalLimits.getMaxValueSize; + CountlyCommon.sharedInstance.maxSegmentationValues = config.sdkInternalLimits.getMaxSegmentationValues; + + // For backward compatibility, deprecated values are only set incase new values are not provided using sdkInternalLimits interface + if(CountlyCommon.sharedInstance.maxKeyLength == kCountlyMaxKeyLength && config.maxKeyLength != kCountlyMaxKeyLength) { + CountlyCommon.sharedInstance.maxKeyLength = config.maxKeyLength; + CLY_LOG_I(@"%s deprecated maxKeyLength provided, maxKeyLength: [%lu]", __FUNCTION__, (unsigned long)config.maxKeyLength); + } + if(CountlyCommon.sharedInstance.maxValueLength == kCountlyMaxValueSize && config.maxValueLength != kCountlyMaxValueSize) { + CountlyCommon.sharedInstance.maxValueLength = config.maxValueLength; + CLY_LOG_I(@"%s deprecated maxValueLength provided, maxValueLength: [%lu]", __FUNCTION__, (unsigned long)config.maxValueLength); + } + if(CountlyCommon.sharedInstance.maxSegmentationValues == kCountlyMaxSegmentationValues && config.maxSegmentationValues != kCountlyMaxSegmentationValues) { + CountlyCommon.sharedInstance.maxSegmentationValues = config.maxSegmentationValues; + CLY_LOG_I(@"%s deprecated maxSegmentationValues provided, maxSegmentationValues: [%lu]", __FUNCTION__, (unsigned long)config.maxSegmentationValues); + } + + CountlyConsentManager.sharedInstance.requiresConsent = config.requiresConsent; + + if (!config.appKey.length || [config.appKey isEqualToString:@"YOUR_APP_KEY"]) + [NSException raise:@"CountlyAppKeyNotSetException" format:@"appKey property on CountlyConfig object is not set"]; + + if (!config.host.length || [config.host isEqualToString:@"https://YOUR_COUNTLY_SERVER"]) + [NSException raise:@"CountlyHostNotSetException" format:@"host property on CountlyConfig object is not set"]; + + CLY_LOG_I(@"%s initializing, appKey: [%@], serverUrl: [%@], sdkName: [%@], sdkVersion: [%@], device: [%@], osName: [%@], osVersion: [%@], defaultSDKName: [%@], defaultSDKVersion: [%@]", + __FUNCTION__, config.appKey, config.host, CountlyCommon.sharedInstance.SDKName, CountlyCommon.sharedInstance.SDKVersion, CountlyDeviceInfo.device, + CountlyDeviceInfo.osName, CountlyDeviceInfo.osVersion,kCountlySDKName, kCountlySDKVersion); + + + if (!CountlyDeviceInfo.sharedInstance.deviceID || config.resetStoredDeviceID) + { + [self storeCustomDeviceIDState:config.deviceID]; + + [CountlyDeviceInfo.sharedInstance initializeDeviceID:config.deviceID]; + } + + CountlyConnectionManager.sharedInstance.appKey = config.appKey; + CountlyConnectionManager.sharedInstance.host = config.host; + CountlyConnectionManager.sharedInstance.alwaysUsePOST = config.alwaysUsePOST; + CountlyConnectionManager.sharedInstance.pinnedCertificates = config.pinnedCertificates; + CountlyConnectionManager.sharedInstance.secretSalt = config.secretSalt; + CountlyConnectionManager.sharedInstance.URLSessionConfiguration = config.URLSessionConfiguration; + + CountlyPersistency.sharedInstance.eventSendThreshold = config.eventSendThreshold; + CountlyPersistency.sharedInstance.requestDropAgeHours = config.requestDropAgeHours; + CountlyPersistency.sharedInstance.storedRequestsLimit = MAX(1, config.storedRequestsLimit); + + CountlyCommon.sharedInstance.manualSessionHandling = config.manualSessionHandling; + CountlyCommon.sharedInstance.enableManualSessionControlHybridMode = config.enableManualSessionControlHybridMode; + + CountlyCommon.sharedInstance.attributionID = config.attributionID; + + NSDictionary* customMetricsTruncated = [config.customMetrics cly_truncated:@"Custom metric"]; + CountlyDeviceInfo.sharedInstance.customMetrics = [customMetricsTruncated cly_limited:@"Custom metric"]; + + [Countly.user save]; + // If something added related to server config, make sure to check CountlyServerConfig.notifySdkConfigChange + [CountlyServerConfig.sharedInstance fetchServerConfig:config]; + #if (TARGET_OS_IOS) - CountlyFeedbacksInternal.sharedInstance.message = config.starRatingMessage; - CountlyFeedbacksInternal.sharedInstance.sessionCount = config.starRatingSessionCount; - CountlyFeedbacksInternal.sharedInstance.disableAskingForEachAppVersion = config.starRatingDisableAskingForEachAppVersion; - CountlyFeedbacksInternal.sharedInstance.ratingCompletionForAutoAsk = config.starRatingCompletion; - [CountlyFeedbacksInternal.sharedInstance checkForStarRatingAutoAsk]; + CountlyFeedbacksInternal.sharedInstance.message = config.starRatingMessage; + CountlyFeedbacksInternal.sharedInstance.sessionCount = config.starRatingSessionCount; + CountlyFeedbacksInternal.sharedInstance.disableAskingForEachAppVersion = config.starRatingDisableAskingForEachAppVersion; + CountlyFeedbacksInternal.sharedInstance.ratingCompletionForAutoAsk = config.starRatingCompletion; + [CountlyFeedbacksInternal.sharedInstance checkForStarRatingAutoAsk]; #endif + + if(config.disableLocation) + { + [CountlyLocationManager.sharedInstance disableLocation]; + } + else + { + [CountlyLocationManager.sharedInstance updateLocation:config.location city:config.city ISOCountryCode:config.ISOCountryCode IP:config.IP]; + } + + if (!CountlyCommon.sharedInstance.manualSessionHandling) + [CountlyConnectionManager.sharedInstance beginSession]; + else + [CountlyCommon.sharedInstance recordOrientation]; + + //NOTE: If there is no consent for sessions, location info and attribution should be sent separately, as they cannot be sent with begin_session request. - if (config.disableLocation) - { - [CountlyLocationManager.sharedInstance disableLocation]; - } - else - { - [CountlyLocationManager.sharedInstance updateLocation:config.location city:config.city ISOCountryCode:config.ISOCountryCode IP:config.IP]; - } - - if (!CountlyCommon.sharedInstance.manualSessionHandling) - [CountlyConnectionManager.sharedInstance beginSession]; - else - [CountlyCommon.sharedInstance recordOrientation]; - - // NOTE: If there is no consent for sessions, location info and attribution should be sent separately, as they cannot be sent with begin_session request. - -#if (TARGET_OS_IOS || TARGET_OS_VISION || TARGET_OS_OSX) - #ifndef COUNTLY_EXCLUDE_PUSHNOTIFICATIONS - if ([config.features containsObject:CLYPushNotifications]) - { - CountlyPushNotifications.sharedInstance.isEnabledOnInitialConfig = YES; - CountlyPushNotifications.sharedInstance.pushTestMode = config.pushTestMode; - CountlyPushNotifications.sharedInstance.sendPushTokenAlways = config.sendPushTokenAlways; - CountlyPushNotifications.sharedInstance.doNotShowAlertForNotifications = config.doNotShowAlertForNotifications; - CountlyPushNotifications.sharedInstance.launchNotification = config.launchNotification; - [CountlyPushNotifications.sharedInstance startPushNotifications]; - } - #endif +#if (TARGET_OS_IOS || TARGET_OS_VISION || TARGET_OS_OSX ) +#ifndef COUNTLY_EXCLUDE_PUSHNOTIFICATIONS + if ([config.features containsObject:CLYPushNotifications]) + { + CountlyPushNotifications.sharedInstance.isEnabledOnInitialConfig = YES; + CountlyPushNotifications.sharedInstance.pushTestMode = config.pushTestMode; + CountlyPushNotifications.sharedInstance.sendPushTokenAlways = config.sendPushTokenAlways; + CountlyPushNotifications.sharedInstance.doNotShowAlertForNotifications = config.doNotShowAlertForNotifications; + CountlyPushNotifications.sharedInstance.launchNotification = config.launchNotification; + [CountlyPushNotifications.sharedInstance startPushNotifications]; + } #endif - - if (config.crashes.crashFilterCallback) - { - [CountlyCrashReporter.sharedInstance setCrashFilterCallback:config.crashes.crashFilterCallback]; - } - - CountlyCrashReporter.sharedInstance.crashSegmentation = config.crashSegmentation; - CountlyCrashReporter.sharedInstance.crashLogLimit = config.sdkInternalLimits.getMaxBreadcrumbCount; - // For backward compatibility, deprecated values are only set incase new values are not provided using sdkInternalLimits interface - if (CountlyCrashReporter.sharedInstance.crashLogLimit == kCountlyMaxBreadcrumbCount && config.crashLogLimit != kCountlyMaxBreadcrumbCount) - { - CountlyCrashReporter.sharedInstance.crashLogLimit = MAX(1, config.crashLogLimit); - CLY_LOG_W(@"%s deprecated maxBreadcrumbCount provided, maxBreadcrumbCount: [%lu]", __FUNCTION__, (unsigned long)config.crashLogLimit); - } - CountlyCrashReporter.sharedInstance.crashFilter = config.crashFilter; - CountlyCrashReporter.sharedInstance.shouldUsePLCrashReporter = config.shouldUsePLCrashReporter; - CountlyCrashReporter.sharedInstance.shouldUseMachSignalHandler = config.shouldUseMachSignalHandler; - CountlyCrashReporter.sharedInstance.crashOccuredOnPreviousSessionCallback = config.crashOccuredOnPreviousSessionCallback; - CountlyCrashReporter.sharedInstance.shouldSendCrashReportCallback = config.shouldSendCrashReportCallback; - if ([config.features containsObject:CLYCrashReporting]) - { - CountlyCrashReporter.sharedInstance.isEnabledOnInitialConfig = YES; - if (CountlyServerConfig.sharedInstance.crashReportingEnabled) +#endif + + if(config.crashes.crashFilterCallback) { + [CountlyCrashReporter.sharedInstance setCrashFilterCallback:config.crashes.crashFilterCallback]; + } + + CountlyCrashReporter.sharedInstance.crashSegmentation = config.crashSegmentation; + CountlyCrashReporter.sharedInstance.crashLogLimit = config.sdkInternalLimits.getMaxBreadcrumbCount; + // For backward compatibility, deprecated values are only set incase new values are not provided using sdkInternalLimits interface + if(CountlyCrashReporter.sharedInstance.crashLogLimit == kCountlyMaxBreadcrumbCount && config.crashLogLimit != kCountlyMaxBreadcrumbCount) { + CountlyCrashReporter.sharedInstance.crashLogLimit = MAX(1, config.crashLogLimit); + CLY_LOG_W(@"%s deprecated maxBreadcrumbCount provided, maxBreadcrumbCount: [%lu]", __FUNCTION__, (unsigned long)config.crashLogLimit); + } + CountlyCrashReporter.sharedInstance.crashFilter = config.crashFilter; + CountlyCrashReporter.sharedInstance.shouldUsePLCrashReporter = config.shouldUsePLCrashReporter; + CountlyCrashReporter.sharedInstance.shouldUseMachSignalHandler = config.shouldUseMachSignalHandler; + CountlyCrashReporter.sharedInstance.crashOccuredOnPreviousSessionCallback = config.crashOccuredOnPreviousSessionCallback; + CountlyCrashReporter.sharedInstance.shouldSendCrashReportCallback = config.shouldSendCrashReportCallback; + if ([config.features containsObject:CLYCrashReporting]) { - [CountlyCrashReporter.sharedInstance startCrashReporting]; + CountlyCrashReporter.sharedInstance.isEnabledOnInitialConfig = YES; + if (CountlyServerConfig.sharedInstance.crashReportingEnabled) + { + [CountlyCrashReporter.sharedInstance startCrashReporting]; + } } - } -#if (TARGET_OS_IOS || TARGET_OS_TV) - if (config.enableAutomaticViewTracking || [config.features containsObject:CLYAutoViewTracking]) - { - // Print deprecation flag for feature - CountlyViewTrackingInternal.sharedInstance.isEnabledOnInitialConfig = YES; - if (CountlyServerConfig.sharedInstance.viewTrackingEnabled) +#if (TARGET_OS_IOS || TARGET_OS_TV ) + if (config.enableAutomaticViewTracking || [config.features containsObject:CLYAutoViewTracking]) { - [CountlyViewTrackingInternal.sharedInstance startAutoViewTracking]; + // Print deprecation flag for feature + CountlyViewTrackingInternal.sharedInstance.isEnabledOnInitialConfig = YES; + if (CountlyServerConfig.sharedInstance.viewTrackingEnabled) + { + [CountlyViewTrackingInternal.sharedInstance startAutoViewTracking]; + } + } + if (config.automaticViewTrackingExclusionList) { + [CountlyViewTrackingInternal.sharedInstance addAutoViewTrackingExclutionList:config.automaticViewTrackingExclusionList]; } - } - if (config.automaticViewTrackingExclusionList) - { - [CountlyViewTrackingInternal.sharedInstance addAutoViewTrackingExclutionList:config.automaticViewTrackingExclusionList]; - } #endif - - if (config.disableViewRestartForManualRecording) - { - CountlyViewTrackingInternal.sharedInstance.isManualViewRestartActive = NO; - } - - if (config.experimental.enablePreviousNameRecording) - { - CountlyViewTrackingInternal.sharedInstance.enablePreviousNameRecording = YES; - } - if (config.experimental.enableVisibiltyTracking) - { - CountlyCommon.sharedInstance.enableVisibiltyTracking = YES; - } - if (config.globalViewSegmentation) - { - [CountlyViewTrackingInternal.sharedInstance setGlobalViewSegmentation:config.globalViewSegmentation]; - } - timer = [NSTimer timerWithTimeInterval:config.updateSessionPeriod target:self selector:@selector(onTimer:) userInfo:nil repeats:YES]; - [NSRunLoop.mainRunLoop addTimer:timer forMode:NSRunLoopCommonModes]; - - CountlyRemoteConfigInternal.sharedInstance.isRCAutomaticTriggersEnabled = config.enableRemoteConfigAutomaticTriggers || config.enableRemoteConfig; - CountlyRemoteConfigInternal.sharedInstance.isRCValueCachingEnabled = config.enableRemoteConfigValueCaching; - CountlyRemoteConfigInternal.sharedInstance.remoteConfigCompletionHandler = config.remoteConfigCompletionHandler; - if (config.getRemoteConfigGlobalCallbacks) - { - CountlyRemoteConfigInternal.sharedInstance.remoteConfigGlobalCallbacks = config.getRemoteConfigGlobalCallbacks; - } - if (config.enrollABOnRCDownload) - { - CountlyRemoteConfigInternal.sharedInstance.enrollABOnRCDownload = config.enrollABOnRCDownload; - } - [CountlyRemoteConfigInternal.sharedInstance downloadRemoteConfigAutomatically]; - if (config.apm.getAppStartTimestampOverride) - { - appLoadStartTime = config.apm.getAppStartTimestampOverride; - } + + if(config.disableViewRestartForManualRecording){ + CountlyViewTrackingInternal.sharedInstance.isManualViewRestartActive = NO; + } + + if(config.experimental.enablePreviousNameRecording) { + CountlyViewTrackingInternal.sharedInstance.enablePreviousNameRecording = YES; + } + if(config.experimental.enableVisibiltyTracking) { + CountlyCommon.sharedInstance.enableVisibiltyTracking = YES; + } + if (config.globalViewSegmentation) { + [CountlyViewTrackingInternal.sharedInstance setGlobalViewSegmentation:config.globalViewSegmentation]; + } + timer = [NSTimer timerWithTimeInterval:config.updateSessionPeriod target:self selector:@selector(onTimer:) userInfo:nil repeats:YES]; + [NSRunLoop.mainRunLoop addTimer:timer forMode:NSRunLoopCommonModes]; + + CountlyRemoteConfigInternal.sharedInstance.isRCAutomaticTriggersEnabled = config.enableRemoteConfigAutomaticTriggers || config.enableRemoteConfig; + CountlyRemoteConfigInternal.sharedInstance.isRCValueCachingEnabled = config.enableRemoteConfigValueCaching; + CountlyRemoteConfigInternal.sharedInstance.remoteConfigCompletionHandler = config.remoteConfigCompletionHandler; + if (config.getRemoteConfigGlobalCallbacks) { + CountlyRemoteConfigInternal.sharedInstance.remoteConfigGlobalCallbacks = config.getRemoteConfigGlobalCallbacks; + } + if (config.enrollABOnRCDownload) { + CountlyRemoteConfigInternal.sharedInstance.enrollABOnRCDownload = config.enrollABOnRCDownload; + } + [CountlyRemoteConfigInternal.sharedInstance downloadRemoteConfigAutomatically]; + if (config.apm.getAppStartTimestampOverride) { + appLoadStartTime = config.apm.getAppStartTimestampOverride; + } #if (TARGET_OS_IOS) - if (config.content.getGlobalContentCallback) - { - CountlyContentBuilderInternal.sharedInstance.contentCallback = config.content.getGlobalContentCallback; - } - if (config.content.getZoneTimerInterval) - { - CountlyContentBuilderInternal.sharedInstance.zoneTimerInterval = config.content.getZoneTimerInterval; - } - if (config.content.getWebViewDisplayOption) - { - CountlyContentBuilderInternal.sharedInstance.webViewDisplayOption = config.content.getWebViewDisplayOption; - } + if(config.content.getGlobalContentCallback) { + CountlyContentBuilderInternal.sharedInstance.contentCallback = config.content.getGlobalContentCallback; + } + if(config.content.getZoneTimerInterval){ + CountlyContentBuilderInternal.sharedInstance.zoneTimerInterval = config.content.getZoneTimerInterval; + } + if(config.content.getWebViewDisplayOption){ + CountlyContentBuilderInternal.sharedInstance.webViewDisplayOption = config.content.getWebViewDisplayOption; + } #endif + + [CountlyPerformanceMonitoring.sharedInstance startWithConfig:config.apm]; + + CountlyCommon.sharedInstance.enableOrientationTracking = config.enableOrientationTracking; + [CountlyCommon.sharedInstance observeDeviceOrientationChanges]; + + [CountlyConnectionManager.sharedInstance proceedOnQueue]; + + //TODO: Should move at the top after checking the the edge cases of current implementation + if (config.enableAllConsents) + [self giveAllConsents]; + else if (config.consents) + [self giveConsentForFeatures:config.consents]; + else if (config.requiresConsent) + [CountlyConsentManager.sharedInstance sendConsents]; + + if (!CountlyConsentManager.sharedInstance.consentForSessions) + { + //Send an empty location if location is disabled or location consent is not given, without checking for location consent. + if (!CountlyConsentManager.sharedInstance.consentForLocation || CountlyLocationManager.sharedInstance.isLocationInfoDisabled) + { + [CountlyConnectionManager.sharedInstance sendLocationInfo]; + } + else + { + [CountlyLocationManager.sharedInstance sendLocationInfo]; + } + [CountlyConnectionManager.sharedInstance sendAttribution]; + } + + + if (config.campaignType && config.campaignData) + [self recordDirectAttributionWithCampaignType:config.campaignType andCampaignData:config.campaignData]; + + if (config.indirectAttribution) + [self recordIndirectAttribution:config.indirectAttribution]; + + [CountlyHealthTracker.sharedInstance sendHealthCheck]; - [CountlyPerformanceMonitoring.sharedInstance startWithConfig:config.apm]; - - CountlyCommon.sharedInstance.enableOrientationTracking = config.enableOrientationTracking; - [CountlyCommon.sharedInstance observeDeviceOrientationChanges]; - - [CountlyConnectionManager.sharedInstance proceedOnQueue]; - - // TODO: Should move at the top after checking the the edge cases of current implementation - if (config.enableAllConsents) - [self giveAllConsents]; - else if (config.consents) - [self giveConsentForFeatures:config.consents]; - else if (config.requiresConsent) - [CountlyConsentManager.sharedInstance sendConsents]; + CountlyCommon.sharedInstance.hasFinishedInit = YES; +} - if (!CountlyConsentManager.sharedInstance.consentForSessions) - { - // Send an empty location if location is disabled or location consent is not given, without checking for location consent. - if (!CountlyConsentManager.sharedInstance.consentForLocation || CountlyLocationManager.sharedInstance.isLocationInfoDisabled) +- (CountlyConfig *) checkAndFixInternalLimitsConfig:(CountlyConfig *)config +{ + if (config.sdkInternalLimits.getMaxKeyLength == 0) { + [config.sdkInternalLimits setMaxKeyLength:kCountlyMaxKeyLength]; + CLY_LOG_W(@"%s Ignoring provided value of %lu for 'maxKeyLength' because it's less than 1", __FUNCTION__, (unsigned long)config.sdkInternalLimits.getMaxKeyLength); + } + else if(config.sdkInternalLimits.getMaxKeyLength != kCountlyMaxKeyLength) { - [CountlyConnectionManager.sharedInstance sendLocationInfo]; + CLY_LOG_I(@"%s provided 'maxKeyLength' override:[ %lu ]", __FUNCTION__, (unsigned long)config.sdkInternalLimits.getMaxKeyLength); } - else + + if (config.sdkInternalLimits.getMaxValueSize == 0) { + [config.sdkInternalLimits setMaxValueSize:kCountlyMaxValueSize]; + CLY_LOG_W(@"%s Ignoring provided value of %lu for 'maxValueSize' because it's less than 1", __FUNCTION__, (unsigned long)config.sdkInternalLimits.getMaxValueSize); + } + else if(config.sdkInternalLimits.getMaxValueSize != kCountlyMaxValueSize) { - [CountlyLocationManager.sharedInstance sendLocationInfo]; + CLY_LOG_I(@"%s provided 'maxValueSize' override:[ %lu ]", __FUNCTION__, (unsigned long)config.sdkInternalLimits.getMaxValueSize); } - [CountlyConnectionManager.sharedInstance sendAttribution]; - } - - if (config.campaignType && config.campaignData) - [self recordDirectAttributionWithCampaignType:config.campaignType andCampaignData:config.campaignData]; - - if (config.indirectAttribution) - [self recordIndirectAttribution:config.indirectAttribution]; - - [CountlyHealthTracker.sharedInstance sendHealthCheck]; - - CountlyCommon.sharedInstance.hasFinishedInit = YES; -} - -- (CountlyConfig *)checkAndFixInternalLimitsConfig:(CountlyConfig *)config -{ - if (config.sdkInternalLimits.getMaxKeyLength == 0) - { - [config.sdkInternalLimits setMaxKeyLength:kCountlyMaxKeyLength]; - CLY_LOG_W(@"%s Ignoring provided value of %lu for 'maxKeyLength' because it's less than 1", __FUNCTION__, (unsigned long)config.sdkInternalLimits.getMaxKeyLength); - } - else if (config.sdkInternalLimits.getMaxKeyLength != kCountlyMaxKeyLength) - { - CLY_LOG_I(@"%s provided 'maxKeyLength' override:[ %lu ]", __FUNCTION__, (unsigned long)config.sdkInternalLimits.getMaxKeyLength); - } - - if (config.sdkInternalLimits.getMaxValueSize == 0) - { - [config.sdkInternalLimits setMaxValueSize:kCountlyMaxValueSize]; - CLY_LOG_W(@"%s Ignoring provided value of %lu for 'maxValueSize' because it's less than 1", __FUNCTION__, (unsigned long)config.sdkInternalLimits.getMaxValueSize); - } - else if (config.sdkInternalLimits.getMaxValueSize != kCountlyMaxValueSize) - { - CLY_LOG_I(@"%s provided 'maxValueSize' override:[ %lu ]", __FUNCTION__, (unsigned long)config.sdkInternalLimits.getMaxValueSize); - } - - if (config.sdkInternalLimits.getMaxSegmentationValues == 0) - { - [config.sdkInternalLimits setMaxSegmentationValues:kCountlyMaxSegmentationValues]; - CLY_LOG_W(@"%s Ignoring provided value of %lu for 'maxSegmentationValues' because it's less than 1", __FUNCTION__, (unsigned long)config.sdkInternalLimits.getMaxSegmentationValues); - } - else if (config.sdkInternalLimits.getMaxSegmentationValues != kCountlyMaxSegmentationValues) - { - CLY_LOG_I(@"%s provided 'maxSegmentationValues' override:[ %lu ]", __FUNCTION__, (unsigned long)config.sdkInternalLimits.getMaxSegmentationValues); - } - - if (config.sdkInternalLimits.getMaxBreadcrumbCount == 0) - { - [config.sdkInternalLimits setMaxBreadcrumbCount:kCountlyMaxBreadcrumbCount]; - CLY_LOG_W(@"%s Ignoring provided value of %lu for 'maxBreadcrumbCount' because it's less than 1", __FUNCTION__, (unsigned long)config.sdkInternalLimits.getMaxBreadcrumbCount); - } - else if (config.sdkInternalLimits.getMaxBreadcrumbCount != kCountlyMaxBreadcrumbCount) - { - CLY_LOG_I(@"%s provided 'maxBreadcrumbCount' override:[ %lu ]", __FUNCTION__, (unsigned long)config.sdkInternalLimits.getMaxBreadcrumbCount); - } - - if (config.sdkInternalLimits.getMaxStackTraceLineLength != kCountlyMaxStackTraceLinesPerThread) - { - CLY_LOG_W(@"%s 'maxStackTraceLineLength' is currently a placeholder and doesn't actively utilize the set values.", __FUNCTION__); - } - - if (config.sdkInternalLimits.getMaxStackTraceLinesPerThread != kCountlyMaxStackTraceLineLength) - { - CLY_LOG_I(@"%s 'maxStackTraceLinesPerThread' is currently a placeholder and doesn't actively utilize the set values.", __FUNCTION__); - } - return config; + + if (config.sdkInternalLimits.getMaxSegmentationValues == 0) { + [config.sdkInternalLimits setMaxSegmentationValues:kCountlyMaxSegmentationValues]; + CLY_LOG_W(@"%s Ignoring provided value of %lu for 'maxSegmentationValues' because it's less than 1", __FUNCTION__, (unsigned long)config.sdkInternalLimits.getMaxSegmentationValues); + } + else if(config.sdkInternalLimits.getMaxSegmentationValues != kCountlyMaxSegmentationValues) + { + CLY_LOG_I(@"%s provided 'maxSegmentationValues' override:[ %lu ]", __FUNCTION__, (unsigned long)config.sdkInternalLimits.getMaxSegmentationValues); + } + + if (config.sdkInternalLimits.getMaxBreadcrumbCount == 0) { + [config.sdkInternalLimits setMaxBreadcrumbCount:kCountlyMaxBreadcrumbCount]; + CLY_LOG_W(@"%s Ignoring provided value of %lu for 'maxBreadcrumbCount' because it's less than 1", __FUNCTION__, (unsigned long)config.sdkInternalLimits.getMaxBreadcrumbCount); + } + else if(config.sdkInternalLimits.getMaxBreadcrumbCount != kCountlyMaxBreadcrumbCount) + { + CLY_LOG_I(@"%s provided 'maxBreadcrumbCount' override:[ %lu ]", __FUNCTION__, (unsigned long)config.sdkInternalLimits.getMaxBreadcrumbCount); + } + + if(config.sdkInternalLimits.getMaxStackTraceLineLength != kCountlyMaxStackTraceLinesPerThread) + { + CLY_LOG_W(@"%s 'maxStackTraceLineLength' is currently a placeholder and doesn't actively utilize the set values.", __FUNCTION__); + } + + if(config.sdkInternalLimits.getMaxStackTraceLinesPerThread != kCountlyMaxStackTraceLineLength) + { + CLY_LOG_I(@"%s 'maxStackTraceLinesPerThread' is currently a placeholder and doesn't actively utilize the set values.", __FUNCTION__); + } + return config; } #pragma mark - - (void)onTimer:(NSTimer *)timer { - CLY_LOG_D(@"%s tick is happening sending events, manualSessions: [%d], hybridSessions: [%d], isSuspended: [%d]", __FUNCTION__, CountlyCommon.sharedInstance.manualSessionHandling, CountlyCommon.sharedInstance.enableManualSessionControlHybridMode, isSuspended); - if (isSuspended) - return; - - if (!CountlyCommon.sharedInstance.manualSessionHandling) - { - [CountlyConnectionManager.sharedInstance updateSession]; - } - // this condtion is called only when both manual session handling and hybrid mode is enabled. - else if (CountlyCommon.sharedInstance.enableManualSessionControlHybridMode) - { - [CountlyConnectionManager.sharedInstance updateSession]; - } - - [CountlyConnectionManager.sharedInstance sendEventsWithSaveIfNeeded]; + CLY_LOG_D(@"%s tick is happening sending events, manualSessions: [%d], hybridSessions: [%d], isSuspended: [%d]", __FUNCTION__, CountlyCommon.sharedInstance.manualSessionHandling, CountlyCommon.sharedInstance.enableManualSessionControlHybridMode, isSuspended); + if (isSuspended) + return; + + if (!CountlyCommon.sharedInstance.manualSessionHandling) + { + [CountlyConnectionManager.sharedInstance updateSession]; + } + // this condtion is called only when both manual session handling and hybrid mode is enabled. + else if (CountlyCommon.sharedInstance.enableManualSessionControlHybridMode) + { + [CountlyConnectionManager.sharedInstance updateSession]; + } + + [CountlyConnectionManager.sharedInstance sendEventsWithSaveIfNeeded]; } - (void)suspend { #if (TARGET_OS_WATCH) - CLY_LOG_I(@"%s", __FUNCTION__); + CLY_LOG_I(@"%s", __FUNCTION__); #endif - - if (!CountlyCommon.sharedInstance.hasStarted) - return; - - if (isSuspended) - return; - - CLY_LOG_D(@"%s sending events, saving the state, manualSessions: [%d]", __FUNCTION__, CountlyCommon.sharedInstance.manualSessionHandling); - - isSuspended = YES; - - [CountlyViewTrackingInternal.sharedInstance applicationDidEnterBackground]; - - [CountlyConnectionManager.sharedInstance sendEventsWithSaveIfNeeded]; - - if (!CountlyCommon.sharedInstance.manualSessionHandling) - [CountlyConnectionManager.sharedInstance endSession]; - - [CountlyPersistency.sharedInstance saveToFile]; + + if (!CountlyCommon.sharedInstance.hasStarted) + return; + + if (isSuspended) + return; + + CLY_LOG_D(@"%s sending events, saving the state, manualSessions: [%d]", __FUNCTION__, CountlyCommon.sharedInstance.manualSessionHandling); + + isSuspended = YES; + + [CountlyViewTrackingInternal.sharedInstance applicationDidEnterBackground]; + + [CountlyConnectionManager.sharedInstance sendEventsWithSaveIfNeeded]; + + if (!CountlyCommon.sharedInstance.manualSessionHandling) + [CountlyConnectionManager.sharedInstance endSession]; + + [CountlyPersistency.sharedInstance saveToFile]; } - (void)resume { #if (TARGET_OS_WATCH) - CLY_LOG_I(@"%s", __FUNCTION__); + CLY_LOG_I(@"%s", __FUNCTION__); #endif - - if (!CountlyCommon.sharedInstance.hasStarted) - return; - + + if (!CountlyCommon.sharedInstance.hasStarted) + return; + #if (TARGET_OS_WATCH) - // NOTE: Skip first time to prevent double begin session because of applicationDidBecomeActive call on launch of watchOS apps - static BOOL isFirstCall = YES; - - if (isFirstCall) - { - isFirstCall = NO; - return; - } + //NOTE: Skip first time to prevent double begin session because of applicationDidBecomeActive call on launch of watchOS apps + static BOOL isFirstCall = YES; + + if (isFirstCall) + { + isFirstCall = NO; + return; + } #endif - - CLY_LOG_D(@"%s manualSessions: [%d]", __FUNCTION__, CountlyCommon.sharedInstance.manualSessionHandling); - - if (!CountlyCommon.sharedInstance.manualSessionHandling) - [CountlyConnectionManager.sharedInstance beginSession]; - - [CountlyViewTrackingInternal.sharedInstance applicationWillEnterForeground]; - - isSuspended = NO; + + CLY_LOG_D(@"%s manualSessions: [%d]", __FUNCTION__, CountlyCommon.sharedInstance.manualSessionHandling); + + if (!CountlyCommon.sharedInstance.manualSessionHandling) + [CountlyConnectionManager.sharedInstance beginSession]; + + [CountlyViewTrackingInternal.sharedInstance applicationWillEnterForeground]; + + isSuspended = NO; } - (void)applicationDidBecomeActive:(NSNotification *)notification { - CLY_LOG_D(@"%s app enters foreground", __FUNCTION__); + CLY_LOG_D(@"%s app enters foreground", __FUNCTION__); [CountlyServerConfig.sharedInstance fetchServerConfigIfTimeIsUp]; - [self resume]; + [self resume]; } - (void)applicationWillResignActive:(NSNotification *)notification { - CLY_LOG_D(@"%s, app enters background", __FUNCTION__); - [CountlyHealthTracker.sharedInstance saveState]; + CLY_LOG_D(@"%s, app enters background", __FUNCTION__); + [CountlyHealthTracker.sharedInstance saveState]; } - (void)applicationDidEnterBackground:(NSNotification *)notification { - CLY_LOG_D(@"%s, app did enter background.", __FUNCTION__); - [CountlyHealthTracker.sharedInstance saveState]; - [self suspend]; + CLY_LOG_D(@"%s, app did enter background.", __FUNCTION__); + [CountlyHealthTracker.sharedInstance saveState]; + [self suspend]; } - (void)applicationWillEnterForeground:(NSNotification *)notification { - CLY_LOG_D(@"%s, app will enter foreground.", __FUNCTION__); + CLY_LOG_D(@"%s, app will enter foreground.", __FUNCTION__); } - (void)applicationWillTerminate:(NSNotification *)notification { - CLY_LOG_D(@"%s, app will terminate.", __FUNCTION__); - - [CountlyHealthTracker.sharedInstance saveState]; - - CountlyConnectionManager.sharedInstance.isTerminating = YES; - - [CountlyViewTrackingInternal.sharedInstance applicationWillTerminate]; - - [CountlyConnectionManager.sharedInstance sendEventsWithSaveIfNeeded]; - - [CountlyPerformanceMonitoring.sharedInstance endBackgroundTrace]; - - [CountlyPersistency.sharedInstance saveToFileSync]; + CLY_LOG_D(@"%s, app will terminate.", __FUNCTION__); + + [CountlyHealthTracker.sharedInstance saveState]; + + CountlyConnectionManager.sharedInstance.isTerminating = YES; + + [CountlyViewTrackingInternal.sharedInstance applicationWillTerminate]; + + [CountlyConnectionManager.sharedInstance sendEventsWithSaveIfNeeded]; + + [CountlyPerformanceMonitoring.sharedInstance endBackgroundTrace]; + + [CountlyPersistency.sharedInstance saveToFileSync]; } + - (void)dealloc { - [NSNotificationCenter.defaultCenter removeObserver:self]; - - if (timer) - { - [timer invalidate]; - timer = nil; - } + [NSNotificationCenter.defaultCenter removeObserver:self]; + + if (timer) + { + [timer invalidate]; + timer = nil; + } } + #pragma mark - Override Configuration - (void)setNewHost:(NSString *)newHost { - CLY_LOG_I(@"%s %@", __FUNCTION__, newHost); - - if (!newHost.length) - { - CLY_LOG_W(@"%s new host is invalid!", __FUNCTION__); - return; - } - - CountlyConnectionManager.sharedInstance.host = newHost; + CLY_LOG_I(@"%s %@", __FUNCTION__, newHost); + + if (!newHost.length) + { + CLY_LOG_W(@"%s new host is invalid!", __FUNCTION__); + return; + } + + CountlyConnectionManager.sharedInstance.host = newHost; } - (void)setNewURLSessionConfiguration:(NSURLSessionConfiguration *)newURLSessionConfiguration { - CLY_LOG_I(@"%s %@", __FUNCTION__, newURLSessionConfiguration); - - CountlyConnectionManager.sharedInstance.URLSessionConfiguration = newURLSessionConfiguration; + CLY_LOG_I(@"%s %@", __FUNCTION__, newURLSessionConfiguration); + + CountlyConnectionManager.sharedInstance.URLSessionConfiguration = newURLSessionConfiguration; } - (void)setNewAppKey:(NSString *)newAppKey { - CLY_LOG_I(@"%s %@", __FUNCTION__, newAppKey); - - if (!newAppKey.length) - { - CLY_LOG_W(@"%s new app key is invalid!", __FUNCTION__); - return; - } - - [self suspend]; - - [CountlyPerformanceMonitoring.sharedInstance clearAllCustomTraces]; + CLY_LOG_I(@"%s %@", __FUNCTION__, newAppKey); + + if (!newAppKey.length) + { + CLY_LOG_W(@"%s new app key is invalid!", __FUNCTION__); + return; + } + + [self suspend]; + + [CountlyPerformanceMonitoring.sharedInstance clearAllCustomTraces]; + + CountlyConnectionManager.sharedInstance.appKey = newAppKey; + + [self resume]; +} - CountlyConnectionManager.sharedInstance.appKey = newAppKey; - [self resume]; -} #pragma mark - Queue Operations -- (void)recordMetrics:(NSDictionary *_Nullable)metricsOverride +- (void)recordMetrics:(NSDictionary * _Nullable)metricsOverride { - CLY_LOG_I(@"%s %@", __FUNCTION__, metricsOverride); - [CountlyConnectionManager.sharedInstance recordMetrics:metricsOverride]; + CLY_LOG_I(@"%s %@", __FUNCTION__, metricsOverride); + [CountlyConnectionManager.sharedInstance recordMetrics:metricsOverride]; } - (void)flushQueues { - CLY_LOG_I(@"%s", __FUNCTION__); - - [CountlyPersistency.sharedInstance flushEvents]; - [CountlyPersistency.sharedInstance flushQueue]; + CLY_LOG_I(@"%s", __FUNCTION__); + + [CountlyPersistency.sharedInstance flushEvents]; + [CountlyPersistency.sharedInstance flushQueue]; } - (void)replaceAllAppKeysInQueueWithCurrentAppKey { - CLY_LOG_I(@"%s", __FUNCTION__); - - [CountlyPersistency.sharedInstance replaceAllAppKeysInQueueWithCurrentAppKey]; + CLY_LOG_I(@"%s", __FUNCTION__); + + [CountlyPersistency.sharedInstance replaceAllAppKeysInQueueWithCurrentAppKey]; } - (void)removeDifferentAppKeysFromQueue { - CLY_LOG_I(@"%s", __FUNCTION__); - - [CountlyPersistency.sharedInstance removeDifferentAppKeysFromQueue]; + CLY_LOG_I(@"%s", __FUNCTION__); + + [CountlyPersistency.sharedInstance removeDifferentAppKeysFromQueue]; } -- (void)addDirectRequest:(NSDictionary *_Nullable)requestParameters +- (void)addDirectRequest:(NSDictionary * _Nullable)requestParameters { - CLY_LOG_I(@"%s %@", __FUNCTION__, requestParameters); - - [CountlyConnectionManager.sharedInstance addDirectRequest:requestParameters]; + CLY_LOG_I(@"%s %@", __FUNCTION__, requestParameters); + + [CountlyConnectionManager.sharedInstance addDirectRequest:requestParameters]; } -- (void)addCustomNetworkRequestHeaders:(NSDictionary *_Nullable)customHeaderValues -{ - CLY_LOG_I(@"%s %@", __FUNCTION__, customHeaderValues); - - // Ignore nil or empty dictionary - if (customHeaderValues == nil || customHeaderValues.count == 0) - { - return; - } - - [CountlyConnectionManager.sharedInstance addCustomNetworkRequestHeaders:customHeaderValues]; +- (void)addCustomNetworkRequestHeaders:(NSDictionary *_Nullable)customHeaderValues { + CLY_LOG_I(@"%s %@", __FUNCTION__, customHeaderValues); + + // Ignore nil or empty dictionary + if (customHeaderValues == nil || customHeaderValues.count == 0) { + return; + } + + [CountlyConnectionManager.sharedInstance addCustomNetworkRequestHeaders:customHeaderValues]; } + #pragma mark - Sessions - (void)beginSession { - CLY_LOG_I(@"%s", __FUNCTION__); - - if (CountlyCommon.sharedInstance.manualSessionHandling) - [CountlyConnectionManager.sharedInstance beginSession]; + CLY_LOG_I(@"%s", __FUNCTION__); + + if (CountlyCommon.sharedInstance.manualSessionHandling) + [CountlyConnectionManager.sharedInstance beginSession]; } - (void)updateSession { - CLY_LOG_I(@"%s", __FUNCTION__); - - if (CountlyCommon.sharedInstance.manualSessionHandling) - [CountlyConnectionManager.sharedInstance updateSession]; + CLY_LOG_I(@"%s", __FUNCTION__); + + if (CountlyCommon.sharedInstance.manualSessionHandling) + [CountlyConnectionManager.sharedInstance updateSession]; } - (void)endSession { - CLY_LOG_I(@"%s", __FUNCTION__); - - if (CountlyCommon.sharedInstance.manualSessionHandling) - { - [CountlyConnectionManager.sharedInstance sendEventsWithSaveIfNeeded]; - [CountlyConnectionManager.sharedInstance endSession]; - } + CLY_LOG_I(@"%s", __FUNCTION__); + + if (CountlyCommon.sharedInstance.manualSessionHandling) + { + [CountlyConnectionManager.sharedInstance sendEventsWithSaveIfNeeded]; + [CountlyConnectionManager.sharedInstance endSession]; + } } + + + #pragma mark - Device ID - (NSString *)deviceID { - CLY_LOG_I(@"%s", __FUNCTION__); - - return CountlyDeviceInfo.sharedInstance.deviceID.cly_URLEscaped; + CLY_LOG_I(@"%s", __FUNCTION__); + + return CountlyDeviceInfo.sharedInstance.deviceID.cly_URLEscaped; } - (CLYDeviceIDType)deviceIDType { - CLY_LOG_I(@"%s", __FUNCTION__); - - if (CountlyDeviceInfo.sharedInstance.isDeviceIDTemporary) - return CLYDeviceIDTypeTemporary; + CLY_LOG_I(@"%s", __FUNCTION__); + + if (CountlyDeviceInfo.sharedInstance.isDeviceIDTemporary) + return CLYDeviceIDTypeTemporary; + + if ([CountlyPersistency.sharedInstance retrieveIsCustomDeviceID]) + return CLYDeviceIDTypeCustom; - if ([CountlyPersistency.sharedInstance retrieveIsCustomDeviceID]) - return CLYDeviceIDTypeCustom; - -#if (TARGET_OS_IOS || TARGET_OS_VISION || TARGET_OS_TV) - return CLYDeviceIDTypeIDFV; +#if (TARGET_OS_IOS || TARGET_OS_VISION || TARGET_OS_TV ) + return CLYDeviceIDTypeIDFV; #else - return CLYDeviceIDTypeNSUUID; + return CLYDeviceIDTypeNSUUID; #endif } - (void)setID:(NSString *)deviceID; { - if (deviceID == nil || !deviceID.length) - { - CLY_LOG_W(@"%s Passing `nil` or empty string as devie ID is not allowed.", __FUNCTION__); - return; - } - - CLYDeviceIDType deviceIDType = [Countly.sharedInstance deviceIDType]; - if ([deviceIDType isEqualToString:CLYDeviceIDTypeCustom]) - { - [Countly.sharedInstance setIDInternal:deviceID onServer:NO]; - } - else - { - [Countly.sharedInstance setIDInternal:deviceID onServer:YES]; - } + if (deviceID == nil || !deviceID.length) + { + CLY_LOG_W(@"%s Passing `nil` or empty string as devie ID is not allowed.", __FUNCTION__); + return; + } + + CLYDeviceIDType deviceIDType = [Countly.sharedInstance deviceIDType]; + if([deviceIDType isEqualToString:CLYDeviceIDTypeCustom]) + { + [Countly.sharedInstance setIDInternal:deviceID onServer: NO]; + } + else + { + [Countly.sharedInstance setIDInternal:deviceID onServer: YES]; + } } -- (void)changeDeviceIDWithMerge:(NSString *_Nullable)deviceID -{ - CLY_LOG_I(@"%s", __FUNCTION__); - [self setIDInternal:deviceID onServer:YES]; +- (void)changeDeviceIDWithMerge:(NSString * _Nullable)deviceID { + CLY_LOG_I(@"%s", __FUNCTION__); + [self setIDInternal:deviceID onServer:YES]; } -- (void)changeDeviceIDWithoutMerge:(NSString *_Nullable)deviceID -{ - CLY_LOG_I(@"%s", __FUNCTION__); - [self setIDInternal:deviceID onServer:NO]; +- (void)changeDeviceIDWithoutMerge:(NSString * _Nullable)deviceID { + CLY_LOG_I(@"%s", __FUNCTION__); + [self setIDInternal:deviceID onServer:NO]; } - (void)enableTemporaryDeviceIDMode { - CLY_LOG_I(@"%s", __FUNCTION__); - [Countly.sharedInstance setIDInternal:CLYTemporaryDeviceID onServer:NO]; + CLY_LOG_I(@"%s", __FUNCTION__); + [Countly.sharedInstance setIDInternal:CLYTemporaryDeviceID onServer:NO]; } - (void)setNewDeviceID:(NSString *)deviceID onServer:(BOOL)onServer { - [Countly.sharedInstance setIDInternal:deviceID onServer:onServer]; + [Countly.sharedInstance setIDInternal:deviceID onServer:onServer]; } - (void)setIDInternal:(NSString *)deviceID onServer:(BOOL)onServer { - CLY_LOG_I(@"%s deviceID: [%@], onServer: [%d]", __FUNCTION__, deviceID, onServer); - if (!CountlyCommon.sharedInstance.hasStarted) - return; - - if (!deviceID.length) - { - CLY_LOG_W(@"%s Passing `CLYDefaultDeviceID` or `nil` or empty string as devie ID is deprecated, and will not be allowed in the future.", __FUNCTION__); - } - - [self storeCustomDeviceIDState:deviceID]; + CLY_LOG_I(@"%s deviceID: [%@], onServer: [%d]", __FUNCTION__, deviceID, onServer); + if (!CountlyCommon.sharedInstance.hasStarted) + return; - deviceID = [CountlyDeviceInfo.sharedInstance ensafeDeviceID:deviceID]; - - if ([deviceID isEqualToString:CountlyDeviceInfo.sharedInstance.deviceID]) - { - CLY_LOG_W(@"%s Attempted to set the same device ID again. So, setting new device ID is aborted.", __FUNCTION__); - return; - } - - if (CountlyDeviceInfo.sharedInstance.isDeviceIDTemporary) - { - CLY_LOG_I(@"%s Going out of CLYTemporaryDeviceID mode and switching back to normal mode.", __FUNCTION__); + if (!deviceID.length) + { + CLY_LOG_W(@"%s Passing `CLYDefaultDeviceID` or `nil` or empty string as devie ID is deprecated, and will not be allowed in the future.", __FUNCTION__); + } + + [self storeCustomDeviceIDState:deviceID]; - [CountlyDeviceInfo.sharedInstance initializeDeviceID:deviceID]; + deviceID = [CountlyDeviceInfo.sharedInstance ensafeDeviceID:deviceID]; - [CountlyPersistency.sharedInstance replaceAllTemporaryDeviceIDsInQueueWithDeviceID:deviceID]; + if ([deviceID isEqualToString:CountlyDeviceInfo.sharedInstance.deviceID]) + { + CLY_LOG_W(@"%s Attempted to set the same device ID again. So, setting new device ID is aborted.", __FUNCTION__); + return; + } - [CountlyConnectionManager.sharedInstance proceedOnQueue]; + if (CountlyDeviceInfo.sharedInstance.isDeviceIDTemporary) + { + CLY_LOG_I(@"%s Going out of CLYTemporaryDeviceID mode and switching back to normal mode.", __FUNCTION__); - [CountlyRemoteConfigInternal.sharedInstance downloadRemoteConfigAutomatically]; + [CountlyDeviceInfo.sharedInstance initializeDeviceID:deviceID]; + + [CountlyPersistency.sharedInstance replaceAllTemporaryDeviceIDsInQueueWithDeviceID:deviceID]; - [CountlyHealthTracker.sharedInstance sendHealthCheck]; + [CountlyConnectionManager.sharedInstance proceedOnQueue]; - return; - } + [CountlyRemoteConfigInternal.sharedInstance downloadRemoteConfigAutomatically]; + + [CountlyHealthTracker.sharedInstance sendHealthCheck]; - if ([deviceID isEqualToString:CLYTemporaryDeviceID] && onServer) - { - CLY_LOG_W(@"%s Attempted to set device ID as CLYTemporaryDeviceID with onServer option. So, onServer value is overridden as NO.", __FUNCTION__); - onServer = NO; - } + return; + } - if (onServer) - { - NSString *oldDeviceID = CountlyDeviceInfo.sharedInstance.deviceID; + if ([deviceID isEqualToString:CLYTemporaryDeviceID] && onServer) + { + CLY_LOG_W(@"%s Attempted to set device ID as CLYTemporaryDeviceID with onServer option. So, onServer value is overridden as NO.", __FUNCTION__); + onServer = NO; + } - [CountlyDeviceInfo.sharedInstance initializeDeviceID:deviceID]; + if (onServer) + { + NSString* oldDeviceID = CountlyDeviceInfo.sharedInstance.deviceID; - [CountlyConnectionManager.sharedInstance sendOldDeviceID:oldDeviceID]; - } - else - { - [self suspend]; + [CountlyDeviceInfo.sharedInstance initializeDeviceID:deviceID]; - [CountlyDeviceInfo.sharedInstance initializeDeviceID:deviceID]; + [CountlyConnectionManager.sharedInstance sendOldDeviceID:oldDeviceID]; + } + else + { + [self suspend]; - [CountlyConsentManager.sharedInstance cancelConsentForAllFeaturesWithoutSendingConsentsRequest]; + [CountlyDeviceInfo.sharedInstance initializeDeviceID:deviceID]; - [self resume]; + [CountlyConsentManager.sharedInstance cancelConsentForAllFeaturesWithoutSendingConsentsRequest]; - [CountlyPersistency.sharedInstance clearAllTimedEvents]; - } + [self resume]; - [CountlyRemoteConfigInternal.sharedInstance clearCachedRemoteConfig]; + [CountlyPersistency.sharedInstance clearAllTimedEvents]; + } - if (![deviceID isEqualToString:CLYTemporaryDeviceID]) - { - [CountlyRemoteConfigInternal.sharedInstance downloadRemoteConfigAutomatically]; - } + + [CountlyRemoteConfigInternal.sharedInstance clearCachedRemoteConfig]; + + if (![deviceID isEqualToString:CLYTemporaryDeviceID] ) + { + [CountlyRemoteConfigInternal.sharedInstance downloadRemoteConfigAutomatically]; + } } - (void)storeCustomDeviceIDState:(NSString *)deviceID { - BOOL isCustomDeviceID = deviceID.length && ![deviceID isEqualToString:CLYTemporaryDeviceID]; - [CountlyPersistency.sharedInstance storeIsCustomDeviceID:isCustomDeviceID]; + BOOL isCustomDeviceID = deviceID.length && ![deviceID isEqualToString:CLYTemporaryDeviceID]; + [CountlyPersistency.sharedInstance storeIsCustomDeviceID:isCustomDeviceID]; } #pragma mark - Consents - (void)giveConsentForFeature:(NSString *)featureName { - CLY_LOG_I(@"%s featureName: [%@]", __FUNCTION__, featureName); + CLY_LOG_I(@"%s featureName: [%@]", __FUNCTION__, featureName); - if (!featureName.length) - return; + if (!featureName.length) + return; - [CountlyConsentManager.sharedInstance giveConsentForFeatures:@[ featureName ]]; + [CountlyConsentManager.sharedInstance giveConsentForFeatures:@[featureName]]; } - (void)giveConsentForFeatures:(NSArray *)features { - CLY_LOG_I(@"%s features: [%@]", __FUNCTION__, features); - [CountlyConsentManager.sharedInstance giveConsentForFeatures:features]; + CLY_LOG_I(@"%s features: [%@]", __FUNCTION__, features); + [CountlyConsentManager.sharedInstance giveConsentForFeatures:features]; } - (void)giveConsentForAllFeatures { - CLY_LOG_I(@"%s", __FUNCTION__); - [CountlyConsentManager.sharedInstance giveAllConsents]; + CLY_LOG_I(@"%s", __FUNCTION__); + [CountlyConsentManager.sharedInstance giveAllConsents]; } - (void)giveAllConsents { - CLY_LOG_I(@"%s", __FUNCTION__); - [CountlyConsentManager.sharedInstance giveAllConsents]; + CLY_LOG_I(@"%s", __FUNCTION__); + [CountlyConsentManager.sharedInstance giveAllConsents]; } - (void)cancelConsentForFeature:(NSString *)featureName { - CLY_LOG_I(@"%s featureName: [%@]", __FUNCTION__, featureName); + CLY_LOG_I(@"%s featureName: [%@]", __FUNCTION__, featureName); - if (!featureName.length) - return; + if (!featureName.length) + return; - [CountlyConsentManager.sharedInstance cancelConsentForFeatures:@[ featureName ]]; + [CountlyConsentManager.sharedInstance cancelConsentForFeatures:@[featureName]]; } - (void)cancelConsentForFeatures:(NSArray *)features { - CLY_LOG_I(@"%s features: [%@]", __FUNCTION__, features); - [CountlyConsentManager.sharedInstance cancelConsentForFeatures:features]; + CLY_LOG_I(@"%s features: [%@]", __FUNCTION__, features); + [CountlyConsentManager.sharedInstance cancelConsentForFeatures:features]; } - (void)cancelConsentForAllFeatures { - CLY_LOG_I(@"%s", __FUNCTION__); - [CountlyConsentManager.sharedInstance cancelConsentForAllFeatures]; + CLY_LOG_I(@"%s", __FUNCTION__); + [CountlyConsentManager.sharedInstance cancelConsentForAllFeatures]; } + + #pragma mark - Events - (void)recordEvent:(NSString *)key { - [self recordEvent:key segmentation:nil count:1 sum:0 duration:0]; + [self recordEvent:key segmentation:nil count:1 sum:0 duration:0]; } - (void)recordEvent:(NSString *)key count:(NSUInteger)count { - [self recordEvent:key segmentation:nil count:count sum:0 duration:0]; + [self recordEvent:key segmentation:nil count:count sum:0 duration:0]; } - (void)recordEvent:(NSString *)key sum:(double)sum { - [self recordEvent:key segmentation:nil count:1 sum:sum duration:0]; + [self recordEvent:key segmentation:nil count:1 sum:sum duration:0]; } - (void)recordEvent:(NSString *)key duration:(NSTimeInterval)duration { - [self recordEvent:key segmentation:nil count:1 sum:0 duration:duration]; + [self recordEvent:key segmentation:nil count:1 sum:0 duration:duration]; } - (void)recordEvent:(NSString *)key count:(NSUInteger)count sum:(double)sum { - [self recordEvent:key segmentation:nil count:count sum:sum duration:0]; + [self recordEvent:key segmentation:nil count:count sum:sum duration:0]; } - (void)recordEvent:(NSString *)key segmentation:(NSDictionary *)segmentation { - [self recordEvent:key segmentation:segmentation count:1 sum:0 duration:0]; + [self recordEvent:key segmentation:segmentation count:1 sum:0 duration:0]; } - (void)recordEvent:(NSString *)key segmentation:(NSDictionary *)segmentation count:(NSUInteger)count { - [self recordEvent:key segmentation:segmentation count:count sum:0 duration:0]; + [self recordEvent:key segmentation:segmentation count:count sum:0 duration:0]; } - (void)recordEvent:(NSString *)key segmentation:(NSDictionary *)segmentation count:(NSUInteger)count sum:(double)sum { - [self recordEvent:key segmentation:segmentation count:count sum:sum duration:0]; + [self recordEvent:key segmentation:segmentation count:count sum:sum duration:0]; } - (void)recordEvent:(NSString *)key segmentation:(NSDictionary *)segmentation count:(NSUInteger)count sum:(double)sum duration:(NSTimeInterval)duration { - CLY_LOG_I(@"%s key: [%@], segmentation: [%@], count: [%lu], sum: [%f], duration: [%f]", __FUNCTION__, key, segmentation, (unsigned long)count, sum, duration); + CLY_LOG_I(@"%s key: [%@], segmentation: [%@], count: [%lu], sum: [%f], duration: [%f]", __FUNCTION__, key, segmentation, (unsigned long)count, sum, duration); - NSNumber *isReservedEvent = [self isReservedEvent:key]; + NSNumber* isReservedEvent = [self isReservedEvent:key]; - if (isReservedEvent) - { - CLY_LOG_V(@"%s, A reserved event detected: %@", __FUNCTION__, key); - if (!isReservedEvent.boolValue) + if (isReservedEvent) { - CLY_LOG_W(@"%s, No consent given for the reserved event! Event will not be recorded.", __FUNCTION__); - return; + CLY_LOG_V(@"%s, A reserved event detected: %@", __FUNCTION__, key); + if (!isReservedEvent.boolValue) + { + CLY_LOG_W(@"%s, No consent given for the reserved event! Event will not be recorded.", __FUNCTION__); + return; + } + CLY_LOG_V(@"%s, Specific consent given for the reserved event! So, it will be recorded.", __FUNCTION__); + } else if (!CountlyConsentManager.sharedInstance.consentForEvents) { + CLY_LOG_W(@"%s, Consent for events not given! Event will not be recorded.", __FUNCTION__); + return; + } + + if (!CountlyServerConfig.sharedInstance.customEventTrackingEnabled) + { + CLY_LOG_D(@"%s, aborted: Custom Event Tracking is disabled from server config!", __FUNCTION__); + return; } - CLY_LOG_V(@"%s, Specific consent given for the reserved event! So, it will be recorded.", __FUNCTION__); - } - else if (!CountlyConsentManager.sharedInstance.consentForEvents) - { - CLY_LOG_W(@"%s, Consent for events not given! Event will not be recorded.", __FUNCTION__); - return; - } - - if (!CountlyServerConfig.sharedInstance.customEventTrackingEnabled) - { - CLY_LOG_D(@"%s, aborted: Custom Event Tracking is disabled from server config!", __FUNCTION__); - return; - } - - if (![CountlyServerConfig.sharedInstance shouldRecordEvent:key]) - { - CLY_LOG_D(@"%s, aborted: Event '%@' is filtered by server config event filter!", __FUNCTION__, key); - return; - } - // Apply global segmentation filter (sb/sw) and event-specific segmentation filter (esb/esw) - NSDictionary *filtered = [CountlyServerConfig.sharedInstance filterSegmentation:segmentation eventKey:key]; - filtered = [filtered cly_truncated:@"Event segmentation"]; - segmentation = [filtered cly_limited:@"Event segmentation"]; + if (![CountlyServerConfig.sharedInstance shouldRecordEvent:key]) + { + CLY_LOG_D(@"%s, aborted: Event '%@' is filtered by server config event filter!", __FUNCTION__, key); + return; + } + + // Apply global segmentation filter (sb/sw) and event-specific segmentation filter (esb/esw) + NSDictionary* filtered = [CountlyServerConfig.sharedInstance filterSegmentation:segmentation eventKey:key]; + filtered = [filtered cly_truncated:@"Event segmentation"]; + segmentation = [filtered cly_limited:@"Event segmentation"]; - [self recordEvent:key segmentation:segmentation count:count sum:sum duration:duration ID:nil timestamp:CountlyCommon.sharedInstance.uniqueTimestamp]; + [self recordEvent:key segmentation:segmentation count:count sum:sum duration:duration ID:nil timestamp:CountlyCommon.sharedInstance.uniqueTimestamp]; } #pragma mark - - (void)recordReservedEvent:(NSString *)key segmentation:(NSDictionary *)segmentation { - [self recordEvent:key segmentation:segmentation count:1 sum:0 duration:0 ID:nil timestamp:CountlyCommon.sharedInstance.uniqueTimestamp]; + [self recordEvent:key segmentation:segmentation count:1 sum:0 duration:0 ID:nil timestamp:CountlyCommon.sharedInstance.uniqueTimestamp]; } - (void)recordReservedEvent:(NSString *)key segmentation:(NSDictionary *)segmentation ID:(NSString *)ID { - [self recordEvent:key segmentation:segmentation count:1 sum:0 duration:0 ID:ID timestamp:CountlyCommon.sharedInstance.uniqueTimestamp]; + [self recordEvent:key segmentation:segmentation count:1 sum:0 duration:0 ID:ID timestamp:CountlyCommon.sharedInstance.uniqueTimestamp]; } - (void)recordReservedEvent:(NSString *)key segmentation:(NSDictionary *)segmentation count:(NSUInteger)count sum:(double)sum duration:(NSTimeInterval)duration ID:(NSString *)ID timestamp:(NSTimeInterval)timestamp { - [self recordEvent:key segmentation:segmentation count:count sum:sum duration:duration ID:ID timestamp:timestamp]; + [self recordEvent:key segmentation:segmentation count:count sum:sum duration:duration ID:ID timestamp:timestamp]; } #pragma mark - - (void)recordEvent:(NSString *)key segmentation:(NSDictionary *)segmentation count:(NSUInteger)count sum:(double)sum duration:(NSTimeInterval)duration ID:(NSString *)ID timestamp:(NSTimeInterval)timestamp { - if (key.length == 0) - { - CLY_LOG_D(@"%s omitting the call, key is empty", __FUNCTION__); - return; - } - - CountlyEvent *event = CountlyEvent.new; - event.ID = ID; - if (!event.ID.length) - { - event.ID = CountlyCommon.sharedInstance.randomEventID; - } - - if ([key isEqualToString:kCountlyReservedEventView]) - { - event.PVID = CountlyViewTrackingInternal.sharedInstance.previousViewID ?: @""; - } - else - { - event.CVID = CountlyViewTrackingInternal.sharedInstance.currentViewID ?: @""; - } - - // Check if the event is a reserved event - BOOL isReservedEvent = [self isReservedEvent:key]; - - NSMutableDictionary *filteredSegmentations = segmentation.cly_filterSupportedDataTypes; - if (filteredSegmentations == nil) - filteredSegmentations = NSMutableDictionary.new; - - event.count = MAX(count, 1); - event.sum = sum; - event.timestamp = timestamp; - event.hourOfDay = CountlyCommon.sharedInstance.hourOfDay; - event.dayOfWeek = CountlyCommon.sharedInstance.dayOfWeek; - event.duration = duration; - - if (!isReservedEvent) - { - CLY_LOG_V(@"%s will add event id and name properties because it is not a reserved event ", __FUNCTION__); - key = [key cly_truncatedKey:@"Event key"]; - NSString *capturedPreviousID = nil; - NSString *capturedPreviousName = nil; -#if __has_include() - os_unfair_lock_lock(&previousEventLock); -#endif - capturedPreviousID = previousEventID; - previousEventID = event.ID; // update chain - if (CountlyViewTrackingInternal.sharedInstance.enablePreviousNameRecording) + if (key.length == 0) { + CLY_LOG_D(@"%s omitting the call, key is empty", __FUNCTION__); + return; + } + + CountlyEvent *event = CountlyEvent.new; + event.ID = ID; + if (!event.ID.length) { - capturedPreviousName = previousEventName; - previousEventName = key; + event.ID = CountlyCommon.sharedInstance.randomEventID; } - event.PEID = capturedPreviousID ?: @""; - if (CountlyViewTrackingInternal.sharedInstance.enablePreviousNameRecording) + + if ([key isEqualToString:kCountlyReservedEventView]) { - filteredSegmentations[kCountlyPreviousEventName] = capturedPreviousName ?: @""; - filteredSegmentations[kCountlyCurrentView] = CountlyViewTrackingInternal.sharedInstance.currentViewName ?: @""; + event.PVID = CountlyViewTrackingInternal.sharedInstance.previousViewID ?: @""; } - event.key = key; - event.segmentation = [self processSegmentation:filteredSegmentations eventKey:key]; - id callback = nil; - if ([CountlyServerConfig.sharedInstance isJourneyTriggerEvent:key]) + else { - callback = ^(NSString *response, BOOL success) { - if (success) - { -#if (TARGET_OS_IOS) - dispatch_async(dispatch_get_main_queue(), ^{ - [CountlyContentBuilderInternal.sharedInstance refreshContentZoneJTE]; - }); + event.CVID = CountlyViewTrackingInternal.sharedInstance.currentViewID ?: @""; + } + + // Check if the event is a reserved event + BOOL isReservedEvent = [self isReservedEvent:key]; + + NSMutableDictionary *filteredSegmentations = segmentation.cly_filterSupportedDataTypes; + if(filteredSegmentations == nil) + filteredSegmentations = NSMutableDictionary.new; + + event.count = MAX(count, 1); + event.sum = sum; + event.timestamp = timestamp; + event.hourOfDay = CountlyCommon.sharedInstance.hourOfDay; + event.dayOfWeek = CountlyCommon.sharedInstance.dayOfWeek; + event.duration = duration; + + if (!isReservedEvent) + { + CLY_LOG_V(@"%s will add event id and name properties because it is not a reserved event ", __FUNCTION__); + key = [key cly_truncatedKey:@"Event key"]; + NSString* capturedPreviousID = nil; + NSString* capturedPreviousName = nil; +#if __has_include() + os_unfair_lock_lock(&previousEventLock); #endif + capturedPreviousID = previousEventID; + previousEventID = event.ID; // update chain + if(CountlyViewTrackingInternal.sharedInstance.enablePreviousNameRecording) { + capturedPreviousName = previousEventName; + previousEventName = key; } - }; - } - [CountlyPersistency.sharedInstance recordEvent:event callback:callback]; + event.PEID = capturedPreviousID ?: @""; + if(CountlyViewTrackingInternal.sharedInstance.enablePreviousNameRecording) { + filteredSegmentations[kCountlyPreviousEventName] = capturedPreviousName ?: @""; + filteredSegmentations[kCountlyCurrentView] = CountlyViewTrackingInternal.sharedInstance.currentViewName ?: @""; + } + event.key = key; + event.segmentation = [self processSegmentation:filteredSegmentations eventKey:key]; + id callback = nil; + if ([CountlyServerConfig.sharedInstance isJourneyTriggerEvent:key]){ + callback = ^(NSString *response, BOOL success) { + if (success) + { + #if (TARGET_OS_IOS) + dispatch_async(dispatch_get_main_queue(), ^{ + [CountlyContentBuilderInternal.sharedInstance refreshContentZoneJTE]; + }); + #endif + } + }; + } + [CountlyPersistency.sharedInstance recordEvent:event callback:callback]; #if __has_include() - os_unfair_lock_unlock(&previousEventLock); + os_unfair_lock_unlock(&previousEventLock); #endif - } - else - { - event.key = key; - event.segmentation = [self processSegmentation:filteredSegmentations eventKey:key]; - [CountlyPersistency.sharedInstance recordEvent:event]; - } -} - -- (NSDictionary *)processSegmentation:(NSMutableDictionary *)segmentation eventKey:(NSString *)eventKey -{ - BOOL isViewEvent = [eventKey isEqualToString:kCountlyReservedEventView]; - - // Add previous view name if enabled and the event is a view event - if (isViewEvent && CountlyViewTrackingInternal.sharedInstance.enablePreviousNameRecording) - { - segmentation[kCountlyPreviousView] = CountlyViewTrackingInternal.sharedInstance.previousViewName ?: @""; - } - - // Add visibility tracking information if enabled - if (CountlyCommon.sharedInstance.enableVisibiltyTracking) - { - BOOL isViewStart = [segmentation[kCountlyVTKeyVisit] isEqual:@1]; - - // Add visibility if it's not a view event or it's a view start event - if (!isViewEvent || isViewStart) + } + else { - segmentation[kCountlyVisibility] = @([self isAppInForeground] ? 1 : 0); + event.key = key; + event.segmentation = [self processSegmentation:filteredSegmentations eventKey:key]; + [CountlyPersistency.sharedInstance recordEvent:event]; } - } +} - // Return segmentation dictionary if not empty, otherwise return nil - return segmentation.count > 0 ? segmentation : nil; +- (NSDictionary *)processSegmentation:(NSMutableDictionary *)segmentation eventKey:(NSString *)eventKey { + BOOL isViewEvent = [eventKey isEqualToString:kCountlyReservedEventView]; + + // Add previous view name if enabled and the event is a view event + if (isViewEvent && CountlyViewTrackingInternal.sharedInstance.enablePreviousNameRecording) { + segmentation[kCountlyPreviousView] = CountlyViewTrackingInternal.sharedInstance.previousViewName ?: @""; + } + + // Add visibility tracking information if enabled + if (CountlyCommon.sharedInstance.enableVisibiltyTracking) { + BOOL isViewStart = [segmentation[kCountlyVTKeyVisit] isEqual:@1]; + + // Add visibility if it's not a view event or it's a view start event + if (!isViewEvent || isViewStart) { + segmentation[kCountlyVisibility] = @([self isAppInForeground] ? 1 : 0); + } + } + + // Return segmentation dictionary if not empty, otherwise return nil + return segmentation.count > 0 ? segmentation : nil; } -- (BOOL)isAppInForeground -{ +- (BOOL)isAppInForeground { #if TARGET_OS_IOS || TARGET_OS_TV - UIApplicationState state = [UIApplication sharedApplication].applicationState; - return state == UIApplicationStateActive; + UIApplicationState state = [UIApplication sharedApplication].applicationState; + return state == UIApplicationStateActive; #elif TARGET_OS_OSX - NSApplication *app = [NSApplication sharedApplication]; - return app.isActive; + NSApplication *app = [NSApplication sharedApplication]; + return app.isActive; #elif TARGET_OS_WATCH - WKExtension *extension = [WKExtension sharedExtension]; - return extension.applicationState == WKApplicationStateActive; + WKExtension *extension = [WKExtension sharedExtension]; + return extension.applicationState == WKApplicationStateActive; #else - return NO; + return NO; #endif } /// This checks that given key requires any extra consent - (NSNumber *)isReservedEvent:(NSString *)key { - NSDictionary *reservedEvents = @{ - kCountlyReservedEventOrientation : @(CountlyConsentManager.sharedInstance.consentForUserDetails), - kCountlyReservedEventStarRating : @(CountlyConsentManager.sharedInstance.consentForFeedback), - kCountlyReservedEventSurvey : @(CountlyConsentManager.sharedInstance.consentForFeedback), - kCountlyReservedEventNPS : @(CountlyConsentManager.sharedInstance.consentForFeedback), - kCountlyReservedEventPushAction : @(CountlyConsentManager.sharedInstance.consentForPushNotifications), - kCountlyReservedEventView : @(CountlyConsentManager.sharedInstance.consentForViewTracking), - }; + NSDictionary * reservedEvents = + @{ + kCountlyReservedEventOrientation: @(CountlyConsentManager.sharedInstance.consentForUserDetails), + kCountlyReservedEventStarRating: @(CountlyConsentManager.sharedInstance.consentForFeedback), + kCountlyReservedEventSurvey: @(CountlyConsentManager.sharedInstance.consentForFeedback), + kCountlyReservedEventNPS: @(CountlyConsentManager.sharedInstance.consentForFeedback), + kCountlyReservedEventPushAction: @(CountlyConsentManager.sharedInstance.consentForPushNotifications), + kCountlyReservedEventView: @(CountlyConsentManager.sharedInstance.consentForViewTracking), + }; - NSNumber *aReservedEvent = reservedEvents[key]; - return aReservedEvent; + NSNumber* aReservedEvent = reservedEvents[key]; + return aReservedEvent; } #pragma mark - - (void)startEvent:(NSString *)key { - CLY_LOG_I(@"%s key: [%@]", __FUNCTION__, key); + CLY_LOG_I(@"%s key: [%@]", __FUNCTION__, key); - if (!CountlyConsentManager.sharedInstance.consentForEvents) - return; + if (!CountlyConsentManager.sharedInstance.consentForEvents) + return; - CountlyEvent *event = CountlyEvent.new; - event.key = key; - event.timestamp = CountlyCommon.sharedInstance.uniqueTimestamp; + CountlyEvent *event = CountlyEvent.new; + event.key = key; + event.timestamp = CountlyCommon.sharedInstance.uniqueTimestamp; - [CountlyPersistency.sharedInstance recordTimedEvent:event]; + [CountlyPersistency.sharedInstance recordTimedEvent:event]; } - (void)endEvent:(NSString *)key { - [self endEvent:key segmentation:nil count:1 sum:0]; + [self endEvent:key segmentation:nil count:1 sum:0]; } - (void)endEvent:(NSString *)key segmentation:(NSDictionary *)segmentation count:(NSUInteger)count sum:(double)sum { - CLY_LOG_I(@"%s key: [%@], segmentation: [%@], count: [%lu], sum: [%f]", __FUNCTION__, key, segmentation, (unsigned long)count, sum); + CLY_LOG_I(@"%s key: [%@], segmentation: [%@], count: [%lu], sum: [%f]", __FUNCTION__, key, segmentation, (unsigned long)count, sum); - if (!CountlyConsentManager.sharedInstance.consentForEvents) - return; + if (!CountlyConsentManager.sharedInstance.consentForEvents) + return; - CountlyEvent *event = [CountlyPersistency.sharedInstance timedEventForKey:key]; + CountlyEvent *event = [CountlyPersistency.sharedInstance timedEventForKey:key]; - if (!event) - { - CLY_LOG_W(@"%s Event with key '%@' not started yet or cancelled/ended before!", __FUNCTION__, key); - return; - } + if (!event) + { + CLY_LOG_W(@"%s Event with key '%@' not started yet or cancelled/ended before!", __FUNCTION__, key); + return; + } - NSTimeInterval duration = NSDate.date.timeIntervalSince1970 - event.timestamp; - [self recordEvent:key segmentation:segmentation count:count sum:sum duration:duration]; + NSTimeInterval duration = NSDate.date.timeIntervalSince1970 - event.timestamp; + [self recordEvent:key segmentation:segmentation count:count sum:sum duration:duration]; } - (void)cancelEvent:(NSString *)key { - CLY_LOG_I(@"%s key: [%@]", __FUNCTION__, key); + CLY_LOG_I(@"%s key: [%@]", __FUNCTION__, key); - if (!CountlyConsentManager.sharedInstance.consentForEvents) - return; + if (!CountlyConsentManager.sharedInstance.consentForEvents) + return; - CountlyEvent *event = [CountlyPersistency.sharedInstance timedEventForKey:key]; + CountlyEvent *event = [CountlyPersistency.sharedInstance timedEventForKey:key]; - if (!event) - { - CLY_LOG_W(@"%s Event with key '%@' not started yet or cancelled/ended before!", __FUNCTION__, key); - return; - } + if (!event) + { + CLY_LOG_W(@"%s Event with key '%@' not started yet or cancelled/ended before!", __FUNCTION__, key); + return; + } - CLY_LOG_D(@"%s Event with key '%@' cancelled!", __FUNCTION__, key); + CLY_LOG_D(@"%s Event with key '%@' cancelled!", __FUNCTION__, key); } + #pragma mark - Push Notifications -#if (TARGET_OS_IOS || TARGET_OS_VISION || TARGET_OS_OSX) - #ifndef COUNTLY_EXCLUDE_PUSHNOTIFICATIONS +#if (TARGET_OS_IOS || TARGET_OS_VISION || TARGET_OS_OSX ) +#ifndef COUNTLY_EXCLUDE_PUSHNOTIFICATIONS - (void)askForNotificationPermission { - CLY_LOG_I(@"%s", __FUNCTION__); - [CountlyPushNotifications.sharedInstance askForNotificationPermissionWithOptions:0 completionHandler:nil]; + CLY_LOG_I(@"%s", __FUNCTION__); + [CountlyPushNotifications.sharedInstance askForNotificationPermissionWithOptions:0 completionHandler:nil]; } -- (void)askForNotificationPermissionWithOptions:(UNAuthorizationOptions)options completionHandler:(void (^)(BOOL granted, NSError *error))completionHandler; +- (void)askForNotificationPermissionWithOptions:(UNAuthorizationOptions)options completionHandler:(void (^)(BOOL granted, NSError * error))completionHandler; { - CLY_LOG_I(@"%s options: [%lu], completionHandler: [%@]", __FUNCTION__, (unsigned long)options, completionHandler); - [CountlyPushNotifications.sharedInstance askForNotificationPermissionWithOptions:options completionHandler:completionHandler]; + CLY_LOG_I(@"%s options: [%lu], completionHandler: [%@]", __FUNCTION__, (unsigned long)options, completionHandler); + [CountlyPushNotifications.sharedInstance askForNotificationPermissionWithOptions:options completionHandler:completionHandler]; } - (void)recordActionForNotification:(NSDictionary *)userInfo clickedButtonIndex:(NSInteger)buttonIndex; { - CLY_LOG_I(@"%s userInfo: [%@], buttonIndex: [%ld]", __FUNCTION__, userInfo, (long)buttonIndex); - [CountlyPushNotifications.sharedInstance recordActionForNotification:userInfo clickedButtonIndex:buttonIndex]; + CLY_LOG_I(@"%s userInfo: [%@], buttonIndex: [%ld]", __FUNCTION__, userInfo, (long)buttonIndex); + [CountlyPushNotifications.sharedInstance recordActionForNotification:userInfo clickedButtonIndex:buttonIndex]; } - (void)recordPushNotificationToken { - CLY_LOG_I(@"%s", __FUNCTION__); - [CountlyPushNotifications.sharedInstance sendToken]; + CLY_LOG_I(@"%s", __FUNCTION__); + [CountlyPushNotifications.sharedInstance sendToken]; } - (void)clearPushNotificationToken { - CLY_LOG_I(@"%s", __FUNCTION__); - [CountlyPushNotifications.sharedInstance clearToken]; + CLY_LOG_I(@"%s", __FUNCTION__); + [CountlyPushNotifications.sharedInstance clearToken]; } - #endif #endif +#endif + + #pragma mark - Location -- (void)recordLocation:(CLLocationCoordinate2D)location city:(NSString *_Nullable)city ISOCountryCode:(NSString *_Nullable)ISOCountryCode IP:(NSString *_Nullable)IP +- (void)recordLocation:(CLLocationCoordinate2D)location city:(NSString * _Nullable)city ISOCountryCode:(NSString * _Nullable)ISOCountryCode IP:(NSString * _Nullable)IP { - CLY_LOG_I(@"%s lat: [%f], long: [%f], city: [%@], country: [%@], ip: [%@]", __FUNCTION__, location.latitude, location.longitude, city, ISOCountryCode, IP); - [CountlyLocationManager.sharedInstance recordLocation:location city:city ISOCountryCode:ISOCountryCode IP:IP]; + CLY_LOG_I(@"%s lat: [%f], long: [%f], city: [%@], country: [%@], ip: [%@]", __FUNCTION__, location.latitude, location.longitude, city, ISOCountryCode, IP); + [CountlyLocationManager.sharedInstance recordLocation:location city:city ISOCountryCode:ISOCountryCode IP:IP]; } - (void)disableLocationInfo { - CLY_LOG_I(@"%s", __FUNCTION__); - [CountlyLocationManager.sharedInstance disableLocationInfo]; + CLY_LOG_I(@"%s", __FUNCTION__); + [CountlyLocationManager.sharedInstance disableLocationInfo]; } + + #pragma mark - Crash Reporting - (void)recordException:(NSException *)exception { - CLY_LOG_I(@"%s exception: [%@]", __FUNCTION__, exception); - [CountlyCrashReporter.sharedInstance recordException:exception isFatal:NO stackTrace:nil segmentation:nil]; + CLY_LOG_I(@"%s exception: [%@]", __FUNCTION__, exception); + [CountlyCrashReporter.sharedInstance recordException:exception isFatal:NO stackTrace:nil segmentation:nil]; } - (void)recordException:(NSException *)exception isFatal:(BOOL)isFatal { - CLY_LOG_I(@"%s exception: [%@], isFatal: [%d]", __FUNCTION__, exception, isFatal); - [CountlyCrashReporter.sharedInstance recordException:exception isFatal:isFatal stackTrace:nil segmentation:nil]; + CLY_LOG_I(@"%s exception: [%@], isFatal: [%d]", __FUNCTION__, exception, isFatal); + [CountlyCrashReporter.sharedInstance recordException:exception isFatal:isFatal stackTrace:nil segmentation:nil]; } - (void)recordException:(NSException *)exception isFatal:(BOOL)isFatal stackTrace:(NSArray *)stackTrace segmentation:(NSDictionary *)segmentation { - CLY_LOG_I(@"%s exception: [%@], isFatal: [%d], stackTrace: [%@], segmentation: [%@]", __FUNCTION__, exception, isFatal, stackTrace, segmentation); - [CountlyCrashReporter.sharedInstance recordException:exception isFatal:isFatal stackTrace:stackTrace segmentation:segmentation]; + CLY_LOG_I(@"%s exception: [%@], isFatal: [%d], stackTrace: [%@], segmentation: [%@]", __FUNCTION__, exception, isFatal, stackTrace, segmentation); + [CountlyCrashReporter.sharedInstance recordException:exception isFatal:isFatal stackTrace:stackTrace segmentation:segmentation]; } -- (void)recordError:(NSString *)errorName stackTrace:(NSArray *_Nullable)stackTrace +- (void)recordError:(NSString *)errorName stackTrace:(NSArray * _Nullable)stackTrace { - CLY_LOG_I(@"%s errorName: [%@], stackTrace: [%@]", __FUNCTION__, errorName, stackTrace); - [CountlyCrashReporter.sharedInstance recordError:errorName isFatal:NO stackTrace:stackTrace segmentation:nil]; + CLY_LOG_I(@"%s errorName: [%@], stackTrace: [%@]", __FUNCTION__, errorName, stackTrace); + [CountlyCrashReporter.sharedInstance recordError:errorName isFatal:NO stackTrace:stackTrace segmentation:nil]; } -- (void)recordError:(NSString *)errorName isFatal:(BOOL)isFatal stackTrace:(NSArray *_Nullable)stackTrace segmentation:(NSDictionary *)segmentation +- (void)recordError:(NSString *)errorName isFatal:(BOOL)isFatal stackTrace:(NSArray * _Nullable)stackTrace segmentation:(NSDictionary *)segmentation { - CLY_LOG_I(@"%s errorName: [%@], isFatal: [%d], stackTrace: [%@], segmentation: [%@]", __FUNCTION__, errorName, isFatal, stackTrace, segmentation); - [CountlyCrashReporter.sharedInstance recordError:errorName isFatal:isFatal stackTrace:stackTrace segmentation:segmentation]; + CLY_LOG_I(@"%s errorName: [%@], isFatal: [%d], stackTrace: [%@], segmentation: [%@]", __FUNCTION__, errorName, isFatal, stackTrace, segmentation); + [CountlyCrashReporter.sharedInstance recordError:errorName isFatal:isFatal stackTrace:stackTrace segmentation:segmentation]; } - (void)recordHandledException:(NSException *)exception { - CLY_LOG_I(@"%s exception: [%@]", __FUNCTION__, exception); - [CountlyCrashReporter.sharedInstance recordException:exception isFatal:NO stackTrace:nil segmentation:nil]; + CLY_LOG_I(@"%s exception: [%@]", __FUNCTION__, exception); + [CountlyCrashReporter.sharedInstance recordException:exception isFatal:NO stackTrace:nil segmentation:nil]; } - (void)recordHandledException:(NSException *)exception withStackTrace:(NSArray *)stackTrace { - CLY_LOG_I(@"%s exception: [%@], stackTrace: [%@]", __FUNCTION__, exception, stackTrace); - [CountlyCrashReporter.sharedInstance recordException:exception isFatal:NO stackTrace:stackTrace segmentation:nil]; + CLY_LOG_I(@"%s exception: [%@], stackTrace: [%@]", __FUNCTION__, exception, stackTrace); + [CountlyCrashReporter.sharedInstance recordException:exception isFatal:NO stackTrace:stackTrace segmentation:nil]; } -- (void)recordUnhandledException:(NSException *)exception withStackTrace:(NSArray *_Nullable)stackTrace +- (void)recordUnhandledException:(NSException *)exception withStackTrace:(NSArray * _Nullable)stackTrace { - CLY_LOG_I(@"%s exception: [%@], stackTrace: [%@]", __FUNCTION__, exception, stackTrace); - [CountlyCrashReporter.sharedInstance recordException:exception isFatal:YES stackTrace:stackTrace segmentation:nil]; + CLY_LOG_I(@"%s exception: [%@], stackTrace: [%@]", __FUNCTION__, exception, stackTrace); + [CountlyCrashReporter.sharedInstance recordException:exception isFatal:YES stackTrace:stackTrace segmentation:nil]; } - (void)recordCrashLog:(NSString *)log { - CLY_LOG_I(@"%s log: [%@]", __FUNCTION__, log); - [CountlyCrashReporter.sharedInstance log:log]; + CLY_LOG_I(@"%s log: [%@]", __FUNCTION__, log); + [CountlyCrashReporter.sharedInstance log:log]; } - (void)clearCrashLogs { - CLY_LOG_I(@"%s", __FUNCTION__); - [CountlyCrashReporter.sharedInstance clearCrashLogs]; + CLY_LOG_I(@"%s", __FUNCTION__); + [CountlyCrashReporter.sharedInstance clearCrashLogs]; } - (void)crashLog:(NSString *)format, ... { + } #pragma mark - View Tracking - (void)recordView:(NSString *)viewName; { - CLY_LOG_I(@"%s viewName: [%@]", __FUNCTION__, viewName); - [CountlyViewTrackingInternal.sharedInstance startAutoStoppedView:viewName segmentation:nil]; + CLY_LOG_I(@"%s viewName: [%@]", __FUNCTION__, viewName); + [CountlyViewTrackingInternal.sharedInstance startAutoStoppedView:viewName segmentation:nil]; } - (void)recordView:(NSString *)viewName segmentation:(NSDictionary *)segmentation { - CLY_LOG_I(@"%s viewName: [%@], segmentation: [%@]", __FUNCTION__, viewName, segmentation); + CLY_LOG_I(@"%s viewName: [%@], segmentation: [%@]", __FUNCTION__, viewName, segmentation); - [CountlyViewTrackingInternal.sharedInstance startAutoStoppedView:viewName segmentation:segmentation]; + [CountlyViewTrackingInternal.sharedInstance startAutoStoppedView:viewName segmentation:segmentation]; } -#if (TARGET_OS_IOS || TARGET_OS_VISION || TARGET_OS_TV) +#if (TARGET_OS_IOS || TARGET_OS_VISION || TARGET_OS_TV ) - (void)addExceptionForAutoViewTracking:(NSString *)exception { - CLY_LOG_I(@"%s exception: [%@]", __FUNCTION__, exception); + CLY_LOG_I(@"%s exception: [%@]", __FUNCTION__, exception); - [CountlyViewTrackingInternal.sharedInstance addExceptionForAutoViewTracking:exception.copy]; + [CountlyViewTrackingInternal.sharedInstance addExceptionForAutoViewTracking:exception.copy]; } - (void)removeExceptionForAutoViewTracking:(NSString *)exception { - CLY_LOG_I(@"%s exception: [%@]", __FUNCTION__, exception); + CLY_LOG_I(@"%s exception: [%@]", __FUNCTION__, exception); - [CountlyViewTrackingInternal.sharedInstance removeExceptionForAutoViewTracking:exception.copy]; + [CountlyViewTrackingInternal.sharedInstance removeExceptionForAutoViewTracking:exception.copy]; } - (void)setIsAutoViewTrackingActive:(BOOL)isAutoViewTrackingActive { - CLY_LOG_I(@"%s isAutoViewTrackingActive: [%d]", __FUNCTION__, isAutoViewTrackingActive); + CLY_LOG_I(@"%s isAutoViewTrackingActive: [%d]", __FUNCTION__, isAutoViewTrackingActive); - CountlyViewTrackingInternal.sharedInstance.isAutoViewTrackingActive = isAutoViewTrackingActive; + CountlyViewTrackingInternal.sharedInstance.isAutoViewTrackingActive = isAutoViewTrackingActive; } - (BOOL)isAutoViewTrackingActive { - CLY_LOG_I(@"%s", __FUNCTION__); - return CountlyViewTrackingInternal.sharedInstance.isAutoViewTrackingActive; + CLY_LOG_I(@"%s", __FUNCTION__); + return CountlyViewTrackingInternal.sharedInstance.isAutoViewTrackingActive; } #endif #pragma mark - Star Rating #if (TARGET_OS_IOS) -- (void)askForStarRating:(void (^)(NSInteger rating))completion +- (void)askForStarRating:(void(^)(NSInteger rating))completion { - CLY_LOG_I(@"%s completion: [%@]", __FUNCTION__, completion); - [CountlyFeedbacksInternal.sharedInstance showDialog:completion]; + CLY_LOG_I(@"%s completion: [%@]", __FUNCTION__, completion); + [CountlyFeedbacksInternal.sharedInstance showDialog:completion]; } -- (void)presentFeedbackWidgetWithID:(NSString *)widgetID completionHandler:(void (^)(NSError *error))completionHandler +- (void)presentFeedbackWidgetWithID:(NSString *)widgetID completionHandler:(void (^)(NSError * error))completionHandler { - CLY_LOG_I(@"%s widgetID: [%@], completionHandler: [%@]", __FUNCTION__, widgetID, completionHandler); - - [self presentRatingWidgetWithID:widgetID closeButtonText:nil completionHandler:completionHandler]; + CLY_LOG_I(@"%s widgetID: [%@], completionHandler: [%@]", __FUNCTION__, widgetID, completionHandler); + + [self presentRatingWidgetWithID:widgetID closeButtonText:nil completionHandler:completionHandler]; } -- (void)presentRatingWidgetWithID:(NSString *)widgetID completionHandler:(void (^)(NSError *error))completionHandler +- (void)presentRatingWidgetWithID:(NSString *)widgetID completionHandler:(void (^)(NSError * error))completionHandler { - CLY_LOG_I(@"%s widgetID: [%@], completionHandler: [%@]", __FUNCTION__, widgetID, completionHandler); - [self presentRatingWidgetWithID:widgetID closeButtonText:nil completionHandler:completionHandler]; + CLY_LOG_I(@"%s widgetID: [%@], completionHandler: [%@]", __FUNCTION__, widgetID, completionHandler); + [self presentRatingWidgetWithID:widgetID closeButtonText:nil completionHandler:completionHandler]; } -- (void)presentRatingWidgetWithID:(NSString *)widgetID closeButtonText:(NSString *_Nullable)closeButtonText completionHandler:(void (^)(NSError *__nullable error))completionHandler +- (void)presentRatingWidgetWithID:(NSString *)widgetID closeButtonText:(NSString * _Nullable)closeButtonText completionHandler:(void (^)(NSError * __nullable error))completionHandler { - - CLY_LOG_I(@"%s widgetID: [%@], closeButtonText: [%@], completionHandler: [%@]", __FUNCTION__, widgetID, closeButtonText, completionHandler); - - [CountlyFeedbacksInternal.sharedInstance presentRatingWidgetWithID:widgetID closeButtonText:closeButtonText completionHandler:completionHandler]; + + CLY_LOG_I(@"%s widgetID: [%@], closeButtonText: [%@], completionHandler: [%@]", __FUNCTION__, widgetID, closeButtonText, completionHandler); + + [CountlyFeedbacksInternal.sharedInstance presentRatingWidgetWithID:widgetID closeButtonText:closeButtonText completionHandler:completionHandler]; } -- (void)recordRatingWidgetWithID:(NSString *)widgetID rating:(NSInteger)rating email:(NSString *_Nullable)email comment:(NSString *_Nullable)comment userCanBeContacted:(BOOL)userCanBeContacted +- (void)recordRatingWidgetWithID:(NSString *)widgetID rating:(NSInteger)rating email:(NSString * _Nullable)email comment:(NSString * _Nullable)comment userCanBeContacted:(BOOL)userCanBeContacted { - CLY_LOG_I(@"%s widgetID: [%@], rating: [%ld], email: [%@], comment: [%@], userCanBeContacted: [%d]", __FUNCTION__, widgetID, (long)rating, email, comment, userCanBeContacted); + CLY_LOG_I(@"%s widgetID: [%@], rating: [%ld], email: [%@], comment: [%@], userCanBeContacted: [%d]", __FUNCTION__, widgetID, (long)rating, email, comment, userCanBeContacted); - [CountlyFeedbacksInternal.sharedInstance recordRatingWidgetWithID:widgetID rating:rating email:email comment:comment userCanBeContacted:userCanBeContacted]; + [CountlyFeedbacksInternal.sharedInstance recordRatingWidgetWithID:widgetID rating:rating email:email comment:comment userCanBeContacted:userCanBeContacted]; } -- (void)getFeedbackWidgets:(void (^)(NSArray *feedbackWidgets, NSError *error))completionHandler +- (void)getFeedbackWidgets:(void (^)(NSArray *feedbackWidgets, NSError * error))completionHandler { - CLY_LOG_I(@"%s completionHandler: [%@]", __FUNCTION__, completionHandler); - [CountlyFeedbacksInternal.sharedInstance getFeedbackWidgets:completionHandler]; + CLY_LOG_I(@"%s completionHandler: [%@]", __FUNCTION__, completionHandler); + [CountlyFeedbacksInternal.sharedInstance getFeedbackWidgets:completionHandler]; } #endif #pragma mark - Attribution - (void)recordAttributionID:(NSString *)attributionID { - CLY_LOG_I(@"%s attributionID: [%@]", __FUNCTION__, attributionID); + CLY_LOG_I(@"%s attributionID: [%@]", __FUNCTION__, attributionID); - if (!CountlyConsentManager.sharedInstance.consentForAttribution) - return; + if (!CountlyConsentManager.sharedInstance.consentForAttribution) + return; - CountlyCommon.sharedInstance.attributionID = attributionID; + CountlyCommon.sharedInstance.attributionID = attributionID; - [CountlyConnectionManager.sharedInstance sendAttribution]; + [CountlyConnectionManager.sharedInstance sendAttribution]; } - (void)recordDirectAttributionWithCampaignType:(NSString *)campaignType andCampaignData:(NSString *)campaignData { - CLY_LOG_I(@"%s campaignType: [%@], campaignData: [%@]", __FUNCTION__, campaignType, campaignData); - - if (!CountlyConsentManager.sharedInstance.consentForAttribution) - return; - - if (!campaignType.length) - { - CLY_LOG_E(@"%s campaignType must be non-zero length valid string. Method execution will be aborted!", __FUNCTION__); - return; - } - - if (!campaignData.length) - { - CLY_LOG_E(@"%s campaignData must be non-zero length valid string. Method execution will be aborted!", __FUNCTION__); - return; - } - - if ([campaignType isEqualToString:@"_special_test"]) - { - [CountlyConnectionManager.sharedInstance sendAttributionData:campaignData]; - return; - } - - if (![campaignType isEqualToString:@"countly"]) - { - CLY_LOG_W(@"%s Recording direct attribution with a type other than 'countly' is currently not supported. Method execution will be aborted!", __FUNCTION__); - return; - } - - NSError *error = nil; - NSDictionary *campaignDataDictionary = [NSJSONSerialization JSONObjectWithData:[campaignData cly_dataUTF8] options:0 error:&error]; - if (error) - { - CLY_LOG_E(@"%s Campaign data is not in expected format. Method execution will be aborted!", __FUNCTION__); - return; - } - - NSString *campaignID = campaignDataDictionary[@"cid"]; - if (!campaignID.length) - { - CLY_LOG_E(@"%s Campaign ID must be non-zero length valid string. Method execution will be aborted!", __FUNCTION__); - return; - } - - NSString *campaignUserID = campaignDataDictionary[@"cuid"]; - if (!campaignUserID.length) - { - CLY_LOG_W(@"%s Campaign User ID must be non-zero length valid string. It will be ignored!", __FUNCTION__); - } - - [CountlyConnectionManager.sharedInstance sendDirectAttributionWithCampaignID:campaignID andCampaignUserID:campaignUserID]; + CLY_LOG_I(@"%s campaignType: [%@], campaignData: [%@]", __FUNCTION__, campaignType, campaignData); + + if (!CountlyConsentManager.sharedInstance.consentForAttribution) + return; + + if (!campaignType.length) + { + CLY_LOG_E(@"%s campaignType must be non-zero length valid string. Method execution will be aborted!", __FUNCTION__); + return; + } + + if (!campaignData.length) + { + CLY_LOG_E(@"%s campaignData must be non-zero length valid string. Method execution will be aborted!", __FUNCTION__); + return; + } + + if ([campaignType isEqualToString:@"_special_test"]) + { + [CountlyConnectionManager.sharedInstance sendAttributionData:campaignData]; + return; + } + + if (![campaignType isEqualToString:@"countly"]) + { + CLY_LOG_W(@"%s Recording direct attribution with a type other than 'countly' is currently not supported. Method execution will be aborted!", __FUNCTION__); + return; + } + + NSError* error = nil; + NSDictionary* campaignDataDictionary = [NSJSONSerialization JSONObjectWithData:[campaignData cly_dataUTF8] options:0 error:&error]; + if (error) + { + CLY_LOG_E(@"%s Campaign data is not in expected format. Method execution will be aborted!", __FUNCTION__); + return; + } + + NSString* campaignID = campaignDataDictionary[@"cid"]; + if (!campaignID.length) + { + CLY_LOG_E(@"%s Campaign ID must be non-zero length valid string. Method execution will be aborted!", __FUNCTION__); + return; + } + + NSString* campaignUserID = campaignDataDictionary[@"cuid"]; + if (!campaignUserID.length) + { + CLY_LOG_W(@"%s Campaign User ID must be non-zero length valid string. It will be ignored!", __FUNCTION__); + } + + [CountlyConnectionManager.sharedInstance sendDirectAttributionWithCampaignID:campaignID andCampaignUserID:campaignUserID]; } - (void)recordIndirectAttribution:(NSDictionary *)attribution { - CLY_LOG_I(@"%s attribution: [%@]", __FUNCTION__, attribution); + CLY_LOG_I(@"%s attribution: [%@]", __FUNCTION__, attribution); - if (!CountlyConsentManager.sharedInstance.consentForAttribution) - return; + if (!CountlyConsentManager.sharedInstance.consentForAttribution) + return; - NSMutableDictionary *filtered = attribution.mutableCopy; - [attribution enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSString *value, BOOL *stop) { - if (!value.length) - [filtered removeObjectForKey:key]; - }]; + NSMutableDictionary* filtered = attribution.mutableCopy; + [attribution enumerateKeysAndObjectsUsingBlock:^(NSString * key, NSString * value, BOOL * stop) + { + if (!value.length) + [filtered removeObjectForKey:key]; + }]; - NSDictionary *truncated = [filtered cly_truncated:@"Indirect attribution"]; - NSDictionary *limited = [truncated cly_limited:@"Indirect attribution"]; + NSDictionary* truncated = [filtered cly_truncated:@"Indirect attribution"]; + NSDictionary* limited = [truncated cly_limited:@"Indirect attribution"]; - [CountlyConnectionManager.sharedInstance sendIndirectAttribution:limited]; + [CountlyConnectionManager.sharedInstance sendIndirectAttribution:limited]; } #pragma mark - Remote Config - (id)remoteConfigValueForKey:(NSString *)key { - CLY_LOG_I(@"%s key: [%@]", __FUNCTION__, key); - return [CountlyRemoteConfigInternal.sharedInstance remoteConfigValueForKey:key]; + CLY_LOG_I(@"%s key: [%@]", __FUNCTION__, key); + return [CountlyRemoteConfigInternal.sharedInstance remoteConfigValueForKey:key]; } -- (void)updateRemoteConfigWithCompletionHandler:(void (^)(NSError *error))completionHandler +- (void)updateRemoteConfigWithCompletionHandler:(void (^)(NSError * error))completionHandler { - CLY_LOG_I(@"%s completionHandler: [%@]", __FUNCTION__, completionHandler); - [CountlyRemoteConfigInternal.sharedInstance updateRemoteConfigForKeys:nil omitKeys:nil completionHandler:completionHandler]; + CLY_LOG_I(@"%s completionHandler: [%@]", __FUNCTION__, completionHandler); + [CountlyRemoteConfigInternal.sharedInstance updateRemoteConfigForKeys:nil omitKeys:nil completionHandler:completionHandler]; } -- (void)updateRemoteConfigOnlyForKeys:(NSArray *)keys completionHandler:(void (^)(NSError *error))completionHandler +- (void)updateRemoteConfigOnlyForKeys:(NSArray *)keys completionHandler:(void (^)(NSError * error))completionHandler { - CLY_LOG_I(@"%s keys: [%@], completionHandler: [%@]", __FUNCTION__, keys, completionHandler); - [CountlyRemoteConfigInternal.sharedInstance updateRemoteConfigForKeys:keys omitKeys:nil completionHandler:completionHandler]; + CLY_LOG_I(@"%s keys: [%@], completionHandler: [%@]", __FUNCTION__, keys, completionHandler); + [CountlyRemoteConfigInternal.sharedInstance updateRemoteConfigForKeys:keys omitKeys:nil completionHandler:completionHandler]; } -- (void)updateRemoteConfigExceptForKeys:(NSArray *)omitKeys completionHandler:(void (^)(NSError *error))completionHandler +- (void)updateRemoteConfigExceptForKeys:(NSArray *)omitKeys completionHandler:(void (^)(NSError * error))completionHandler { - CLY_LOG_I(@"%s omitKeys: [%@], completionHandler: [%@]", __FUNCTION__, omitKeys, completionHandler); - [CountlyRemoteConfigInternal.sharedInstance updateRemoteConfigForKeys:nil omitKeys:omitKeys completionHandler:completionHandler]; + CLY_LOG_I(@"%s omitKeys: [%@], completionHandler: [%@]", __FUNCTION__, omitKeys, completionHandler); + [CountlyRemoteConfigInternal.sharedInstance updateRemoteConfigForKeys:nil omitKeys:omitKeys completionHandler:completionHandler]; } #pragma mark - Performance Monitoring - (void)recordNetworkTrace:(NSString *)traceName requestPayloadSize:(NSInteger)requestPayloadSize responsePayloadSize:(NSInteger)responsePayloadSize responseStatusCode:(NSInteger)responseStatusCode startTime:(long long)startTime endTime:(long long)endTime { - CLY_LOG_I(@"%s traceName: [%@], requestPayloadSize: [%ld], responsePayloadSize: [%ld], responseStatusCode: [%ld], startTime: [%lld], endTime: [%lld]", __FUNCTION__, traceName, (long)requestPayloadSize, (long)responsePayloadSize, (long)responseStatusCode, startTime, endTime); + CLY_LOG_I(@"%s traceName: [%@], requestPayloadSize: [%ld], responsePayloadSize: [%ld], responseStatusCode: [%ld], startTime: [%lld], endTime: [%lld]", __FUNCTION__, traceName, (long)requestPayloadSize, (long)responsePayloadSize, (long)responseStatusCode, startTime, endTime); - [CountlyPerformanceMonitoring.sharedInstance recordNetworkTrace:traceName requestPayloadSize:requestPayloadSize responsePayloadSize:responsePayloadSize responseStatusCode:responseStatusCode startTime:startTime endTime:endTime]; + [CountlyPerformanceMonitoring.sharedInstance recordNetworkTrace:traceName requestPayloadSize:requestPayloadSize responsePayloadSize:responsePayloadSize responseStatusCode:responseStatusCode startTime:startTime endTime:endTime]; } - (void)startCustomTrace:(NSString *)traceName { - CLY_LOG_I(@"%s traceName: [%@]", __FUNCTION__, traceName); - [CountlyPerformanceMonitoring.sharedInstance startCustomTrace:traceName]; + CLY_LOG_I(@"%s traceName: [%@]", __FUNCTION__, traceName); + [CountlyPerformanceMonitoring.sharedInstance startCustomTrace:traceName]; } -- (void)endCustomTrace:(NSString *)traceName metrics:(NSDictionary *_Nullable)metrics +- (void)endCustomTrace:(NSString *)traceName metrics:(NSDictionary * _Nullable)metrics { - CLY_LOG_I(@"%s traceName: [%@], metrics: [%@]", __FUNCTION__, traceName, metrics); - [CountlyPerformanceMonitoring.sharedInstance endCustomTrace:traceName metrics:metrics]; + CLY_LOG_I(@"%s traceName: [%@], metrics: [%@]", __FUNCTION__, traceName, metrics); + [CountlyPerformanceMonitoring.sharedInstance endCustomTrace:traceName metrics:metrics]; } - (void)cancelCustomTrace:(NSString *)traceName { - CLY_LOG_I(@"%s traceName: [%@]", __FUNCTION__, traceName); - [CountlyPerformanceMonitoring.sharedInstance cancelCustomTrace:traceName]; + CLY_LOG_I(@"%s traceName: [%@]", __FUNCTION__, traceName); + [CountlyPerformanceMonitoring.sharedInstance cancelCustomTrace:traceName]; } - (void)clearAllCustomTraces { - CLY_LOG_I(@"%s", __FUNCTION__); - [CountlyPerformanceMonitoring.sharedInstance clearAllCustomTraces]; + CLY_LOG_I(@"%s", __FUNCTION__); + [CountlyPerformanceMonitoring.sharedInstance clearAllCustomTraces]; } - (void)appLoadingFinished { - CLY_LOG_I(@"%s", __FUNCTION__); + CLY_LOG_I(@"%s", __FUNCTION__); - long long appLoadEndTime = floor(NSDate.date.timeIntervalSince1970 * 1000); + long long appLoadEndTime = floor(NSDate.date.timeIntervalSince1970 * 1000); - [CountlyPerformanceMonitoring.sharedInstance recordAppStartDurationTraceWithStartTime:appLoadStartTime endTime:appLoadEndTime]; + [CountlyPerformanceMonitoring.sharedInstance recordAppStartDurationTraceWithStartTime:appLoadStartTime endTime:appLoadEndTime]; } - (void)halt { - CLY_LOG_I(@"%s", __FUNCTION__); - [self halt:true]; + CLY_LOG_I(@"%s", __FUNCTION__); + [self halt:true]; } -- (void)halt:(BOOL)clearStorage +- (void)halt:(BOOL) clearStorage { - CLY_LOG_I(@"%s clearStorage: [%d]", __FUNCTION__, clearStorage); + CLY_LOG_I(@"%s clearStorage: [%d]", __FUNCTION__, clearStorage); - // Reset view tracking state BEFORE halt — sharedInstance() returns nil after halt. - // Use KVC to clear internal state directly since stopAllViews checks consent - // and may be a no-op if previous test required consent. - if (CountlyViewTrackingInternal.sharedInstance) - { - CountlyViewTrackingInternal *viewTracking = CountlyViewTrackingInternal.sharedInstance; - [viewTracking setValue:NSMutableDictionary.new forKey:@"viewDataDictionary"]; - [viewTracking setValue:nil forKey:@"currentViewID"]; - [viewTracking setValue:nil forKey:@"currentViewName"]; - [viewTracking setValue:nil forKey:@"previousViewID"]; - [viewTracking setValue:nil forKey:@"previousViewName"]; - [viewTracking setValue:@NO forKey:@"isAutoViewTrackingActive"]; - [viewTracking resetFirstView]; - } + // Reset view tracking state BEFORE halt — sharedInstance() returns nil after halt. + // Use KVC to clear internal state directly since stopAllViews checks consent + // and may be a no-op if previous test required consent. + if (CountlyViewTrackingInternal.sharedInstance) + { + CountlyViewTrackingInternal* viewTracking = CountlyViewTrackingInternal.sharedInstance; + [viewTracking setValue:NSMutableDictionary.new forKey:@"viewDataDictionary"]; + [viewTracking setValue:nil forKey:@"currentViewID"]; + [viewTracking setValue:nil forKey:@"currentViewName"]; + [viewTracking setValue:nil forKey:@"previousViewID"]; + [viewTracking setValue:nil forKey:@"previousViewName"]; + [viewTracking setValue:@NO forKey:@"isAutoViewTrackingActive"]; + [viewTracking resetFirstView]; + } - // Reset health tracker state - [CountlyHealthTracker.sharedInstance resetInstance]; + // Reset health tracker state + [CountlyHealthTracker.sharedInstance resetInstance]; - // Clear crash logs (safe operation - just clears array or deletes file) - if (CountlyCrashReporter.sharedInstance) - { - [CountlyCrashReporter.sharedInstance clearCrashLogs]; - } + // Clear crash logs (safe operation - just clears array or deletes file) + if (CountlyCrashReporter.sharedInstance) + { + [CountlyCrashReporter.sharedInstance clearCrashLogs]; + } - // Clear custom performance monitoring traces (safe operation - just clears dictionary) - if (CountlyPerformanceMonitoring.sharedInstance) - { - [CountlyPerformanceMonitoring.sharedInstance clearAllCustomTraces]; - } - - // Note: CountlyRemoteConfigInternal.clearAll is not called here because it triggers - // storeRemoteConfig which involves file I/O and can block during shutdown. - // Remote config state will persist across halt/start but is cleared via UserDefaults - // key removal below. - - [CountlyConsentManager.sharedInstance resetInstance]; - [CountlyPersistency.sharedInstance resetInstance:clearStorage]; - [CountlyDeviceInfo.sharedInstance resetInstance]; - [CountlyConnectionManager.sharedInstance resetInstance]; - [CountlyServerConfig.sharedInstance resetInstance]; - [CountlyUserDetails.sharedInstance clearUserDetails]; - [self resetInstance]; - [CountlyCommon.sharedInstance resetInstance]; - - if (clearStorage) - { - NSString *appDomain = [[NSBundle mainBundle] bundleIdentifier]; - [NSUserDefaults.standardUserDefaults removePersistentDomainForName:appDomain]; - - // halt(true) calls removePersistentDomainForName which doesn't work in xctest - // environment (different bundle ID), so clear SDK keys manually - NSArray *sdkKeys = @[ - @"kCountlyServerConfigPersistencyKey", @"kCountlyHealthCheckStatePersistencyKey", @"kCountlyQueuedRequestsPersistencyKey", @"kCountlyStartedEventsPersistencyKey", @"kCountlyStoredDeviceIDKey", @"kCountlyStoredNSUUIDKey", @"kCountlyStarRatingStatusKey", @"kCountlyRemoteConfigKey", - @"kCountlyIsCustomDeviceIDKey", @"kCountlyNotificationPermissionKey", @"kCountlyWatchParentDeviceIDKey" - ]; - for (NSString *key in sdkKeys) + // Clear custom performance monitoring traces (safe operation - just clears dictionary) + if (CountlyPerformanceMonitoring.sharedInstance) { - [NSUserDefaults.standardUserDefaults removeObjectForKey:key]; + [CountlyPerformanceMonitoring.sharedInstance clearAllCustomTraces]; } - // Single synchronize after all UserDefaults modifications - [NSUserDefaults.standardUserDefaults synchronize]; - } + // Note: CountlyRemoteConfigInternal.clearAll is not called here because it triggers + // storeRemoteConfig which involves file I/O and can block during shutdown. + // Remote config state will persist across halt/start but is cleared via UserDefaults + // key removal below. + + [CountlyConsentManager.sharedInstance resetInstance]; + [CountlyPersistency.sharedInstance resetInstance:clearStorage]; + [CountlyDeviceInfo.sharedInstance resetInstance]; + [CountlyConnectionManager.sharedInstance resetInstance]; + [CountlyServerConfig.sharedInstance resetInstance]; + [CountlyUserDetails.sharedInstance clearUserDetails]; + [self resetInstance]; + [CountlyCommon.sharedInstance resetInstance]; + + if(clearStorage) + { + NSString *appDomain = [[NSBundle mainBundle] bundleIdentifier]; + [NSUserDefaults.standardUserDefaults removePersistentDomainForName:appDomain]; + + // halt(true) calls removePersistentDomainForName which doesn't work in xctest + // environment (different bundle ID), so clear SDK keys manually + NSArray* sdkKeys = @[ + @"kCountlyServerConfigPersistencyKey", + @"kCountlyHealthCheckStatePersistencyKey", + @"kCountlyQueuedRequestsPersistencyKey", + @"kCountlyStartedEventsPersistencyKey", + @"kCountlyStoredDeviceIDKey", + @"kCountlyStoredNSUUIDKey", + @"kCountlyStarRatingStatusKey", + @"kCountlyRemoteConfigKey", + @"kCountlyIsCustomDeviceIDKey", + @"kCountlyNotificationPermissionKey", + @"kCountlyWatchParentDeviceIDKey" + ]; + for (NSString* key in sdkKeys) + { + [NSUserDefaults.standardUserDefaults removeObjectForKey:key]; + } + + // Single synchronize after all UserDefaults modifications + [NSUserDefaults.standardUserDefaults synchronize]; + } } - (void)attemptToSendStoredRequests { - CLY_LOG_I(@"%s", __FUNCTION__); - [CountlyConnectionManager.sharedInstance attemptToSendStoredRequests]; + CLY_LOG_I(@"%s", __FUNCTION__); + [CountlyConnectionManager.sharedInstance attemptToSendStoredRequests]; } #pragma mark - Interfaces #if (TARGET_OS_IOS) -- (CountlyContentBuilder *)content +- (CountlyContentBuilder *) content { - return CountlyContentBuilder.sharedInstance; + return CountlyContentBuilder.sharedInstance; } -- (CountlyFeedbacks *)feedback +- (CountlyFeedbacks *) feedback { - return CountlyFeedbacks.sharedInstance; + return CountlyFeedbacks.sharedInstance; } #endif -- (CountlyViewTracking *)views +- (CountlyViewTracking *) views { - return CountlyViewTracking.sharedInstance; + return CountlyViewTracking.sharedInstance; } + (CountlyUserDetails *)user { - return CountlyUserDetails.sharedInstance; + return CountlyUserDetails.sharedInstance; } -- (CountlyRemoteConfig *)remoteConfig -{ - return CountlyRemoteConfig.sharedInstance; +- (CountlyRemoteConfig *) remoteConfig { + return CountlyRemoteConfig.sharedInstance; } @end diff --git a/CountlyCommon.h b/CountlyCommon.h index 3026caa7..83753137 100644 --- a/CountlyCommon.h +++ b/CountlyCommon.h @@ -4,34 +4,34 @@ // // Please visit www.count.ly for more information. +#import #import "Countly.h" -#import "CountlyConfig.h" +#import "CountlyServerConfig.h" +#import "CountlyPersistency.h" #import "CountlyConnectionManager.h" -#import "CountlyConsentManager.h" -#import "CountlyContentBuilderInternal.h" -#import "CountlyCrashData.h" -#import "CountlyCrashReporter.h" -#import "CountlyDeviceInfo.h" #import "CountlyEvent.h" -#import "CountlyExperimentalConfig.h" -#import "CountlyFeedbackWidget.h" +#import "CountlyUserDetails.h" +#import "CountlyDeviceInfo.h" +#import "CountlyCrashReporter.h" +#import "CountlyConfig.h" +#import "CountlyViewTrackingInternal.h" #import "CountlyFeedbacksInternal.h" -#import "CountlyHealthTracker.h" -#import "CountlyLocationManager.h" +#import "CountlyFeedbackWidget.h" +#import "CountlyPushNotifications.h" #import "CountlyNotificationService.h" +#import "CountlyConsentManager.h" +#import "CountlyLocationManager.h" +#import "CountlyRemoteConfigInternal.h" #import "CountlyPerformanceMonitoring.h" -#import "CountlyPersistency.h" -#import "CountlyPushNotifications.h" #import "CountlyRCData.h" -#import "CountlyRemoteConfig.h" -#import "CountlyRemoteConfigInternal.h" -#import "CountlyServerConfig.h" -#import "CountlyUserDetails.h" #import "CountlyViewData.h" +#import "CountlyRemoteConfig.h" #import "CountlyViewTracking.h" -#import "CountlyViewTrackingInternal.h" #import "Resettable.h" -#import +#import "CountlyCrashData.h" +#import "CountlyContentBuilderInternal.h" +#import "CountlyExperimentalConfig.h" +#import "CountlyHealthTracker.h" #define CLY_LOG_E(fmt, ...) CountlyInternalLog(CLYInternalLogLevelError, fmt, ##__VA_ARGS__) #define CLY_LOG_W(fmt, ...) CountlyInternalLog(CLYInternalLogLevelWarning, fmt, ##__VA_ARGS__) @@ -40,57 +40,63 @@ #define CLY_LOG_V(fmt, ...) CountlyInternalLog(CLYInternalLogLevelVerbose, fmt, ##__VA_ARGS__) #if (TARGET_OS_IOS) - #import - #import +#import +#import #endif #if (TARGET_OS_WATCH) - #import "WatchConnectivity/WatchConnectivity.h" - #import +#import +#import "WatchConnectivity/WatchConnectivity.h" #endif #if (TARGET_OS_TV) - #import +#import #endif #import NS_ASSUME_NONNULL_BEGIN -extern NSString *const kCountlyErrorDomain; +extern NSString* const kCountlyErrorDomain; -extern NSString *const kCountlyReservedEventOrientation; -extern NSString *const kCountlyVisibility; +extern NSString* const kCountlyReservedEventOrientation; +extern NSString* const kCountlyVisibility; -NS_ERROR_ENUM(kCountlyErrorDomain){ - CLYErrorFeedbackWidgetNotAvailable = 10001, CLYErrorFeedbackWidgetNotTargetedForDevice = 10002, CLYErrorRemoteConfigGeneralAPIError = 10011, CLYErrorFeedbacksGeneralAPIError = 10012, CLYErrorServerConfigGeneralAPIError = 10013, +NS_ERROR_ENUM(kCountlyErrorDomain) +{ + CLYErrorFeedbackWidgetNotAvailable = 10001, + CLYErrorFeedbackWidgetNotTargetedForDevice = 10002, + CLYErrorRemoteConfigGeneralAPIError = 10011, + CLYErrorFeedbacksGeneralAPIError = 10012, + CLYErrorServerConfigGeneralAPIError = 10013, }; -extern NSString *const kCountlySDKVersion; -extern NSString *const kCountlySDKName; +extern NSString* const kCountlySDKVersion; +extern NSString* const kCountlySDKName; @interface CountlyCommon : NSObject -@property(nonatomic, copy) NSString *SDKVersion; -@property(nonatomic, copy) NSString *SDKName; +@property (nonatomic, copy) NSString* SDKVersion; +@property (nonatomic, copy) NSString* SDKName; -@property(nonatomic) BOOL hasStarted; -@property(nonatomic) BOOL hasFinishedInit; -@property(nonatomic) BOOL enableDebug; +@property (nonatomic) BOOL hasStarted; +@property (nonatomic) BOOL hasFinishedInit; +@property (nonatomic) BOOL enableDebug; -@property(nonatomic) BOOL shouldIgnoreTrustCheck; -@property(nonatomic, weak) id loggerDelegate; -@property(nonatomic) CLYInternalLogLevel internalLogLevel; -@property(nonatomic, copy) NSString *attributionID; -@property(nonatomic) BOOL manualSessionHandling; -@property(nonatomic) BOOL enableManualSessionControlHybridMode; -@property(nonatomic) BOOL enableOrientationTracking; +@property (nonatomic) BOOL shouldIgnoreTrustCheck; +@property (nonatomic, weak) id loggerDelegate; +@property (nonatomic) CLYInternalLogLevel internalLogLevel; +@property (nonatomic, copy) NSString* attributionID; +@property (nonatomic) BOOL manualSessionHandling; +@property (nonatomic) BOOL enableManualSessionControlHybridMode; +@property (nonatomic) BOOL enableOrientationTracking; -@property(nonatomic) BOOL enableVisibiltyTracking; +@property (nonatomic) BOOL enableVisibiltyTracking; -@property(nonatomic) NSUInteger maxKeyLength; -@property(nonatomic) NSUInteger maxValueLength; -@property(nonatomic) NSUInteger maxSegmentationValues; + +@property (nonatomic) NSUInteger maxKeyLength; +@property (nonatomic) NSUInteger maxValueLength; +@property (nonatomic) NSUInteger maxSegmentationValues; void CountlyInternalLog(CLYInternalLogLevel level, NSString *format, ...) NS_FORMAT_FUNCTION(2, 3); void CountlyPrint(NSString *stringToPrint); @@ -106,10 +112,10 @@ void CountlyPrint(NSString *stringToPrint); - (void)startBackgroundTask; - (void)finishBackgroundTask; -#if (TARGET_OS_IOS || TARGET_OS_VISION || TARGET_OS_TV) +#if (TARGET_OS_IOS || TARGET_OS_VISION || TARGET_OS_TV ) - (UIViewController *)topViewController; - (void)tryPresentingViewController:(UIViewController *)viewController; -- (void)tryPresentingViewController:(UIViewController *)viewController withCompletion:(void (^__nullable)(void))completion; +- (void)tryPresentingViewController:(UIViewController *)viewController withCompletion:(void (^ __nullable) (void))completion; #endif - (void)observeDeviceOrientationChanges; @@ -123,22 +129,23 @@ void CountlyPrint(NSString *stringToPrint); - (CGSize)getWindowSize; @end + #if (TARGET_OS_IOS) -@interface CLYInternalViewController : UIViewController -@property(nonatomic, weak) WKWebView *webView; +@interface CLYInternalViewController : UIViewController +@property (nonatomic, weak) WKWebView* webView; @end @interface CLYButton : UIButton -@property(nonatomic, copy) void (^onClick)(id sender); +@property (nonatomic, copy) void (^onClick)(id sender); + (CLYButton *)dismissAlertButton; -+ (CLYButton *)dismissAlertButton:(NSString *_Nullable)closeButtonText; ++ (CLYButton *)dismissAlertButton:(NSString * _Nullable)closeButtonText; - (void)positionToTopRight; - (void)positionToTopRightConsideringStatusBar; @end #endif -@interface CLYDelegateInterceptor : NSObject -@property(nonatomic, weak) id originalDelegate; +@interface CLYDelegateInterceptor : NSObject +@property (nonatomic, weak) id originalDelegate; @end @interface NSString (Countly) diff --git a/CountlyCommon.m b/CountlyCommon.m index bd5f9636..ecd3bc09 100644 --- a/CountlyCommon.m +++ b/CountlyCommon.m @@ -7,185 +7,187 @@ #import "CountlyCommon.h" #include -NSString *const kCountlyReservedEventOrientation = @"[CLY]_orientation"; -NSString *const kCountlyOrientationKeyMode = @"mode"; +NSString* const kCountlyReservedEventOrientation = @"[CLY]_orientation"; +NSString* const kCountlyOrientationKeyMode = @"mode"; -NSString *const kCountlyVisibility = @"cly_v"; +NSString* const kCountlyVisibility = @"cly_v"; -@interface CountlyCommon () { - NSCalendar *gregorianCalendar; - NSTimeInterval startTime; + +@interface CountlyCommon () +{ + NSCalendar* gregorianCalendar; + NSTimeInterval startTime; } @property long long lastTimestamp; -#if (TARGET_OS_IOS || TARGET_OS_VISION) -@property(nonatomic) NSString *lastInterfaceOrientation; +#if (TARGET_OS_IOS || TARGET_OS_VISION ) +@property (nonatomic) NSString* lastInterfaceOrientation; #endif -#if (TARGET_OS_IOS || TARGET_OS_VISION || TARGET_OS_TV) -@property(nonatomic) UIBackgroundTaskIdentifier bgTask; +#if (TARGET_OS_IOS || TARGET_OS_VISION || TARGET_OS_TV ) +@property (nonatomic) UIBackgroundTaskIdentifier bgTask; #endif @end -NSString *const kCountlySDKVersion = @"26.1.1"; -NSString *const kCountlySDKName = @"objc-native-ios"; +NSString* const kCountlySDKVersion = @"26.1.1"; +NSString* const kCountlySDKName = @"objc-native-ios"; + +NSString* const kCountlyErrorDomain = @"ly.count.ErrorDomain"; -NSString *const kCountlyErrorDomain = @"ly.count.ErrorDomain"; +NSString* const kCountlyInternalLogPrefix = @"[Countly] "; -NSString *const kCountlyInternalLogPrefix = @"[Countly] "; @implementation CountlyCommon @synthesize lastTimestamp; -static CountlyCommon *s_sharedInstance = nil; +static CountlyCommon *s_sharedInstance = nil; static dispatch_once_t onceToken; + (instancetype)sharedInstance { - dispatch_once(&onceToken, ^{ - s_sharedInstance = self.new; - }); - return s_sharedInstance; + dispatch_once(&onceToken, ^{s_sharedInstance = self.new;}); + return s_sharedInstance; } - (instancetype)init { - if (self = [super init]) - { - gregorianCalendar = [NSCalendar.alloc initWithCalendarIdentifier:NSCalendarIdentifierGregorian]; - startTime = NSDate.date.timeIntervalSince1970; - - self.lastTimestamp = 0; - self.SDKVersion = kCountlySDKVersion; - self.SDKName = kCountlySDKName; - } + if (self = [super init]) + { + gregorianCalendar = [NSCalendar.alloc initWithCalendarIdentifier:NSCalendarIdentifierGregorian]; + startTime = NSDate.date.timeIntervalSince1970; + + self.lastTimestamp = 0; + self.SDKVersion = kCountlySDKVersion; + self.SDKName = kCountlySDKName; + } - return self; + return self; } -- (void)resetInstance -{ - CLY_LOG_I(@"%s", __FUNCTION__); -#if (TARGET_OS_IOS || TARGET_OS_VISION) - [NSNotificationCenter.defaultCenter removeObserver:self name:UIDeviceOrientationDidChangeNotification object:nil]; +- (void)resetInstance { + CLY_LOG_I(@"%s", __FUNCTION__); +#if (TARGET_OS_IOS || TARGET_OS_VISION ) + [NSNotificationCenter.defaultCenter removeObserver:self name:UIDeviceOrientationDidChangeNotification object:nil]; #endif - _hasStarted = false; - _hasFinishedInit = false; - _maxKeyLength = kCountlyMaxKeyLength; - _maxValueLength = kCountlyMaxValueSize; - _maxSegmentationValues = kCountlyMaxSegmentationValues; - onceToken = 0; - s_sharedInstance = nil; -} + _hasStarted = false; + _hasFinishedInit = false; + _maxKeyLength = kCountlyMaxKeyLength; + _maxValueLength = kCountlyMaxValueSize; + _maxSegmentationValues = kCountlyMaxSegmentationValues; + onceToken = 0; + s_sharedInstance = nil; + } + - (BOOL)hasStarted { - if (!_hasStarted) - CountlyPrint(@"SDK should be started first!"); + if (!_hasStarted) + CountlyPrint(@"SDK should be started first!"); - return _hasStarted; + return _hasStarted; } -// NOTE: This is an equivalent of hasStarted, but without internal logging. +//NOTE: This is an equivalent of hasStarted, but without internal logging. - (BOOL)hasStarted_ { - return _hasStarted; + return _hasStarted; } void CountlyInternalLog(CLYInternalLogLevel level, NSString *format, ...) { - if (level == CLYInternalLogLevelError) - { - [CountlyHealthTracker.sharedInstance logError]; - } - else if (level == CLYInternalLogLevelWarning) - { - [CountlyHealthTracker.sharedInstance logWarning]; - } - - if (!CountlyCommon.sharedInstance.enableDebug && !CountlyCommon.sharedInstance.loggerDelegate) - return; + if (level == CLYInternalLogLevelError) { + [CountlyHealthTracker.sharedInstance logError]; + } else if(level == CLYInternalLogLevelWarning) { + [CountlyHealthTracker.sharedInstance logWarning]; + } + + if (!CountlyCommon.sharedInstance.enableDebug && !CountlyCommon.sharedInstance.loggerDelegate) + return; - if (level > CountlyCommon.sharedInstance.internalLogLevel) - return; + if (level > CountlyCommon.sharedInstance.internalLogLevel) + return; - va_list args; - va_start(args, format); + va_list args; + va_start(args, format); - NSString *logString = [NSString.alloc initWithFormat:format arguments:args]; + NSString* logString = [NSString.alloc initWithFormat:format arguments:args]; - NSArray *logLevelPrefixes = @[ - @"None", - @"Error", - @"Warning", - @"Info", - @"Debug", - @"Verbose", - ]; + NSArray *logLevelPrefixes = + @[ + @"None", + @"Error", + @"Warning", + @"Info", + @"Debug", + @"Verbose", + ]; - logString = [NSString stringWithFormat:@"[%@] %@", logLevelPrefixes[level], logString]; + logString = [NSString stringWithFormat:@"[%@] %@", logLevelPrefixes[level], logString]; #if DEBUG - if (CountlyCommon.sharedInstance.enableDebug) - CountlyPrint(logString); + if (CountlyCommon.sharedInstance.enableDebug) + CountlyPrint(logString); #endif - if ([CountlyCommon.sharedInstance.loggerDelegate respondsToSelector:@selector(internalLog:withLevel:)]) - { - NSString *logStringWithPrefix = [NSString stringWithFormat:@"%@%@", kCountlyInternalLogPrefix, logString]; - [CountlyCommon.sharedInstance.loggerDelegate internalLog:logStringWithPrefix withLevel:level]; - } + if ([CountlyCommon.sharedInstance.loggerDelegate respondsToSelector:@selector(internalLog:withLevel:)]) + { + NSString* logStringWithPrefix = [NSString stringWithFormat:@"%@%@", kCountlyInternalLogPrefix, logString]; + [CountlyCommon.sharedInstance.loggerDelegate internalLog:logStringWithPrefix withLevel:level]; + } - va_end(args); + va_end(args); } -void CountlyPrint(NSString *stringToPrint) { NSLog(@"%@%@", kCountlyInternalLogPrefix, stringToPrint); } +void CountlyPrint(NSString *stringToPrint) +{ + NSLog(@"%@%@", kCountlyInternalLogPrefix, stringToPrint); +} #pragma mark - Time/Date related methods - (NSInteger)hourOfDay { - NSDateComponents *components = [gregorianCalendar components:NSCalendarUnitHour fromDate:NSDate.date]; - return components.hour; + NSDateComponents* components = [gregorianCalendar components:NSCalendarUnitHour fromDate:NSDate.date]; + return components.hour; } - (NSInteger)dayOfWeek { - NSDateComponents *components = [gregorianCalendar components:NSCalendarUnitWeekday fromDate:NSDate.date]; - return components.weekday - 1; + NSDateComponents* components = [gregorianCalendar components:NSCalendarUnitWeekday fromDate:NSDate.date]; + return components.weekday - 1; } - (NSInteger)timeZone { - return NSTimeZone.systemTimeZone.secondsFromGMT / 60; + return NSTimeZone.systemTimeZone.secondsFromGMT / 60; } - (NSInteger)timeSinceLaunch { - return (int)NSDate.date.timeIntervalSince1970 - startTime; + return (int)NSDate.date.timeIntervalSince1970 - startTime; } - (NSTimeInterval)uniqueTimestamp { - long long now = floor(NSDate.date.timeIntervalSince1970 * 1000); + long long now = floor(NSDate.date.timeIntervalSince1970 * 1000); - if (now <= self.lastTimestamp) - self.lastTimestamp++; - else - self.lastTimestamp = now; + if (now <= self.lastTimestamp) + self.lastTimestamp++; + else + self.lastTimestamp = now; - return (NSTimeInterval)(self.lastTimestamp / 1000.0); + return (NSTimeInterval)(self.lastTimestamp / 1000.0); } - (NSString *)randomEventID { - const int size = 6; - void *randomBuffer = malloc(size); - arc4random_buf(randomBuffer, size); - NSData *randomData = [NSData dataWithBytesNoCopy:randomBuffer length:size freeWhenDone:YES]; - NSString *randomBase64 = [randomData base64EncodedStringWithOptions:0]; - NSTimeInterval timestamp = self.uniqueTimestamp; - NSString *randomEventID = [NSString stringWithFormat:@"%@%lld", randomBase64, (long long)(timestamp * 1000)]; - return randomEventID; + const int size = 6; + void *randomBuffer = malloc(size); + arc4random_buf(randomBuffer, size); + NSData* randomData = [NSData dataWithBytesNoCopy:randomBuffer length:size freeWhenDone:YES]; + NSString* randomBase64 = [randomData base64EncodedStringWithOptions:0]; + NSTimeInterval timestamp = self.uniqueTimestamp; + NSString* randomEventID = [NSString stringWithFormat:@"%@%lld", randomBase64, (long long)(timestamp * 1000)]; + return randomEventID; } #pragma mark - Orientation @@ -193,62 +195,61 @@ - (NSString *)randomEventID - (void)observeDeviceOrientationChanges { #if (TARGET_OS_IOS) - [NSNotificationCenter.defaultCenter addObserver:self selector:@selector(deviceOrientationDidChange:) name:UIDeviceOrientationDidChangeNotification object:nil]; + [NSNotificationCenter.defaultCenter addObserver:self selector:@selector(deviceOrientationDidChange:) name:UIDeviceOrientationDidChangeNotification object:nil]; #endif } - (void)deviceOrientationDidChange:(NSNotification *)notification { - if (!self.enableOrientationTracking) - return; + if (!self.enableOrientationTracking) + return; - // NOTE: Delay is needed for interface orientation change animation to complete. Otherwise old interface orientation value is returned. - [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(recordOrientation) object:nil]; - [self performSelector:@selector(recordOrientation) withObject:nil afterDelay:0.5]; + //NOTE: Delay is needed for interface orientation change animation to complete. Otherwise old interface orientation value is returned. + [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(recordOrientation) object:nil]; + [self performSelector:@selector(recordOrientation) withObject:nil afterDelay:0.5]; } - (void)recordOrientation { #if (TARGET_OS_IOS) - if (!self.enableOrientationTracking) - return; - - if ([UIApplication sharedApplication].applicationState == UIApplicationStateBackground) - { - CLY_LOG_W(@"%s App is in the background, 'Record Orientation' will be ignored", __FUNCTION__); - return; - } - #pragma GCC diagnostic push - #pragma GCC diagnostic ignored "-Wdeprecated-declarations" - UIInterfaceOrientation interfaceOrientation = UIApplication.sharedApplication.statusBarOrientation; - #pragma GCC diagnostic pop - - NSString *mode = nil; - if (UIInterfaceOrientationIsPortrait(interfaceOrientation)) - mode = @"portrait"; - else if (UIInterfaceOrientationIsLandscape(interfaceOrientation)) - mode = @"landscape"; - - if (!mode) - { - CLY_LOG_D(@"Interface orientation is not landscape or portrait."); - return; - } - - if ([mode isEqualToString:self.lastInterfaceOrientation]) - { - CLY_LOG_V(@"Interface orientation is still same: %@", self.lastInterfaceOrientation); - return; - } - - CLY_LOG_D(@"Interface orientation is now: %@", mode); - - if (!CountlyConsentManager.sharedInstance.consentForUserDetails) - return; - - self.lastInterfaceOrientation = mode; - - [Countly.sharedInstance recordReservedEvent:kCountlyReservedEventOrientation segmentation:@{kCountlyOrientationKeyMode : mode}]; + if (!self.enableOrientationTracking) + return; + + if ([UIApplication sharedApplication].applicationState == UIApplicationStateBackground) { + CLY_LOG_W(@"%s App is in the background, 'Record Orientation' will be ignored", __FUNCTION__); + return; + } +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" + UIInterfaceOrientation interfaceOrientation = UIApplication.sharedApplication.statusBarOrientation; +#pragma GCC diagnostic pop + + NSString* mode = nil; + if (UIInterfaceOrientationIsPortrait(interfaceOrientation)) + mode = @"portrait"; + else if (UIInterfaceOrientationIsLandscape(interfaceOrientation)) + mode = @"landscape"; + + if (!mode) + { + CLY_LOG_D(@"Interface orientation is not landscape or portrait."); + return; + } + + if ([mode isEqualToString:self.lastInterfaceOrientation]) + { + CLY_LOG_V(@"Interface orientation is still same: %@", self.lastInterfaceOrientation); + return; + } + + CLY_LOG_D(@"Interface orientation is now: %@", mode); + + if (!CountlyConsentManager.sharedInstance.consentForUserDetails) + return; + + self.lastInterfaceOrientation = mode; + + [Countly.sharedInstance recordReservedEvent:kCountlyReservedEventOrientation segmentation:@{kCountlyOrientationKeyMode: mode}]; #endif } @@ -257,398 +258,389 @@ - (void)recordOrientation - (void)startBackgroundTask { #if (TARGET_OS_IOS || TARGET_OS_VISION || TARGET_OS_TV) - if (self.bgTask != UIBackgroundTaskInvalid) - return; + if (self.bgTask != UIBackgroundTaskInvalid) + return; - self.bgTask = [UIApplication.sharedApplication beginBackgroundTaskWithExpirationHandler:^{ - [UIApplication.sharedApplication endBackgroundTask:self.bgTask]; - self.bgTask = UIBackgroundTaskInvalid; - }]; + self.bgTask = [UIApplication.sharedApplication beginBackgroundTaskWithExpirationHandler:^ + { + [UIApplication.sharedApplication endBackgroundTask:self.bgTask]; + self.bgTask = UIBackgroundTaskInvalid; + }]; #endif } - (void)finishBackgroundTask { #if (TARGET_OS_IOS || TARGET_OS_VISION || TARGET_OS_TV) - if (self.bgTask != UIBackgroundTaskInvalid && !CountlyConnectionManager.sharedInstance.connection) - { - [UIApplication.sharedApplication endBackgroundTask:self.bgTask]; - self.bgTask = UIBackgroundTaskInvalid; - } + if (self.bgTask != UIBackgroundTaskInvalid && !CountlyConnectionManager.sharedInstance.connection) + { + [UIApplication.sharedApplication endBackgroundTask:self.bgTask]; + self.bgTask = UIBackgroundTaskInvalid; + } #endif } #if (TARGET_OS_IOS || TARGET_OS_TV) - (UIViewController *)topViewController { - #pragma GCC diagnostic push - #pragma GCC diagnostic ignored "-Wdeprecated-declarations" - UIViewController *topVC = UIApplication.sharedApplication.keyWindow.rootViewController; - #pragma GCC diagnostic pop - - while (YES) - { - if (topVC.presentedViewController) - topVC = topVC.presentedViewController; - else if ([topVC isKindOfClass:UINavigationController.class]) - topVC = ((UINavigationController *)topVC).topViewController; - else if ([topVC isKindOfClass:UITabBarController.class]) - topVC = ((UITabBarController *)topVC).selectedViewController; - else - break; - } +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" + UIViewController* topVC = UIApplication.sharedApplication.keyWindow.rootViewController; +#pragma GCC diagnostic pop - return topVC; + while (YES) + { + if (topVC.presentedViewController) + topVC = topVC.presentedViewController; + else if ([topVC isKindOfClass:UINavigationController.class]) + topVC = ((UINavigationController *)topVC).topViewController; + else if ([topVC isKindOfClass:UITabBarController.class]) + topVC = ((UITabBarController *)topVC).selectedViewController; + else + break; + } + + return topVC; } - (void)tryPresentingViewController:(UIViewController *)viewController { - [self tryPresentingViewController:viewController withCompletion:nil]; + [self tryPresentingViewController:viewController withCompletion:nil]; } -- (void)tryPresentingViewController:(UIViewController *)viewController withCompletion:(void (^__nullable)(void))completion +- (void)tryPresentingViewController:(UIViewController *)viewController withCompletion:(void (^ __nullable) (void))completion { - UIViewController *topVC = self.topViewController; + UIViewController* topVC = self.topViewController; - if (topVC) - { - [topVC presentViewController:viewController - animated:YES - completion:^{ - if (completion) - completion(); - }]; + if (topVC) + { + [topVC presentViewController:viewController animated:YES completion:^ + { + if (completion) + completion(); + }]; - return; - } + return; + } - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ - [self tryPresentingViewController:viewController]; - }); + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^ + { + [self tryPresentingViewController:viewController]; + }); } #endif - (NSURLSession *)URLSession { - if (CountlyConnectionManager.sharedInstance.URLSessionConfiguration) - { - return [NSURLSession sessionWithConfiguration:CountlyConnectionManager.sharedInstance.URLSessionConfiguration]; - } - else - { - return NSURLSession.sharedSession; - } + if (CountlyConnectionManager.sharedInstance.URLSessionConfiguration) + { + return [NSURLSession sessionWithConfiguration:CountlyConnectionManager.sharedInstance.URLSessionConfiguration]; + } + else + { + return NSURLSession.sharedSession; + } } #if (TARGET_OS_IOS) -- (bool)hasTopNotch:(UIEdgeInsets)safeArea +- (bool) hasTopNotch:(UIEdgeInsets)safeArea { - return safeArea.top >= 44; + return safeArea.top >= 44; } #endif -- (CGSize)getWindowSize -{ +- (CGSize)getWindowSize{ #if (TARGET_OS_IOS) - UIWindow *window = nil; - - if (@available(iOS 13.0, *)) - { - for (UIScene *scene in UIApplication.sharedApplication.connectedScenes) - { - if ([scene isKindOfClass:[UIWindowScene class]]) - { - window = ((UIWindowScene *)scene).windows.firstObject; - break; - } + UIWindow *window = nil; + + if (@available(iOS 13.0, *)) { + for (UIScene *scene in UIApplication.sharedApplication.connectedScenes) { + if ([scene isKindOfClass:[UIWindowScene class]]) { + window = ((UIWindowScene *)scene).windows.firstObject; + break; + } + } + } else { + window = UIApplication.sharedApplication.delegate.window; } - } - else - { - window = UIApplication.sharedApplication.delegate.window; - } - if (!window) - return CGSizeZero; + if (!window) return CGSizeZero; - CGSize size = window.bounds.size; + CGSize size = window.bounds.size; - if (@available(iOS 11.0, *)) - { - UIEdgeInsets safeArea = window.safeAreaInsets; - if ([self hasTopNotch:safeArea] || CountlyContentBuilderInternal.sharedInstance.webViewDisplayOption == SAFE_AREA) - { - size.height -= (safeArea.top); // always respect notch - } - if (CountlyContentBuilderInternal.sharedInstance.webViewDisplayOption == SAFE_AREA) - { - size.height -= safeArea.bottom; + if (@available(iOS 11.0, *)) { + UIEdgeInsets safeArea = window.safeAreaInsets; + if([self hasTopNotch:safeArea] || CountlyContentBuilderInternal.sharedInstance.webViewDisplayOption == SAFE_AREA){ + size.height -= (safeArea.top); // always respect notch + } + if(CountlyContentBuilderInternal.sharedInstance.webViewDisplayOption == SAFE_AREA){ + size.height -= safeArea.bottom; + } + size.width -= MAX(safeArea.left, safeArea.right); // regardles of given safe area, act for cutout } - size.width -= MAX(safeArea.left, safeArea.right); // regardles of given safe area, act for cutout - } - return size; + return size; #else - return CGSizeZero; + return CGSizeZero; #endif } + @end + #pragma mark - Internal ViewController #if (TARGET_OS_IOS) @implementation CLYInternalViewController : UIViewController - (void)viewWillLayoutSubviews { - [super viewWillLayoutSubviews]; - - if (self.webView) - { - CGRect frame = CGRectInset(self.view.bounds, 20.0, 20.0); + [super viewWillLayoutSubviews]; - UIEdgeInsets insets = UIEdgeInsetsZero; - if (@available(iOS 11.0, *)) + if (self.webView) { - #pragma GCC diagnostic push - #pragma GCC diagnostic ignored "-Wdeprecated-declarations" - insets = UIApplication.sharedApplication.keyWindow.safeAreaInsets; - #pragma GCC diagnostic pop + CGRect frame = CGRectInset(self.view.bounds, 20.0, 20.0); + + UIEdgeInsets insets = UIEdgeInsetsZero; + if (@available(iOS 11.0, *)) + { + #pragma GCC diagnostic push + #pragma GCC diagnostic ignored "-Wdeprecated-declarations" + insets = UIApplication.sharedApplication.keyWindow.safeAreaInsets; + #pragma GCC diagnostic pop + } + + self.webView.navigationDelegate = self; + frame = UIEdgeInsetsInsetRect(frame, insets); + self.webView.frame = frame; } +} - self.webView.navigationDelegate = self; - frame = UIEdgeInsetsInsetRect(frame, insets); - self.webView.frame = frame; - } -} - -- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler -{ - NSString *url = navigationAction.request.URL.absoluteString; - if ([url containsString:@"cly_x_int=1"]) - { - CLY_LOG_I(@"%s Opening url [%@] in external browser", __FUNCTION__, url); - [[UIApplication sharedApplication] openURL:navigationAction.request.URL - options:@{} - completionHandler:^(BOOL success) { - if (success) - { - CLY_LOG_I(@"%s url [%@] opened in external browser", __FUNCTION__, url); - } - else - { - CLY_LOG_I(@"%s unable to open url [%@] in external browser", __FUNCTION__, url); - } - }]; - decisionHandler(WKNavigationActionPolicyCancel); - return; - } - decisionHandler(WKNavigationActionPolicyAllow); +- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler { + NSString *url = navigationAction.request.URL.absoluteString; + if ([url containsString:@"cly_x_int=1"]) { + CLY_LOG_I(@"%s Opening url [%@] in external browser", __FUNCTION__, url); + [[UIApplication sharedApplication] openURL:navigationAction.request.URL options:@{} completionHandler:^(BOOL success) { + if (success) { + CLY_LOG_I(@"%s url [%@] opened in external browser", __FUNCTION__, url); + } + else { + CLY_LOG_I(@"%s unable to open url [%@] in external browser", __FUNCTION__, url); + } + }]; + decisionHandler(WKNavigationActionPolicyCancel); + return; + } + decisionHandler(WKNavigationActionPolicyAllow); } - (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(null_unspecified WKNavigation *)navigation { - CLY_LOG_I(@"%s Web view has start loading", __FUNCTION__); + CLY_LOG_I(@"%s Web view has start loading", __FUNCTION__); + } -- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation -{ - CLY_LOG_I(@"%s Web view has finished loading", __FUNCTION__); +- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation { + CLY_LOG_I(@"%s Web view has finished loading", __FUNCTION__); } + @end + @implementation CLYButton : UIButton -const CGFloat kCountlyDismissButtonSize = 30.0; -const CGFloat kCountlyDismissButtonMargin = 10.0; +const CGFloat kCountlyDismissButtonSize = 30.0; +const CGFloat kCountlyDismissButtonMargin = 10.0; const CGFloat kCountlyDismissButtonStandardStatusBarHeight = 20.0; - (instancetype)initWithFrame:(CGRect)frame { - if (self = [super initWithFrame:frame]) - { - [self addTarget:self action:@selector(touchUpInside:) forControlEvents:UIControlEventTouchUpInside]; - } + if (self = [super initWithFrame:frame]) + { + [self addTarget:self action:@selector(touchUpInside:) forControlEvents:UIControlEventTouchUpInside]; + } - return self; + return self; } - (void)touchUpInside:(id)sender { - if (self.onClick) - self.onClick(self); + if (self.onClick) + self.onClick(self); } -+ (CLYButton *)dismissAlertButton:(NSString *_Nullable)closeButtonText ++ (CLYButton *)dismissAlertButton:(NSString * _Nullable)closeButtonText { - if (!closeButtonText) - { - closeButtonText = @"x"; - } - CLYButton *dismissButton = [CLYButton buttonWithType:UIButtonTypeCustom]; - dismissButton.frame = (CGRect){CGPointZero, kCountlyDismissButtonSize, kCountlyDismissButtonSize}; - [dismissButton setTitle:closeButtonText forState:UIControlStateNormal]; - [dismissButton setTitleColor:UIColor.whiteColor forState:UIControlStateNormal]; - dismissButton.backgroundColor = [UIColor.blackColor colorWithAlphaComponent:0.5]; - dismissButton.layer.cornerRadius = dismissButton.bounds.size.width * 0.5; - dismissButton.layer.borderColor = [UIColor.blackColor colorWithAlphaComponent:0.7].CGColor; - dismissButton.layer.borderWidth = 1.0; - dismissButton.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleBottomMargin; - - return dismissButton; + if (!closeButtonText) { + closeButtonText = @"x"; + } + CLYButton* dismissButton = [CLYButton buttonWithType:UIButtonTypeCustom]; + dismissButton.frame = (CGRect){CGPointZero, kCountlyDismissButtonSize, kCountlyDismissButtonSize}; + [dismissButton setTitle:closeButtonText forState:UIControlStateNormal]; + [dismissButton setTitleColor:UIColor.whiteColor forState:UIControlStateNormal]; + dismissButton.backgroundColor = [UIColor.blackColor colorWithAlphaComponent:0.5]; + dismissButton.layer.cornerRadius = dismissButton.bounds.size.width * 0.5; + dismissButton.layer.borderColor = [UIColor.blackColor colorWithAlphaComponent:0.7].CGColor; + dismissButton.layer.borderWidth = 1.0; + dismissButton.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleBottomMargin; + + return dismissButton; } + (CLYButton *)dismissAlertButton { - return [CLYButton dismissAlertButton:nil]; + return [CLYButton dismissAlertButton:nil]; } - (void)positionToTopRight { - [self positionToTopRight:NO]; + [self positionToTopRight:NO]; } - (void)positionToTopRightConsideringStatusBar { - [self positionToTopRight:YES]; + [self positionToTopRight:YES]; } - (void)positionToTopRight:(BOOL)shouldConsiderStatusBar { - CGRect rect = self.frame; - rect.origin.x = self.superview.bounds.size.width - self.bounds.size.width - kCountlyDismissButtonMargin; - rect.origin.y = kCountlyDismissButtonMargin; + CGRect rect = self.frame; + rect.origin.x = self.superview.bounds.size.width - self.bounds.size.width - kCountlyDismissButtonMargin; + rect.origin.y = kCountlyDismissButtonMargin; - if (shouldConsiderStatusBar) - { - if (@available(iOS 11.0, *)) + if (shouldConsiderStatusBar) { - #pragma GCC diagnostic push - #pragma GCC diagnostic ignored "-Wdeprecated-declarations" - CGFloat top = UIApplication.sharedApplication.keyWindow.safeAreaInsets.top; - #pragma GCC diagnostic pop - - if (top) - { - rect.origin.y += top; - } - else - { - rect.origin.y += kCountlyDismissButtonStandardStatusBarHeight; - } + if (@available(iOS 11.0, *)) + { +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" + CGFloat top = UIApplication.sharedApplication.keyWindow.safeAreaInsets.top; +#pragma GCC diagnostic pop + + if (top) + { + rect.origin.y += top; + } + else + { + rect.origin.y += kCountlyDismissButtonStandardStatusBarHeight; + } + } + else + { + rect.origin.y += kCountlyDismissButtonStandardStatusBarHeight; + } } - else - { - rect.origin.y += kCountlyDismissButtonStandardStatusBarHeight; - } - } - self.frame = rect; + self.frame = rect; } @end #endif + #pragma mark - Proxy Object @implementation CLYDelegateInterceptor - (NSMethodSignature *)methodSignatureForSelector:(SEL)sel { - return [self.originalDelegate methodSignatureForSelector:sel]; + return [self.originalDelegate methodSignatureForSelector:sel]; } - (void)forwardInvocation:(NSInvocation *)invocation { - if ([self.originalDelegate respondsToSelector:invocation.selector]) - [invocation invokeWithTarget:self.originalDelegate]; - else - [super forwardInvocation:invocation]; + if ([self.originalDelegate respondsToSelector:invocation.selector]) + [invocation invokeWithTarget:self.originalDelegate]; + else + [super forwardInvocation:invocation]; } @end + + #pragma mark - Categories -NSString *CountlyJSONFromObject(id object) +NSString* CountlyJSONFromObject(id object) { - if (!object) - return nil; + if (!object) + return nil; - if (![NSJSONSerialization isValidJSONObject:object]) - { - CLY_LOG_W(@"Object is not valid for converting to JSON!"); - return nil; - } + if (![NSJSONSerialization isValidJSONObject:object]) + { + CLY_LOG_W(@"Object is not valid for converting to JSON!"); + return nil; + } - NSError *error = nil; - NSData *data = [NSJSONSerialization dataWithJSONObject:object options:0 error:&error]; - if (error) - { - CLY_LOG_W(@"%s, JSON can not be created error:[ %@ ]", __FUNCTION__, error); - } + NSError *error = nil; + NSData *data = [NSJSONSerialization dataWithJSONObject:object options:0 error:&error]; + if (error) + { + CLY_LOG_W(@"%s, JSON can not be created error:[ %@ ]", __FUNCTION__, error); + } - return [data cly_stringUTF8]; + return [data cly_stringUTF8]; } @implementation NSString (Countly) - (NSString *)cly_URLEscaped { - NSCharacterSet *charset = [NSCharacterSet characterSetWithCharactersInString:@"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._~"]; - return [self stringByAddingPercentEncodingWithAllowedCharacters:charset]; + NSCharacterSet* charset = [NSCharacterSet characterSetWithCharactersInString:@"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._~"]; + return [self stringByAddingPercentEncodingWithAllowedCharacters:charset]; } - (NSString *)cly_SHA256 { - const char *s = [self UTF8String]; - unsigned char digest[CC_SHA256_DIGEST_LENGTH]; - CC_SHA256(s, (CC_LONG)strlen(s), digest); + const char* s = [self UTF8String]; + unsigned char digest[CC_SHA256_DIGEST_LENGTH]; + CC_SHA256(s, (CC_LONG)strlen(s), digest); - NSMutableString *hash = NSMutableString.new; - for (int i = 0; i < CC_SHA256_DIGEST_LENGTH; i++) - [hash appendFormat:@"%02x", digest[i]]; + NSMutableString* hash = NSMutableString.new; + for (int i = 0; i < CC_SHA256_DIGEST_LENGTH; i++) + [hash appendFormat:@"%02x", digest[i]]; - return hash; + return hash; } - (NSData *)cly_dataUTF8 { - return [self dataUsingEncoding:NSUTF8StringEncoding]; + return [self dataUsingEncoding:NSUTF8StringEncoding]; } - (NSString *)cly_valueForQueryStringKey:(NSString *)key { - NSString *tempURLString = [@"http://example.com/path?" stringByAppendingString:self]; - NSURLComponents *URLComponents = [NSURLComponents componentsWithString:tempURLString]; - for (NSURLQueryItem *queryItem in URLComponents.queryItems) - { - if ([queryItem.name isEqualToString:key]) + NSString* tempURLString = [@"http://example.com/path?" stringByAppendingString:self]; + NSURLComponents* URLComponents = [NSURLComponents componentsWithString:tempURLString]; + for (NSURLQueryItem* queryItem in URLComponents.queryItems) { - return queryItem.value; + if ([queryItem.name isEqualToString:key]) + { + return queryItem.value; + } } - } - return nil; + return nil; } - (NSString *)cly_truncatedKey:(NSString *)explanation { - if (self.length > CountlyCommon.sharedInstance.maxKeyLength) - { - CLY_LOG_W(@"%@ length is more than the limit (%ld)! So, it will be truncated: %@.", explanation, (long)CountlyCommon.sharedInstance.maxKeyLength, self); - return [self substringToIndex:CountlyCommon.sharedInstance.maxKeyLength]; - } + if (self.length > CountlyCommon.sharedInstance.maxKeyLength) + { + CLY_LOG_W(@"%@ length is more than the limit (%ld)! So, it will be truncated: %@.", explanation, (long)CountlyCommon.sharedInstance.maxKeyLength, self); + return [self substringToIndex:CountlyCommon.sharedInstance.maxKeyLength]; + } - return self; + return self; } - (NSString *)cly_truncatedValue:(NSString *)explanation { - if (self.length > CountlyCommon.sharedInstance.maxValueLength) - { - CLY_LOG_W(@"%@ length is more than the limit (%ld)! So, it will be truncated: %@.", explanation, (long)CountlyCommon.sharedInstance.maxValueLength, self); - return [self substringToIndex:CountlyCommon.sharedInstance.maxValueLength]; - } + if (self.length > CountlyCommon.sharedInstance.maxValueLength) + { + CLY_LOG_W(@"%@ length is more than the limit (%ld)! So, it will be truncated: %@.", explanation, (long)CountlyCommon.sharedInstance.maxValueLength, self); + return [self substringToIndex:CountlyCommon.sharedInstance.maxValueLength]; + } - return self; + return self; } @end @@ -656,94 +648,88 @@ - (NSString *)cly_truncatedValue:(NSString *)explanation @implementation NSArray (Countly) - (NSString *)cly_JSONify { - return [CountlyJSONFromObject(self) cly_URLEscaped]; + return [CountlyJSONFromObject(self) cly_URLEscaped]; } -- (NSArray *)cly_filterSupportedDataTypes -{ - NSMutableArray *filteredArray = [NSMutableArray array]; - for (id obj in self) - { - if ([obj isKindOfClass:[NSNumber class]] || [obj isKindOfClass:[NSString class]]) - { - [filteredArray addObject:obj]; - } - else - { - CLY_LOG_W(@"%s, Removed invalid type from array: %@", __FUNCTION__, [obj class]); +- (NSArray *) cly_filterSupportedDataTypes { + NSMutableArray *filteredArray = [NSMutableArray array]; + for (id obj in self) { + if ([obj isKindOfClass:[NSNumber class]] || [obj isKindOfClass:[NSString class]]) { + [filteredArray addObject:obj]; + } else { + CLY_LOG_W(@"%s, Removed invalid type from array: %@", __FUNCTION__, [obj class]); + } } - } - return filteredArray.copy; + return filteredArray.copy; } @end @implementation NSDictionary (Countly) - (NSString *)cly_JSONify { - return [CountlyJSONFromObject(self) cly_URLEscaped]; + return [CountlyJSONFromObject(self) cly_URLEscaped]; } - (NSDictionary *)cly_truncated:(NSString *)explanation { - NSMutableDictionary *truncatedDict = self.mutableCopy; - [self enumerateKeysAndObjectsUsingBlock:^(NSString *key, id obj, BOOL *stop) { - NSString *truncatedKey = [key cly_truncatedKey:[explanation stringByAppendingString:@" key"]]; - if (![truncatedKey isEqualToString:key]) + NSMutableDictionary* truncatedDict = self.mutableCopy; + [self enumerateKeysAndObjectsUsingBlock:^(NSString * key, id obj, BOOL * stop) { - truncatedDict[truncatedKey] = obj; - [truncatedDict removeObjectForKey:key]; - } - - if ([obj isKindOfClass:NSString.class]) - { - NSString *truncatedValue = [obj cly_truncatedValue:[explanation stringByAppendingString:@" value"]]; - if (![truncatedValue isEqualToString:obj]) - { - truncatedDict[truncatedKey] = truncatedValue; - } - } - }]; - - return truncatedDict.copy; + NSString* truncatedKey = [key cly_truncatedKey:[explanation stringByAppendingString:@" key"]]; + if (![truncatedKey isEqualToString:key]) + { + truncatedDict[truncatedKey] = obj; + [truncatedDict removeObjectForKey:key]; + } + + if ([obj isKindOfClass:NSString.class]) + { + NSString* truncatedValue = [obj cly_truncatedValue:[explanation stringByAppendingString:@" value"]]; + if (![truncatedValue isEqualToString:obj]) + { + truncatedDict[truncatedKey] = truncatedValue; + } + } + }]; + + return truncatedDict.copy; } - (NSDictionary *)cly_limited:(NSString *)explanation { - NSArray *allKeys = self.allKeys; + NSArray* allKeys = self.allKeys; - if (allKeys.count <= CountlyCommon.sharedInstance.maxSegmentationValues) - return self; + if (allKeys.count <= CountlyCommon.sharedInstance.maxSegmentationValues) + return self; - NSMutableArray *excessKeys = allKeys.mutableCopy; - [excessKeys removeObjectsInRange:(NSRange){0, CountlyCommon.sharedInstance.maxSegmentationValues}]; + NSMutableArray* excessKeys = allKeys.mutableCopy; + [excessKeys removeObjectsInRange:(NSRange){0, CountlyCommon.sharedInstance.maxSegmentationValues}]; - CLY_LOG_W(@"%s, Number of key-value pairs in %@ is more than the limit (%ld)! So, some of them will be removed %@", __FUNCTION__, explanation, (long)CountlyCommon.sharedInstance.maxSegmentationValues, [excessKeys description]); + CLY_LOG_W(@"%s, Number of key-value pairs in %@ is more than the limit (%ld)! So, some of them will be removed %@", __FUNCTION__, explanation, (long)CountlyCommon.sharedInstance.maxSegmentationValues, [excessKeys description]); - NSMutableDictionary *limitedDict = self.mutableCopy; - [limitedDict removeObjectsForKeys:excessKeys]; + NSMutableDictionary* limitedDict = self.mutableCopy; + [limitedDict removeObjectsForKeys:excessKeys]; - return limitedDict.copy; + return limitedDict.copy; } -- (NSMutableDictionary *)cly_filterSupportedDataTypes +- (NSMutableDictionary *) cly_filterSupportedDataTypes { - NSMutableDictionary *filteredDictionary = [NSMutableDictionary dictionary]; - - for (NSString *key in self) - { - id value = [self objectForKey:key]; - - if ([value isKindOfClass:[NSNumber class]] || [value isKindOfClass:[NSString class]] || ([value isKindOfClass:[NSArray class]] && (value = [value cly_filterSupportedDataTypes]))) - { - [filteredDictionary setObject:value forKey:key]; - } - else - { - CLY_LOG_W(@"%s, Removed invalid type for key %@: %@", __FUNCTION__, key, [value class]); + NSMutableDictionary *filteredDictionary = [NSMutableDictionary dictionary]; + + for (NSString *key in self) { + id value = [self objectForKey:key]; + + if ([value isKindOfClass:[NSNumber class]] || + [value isKindOfClass:[NSString class]] || + ([value isKindOfClass:[NSArray class]] && (value = [value cly_filterSupportedDataTypes]))) { + [filteredDictionary setObject:value forKey:key]; + } else { + CLY_LOG_W(@"%s, Removed invalid type for key %@: %@", __FUNCTION__, key, [value class]); + } } - } - - return filteredDictionary.mutableCopy; + + return filteredDictionary.mutableCopy; } @end @@ -751,6 +737,6 @@ - (NSMutableDictionary *)cly_filterSupportedDataTypes @implementation NSData (Countly) - (NSString *)cly_stringUTF8 { - return [NSString.alloc initWithData:self encoding:NSUTF8StringEncoding]; + return [NSString.alloc initWithData:self encoding:NSUTF8StringEncoding]; } @end diff --git a/CountlyConnectionManager.m b/CountlyConnectionManager.m index 4d43f84e..3b18749f 100644 --- a/CountlyConnectionManager.m +++ b/CountlyConnectionManager.m @@ -7,1441 +7,1427 @@ #import "CountlyCommon.h" #import -@interface CountlyConnectionManager () { - NSTimeInterval unsentSessionLength; - NSTimeInterval lastSessionStartTime; - BOOL isCrashing; - BOOL isSessionStarted; +@interface CountlyConnectionManager () +{ + NSTimeInterval unsentSessionLength; + NSTimeInterval lastSessionStartTime; + BOOL isCrashing; + BOOL isSessionStarted; } -@property(nonatomic) NSURLSession *URLSession; +@property (nonatomic) NSURLSession* URLSession; -@property(nonatomic, strong) NSDate *startTime; -@property(nonatomic, assign) atomic_bool backoff; -@property(nonatomic, assign) atomic_bool isProcessingQueue; -@property(nonatomic, strong) NSMutableDictionary *internalRequestCallbacks; -@property(nonatomic, strong) NSMutableArray *queueFlushRunnables; -@property(nonatomic) BOOL hasAnyRequestFailed; -@property(nonatomic, strong) dispatch_queue_t callbackQueue; // Serial queue for thread-safe callback/runnable access +@property (nonatomic, strong) NSDate *startTime; +@property (nonatomic, assign) atomic_bool backoff; +@property (nonatomic, assign) atomic_bool isProcessingQueue; +@property (nonatomic, strong) NSMutableDictionary *internalRequestCallbacks; +@property (nonatomic, strong) NSMutableArray *queueFlushRunnables; +@property (nonatomic) BOOL hasAnyRequestFailed; +@property (nonatomic, strong) dispatch_queue_t callbackQueue; // Serial queue for thread-safe callback/runnable access @end -NSString *const kCountlyQSKeyAppKey = @"app_key"; - -NSString *const kCountlyQSKeyDeviceID = @"device_id"; -NSString *const kCountlyQSKeyDeviceIDOld = @"old_device_id"; -NSString *const kCountlyQSKeyDeviceIDType = @"t"; - -NSString *const kCountlyQSKeyTimestamp = @"timestamp"; -NSString *const kCountlyQSKeyTimeZone = @"tz"; -NSString *const kCountlyQSKeyTimeHourOfDay = @"hour"; -NSString *const kCountlyQSKeyTimeDayOfWeek = @"dow"; - -NSString *const kCountlyQSKeySDKVersion = @"sdk_version"; -NSString *const kCountlyQSKeySDKName = @"sdk_name"; - -NSString *const kCountlyQSKeySessionBegin = @"begin_session"; -NSString *const kCountlyQSKeySessionDuration = @"session_duration"; -NSString *const kCountlyQSKeySessionEnd = @"end_session"; - -NSString *const kCountlyQSKeyPushTokenSession = @"token_session"; -NSString *const kCountlyQSKeyPushTokeniOS = @"ios_token"; -NSString *const kCountlyQSKeyPushTestMode = @"test_mode"; - -NSString *const kCountlyQSKeyLocation = @"location"; -NSString *const kCountlyQSKeyLocationCity = @"city"; -NSString *const kCountlyQSKeyLocationCountry = @"country_code"; -NSString *const kCountlyQSKeyLocationIP = @"ip_address"; - -NSString *const kCountlyQSKeyAttributionID = @"aid"; -NSString *const kCountlyQSKeyIDFA = @"idfa"; -NSString *const kCountlyQSKeyADID = @"adid"; -NSString *const kCountlyQSKeyCampaignID = @"campaign_id"; -NSString *const kCountlyQSKeyCampaignUser = @"campaign_user"; -NSString *const kCountlyQSKeyAttributionData = @"attribution_data"; - -NSString *const kCountlyQSKeyMetrics = @"metrics"; -NSString *const kCountlyQSKeyEvents = @"events"; -NSString *const kCountlyQSKeyUserDetails = @"user_details"; -NSString *const kCountlyQSKeyCrash = @"crash"; -NSString *const kCountlyQSKeyChecksum256 = @"checksum256"; -NSString *const kCountlyQSKeyConsent = @"consent"; -NSString *const kCountlyQSKeyAPM = @"apm"; -NSString *const kCountlyQSKeyRemainingRequest = @"rr"; - -NSString *const kCountlyQSKeyMethod = @"method"; - -NSString *const kCountlyRCKeyABOptIn = @"ab"; -NSString *const kCountlyRCKeyABOptOut = @"ab_opt_out"; -NSString *const kCountlyEndPointOverrideTag = @"&new_end_point="; -NSString *const kCountlyNewEndPoint = @"new_end_point"; -NSString *const kCountlyCallbackID = @"callback_id"; +NSString* const kCountlyQSKeyAppKey = @"app_key"; + +NSString* const kCountlyQSKeyDeviceID = @"device_id"; +NSString* const kCountlyQSKeyDeviceIDOld = @"old_device_id"; +NSString* const kCountlyQSKeyDeviceIDType = @"t"; + +NSString* const kCountlyQSKeyTimestamp = @"timestamp"; +NSString* const kCountlyQSKeyTimeZone = @"tz"; +NSString* const kCountlyQSKeyTimeHourOfDay = @"hour"; +NSString* const kCountlyQSKeyTimeDayOfWeek = @"dow"; + +NSString* const kCountlyQSKeySDKVersion = @"sdk_version"; +NSString* const kCountlyQSKeySDKName = @"sdk_name"; + +NSString* const kCountlyQSKeySessionBegin = @"begin_session"; +NSString* const kCountlyQSKeySessionDuration = @"session_duration"; +NSString* const kCountlyQSKeySessionEnd = @"end_session"; + +NSString* const kCountlyQSKeyPushTokenSession = @"token_session"; +NSString* const kCountlyQSKeyPushTokeniOS = @"ios_token"; +NSString* const kCountlyQSKeyPushTestMode = @"test_mode"; + +NSString* const kCountlyQSKeyLocation = @"location"; +NSString* const kCountlyQSKeyLocationCity = @"city"; +NSString* const kCountlyQSKeyLocationCountry = @"country_code"; +NSString* const kCountlyQSKeyLocationIP = @"ip_address"; + +NSString* const kCountlyQSKeyAttributionID = @"aid"; +NSString* const kCountlyQSKeyIDFA = @"idfa"; +NSString* const kCountlyQSKeyADID = @"adid"; +NSString* const kCountlyQSKeyCampaignID = @"campaign_id"; +NSString* const kCountlyQSKeyCampaignUser = @"campaign_user"; +NSString* const kCountlyQSKeyAttributionData = @"attribution_data"; + +NSString* const kCountlyQSKeyMetrics = @"metrics"; +NSString* const kCountlyQSKeyEvents = @"events"; +NSString* const kCountlyQSKeyUserDetails = @"user_details"; +NSString* const kCountlyQSKeyCrash = @"crash"; +NSString* const kCountlyQSKeyChecksum256 = @"checksum256"; +NSString* const kCountlyQSKeyConsent = @"consent"; +NSString* const kCountlyQSKeyAPM = @"apm"; +NSString* const kCountlyQSKeyRemainingRequest = @"rr"; + +NSString* const kCountlyQSKeyMethod = @"method"; + +NSString* const kCountlyRCKeyABOptIn = @"ab"; +NSString* const kCountlyRCKeyABOptOut = @"ab_opt_out"; +NSString* const kCountlyEndPointOverrideTag = @"&new_end_point="; +NSString* const kCountlyNewEndPoint = @"new_end_point"; +NSString* const kCountlyCallbackID = @"callback_id"; CLYAttributionKey const CLYAttributionKeyIDFA = kCountlyQSKeyIDFA; CLYAttributionKey const CLYAttributionKeyADID = kCountlyQSKeyADID; -NSString *const kCountlyUploadBoundary = @"0cae04a8b698d63ff6ea55d168993f21"; +NSString* const kCountlyUploadBoundary = @"0cae04a8b698d63ff6ea55d168993f21"; -NSString *const kCountlyEndpointI = @"/i"; // NOTE: input endpoint -NSString *const kCountlyEndpointO = @"/o"; // NOTE: output endpoint -NSString *const kCountlyEndpointSDK = @"/sdk"; -NSString *const kCountlyEndpointFeedback = @"/feedback"; -NSString *const kCountlyEndpointWidget = @"/widget"; -NSString *const kCountlyEndpointSurveys = @"/surveys"; +NSString* const kCountlyEndpointI = @"/i"; //NOTE: input endpoint +NSString* const kCountlyEndpointO = @"/o"; //NOTE: output endpoint +NSString* const kCountlyEndpointSDK = @"/sdk"; +NSString* const kCountlyEndpointFeedback = @"/feedback"; +NSString* const kCountlyEndpointWidget = @"/widget"; +NSString* const kCountlyEndpointSurveys = @"/surveys"; const NSInteger kCountlyGETRequestMaxLength = 2048; @implementation CountlyConnectionManager : NSObject static CountlyConnectionManager *s_sharedInstance = nil; -static dispatch_once_t onceToken; +static dispatch_once_t onceToken; + (instancetype)sharedInstance { - if (!CountlyCommon.sharedInstance.hasStarted) - return nil; - dispatch_once(&onceToken, ^{ - s_sharedInstance = self.new; - }); - return s_sharedInstance; + if (!CountlyCommon.sharedInstance.hasStarted) + return nil; + dispatch_once(&onceToken, ^{s_sharedInstance = self.new;}); + return s_sharedInstance; } - (instancetype)init { - if (self = [super init]) - { - unsentSessionLength = 0.0; - isSessionStarted = NO; - atomic_init(&_backoff, NO); - atomic_init(&_isProcessingQueue, NO); - _internalRequestCallbacks = [NSMutableDictionary dictionary]; - _queueFlushRunnables = [NSMutableArray array]; - _hasAnyRequestFailed = NO; - _callbackQueue = dispatch_queue_create("ly.count.callbackQueue", DISPATCH_QUEUE_SERIAL); - } - - return self; + if (self = [super init]) + { + unsentSessionLength = 0.0; + isSessionStarted = NO; + atomic_init(&_backoff, NO); + atomic_init(&_isProcessingQueue, NO); + _internalRequestCallbacks = [NSMutableDictionary dictionary]; + _queueFlushRunnables = [NSMutableArray array]; + _hasAnyRequestFailed = NO; + _callbackQueue = dispatch_queue_create("ly.count.callbackQueue", DISPATCH_QUEUE_SERIAL); + } + + return self; } -- (BOOL)isSessionStarted -{ - return isSessionStarted; + +- (BOOL)isSessionStarted { + return isSessionStarted; } -- (void)resetInstance -{ - CLY_LOG_I(@"%s", __FUNCTION__); - onceToken = 0; - s_sharedInstance = nil; - isSessionStarted = NO; - dispatch_sync(_callbackQueue, ^{ - [self->_internalRequestCallbacks removeAllObjects]; - [self->_queueFlushRunnables removeAllObjects]; - }); - _hasAnyRequestFailed = NO; - atomic_store(&_isProcessingQueue, NO); +- (void)resetInstance { + CLY_LOG_I(@"%s", __FUNCTION__); + onceToken = 0; + s_sharedInstance = nil; + isSessionStarted = NO; + dispatch_sync(_callbackQueue, ^{ + [self->_internalRequestCallbacks removeAllObjects]; + [self->_queueFlushRunnables removeAllObjects]; + }); + _hasAnyRequestFailed = NO; + atomic_store(&_isProcessingQueue, NO); } - (void)setHost:(NSString *)host { - if ([host hasSuffix:@"/"]) - { - CLY_LOG_W(@"Host has an extra \"/\" at the end! It will be removed by the SDK.\ + if ([host hasSuffix:@"/"]) + { + CLY_LOG_W(@"Host has an extra \"/\" at the end! It will be removed by the SDK.\ But please make sure you fix it to avoid this warning in the future."); - _host = [host substringToIndex:host.length - 1]; - } - else - { - _host = host; - } + _host = [host substringToIndex:host.length - 1]; + } + else + { + _host = host; + } } - (void)setURLSessionConfiguration:(NSURLSessionConfiguration *)URLSessionConfiguration { - if (URLSessionConfiguration != nil) - { - _URLSessionConfiguration = URLSessionConfiguration; - _URLSession = nil; - } + if (URLSessionConfiguration != nil) + { + _URLSessionConfiguration = URLSessionConfiguration; + _URLSession = nil; + } } -- (void)addCustomNetworkRequestHeaders:(NSDictionary *_Nullable)customHeaderValues -{ - if (_URLSessionConfiguration == nil) - { - return; - } +- (void)addCustomNetworkRequestHeaders:(NSDictionary *_Nullable)customHeaderValues { + if (_URLSessionConfiguration == nil) { + return; + } - // Start with current headers (or empty if nil) - NSMutableDictionary *updatedHeaders = [NSMutableDictionary dictionaryWithDictionary:_URLSessionConfiguration.HTTPAdditionalHeaders ?: @{}]; + // Start with current headers (or empty if nil) + NSMutableDictionary *updatedHeaders = [NSMutableDictionary dictionaryWithDictionary:_URLSessionConfiguration.HTTPAdditionalHeaders ?: @{}]; - // Enumerate and validate custom headers - [customHeaderValues enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSString *value, BOOL *stop) { - if (key == nil || key.length == 0) - { - return; // Skip empty key - } - if (value == nil) - { - return; // Skip nil value - } + // Enumerate and validate custom headers + [customHeaderValues enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSString *value, BOOL *stop) { + if (key == nil || key.length == 0) { + return; // Skip empty key + } + if (value == nil) { + return; // Skip nil value + } - // Add or override - updatedHeaders[key] = value; - }]; + // Add or override + updatedHeaders[key] = value; + }]; - // Apply updated headers - _URLSessionConfiguration.HTTPAdditionalHeaders = [updatedHeaders copy]; - _URLSession = nil; + // Apply updated headers + _URLSessionConfiguration.HTTPAdditionalHeaders = [updatedHeaders copy]; + _URLSession = nil; } - (void)proceedOnQueue { - CLY_LOG_D(@"Proceeding on queue..."); - - if (!CountlyServerConfig.sharedInstance.networkingEnabled) - { - CLY_LOG_D(@"Proceeding on queue is aborted: SDK Networking is disabled from server config!"); - return; - } + CLY_LOG_D(@"Proceeding on queue..."); + + if (!CountlyServerConfig.sharedInstance.networkingEnabled) + { + CLY_LOG_D(@"Proceeding on queue is aborted: SDK Networking is disabled from server config!"); + return; + } + + if (self.connection || atomic_exchange(&_isProcessingQueue, YES)) + { + CLY_LOG_D(@"Proceeding on queue is aborted: Already has a request in process!"); + return; + } - if (self.connection || atomic_exchange(&_isProcessingQueue, YES)) - { - CLY_LOG_D(@"Proceeding on queue is aborted: Already has a request in process!"); - return; - } + if (isCrashing) + { + CLY_LOG_D(@"Proceeding on queue is aborted: Application is crashing!"); + atomic_store(&_isProcessingQueue, NO); + return; + } - if (isCrashing) - { - CLY_LOG_D(@"Proceeding on queue is aborted: Application is crashing!"); - atomic_store(&_isProcessingQueue, NO); - return; - } + if (self.isTerminating) + { + CLY_LOG_D(@"Proceeding on queue is aborted: Application is terminating!"); + atomic_store(&_isProcessingQueue, NO); + return; + } - if (self.isTerminating) - { - CLY_LOG_D(@"Proceeding on queue is aborted: Application is terminating!"); - atomic_store(&_isProcessingQueue, NO); - return; - } + if (CountlyPersistency.sharedInstance.isQueueBeingModified) + { + CLY_LOG_D(@"Proceeding on queue is aborted: Queue is being modified!"); + atomic_store(&_isProcessingQueue, NO); + return; + } - if (CountlyPersistency.sharedInstance.isQueueBeingModified) - { - CLY_LOG_D(@"Proceeding on queue is aborted: Queue is being modified!"); - atomic_store(&_isProcessingQueue, NO); - return; - } + BOOL backoffFlag = atomic_load(&_backoff) ? YES : NO; + if (backoffFlag) { + CLY_LOG_I(@"%s, currently backed off, skipping proceeding the queue", __FUNCTION__); + atomic_store(&_isProcessingQueue, NO); + return; + } + + if (!self.startTime) { + self.startTime = [NSDate date]; // Record start time only when it's not already recorded + self.hasAnyRequestFailed = NO; // Reset failure flag when starting queue processing + CLY_LOG_D(@"%s, Proceeding on queue started, queued request count %lu", __FUNCTION__, [CountlyPersistency.sharedInstance remainingRequestCount]); + } - BOOL backoffFlag = atomic_load(&_backoff) ? YES : NO; - if (backoffFlag) - { - CLY_LOG_I(@"%s, currently backed off, skipping proceeding the queue", __FUNCTION__); - atomic_store(&_isProcessingQueue, NO); - return; - } - - if (!self.startTime) - { - self.startTime = [NSDate date]; // Record start time only when it's not already recorded - self.hasAnyRequestFailed = NO; // Reset failure flag when starting queue processing - CLY_LOG_D(@"%s, Proceeding on queue started, queued request count %lu", __FUNCTION__, [CountlyPersistency.sharedInstance remainingRequestCount]); - } - - NSString *firstItemInQueue = [CountlyPersistency.sharedInstance firstItemInQueue]; - if (!firstItemInQueue) - { - // Calculate total time when the queue becomes empty - NSTimeInterval elapsedTime = -[self.startTime timeIntervalSinceNow]; - CLY_LOG_D(@"%s, Queue is empty. All requests are processed. Total time taken: %.2f seconds", __FUNCTION__, elapsedTime); - - // Execute and clear runnables only if all requests succeeded - if (!self.hasAnyRequestFailed) + NSString* firstItemInQueue = [CountlyPersistency.sharedInstance firstItemInQueue]; + if (!firstItemInQueue) { - // Thread-safe copy and clear of runnables - __block NSArray *runnablesToExecute = nil; - dispatch_sync(_callbackQueue, ^{ - if (self->_queueFlushRunnables.count > 0) - { - CLY_LOG_D(@"%s, All requests succeeded. Executing %lu queue flush runnables.", __FUNCTION__, (unsigned long)self->_queueFlushRunnables.count); - runnablesToExecute = [self->_queueFlushRunnables copy]; - [self->_queueFlushRunnables removeAllObjects]; + // Calculate total time when the queue becomes empty + NSTimeInterval elapsedTime = -[self.startTime timeIntervalSinceNow]; + CLY_LOG_D(@"%s, Queue is empty. All requests are processed. Total time taken: %.2f seconds", __FUNCTION__, elapsedTime); + + // Execute and clear runnables only if all requests succeeded + if (!self.hasAnyRequestFailed) { + // Thread-safe copy and clear of runnables + __block NSArray *runnablesToExecute = nil; + dispatch_sync(_callbackQueue, ^{ + if (self->_queueFlushRunnables.count > 0) { + CLY_LOG_D(@"%s, All requests succeeded. Executing %lu queue flush runnables.", __FUNCTION__, (unsigned long)self->_queueFlushRunnables.count); + runnablesToExecute = [self->_queueFlushRunnables copy]; + [self->_queueFlushRunnables removeAllObjects]; + } + }); + + // Execute runnables outside the lock to prevent deadlocks + if (runnablesToExecute) { + for (CLYQueueFlushRunnable runnable in runnablesToExecute) { + runnable(); + } + CLY_LOG_D(@"%s, All queue flush runnables executed and removed.", __FUNCTION__); + } + } else { + CLY_LOG_D(@"%s, Some requests failed. Runnables will not be executed.", __FUNCTION__); } - }); - // Execute runnables outside the lock to prevent deadlocks - if (runnablesToExecute) - { - for (CLYQueueFlushRunnable runnable in runnablesToExecute) - { - runnable(); - } - CLY_LOG_D(@"%s, All queue flush runnables executed and removed.", __FUNCTION__); - } + // Reset start time and failure flag for future queue processing + self.startTime = nil; + self.hasAnyRequestFailed = NO; + atomic_store(&_isProcessingQueue, NO); + return; } - else + + BOOL isOldRequest = [CountlyPersistency.sharedInstance isOldRequest:firstItemInQueue]; + if(isOldRequest) { - CLY_LOG_D(@"%s, Some requests failed. Runnables will not be executed.", __FUNCTION__); - } - - // Reset start time and failure flag for future queue processing - self.startTime = nil; - self.hasAnyRequestFailed = NO; - atomic_store(&_isProcessingQueue, NO); - return; - } - - BOOL isOldRequest = [CountlyPersistency.sharedInstance isOldRequest:firstItemInQueue]; - if (isOldRequest) - { - [CountlyPersistency.sharedInstance removeFromQueue:firstItemInQueue]; - - [CountlyPersistency.sharedInstance saveToFile]; + [CountlyPersistency.sharedInstance removeFromQueue:firstItemInQueue]; + + [CountlyPersistency.sharedInstance saveToFile]; - atomic_store(&_isProcessingQueue, NO); - [self proceedOnQueue]; - - return; - } - - NSString *temporaryDeviceIDQueryString = [NSString stringWithFormat:@"&%@=%@", kCountlyQSKeyDeviceID, CLYTemporaryDeviceID]; - if ([firstItemInQueue containsString:temporaryDeviceIDQueryString]) - { - CLY_LOG_D(@"Proceeding on queue is aborted: Device ID in request is CLYTemporaryDeviceID!"); - atomic_store(&_isProcessingQueue, NO); - return; - } - - _URLSessionConfiguration.timeoutIntervalForRequest = [CountlyServerConfig.sharedInstance requestTimeoutDuration]; - _URLSessionConfiguration.timeoutIntervalForResource = [CountlyServerConfig.sharedInstance requestTimeoutDuration]; - NSString *queryString = firstItemInQueue; - NSString *endPoint = kCountlyEndpointI; - - NSString *overrideEndPoint = [self extractAndRemoveParameter:&queryString parameter:kCountlyNewEndPoint]; - if (overrideEndPoint) - { - endPoint = overrideEndPoint; - } - - NSString *callbackID = [self extractAndRemoveParameter:&queryString parameter:kCountlyCallbackID]; - __block CLYRequestCallback requestCallback = nil; - if (callbackID) - { - dispatch_sync(_callbackQueue, ^{ - requestCallback = self.internalRequestCallbacks[callbackID]; - }); - } + atomic_store(&_isProcessingQueue, NO); + [self proceedOnQueue]; - [CountlyCommon.sharedInstance startBackgroundTask]; + return; + } - queryString = [self appendRemainingRequest:queryString]; - NSMutableData *pictureUploadData = [self pictureUploadDataForQueryString:queryString]; - if (!pictureUploadData) - { - queryString = [self appendChecksum:queryString]; - } + NSString* temporaryDeviceIDQueryString = [NSString stringWithFormat:@"&%@=%@", kCountlyQSKeyDeviceID, CLYTemporaryDeviceID]; + if ([firstItemInQueue containsString:temporaryDeviceIDQueryString]) + { + CLY_LOG_D(@"Proceeding on queue is aborted: Device ID in request is CLYTemporaryDeviceID!"); + atomic_store(&_isProcessingQueue, NO); + return; + } - NSString *serverInputEndpoint = [self.host stringByAppendingString:endPoint]; - NSMutableURLRequest *request; + _URLSessionConfiguration.timeoutIntervalForRequest = [CountlyServerConfig.sharedInstance requestTimeoutDuration]; + _URLSessionConfiguration.timeoutIntervalForResource = [CountlyServerConfig.sharedInstance requestTimeoutDuration]; + NSString* queryString = firstItemInQueue; + NSString* endPoint = kCountlyEndpointI; + + NSString* overrideEndPoint = [self extractAndRemoveParameter:&queryString parameter: kCountlyNewEndPoint]; + if(overrideEndPoint) { + endPoint = overrideEndPoint; + } + + NSString* callbackID = [self extractAndRemoveParameter:&queryString parameter: kCountlyCallbackID]; + __block CLYRequestCallback requestCallback = nil; + if(callbackID){ + dispatch_sync(_callbackQueue, ^{ + requestCallback = self.internalRequestCallbacks[callbackID]; + }); + } + + + [CountlyCommon.sharedInstance startBackgroundTask]; - if (pictureUploadData) - { - request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:serverInputEndpoint]]; - NSString *contentType = [@"multipart/form-data; boundary=" stringByAppendingString:kCountlyUploadBoundary]; - [request addValue:contentType forHTTPHeaderField:@"Content-Type"]; + queryString = [self appendRemainingRequest:queryString]; + NSMutableData* pictureUploadData = [self pictureUploadDataForQueryString:queryString]; - NSArray *query = [queryString componentsSeparatedByString:@"&"]; - NSEnumerator *e = [query objectEnumerator]; - NSString *kvString; - while (kvString = [e nextObject]) + if (!pictureUploadData) { - NSArray *kv = [kvString componentsSeparatedByString:@"="]; - [self addMultipart:pictureUploadData andKey:[kv[0] stringByRemovingPercentEncoding] andValue:[kv[1] stringByRemovingPercentEncoding]]; + queryString = [self appendChecksum:queryString]; } - if (self.secretSalt) + NSString* serverInputEndpoint = [self.host stringByAppendingString:endPoint]; + NSMutableURLRequest* request; + + if (pictureUploadData) + { + request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:serverInputEndpoint]]; + NSString *contentType = [@"multipart/form-data; boundary=" stringByAppendingString:kCountlyUploadBoundary]; + [request addValue:contentType forHTTPHeaderField: @"Content-Type"]; + + NSArray *query = [queryString componentsSeparatedByString:@"&"]; + NSEnumerator *e = [query objectEnumerator]; + NSString* kvString; + while (kvString = [e nextObject]) { + NSArray *kv = [kvString componentsSeparatedByString:@"="]; + [self addMultipart:pictureUploadData andKey:[kv[0] stringByRemovingPercentEncoding] andValue:[kv[1] stringByRemovingPercentEncoding]]; + } + + if (self.secretSalt) + { + NSString* checksum = [[[queryString stringByRemovingPercentEncoding] stringByAppendingString:self.secretSalt] cly_SHA256]; + [self addMultipart:pictureUploadData andKey:kCountlyQSKeyChecksum256 andValue:checksum]; + } + + NSString* boundaryEnd = [NSString stringWithFormat:@"\r\n--%@--\r\n", kCountlyUploadBoundary]; + [pictureUploadData appendData:[boundaryEnd cly_dataUTF8]]; + request.HTTPMethod = @"POST"; + request.HTTPBody = pictureUploadData; + } + else if (queryString.length > kCountlyGETRequestMaxLength || self.alwaysUsePOST) { - NSString *checksum = [[[queryString stringByRemovingPercentEncoding] stringByAppendingString:self.secretSalt] cly_SHA256]; - [self addMultipart:pictureUploadData andKey:kCountlyQSKeyChecksum256 andValue:checksum]; + request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:serverInputEndpoint]]; + request.HTTPMethod = @"POST"; + request.HTTPBody = [queryString cly_dataUTF8]; + } + else + { + NSString* fullRequestURL = [serverInputEndpoint stringByAppendingFormat:@"?%@", queryString]; + request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:fullRequestURL]]; } - NSString *boundaryEnd = [NSString stringWithFormat:@"\r\n--%@--\r\n", kCountlyUploadBoundary]; - [pictureUploadData appendData:[boundaryEnd cly_dataUTF8]]; - request.HTTPMethod = @"POST"; - request.HTTPBody = pictureUploadData; - } - else if (queryString.length > kCountlyGETRequestMaxLength || self.alwaysUsePOST) - { - request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:serverInputEndpoint]]; - request.HTTPMethod = @"POST"; - request.HTTPBody = [queryString cly_dataUTF8]; - } - else - { - NSString *fullRequestURL = [serverInputEndpoint stringByAppendingFormat:@"?%@", queryString]; - request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:fullRequestURL]]; - } - - request.cachePolicy = NSURLRequestReloadIgnoringLocalCacheData; - NSDate *startTimeRequest = [NSDate date]; - self.connection = [self.URLSession dataTaskWithRequest:request - completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { - self.connection = nil; - atomic_store(&self->_isProcessingQueue, NO); - NSDate *endTimeRequest = [NSDate date]; - long duration = (long)[endTimeRequest timeIntervalSinceDate:startTimeRequest]; - - CLY_LOG_V(@"Approximate received data size for request <%p> is %ld bytes.", (id)request, (long)data.length); - - if (response) - { - NSInteger code = ((NSHTTPURLResponse *)response).statusCode; - CLY_LOG_V(@"%s, Response received from server with status code:[ %ld ] request:[ %@ ]", __FUNCTION__, (long)code, ((NSHTTPURLResponse *)response).URL); - } - - if (!error) - { - if ([self isRequestSuccessful:response data:data]) - { - CLY_LOG_D(@"Request <%p> successfully completed.", request); - - if (requestCallback) - { - requestCallback([response description], YES); - // Clean up callback after execution - if (callbackID) - { - dispatch_sync(self->_callbackQueue, ^{ - [self.internalRequestCallbacks removeObjectForKey:callbackID]; - }); - } - } - - [CountlyPersistency.sharedInstance removeFromQueue:firstItemInQueue]; - - [CountlyPersistency.sharedInstance saveToFile]; - - if (CountlyServerConfig.sharedInstance.backoffMechanism && [self backoff:duration queryString:queryString]) - { - CLY_LOG_D(@"%s, backed off dropping proceeding the queue", __FUNCTION__); - self.startTime = nil; - self.hasAnyRequestFailed = NO; // Reset on backoff - [self backoffCountdown]; - } - else - { - [self proceedOnQueue]; - } - } - else - { - CLY_LOG_D(@"%s, request:[ <%p> ] failed! response:[ %@ ]", __FUNCTION__, request, [data cly_stringUTF8]); - - self.hasAnyRequestFailed = YES; // Mark that a request has failed - - if (requestCallback) - { - requestCallback([data cly_stringUTF8], NO); - // Clean up callback after execution - if (callbackID) - { - dispatch_sync(self->_callbackQueue, ^{ - [self.internalRequestCallbacks removeObjectForKey:callbackID]; - }); - } - } - - [CountlyHealthTracker.sharedInstance logFailedNetworkRequestWithStatusCode:((NSHTTPURLResponse *)response).statusCode errorResponse:[data cly_stringUTF8]]; - [CountlyHealthTracker.sharedInstance saveState]; - self.startTime = nil; - } - } - else - { - CLY_LOG_D(@"%s, request:[ <%p> ] failed! error:[ %@ ]", __FUNCTION__, request, error); - - self.hasAnyRequestFailed = YES; // Mark that a request has failed - - if (requestCallback) - { - requestCallback([error description], NO); - // Clean up callback after execution - if (callbackID) - { - dispatch_sync(self->_callbackQueue, ^{ - [self.internalRequestCallbacks removeObjectForKey:callbackID]; - }); - } - } + request.cachePolicy = NSURLRequestReloadIgnoringLocalCacheData; + NSDate *startTimeRequest = [NSDate date]; + self.connection = [self.URLSession dataTaskWithRequest:request completionHandler:^(NSData * data, NSURLResponse * response, NSError * error) + { + self.connection = nil; + atomic_store(&self->_isProcessingQueue, NO); + NSDate *endTimeRequest = [NSDate date]; + long duration = (long)[endTimeRequest timeIntervalSinceDate:startTimeRequest]; + + CLY_LOG_V(@"Approximate received data size for request <%p> is %ld bytes.", (id)request, (long)data.length); + + if(response) { + NSInteger code = ((NSHTTPURLResponse*)response).statusCode; + CLY_LOG_V(@"%s, Response received from server with status code:[ %ld ] request:[ %@ ]", __FUNCTION__, (long)code, ((NSHTTPURLResponse*)response).URL); + } + + + if (!error) + { + if ([self isRequestSuccessful:response data:data]) + { + CLY_LOG_D(@"Request <%p> successfully completed.", request); + + if(requestCallback){ + requestCallback([response description], YES); + // Clean up callback after execution + if (callbackID) { + dispatch_sync(self->_callbackQueue, ^{ + [self.internalRequestCallbacks removeObjectForKey:callbackID]; + }); + } + } + + [CountlyPersistency.sharedInstance removeFromQueue:firstItemInQueue]; + + [CountlyPersistency.sharedInstance saveToFile]; + + if(CountlyServerConfig.sharedInstance.backoffMechanism && [self backoff:duration queryString:queryString]){ + CLY_LOG_D(@"%s, backed off dropping proceeding the queue", __FUNCTION__); + self.startTime = nil; + self.hasAnyRequestFailed = NO; // Reset on backoff + [self backoffCountdown]; + } else { + [self proceedOnQueue]; + + } + } + else + { + CLY_LOG_D(@"%s, request:[ <%p> ] failed! response:[ %@ ]", __FUNCTION__, request, [data cly_stringUTF8]); + + self.hasAnyRequestFailed = YES; // Mark that a request has failed + + if(requestCallback){ + requestCallback([data cly_stringUTF8], NO); + // Clean up callback after execution + if (callbackID) { + dispatch_sync(self->_callbackQueue, ^{ + [self.internalRequestCallbacks removeObjectForKey:callbackID]; + }); + } + } + + [CountlyHealthTracker.sharedInstance logFailedNetworkRequestWithStatusCode:((NSHTTPURLResponse*)response).statusCode errorResponse: [data cly_stringUTF8]]; + [CountlyHealthTracker.sharedInstance saveState]; + self.startTime = nil; + } + } + else + { + CLY_LOG_D(@"%s, request:[ <%p> ] failed! error:[ %@ ]", __FUNCTION__, request, error); + + self.hasAnyRequestFailed = YES; // Mark that a request has failed + + if(requestCallback){ + requestCallback([error description], NO); + // Clean up callback after execution + if (callbackID) { + dispatch_sync(self->_callbackQueue, ^{ + [self.internalRequestCallbacks removeObjectForKey:callbackID]; + }); + } + } #if (TARGET_OS_WATCH) - [CountlyPersistency.sharedInstance saveToFile]; + [CountlyPersistency.sharedInstance saveToFile]; #endif - self.startTime = nil; - } - }]; + self.startTime = nil; + } + }]; - [self.connection resume]; + [self.connection resume]; - [self logRequest:request]; + [self logRequest:request]; } - (void)recordMetrics:(nullable NSDictionary *)metricsOverride { - CLY_LOG_I(@"%s", __FUNCTION__, metricsOverride); - if (!CountlyConsentManager.sharedInstance.consentForMetrics) - return; - - NSDictionary *defaultMetrics = [CountlyDeviceInfo metricsDictionary]; - if (!defaultMetrics) - { - CLY_LOG_W(@"%s Default metrics is nil, aborting", __FUNCTION__); + CLY_LOG_I(@"%s", __FUNCTION__, metricsOverride); + if (!CountlyConsentManager.sharedInstance.consentForMetrics) return; - } - CLY_LOG_I(@"%s default metrics:[%@]", __FUNCTION__, defaultMetrics); - - NSDictionary *finalMetrics; - if (metricsOverride && metricsOverride.count > 0) - { - NSMutableDictionary *mutableMetrics = [defaultMetrics mutableCopy]; - [mutableMetrics addEntriesFromDictionary:metricsOverride]; - finalMetrics = [mutableMetrics copy]; - } - else - { - finalMetrics = defaultMetrics; - } - CLY_LOG_I(@"%s final metrics:[%@]", __FUNCTION__, finalMetrics); - - NSString *queryString = [[self queryEssentials] stringByAppendingFormat:@"&%@=%@", kCountlyQSKeyMetrics, [finalMetrics cly_JSONify]]; - - [CountlyPersistency.sharedInstance addToQueue:queryString]; - [self proceedOnQueue]; + + NSDictionary *defaultMetrics = [CountlyDeviceInfo metricsDictionary]; + if (!defaultMetrics) { + CLY_LOG_W(@"%s Default metrics is nil, aborting", __FUNCTION__); + return; + } + CLY_LOG_I(@"%s default metrics:[%@]", __FUNCTION__, defaultMetrics); + + NSDictionary *finalMetrics; + if (metricsOverride && metricsOverride.count > 0) { + NSMutableDictionary *mutableMetrics = [defaultMetrics mutableCopy]; + [mutableMetrics addEntriesFromDictionary:metricsOverride]; + finalMetrics = [mutableMetrics copy]; + } else { + finalMetrics = defaultMetrics; + } + CLY_LOG_I(@"%s final metrics:[%@]", __FUNCTION__, finalMetrics); + + NSString* queryString = [[self queryEssentials] stringByAppendingFormat:@"&%@=%@", + kCountlyQSKeyMetrics, [finalMetrics cly_JSONify]]; + + [CountlyPersistency.sharedInstance addToQueue:queryString]; + [self proceedOnQueue]; } - (BOOL)backoff:(long)responseTimeSeconds queryString:(NSString *)queryString { - BOOL result = NO; - // Check if the current response time is within acceptable limits - if (responseTimeSeconds >= [CountlyServerConfig.sharedInstance bomAcceptedTimeoutSeconds]) - { - // Check if the remaining request count is within acceptable limits - NSUInteger remainingRequests = [CountlyPersistency.sharedInstance remainingRequestCount]; - NSUInteger threshold = (NSUInteger)(CountlyPersistency.sharedInstance.storedRequestsLimit * [CountlyServerConfig.sharedInstance bomRQPercentage]); - - if (remainingRequests <= threshold) - { - // Calculate the age of the current request - double requestTimestamp = [[queryString cly_valueForQueryStringKey:kCountlyQSKeyTimestamp] longLongValue] / 1000.0; - double requestAgeInSeconds = [NSDate date].timeIntervalSince1970 - requestTimestamp; - - if (requestAgeInSeconds <= [CountlyServerConfig.sharedInstance bomRequestAge] * 3600.0) - { - // Server is too busy, back off - result = YES; - [CountlyHealthTracker.sharedInstance logBackoffRequest]; - } + BOOL result = NO; + // Check if the current response time is within acceptable limits + if (responseTimeSeconds >= [CountlyServerConfig.sharedInstance bomAcceptedTimeoutSeconds]) { + // Check if the remaining request count is within acceptable limits + NSUInteger remainingRequests = [CountlyPersistency.sharedInstance remainingRequestCount]; + NSUInteger threshold = (NSUInteger)(CountlyPersistency.sharedInstance.storedRequestsLimit * [CountlyServerConfig.sharedInstance bomRQPercentage]); + + if (remainingRequests <= threshold) { + // Calculate the age of the current request + double requestTimestamp = [[queryString cly_valueForQueryStringKey:kCountlyQSKeyTimestamp] longLongValue] / 1000.0; + double requestAgeInSeconds = [NSDate date].timeIntervalSince1970 - requestTimestamp; + + if (requestAgeInSeconds <= [CountlyServerConfig.sharedInstance bomRequestAge] * 3600.0) { + // Server is too busy, back off + result = YES; + [CountlyHealthTracker.sharedInstance logBackoffRequest]; + } + } } - } - - if (!result) - { - [CountlyHealthTracker.sharedInstance logConsecutiveBackoffRequest]; - } - - return result; + + if (!result) { + [CountlyHealthTracker.sharedInstance logConsecutiveBackoffRequest]; + } + + return result; } - (void)backoffCountdown { - __weak typeof(self) weakSelf = self; - CLY_LOG_D(@"%s, backed off, countdown start for %f seconds", __FUNCTION__, [CountlyServerConfig.sharedInstance bomDuration]); - - atomic_store(&_backoff, YES); - dispatch_time_t delay = dispatch_time(DISPATCH_TIME_NOW, (int64_t)([CountlyServerConfig.sharedInstance bomDuration] * NSEC_PER_SEC)); - dispatch_after(delay, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - __strong typeof(weakSelf) strongSelf = weakSelf; - if (!strongSelf) - return; - - CLY_LOG_D(@"%s, countdown finished, running tick in background thread", __FUNCTION__); - atomic_store(&strongSelf->_backoff, NO); - [strongSelf proceedOnQueue]; - }); + __weak typeof(self) weakSelf = self; + CLY_LOG_D(@"%s, backed off, countdown start for %f seconds", __FUNCTION__, [CountlyServerConfig.sharedInstance bomDuration]); + + atomic_store(&_backoff, YES); + dispatch_time_t delay = dispatch_time(DISPATCH_TIME_NOW, (int64_t)([CountlyServerConfig.sharedInstance bomDuration] * NSEC_PER_SEC)); + dispatch_after(delay, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + __strong typeof(weakSelf) strongSelf = weakSelf; + if (!strongSelf) return; + + + CLY_LOG_D(@"%s, countdown finished, running tick in background thread", __FUNCTION__); + atomic_store(&strongSelf->_backoff, NO); + [strongSelf proceedOnQueue]; + }); } -- (NSString *)extractAndRemoveParameter:(NSString **)queryString parameter:(NSString *)parameter +- (NSString*)extractAndRemoveParameter:(NSString **)queryString parameter:(NSString*)parameter { - CLY_LOG_D(@"%s, Extracting parameter: %@", __FUNCTION__, parameter); - - if ([*queryString containsString:parameter]) - { - NSString *parameterExtracted = [*queryString cly_valueForQueryStringKey:parameter]; - if (parameterExtracted) - { - NSString *stringToRemove = [NSString stringWithFormat:@"&%@=%@", parameter, parameterExtracted]; - *queryString = [*queryString stringByReplacingOccurrencesOfString:stringToRemove withString:@""]; - CLY_LOG_D(@"%s, Parameter extracted successfully: %@ = %@", __FUNCTION__, parameter, parameterExtracted); - return parameterExtracted; - } - CLY_LOG_D(@"%s, Parameter found but value extraction failed for: %@", __FUNCTION__, parameter); - } - else - { - CLY_LOG_D(@"%s, Parameter not found in query string: %@", __FUNCTION__, parameter); - } - return nil; + CLY_LOG_D(@"%s, Extracting parameter: %@", __FUNCTION__, parameter); + + if([*queryString containsString:parameter]) { + NSString* parameterExtracted = [*queryString cly_valueForQueryStringKey:parameter]; + if(parameterExtracted) { + NSString* stringToRemove = [NSString stringWithFormat:@"&%@=%@",parameter,parameterExtracted]; + *queryString = [*queryString stringByReplacingOccurrencesOfString:stringToRemove withString:@""]; + CLY_LOG_D(@"%s, Parameter extracted successfully: %@ = %@", __FUNCTION__, parameter, parameterExtracted); + return parameterExtracted; + } + CLY_LOG_D(@"%s, Parameter found but value extraction failed for: %@", __FUNCTION__, parameter); + } else { + CLY_LOG_D(@"%s, Parameter not found in query string: %@", __FUNCTION__, parameter); + } + return nil; } - (void)logRequest:(NSURLRequest *)request { - NSString *bodyAsString = @""; - NSInteger sentSize = request.URL.absoluteString.length; + NSString* bodyAsString = @""; + NSInteger sentSize = request.URL.absoluteString.length; - if (request.HTTPBody) - { - bodyAsString = [request.HTTPBody cly_stringUTF8]; - if (!bodyAsString) - bodyAsString = @"Picture uploading..."; + if (request.HTTPBody) + { + bodyAsString = [request.HTTPBody cly_stringUTF8]; + if (!bodyAsString) + bodyAsString = @"Picture uploading..."; - sentSize += request.HTTPBody.length; - } + sentSize += request.HTTPBody.length; + } - CLY_LOG_D(@"%s, request:[ <%p> ] started. [%@] %@ %@", __FUNCTION__, (id)request, request.HTTPMethod, request.URL.absoluteString, bodyAsString); - CLY_LOG_V(@"Approximate sent data size for request <%p> is %ld bytes.", (id)request, (long)sentSize); + CLY_LOG_D(@"%s, request:[ <%p> ] started. [%@] %@ %@", __FUNCTION__, (id)request, request.HTTPMethod, request.URL.absoluteString, bodyAsString); + CLY_LOG_V(@"Approximate sent data size for request <%p> is %ld bytes.", (id)request, (long)sentSize); } -#pragma mark--- +#pragma mark --- - (void)beginSession { - if (!CountlyConsentManager.sharedInstance.consentForSessions) - return; - - if (!CountlyServerConfig.sharedInstance.sessionTrackingEnabled) - return; - - if (isSessionStarted) - { - CLY_LOG_W(@"%s A session is already running, this 'beginSession' will be ignored", __FUNCTION__); - return; - } - + if (!CountlyConsentManager.sharedInstance.consentForSessions) + return; + + if(!CountlyServerConfig.sharedInstance.sessionTrackingEnabled) + return; + + if (isSessionStarted) { + CLY_LOG_W(@"%s A session is already running, this 'beginSession' will be ignored", __FUNCTION__); + return; + } + #if TARGET_OS_IOS || TARGET_OS_TV - if (!CountlyCommon.sharedInstance.manualSessionHandling && [UIApplication sharedApplication].applicationState == UIApplicationStateBackground) - { - CLY_LOG_W(@"%s App is in the background, 'beginSession' will be ignored", __FUNCTION__); - return; - } + if (!CountlyCommon.sharedInstance.manualSessionHandling && [UIApplication sharedApplication].applicationState == UIApplicationStateBackground) { + CLY_LOG_W(@"%s App is in the background, 'beginSession' will be ignored", __FUNCTION__); + return; + } #elif TARGET_OS_OSX - if (!CountlyCommon.sharedInstance.manualSessionHandling && ![NSApplication sharedApplication].isActive) - { - CLY_LOG_W(@"%s App is not active, 'beginSession' will be ignored", __FUNCTION__); - return; - } + if (!CountlyCommon.sharedInstance.manualSessionHandling && ![NSApplication sharedApplication].isActive) { + CLY_LOG_W(@"%s App is not active, 'beginSession' will be ignored", __FUNCTION__); + return; + } #elif TARGET_OS_WATCH - if (!CountlyCommon.sharedInstance.manualSessionHandling && [WKExtension sharedExtension].applicationState == WKApplicationStateBackground) - { - CLY_LOG_W(@"%s App is in the background, 'beginSession' will be ignored", __FUNCTION__); - return; - } + if (!CountlyCommon.sharedInstance.manualSessionHandling && [WKExtension sharedExtension].applicationState == WKApplicationStateBackground) { + CLY_LOG_W(@"%s App is in the background, 'beginSession' will be ignored", __FUNCTION__); + return; + } #endif - if ([Countly.user hasUnsyncedChanges]) - { - [Countly.user save]; - } - - isSessionStarted = YES; - lastSessionStartTime = NSDate.date.timeIntervalSince1970; - unsentSessionLength = 0.0; - - NSString *queryString = [[self queryEssentials] stringByAppendingFormat:@"&%@=%@&%@=%@", kCountlyQSKeySessionBegin, @"1", kCountlyQSKeyMetrics, [CountlyDeviceInfo metrics]]; + if([Countly.user hasUnsyncedChanges]) + { + [Countly.user save]; + } - if (CountlyServerConfig.sharedInstance.locationTrackingEnabled) - { - NSString *locationRelatedInfoQueryString = [self locationRelatedInfoQueryString]; - if (locationRelatedInfoQueryString) - queryString = [queryString stringByAppendingString:locationRelatedInfoQueryString]; - } + isSessionStarted = YES; + lastSessionStartTime = NSDate.date.timeIntervalSince1970; + unsentSessionLength = 0.0; - NSString *attributionQueryString = [self attributionQueryString]; - if (attributionQueryString) - queryString = [queryString stringByAppendingString:attributionQueryString]; + NSString* queryString = [[self queryEssentials] stringByAppendingFormat:@"&%@=%@&%@=%@", + kCountlyQSKeySessionBegin, @"1", + kCountlyQSKeyMetrics, [CountlyDeviceInfo metrics]]; - [CountlyPersistency.sharedInstance addToQueue:queryString]; + if(CountlyServerConfig.sharedInstance.locationTrackingEnabled) { + NSString* locationRelatedInfoQueryString = [self locationRelatedInfoQueryString]; + if (locationRelatedInfoQueryString) + queryString = [queryString stringByAppendingString:locationRelatedInfoQueryString]; + } - [CountlyCommon.sharedInstance recordOrientation]; + NSString* attributionQueryString = [self attributionQueryString]; + if (attributionQueryString) + queryString = [queryString stringByAppendingString:attributionQueryString]; - [self proceedOnQueue]; + [CountlyPersistency.sharedInstance addToQueue:queryString]; + + [CountlyCommon.sharedInstance recordOrientation]; + + [self proceedOnQueue]; } - (void)updateSession { - if (!CountlyConsentManager.sharedInstance.consentForSessions) - return; - - if (!CountlyServerConfig.sharedInstance.sessionTrackingEnabled) - return; - - if (!isSessionStarted) - { - CLY_LOG_W(@"%s No session is running, this 'updateSession' will be ignored", __FUNCTION__); - return; - } - - if ([Countly.user hasUnsyncedChanges]) - { - [Countly.user save]; - } + if (!CountlyConsentManager.sharedInstance.consentForSessions) + return; + + if(!CountlyServerConfig.sharedInstance.sessionTrackingEnabled) + return; + + if (!isSessionStarted) { + CLY_LOG_W(@"%s No session is running, this 'updateSession' will be ignored", __FUNCTION__); + return; + } + + if([Countly.user hasUnsyncedChanges]) + { + [Countly.user save]; + } - NSString *queryString = [[self queryEssentials] stringByAppendingFormat:@"&%@=%d", kCountlyQSKeySessionDuration, (int)[self sessionLengthInSeconds]]; + NSString* queryString = [[self queryEssentials] stringByAppendingFormat:@"&%@=%d", + kCountlyQSKeySessionDuration, (int)[self sessionLengthInSeconds]]; - [CountlyPersistency.sharedInstance addToQueue:queryString]; + [CountlyPersistency.sharedInstance addToQueue:queryString]; - [self proceedOnQueue]; + [self proceedOnQueue]; } - (void)endSession { - if (!CountlyConsentManager.sharedInstance.consentForSessions) - return; - - if (!CountlyServerConfig.sharedInstance.sessionTrackingEnabled) - return; - - if (!isSessionStarted) - { - CLY_LOG_W(@"%s No session is running, this 'endSession' will be ignored", __FUNCTION__); - return; - } - - isSessionStarted = NO; - NSString *queryString = [[self queryEssentials] stringByAppendingFormat:@"&%@=%@&%@=%d", kCountlyQSKeySessionEnd, @"1", kCountlyQSKeySessionDuration, (int)[self sessionLengthInSeconds]]; + if (!CountlyConsentManager.sharedInstance.consentForSessions) + return; + + if(!CountlyServerConfig.sharedInstance.sessionTrackingEnabled) + return; + + if (!isSessionStarted) { + CLY_LOG_W(@"%s No session is running, this 'endSession' will be ignored", __FUNCTION__); + return; + } - [CountlyPersistency.sharedInstance addToQueue:queryString]; + isSessionStarted = NO; + NSString* queryString = [[self queryEssentials] stringByAppendingFormat:@"&%@=%@&%@=%d", + kCountlyQSKeySessionEnd, @"1", + kCountlyQSKeySessionDuration, (int)[self sessionLengthInSeconds]]; - [self proceedOnQueue]; + [CountlyPersistency.sharedInstance addToQueue:queryString]; - [CountlyViewTrackingInternal.sharedInstance resetFirstView]; + [self proceedOnQueue]; + + [CountlyViewTrackingInternal.sharedInstance resetFirstView]; } -#pragma mark--- +#pragma mark --- - (void)sendEventsWithSaveIfNeeded { - if ([Countly.user hasUnsyncedChanges]) - { - [Countly.user save]; - } - else - { - [self sendEventsInternal]; - } + if([Countly.user hasUnsyncedChanges]) + { + [Countly.user save]; + } + else + { + [self sendEventsInternal]; + } } - (void)sendEvents { - [self sendEventsInternal]; + [self sendEventsInternal]; } - (void)attemptToSendStoredRequests { - [self addEventsToQueue]; - [CountlyPersistency.sharedInstance saveToFileSync]; - [self proceedOnQueue]; + [self addEventsToQueue]; + [CountlyPersistency.sharedInstance saveToFileSync]; + [self proceedOnQueue]; } - (void)sendEventsInternal { - [self addEventsToQueue]; - [self proceedOnQueue]; + [self addEventsToQueue]; + [self proceedOnQueue]; } - (void)addEventsToQueue { - [self addEventsToQueue:nil]; + [self addEventsToQueue:nil]; } - (void)addEventsToQueue:(CLYRequestCallback)callback { - NSString *events = [CountlyPersistency.sharedInstance serializedRecordedEvents]; + NSString* events = [CountlyPersistency.sharedInstance serializedRecordedEvents]; - if (!events) - return; + if (!events) + return; - NSString *queryString = [[self queryEssentials] stringByAppendingFormat:@"&%@=%@", kCountlyQSKeyEvents, events]; - [self addToQueueWithCallback:queryString callback:callback]; + NSString* queryString = [[self queryEssentials] stringByAppendingFormat:@"&%@=%@", kCountlyQSKeyEvents, events]; + [self addToQueueWithCallback:queryString callback:callback]; } - (void)sendEventsWithCallback:(CLYRequestCallback)callback { - [self addEventsToQueue:callback]; - [self proceedOnQueue]; + [self addEventsToQueue:callback]; + [self proceedOnQueue]; } -#pragma mark--- +#pragma mark --- - (void)sendPushToken:(NSString *)token { #ifndef COUNTLY_EXCLUDE_PUSHNOTIFICATIONS - NSInteger testMode = 0; // NOTE: default is 0: Production - not test mode + NSInteger testMode = 0; //NOTE: default is 0: Production - not test mode - if ([CountlyPushNotifications.sharedInstance.pushTestMode isEqualToString:CLYPushTestModeDevelopment]) - testMode = 1; // NOTE: 1: Developement/Debug builds - standard test mode using Sandbox APNs - else if ([CountlyPushNotifications.sharedInstance.pushTestMode isEqualToString:CLYPushTestModeTestFlightOrAdHoc]) - testMode = 2; // NOTE: 2: TestFlight/AdHoc builds - special test mode using Production APNs + if ([CountlyPushNotifications.sharedInstance.pushTestMode isEqualToString:CLYPushTestModeDevelopment]) + testMode = 1; //NOTE: 1: Developement/Debug builds - standard test mode using Sandbox APNs + else if ([CountlyPushNotifications.sharedInstance.pushTestMode isEqualToString:CLYPushTestModeTestFlightOrAdHoc]) + testMode = 2; //NOTE: 2: TestFlight/AdHoc builds - special test mode using Production APNs - NSString *queryString = [[self queryEssentials] stringByAppendingFormat:@"&%@=%@&%@=%@&%@=%ld", kCountlyQSKeyPushTokenSession, @"1", kCountlyQSKeyPushTokeniOS, token, kCountlyQSKeyPushTestMode, (long)testMode]; + NSString* queryString = [[self queryEssentials] stringByAppendingFormat:@"&%@=%@&%@=%@&%@=%ld", + kCountlyQSKeyPushTokenSession, @"1", + kCountlyQSKeyPushTokeniOS, token, + kCountlyQSKeyPushTestMode, (long)testMode]; - [CountlyPersistency.sharedInstance addToQueue:queryString]; + [CountlyPersistency.sharedInstance addToQueue:queryString]; - [self proceedOnQueue]; + [self proceedOnQueue]; #endif } - (void)sendLocationInfo { - NSString *locationRelatedInfoQueryString = [self locationRelatedInfoQueryString]; + NSString* locationRelatedInfoQueryString = [self locationRelatedInfoQueryString]; - if (!locationRelatedInfoQueryString) - return; + if (!locationRelatedInfoQueryString) + return; - NSString *queryString = [[self queryEssentials] stringByAppendingString:locationRelatedInfoQueryString]; + NSString* queryString = [[self queryEssentials] stringByAppendingString:locationRelatedInfoQueryString]; - [CountlyPersistency.sharedInstance addToQueue:queryString]; + [CountlyPersistency.sharedInstance addToQueue:queryString]; - [self proceedOnQueue]; + [self proceedOnQueue]; } - (void)sendUserDetails:(NSString *)userDetails { - NSString *queryString = [[self queryEssentials] stringByAppendingFormat:@"&%@=%@", kCountlyQSKeyUserDetails, userDetails]; + NSString* queryString = [[self queryEssentials] stringByAppendingFormat:@"&%@=%@", + kCountlyQSKeyUserDetails, userDetails]; - [CountlyPersistency.sharedInstance addToQueue:queryString]; + [CountlyPersistency.sharedInstance addToQueue:queryString]; - [self proceedOnQueue]; + [self proceedOnQueue]; } - (void)sendCrashReport:(NSString *)report immediately:(BOOL)immediately; { - if (!CountlyServerConfig.sharedInstance.networkingEnabled) - { - CLY_LOG_D(@"'sendCrashReport' is aborted: SDK Networking is disabled from server config!"); - return; - } + if (!CountlyServerConfig.sharedInstance.networkingEnabled) + { + CLY_LOG_D(@"'sendCrashReport' is aborted: SDK Networking is disabled from server config!"); + return; + } + + if (!report) + { + CLY_LOG_W(@"Crash report is nil. Converting to JSON may have failed due to custom objects in initial config's crashSegmentation property."); + return; + } - if (!report) - { - CLY_LOG_W(@"Crash report is nil. Converting to JSON may have failed due to custom objects in initial config's crashSegmentation property."); - return; - } + NSString* queryString = [[self queryEssentials] stringByAppendingFormat:@"&%@=%@", + kCountlyQSKeyCrash, report]; - NSString *queryString = [[self queryEssentials] stringByAppendingFormat:@"&%@=%@", kCountlyQSKeyCrash, report]; + if (!immediately) + { + [CountlyPersistency.sharedInstance addToQueue:queryString]; + [self proceedOnQueue]; + return; + } - if (!immediately) - { - [CountlyPersistency.sharedInstance addToQueue:queryString]; - [self proceedOnQueue]; - return; - } + //NOTE: Prevent `event` and `end_session` requests from being started, after `sendEvents` and `endSession` calls below. + isCrashing = YES; - // NOTE: Prevent `event` and `end_session` requests from being started, after `sendEvents` and `endSession` calls below. - isCrashing = YES; + [self sendEventsWithSaveIfNeeded]; - [self sendEventsWithSaveIfNeeded]; + if (!CountlyCommon.sharedInstance.manualSessionHandling) + [self endSession]; - if (!CountlyCommon.sharedInstance.manualSessionHandling) - [self endSession]; + if (CountlyDeviceInfo.sharedInstance.isDeviceIDTemporary) + { + CLY_LOG_D(@"Device ID is set as CLYTemporaryDeviceID! Crash report stored to be sent later!"); - if (CountlyDeviceInfo.sharedInstance.isDeviceIDTemporary) - { - CLY_LOG_D(@"Device ID is set as CLYTemporaryDeviceID! Crash report stored to be sent later!"); + [CountlyPersistency.sharedInstance addToQueue:queryString]; + [CountlyPersistency.sharedInstance saveToFileSync]; + return; + } - [CountlyPersistency.sharedInstance addToQueue:queryString]; [CountlyPersistency.sharedInstance saveToFileSync]; - return; - } - [CountlyPersistency.sharedInstance saveToFileSync]; - - queryString = [queryString stringByAppendingFormat:@"&%@=%@", kCountlyAppVersionKey, CountlyDeviceInfo.appVersion]; + queryString = [queryString stringByAppendingFormat:@"&%@=%@", + kCountlyAppVersionKey, CountlyDeviceInfo.appVersion]; + + NSString* serverInputEndpoint = [self.host stringByAppendingString:kCountlyEndpointI]; + NSMutableURLRequest* request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:serverInputEndpoint]]; + request.HTTPMethod = @"POST"; + request.HTTPBody = [[self appendChecksum:queryString] cly_dataUTF8]; - NSString *serverInputEndpoint = [self.host stringByAppendingString:kCountlyEndpointI]; - NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:serverInputEndpoint]]; - request.HTTPMethod = @"POST"; - request.HTTPBody = [[self appendChecksum:queryString] cly_dataUTF8]; + dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); - dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); + [[self.URLSession dataTaskWithRequest:request completionHandler:^(NSData* data, NSURLResponse* response, NSError* error) + { + if (error || ![self isRequestSuccessful:response data:data]) + { + CLY_LOG_D(@"%s, request: [ %p ] failed! %@: %@", __FUNCTION__, request, error ? @"Error" : @"Server reply", error ?: [data cly_stringUTF8]); + [CountlyPersistency.sharedInstance addToQueue:queryString]; + [CountlyPersistency.sharedInstance saveToFileSync]; + } + else + { + CLY_LOG_D(@"Request <%p> successfully completed.", request); + } - [[self.URLSession dataTaskWithRequest:request - completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { - if (error || ![self isRequestSuccessful:response data:data]) - { - CLY_LOG_D(@"%s, request: [ %p ] failed! %@: %@", __FUNCTION__, request, error ? @"Error" : @"Server reply", error ?: [data cly_stringUTF8]); - [CountlyPersistency.sharedInstance addToQueue:queryString]; - [CountlyPersistency.sharedInstance saveToFileSync]; - } - else - { - CLY_LOG_D(@"Request <%p> successfully completed.", request); - } + dispatch_semaphore_signal(semaphore); - dispatch_semaphore_signal(semaphore); - }] resume]; + }] resume]; - [self logRequest:request]; + [self logRequest:request]; - dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); + dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); } - (void)sendOldDeviceID:(NSString *)oldDeviceID { - NSString *queryString = [[self queryEssentials] stringByAppendingFormat:@"&%@=%@", kCountlyQSKeyDeviceIDOld, oldDeviceID.cly_URLEscaped]; + NSString* queryString = [[self queryEssentials] stringByAppendingFormat:@"&%@=%@", + kCountlyQSKeyDeviceIDOld, oldDeviceID.cly_URLEscaped]; - [CountlyPersistency.sharedInstance addToQueue:queryString]; + [CountlyPersistency.sharedInstance addToQueue:queryString]; - [self proceedOnQueue]; + [self proceedOnQueue]; } - (void)sendAttribution { - NSString *attributionQueryString = [self attributionQueryString]; - if (!attributionQueryString) - return; + NSString * attributionQueryString = [self attributionQueryString]; + if (!attributionQueryString) + return; - NSString *queryString = [[self queryEssentials] stringByAppendingString:attributionQueryString]; + NSString* queryString = [[self queryEssentials] stringByAppendingString:attributionQueryString]; - [CountlyPersistency.sharedInstance addToQueue:queryString]; + [CountlyPersistency.sharedInstance addToQueue:queryString]; - [self proceedOnQueue]; + [self proceedOnQueue]; } - (void)sendDirectAttributionWithCampaignID:(NSString *)campaignID andCampaignUserID:(NSString *)campaignUserID { - NSMutableString *queryString = [self queryEssentials].mutableCopy; - [queryString appendFormat:@"&%@=%@", kCountlyQSKeyCampaignID, campaignID]; + NSMutableString* queryString = [self queryEssentials].mutableCopy; + [queryString appendFormat:@"&%@=%@", kCountlyQSKeyCampaignID, campaignID]; - if (campaignUserID.length) - { - [queryString appendFormat:@"&%@=%@", kCountlyQSKeyCampaignUser, campaignUserID]; - } + if (campaignUserID.length) + { + [queryString appendFormat:@"&%@=%@", kCountlyQSKeyCampaignUser, campaignUserID]; + } - [CountlyPersistency.sharedInstance addToQueue:queryString.copy]; + [CountlyPersistency.sharedInstance addToQueue:queryString.copy]; - [self proceedOnQueue]; + [self proceedOnQueue]; } - (void)sendAttributionData:(NSString *)attributionData { - NSMutableString *queryString = [self queryEssentials].mutableCopy; - [queryString appendFormat:@"&%@=%@", kCountlyQSKeyAttributionData, [attributionData cly_URLEscaped]]; + NSMutableString* queryString = [self queryEssentials].mutableCopy; + [queryString appendFormat:@"&%@=%@", kCountlyQSKeyAttributionData, [attributionData cly_URLEscaped]]; - [CountlyPersistency.sharedInstance addToQueue:queryString.copy]; + [CountlyPersistency.sharedInstance addToQueue:queryString.copy]; - [self proceedOnQueue]; + [self proceedOnQueue]; } - (void)sendIndirectAttribution:(NSDictionary *)attribution { - NSMutableString *queryString = [self queryEssentials].mutableCopy; - [queryString appendFormat:@"&%@=%@", kCountlyQSKeyAttributionID, [attribution cly_JSONify]]; + NSMutableString* queryString = [self queryEssentials].mutableCopy; + [queryString appendFormat:@"&%@=%@", kCountlyQSKeyAttributionID, [attribution cly_JSONify]]; - [CountlyPersistency.sharedInstance addToQueue:queryString.copy]; + [CountlyPersistency.sharedInstance addToQueue:queryString.copy]; - [self proceedOnQueue]; + [self proceedOnQueue]; } - (void)sendConsents:(NSString *)consents { - NSString *queryString = [[self queryEssentials] stringByAppendingFormat:@"&%@=%@", kCountlyQSKeyConsent, consents]; + NSString* queryString = [[self queryEssentials] stringByAppendingFormat:@"&%@=%@", + kCountlyQSKeyConsent, consents]; - [CountlyPersistency.sharedInstance addToQueue:queryString]; + [CountlyPersistency.sharedInstance addToQueue:queryString]; - [self proceedOnQueue]; + [self proceedOnQueue]; } - (void)sendPerformanceMonitoringTrace:(NSString *)trace { - NSString *queryString = [[self queryEssentials] stringByAppendingFormat:@"&%@=%@", kCountlyQSKeyAPM, trace]; + NSString* queryString = [[self queryEssentials] stringByAppendingFormat:@"&%@=%@", + kCountlyQSKeyAPM, trace]; - [CountlyPersistency.sharedInstance addToQueue:queryString]; + [CountlyPersistency.sharedInstance addToQueue:queryString]; - [self proceedOnQueue]; + [self proceedOnQueue]; } -#pragma mark--- +#pragma mark --- -- (void)sendEnrollABRequestForKeys:(NSArray *)keys +- (void)sendEnrollABRequestForKeys:(NSArray*)keys { - NSString *queryString = [[self queryEssentials] stringByAppendingFormat:@"&%@=%@", kCountlyQSKeyMethod, kCountlyRCKeyABOptIn]; - - if (keys) - { - queryString = [queryString stringByAppendingFormat:@"&%@=%@", kCountlyRCKeyKeys, [keys cly_JSONify]]; - } - - queryString = [queryString stringByAppendingFormat:@"%@%@%@", kCountlyEndPointOverrideTag, kCountlyEndpointO, kCountlyEndpointSDK]; - - [CountlyPersistency.sharedInstance addToQueue:queryString]; - - [self proceedOnQueue]; + NSString* queryString = [[self queryEssentials] stringByAppendingFormat:@"&%@=%@", kCountlyQSKeyMethod, kCountlyRCKeyABOptIn]; + + if (keys) + { + queryString = [queryString stringByAppendingFormat:@"&%@=%@", kCountlyRCKeyKeys, [keys cly_JSONify]]; + } + + queryString = [queryString stringByAppendingFormat:@"%@%@%@", kCountlyEndPointOverrideTag, kCountlyEndpointO, kCountlyEndpointSDK]; + + [CountlyPersistency.sharedInstance addToQueue:queryString]; + + [self proceedOnQueue]; } -- (void)sendExitABRequestForKeys:(NSArray *)keys +- (void)sendExitABRequestForKeys:(NSArray*)keys { - NSString *queryString = [[self queryEssentials] stringByAppendingFormat:@"&%@=%@", kCountlyQSKeyMethod, kCountlyRCKeyABOptOut]; - - if (keys) - { - queryString = [queryString stringByAppendingFormat:@"&%@=%@", kCountlyRCKeyKeys, [keys cly_JSONify]]; - } - - [CountlyPersistency.sharedInstance addToQueue:queryString]; - - [self proceedOnQueue]; + NSString* queryString = [[self queryEssentials] stringByAppendingFormat:@"&%@=%@", kCountlyQSKeyMethod, kCountlyRCKeyABOptOut]; + + if (keys) + { + queryString = [queryString stringByAppendingFormat:@"&%@=%@", kCountlyRCKeyKeys, [keys cly_JSONify]]; + } + + [CountlyPersistency.sharedInstance addToQueue:queryString]; + + [self proceedOnQueue]; } -#pragma mark--- +#pragma mark --- - (void)addDirectRequest:(NSDictionary *)requestParameters { - if (!CountlyConsentManager.sharedInstance.hasAnyConsent) - return; + if (!CountlyConsentManager.sharedInstance.hasAnyConsent) + return; - NSMutableDictionary *mutableRequestParameters = requestParameters.mutableCopy; + NSMutableDictionary* mutableRequestParameters = requestParameters.mutableCopy; - for (NSString *reservedKey in self.reservedQueryStringKeys) - { - if (mutableRequestParameters[reservedKey]) + for (NSString * reservedKey in self.reservedQueryStringKeys) { - CLY_LOG_W(@"A reserved query string key detected in direct request parameters and it will be removed: %@", reservedKey); - [mutableRequestParameters removeObjectForKey:reservedKey]; + if (mutableRequestParameters[reservedKey]) + { + CLY_LOG_W(@"A reserved query string key detected in direct request parameters and it will be removed: %@", reservedKey); + [mutableRequestParameters removeObjectForKey:reservedKey]; + } } - } + + mutableRequestParameters[@"dr"] = [NSNumber numberWithInt:1]; + NSMutableString* queryString = [self queryEssentials].mutableCopy; - mutableRequestParameters[@"dr"] = [NSNumber numberWithInt:1]; - NSMutableString *queryString = [self queryEssentials].mutableCopy; - - [mutableRequestParameters enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSString *value, BOOL *stop) { - [queryString appendFormat:@"&%@=%@", key, value]; - }]; + [mutableRequestParameters enumerateKeysAndObjectsUsingBlock:^(NSString * key, NSString * value, BOOL * stop) + { + [queryString appendFormat:@"&%@=%@", key, value]; + }]; - [CountlyPersistency.sharedInstance addToQueue:queryString.copy]; + [CountlyPersistency.sharedInstance addToQueue:queryString.copy]; - [self proceedOnQueue]; + [self proceedOnQueue]; } -#pragma mark--- +#pragma mark --- - (NSString *)queryEssentials { - return [NSString stringWithFormat:@"%@=%@&%@=%@&%@=%d&%@=%lld&%@=%d&%@=%d&%@=%d&%@=%@&%@=%@", kCountlyQSKeyAppKey, self.appKey.cly_URLEscaped, kCountlyQSKeyDeviceID, CountlyDeviceInfo.sharedInstance.deviceID.cly_URLEscaped, kCountlyQSKeyDeviceIDType, - (int)CountlyDeviceInfo.sharedInstance.deviceIDTypeValue, kCountlyQSKeyTimestamp, (long long)(CountlyCommon.sharedInstance.uniqueTimestamp * 1000), kCountlyQSKeyTimeHourOfDay, (int)CountlyCommon.sharedInstance.hourOfDay, kCountlyQSKeyTimeDayOfWeek, - (int)CountlyCommon.sharedInstance.dayOfWeek, kCountlyQSKeyTimeZone, (int)CountlyCommon.sharedInstance.timeZone, kCountlyQSKeySDKVersion, CountlyCommon.sharedInstance.SDKVersion, kCountlyQSKeySDKName, CountlyCommon.sharedInstance.SDKName]; + return [NSString stringWithFormat:@"%@=%@&%@=%@&%@=%d&%@=%lld&%@=%d&%@=%d&%@=%d&%@=%@&%@=%@", + kCountlyQSKeyAppKey, self.appKey.cly_URLEscaped, + kCountlyQSKeyDeviceID, CountlyDeviceInfo.sharedInstance.deviceID.cly_URLEscaped, + kCountlyQSKeyDeviceIDType, (int)CountlyDeviceInfo.sharedInstance.deviceIDTypeValue, + kCountlyQSKeyTimestamp, (long long)(CountlyCommon.sharedInstance.uniqueTimestamp * 1000), + kCountlyQSKeyTimeHourOfDay, (int)CountlyCommon.sharedInstance.hourOfDay, + kCountlyQSKeyTimeDayOfWeek, (int)CountlyCommon.sharedInstance.dayOfWeek, + kCountlyQSKeyTimeZone, (int)CountlyCommon.sharedInstance.timeZone, + kCountlyQSKeySDKVersion, CountlyCommon.sharedInstance.SDKVersion, + kCountlyQSKeySDKName, CountlyCommon.sharedInstance.SDKName]; } + - (NSArray *)reservedQueryStringKeys { - return @[ - kCountlyQSKeyAppKey, - kCountlyQSKeyDeviceID, - kCountlyQSKeyDeviceIDType, - kCountlyQSKeyTimestamp, - kCountlyQSKeyTimeHourOfDay, - kCountlyQSKeyTimeDayOfWeek, - kCountlyQSKeyTimeZone, - kCountlyQSKeySDKVersion, - kCountlyQSKeySDKName, - kCountlyQSKeyDeviceID, - kCountlyQSKeyDeviceIDOld, - kCountlyQSKeyChecksum256, - ]; + return + @[ + kCountlyQSKeyAppKey, + kCountlyQSKeyDeviceID, + kCountlyQSKeyDeviceIDType, + kCountlyQSKeyTimestamp, + kCountlyQSKeyTimeHourOfDay, + kCountlyQSKeyTimeDayOfWeek, + kCountlyQSKeyTimeZone, + kCountlyQSKeySDKVersion, + kCountlyQSKeySDKName, + kCountlyQSKeyDeviceID, + kCountlyQSKeyDeviceIDOld, + kCountlyQSKeyChecksum256, + ]; } + - (NSString *)locationRelatedInfoQueryString { - if (!CountlyConsentManager.sharedInstance.consentForLocation || CountlyLocationManager.sharedInstance.isLocationInfoDisabled) - { - // NOTE: Return empty string for location. This is a server requirement to disable IP based location inferring. - return [NSString stringWithFormat:@"&%@=%@", kCountlyQSKeyLocation, @""]; - } + if (!CountlyConsentManager.sharedInstance.consentForLocation || CountlyLocationManager.sharedInstance.isLocationInfoDisabled) + { + //NOTE: Return empty string for location. This is a server requirement to disable IP based location inferring. + return [NSString stringWithFormat:@"&%@=%@", kCountlyQSKeyLocation, @""]; + } - NSString *location = CountlyLocationManager.sharedInstance.location.cly_URLEscaped; - NSString *city = CountlyLocationManager.sharedInstance.city.cly_URLEscaped; - NSString *ISOCountryCode = CountlyLocationManager.sharedInstance.ISOCountryCode.cly_URLEscaped; - NSString *IP = CountlyLocationManager.sharedInstance.IP.cly_URLEscaped; + NSString* location = CountlyLocationManager.sharedInstance.location.cly_URLEscaped; + NSString* city = CountlyLocationManager.sharedInstance.city.cly_URLEscaped; + NSString* ISOCountryCode = CountlyLocationManager.sharedInstance.ISOCountryCode.cly_URLEscaped; + NSString* IP = CountlyLocationManager.sharedInstance.IP.cly_URLEscaped; - NSMutableString *locationInfoQueryString = NSMutableString.new; + NSMutableString* locationInfoQueryString = NSMutableString.new; - if (location) - [locationInfoQueryString appendFormat:@"&%@=%@", kCountlyQSKeyLocation, location]; + if (location) + [locationInfoQueryString appendFormat:@"&%@=%@", kCountlyQSKeyLocation, location]; - if (city) - [locationInfoQueryString appendFormat:@"&%@=%@", kCountlyQSKeyLocationCity, city]; + if (city) + [locationInfoQueryString appendFormat:@"&%@=%@", kCountlyQSKeyLocationCity, city]; - if (ISOCountryCode) - [locationInfoQueryString appendFormat:@"&%@=%@", kCountlyQSKeyLocationCountry, ISOCountryCode]; + if (ISOCountryCode) + [locationInfoQueryString appendFormat:@"&%@=%@", kCountlyQSKeyLocationCountry, ISOCountryCode]; - if (IP) - [locationInfoQueryString appendFormat:@"&%@=%@", kCountlyQSKeyLocationIP, IP]; + if (IP) + [locationInfoQueryString appendFormat:@"&%@=%@", kCountlyQSKeyLocationIP, IP]; - if (locationInfoQueryString.length) - return locationInfoQueryString.copy; + if (locationInfoQueryString.length) + return locationInfoQueryString.copy; - return nil; + return nil; } - (NSString *)attributionQueryString { - if (!CountlyConsentManager.sharedInstance.consentForAttribution) - return nil; + if (!CountlyConsentManager.sharedInstance.consentForAttribution) + return nil; - if (!CountlyCommon.sharedInstance.attributionID) - return nil; + if (!CountlyCommon.sharedInstance.attributionID) + return nil; - NSDictionary *attribution = @{kCountlyQSKeyIDFA : CountlyCommon.sharedInstance.attributionID}; + NSDictionary* attribution = @{kCountlyQSKeyIDFA: CountlyCommon.sharedInstance.attributionID}; - return [NSString stringWithFormat:@"&%@=%@", kCountlyQSKeyAttributionID, [attribution cly_JSONify]]; + return [NSString stringWithFormat:@"&%@=%@", kCountlyQSKeyAttributionID, [attribution cly_JSONify]]; } - (NSMutableData *)pictureUploadDataForQueryString:(NSString *)queryString { #if (TARGET_OS_IOS || TARGET_OS_VISION) - NSString *localPicturePath = nil; + NSString* localPicturePath = nil; - NSString *userDetails = [queryString cly_valueForQueryStringKey:kCountlyQSKeyUserDetails]; - NSString *unescapedUserDetails = [userDetails stringByRemovingPercentEncoding]; - if (!unescapedUserDetails) - return nil; + NSString* userDetails = [queryString cly_valueForQueryStringKey:kCountlyQSKeyUserDetails]; + NSString* unescapedUserDetails = [userDetails stringByRemovingPercentEncoding]; + if (!unescapedUserDetails) + return nil; - NSDictionary *pathDictionary = [NSJSONSerialization JSONObjectWithData:[unescapedUserDetails cly_dataUTF8] options:0 error:nil]; - localPicturePath = pathDictionary[kCountlyLocalPicturePath]; + NSDictionary* pathDictionary = [NSJSONSerialization JSONObjectWithData:[unescapedUserDetails cly_dataUTF8] options:0 error:nil]; + localPicturePath = pathDictionary[kCountlyLocalPicturePath]; - if (!localPicturePath.length) - return nil; + if (!localPicturePath.length) + return nil; - CLY_LOG_D(@"Local picture path successfully extracted from query string: %@", localPicturePath); + CLY_LOG_D(@"Local picture path successfully extracted from query string: %@", localPicturePath); - NSArray *allowedFileTypes = @[ @"gif", @"png", @"jpg", @"jpeg" ]; - NSString *fileExt = localPicturePath.pathExtension.lowercaseString; - NSInteger fileExtIndex = [allowedFileTypes indexOfObject:fileExt]; + NSArray* allowedFileTypes = @[@"gif", @"png", @"jpg", @"jpeg"]; + NSString* fileExt = localPicturePath.pathExtension.lowercaseString; + NSInteger fileExtIndex = [allowedFileTypes indexOfObject:fileExt]; - if (fileExtIndex == NSNotFound) - { - CLY_LOG_W(@"Unsupported file extension for picture upload: %@", fileExt); - return nil; - } + if (fileExtIndex == NSNotFound) + { + CLY_LOG_W(@"Unsupported file extension for picture upload: %@", fileExt); + return nil; + } - NSData *imageData = [NSData dataWithContentsOfURL:[NSURL URLWithString:localPicturePath]]; + NSData* imageData = [NSData dataWithContentsOfURL:[NSURL URLWithString:localPicturePath]]; - if (!imageData) - { - CLY_LOG_W(@"Local picture data can not be read!"); - return nil; - } + if (!imageData) + { + CLY_LOG_W(@"Local picture data can not be read!"); + return nil; + } - CLY_LOG_D(@"Local picture data read successfully."); + CLY_LOG_D(@"Local picture data read successfully."); - // NOTE: Overcome failing PNG file upload if data is directly read from disk - if (fileExtIndex == 1) - imageData = UIImagePNGRepresentation([UIImage imageWithData:imageData]); + //NOTE: Overcome failing PNG file upload if data is directly read from disk + if (fileExtIndex == 1) + imageData = UIImagePNGRepresentation([UIImage imageWithData:imageData]); - // NOTE: Remap content type from jpg to jpeg - if (fileExtIndex == 2) - fileExtIndex = 3; + //NOTE: Remap content type from jpg to jpeg + if (fileExtIndex == 2) + fileExtIndex = 3; - NSString *boundaryStart = [NSString stringWithFormat:@"--%@\r\n", kCountlyUploadBoundary]; - NSString *contentDisposition = [NSString stringWithFormat:@"Content-Disposition: form-data; name=\"pictureFile\"; filename=\"%@\"\r\n", localPicturePath.lastPathComponent]; - NSString *contentType = [NSString stringWithFormat:@"Content-Type: image/%@\r\n\r\n", allowedFileTypes[fileExtIndex]]; + NSString* boundaryStart = [NSString stringWithFormat:@"--%@\r\n", kCountlyUploadBoundary]; + NSString* contentDisposition = [NSString stringWithFormat:@"Content-Disposition: form-data; name=\"pictureFile\"; filename=\"%@\"\r\n", localPicturePath.lastPathComponent]; + NSString* contentType = [NSString stringWithFormat:@"Content-Type: image/%@\r\n\r\n", allowedFileTypes[fileExtIndex]]; - NSMutableData *uploadData = NSMutableData.new; - [uploadData appendData:[boundaryStart cly_dataUTF8]]; - [uploadData appendData:[contentDisposition cly_dataUTF8]]; - [uploadData appendData:[contentType cly_dataUTF8]]; - [uploadData appendData:imageData]; - return uploadData; + NSMutableData* uploadData = NSMutableData.new; + [uploadData appendData:[boundaryStart cly_dataUTF8]]; + [uploadData appendData:[contentDisposition cly_dataUTF8]]; + [uploadData appendData:[contentType cly_dataUTF8]]; + [uploadData appendData:imageData]; + return uploadData; #endif - return nil; + return nil; } - (void)addMultipart:(NSMutableData *)uploadData andKey:(NSString *)key andValue:(NSString *)value { - NSString *boundaryStart = [NSString stringWithFormat:@"\r\n--%@\r\n", kCountlyUploadBoundary]; - NSString *contentDisposition = [NSString stringWithFormat:@"Content-Disposition: form-data; name=\"%@\";\r\n\r\n", key]; + NSString* boundaryStart = [NSString stringWithFormat:@"\r\n--%@\r\n", kCountlyUploadBoundary]; + NSString* contentDisposition = [NSString stringWithFormat:@"Content-Disposition: form-data; name=\"%@\";\r\n\r\n", key]; - [uploadData appendData:[boundaryStart cly_dataUTF8]]; - [uploadData appendData:[contentDisposition cly_dataUTF8]]; - [uploadData appendData:[value cly_dataUTF8]]; + [uploadData appendData:[boundaryStart cly_dataUTF8]]; + [uploadData appendData:[contentDisposition cly_dataUTF8]]; + [uploadData appendData:[value cly_dataUTF8]]; } - (NSString *)appendChecksum:(NSString *)queryString { - if (self.secretSalt) - { - NSString *checksum = [[queryString stringByAppendingString:self.secretSalt] cly_SHA256]; - return [queryString stringByAppendingFormat:@"&%@=%@", kCountlyQSKeyChecksum256, checksum]; - } + if (self.secretSalt) + { + NSString* checksum = [[queryString stringByAppendingString:self.secretSalt] cly_SHA256]; + return [queryString stringByAppendingFormat:@"&%@=%@", kCountlyQSKeyChecksum256, checksum]; + } - return queryString; + return queryString; } - (NSString *)appendRemainingRequest:(NSString *)queryString { - NSUInteger rrCount = [CountlyPersistency.sharedInstance remainingRequestCount] - 1; - return [queryString stringByAppendingFormat:@"&%@=%lu", kCountlyQSKeyRemainingRequest, (unsigned long)rrCount]; - - return queryString; + NSUInteger rrCount = [CountlyPersistency.sharedInstance remainingRequestCount] - 1; + return [queryString stringByAppendingFormat:@"&%@=%lu", kCountlyQSKeyRemainingRequest, (unsigned long)rrCount]; + + return queryString; } -- (BOOL)isRequestSuccessful:(NSURLResponse *)response data:(NSData *)data +- (BOOL)isRequestSuccessful:(NSURLResponse *)response data:(NSData *)data { - if (!response) - return NO; + if (!response) + return NO; - NSInteger code = ((NSHTTPURLResponse *)response).statusCode; + NSInteger code = ((NSHTTPURLResponse*)response).statusCode; - if (code >= 200 && code < 300) - { - NSError *error = nil; - NSDictionary *serverReply = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error]; - - if (error) + if (code >= 200 && code < 300) { - CLY_LOG_W(@"Server reply is not a valid JSON!"); - return NO; - } - - CLY_LOG_V(@"%s, response:[ %@ ] request:[ %@ ]", __FUNCTION__, serverReply, ((NSHTTPURLResponse *)response).URL); + NSError* error = nil; + NSDictionary* serverReply = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error]; - NSString *result = serverReply[@"result"]; - - if (result) + if (error) + { + CLY_LOG_W(@"Server reply is not a valid JSON!"); + return NO; + } + + CLY_LOG_V(@"%s, response:[ %@ ] request:[ %@ ]", __FUNCTION__, serverReply, ((NSHTTPURLResponse*)response).URL); + + NSString* result = serverReply[@"result"]; + + if(result) + { + return YES; + } + + return NO; + + } + else { - return YES; + CLY_LOG_V(@"HTTP status code is not 2XX series."); + return NO; } - - return NO; - } - else - { - CLY_LOG_V(@"HTTP status code is not 2XX series."); - return NO; - } } - (NSInteger)sessionLengthInSeconds { - NSTimeInterval currentTime = NSDate.date.timeIntervalSince1970; - unsentSessionLength += (currentTime - lastSessionStartTime); - lastSessionStartTime = currentTime; - int sessionLengthInSeconds = (int)unsentSessionLength; - unsentSessionLength -= sessionLengthInSeconds; - return sessionLengthInSeconds; + NSTimeInterval currentTime = NSDate.date.timeIntervalSince1970; + unsentSessionLength += (currentTime - lastSessionStartTime); + lastSessionStartTime = currentTime; + int sessionLengthInSeconds = (int)unsentSessionLength; + unsentSessionLength -= sessionLengthInSeconds; + return sessionLengthInSeconds; } -#pragma mark--- +#pragma mark --- - (NSURLSession *)URLSession { - if (!_URLSession) - { - if (self.pinnedCertificates) + if (!_URLSession) { - CLY_LOG_D(@"%d pinned certificate(s) specified in config.", (int)self.pinnedCertificates.count); - _URLSession = [NSURLSession sessionWithConfiguration:self.URLSessionConfiguration delegate:self delegateQueue:nil]; - } - else - { - _URLSession = [NSURLSession sessionWithConfiguration:self.URLSessionConfiguration]; + if (self.pinnedCertificates) + { + CLY_LOG_D(@"%d pinned certificate(s) specified in config.", (int)self.pinnedCertificates.count); + _URLSession = [NSURLSession sessionWithConfiguration:self.URLSessionConfiguration delegate:self delegateQueue:nil]; + } + else + { + _URLSession = [NSURLSession sessionWithConfiguration:self.URLSessionConfiguration]; + } } - } - return _URLSession; + return _URLSession; } - (void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential *))completionHandler { - SecPolicyRef policy = SecPolicyCreateSSL(true, (__bridge CFStringRef)challenge.protectionSpace.host); - SecTrustRef serverTrust = challenge.protectionSpace.serverTrust; - SecKeyRef serverKey = NULL; - - if (@available(iOS 14.0, tvOS 14.0, macOS 11.0, watchOS 7.0, *)) - { - serverKey = SecTrustCopyKey(serverTrust); - } - else - { -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wdeprecated-declarations" - serverKey = SecTrustCopyPublicKey(serverTrust); -#pragma GCC diagnostic pop - } - - __block BOOL isLocalAndServerCertMatch = NO; - - for (NSString *certificate in self.pinnedCertificates) - { - NSString *localCertPath = [NSBundle.mainBundle pathForResource:certificate ofType:nil]; - - if (!localCertPath) - [NSException raise:@"CountlyCertificateNotFoundException" format:@"Bundled certificate can not be found for %@", certificate]; - - NSData *localCertData = [NSData dataWithContentsOfFile:localCertPath]; - SecCertificateRef localCert = SecCertificateCreateWithData(NULL, (__bridge CFDataRef)localCertData); - SecTrustRef localTrust = NULL; - SecTrustCreateWithCertificates(localCert, policy, &localTrust); - SecKeyRef localKey = NULL; + SecPolicyRef policy = SecPolicyCreateSSL(true, (__bridge CFStringRef)challenge.protectionSpace.host); + SecTrustRef serverTrust = challenge.protectionSpace.serverTrust; + SecKeyRef serverKey = NULL; if (@available(iOS 14.0, tvOS 14.0, macOS 11.0, watchOS 7.0, *)) { - localKey = SecTrustCopyKey(localTrust); + serverKey = SecTrustCopyKey(serverTrust); } else { #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wdeprecated-declarations" - localKey = SecTrustCopyPublicKey(localTrust); + serverKey = SecTrustCopyPublicKey(serverTrust); #pragma GCC diagnostic pop } - CFRelease(localCert); - CFRelease(localTrust); + __block BOOL isLocalAndServerCertMatch = NO; - if (serverKey != NULL && localKey != NULL && [(__bridge id)serverKey isEqual:(__bridge id)localKey]) + for (NSString* certificate in self.pinnedCertificates) { - CLY_LOG_D(@"Pinned certificate and server certificate match."); + NSString* localCertPath = [NSBundle.mainBundle pathForResource:certificate ofType:nil]; - isLocalAndServerCertMatch = YES; - CFRelease(localKey); - break; - } + if (!localCertPath) + [NSException raise:@"CountlyCertificateNotFoundException" format:@"Bundled certificate can not be found for %@", certificate]; + + NSData* localCertData = [NSData dataWithContentsOfFile:localCertPath]; + SecCertificateRef localCert = SecCertificateCreateWithData(NULL, (__bridge CFDataRef)localCertData); + SecTrustRef localTrust = NULL; + SecTrustCreateWithCertificates(localCert, policy, &localTrust); + SecKeyRef localKey = NULL; + + if (@available(iOS 14.0, tvOS 14.0, macOS 11.0, watchOS 7.0, *)) + { + localKey = SecTrustCopyKey(localTrust); + } + else + { +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" + localKey = SecTrustCopyPublicKey(localTrust); +#pragma GCC diagnostic pop + } - if (localKey) - CFRelease(localKey); - } + CFRelease(localCert); + CFRelease(localTrust); + if (serverKey != NULL && localKey != NULL && [(__bridge id)serverKey isEqual:(__bridge id)localKey]) + { + CLY_LOG_D(@"Pinned certificate and server certificate match."); + + isLocalAndServerCertMatch = YES; + CFRelease(localKey); + break; + } + + if (localKey) + CFRelease(localKey); + } + #if DEBUG - if (CountlyCommon.sharedInstance.shouldIgnoreTrustCheck) - { - CFDataRef exceptions = SecTrustCopyExceptions(serverTrust); - SecTrustSetExceptions(serverTrust, exceptions); - CFRelease(exceptions); - } + if (CountlyCommon.sharedInstance.shouldIgnoreTrustCheck) + { + CFDataRef exceptions = SecTrustCopyExceptions(serverTrust); + SecTrustSetExceptions(serverTrust, exceptions); + CFRelease(exceptions); + } #endif - - SecTrustResultType serverTrustResult; + + SecTrustResultType serverTrustResult; #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wdeprecated-declarations" - SecTrustEvaluate(serverTrust, &serverTrustResult); + SecTrustEvaluate(serverTrust, &serverTrustResult); #pragma GCC diagnostic pop - BOOL isServerCertValid = (serverTrustResult == kSecTrustResultUnspecified || serverTrustResult == kSecTrustResultProceed); + BOOL isServerCertValid = (serverTrustResult == kSecTrustResultUnspecified || serverTrustResult == kSecTrustResultProceed); - if (isLocalAndServerCertMatch && isServerCertValid) - { - CLY_LOG_D(@"Pinned certificate check is successful. Proceeding with request."); - completionHandler(NSURLSessionAuthChallengeUseCredential, [NSURLCredential credentialForTrust:serverTrust]); - } - else - { - if (!isLocalAndServerCertMatch) - CLY_LOG_W(@"Pinned certificate and server certificate does not match!"); + if (isLocalAndServerCertMatch && isServerCertValid) + { + CLY_LOG_D(@"Pinned certificate check is successful. Proceeding with request."); + completionHandler(NSURLSessionAuthChallengeUseCredential, [NSURLCredential credentialForTrust:serverTrust]); + } + else + { + if (!isLocalAndServerCertMatch) + CLY_LOG_W(@"Pinned certificate and server certificate does not match!"); - if (!isServerCertValid) - CLY_LOG_W(@"Server certificate is not valid! SecTrustEvaluate result is: %u", serverTrustResult); + if (!isServerCertValid) + CLY_LOG_W(@"Server certificate is not valid! SecTrustEvaluate result is: %u", serverTrustResult); - CLY_LOG_D(@"Pinned certificate check failed! Cancelling request."); - completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, NULL); - } + CLY_LOG_D(@"Pinned certificate check failed! Cancelling request."); + completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, NULL); + } - if (serverKey) - CFRelease(serverKey); + if (serverKey) + CFRelease(serverKey); - CFRelease(policy); + CFRelease(policy); } #pragma mark - Request Callbacks - (void)registerRequestCallback:(NSString *)callbackID callback:(CLYRequestCallback)callback { - if (!callbackID || callbackID.length == 0) - { - CLY_LOG_W(@"%s, Callback ID is nil or empty. Callback registration ignored.", __FUNCTION__); - return; - } + if (!callbackID || callbackID.length == 0) + { + CLY_LOG_W(@"%s, Callback ID is nil or empty. Callback registration ignored.", __FUNCTION__); + return; + } - if (!callback) - { - CLY_LOG_W(@"%s, Callback block is nil. Callback registration ignored.", __FUNCTION__); - return; - } + if (!callback) + { + CLY_LOG_W(@"%s, Callback block is nil. Callback registration ignored.", __FUNCTION__); + return; + } - dispatch_sync(_callbackQueue, ^{ - CLY_LOG_D(@"%s, Registering request callback with ID: %@", __FUNCTION__, callbackID); - self.internalRequestCallbacks[callbackID] = callback; - }); + dispatch_sync(_callbackQueue, ^{ + CLY_LOG_D(@"%s, Registering request callback with ID: %@", __FUNCTION__, callbackID); + self.internalRequestCallbacks[callbackID] = callback; + }); } - (void)removeRequestCallback:(NSString *)callbackID { - if (!callbackID || callbackID.length == 0) - { - CLY_LOG_W(@"%s, Callback ID is nil or empty. Callback removal ignored.", __FUNCTION__); - return; - } + if (!callbackID || callbackID.length == 0) + { + CLY_LOG_W(@"%s, Callback ID is nil or empty. Callback removal ignored.", __FUNCTION__); + return; + } - dispatch_sync(_callbackQueue, ^{ - CLY_LOG_D(@"%s, Removing request callback with ID: %@", __FUNCTION__, callbackID); - [self.internalRequestCallbacks removeObjectForKey:callbackID]; - }); + dispatch_sync(_callbackQueue, ^{ + CLY_LOG_D(@"%s, Removing request callback with ID: %@", __FUNCTION__, callbackID); + [self.internalRequestCallbacks removeObjectForKey:callbackID]; + }); } - (void)addQueueFlushRunnable:(CLYQueueFlushRunnable)runnable { - if (!runnable) - { - CLY_LOG_W(@"%s, Runnable is nil. Cannot add to queue flush runnables.", __FUNCTION__); - return; - } + if (!runnable) + { + CLY_LOG_W(@"%s, Runnable is nil. Cannot add to queue flush runnables.", __FUNCTION__); + return; + } - CLYQueueFlushRunnable runnableCopy = [runnable copy]; - dispatch_sync(_callbackQueue, ^{ - CLY_LOG_D(@"%s, Adding queue flush runnable. Total count: %lu", __FUNCTION__, (unsigned long)(self->_queueFlushRunnables.count + 1)); - [self->_queueFlushRunnables addObject:runnableCopy]; - }); + CLYQueueFlushRunnable runnableCopy = [runnable copy]; + dispatch_sync(_callbackQueue, ^{ + CLY_LOG_D(@"%s, Adding queue flush runnable. Total count: %lu", __FUNCTION__, (unsigned long)(self->_queueFlushRunnables.count + 1)); + [self->_queueFlushRunnables addObject:runnableCopy]; + }); } - (void)clearQueueFlushRunnables { - dispatch_sync(_callbackQueue, ^{ - CLY_LOG_D(@"%s, Clearing %lu queue flush runnables.", __FUNCTION__, (unsigned long)self->_queueFlushRunnables.count); - [self->_queueFlushRunnables removeAllObjects]; - }); + dispatch_sync(_callbackQueue, ^{ + CLY_LOG_D(@"%s, Clearing %lu queue flush runnables.", __FUNCTION__, (unsigned long)self->_queueFlushRunnables.count); + [self->_queueFlushRunnables removeAllObjects]; + }); } - (void)addToQueueWithCallback:(NSString *)queryString callback:(CLYRequestCallback)callback { - if (!queryString || queryString.length == 0) - { - CLY_LOG_W(@"%s, Query string is nil or empty. Cannot add to queue with callback.", __FUNCTION__); - return; - } + if (!queryString || queryString.length == 0) + { + CLY_LOG_W(@"%s, Query string is nil or empty. Cannot add to queue with callback.", __FUNCTION__); + return; + } - if (!callback) - { - [CountlyPersistency.sharedInstance addToQueue:queryString]; - return; - } + if (!callback) + { + [CountlyPersistency.sharedInstance addToQueue:queryString]; + return; + } - // Generate a unique callback ID - NSString *callbackID = [[NSUUID UUID] UUIDString]; - CLY_LOG_D(@"%s, Adding request to queue with callback ID: %@", __FUNCTION__, callbackID); + // Generate a unique callback ID + NSString* callbackID = [[NSUUID UUID] UUIDString]; + CLY_LOG_D(@"%s, Adding request to queue with callback ID: %@", __FUNCTION__, callbackID); - // Register the callback - [self registerRequestCallback:callbackID callback:callback]; + // Register the callback + [self registerRequestCallback:callbackID callback:callback]; - // Append callback_id parameter to query string - NSString *queryStringWithCallback = [queryString stringByAppendingFormat:@"&callback_id=%@", callbackID]; + // Append callback_id parameter to query string + NSString* queryStringWithCallback = [queryString stringByAppendingFormat:@"&callback_id=%@", callbackID]; - // Add to queue - [CountlyPersistency.sharedInstance addToQueue:queryStringWithCallback]; + // Add to queue + [CountlyPersistency.sharedInstance addToQueue:queryStringWithCallback]; } @end diff --git a/CountlyServerConfig.m b/CountlyServerConfig.m index b0d3a63e..243fff6f 100644 --- a/CountlyServerConfig.m +++ b/CountlyServerConfig.m @@ -7,1067 +7,1019 @@ #import "CountlyCommon.h" @interface CountlyServerConfig () { - NSTimer *_requestTimer; + NSTimer *_requestTimer; } -@property(nonatomic) BOOL trackingEnabled; -@property(nonatomic) BOOL networkingEnabled; -@property(nonatomic) BOOL crashReportingEnabled; -@property(nonatomic) BOOL loggingEnabled; -@property(nonatomic) BOOL customEventTrackingEnabled; -@property(nonatomic) BOOL viewTrackingEnabled; -@property(nonatomic) BOOL sessionTrackingEnabled; -@property(nonatomic) BOOL enterContentZone; -@property(nonatomic) BOOL consentRequired; -@property(nonatomic) BOOL locationTracking; -@property(nonatomic) BOOL refreshContentZone; -@property(nonatomic) BOOL backoffMechanism; - -@property(nonatomic) NSInteger limitKeyLength; -@property(nonatomic) NSInteger limitValueSize; -@property(nonatomic) NSInteger limitSegValues; -@property(nonatomic) NSInteger limitBreadcrumb; -@property(nonatomic) NSInteger limitTraceLine; -@property(nonatomic) NSInteger limitTraceLength; -@property(nonatomic) NSInteger sessionInterval; -@property(nonatomic) NSInteger eventQueueSize; -@property(nonatomic) NSInteger requestQueueSize; -@property(nonatomic) NSInteger contentZoneInterval; -@property(nonatomic) NSInteger dropOldRequestTime; -@property(nonatomic) NSInteger serverConfigUpdateInterval; -@property(nonatomic) NSInteger currentServerConfigUpdateInterval; - -@property(nonatomic) NSInteger bomAcceptedTimeoutSeconds; -@property(nonatomic) double bomRQPercentage; -@property(nonatomic) NSInteger bomRequestAge; -@property(nonatomic) NSInteger bomDuration; - -@property(nonatomic) NSInteger requestTimeoutDuration; - -@property(nonatomic) NSSet *eventFilterSet; -@property(nonatomic) BOOL eventFilterIsWhitelist; -@property(nonatomic) NSSet *userPropertyFilterSet; -@property(nonatomic) BOOL userPropertyFilterIsWhitelist; -@property(nonatomic) NSInteger userPropertyCacheLimit; -@property(nonatomic) NSSet *segmentationFilterSet; -@property(nonatomic) BOOL segmentationFilterIsWhitelist; -@property(nonatomic) NSDictionary *> *eventSegmentationFilterMap; -@property(nonatomic) BOOL eventSegmentationFilterIsWhitelist; -@property(nonatomic) NSSet *journeyTriggerEvents; - -@property(nonatomic) NSInteger version; -@property(nonatomic) long long timestamp; -@property(nonatomic) long long lastFetchTimestamp; -@property(nonatomic) BOOL serverConfigUpdatesDisabled; +@property (nonatomic) BOOL trackingEnabled; +@property (nonatomic) BOOL networkingEnabled; +@property (nonatomic) BOOL crashReportingEnabled; +@property (nonatomic) BOOL loggingEnabled; +@property (nonatomic) BOOL customEventTrackingEnabled; +@property (nonatomic) BOOL viewTrackingEnabled; +@property (nonatomic) BOOL sessionTrackingEnabled; +@property (nonatomic) BOOL enterContentZone; +@property (nonatomic) BOOL consentRequired; +@property (nonatomic) BOOL locationTracking; +@property (nonatomic) BOOL refreshContentZone; +@property (nonatomic) BOOL backoffMechanism; + +@property (nonatomic) NSInteger limitKeyLength; +@property (nonatomic) NSInteger limitValueSize; +@property (nonatomic) NSInteger limitSegValues; +@property (nonatomic) NSInteger limitBreadcrumb; +@property (nonatomic) NSInteger limitTraceLine; +@property (nonatomic) NSInteger limitTraceLength; +@property (nonatomic) NSInteger sessionInterval; +@property (nonatomic) NSInteger eventQueueSize; +@property (nonatomic) NSInteger requestQueueSize; +@property (nonatomic) NSInteger contentZoneInterval; +@property (nonatomic) NSInteger dropOldRequestTime; +@property (nonatomic) NSInteger serverConfigUpdateInterval; +@property (nonatomic) NSInteger currentServerConfigUpdateInterval; + +@property (nonatomic) NSInteger bomAcceptedTimeoutSeconds; +@property (nonatomic) double bomRQPercentage; +@property (nonatomic) NSInteger bomRequestAge; +@property (nonatomic) NSInteger bomDuration; + +@property (nonatomic) NSInteger requestTimeoutDuration; + +@property (nonatomic) NSSet *eventFilterSet; +@property (nonatomic) BOOL eventFilterIsWhitelist; +@property (nonatomic) NSSet *userPropertyFilterSet; +@property (nonatomic) BOOL userPropertyFilterIsWhitelist; +@property (nonatomic) NSInteger userPropertyCacheLimit; +@property (nonatomic) NSSet *segmentationFilterSet; +@property (nonatomic) BOOL segmentationFilterIsWhitelist; +@property (nonatomic) NSDictionary *> *eventSegmentationFilterMap; +@property (nonatomic) BOOL eventSegmentationFilterIsWhitelist; +@property (nonatomic) NSSet *journeyTriggerEvents; + +@property (nonatomic) NSInteger version; +@property (nonatomic) long long timestamp; +@property (nonatomic) long long lastFetchTimestamp; +@property (nonatomic) BOOL serverConfigUpdatesDisabled; @end NSString *const kCountlySCKeySC = @"sc"; -NSString *const kTracking = @"tracking"; -NSString *const kNetworking = @"networking"; +NSString *const kTracking = @"tracking"; +NSString *const kNetworking = @"networking"; // request keys -NSString *const kRTimestamp = @"t"; -NSString *const kRVersion = @"v"; -NSString *const kRConfig = @"c"; -NSString *const kRReqQueueSize = @"rqs"; -NSString *const kREventQueueSize = @"eqs"; -NSString *const kRLogging = @"log"; +NSString *const kRTimestamp = @"t"; +NSString *const kRVersion = @"v"; +NSString *const kRConfig = @"c"; +NSString *const kRReqQueueSize = @"rqs"; +NSString *const kREventQueueSize = @"eqs"; +NSString *const kRLogging = @"log"; NSString *const kRSessionUpdateInterval = @"sui"; -NSString *const kRSessionTracking = @"st"; -NSString *const kRViewTracking = @"vt"; -NSString *const kRLocationTracking = @"lt"; -NSString *const kRRefreshContentZone = @"rcz"; -NSString *const kRbackoffMechanism = @"bom"; - -NSString *const kRLimitKeyLength = @"lkl"; -NSString *const kRLimitValueSize = @"lvs"; -NSString *const kRLimitSegValues = @"lsv"; -NSString *const kRLimitBreadcrumb = @"lbc"; -NSString *const kRLimitTraceLine = @"ltlpt"; -NSString *const kRLimitTraceLength = @"ltl"; -NSString *const kRCustomEventTracking = @"cet"; -NSString *const kREnterContentZone = @"ecz"; -NSString *const kRContentZoneInterval = @"czi"; -NSString *const kRConsentRequired = @"cr"; -NSString *const kRDropOldRequestTime = @"dort"; -NSString *const kRCrashReporting = @"crt"; +NSString *const kRSessionTracking = @"st"; +NSString *const kRViewTracking = @"vt"; +NSString *const kRLocationTracking = @"lt"; +NSString *const kRRefreshContentZone = @"rcz"; +NSString *const kRbackoffMechanism = @"bom"; + +NSString *const kRLimitKeyLength = @"lkl"; +NSString *const kRLimitValueSize = @"lvs"; +NSString *const kRLimitSegValues = @"lsv"; +NSString *const kRLimitBreadcrumb = @"lbc"; +NSString *const kRLimitTraceLine = @"ltlpt"; +NSString *const kRLimitTraceLength = @"ltl"; +NSString *const kRCustomEventTracking = @"cet"; +NSString *const kREnterContentZone = @"ecz"; +NSString *const kRContentZoneInterval = @"czi"; +NSString *const kRConsentRequired = @"cr"; +NSString *const kRDropOldRequestTime = @"dort"; +NSString *const kRCrashReporting = @"crt"; NSString *const kRServerConfigUpdateInterval = @"scui"; -NSString *const kRBOMAcceptedTimeout = @"bom_at"; -NSString *const kRBOMRQPercentage = @"bom_rqp"; -NSString *const kRBOMRequestAge = @"bom_ra"; -NSString *const kRBOMDuration = @"bom_d"; - -NSString *const kREventBlacklist = @"eb"; -NSString *const kREventWhitelist = @"ew"; -NSString *const kRUserPropertyBlacklist = @"upb"; -NSString *const kRUserPropertyWhitelist = @"upw"; -NSString *const kRUserPropertyCacheLimit = @"upcl"; -NSString *const kRSegmentationBlacklist = @"sb"; -NSString *const kRSegmentationWhitelist = @"sw"; +NSString *const kRBOMAcceptedTimeout = @"bom_at"; +NSString *const kRBOMRQPercentage = @"bom_rqp"; +NSString *const kRBOMRequestAge = @"bom_ra"; +NSString *const kRBOMDuration = @"bom_d"; + +NSString *const kREventBlacklist = @"eb"; +NSString *const kREventWhitelist = @"ew"; +NSString *const kRUserPropertyBlacklist = @"upb"; +NSString *const kRUserPropertyWhitelist = @"upw"; +NSString *const kRUserPropertyCacheLimit = @"upcl"; +NSString *const kRSegmentationBlacklist = @"sb"; +NSString *const kRSegmentationWhitelist = @"sw"; NSString *const kREventSegmentationBlacklist = @"esb"; NSString *const kREventSegmentationWhitelist = @"esw"; -NSString *const kRJourneyTriggerEvents = @"jte"; +NSString *const kRJourneyTriggerEvents = @"jte"; static CountlyServerConfig *s_sharedInstance = nil; -static dispatch_once_t onceToken; +static dispatch_once_t onceToken; @implementation CountlyServerConfig + (instancetype)sharedInstance { - if (!CountlyCommon.sharedInstance.hasStarted) - return nil; + if (!CountlyCommon.sharedInstance.hasStarted) + return nil; - dispatch_once(&onceToken, ^{ - s_sharedInstance = self.new; - }); - return s_sharedInstance; + dispatch_once(&onceToken, ^{ + s_sharedInstance = self.new; + }); + return s_sharedInstance; } - (instancetype)init { - self = [super init]; - if (self) - { - _timestamp = 0; - _version = 0; - _currentServerConfigUpdateInterval = 4; - _requestTimer = nil; - _serverConfigUpdatesDisabled = NO; - _requestTimeoutDuration = 30; - [self setDefaultValues]; - } - return self; + self = [super init]; + if (self) + { + _timestamp = 0; + _version = 0; + _currentServerConfigUpdateInterval = 4; + _requestTimer = nil; + _serverConfigUpdatesDisabled = NO; + _requestTimeoutDuration = 30; + [self setDefaultValues]; + } + return self; } - (void)resetInstance { - CLY_LOG_I(@"%s", __FUNCTION__); - _timestamp = 0; - _version = 0; - _currentServerConfigUpdateInterval = 4; - _serverConfigUpdatesDisabled = NO; - _requestTimeoutDuration = 30; - _lastFetchTimestamp = 0; - if (_requestTimer) - { - [_requestTimer invalidate]; - _requestTimer = nil; - } - [self setDefaultValues]; - onceToken = 0; - s_sharedInstance = nil; + CLY_LOG_I(@"%s", __FUNCTION__); + _timestamp = 0; + _version = 0; + _currentServerConfigUpdateInterval = 4; + _serverConfigUpdatesDisabled = NO; + _requestTimeoutDuration = 30; + _lastFetchTimestamp = 0; + if (_requestTimer) + { + [_requestTimer invalidate]; + _requestTimer = nil; + } + [self setDefaultValues]; + onceToken = 0; + s_sharedInstance = nil; } - (void)retrieveServerConfigFromStorage:(CountlyConfig *)config { - NSMutableDictionary *persistentBehaviorSettings = [CountlyPersistency.sharedInstance retrieveServerConfig]; - if (persistentBehaviorSettings.count == 0 && config.sdkBehaviorSettings) - { - NSError *error = nil; - id parsed = [NSJSONSerialization JSONObjectWithData:[config.sdkBehaviorSettings cly_dataUTF8] options:0 error:&error]; - - if ([parsed isKindOfClass:[NSDictionary class]]) - { - persistentBehaviorSettings = [(NSDictionary *)parsed mutableCopy]; - } - else + NSMutableDictionary *persistentBehaviorSettings = [CountlyPersistency.sharedInstance retrieveServerConfig]; + if (persistentBehaviorSettings.count == 0 && config.sdkBehaviorSettings) { - CLY_LOG_W(@"%s, Failed to parse sdkBehaviorSettings or not a dictionary: %@", __FUNCTION__, error); + NSError *error = nil; + id parsed = [NSJSONSerialization JSONObjectWithData:[config.sdkBehaviorSettings cly_dataUTF8] options:0 error:&error]; + + if ([parsed isKindOfClass:[NSDictionary class]]) { + persistentBehaviorSettings = [(NSDictionary *)parsed mutableCopy]; + } else { + CLY_LOG_W(@"%s, Failed to parse sdkBehaviorSettings or not a dictionary: %@", __FUNCTION__, error); + } } - } - [self populateServerConfig:persistentBehaviorSettings withConfig:config]; - [CountlyPersistency.sharedInstance storeServerConfig:persistentBehaviorSettings]; + [self populateServerConfig:persistentBehaviorSettings withConfig:config]; + [CountlyPersistency.sharedInstance storeServerConfig:persistentBehaviorSettings]; } -- (void)mergeBehaviorSettings:(NSMutableDictionary *)baseConfig withConfig:(NSDictionary *)newConfig +- (void)mergeBehaviorSettings:(NSMutableDictionary *)baseConfig + withConfig:(NSDictionary *)newConfig { - // c, t and v paramters must exist - if (newConfig.count != 3 || !newConfig[kRConfig]) - { - CLY_LOG_D(@"%s, missing entries for a behavior settings omitting", __FUNCTION__); - return; - } - - if (!newConfig[kRVersion] || !newConfig[kRTimestamp]) - { - CLY_LOG_D(@"%s, version or timestamp is missing in the behavioır settings omitting", __FUNCTION__); - return; - } - - if (!([newConfig[kRConfig] isKindOfClass:[NSDictionary class]]) || ((NSDictionary *)newConfig[kRConfig]).count == 0) - { - CLY_LOG_D(@"%s, invalid behavior settings omitting", __FUNCTION__); - return; - } - - id timestamp = newConfig[kRTimestamp]; - if (timestamp) - { - baseConfig[kRTimestamp] = timestamp; - } - - id version = newConfig[kRVersion]; - if (version) - { - baseConfig[kRVersion] = version; - } - - NSDictionary *cBase = baseConfig[kRConfig] ?: NSMutableDictionary.new; - NSDictionary *cNew = newConfig[kRConfig]; - - if ([cBase isKindOfClass:[NSDictionary class]] || [cNew isKindOfClass:[NSDictionary class]]) - { - NSMutableDictionary *cMerged = [cBase mutableCopy]; - - [cNew enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) { - if (obj != nil && obj != [NSNull null]) - { - cMerged[key] = obj; - } - }]; - - [self removeConflictingFilterKeys:cMerged newConfig:cNew]; - baseConfig[kRConfig] = cMerged; - } + // c, t and v paramters must exist + if(newConfig.count != 3 || !newConfig[kRConfig]) { + CLY_LOG_D(@"%s, missing entries for a behavior settings omitting", __FUNCTION__); + return; + } + + if (!newConfig[kRVersion] || !newConfig[kRTimestamp]) + { + CLY_LOG_D(@"%s, version or timestamp is missing in the behavioır settings omitting", __FUNCTION__); + return; + } + + if(!([newConfig[kRConfig] isKindOfClass:[NSDictionary class]]) || ((NSDictionary *)newConfig[kRConfig]).count == 0){ + CLY_LOG_D(@"%s, invalid behavior settings omitting", __FUNCTION__); + return; + } + + id timestamp = newConfig[kRTimestamp]; + if (timestamp) { + baseConfig[kRTimestamp] = timestamp; + } + + id version = newConfig[kRVersion]; + if (version) { + baseConfig[kRVersion] = version; + } + + NSDictionary *cBase = baseConfig[kRConfig] ?: NSMutableDictionary.new; + NSDictionary *cNew = newConfig[kRConfig]; + + if ([cBase isKindOfClass:[NSDictionary class]] || [cNew isKindOfClass:[NSDictionary class]]) { + NSMutableDictionary *cMerged = [cBase mutableCopy]; + + [cNew enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) { + if (obj != nil && obj != [NSNull null]) { + cMerged[key] = obj; + } + }]; + + [self removeConflictingFilterKeys:cMerged newConfig:cNew]; + baseConfig[kRConfig] = cMerged; + } } - (void)setBoolProperty:(BOOL *)property fromDictionary:(NSMutableDictionary *)dictionary key:(NSString *)key logString:(NSMutableString *)logString { - id value = dictionary[key]; - if (!value) - return; - - if (CFGetTypeID((__bridge CFTypeRef)value) == CFBooleanGetTypeID()) - { - *property = [value boolValue]; - [logString appendFormat:@"%@: %@, ", key, *property ? @"YES" : @"NO"]; - } - else - { - CLY_LOG_W(@"%s, Invalid type for bool key '%@', removing", __FUNCTION__, key); - [dictionary removeObjectForKey:key]; - } + id value = dictionary[key]; + if (!value) + return; + + if (CFGetTypeID((__bridge CFTypeRef)value) == CFBooleanGetTypeID()) + { + *property = [value boolValue]; + [logString appendFormat:@"%@: %@, ", key, *property ? @"YES" : @"NO"]; + } + else + { + CLY_LOG_W(@"%s, Invalid type for bool key '%@', removing", __FUNCTION__, key); + [dictionary removeObjectForKey:key]; + } } - (void)setIntegerProperty:(NSInteger *)property fromDictionary:(NSMutableDictionary *)dictionary key:(NSString *)key logString:(NSMutableString *)logString { - [self setIntegerProperty:property fromDictionary:dictionary key:key minValue:1 logString:logString]; + [self setIntegerProperty:property fromDictionary:dictionary key:key minValue:1 logString:logString]; } - (void)setIntegerProperty:(NSInteger *)property fromDictionary:(NSMutableDictionary *)dictionary key:(NSString *)key minValue:(NSInteger)minValue logString:(NSMutableString *)logString { - id value = dictionary[key]; - if (!value) - return; - - if ([value isKindOfClass:[NSNumber class]] && CFGetTypeID((__bridge CFTypeRef)value) != CFBooleanGetTypeID()) - { - NSInteger intVal = [value integerValue]; - if (intVal >= minValue) + id value = dictionary[key]; + if (!value) + return; + + if ([value isKindOfClass:[NSNumber class]] && CFGetTypeID((__bridge CFTypeRef)value) != CFBooleanGetTypeID()) { - *property = intVal; - [logString appendFormat:@"%@: %ld, ", key, (long)*property]; + NSInteger intVal = [value integerValue]; + if (intVal >= minValue) + { + *property = intVal; + [logString appendFormat:@"%@: %ld, ", key, (long)*property]; + } + else + { + CLY_LOG_W(@"%s, Invalid value (%ld) for integer key '%@' (min: %ld), removing", __FUNCTION__, (long)intVal, key, (long)minValue); + [dictionary removeObjectForKey:key]; + } } else { - CLY_LOG_W(@"%s, Invalid value (%ld) for integer key '%@' (min: %ld), removing", __FUNCTION__, (long)intVal, key, (long)minValue); - [dictionary removeObjectForKey:key]; + CLY_LOG_W(@"%s, Invalid type for integer key '%@', removing", __FUNCTION__, key); + [dictionary removeObjectForKey:key]; } - } - else - { - CLY_LOG_W(@"%s, Invalid type for integer key '%@', removing", __FUNCTION__, key); - [dictionary removeObjectForKey:key]; - } } - (void)setDoubleProperty:(double *)property fromDictionary:(NSMutableDictionary *)dictionary key:(NSString *)key logString:(NSMutableString *)logString { - id value = dictionary[key]; - if (!value) - return; - - if ([value isKindOfClass:[NSNumber class]] && CFGetTypeID((__bridge CFTypeRef)value) != CFBooleanGetTypeID()) - { - double dblVal = [value doubleValue]; - if (dblVal > 0.0 && dblVal < 1.0) + id value = dictionary[key]; + if (!value) + return; + + if ([value isKindOfClass:[NSNumber class]] && CFGetTypeID((__bridge CFTypeRef)value) != CFBooleanGetTypeID()) { - *property = dblVal; - [logString appendFormat:@"%@: %lf, ", key, (double)*property]; + double dblVal = [value doubleValue]; + if (dblVal > 0.0 && dblVal < 1.0) + { + *property = dblVal; + [logString appendFormat:@"%@: %lf, ", key, (double)*property]; + } + else + { + CLY_LOG_W(@"%s, Invalid value (%lf) for double key '%@', removing", __FUNCTION__, dblVal, key); + [dictionary removeObjectForKey:key]; + } } else { - CLY_LOG_W(@"%s, Invalid value (%lf) for double key '%@', removing", __FUNCTION__, dblVal, key); - [dictionary removeObjectForKey:key]; + CLY_LOG_W(@"%s, Invalid type for double key '%@', removing", __FUNCTION__, key); + [dictionary removeObjectForKey:key]; } - } - else - { - CLY_LOG_W(@"%s, Invalid type for double key '%@', removing", __FUNCTION__, key); - [dictionary removeObjectForKey:key]; - } } - (void)populateServerConfig:(NSMutableDictionary *)serverConfig withConfig:(CountlyConfig *)config { - if (config.requestTimeoutDuration <= 0) - { - config.requestTimeoutDuration = 1; - } - - _requestTimeoutDuration = config.requestTimeoutDuration; - - if (!serverConfig[kRConfig]) - { - CLY_LOG_D(@"%s, config key is missing in the server configuration omitting", __FUNCTION__); - return; - } - - NSMutableDictionary *dictionary; - if ([serverConfig[kRConfig] isKindOfClass:[NSDictionary class]]) - { - dictionary = [serverConfig[kRConfig] mutableCopy]; - } - else - { - CLY_LOG_D(@"%s, config is not a dictionary, omitting", __FUNCTION__); - return; - } - - if (!serverConfig[kRVersion] || !serverConfig[kRTimestamp]) - { - CLY_LOG_D(@"%s, version or timestamp is missing in the server configuration omitting", __FUNCTION__); - return; - } - - _version = [serverConfig[kRVersion] integerValue]; - _timestamp = [serverConfig[kRTimestamp] longLongValue]; - - NSMutableString *logString = [NSMutableString stringWithString:@"Server Config: "]; - - // Known keys set — used to remove unknown keys after validation - NSMutableSet *knownKeys = [NSMutableSet setWithArray:@[ - kTracking, - kNetworking, - kRSessionUpdateInterval, - kRReqQueueSize, - kREventQueueSize, - kRCrashReporting, - kRSessionTracking, - kRLogging, - kRLimitKeyLength, - kRLimitValueSize, - kRLimitSegValues, - kRLimitBreadcrumb, - kRLimitTraceLine, - kRLimitTraceLength, - kRCustomEventTracking, - kRViewTracking, - kREnterContentZone, - kRContentZoneInterval, - kRConsentRequired, - kRDropOldRequestTime, - kRServerConfigUpdateInterval, - kRLocationTracking, - kRRefreshContentZone, - kRbackoffMechanism, - kRBOMAcceptedTimeout, - kRBOMRQPercentage, - kRBOMRequestAge, - kRBOMDuration, - kRUserPropertyCacheLimit, - kREventBlacklist, - kREventWhitelist, - kRUserPropertyBlacklist, - kRUserPropertyWhitelist, - kRSegmentationBlacklist, - kRSegmentationWhitelist, - kREventSegmentationBlacklist, - kREventSegmentationWhitelist, - kRJourneyTriggerEvents - ]]; - - // Remove unknown keys - for (NSString *key in dictionary.allKeys) - { - if (![knownKeys containsObject:key]) + if(config.requestTimeoutDuration <= 0) { + config.requestTimeoutDuration = 1; + } + + _requestTimeoutDuration = config.requestTimeoutDuration; + + if (!serverConfig[kRConfig]) + { + CLY_LOG_D(@"%s, config key is missing in the server configuration omitting", __FUNCTION__); + return; + } + + NSMutableDictionary *dictionary; + if ([serverConfig[kRConfig] isKindOfClass:[NSDictionary class]]) + { + dictionary = [serverConfig[kRConfig] mutableCopy]; + } + else + { + CLY_LOG_D(@"%s, config is not a dictionary, omitting", __FUNCTION__); + return; + } + + if (!serverConfig[kRVersion] || !serverConfig[kRTimestamp]) { - CLY_LOG_W(@"%s, Unknown config key '%@', removing", __FUNCTION__, key); - [dictionary removeObjectForKey:key]; + CLY_LOG_D(@"%s, version or timestamp is missing in the server configuration omitting", __FUNCTION__); + return; + } + + _version = [serverConfig[kRVersion] integerValue]; + _timestamp = [serverConfig[kRTimestamp] longLongValue]; + + NSMutableString *logString = [NSMutableString stringWithString:@"Server Config: "]; + + // Known keys set — used to remove unknown keys after validation + NSMutableSet *knownKeys = [NSMutableSet setWithArray:@[ + kTracking, + kNetworking, + kRSessionUpdateInterval, + kRReqQueueSize, + kREventQueueSize, + kRCrashReporting, + kRSessionTracking, + kRLogging, + kRLimitKeyLength, + kRLimitValueSize, + kRLimitSegValues, + kRLimitBreadcrumb, + kRLimitTraceLine, + kRLimitTraceLength, + kRCustomEventTracking, + kRViewTracking, + kREnterContentZone, + kRContentZoneInterval, + kRConsentRequired, + kRDropOldRequestTime, + kRServerConfigUpdateInterval, + kRLocationTracking, + kRRefreshContentZone, + kRbackoffMechanism, + kRBOMAcceptedTimeout, + kRBOMRQPercentage, + kRBOMRequestAge, + kRBOMDuration, + kRUserPropertyCacheLimit, + kREventBlacklist, + kREventWhitelist, + kRUserPropertyBlacklist, + kRUserPropertyWhitelist, + kRSegmentationBlacklist, + kRSegmentationWhitelist, + kREventSegmentationBlacklist, + kREventSegmentationWhitelist, + kRJourneyTriggerEvents + ]]; + + // Remove unknown keys + for (NSString *key in dictionary.allKeys) + { + if (![knownKeys containsObject:key]) + { + CLY_LOG_W(@"%s, Unknown config key '%@', removing", __FUNCTION__, key); + [dictionary removeObjectForKey:key]; + } + } + + [self setBoolProperty:&_trackingEnabled fromDictionary:dictionary key:kTracking logString:logString]; + [self setBoolProperty:&_networkingEnabled fromDictionary:dictionary key:kNetworking logString:logString]; + [self setIntegerProperty:&_sessionInterval fromDictionary:dictionary key:kRSessionUpdateInterval logString:logString]; + [self setIntegerProperty:&_requestQueueSize fromDictionary:dictionary key:kRReqQueueSize logString:logString]; + [self setIntegerProperty:&_eventQueueSize fromDictionary:dictionary key:kREventQueueSize logString:logString]; + [self setBoolProperty:&_crashReportingEnabled fromDictionary:dictionary key:kRCrashReporting logString:logString]; + [self setBoolProperty:&_sessionTrackingEnabled fromDictionary:dictionary key:kRSessionTracking logString:logString]; + [self setBoolProperty:&_loggingEnabled fromDictionary:dictionary key:kRLogging logString:logString]; + [self setIntegerProperty:&_limitKeyLength fromDictionary:dictionary key:kRLimitKeyLength logString:logString]; + [self setIntegerProperty:&_limitValueSize fromDictionary:dictionary key:kRLimitValueSize logString:logString]; + [self setIntegerProperty:&_limitSegValues fromDictionary:dictionary key:kRLimitSegValues logString:logString]; + [self setIntegerProperty:&_limitBreadcrumb fromDictionary:dictionary key:kRLimitBreadcrumb logString:logString]; + [self setIntegerProperty:&_limitTraceLine fromDictionary:dictionary key:kRLimitTraceLine logString:logString]; + [self setIntegerProperty:&_limitTraceLength fromDictionary:dictionary key:kRLimitTraceLength logString:logString]; + [self setBoolProperty:&_customEventTrackingEnabled fromDictionary:dictionary key:kRCustomEventTracking logString:logString]; + [self setBoolProperty:&_viewTrackingEnabled fromDictionary:dictionary key:kRViewTracking logString:logString]; + [self setBoolProperty:&_enterContentZone fromDictionary:dictionary key:kREnterContentZone logString:logString]; + [self setIntegerProperty:&_contentZoneInterval fromDictionary:dictionary key:kRContentZoneInterval minValue:16 logString:logString]; + [self setBoolProperty:&_consentRequired fromDictionary:dictionary key:kRConsentRequired logString:logString]; + [self setIntegerProperty:&_dropOldRequestTime fromDictionary:dictionary key:kRDropOldRequestTime minValue:0 logString:logString]; + [self setIntegerProperty:&_serverConfigUpdateInterval fromDictionary:dictionary key:kRServerConfigUpdateInterval logString:logString]; + [self setBoolProperty:&_locationTracking fromDictionary:dictionary key:kRLocationTracking logString:logString]; + [self setBoolProperty:&_refreshContentZone fromDictionary:dictionary key:kRRefreshContentZone logString:logString]; + [self setBoolProperty:&_backoffMechanism fromDictionary:dictionary key:kRbackoffMechanism logString:logString]; + [self setIntegerProperty:&_bomAcceptedTimeoutSeconds fromDictionary:dictionary key:kRBOMAcceptedTimeout logString:logString]; + [self setDoubleProperty:&_bomRQPercentage fromDictionary:dictionary key:kRBOMRQPercentage logString:logString]; + [self setIntegerProperty:&_bomRequestAge fromDictionary:dictionary key:kRBOMRequestAge logString:logString]; + [self setIntegerProperty:&_bomDuration fromDictionary:dictionary key:kRBOMDuration logString:logString]; + [self setIntegerProperty:&_userPropertyCacheLimit fromDictionary:dictionary key:kRUserPropertyCacheLimit logString:logString]; + + [self updateListingFilters:dictionary logString:logString]; + + // Update the config dictionary with cleaned values + serverConfig[kRConfig] = dictionary; + + if(![logString isEqualToString: @"Server Config: "]){ + // means new config gotten, if that is the case notify SDK + [self notifySdkConfigChange: config]; } - } - - [self setBoolProperty:&_trackingEnabled fromDictionary:dictionary key:kTracking logString:logString]; - [self setBoolProperty:&_networkingEnabled fromDictionary:dictionary key:kNetworking logString:logString]; - [self setIntegerProperty:&_sessionInterval fromDictionary:dictionary key:kRSessionUpdateInterval logString:logString]; - [self setIntegerProperty:&_requestQueueSize fromDictionary:dictionary key:kRReqQueueSize logString:logString]; - [self setIntegerProperty:&_eventQueueSize fromDictionary:dictionary key:kREventQueueSize logString:logString]; - [self setBoolProperty:&_crashReportingEnabled fromDictionary:dictionary key:kRCrashReporting logString:logString]; - [self setBoolProperty:&_sessionTrackingEnabled fromDictionary:dictionary key:kRSessionTracking logString:logString]; - [self setBoolProperty:&_loggingEnabled fromDictionary:dictionary key:kRLogging logString:logString]; - [self setIntegerProperty:&_limitKeyLength fromDictionary:dictionary key:kRLimitKeyLength logString:logString]; - [self setIntegerProperty:&_limitValueSize fromDictionary:dictionary key:kRLimitValueSize logString:logString]; - [self setIntegerProperty:&_limitSegValues fromDictionary:dictionary key:kRLimitSegValues logString:logString]; - [self setIntegerProperty:&_limitBreadcrumb fromDictionary:dictionary key:kRLimitBreadcrumb logString:logString]; - [self setIntegerProperty:&_limitTraceLine fromDictionary:dictionary key:kRLimitTraceLine logString:logString]; - [self setIntegerProperty:&_limitTraceLength fromDictionary:dictionary key:kRLimitTraceLength logString:logString]; - [self setBoolProperty:&_customEventTrackingEnabled fromDictionary:dictionary key:kRCustomEventTracking logString:logString]; - [self setBoolProperty:&_viewTrackingEnabled fromDictionary:dictionary key:kRViewTracking logString:logString]; - [self setBoolProperty:&_enterContentZone fromDictionary:dictionary key:kREnterContentZone logString:logString]; - [self setIntegerProperty:&_contentZoneInterval fromDictionary:dictionary key:kRContentZoneInterval minValue:16 logString:logString]; - [self setBoolProperty:&_consentRequired fromDictionary:dictionary key:kRConsentRequired logString:logString]; - [self setIntegerProperty:&_dropOldRequestTime fromDictionary:dictionary key:kRDropOldRequestTime minValue:0 logString:logString]; - [self setIntegerProperty:&_serverConfigUpdateInterval fromDictionary:dictionary key:kRServerConfigUpdateInterval logString:logString]; - [self setBoolProperty:&_locationTracking fromDictionary:dictionary key:kRLocationTracking logString:logString]; - [self setBoolProperty:&_refreshContentZone fromDictionary:dictionary key:kRRefreshContentZone logString:logString]; - [self setBoolProperty:&_backoffMechanism fromDictionary:dictionary key:kRbackoffMechanism logString:logString]; - [self setIntegerProperty:&_bomAcceptedTimeoutSeconds fromDictionary:dictionary key:kRBOMAcceptedTimeout logString:logString]; - [self setDoubleProperty:&_bomRQPercentage fromDictionary:dictionary key:kRBOMRQPercentage logString:logString]; - [self setIntegerProperty:&_bomRequestAge fromDictionary:dictionary key:kRBOMRequestAge logString:logString]; - [self setIntegerProperty:&_bomDuration fromDictionary:dictionary key:kRBOMDuration logString:logString]; - [self setIntegerProperty:&_userPropertyCacheLimit fromDictionary:dictionary key:kRUserPropertyCacheLimit logString:logString]; - - [self updateListingFilters:dictionary logString:logString]; - - // Update the config dictionary with cleaned values - serverConfig[kRConfig] = dictionary; - - if (![logString isEqualToString:@"Server Config: "]) - { - // means new config gotten, if that is the case notify SDK - [self notifySdkConfigChange:config]; - } - - CLY_LOG_D(@"%s, version:[%li], timestamp:[%lli], %@", __FUNCTION__, _version, _timestamp, logString); + + CLY_LOG_D(@"%s, version:[%li], timestamp:[%lli], %@", __FUNCTION__, _version, _timestamp, logString); } - (void)notifySdkConfigChange:(CountlyConfig *)config { - config.enableDebug = _loggingEnabled || config.enableDebug; - CountlyCommon.sharedInstance.enableDebug = config.enableDebug; - - // Limits could be moved to another function, but letting them stay here serves us a monopolized view of notify - if (config.maxKeyLength != kCountlyMaxKeyLength) - { - [config.sdkInternalLimits setMaxKeyLength:config.maxKeyLength]; - } - - if (config.maxValueLength != kCountlyMaxValueSize) - { - [config.sdkInternalLimits setMaxValueSize:config.maxValueLength]; - } - - if (config.maxSegmentationValues != kCountlyMaxSegmentationValues) - { - [config.sdkInternalLimits setMaxSegmentationValues:config.maxSegmentationValues]; - } - - if (config.crashLogLimit != kCountlyMaxBreadcrumbCount) - { - [config.sdkInternalLimits setMaxBreadcrumbCount:config.crashLogLimit]; - } - - [config.sdkInternalLimits setMaxKeyLength:_limitKeyLength ?: config.sdkInternalLimits.getMaxKeyLength]; - [config.sdkInternalLimits setMaxValueSize:_limitValueSize ?: config.sdkInternalLimits.getMaxValueSize]; - [config.sdkInternalLimits setMaxSegmentationValues:_limitSegValues ?: config.sdkInternalLimits.getMaxSegmentationValues]; - [config.sdkInternalLimits setMaxBreadcrumbCount:_limitBreadcrumb ?: config.sdkInternalLimits.getMaxBreadcrumbCount]; - [config.sdkInternalLimits setMaxStackTraceLineLength:_limitTraceLength ?: config.sdkInternalLimits.getMaxStackTraceLineLength]; - [config.sdkInternalLimits setMaxStackTraceLinesPerThread:_limitTraceLine ?: config.sdkInternalLimits.getMaxStackTraceLinesPerThread]; - - CountlyCommon.sharedInstance.maxKeyLength = config.sdkInternalLimits.getMaxKeyLength; - CountlyCommon.sharedInstance.maxValueLength = config.sdkInternalLimits.getMaxValueSize; - CountlyCommon.sharedInstance.maxSegmentationValues = config.sdkInternalLimits.getMaxSegmentationValues; - - config.eventSendThreshold = _eventQueueSize ?: config.eventSendThreshold; - config.requestDropAgeHours = _dropOldRequestTime ?: config.requestDropAgeHours; - config.storedRequestsLimit = _requestQueueSize ?: config.storedRequestsLimit; - CountlyPersistency.sharedInstance.eventSendThreshold = config.eventSendThreshold; - CountlyPersistency.sharedInstance.requestDropAgeHours = config.requestDropAgeHours; - CountlyPersistency.sharedInstance.storedRequestsLimit = MAX(1, config.storedRequestsLimit); - - config.updateSessionPeriod = _sessionInterval ?: config.updateSessionPeriod; - _sessionInterval = config.updateSessionPeriod; - - BOOL consentDidChange = !config.requiresConsent && _consentRequired; - config.requiresConsent = _consentRequired ?: config.requiresConsent; - CountlyConsentManager.sharedInstance.requiresConsent = config.requiresConsent; - if (consentDidChange && CountlyCommon.sharedInstance.hasFinishedInit) - { - [CountlyConsentManager.sharedInstance sendConsents]; - if (!CountlyConsentManager.sharedInstance.consentForLocation) + config.enableDebug = _loggingEnabled || config.enableDebug; + CountlyCommon.sharedInstance.enableDebug = config.enableDebug; + + // Limits could be moved to another function, but letting them stay here serves us a monopolized view of notify + if (config.maxKeyLength != kCountlyMaxKeyLength) + { + [config.sdkInternalLimits setMaxKeyLength:config.maxKeyLength]; + } + + if (config.maxValueLength != kCountlyMaxValueSize) + { + [config.sdkInternalLimits setMaxValueSize:config.maxValueLength]; + } + + if (config.maxSegmentationValues != kCountlyMaxSegmentationValues) { - [CountlyConnectionManager.sharedInstance sendLocationInfo]; + [config.sdkInternalLimits setMaxSegmentationValues:config.maxSegmentationValues]; + } + + if (config.crashLogLimit != kCountlyMaxBreadcrumbCount) + { + [config.sdkInternalLimits setMaxBreadcrumbCount:config.crashLogLimit]; + } + + [config.sdkInternalLimits setMaxKeyLength:_limitKeyLength ?: config.sdkInternalLimits.getMaxKeyLength]; + [config.sdkInternalLimits setMaxValueSize:_limitValueSize ?: config.sdkInternalLimits.getMaxValueSize]; + [config.sdkInternalLimits setMaxSegmentationValues:_limitSegValues ?: config.sdkInternalLimits.getMaxSegmentationValues]; + [config.sdkInternalLimits setMaxBreadcrumbCount:_limitBreadcrumb ?: config.sdkInternalLimits.getMaxBreadcrumbCount]; + [config.sdkInternalLimits setMaxStackTraceLineLength:_limitTraceLength ?: config.sdkInternalLimits.getMaxStackTraceLineLength]; + [config.sdkInternalLimits setMaxStackTraceLinesPerThread:_limitTraceLine ?: config.sdkInternalLimits.getMaxStackTraceLinesPerThread]; + + CountlyCommon.sharedInstance.maxKeyLength = config.sdkInternalLimits.getMaxKeyLength; + CountlyCommon.sharedInstance.maxValueLength = config.sdkInternalLimits.getMaxValueSize; + CountlyCommon.sharedInstance.maxSegmentationValues = config.sdkInternalLimits.getMaxSegmentationValues; + + config.eventSendThreshold = _eventQueueSize ?: config.eventSendThreshold; + config.requestDropAgeHours = _dropOldRequestTime ?: config.requestDropAgeHours; + config.storedRequestsLimit = _requestQueueSize ?: config.storedRequestsLimit; + CountlyPersistency.sharedInstance.eventSendThreshold = config.eventSendThreshold; + CountlyPersistency.sharedInstance.requestDropAgeHours = config.requestDropAgeHours; + CountlyPersistency.sharedInstance.storedRequestsLimit = MAX(1, config.storedRequestsLimit); + + config.updateSessionPeriod = _sessionInterval ?: config.updateSessionPeriod; + _sessionInterval = config.updateSessionPeriod; + + BOOL consentDidChange = !config.requiresConsent && _consentRequired; + config.requiresConsent = _consentRequired ?: config.requiresConsent; + CountlyConsentManager.sharedInstance.requiresConsent = config.requiresConsent; + if(consentDidChange && CountlyCommon.sharedInstance.hasFinishedInit){ + [CountlyConsentManager.sharedInstance sendConsents]; + if (!CountlyConsentManager.sharedInstance.consentForLocation) + { + [CountlyConnectionManager.sharedInstance sendLocationInfo]; + } } - } #if (TARGET_OS_IOS) - [config.content setZoneTimerInterval:_contentZoneInterval ?: config.content.getZoneTimerInterval]; - if (config.content.getZoneTimerInterval) - { - CountlyContentBuilderInternal.sharedInstance.zoneTimerInterval = config.content.getZoneTimerInterval; - } - if (!_enterContentZone) - { - dispatch_async(dispatch_get_main_queue(), ^{ - [CountlyContentBuilderInternal.sharedInstance exitContentZone]; - }); - } - else - { - dispatch_async(dispatch_get_main_queue(), ^{ - [CountlyContentBuilderInternal.sharedInstance exitContentZone]; - [CountlyContentBuilderInternal.sharedInstance enterContentZone:@[]]; - }); - } + [config.content setZoneTimerInterval:_contentZoneInterval ?: config.content.getZoneTimerInterval]; + if (config.content.getZoneTimerInterval) + { + CountlyContentBuilderInternal.sharedInstance.zoneTimerInterval = config.content.getZoneTimerInterval; + } + if (!_enterContentZone) + { + dispatch_async(dispatch_get_main_queue(), ^{ + [CountlyContentBuilderInternal.sharedInstance exitContentZone]; + }); + } + else + { + dispatch_async(dispatch_get_main_queue(), ^{ + [CountlyContentBuilderInternal.sharedInstance exitContentZone]; + [CountlyContentBuilderInternal.sharedInstance enterContentZone:@[]]; + }); + } #endif - CountlyCrashReporter.sharedInstance.crashLogLimit = config.sdkInternalLimits.getMaxBreadcrumbCount; - - if (_serverConfigUpdateInterval && _serverConfigUpdateInterval != _currentServerConfigUpdateInterval && _requestTimer) - { - _currentServerConfigUpdateInterval = _serverConfigUpdateInterval; - [_requestTimer invalidate]; - _requestTimer = nil; - _requestTimer = [NSTimer timerWithTimeInterval:_currentServerConfigUpdateInterval * 60 * 60 target:self selector:@selector(fetchServerConfigTimer:) userInfo:config repeats:YES]; - [NSRunLoop.mainRunLoop addTimer:_requestTimer forMode:NSRunLoopCommonModes]; - } - - if (!_locationTracking && !CountlyLocationManager.sharedInstance.isLocationInfoDisabled) - { - [CountlyLocationManager.sharedInstance disableLocation]; - [CountlyConnectionManager.sharedInstance sendLocationInfo]; - } - - if (_backoffMechanism && config.disableBackoffMechanism) - { - _backoffMechanism = NO; - } + CountlyCrashReporter.sharedInstance.crashLogLimit = config.sdkInternalLimits.getMaxBreadcrumbCount; + + if (_serverConfigUpdateInterval && _serverConfigUpdateInterval != _currentServerConfigUpdateInterval && _requestTimer) + { + _currentServerConfigUpdateInterval = _serverConfigUpdateInterval; + [_requestTimer invalidate]; + _requestTimer = nil; + _requestTimer = [NSTimer timerWithTimeInterval:_currentServerConfigUpdateInterval * 60 * 60 target:self selector:@selector(fetchServerConfigTimer:) userInfo:config repeats:YES]; + [NSRunLoop.mainRunLoop addTimer:_requestTimer forMode:NSRunLoopCommonModes]; + } + + if (!_locationTracking && !CountlyLocationManager.sharedInstance.isLocationInfoDisabled) + { + [CountlyLocationManager.sharedInstance disableLocation]; + [CountlyConnectionManager.sharedInstance sendLocationInfo]; + } + + if(_backoffMechanism && config.disableBackoffMechanism){ + _backoffMechanism = NO; + } } - (void)fetchServerConfigTimer:(NSTimer *)timer { - CountlyConfig *config = (CountlyConfig *)timer.userInfo; // Retrieve CountlyConfig from userInfo - if (config) - { - [self fetchServerConfig:config]; - } + CountlyConfig *config = (CountlyConfig *)timer.userInfo; // Retrieve CountlyConfig from userInfo + if (config) + { + [self fetchServerConfig:config]; + } } - (void)fetchServerConfigIfTimeIsUp { - if (_serverConfigUpdatesDisabled) - { - return; - } - - if (_lastFetchTimestamp) - { - long long currentTime = NSDate.date.timeIntervalSince1970 * 1000; - long long timePassed = currentTime - _lastFetchTimestamp; - - if (timePassed > _currentServerConfigUpdateInterval * 60 * 60 * 1000) + if (_serverConfigUpdatesDisabled) { + return; + } + + if (_lastFetchTimestamp) { - [self fetchServerConfig:CountlyConfig.new]; + long long currentTime = NSDate.date.timeIntervalSince1970 * 1000; + long long timePassed = currentTime - _lastFetchTimestamp; + + if (timePassed > _currentServerConfigUpdateInterval * 60 * 60 * 1000) + { + [self fetchServerConfig:CountlyConfig.new]; + } } - } } - (void)fetchServerConfig:(CountlyConfig *)config { - CLY_LOG_D(@"%s, fetching sdk behavior settings", __FUNCTION__); + CLY_LOG_D(@"%s, fetching sdk behavior settings", __FUNCTION__); + + if (_serverConfigUpdatesDisabled) { + CLY_LOG_D(@"%s, sdk behavior settings updates disabled, omitting fetch", __FUNCTION__); + return; + } + + if (CountlyDeviceInfo.sharedInstance.isDeviceIDTemporary) + return; + + _lastFetchTimestamp = NSDate.date.timeIntervalSince1970 * 1000; - if (_serverConfigUpdatesDisabled) - { - CLY_LOG_D(@"%s, sdk behavior settings updates disabled, omitting fetch", __FUNCTION__); - return; - } + if (!_requestTimer) + { + _requestTimer = [NSTimer timerWithTimeInterval:_currentServerConfigUpdateInterval * 60 * 60 target:self selector:@selector(fetchServerConfigTimer:) userInfo:config repeats:YES]; + [NSRunLoop.mainRunLoop addTimer:_requestTimer forMode:NSRunLoopCommonModes]; + } - if (CountlyDeviceInfo.sharedInstance.isDeviceIDTemporary) - return; + id handler = ^(NSData *data, NSURLResponse *response, NSError *error) { + NSDictionary *serverConfigResponse = nil; + if (!error) + { + serverConfigResponse = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error]; + CLY_LOG_D(@"Server Config Fetched: %@", serverConfigResponse.description); + } + + if (!error) + { + if (((NSHTTPURLResponse *)response).statusCode != 200) + { + NSMutableDictionary *serverConfig = serverConfigResponse.mutableCopy; + serverConfig[NSLocalizedDescriptionKey] = @"Server configuration general API error"; + error = [NSError errorWithDomain:kCountlyErrorDomain code:CLYErrorServerConfigGeneralAPIError userInfo:serverConfig]; + } + } + + if (error) + { + CLY_LOG_E(@"Error while fetching server configs: %@", error.description); + } + + if (serverConfigResponse[kRConfig] != nil) + { + NSMutableDictionary *persistentBehaviorSettings = [CountlyPersistency.sharedInstance retrieveServerConfig]; + [self mergeBehaviorSettings:persistentBehaviorSettings withConfig:serverConfigResponse]; + [self populateServerConfig:persistentBehaviorSettings withConfig:config]; + [CountlyPersistency.sharedInstance storeServerConfig:persistentBehaviorSettings]; + } + }; + // Set default values + NSURLSessionTask *task = [CountlyCommon.sharedInstance.URLSession dataTaskWithRequest:[self serverConfigRequest] completionHandler:handler]; + [task resume]; +} - _lastFetchTimestamp = NSDate.date.timeIntervalSince1970 * 1000; +- (NSURLRequest *)serverConfigRequest +{ + NSString *queryString = [NSString stringWithFormat:@"%@=%@&%@=%@&%@=%@&%@=%@&%@=%@", kCountlyQSKeyMethod, kCountlySCKeySC, kCountlyQSKeyAppKey, CountlyConnectionManager.sharedInstance.appKey.cly_URLEscaped, kCountlyQSKeyDeviceID, CountlyDeviceInfo.sharedInstance.deviceID.cly_URLEscaped, + kCountlyQSKeySDKName, CountlyCommon.sharedInstance.SDKName, kCountlyQSKeySDKVersion, CountlyCommon.sharedInstance.SDKVersion]; - if (!_requestTimer) - { - _requestTimer = [NSTimer timerWithTimeInterval:_currentServerConfigUpdateInterval * 60 * 60 target:self selector:@selector(fetchServerConfigTimer:) userInfo:config repeats:YES]; - [NSRunLoop.mainRunLoop addTimer:_requestTimer forMode:NSRunLoopCommonModes]; - } + queryString = [queryString stringByAppendingFormat:@"&%@=%@", kCountlyAppVersionKey, CountlyDeviceInfo.appVersion]; - id handler = ^(NSData *data, NSURLResponse *response, NSError *error) { - NSDictionary *serverConfigResponse = nil; - if (!error) - { - serverConfigResponse = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error]; - CLY_LOG_D(@"Server Config Fetched: %@", serverConfigResponse.description); - } + queryString = [CountlyConnectionManager.sharedInstance appendChecksum:queryString]; - if (!error) - { - if (((NSHTTPURLResponse *)response).statusCode != 200) - { - NSMutableDictionary *serverConfig = serverConfigResponse.mutableCopy; - serverConfig[NSLocalizedDescriptionKey] = @"Server configuration general API error"; - error = [NSError errorWithDomain:kCountlyErrorDomain code:CLYErrorServerConfigGeneralAPIError userInfo:serverConfig]; - } - } + NSMutableString *URL = CountlyConnectionManager.sharedInstance.host.mutableCopy; + [URL appendString:kCountlyEndpointO]; + [URL appendString:kCountlyEndpointSDK]; - if (error) + if (queryString.length > kCountlyGETRequestMaxLength || CountlyConnectionManager.sharedInstance.alwaysUsePOST) { - CLY_LOG_E(@"Error while fetching server configs: %@", error.description); + NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:URL]]; + request.HTTPMethod = @"POST"; + request.HTTPBody = [queryString cly_dataUTF8]; + return request.copy; } - - if (serverConfigResponse[kRConfig] != nil) + else { - NSMutableDictionary *persistentBehaviorSettings = [CountlyPersistency.sharedInstance retrieveServerConfig]; - [self mergeBehaviorSettings:persistentBehaviorSettings withConfig:serverConfigResponse]; - [self populateServerConfig:persistentBehaviorSettings withConfig:config]; - [CountlyPersistency.sharedInstance storeServerConfig:persistentBehaviorSettings]; + [URL appendFormat:@"?%@", queryString]; + NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:URL]]; + return request; } - }; - // Set default values - NSURLSessionTask *task = [CountlyCommon.sharedInstance.URLSession dataTaskWithRequest:[self serverConfigRequest] completionHandler:handler]; - [task resume]; -} -- (NSURLRequest *)serverConfigRequest -{ - NSString *queryString = [NSString stringWithFormat:@"%@=%@&%@=%@&%@=%@&%@=%@&%@=%@", kCountlyQSKeyMethod, kCountlySCKeySC, kCountlyQSKeyAppKey, CountlyConnectionManager.sharedInstance.appKey.cly_URLEscaped, kCountlyQSKeyDeviceID, CountlyDeviceInfo.sharedInstance.deviceID.cly_URLEscaped, - kCountlyQSKeySDKName, CountlyCommon.sharedInstance.SDKName, kCountlyQSKeySDKVersion, CountlyCommon.sharedInstance.SDKVersion]; - - queryString = [queryString stringByAppendingFormat:@"&%@=%@", kCountlyAppVersionKey, CountlyDeviceInfo.appVersion]; - - queryString = [CountlyConnectionManager.sharedInstance appendChecksum:queryString]; - - NSMutableString *URL = CountlyConnectionManager.sharedInstance.host.mutableCopy; - [URL appendString:kCountlyEndpointO]; - [URL appendString:kCountlyEndpointSDK]; - - if (queryString.length > kCountlyGETRequestMaxLength || CountlyConnectionManager.sharedInstance.alwaysUsePOST) - { - NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:URL]]; - request.HTTPMethod = @"POST"; - request.HTTPBody = [queryString cly_dataUTF8]; - return request.copy; - } - else - { - [URL appendFormat:@"?%@", queryString]; - NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:URL]]; - return request; - } - - CLY_LOG_D(@"serverConfigRequest URL :%@", URL); + CLY_LOG_D(@"serverConfigRequest URL :%@", URL); } -- (void)setDefaultValues -{ - _trackingEnabled = YES; - _networkingEnabled = YES; - _crashReportingEnabled = YES; - _customEventTrackingEnabled = YES; - _enterContentZone = NO; - _locationTracking = YES; - _viewTrackingEnabled = YES; - _sessionTrackingEnabled = YES; - _loggingEnabled = NO; - _refreshContentZone = YES; - _backoffMechanism = YES; - _bomAcceptedTimeoutSeconds = 10; - _bomRQPercentage = 0.5; - _bomRequestAge = 24; - _bomDuration = 60; - - _eventQueueSize = 0; - _requestQueueSize = 0; - _sessionInterval = 0; - _limitKeyLength = 0; - _limitValueSize = 0; - _limitSegValues = 0; - _limitBreadcrumb = 0; - _limitTraceLine = 0; - _limitTraceLength = 0; - _consentRequired = NO; - _dropOldRequestTime = 0; - _contentZoneInterval = 0; - - _eventFilterSet = [NSSet set]; - _eventFilterIsWhitelist = NO; - _userPropertyFilterSet = [NSSet set]; - _userPropertyFilterIsWhitelist = NO; - _userPropertyCacheLimit = 100; - _segmentationFilterSet = [NSSet set]; - _segmentationFilterIsWhitelist = NO; - _eventSegmentationFilterMap = @{}; - _eventSegmentationFilterIsWhitelist = NO; - _journeyTriggerEvents = [NSSet set]; +- (void)setDefaultValues { + _trackingEnabled = YES; + _networkingEnabled = YES; + _crashReportingEnabled = YES; + _customEventTrackingEnabled = YES; + _enterContentZone = NO; + _locationTracking = YES; + _viewTrackingEnabled = YES; + _sessionTrackingEnabled = YES; + _loggingEnabled = NO; + _refreshContentZone = YES; + _backoffMechanism = YES; + _bomAcceptedTimeoutSeconds = 10; + _bomRQPercentage = 0.5; + _bomRequestAge = 24; + _bomDuration = 60; + + _eventQueueSize = 0; + _requestQueueSize = 0; + _sessionInterval = 0; + _limitKeyLength = 0; + _limitValueSize = 0; + _limitSegValues = 0; + _limitBreadcrumb = 0; + _limitTraceLine = 0; + _limitTraceLength = 0; + _consentRequired = NO; + _dropOldRequestTime = 0; + _contentZoneInterval = 0; + + _eventFilterSet = [NSSet set]; + _eventFilterIsWhitelist = NO; + _userPropertyFilterSet = [NSSet set]; + _userPropertyFilterIsWhitelist = NO; + _userPropertyCacheLimit = 100; + _segmentationFilterSet = [NSSet set]; + _segmentationFilterIsWhitelist = NO; + _eventSegmentationFilterMap = @{}; + _eventSegmentationFilterIsWhitelist = NO; + _journeyTriggerEvents = [NSSet set]; } -- (void)disableSDKBehaviourSettings -{ - _serverConfigUpdatesDisabled = YES; +- (void)disableSDKBehaviourSettings { + _serverConfigUpdatesDisabled = YES; } - (BOOL)trackingEnabled { - return _trackingEnabled; + return _trackingEnabled; } - (BOOL)networkingEnabled { - return _networkingEnabled; + return _networkingEnabled; } - (NSInteger)sessionInterval { - return _sessionInterval; + return _sessionInterval; } - (NSInteger)requestQueueSize { - return _requestQueueSize; + return _requestQueueSize; } - (NSInteger)eventQueueSize { - return _eventQueueSize; + return _eventQueueSize; } - (BOOL)crashReportingEnabled { - return _crashReportingEnabled; + return _crashReportingEnabled; } - (BOOL)sessionTrackingEnabled { - return _sessionTrackingEnabled; + return _sessionTrackingEnabled; } - (BOOL)loggingEnabled { - return _loggingEnabled; + return _loggingEnabled; } - (NSInteger)limitKeyLength { - return _limitKeyLength; + return _limitKeyLength; } - (NSInteger)limitValueSize { - return _limitValueSize; + return _limitValueSize; } - (NSInteger)limitSegValues { - return _limitSegValues; + return _limitSegValues; } - (NSInteger)limitBreadcrumb { - return _limitBreadcrumb; + return _limitBreadcrumb; } - (NSInteger)limitTraceLine { - return _limitTraceLine; + return _limitTraceLine; } - (NSInteger)limitTraceLength { - return _limitTraceLength; + return _limitTraceLength; } - (BOOL)customEventTrackingEnabled { - return _customEventTrackingEnabled; + return _customEventTrackingEnabled; } - (BOOL)viewTrackingEnabled { - return _viewTrackingEnabled; + return _viewTrackingEnabled; } - (BOOL)enterContentZone { - return _enterContentZone; + return _enterContentZone; } - (NSInteger)contentZoneInterval { - return _contentZoneInterval; + return _contentZoneInterval; } - (BOOL)consentRequired { - return _consentRequired; + return _consentRequired; } - (NSInteger)dropOldRequestTime { - return _dropOldRequestTime; + return _dropOldRequestTime; } - (BOOL)locationTrackingEnabled { - return _locationTracking; + return _locationTracking; } - (BOOL)refreshContentZoneEnabled { - return _refreshContentZone; + return _refreshContentZone; } - (BOOL)backoffMechanism { - return _backoffMechanism; + return _backoffMechanism; } - (NSInteger)bomAcceptedTimeoutSeconds { - return _bomAcceptedTimeoutSeconds; + return _bomAcceptedTimeoutSeconds; } - (double)bomRQPercentage { - return _bomRQPercentage; + return _bomRQPercentage; } - (NSInteger)bomRequestAge { - return _bomRequestAge; + return _bomRequestAge; } - (NSInteger)bomDuration { - return _bomDuration; + return _bomDuration; } - (NSInteger)requestTimeoutDuration { - return _requestTimeoutDuration; + return _requestTimeoutDuration; } - (NSInteger)userPropertyCacheLimit { - return _userPropertyCacheLimit; + return _userPropertyCacheLimit; } #pragma mark - Listing Filters - (void)removeConflictingFilterKeys:(NSMutableDictionary *)mergedConfig newConfig:(NSDictionary *)newConfig { - // Remove conflicting filter keys per category. - // Within each category, if new config provides a blacklist, remove stored whitelist (and vice versa). - NSArray *filterPairs = @[ - @[ kREventBlacklist, kREventWhitelist ], - @[ kRSegmentationBlacklist, kRSegmentationWhitelist ], - @[ kREventSegmentationBlacklist, kREventSegmentationWhitelist ], - @[ kRUserPropertyBlacklist, kRUserPropertyWhitelist ], - ]; - - for (NSArray *pair in filterPairs) - { - NSString *blacklistKey = pair[0]; - NSString *whitelistKey = pair[1]; - - // Only consider valid filter values (arrays/dicts) for conflict resolution - id blacklistVal = newConfig[blacklistKey]; - id whitelistVal = newConfig[whitelistKey]; - BOOL hasValidBlacklist = [blacklistVal isKindOfClass:NSArray.class] || [blacklistVal isKindOfClass:NSDictionary.class]; - BOOL hasValidWhitelist = [whitelistVal isKindOfClass:NSArray.class] || [whitelistVal isKindOfClass:NSDictionary.class]; - - if (hasValidBlacklist) - { - [mergedConfig removeObjectForKey:whitelistKey]; - } - if (hasValidWhitelist) + // Remove conflicting filter keys per category. + // Within each category, if new config provides a blacklist, remove stored whitelist (and vice versa). + NSArray *filterPairs = @[ + @[kREventBlacklist, kREventWhitelist], + @[kRSegmentationBlacklist, kRSegmentationWhitelist], + @[kREventSegmentationBlacklist, kREventSegmentationWhitelist], + @[kRUserPropertyBlacklist, kRUserPropertyWhitelist], + ]; + + for (NSArray *pair in filterPairs) { - [mergedConfig removeObjectForKey:blacklistKey]; + NSString *blacklistKey = pair[0]; + NSString *whitelistKey = pair[1]; + + // Only consider valid filter values (arrays/dicts) for conflict resolution + id blacklistVal = newConfig[blacklistKey]; + id whitelistVal = newConfig[whitelistKey]; + BOOL hasValidBlacklist = [blacklistVal isKindOfClass:NSArray.class] || [blacklistVal isKindOfClass:NSDictionary.class]; + BOOL hasValidWhitelist = [whitelistVal isKindOfClass:NSArray.class] || [whitelistVal isKindOfClass:NSDictionary.class]; + + if (hasValidBlacklist) + { + [mergedConfig removeObjectForKey:whitelistKey]; + } + if (hasValidWhitelist) + { + [mergedConfig removeObjectForKey:blacklistKey]; + } } - } } - (void)updateListingFilters:(NSMutableDictionary *)dictionary logString:(NSMutableString *)logString { - // Event filter (eb/ew) - blacklist takes precedence - NSArray *eb = dictionary[kREventBlacklist]; - NSArray *ew = dictionary[kREventWhitelist]; - if ([eb isKindOfClass:NSArray.class]) - { - _eventFilterSet = [NSSet setWithArray:eb]; - _eventFilterIsWhitelist = NO; - [logString appendFormat:@"%@: %@, ", kREventBlacklist, eb]; - if (ew) - [dictionary removeObjectForKey:kREventWhitelist]; // blacklist takes precedence - } - else if ([ew isKindOfClass:NSArray.class]) - { - _eventFilterSet = [NSSet setWithArray:ew]; - _eventFilterIsWhitelist = YES; - [logString appendFormat:@"%@: %@, ", kREventWhitelist, ew]; - } - else - { - if (eb) - [dictionary removeObjectForKey:kREventBlacklist]; - if (ew) - [dictionary removeObjectForKey:kREventWhitelist]; - } - - // User property filter (upb/upw) - blacklist takes precedence - NSArray *upb = dictionary[kRUserPropertyBlacklist]; - NSArray *upw = dictionary[kRUserPropertyWhitelist]; - if ([upb isKindOfClass:NSArray.class]) - { - _userPropertyFilterSet = [NSSet setWithArray:upb]; - _userPropertyFilterIsWhitelist = NO; - [logString appendFormat:@"%@: %@, ", kRUserPropertyBlacklist, upb]; - if (upw) - [dictionary removeObjectForKey:kRUserPropertyWhitelist]; - } - else if ([upw isKindOfClass:NSArray.class]) - { - _userPropertyFilterSet = [NSSet setWithArray:upw]; - _userPropertyFilterIsWhitelist = YES; - [logString appendFormat:@"%@: %@, ", kRUserPropertyWhitelist, upw]; - } - else - { - if (upb) - [dictionary removeObjectForKey:kRUserPropertyBlacklist]; - if (upw) - [dictionary removeObjectForKey:kRUserPropertyWhitelist]; - } - - // Segmentation filter (sb/sw) - blacklist takes precedence - NSArray *sb = dictionary[kRSegmentationBlacklist]; - NSArray *sw = dictionary[kRSegmentationWhitelist]; - if ([sb isKindOfClass:NSArray.class]) - { - _segmentationFilterSet = [NSSet setWithArray:sb]; - _segmentationFilterIsWhitelist = NO; - [logString appendFormat:@"%@: %@, ", kRSegmentationBlacklist, sb]; - if (sw) - [dictionary removeObjectForKey:kRSegmentationWhitelist]; - } - else if ([sw isKindOfClass:NSArray.class]) - { - _segmentationFilterSet = [NSSet setWithArray:sw]; - _segmentationFilterIsWhitelist = YES; - [logString appendFormat:@"%@: %@, ", kRSegmentationWhitelist, sw]; - } - else - { - if (sb) - [dictionary removeObjectForKey:kRSegmentationBlacklist]; - if (sw) - [dictionary removeObjectForKey:kRSegmentationWhitelist]; - } - - // Event segmentation filter (esb/esw) - blacklist takes precedence - NSDictionary *esb = dictionary[kREventSegmentationBlacklist]; - NSDictionary *esw = dictionary[kREventSegmentationWhitelist]; - if ([esb isKindOfClass:NSDictionary.class]) - { - NSMutableDictionary *map = NSMutableDictionary.new; - [esb enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSArray *obj, BOOL *stop) { - if ([obj isKindOfClass:NSArray.class]) - { - map[key] = [NSSet setWithArray:obj]; - } - }]; - _eventSegmentationFilterMap = map.copy; - _eventSegmentationFilterIsWhitelist = NO; - [logString appendFormat:@"%@: %@, ", kREventSegmentationBlacklist, esb]; - if (esw) - [dictionary removeObjectForKey:kREventSegmentationWhitelist]; - } - else if ([esw isKindOfClass:NSDictionary.class]) - { - NSMutableDictionary *map = NSMutableDictionary.new; - [esw enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSArray *obj, BOOL *stop) { - if ([obj isKindOfClass:NSArray.class]) - { - map[key] = [NSSet setWithArray:obj]; - } - }]; - _eventSegmentationFilterMap = map.copy; - _eventSegmentationFilterIsWhitelist = YES; - [logString appendFormat:@"%@: %@, ", kREventSegmentationWhitelist, esw]; - } - else - { - if (esb) - [dictionary removeObjectForKey:kREventSegmentationBlacklist]; - if (esw) - [dictionary removeObjectForKey:kREventSegmentationWhitelist]; - } - - // Journey trigger events (jte) - NSArray *jte = dictionary[kRJourneyTriggerEvents]; - if ([jte isKindOfClass:NSArray.class]) - { - _journeyTriggerEvents = [NSSet setWithArray:jte]; - [logString appendFormat:@"%@: %@, ", kRJourneyTriggerEvents, jte]; - } - else - { - if (jte) - [dictionary removeObjectForKey:kRJourneyTriggerEvents]; - } + // Event filter (eb/ew) - blacklist takes precedence + NSArray *eb = dictionary[kREventBlacklist]; + NSArray *ew = dictionary[kREventWhitelist]; + if ([eb isKindOfClass:NSArray.class]) { + _eventFilterSet = [NSSet setWithArray:eb]; + _eventFilterIsWhitelist = NO; + [logString appendFormat:@"%@: %@, ", kREventBlacklist, eb]; + if (ew) + [dictionary removeObjectForKey:kREventWhitelist]; // blacklist takes precedence + } else if ([ew isKindOfClass:NSArray.class]) { + _eventFilterSet = [NSSet setWithArray:ew]; + _eventFilterIsWhitelist = YES; + [logString appendFormat:@"%@: %@, ", kREventWhitelist, ew]; + } else { + if (eb) + [dictionary removeObjectForKey:kREventBlacklist]; + if (ew) + [dictionary removeObjectForKey:kREventWhitelist]; + } + + // User property filter (upb/upw) - blacklist takes precedence + NSArray *upb = dictionary[kRUserPropertyBlacklist]; + NSArray *upw = dictionary[kRUserPropertyWhitelist]; + if ([upb isKindOfClass:NSArray.class]) { + _userPropertyFilterSet = [NSSet setWithArray:upb]; + _userPropertyFilterIsWhitelist = NO; + [logString appendFormat:@"%@: %@, ", kRUserPropertyBlacklist, upb]; + if (upw) + [dictionary removeObjectForKey:kRUserPropertyWhitelist]; + } else if ([upw isKindOfClass:NSArray.class]) { + _userPropertyFilterSet = [NSSet setWithArray:upw]; + _userPropertyFilterIsWhitelist = YES; + [logString appendFormat:@"%@: %@, ", kRUserPropertyWhitelist, upw]; + } else { + if (upb) + [dictionary removeObjectForKey:kRUserPropertyBlacklist]; + if (upw) + [dictionary removeObjectForKey:kRUserPropertyWhitelist]; + } + + // Segmentation filter (sb/sw) - blacklist takes precedence + NSArray *sb = dictionary[kRSegmentationBlacklist]; + NSArray *sw = dictionary[kRSegmentationWhitelist]; + if ([sb isKindOfClass:NSArray.class]) { + _segmentationFilterSet = [NSSet setWithArray:sb]; + _segmentationFilterIsWhitelist = NO; + [logString appendFormat:@"%@: %@, ", kRSegmentationBlacklist, sb]; + if (sw) + [dictionary removeObjectForKey:kRSegmentationWhitelist]; + } else if ([sw isKindOfClass:NSArray.class]) { + _segmentationFilterSet = [NSSet setWithArray:sw]; + _segmentationFilterIsWhitelist = YES; + [logString appendFormat:@"%@: %@, ", kRSegmentationWhitelist, sw]; + } else { + if (sb) + [dictionary removeObjectForKey:kRSegmentationBlacklist]; + if (sw) + [dictionary removeObjectForKey:kRSegmentationWhitelist]; + } + + // Event segmentation filter (esb/esw) - blacklist takes precedence + NSDictionary *esb = dictionary[kREventSegmentationBlacklist]; + NSDictionary *esw = dictionary[kREventSegmentationWhitelist]; + if ([esb isKindOfClass:NSDictionary.class]) { + NSMutableDictionary *map = NSMutableDictionary.new; + [esb enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSArray *obj, BOOL *stop) { + if ([obj isKindOfClass:NSArray.class]) { + map[key] = [NSSet setWithArray:obj]; + } + }]; + _eventSegmentationFilterMap = map.copy; + _eventSegmentationFilterIsWhitelist = NO; + [logString appendFormat:@"%@: %@, ", kREventSegmentationBlacklist, esb]; + if (esw) + [dictionary removeObjectForKey:kREventSegmentationWhitelist]; + } else if ([esw isKindOfClass:NSDictionary.class]) { + NSMutableDictionary *map = NSMutableDictionary.new; + [esw enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSArray *obj, BOOL *stop) { + if ([obj isKindOfClass:NSArray.class]) { + map[key] = [NSSet setWithArray:obj]; + } + }]; + _eventSegmentationFilterMap = map.copy; + _eventSegmentationFilterIsWhitelist = YES; + [logString appendFormat:@"%@: %@, ", kREventSegmentationWhitelist, esw]; + } else { + if (esb) + [dictionary removeObjectForKey:kREventSegmentationBlacklist]; + if (esw) + [dictionary removeObjectForKey:kREventSegmentationWhitelist]; + } + + // Journey trigger events (jte) + NSArray *jte = dictionary[kRJourneyTriggerEvents]; + if ([jte isKindOfClass:NSArray.class]) { + _journeyTriggerEvents = [NSSet setWithArray:jte]; + [logString appendFormat:@"%@: %@, ", kRJourneyTriggerEvents, jte]; + } else { + if (jte) + [dictionary removeObjectForKey:kRJourneyTriggerEvents]; + } } - (BOOL)shouldRecordEvent:(NSString *)eventKey { - if (_eventFilterSet.count == 0) - return YES; - return _eventFilterIsWhitelist == [_eventFilterSet containsObject:eventKey]; + if (_eventFilterSet.count == 0) return YES; + return _eventFilterIsWhitelist == [_eventFilterSet containsObject:eventKey]; } - (BOOL)shouldRecordUserProperty:(NSString *)propertyKey { - if (_userPropertyFilterSet.count == 0) - return YES; - return _userPropertyFilterIsWhitelist == [_userPropertyFilterSet containsObject:propertyKey]; + if (_userPropertyFilterSet.count == 0) return YES; + return _userPropertyFilterIsWhitelist == [_userPropertyFilterSet containsObject:propertyKey]; } - (NSDictionary *)filterSegmentation:(NSDictionary *)segmentation eventKey:(NSString *)eventKey { - if (!segmentation) - { - return segmentation; - } - - BOOL hasGlobalFilter = _segmentationFilterSet.count > 0; - NSSet *eventFilter = _eventSegmentationFilterMap[eventKey]; - BOOL hasEventFilter = eventFilter.count > 0; - - if (!hasGlobalFilter && !hasEventFilter) - { - return segmentation; - } - - NSMutableDictionary *result = [segmentation mutableCopy]; - for (NSString *key in segmentation.allKeys) - { - if (hasGlobalFilter && _segmentationFilterIsWhitelist != [_segmentationFilterSet containsObject:key]) - { - CLY_LOG_D(@"Filtering out segmentation key '%@' by global segmentation filter", key); - [result removeObjectForKey:key]; + if (!segmentation) { + return segmentation; } - else if (hasEventFilter && _eventSegmentationFilterIsWhitelist != [eventFilter containsObject:key]) - { - CLY_LOG_D(@"Filtering out segmentation key '%@' for event '%@' by event segmentation filter", key, eventKey); - [result removeObjectForKey:key]; + + BOOL hasGlobalFilter = _segmentationFilterSet.count > 0; + NSSet *eventFilter = _eventSegmentationFilterMap[eventKey]; + BOOL hasEventFilter = eventFilter.count > 0; + + if (!hasGlobalFilter && !hasEventFilter) { + return segmentation; + } + + NSMutableDictionary *result = [segmentation mutableCopy]; + for (NSString *key in segmentation.allKeys) { + if (hasGlobalFilter && _segmentationFilterIsWhitelist != [_segmentationFilterSet containsObject:key]) { + CLY_LOG_D(@"Filtering out segmentation key '%@' by global segmentation filter", key); + [result removeObjectForKey:key]; + } + else if (hasEventFilter && _eventSegmentationFilterIsWhitelist != [eventFilter containsObject:key]) { + CLY_LOG_D(@"Filtering out segmentation key '%@' for event '%@' by event segmentation filter", key, eventKey); + [result removeObjectForKey:key]; + } } - } - return result.copy; + return result.copy; } - (BOOL)isJourneyTriggerEvent:(NSString *)eventKey { - return [_journeyTriggerEvents containsObject:eventKey]; + return [_journeyTriggerEvents containsObject:eventKey]; } @end From 9e2d4cd6d60775d898e32ae9f0ceff1c618c4058 Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Thu, 30 Apr 2026 12:50:42 +0300 Subject: [PATCH 16/42] feat: new user functions changelog --- CHANGELOG.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3453ab8c..6d512e04 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,19 @@ +## XX.XX.XX +* Added a new user properties functions on `CountlyUserDetails`: + * `setProperty:value:` for setting a single predefined or custom user property. + * `setProperties:` for setting multiple predefined and custom properties in one call. + * `clear` to drop queued user property changes without sending them to the server. + * Empty-string-as-clear semantics for predefined string fields (`name`, `username`, `email`, `organization`, `phone`, `gender`, `picture`, `picturePath`) — passing `@""` sends `null` to the server to clear the field. + * Negative-value-as-clear semantics for `byear` — passing a negative `NSNumber` sends `null` to clear the field. + * Pending events are flushed before the next user details request when a user property changes via this new functions, so they reach the server in the right order. +* Added `providedUserProperties` to `CountlyConfig` to set initial user properties that are applied and saved automatically right after `start`. +* Added `setMaxValueSizePicture:` to `CountlySDKLimitsConfig` to control the maximum size of picture URLs and picture paths independently of other value limits (default 4096). +* Improved `$push` / `$pull` / `$addToSet` wire format: values are now always sent as arrays so multiple consecutive calls on the same key accumulate correctly. + +* Deprecated the direct property setters on `CountlyUserDetails`: `name`, `username`, `email`, `organization`, `phone`, `gender`, `pictureURL`, `pictureLocalPath`, `birthYear`, `custom`. Use `setProperty:value:` or `setProperties:` instead. +* Deprecated `set:value:`, `set:numberValue:`, `set:boolValue:` on `CountlyUserDetails`. Use `setProperty:value:` instead. +* Deprecated `unSet:` on `CountlyUserDetails`. Use `setProperty:value:` with an empty string `@""` to clear a property on the server. + ## 26.1.1 * Added POST method support for contents. * Added robust resource loading checks before displaying content From 45a3aecc1b3a3f24629c9a84f2b5c87c8578e023 Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Thu, 30 Apr 2026 12:52:15 +0300 Subject: [PATCH 17/42] feat: provided user config --- Countly.m | 8 +++++++- CountlyConfig.h | 8 ++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/Countly.m b/Countly.m index 43710ceb..384dc9fb 100644 --- a/Countly.m +++ b/Countly.m @@ -111,6 +111,7 @@ - (void)startWithConfig:(CountlyConfig *)config CountlyCommon.sharedInstance.maxKeyLength = config.sdkInternalLimits.getMaxKeyLength; CountlyCommon.sharedInstance.maxValueLength = config.sdkInternalLimits.getMaxValueSize; + CountlyCommon.sharedInstance.maxValueLengthPicture = config.sdkInternalLimits.getMaxValueSizePicture; CountlyCommon.sharedInstance.maxSegmentationValues = config.sdkInternalLimits.getMaxSegmentationValues; // For backward compatibility, deprecated values are only set incase new values are not provided using sdkInternalLimits interface @@ -165,7 +166,12 @@ - (void)startWithConfig:(CountlyConfig *)config NSDictionary* customMetricsTruncated = [config.customMetrics cly_truncated:@"Custom metric"]; CountlyDeviceInfo.sharedInstance.customMetrics = [customMetricsTruncated cly_limited:@"Custom metric"]; - + + if (config.providedUserProperties.count > 0) { + CLY_LOG_I(@"%s applying providedUserProperties at init [%lu]", __FUNCTION__, (unsigned long)config.providedUserProperties.count); + [Countly.user setProperties:config.providedUserProperties]; + } + [Countly.user save]; // If something added related to server config, make sure to check CountlyServerConfig.notifySdkConfigChange [CountlyServerConfig.sharedInstance fetchServerConfig:config]; diff --git a/CountlyConfig.h b/CountlyConfig.h index 215586a5..f5b4f1cb 100644 --- a/CountlyConfig.h +++ b/CountlyConfig.h @@ -440,6 +440,14 @@ typedef enum : NSUInteger */ - (CountlySDKLimitsConfig *)sdkInternalLimits; +/** + * User properties to set automatically right after SDK init. + * @discussion Equivalent to calling @c [Countly.user setProperties:data] followed by @c save once the SDK has started. + * @discussion Keys matching predefined fields (name, username, email, organization, phone, gender, picture, picturePath, byear) + * are routed to the corresponding top-level user-detail field. All other keys are stored as custom user properties. + */ +@property (nonatomic, copy) NSDictionary * _Nullable providedUserProperties; + /** * For sending all requests using HTTP POST method. * @discussion If set, all requests will be sent using HTTP POST method. Otherwise; only the requests with a file upload or data size more than 2048 bytes will be sent using HTTP POST method. From f4681ae5fc59ddac91f60a424f79c5497de9de55 Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Thu, 30 Apr 2026 12:53:20 +0300 Subject: [PATCH 18/42] feat: picture max size config --- CountlySDKLimitsConfig.h | 3 +++ CountlySDKLimitsConfig.m | 15 ++++++++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/CountlySDKLimitsConfig.h b/CountlySDKLimitsConfig.h index fa3d77f1..74001772 100644 --- a/CountlySDKLimitsConfig.h +++ b/CountlySDKLimitsConfig.h @@ -9,6 +9,7 @@ extern const NSUInteger kCountlyMaxKeyLength; extern const NSUInteger kCountlyMaxValueSize; +extern const NSUInteger kCountlyMaxValueSizePicture; extern const NSUInteger kCountlyMaxBreadcrumbCount; extern const NSUInteger kCountlyMaxSegmentationValues; extern const NSUInteger kCountlyMaxStackTraceLineLength; @@ -19,6 +20,7 @@ extern const NSUInteger kCountlyMaxStackTraceLinesPerThread; - (void)setMaxKeyLength:(NSUInteger)maxKeyLength; - (void)setMaxValueSize:(NSUInteger)maxValueSize; +- (void)setMaxValueSizePicture:(NSUInteger)maxValueSizePicture; - (void)setMaxBreadcrumbCount:(NSUInteger)maxBreadcrumbCount; - (void)setMaxSegmentationValues:(NSUInteger)maxSegmentationValues; - (void)setMaxStackTraceLineLength:(NSUInteger)maxStackTraceLineLength; @@ -26,6 +28,7 @@ extern const NSUInteger kCountlyMaxStackTraceLinesPerThread; - (NSUInteger)getMaxKeyLength; - (NSUInteger)getMaxValueSize; +- (NSUInteger)getMaxValueSizePicture; - (NSUInteger)getMaxBreadcrumbCount; - (NSUInteger)getMaxSegmentationValues; - (NSUInteger)getMaxStackTraceLineLength; diff --git a/CountlySDKLimitsConfig.m b/CountlySDKLimitsConfig.m index 2a085e5c..725322de 100644 --- a/CountlySDKLimitsConfig.m +++ b/CountlySDKLimitsConfig.m @@ -10,6 +10,7 @@ const NSUInteger kCountlyMaxKeyLength = 128; const NSUInteger kCountlyMaxValueSize = 256; +const NSUInteger kCountlyMaxValueSizePicture = 4096; const NSUInteger kCountlyMaxSegmentationValues = 100; const NSUInteger kCountlyMaxBreadcrumbCount = 100; const NSUInteger kCountlyMaxStackTraceLinesPerThread = 30; @@ -19,6 +20,7 @@ @interface CountlySDKLimitsConfig () { NSUInteger _maxKeyLength; NSUInteger _maxValueSize; + NSUInteger _maxValueSizePicture; NSUInteger _maxSegmentationValues; NSUInteger _maxBreadcrumbCount; NSUInteger _maxStackTraceLinesPerThread; @@ -33,12 +35,13 @@ - (instancetype)init { _maxKeyLength = kCountlyMaxKeyLength; _maxValueSize = kCountlyMaxValueSize; + _maxValueSizePicture = kCountlyMaxValueSizePicture; _maxSegmentationValues = kCountlyMaxSegmentationValues; _maxBreadcrumbCount = kCountlyMaxBreadcrumbCount; _maxStackTraceLinesPerThread = kCountlyMaxStackTraceLinesPerThread; _maxStackTraceLineLength = kCountlyMaxStackTraceLineLength; } - + return self; } @@ -52,6 +55,11 @@ - (void)setMaxValueSize:(NSUInteger)maxValueSize _maxValueSize = maxValueSize; } +- (void)setMaxValueSizePicture:(NSUInteger)maxValueSizePicture +{ + _maxValueSizePicture = maxValueSizePicture; +} + - (void)setMaxSegmentationValues:(NSUInteger)maxSegmentationValues { _maxSegmentationValues = maxSegmentationValues; @@ -82,6 +90,11 @@ - (NSUInteger)getMaxValueSize return _maxValueSize; } +- (NSUInteger)getMaxValueSizePicture +{ + return _maxValueSizePicture; +} + - (NSUInteger)getMaxSegmentationValues { return _maxSegmentationValues; From a584d67893e08ae6bf6b608b4b225582987a9551 Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Thu, 30 Apr 2026 12:53:36 +0300 Subject: [PATCH 19/42] feat: picture max size config: impl --- CountlyCommon.h | 2 ++ CountlyCommon.m | 12 ++++++++++++ 2 files changed, 14 insertions(+) diff --git a/CountlyCommon.h b/CountlyCommon.h index 42b4e1ad..5fe39d16 100644 --- a/CountlyCommon.h +++ b/CountlyCommon.h @@ -95,6 +95,7 @@ extern NSString* const kCountlySDKName; @property (nonatomic) NSUInteger maxKeyLength; @property (nonatomic) NSUInteger maxValueLength; +@property (nonatomic) NSUInteger maxValueLengthPicture; @property (nonatomic) NSUInteger maxSegmentationValues; void CountlyInternalLog(CLYInternalLogLevel level, NSString *format, ...) NS_FORMAT_FUNCTION(2, 3); @@ -154,6 +155,7 @@ void CountlyPrint(NSString *stringToPrint); - (NSString *)cly_valueForQueryStringKey:(NSString *)key; - (NSString *)cly_truncatedKey:(NSString *)explanation; - (NSString *)cly_truncatedValue:(NSString *)explanation; +- (NSString *)cly_truncatedPictureValue:(NSString *)explanation; @end @interface NSArray (Countly) diff --git a/CountlyCommon.m b/CountlyCommon.m index ecfa47ef..b1f6bd44 100644 --- a/CountlyCommon.m +++ b/CountlyCommon.m @@ -72,6 +72,7 @@ - (void)resetInstance { _hasStarted = false; _maxKeyLength = kCountlyMaxKeyLength; _maxValueLength = kCountlyMaxValueSize; + _maxValueLengthPicture = kCountlyMaxValueSizePicture; _maxSegmentationValues = kCountlyMaxSegmentationValues; onceToken = 0; s_sharedInstance = nil; @@ -631,6 +632,17 @@ - (NSString *)cly_truncatedKey:(NSString *)explanation return self; } +- (NSString *)cly_truncatedPictureValue:(NSString *)explanation +{ + NSUInteger limit = CountlyCommon.sharedInstance.maxValueLengthPicture; + if (self.length > limit) + { + CLY_LOG_W(@"%@ length is more than the picture limit (%ld)! So, it will be truncated: %@.", explanation, (long)limit, self); + return [self substringToIndex:limit]; + } + return self; +} + - (NSString *)cly_truncatedValue:(NSString *)explanation { if (self.length > CountlyCommon.sharedInstance.maxValueLength) From 3f284c8fd56a5b4562818a386d68eb2848155e9b Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Thu, 30 Apr 2026 12:53:45 +0300 Subject: [PATCH 20/42] feat: picture max size config: impl --- CountlyServerConfig.m | 1 + 1 file changed, 1 insertion(+) diff --git a/CountlyServerConfig.m b/CountlyServerConfig.m index cbf35b34..b0f70e16 100644 --- a/CountlyServerConfig.m +++ b/CountlyServerConfig.m @@ -357,6 +357,7 @@ - (void)notifySdkConfigChange:(CountlyConfig *)config CountlyCommon.sharedInstance.maxKeyLength = config.sdkInternalLimits.getMaxKeyLength; CountlyCommon.sharedInstance.maxValueLength = config.sdkInternalLimits.getMaxValueSize; + CountlyCommon.sharedInstance.maxValueLengthPicture = config.sdkInternalLimits.getMaxValueSizePicture; CountlyCommon.sharedInstance.maxSegmentationValues = config.sdkInternalLimits.getMaxSegmentationValues; config.eventSendThreshold = _eventQueueSize ?: config.eventSendThreshold; From 2d2354e4d56f5da7135cfed4a00ff5af682a0a85 Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Thu, 30 Apr 2026 12:54:36 +0300 Subject: [PATCH 21/42] feat: new functions interface --- CountlyUserDetails.h | 60 ++++++++++++++++++++++++++++++++------------ 1 file changed, 44 insertions(+), 16 deletions(-) diff --git a/CountlyUserDetails.h b/CountlyUserDetails.h index 2d4d28f8..3018cb29 100644 --- a/CountlyUserDetails.h +++ b/CountlyUserDetails.h @@ -49,35 +49,35 @@ extern NSString* const kCountlyLocalPicturePath; * @discussion It can be set to an @c NSString, or @c NSNull for clearing it on server. * It will be sent to server when @c recordUserDetails method is called. */ -@property (nonatomic, copy) id _Nullable name; +@property (nonatomic, copy) id _Nullable name DEPRECATED_MSG_ATTRIBUTE("Use -setProperty:value: with key 'name' instead. Pass @\"\" to clear the field on server."); /** * Default @c username property for user's username in User Profiles. * @discussion It can be set to an @c NSString, or @c NSNull for clearing it on server. * It will be sent to server when @c recordUserDetails method is called. */ -@property (nonatomic, copy) id _Nullable username; +@property (nonatomic, copy) id _Nullable username DEPRECATED_MSG_ATTRIBUTE("Use -setProperty:value: with key 'username' instead."); /** * Default @c email property for user's e-mail in User Profiles. * @discussion It can be set to an @c NSString, or @c NSNull for clearing it on server. * It will be sent to server when @c recordUserDetails method is called. */ -@property (nonatomic, copy) id _Nullable email; +@property (nonatomic, copy) id _Nullable email DEPRECATED_MSG_ATTRIBUTE("Use -setProperty:value: with key 'email' instead."); /** * Default @c organization property for user's organization/company in User Profiles. * @discussion It can be set to an @c NSString, or @c NSNull for clearing it on server. * It will be sent to server when @c recordUserDetails method is called. */ -@property (nonatomic, copy) id _Nullable organization; +@property (nonatomic, copy) id _Nullable organization DEPRECATED_MSG_ATTRIBUTE("Use -setProperty:value: with key 'organization' instead."); /** * Default @c phone property for user's phone number in User Profiles. * @discussion It can be set to an @c NSString, or @c NSNull for clearing it on server. * It will be sent to server when @c recordUserDetails method is called. */ -@property (nonatomic, copy) id _Nullable phone; +@property (nonatomic, copy) id _Nullable phone DEPRECATED_MSG_ATTRIBUTE("Use -setProperty:value: with key 'phone' instead."); /** * Default @c gender property for user's gender in User Profiles. @@ -85,7 +85,7 @@ extern NSString* const kCountlyLocalPicturePath; * It will be sent to server when @c recordUserDetails method is called. * @discussion If it is set to case-insensitive @c m or @c f, it is displayed as @c Male or @c Female. Otherwise it will displayed as @c Unknown. */ -@property (nonatomic, copy) id _Nullable gender; +@property (nonatomic, copy) id _Nullable gender DEPRECATED_MSG_ATTRIBUTE("Use -setProperty:value: with key 'gender' instead."); /** * Default @c pictureURL property for user's profile photo in User Profiles. @@ -93,7 +93,7 @@ extern NSString* const kCountlyLocalPicturePath; * It will be sent to server when @c recordUserDetails method is called. * @discussion It should be a publicly accessible URL string to user's profile photo, so server can download it. */ -@property (nonatomic, copy) id _Nullable pictureURL; +@property (nonatomic, copy) id _Nullable pictureURL DEPRECATED_MSG_ATTRIBUTE("Use -setProperty:value: with key 'picture' instead."); /** * Default @c pictureLocalPath property for user's profile photo in User Profiles. @@ -102,14 +102,14 @@ extern NSString* const kCountlyLocalPicturePath; * @discussion It should be a valid local path string to user's profile photo on the device, so it can be uploaded to server. * If @c pictureURL is also set at the same time, @c pictureLocalPath will be ignored and @c pictureURL will be used. */ -@property (nonatomic, copy) id _Nullable pictureLocalPath; +@property (nonatomic, copy) id _Nullable pictureLocalPath DEPRECATED_MSG_ATTRIBUTE("Use -setProperty:value: with key 'picturePath' instead."); /** * Default @c birthYear property for user's birth year in User Profiles. * @discussion It can be set to an @c NSNumber, or @c NSNull for clearing it on server. * It will be sent to server when @c recordUserDetails method is called. */ -@property (nonatomic, copy) id _Nullable birthYear; +@property (nonatomic, copy) id _Nullable birthYear DEPRECATED_MSG_ATTRIBUTE("Use -setProperty:value: with key 'byear' instead."); /** * @c custom property for user's custom information as key-value pairs in User Profiles. @@ -117,7 +117,7 @@ extern NSString* const kCountlyLocalPicturePath; * It will be sent to server when @c recordUserDetails method is called. * @discussion Key-value pairs in @c custom property can also be modified using custom property modifier methods. */ -@property (nonatomic, copy) id _Nullable custom; +@property (nonatomic, copy) id _Nullable custom DEPRECATED_MSG_ATTRIBUTE("Use -setProperties: instead to set multiple custom properties at once."); /** * Returns @c CountlyUserDetails singleton to be used throughout the app. @@ -134,7 +134,7 @@ extern NSString* const kCountlyLocalPicturePath; * @param key Key for custom property key-value pair * @param value String value for custom property key-value pair */ -- (void)set:(NSString *)key value:(NSString *)value; +- (void)set:(NSString *)key value:(NSString *)value DEPRECATED_MSG_ATTRIBUTE("Use -setProperty:value: instead. This method always treats the key as a custom property; -setProperty: routes predefined keys (name, email, etc.) to top-level fields."); /** * Custom user details property modifier for setting a key-value pair where the value is a number. @@ -142,7 +142,7 @@ extern NSString* const kCountlyLocalPicturePath; * @param key Key for custom property key-value pair * @param value Number value for custom property key-value pair */ -- (void)set:(NSString *)key numberValue:(NSNumber *)value; +- (void)set:(NSString *)key numberValue:(NSNumber *)value DEPRECATED_MSG_ATTRIBUTE("Use -setProperty:value: instead."); /** * Custom user details property modifier for setting a key-value pair where the value is a boolean. @@ -150,7 +150,7 @@ extern NSString* const kCountlyLocalPicturePath; * @param key Key for custom property key-value pair * @param value Boolean value for custom property key-value pair */ -- (void)set:(NSString *)key boolValue:(BOOL)value; +- (void)set:(NSString *)key boolValue:(BOOL)value DEPRECATED_MSG_ATTRIBUTE("Use -setProperty:value: with @(boolValue) instead."); /** * Custom user details property modifier for setting a key-value pair if not set before. @@ -181,7 +181,7 @@ extern NSString* const kCountlyLocalPicturePath; * @discussion When called, this modifier is added to a non-persistent queue and sent to server only when @c save method is called. * @param key Key for custom property key-value pair */ -- (void)unSet:(NSString *)key; +- (void)unSet:(NSString *)key DEPRECATED_MSG_ATTRIBUTE("Use -setProperty:value: with an empty string @\"\" to clear a property on the server."); /** * Custom user details property modifier for incrementing a key-value pair's value by 1. @@ -324,11 +324,39 @@ extern NSString* const kCountlyLocalPicturePath; */ - (void)save; -- (void)setProperties:(NSDictionary *)data; +#pragma mark - User Properties + +/** + * Sets a single user property by key. + * @discussion The key may be either a predefined property (@c name, @c username, @c email, @c organization, @c phone, @c gender, @c picture, @c picturePath, @c byear) or a custom property. Predefined keys are routed to the matching top-level user-detail field; all other keys are stored as custom user properties. + * @discussion The change is queued and sent to server when @c save is called. Pending events in the event queue are flushed first so they reach the server in the right order. + * @discussion Pass an empty string @c @"" to clear a predefined string field on the server. Pass a negative @c NSNumber for @c byear to clear it. + * @param key The user property key. Must not be @c nil. + * @param value The user property value. Supported types are @c NSString, @c NSNumber, and @c NSArray (of supported scalar types). Must not be @c nil or @c NSNull. + */ - (void)setProperty:(NSString *)key value:(id)value; +/** + * Sets multiple user properties in one call. + * @discussion Equivalent to calling @c setProperty:value: for each key-value pair, but bundled into a single batch. Each entry is routed individually — predefined keys go to top-level fields, custom keys are stored under the @c custom dictionary. + * @discussion The change is queued and sent to server when @c save is called. Pending events in the event queue are flushed first so they reach the server in the right order. + * @discussion Pass an empty string @c @"" as a value to clear the corresponding predefined string field on the server. Entries with @c nil or @c NSNull values are skipped with a warning. + * @param data Dictionary of user properties. Must not be @c nil. Empty dictionaries are ignored. + */ +- (void)setProperties:(NSDictionary *)data; + +/** + * Clears all queued user property changes without sending them to the server. + * @discussion Drops all named-field updates and pending custom property modifications accumulated since the last successful @c save call. No request is made. + * @discussion Use this to discard a batch of pending changes before they are sent — for example, if user input is cancelled before submission. + */ +- (void)clear; -- (BOOL) hasUnsyncedChanges; +/** + * Indicates whether there are queued user property changes that have not been sent to the server yet. + * @return @c YES if a subsequent @c save call would produce a user-details request; @c NO otherwise. + */ +- (BOOL)hasUnsyncedChanges; NS_ASSUME_NONNULL_END From 89f170e06f0b9c6eb07d5434263a649ffcc643be Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Thu, 30 Apr 2026 13:01:29 +0300 Subject: [PATCH 22/42] feat: new functions interface: impl --- CountlyUserDetails.m | 316 ++++++++++++++++++++++++++----------------- 1 file changed, 191 insertions(+), 125 deletions(-) diff --git a/CountlyUserDetails.m b/CountlyUserDetails.m index c86a8d9a..5d22ed2f 100644 --- a/CountlyUserDetails.m +++ b/CountlyUserDetails.m @@ -7,7 +7,6 @@ #import "CountlyCommon.h" @interface CountlyUserDetails () -@property (nonatomic) NSMutableDictionary* predefined; @property (nonatomic) NSMutableDictionary* customMods; @property (nonatomic) NSMutableDictionary* customProperties; @@ -25,6 +24,7 @@ - (BOOL)isValidDataType:(id) value; NSString* const kCountlyUDKeyPicture = @"picture"; NSString* const kCountlyUDKeyBirthyear = @"byear"; NSString* const kCountlyUDKeyCustom = @"custom"; +NSString* const kCountlyUDKeyPicturePath = @"picturePath"; NSString* const kCountlyUDKeyModifierSetOnce = @"$setOnce"; NSString* const kCountlyUDKeyModifierIncrement = @"$inc"; @@ -43,6 +43,7 @@ - (BOOL)isValidDataType:(id) value; kCountlyUDKeyPhone, kCountlyUDKeyGender, kCountlyUDKeyPicture, + kCountlyUDKeyPicturePath, kCountlyUDKeyBirthyear }; @@ -62,7 +63,6 @@ - (instancetype)init { if (self = [super init]) { - self.predefined = NSMutableDictionary.new; self.customMods = NSMutableDictionary.new; self.customProperties = NSMutableDictionary.new; } @@ -73,41 +73,22 @@ - (instancetype)init - (NSString *)serializedUserDetails { NSMutableDictionary* userDictionary = NSMutableDictionary.new; - if (self.name) - userDictionary[kCountlyUDKeyName] = - ![self.name isKindOfClass:NSString.class] ? self.name : - [(NSString *)self.name cly_truncatedValue:@"User details name"]; - - if (self.username) - userDictionary[kCountlyUDKeyUsername] = - ![self.username isKindOfClass:NSString.class] ? self.username : - [(NSString *)self.username cly_truncatedValue:@"User details username"]; - - if (self.email) - userDictionary[kCountlyUDKeyEmail] = - ![self.email isKindOfClass:NSString.class] ? self.email : - [(NSString *)self.email cly_truncatedValue:@"User details email"]; - - if (self.organization) - userDictionary[kCountlyUDKeyOrganization] = - ![self.organization isKindOfClass:NSString.class] ? self.organization : - [(NSString *)self.organization cly_truncatedValue:@"User details organization"]; - - if (self.phone) - userDictionary[kCountlyUDKeyPhone] = - ![self.phone isKindOfClass:NSString.class] ? self.phone : - [(NSString *)self.phone cly_truncatedValue:@"User details phone"]; - - if (self.gender) - userDictionary[kCountlyUDKeyGender] = - ![self.gender isKindOfClass:NSString.class] ? self.gender : - [(NSString *)self.gender cly_truncatedValue:@"User details gender"]; - - if (self.pictureURL) - userDictionary[kCountlyUDKeyPicture] = self.pictureURL; - - if (self.birthYear) - userDictionary[kCountlyUDKeyBirthyear] = self.birthYear; + [self serializeStringField:self.name key:kCountlyUDKeyName explanation:@"User details name" into:userDictionary picture:NO]; + [self serializeStringField:self.username key:kCountlyUDKeyUsername explanation:@"User details username" into:userDictionary picture:NO]; + [self serializeStringField:self.email key:kCountlyUDKeyEmail explanation:@"User details email" into:userDictionary picture:NO]; + [self serializeStringField:self.organization key:kCountlyUDKeyOrganization explanation:@"User details organization" into:userDictionary picture:NO]; + [self serializeStringField:self.phone key:kCountlyUDKeyPhone explanation:@"User details phone" into:userDictionary picture:NO]; + [self serializeStringField:self.gender key:kCountlyUDKeyGender explanation:@"User details gender" into:userDictionary picture:NO]; + [self serializeStringField:self.pictureURL key:kCountlyUDKeyPicture explanation:@"User details picture" into:userDictionary picture:YES]; + + if (self.birthYear) { + if ([self.birthYear isKindOfClass:NSNumber.class] && ((NSNumber *)self.birthYear).integerValue < 0) { + // Negative byear means "clear on server" — match Android semantics. + userDictionary[kCountlyUDKeyBirthyear] = NSNull.null; + } else { + userDictionary[kCountlyUDKeyBirthyear] = self.birthYear; + } + } NSMutableDictionary* customAll = NSMutableDictionary.new; @@ -149,7 +130,6 @@ - (void)clearUserDetails self.birthYear = nil; self.custom = nil; - [self.predefined removeAllObjects]; [self.customMods removeAllObjects]; [self.customProperties removeAllObjects]; } @@ -177,29 +157,55 @@ - (BOOL)hasUnsyncedChanges } }]; - return userDetailsChanged || self.predefined.count > 0 || self.customProperties.count > 0 || self.customMods.count > 0; + return userDetailsChanged || self.customProperties.count > 0 || self.customMods.count > 0; } #pragma mark - +// Legacy custom-only setter. Treats every key as a custom user property — never +// routes to predefined fields like `name`/`email`/etc. This preserves the +// pre-`setProperty:` wire format. Use `-setProperty:value:` for the named-aware path. - (void)set:(NSString *)key value:(NSString *)value { CLY_LOG_I(@"%s %@ %@", __FUNCTION__, key, value); - [self setProperty:key value:value]; + [self setCustomProperty:key value:value]; } - (void)set:(NSString *)key numberValue:(NSNumber *)value { CLY_LOG_I(@"%s %@ %@", __FUNCTION__, key, value); - [self setProperty:key value:value]; + [self setCustomProperty:key value:value]; } - (void)set:(NSString *)key boolValue:(BOOL)value { CLY_LOG_I(@"%s %@ %d", __FUNCTION__, key, value); - [self setProperty:key value:@(value)]; + [self setCustomProperty:key value:@(value)]; +} + +- (void)setCustomProperty:(NSString *)key value:(id)value +{ + if (key == nil || value == nil) return; + if (![CountlyServerConfig.sharedInstance shouldRecordUserProperty:key]) { + CLY_LOG_D(@"%s key [%@] is filtered out by user property filter, omitting call", __FUNCTION__, key); + return; + } + if (![self isValidDataType:value]) { + CLY_LOG_D(@"%s unsupported type for key [%@], type [%@], omitting call", + __FUNCTION__, key, NSStringFromClass([value class])); + return; + } + NSString* truncatedLog = [NSString stringWithFormat:@"%s",__FUNCTION__]; + if ([value isKindOfClass:[NSString class]]) { + BOOL isPicture = [key isEqualToString:kCountlyUDKeyPicture] || [key isEqualToString:kCountlyUDKeyPicturePath]; + value = isPicture ? [(NSString *)value cly_truncatedPictureValue:truncatedLog] + : [(NSString *)value cly_truncatedValue:truncatedLog]; + } + NSString *truncatedKey = [key cly_truncatedKey:truncatedLog]; + self.customProperties[truncatedKey] = value; + // No auto-flush — legacy `set:` preserves pre-existing event-flush timing. } - (void)setOnce:(NSString *)key value:(NSString *)value @@ -220,22 +226,17 @@ - (void)setOnce:(NSString *)key boolValue:(BOOL)value; [self doModification:kCountlyUDKeyModifierSetOnce key:key value:@(value)]; } +// Legacy custom-only unsetter. Always treats key as a custom user property; does +// not clear predefined fields like `name`/`email`. Preserves pre-`setProperty:` +// wire format. Clear named fields via direct property assignment to NSNull.null. - (void)unSet:(NSString *)key { CLY_LOG_I(@"%s %@", __FUNCTION__, key); - if(key != nil){ - if(self.customProperties[key]) { - self.customProperties[key] = NSNull.null; - } - - if(self.customMods[key]) { - self.customMods[key] = NSNull.null; - } - - if(self.predefined[key]) { - self.predefined[key] = NSNull.null; - } - } + if (key == nil) return; + + NSString *truncatedKey = [key cly_truncatedKey:@"unSet"]; + self.customProperties[truncatedKey] = NSNull.null; + // No auto-flush — legacy `unSet:` preserves pre-existing event-flush timing. } - (void)increment:(NSString *)key @@ -363,12 +364,6 @@ - (void)save if (self.pictureLocalPath && !self.pictureURL) [CountlyConnectionManager.sharedInstance sendUserDetails:[@{kCountlyLocalPicturePath: self.pictureLocalPath} cly_JSONify]]; - if (self.modifications.count) - { - [self filterAndLimitUserProperties:self.modifications]; - [CountlyConnectionManager.sharedInstance sendUserDetails:[@{kCountlyUDKeyCustom: [self truncateModifications]} cly_JSONify]]; - } - [self clearUserDetails]; } @@ -378,28 +373,45 @@ - (void)doModification:(NSString *)mod key:(NSString *)key value:(id)value { CLY_LOG_W(@"%s call will be ignored as value is nil!", __FUNCTION__); return; } + if (![self isValidDataType:value]) { + CLY_LOG_W(@"%s unsupported value type for key [%@] mod [%@], type [%@], omitting call", + __FUNCTION__, key, mod, NSStringFromClass([value class])); + return; + } + if (![CountlyServerConfig.sharedInstance shouldRecordUserProperty:key]) { + CLY_LOG_D(@"%s key [%@] is filtered out by user property filter, omitting call", __FUNCTION__, key); + return; + } NSString* truncatedLog = [NSString stringWithFormat:@"%s",__FUNCTION__]; - + // If the value is NSString, apply truncation rules if ([value isKindOfClass:[NSString class]]) { value = [[value description] cly_truncatedValue:truncatedLog]; } - + NSString* truncatedKey = [[key description] cly_truncatedKey:truncatedLog]; if (![mod isEqualToString:@"$pull"] && ![mod isEqualToString:@"$push"] && ![mod isEqualToString:@"$addToSet"]) { - self.customMods[truncatedKey] = @{mod: value};; + self.customMods[truncatedKey] = @{mod: value}; } else { - if(self.customMods[truncatedKey] && self.customMods[truncatedKey][mod]){ - NSMutableArray *array = [self.customMods[key][mod] mutableCopy]; - [array addObject:value]; - self.customMods[truncatedKey] = @{mod: array}; + NSMutableArray *array; + if (self.customMods[truncatedKey] && [self.customMods[truncatedKey][mod] isKindOfClass:[NSArray class]]) { + array = [self.customMods[truncatedKey][mod] mutableCopy]; } else { - self.customMods[truncatedKey] = @{mod: value}; + array = [NSMutableArray array]; } + if ([value isKindOfClass:[NSArray class]]) { + [array addObjectsFromArray:value]; + } else { + [array addObject:value]; + } + self.customMods[truncatedKey] = @{mod: array}; } - + // Note: legacy modifier methods (setOnce/push/pull/etc.) deliberately do + // NOT auto-flush events — preserves pre-existing request-timing behavior + // for callers still on the legacy API. Auto-flush is opt-in via the new + // -setProperty:/setProperties: path. } /** @@ -413,41 +425,124 @@ - (void)setPropertiesInternal:(NSDictionary *)data { CLY_LOG_I(@"%s no data was provided", __FUNCTION__); return; } - + + NSString* truncatedLog = [NSString stringWithFormat:@"%s",__FUNCTION__]; + BOOL anyChange = NO; + for (NSString *key in data) { id value = data[key]; - + if (value == nil || value == [NSNull null]) { CLY_LOG_W(@"%s provided value for key [%@] is 'null'", __FUNCTION__, key); continue; } - - NSString* truncatedLog = [NSString stringWithFormat:@"%s",__FUNCTION__]; + if ([value isKindOfClass:[NSString class]]) { - value = [[value description] cly_truncatedValue:truncatedLog]; + BOOL isPicture = [key isEqualToString:kCountlyUDKeyPicture] || [key isEqualToString:kCountlyUDKeyPicturePath]; + value = isPicture ? [[value description] cly_truncatedPictureValue:truncatedLog] + : [[value description] cly_truncatedValue:truncatedLog]; } - + BOOL isNamed = NO; - for (NSUInteger i = 0; i < kCountlyUDNamedFieldsCount; i++) { if ([kCountlyUDNamedFields[i] isEqualToString:key]) { isNamed = YES; - self.predefined[key] = [value description]; + [self assignNamedField:key value:value]; + anyChange = YES; break; } } - - // Handle custom fields + if (!isNamed) { + if (![CountlyServerConfig.sharedInstance shouldRecordUserProperty:key]) { + CLY_LOG_D(@"%s key [%@] is filtered out by user property filter, omitting call", __FUNCTION__, key); + continue; + } NSString* truncatedKey = [[key description] cly_truncatedKey:truncatedLog]; if ([self isValidDataType:value]) { self.customProperties[truncatedKey] = value; + anyChange = YES; } else { CLY_LOG_D(@"%s provided an unsupported type for key: [%@], value: [%@], type: [%@], omitting call",__FUNCTION__, key, value, NSStringFromClass([value class])); } } } + + if (anyChange) [self userPropertiesChanged]; +} + +- (void)serializeStringField:(id)field + key:(NSString *)key + explanation:(NSString *)explanation + into:(NSMutableDictionary *)userDictionary + picture:(BOOL)isPicture +{ + if (!field) return; + + if (![field isKindOfClass:NSString.class]) { + // NSNull — explicit clear. + userDictionary[key] = field; + return; + } + + NSString *str = (NSString *)field; + if (str.length == 0) { + // Empty string means "clear on server" — match Android semantics. + userDictionary[key] = NSNull.null; + return; + } + + userDictionary[key] = isPicture ? [str cly_truncatedPictureValue:explanation] + : [str cly_truncatedValue:explanation]; +} + +- (void)assignNamedField:(NSString *)key value:(id)value { + BOOL isNull = (value == [NSNull null]); + id stringOrNull = isNull ? NSNull.null : [value description]; + + if ([key isEqualToString:kCountlyUDKeyName]) { + self.name = stringOrNull; + } else if ([key isEqualToString:kCountlyUDKeyUsername]) { + self.username = stringOrNull; + } else if ([key isEqualToString:kCountlyUDKeyEmail]) { + self.email = stringOrNull; + } else if ([key isEqualToString:kCountlyUDKeyOrganization]) { + self.organization = stringOrNull; + } else if ([key isEqualToString:kCountlyUDKeyPhone]) { + self.phone = stringOrNull; + } else if ([key isEqualToString:kCountlyUDKeyGender]) { + self.gender = stringOrNull; + } else if ([key isEqualToString:kCountlyUDKeyPicture]) { + self.pictureURL = stringOrNull; + } else if ([key isEqualToString:kCountlyUDKeyPicturePath]) { + if (isNull) { + self.pictureLocalPath = nil; + } else { + NSString *path = [value description]; + // Match Android: drop the path with a warning if the file isn't readable. + if (path.length > 0 && ![[NSFileManager defaultManager] isReadableFileAtPath:path]) { + CLY_LOG_W(@"%s provided picture path file [%@] can not be opened", __FUNCTION__, path); + self.pictureLocalPath = nil; + } else { + self.pictureLocalPath = path; + } + } + } else if ([key isEqualToString:kCountlyUDKeyBirthyear]) { + if (isNull) { + self.birthYear = NSNull.null; + } else if ([value isKindOfClass:[NSNumber class]]) { + self.birthYear = (NSNumber *)value; + } else if ([value isKindOfClass:[NSString class]]) { + NSNumberFormatter *formatter = [NSNumberFormatter new]; + NSNumber *parsed = [formatter numberFromString:(NSString *)value]; + if (parsed) { + self.birthYear = parsed; + } else { + CLY_LOG_W(@"%s incorrect byear number format: %@", __FUNCTION__, value); + } + } + } } - (void)filterAndLimitUserProperties:(NSMutableDictionary *)properties @@ -468,49 +563,15 @@ - (void)filterAndLimitUserProperties:(NSMutableDictionary *)properties } } -- (NSDictionary *)truncateModifications +// Match Android's `onUserPropertiesChanged`: when user properties change, flush any +// pending events first so they reach the server before the next user-details request. +- (void)userPropertiesChanged { - NSMutableDictionary* truncatedDict = self.modifications.mutableCopy; - [self.modifications enumerateKeysAndObjectsUsingBlock:^(NSString * key, id obj, BOOL * stop) - { - NSString* truncatedKey = [key cly_truncatedKey:@"User details modifications key"]; - if (![truncatedKey isEqualToString:key]) - { - truncatedDict[truncatedKey] = obj; - [truncatedDict removeObjectForKey:key]; - } - - if ([obj isKindOfClass:NSString.class]) - { - NSString* truncatedValue = [obj cly_truncatedValue:@"User details modifications value"]; - if (![truncatedValue isEqualToString:obj]) - { - truncatedDict[truncatedKey] = truncatedValue; - } - } - else if ([obj isKindOfClass:NSDictionary.class]) - { - NSMutableDictionary* truncatedValueDict = ((NSDictionary *)obj).mutableCopy; - [(NSDictionary *)obj enumerateKeysAndObjectsUsingBlock:^(NSString * key, id value, BOOL * stop) - { - if ([value isKindOfClass:NSString.class]) - { - NSString* truncatedValue = [value cly_truncatedValue:@"User details modifications value"]; - if (![truncatedValue isEqualToString:value]) - { - truncatedValueDict[key] = truncatedValue; - truncatedDict[truncatedKey] = truncatedValueDict; - } - } - }]; - } - - }]; - - return truncatedDict.copy; + if (!CountlyCommon.sharedInstance.hasStarted) + return; + [CountlyConnectionManager.sharedInstance sendEvents]; } - - (BOOL)isValidDataType:(id) value { if ([value isKindOfClass:[NSNumber class]] || [value isKindOfClass:[NSString class]] || @@ -522,26 +583,31 @@ - (BOOL)isValidDataType:(id) value { // Set a single user property. It can be either a custom one or one of the predefined ones. - (void)setProperty:(NSString *)key value:(id)value { - NSLog(@"[UserProfile] Calling 'setProperty'"); + CLY_LOG_I(@"%s %@ %@", __FUNCTION__, key, value); NSMutableDictionary *data = [NSMutableDictionary dictionary]; if (key != nil && value != nil) { data[key] = value; } - + [self setPropertiesInternal:data]; } // Provide a map of user properties to set. // Those can be either custom user properties or predefined user properties - (void)setProperties:(NSDictionary *)data { - NSLog(@"[UserProfile] Calling 'setProperties'"); + CLY_LOG_I(@"%s", __FUNCTION__); if (data == nil) { - NSLog(@"[UserProfile] Provided data can not be 'null'"); + CLY_LOG_W(@"%s provided data can not be 'null'", __FUNCTION__); return; } - + [self setPropertiesInternal:data]; } + +- (void)clear { + CLY_LOG_I(@"%s", __FUNCTION__); + [self clearUserDetails]; +} @end From 55c508081a679ef993cd7f4a349f7b36fbc6efd0 Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Thu, 30 Apr 2026 13:01:35 +0300 Subject: [PATCH 23/42] feat: new functions interface: tests --- CountlyTests/CountlyUserProfileTests.swift | 402 ++++++++++++++++++++- CountlyTests/TestUtils.swift | 14 + 2 files changed, 412 insertions(+), 4 deletions(-) diff --git a/CountlyTests/CountlyUserProfileTests.swift b/CountlyTests/CountlyUserProfileTests.swift index 3b4d1dd6..ca325203 100644 --- a/CountlyTests/CountlyUserProfileTests.swift +++ b/CountlyTests/CountlyUserProfileTests.swift @@ -25,6 +25,15 @@ class CountlyUserProfileTests: CountlyBaseTestCase { super.setUp() // Initialize or reset necessary objects here Countly.sharedInstance().halt(true) + + // sdkInternalLimits() returns a file-scope static singleton, not a + // per-config instance — so a test setting setMaxValueSize/setMaxKeyLength + // mutates state visible to every later test. Reset to defaults here. + let limits = CountlyConfig().sdkInternalLimits() + limits.setMaxKeyLength(128) + limits.setMaxValueSize(256) + limits.setMaxValueSizePicture(4096) + limits.setMaxSegmentationValues(100) } override func tearDown() { @@ -496,6 +505,8 @@ class CountlyUserProfileTests: CountlyBaseTestCase { } func getUserDataMap()-> [String: Any]{ + // $push/$pull/$addToSet always serialize as arrays (post first-write-as-array fix + // — server-accepted, prevents NSMutableString crash on second-call array merge). let userProperties = ["a12345": "My Property", "b12345": ["$inc": 1], "c12345": ["$inc": 10], @@ -503,9 +514,9 @@ class CountlyUserProfileTests: CountlyBaseTestCase { "e12345": ["$max": 100], "f12345": ["$min": 50], "g12345": ["$setOnce": 200], - "h12345": ["$addToSet": "morning"], - "i12345": ["$push": "morning"], - "j12345": ["$pull": "morning"] ]as [String : Any] + "h12345": ["$addToSet": ["morning"]], + "i12345": ["$push": ["morning"]], + "j12345": ["$pull": ["morning"]] ]as [String : Any] return userProperties; } @@ -515,7 +526,390 @@ class CountlyUserProfileTests: CountlyBaseTestCase { Countly.user().set("a12345", value: "3"); Countly.user().set("a12345", value: "4"); } - + + // MARK: - New API tests (Android `ModuleUserProfileTests` parity) + + /// Android parity: `setAndSaveValues` (line 40) + /// `setProperties` with named + custom keys, then `save`, produces a single + /// user_details request with named fields at top level and custom keys nested. + func test_setAndSaveValues_namedAndCustomBundled() { + let config = createBaseConfig() + config.requiresConsent = false + config.manualSessionHandling = true + Countly.sharedInstance().start(with: config) + + let userProperties: [String: Any] = [ + "name": "Test Test", + "username": "test", + "email": "test@gmail.com", + "organization": "Tester", + "phone": "+1234567890", + "gender": "M", + "picture": "http://domain.com/test.png", + "byear": 2000, + "key1": "value1", + "key2": "value2" + ] + + Countly.user().setProperties(userProperties) + Countly.user().save() + + guard let rq = TestUtils.getCurrentRQ() else { + XCTFail("RQ is nil"); return + } + guard let userDetailsRequest = rq.first(where: { $0.contains("user_details=") }) else { + XCTFail("No user_details request in RQ"); return + } + + let parsed = TestUtils.parseQueryString(userDetailsRequest) + guard let userDetails = parsed["user_details"] as? [String: Any] else { + XCTFail("user_details missing or not a dict"); return + } + + XCTAssertEqual("Test Test", userDetails["name"] as? String) + XCTAssertEqual("test", userDetails["username"] as? String) + XCTAssertEqual("test@gmail.com", userDetails["email"] as? String) + XCTAssertEqual("Tester", userDetails["organization"] as? String) + XCTAssertEqual("+1234567890", userDetails["phone"] as? String) + XCTAssertEqual("M", userDetails["gender"] as? String) + XCTAssertEqual("http://domain.com/test.png", userDetails["picture"] as? String) + XCTAssertEqual(2000, userDetails["byear"] as? Int) + + guard let custom = userDetails["custom"] as? [String: Any] else { + XCTFail("custom dict missing"); return + } + XCTAssertEqual("value1", custom["key1"] as? String) + XCTAssertEqual("value2", custom["key2"] as? String) + } + + /// Android parity: `testClear` (line 289) + /// After `clear`, `hasUnsyncedChanges` returns false and `save` produces no + /// user_details request. + func test_clear_dropsAllPendingChanges() { + let config = createBaseConfig() + config.requiresConsent = false + config.manualSessionHandling = true + Countly.sharedInstance().start(with: config) + + Countly.user().setProperties([ + "name": "Test", + "email": "test@example.com", + "key1": "value1" + ]) + XCTAssertTrue(Countly.user().hasUnsyncedChanges()) + + // `Countly.user().clear()` is ambiguous from Swift due to a duplicated + // `clear` import (Countly module + bridging-header view of + // CountlyUserDetails.h). Resolving via runtime selector lookup. + _ = Countly.user().perform(NSSelectorFromString("clear")) + XCTAssertFalse(Countly.user().hasUnsyncedChanges()) + + let rqBefore = TestUtils.getCurrentRQ()?.count ?? 0 + Countly.user().save() + let rqAfter = TestUtils.getCurrentRQ()?.count ?? 0 + XCTAssertEqual(rqBefore, rqAfter, "save() after clear() must not enqueue a request") + } + + /// Android parity: `testCustomModifiers` (line 272) + /// Multiple `pushUnique` calls on the same key accumulate into an array + /// (the array-merge fix). Scalar values like `$inc` and `$mul` stay as numbers. + func test_customModifiers_addToSetAccumulates() { + let config = createBaseConfig() + config.requiresConsent = false + config.manualSessionHandling = true + Countly.sharedInstance().start(with: config) + + Countly.user().increment(by: "key_inc", value: 1) + Countly.user().multiply("key_mul", value: 2) + Countly.user().pushUnique("key_set", value: "test1") + Countly.user().pushUnique("key_set", value: "test2") + Countly.user().save() + + guard let rq = TestUtils.getCurrentRQ(), + let request = rq.first(where: { $0.contains("user_details=") }) else { + XCTFail("No user_details request"); return + } + + let parsed = TestUtils.parseQueryString(request) + guard let userDetails = parsed["user_details"] as? [String: Any], + let custom = userDetails["custom"] as? [String: Any] else { + XCTFail("custom missing"); return + } + + XCTAssertEqual(1, (custom["key_inc"] as? [String: Any])?["$inc"] as? Int) + XCTAssertEqual(2, (custom["key_mul"] as? [String: Any])?["$mul"] as? Int) + + guard let setEntry = custom["key_set"] as? [String: Any], + let setArray = setEntry["$addToSet"] as? [Any] else { + XCTFail("key_set should hold an $addToSet array"); return + } + XCTAssertEqual(2, setArray.count) + XCTAssertEqual("test1", setArray[0] as? String) + XCTAssertEqual("test2", setArray[1] as? String) + } + + /// Android parity: `testCustomData` (line 243) + /// `setProperty` with a custom key lands in the custom dict. + func test_setProperty_customKeyLandsInCustom() { + let config = createBaseConfig() + config.requiresConsent = false + config.manualSessionHandling = true + Countly.sharedInstance().start(with: config) + + Countly.user().setProperties(["key1": "value1", "key2": "value2"]) + Countly.user().setProperty("key_prop", value: "value_prop") + Countly.user().save() + + guard let rq = TestUtils.getCurrentRQ(), + let request = rq.first(where: { $0.contains("user_details=") }) else { + XCTFail("No user_details request"); return + } + + let parsed = TestUtils.parseQueryString(request) + guard let userDetails = parsed["user_details"] as? [String: Any], + let custom = userDetails["custom"] as? [String: Any] else { + XCTFail("custom missing"); return + } + XCTAssertEqual("value1", custom["key1"] as? String) + XCTAssertEqual("value2", custom["key2"] as? String) + XCTAssertEqual("value_prop", custom["key_prop"] as? String) + } + + /// Android parity: `internalLimit_testCustomData` (line 441) + /// With maxKeyLength=10, custom keys longer than 10 are truncated and merge. + /// Note (Android difference): Android does NOT truncate predefined keys; + /// iOS doesn't pass predefined names through key truncation either since the + /// named-field switch matches the full constant. + func test_internalLimit_truncatesCustomKeys() { + let config = createBaseConfig() + config.requiresConsent = false + config.manualSessionHandling = true + config.sdkInternalLimits().setMaxKeyLength(10) + Countly.sharedInstance().start(with: config) + + Countly.user().setProperties([ + "hair_color_id": 4567, + "hair_color_tone": "bold" + ]) + Countly.user().setProperty("hair_color", value: "black") + Countly.user().setProperty("hair_skin_tone", value: "yellow") + Countly.user().save() + + guard let rq = TestUtils.getCurrentRQ(), + let request = rq.first(where: { $0.contains("user_details=") }) else { + XCTFail("No user_details request"); return + } + + let parsed = TestUtils.parseQueryString(request) + guard let userDetails = parsed["user_details"] as? [String: Any], + let custom = userDetails["custom"] as? [String: Any] else { + XCTFail("custom missing"); return + } + XCTAssertEqual("black", custom["hair_color"] as? String, + "hair_color_id (truncated to hair_color), then hair_color_tone (also truncated to hair_color), then hair_color literal — last write wins") + XCTAssertEqual("yellow", custom["hair_skin_"] as? String, + "hair_skin_tone truncated to hair_skin_") + } + + /// Android parity: `internalLimit_testCustomModifiers` (line 468) + /// With maxKeyLength=10, two `push` calls on different long keys whose + /// truncated form collides should accumulate into the same array. + /// This exercises the truncated-key merge fix and the array-merge fix together. + func test_internalLimit_pushKeysCollideAndAccumulate() { + let config = createBaseConfig() + config.requiresConsent = false + config.manualSessionHandling = true + config.sdkInternalLimits().setMaxKeyLength(10) + Countly.sharedInstance().start(with: config) + + Countly.user().push("key_push_reminder", value: "test1") + Countly.user().push("key_push_rock", value: "test3") + Countly.user().save() + + guard let rq = TestUtils.getCurrentRQ(), + let request = rq.first(where: { $0.contains("user_details=") }) else { + XCTFail("No user_details request"); return + } + + let parsed = TestUtils.parseQueryString(request) + guard let userDetails = parsed["user_details"] as? [String: Any], + let custom = userDetails["custom"] as? [String: Any] else { + XCTFail("custom missing"); return + } + + guard let entry = custom["key_push_r"] as? [String: Any], + let pushArray = entry["$push"] as? [Any] else { + XCTFail("Expected key_push_r with $push array"); return + } + XCTAssertEqual(2, pushArray.count) + XCTAssertEqual("test1", pushArray[0] as? String) + XCTAssertEqual("test3", pushArray[1] as? String) + } + + /// Android parity: `internalLimit_setProperties_maxValueSizePicture` (line 591) + /// Picture URL uses the picture-specific limit (4096), independent of maxValueSize. + func test_internalLimit_pictureUsesItsOwnLimit() { + let config = createBaseConfig() + config.requiresConsent = false + config.manualSessionHandling = true + config.sdkInternalLimits().setMaxValueSize(2) + Countly.sharedInstance().start(with: config) + + let longURL = String(repeating: "a", count: 6000) + Countly.user().setProperty("picture", value: longURL) + Countly.user().save() + + guard let rq = TestUtils.getCurrentRQ(), + let request = rq.first(where: { $0.contains("user_details=") }) else { + XCTFail("No user_details request"); return + } + + let parsed = TestUtils.parseQueryString(request) + guard let userDetails = parsed["user_details"] as? [String: Any] else { + XCTFail("user_details missing"); return + } + let pic = userDetails["picture"] as? String + XCTAssertEqual(4096, pic?.count, "picture should be truncated to 4096, not maxValueSize=2") + } + + /// Android parity: `setUserProperties_null` (line 695) + /// NSNull values are skipped with a warning; nothing reaches the wire. + func test_setProperties_nullValuesSkipped() { + let config = createBaseConfig() + config.requiresConsent = false + config.manualSessionHandling = true + Countly.sharedInstance().start(with: config) + + let rqBefore = TestUtils.getCurrentRQ()?.count ?? 0 + Countly.user().setProperties(["null_key": NSNull()]) + Countly.user().save() + let rqAfter = TestUtils.getCurrentRQ()?.count ?? 0 + XCTAssertEqual(rqBefore, rqAfter, "NSNull-only setProperties + save should not enqueue a user_details request") + } + + // MARK: - iOS-specific Android-parity tests (gaps B, C, A, F, E) + + /// Gap B parity: empty string for a named string field serializes as null + /// (clear-on-server semantics). + func test_emptyStringForNamedField_serializesAsNull() { + let config = createBaseConfig() + config.requiresConsent = false + config.manualSessionHandling = true + Countly.sharedInstance().start(with: config) + + Countly.user().setProperties(["name": "Joe"]) + Countly.user().save() + Countly.user().setProperty("name", value: "") + Countly.user().save() + + guard let rq = TestUtils.getCurrentRQ() else { + XCTFail("RQ is nil"); return + } + let userDetailsRequests = rq.filter { $0.contains("user_details=") } + XCTAssertGreaterThanOrEqual(userDetailsRequests.count, 2) + + let lastClearRequest = userDetailsRequests.last! + let parsed = TestUtils.parseQueryString(lastClearRequest) + guard let userDetails = parsed["user_details"] as? [String: Any] else { + XCTFail("user_details missing"); return + } + + // After serialization, name was empty string — should be NSNull (clear). + XCTAssertTrue(userDetails["name"] is NSNull, + "Empty string should serialize as null. Got: \(String(describing: userDetails["name"]))") + } + + /// Gap C parity: negative byear serializes as null (clear-on-server). + func test_negativeBirthYear_serializesAsNull() { + let config = createBaseConfig() + config.requiresConsent = false + config.manualSessionHandling = true + Countly.sharedInstance().start(with: config) + + Countly.user().setProperty("byear", value: -1) + Countly.user().save() + + guard let rq = TestUtils.getCurrentRQ(), + let request = rq.first(where: { $0.contains("user_details=") }) else { + XCTFail("No user_details request"); return + } + + let parsed = TestUtils.parseQueryString(request) + guard let userDetails = parsed["user_details"] as? [String: Any] else { + XCTFail("user_details missing"); return + } + XCTAssertTrue(userDetails["byear"] is NSNull, + "Negative byear should serialize as null. Got: \(String(describing: userDetails["byear"]))") + } + + /// Gap A parity: `picturePath` pointing at a non-existent file is dropped + /// with a warning; pictureLocalPath remains nil. + func test_picturePath_nonExistentFile_isDropped() { + let config = createBaseConfig() + config.requiresConsent = false + config.manualSessionHandling = true + Countly.sharedInstance().start(with: config) + + Countly.user().setProperty("picturePath", value: "/definitely/not/a/real/file.jpg") + Countly.user().save() + + // No request should fire because pictureLocalPath got nilled out and + // no other user-detail change happened. + guard let rq = TestUtils.getCurrentRQ() else { + XCTFail("RQ is nil"); return + } + let userDetailsRequests = rq.filter { $0.contains("user_details=") } + XCTAssertEqual(0, userDetailsRequests.count, + "Non-existent picturePath should not produce a user_details request") + } + + /// Gap E parity: changing a user property triggers an event-queue flush + /// (Android `onUserPropertiesChanged`). When events are pending and a + /// property change occurs, the events get drained into the request queue. + func test_propertyChange_flushesPendingEvents() { + let config = createBaseConfig() + config.requiresConsent = false + config.manualSessionHandling = true + Countly.sharedInstance().start(with: config) + + Countly.sharedInstance().recordEvent("eventA") + Countly.sharedInstance().recordEvent("eventB") + + // Before the property change, events sit in the event queue. + XCTAssertEqual(2, TestUtils.getCurrentEQ()?.count ?? 0) + + Countly.user().setProperty("level", value: 42) + + // After the property change, the event queue should be drained. + XCTAssertEqual(0, TestUtils.getCurrentEQ()?.count ?? 0, + "Auto-flush should drain the event queue when a user property changes") + } + + /// Gap D parity: `providedUserProperties` from CountlyConfig is applied + /// at SDK start and saved automatically. + func test_providedUserPropertiesAtInit_appliedAndSaved() { + let config = createBaseConfig() + config.requiresConsent = false + config.manualSessionHandling = true + config.providedUserProperties = [ + "email": "init@example.com", + "favorite_color": "blue" + ] + Countly.sharedInstance().start(with: config) + + guard let rq = TestUtils.getCurrentRQ(), + let request = rq.first(where: { $0.contains("user_details=") }) else { + XCTFail("Expected a user_details request from providedUserProperties"); return + } + + let parsed = TestUtils.parseQueryString(request) + guard let userDetails = parsed["user_details"] as? [String: Any] else { + XCTFail("user_details missing"); return + } + XCTAssertEqual("init@example.com", userDetails["email"] as? String) + let custom = userDetails["custom"] as? [String: Any] + XCTAssertEqual("blue", custom?["favorite_color"] as? String) + } } diff --git a/CountlyTests/TestUtils.swift b/CountlyTests/TestUtils.swift index 895bc424..83c21634 100644 --- a/CountlyTests/TestUtils.swift +++ b/CountlyTests/TestUtils.swift @@ -286,6 +286,13 @@ class TestUtils { return result } + /// Best-effort array coercion that handles both Swift `[T]` and bridged NSArray. + static func asArray(_ value: Any) -> [Any]? { + if let arr = value as? [Any] { return arr } + if let nsArr = value as? NSArray { return nsArr as? [Any] } + return nil + } + static func compareDictionaries(_ dict1: [String: Any], _ dict2: [String: Any]) -> Bool { guard dict1.count == dict2.count else { return false @@ -300,6 +307,13 @@ class TestUtils { if !compareDictionaries(nestedDict1, nestedDict2) { return false } + } else if let arr1 = TestUtils.asArray(value), let arr2 = TestUtils.asArray(otherValue) { + // Element-by-element string comparison so Swift `[String]` matches + // NSArray bridged from JSON-parsed request payloads. + if arr1.count != arr2.count { return false } + for (a, b) in zip(arr1, arr2) { + if "\(a)" != "\(b)" { return false } + } } else if "\(value)" != "\(otherValue)" { return false } From 9a3418dadd193343deee260493002a181ef0dbc5 Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray <57103426+arifBurakDemiray@users.noreply.github.com> Date: Thu, 30 Apr 2026 19:05:40 +0300 Subject: [PATCH 24/42] Update CHANGELOG.md --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3453ab8c..62c39bd8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## XX.XX.XX +* Mitigated a race condition in the request queue that could drop or duplicate requests. +* Mitigated an issue where server config defaults overrode user-provided SDK limits. +* Mitigated an issue where invalid or unknown `sdkBehaviorSettings` keys were persisted. +* Mitigated an issue where listing-filter conflicts cleared keys across unrelated categories. +* Mitigated an issue where consent could be sent twice during initialization. + ## 26.1.1 * Added POST method support for contents. * Added robust resource loading checks before displaying content From 86a7ab87122c52219283568fde9eb324c3f5bb01 Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Thu, 30 Apr 2026 20:24:50 +0300 Subject: [PATCH 25/42] feat: single array modiciations as value --- CountlyTests/CountlyUserProfileTests.swift | 10 +++++----- CountlyUserDetails.m | 12 +++++------- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/CountlyTests/CountlyUserProfileTests.swift b/CountlyTests/CountlyUserProfileTests.swift index ca325203..93696a26 100644 --- a/CountlyTests/CountlyUserProfileTests.swift +++ b/CountlyTests/CountlyUserProfileTests.swift @@ -505,8 +505,8 @@ class CountlyUserProfileTests: CountlyBaseTestCase { } func getUserDataMap()-> [String: Any]{ - // $push/$pull/$addToSet always serialize as arrays (post first-write-as-array fix - // — server-accepted, prevents NSMutableString crash on second-call array merge). + // Single-call $push/$pull/$addToSet serialize as the bare value; + // accumulation across multiple calls upgrades to array. let userProperties = ["a12345": "My Property", "b12345": ["$inc": 1], "c12345": ["$inc": 10], @@ -514,9 +514,9 @@ class CountlyUserProfileTests: CountlyBaseTestCase { "e12345": ["$max": 100], "f12345": ["$min": 50], "g12345": ["$setOnce": 200], - "h12345": ["$addToSet": ["morning"]], - "i12345": ["$push": ["morning"]], - "j12345": ["$pull": ["morning"]] ]as [String : Any] + "h12345": ["$addToSet": "morning"], + "i12345": ["$push": "morning"], + "j12345": ["$pull": "morning"] ]as [String : Any] return userProperties; } diff --git a/CountlyUserDetails.m b/CountlyUserDetails.m index 5d22ed2f..5f72a768 100644 --- a/CountlyUserDetails.m +++ b/CountlyUserDetails.m @@ -394,19 +394,17 @@ - (void)doModification:(NSString *)mod key:(NSString *)key value:(id)value { ![mod isEqualToString:@"$push"] && ![mod isEqualToString:@"$addToSet"]) { self.customMods[truncatedKey] = @{mod: value}; - } else { - NSMutableArray *array; - if (self.customMods[truncatedKey] && [self.customMods[truncatedKey][mod] isKindOfClass:[NSArray class]]) { - array = [self.customMods[truncatedKey][mod] mutableCopy]; - } else { - array = [NSMutableArray array]; - } + } else if (self.customMods[truncatedKey] && self.customMods[truncatedKey][mod]) { + id existing = self.customMods[truncatedKey][mod]; + NSMutableArray *array = [existing isKindOfClass:[NSArray class]] ? [existing mutableCopy] : [NSMutableArray arrayWithObject:existing]; if ([value isKindOfClass:[NSArray class]]) { [array addObjectsFromArray:value]; } else { [array addObject:value]; } self.customMods[truncatedKey] = @{mod: array}; + } else { + self.customMods[truncatedKey] = @{mod: value}; } // Note: legacy modifier methods (setOnce/push/pull/etc.) deliberately do // NOT auto-flush events — preserves pre-existing request-timing behavior From 1ee4ce1a3e757ff133fbe0d6205de3a6aabe7649 Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Mon, 4 May 2026 14:19:47 +0300 Subject: [PATCH 26/42] chore: removee unnecessary flaggings --- CountlyUserDetails.m | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/CountlyUserDetails.m b/CountlyUserDetails.m index 5f72a768..d6032d88 100644 --- a/CountlyUserDetails.m +++ b/CountlyUserDetails.m @@ -83,7 +83,7 @@ - (NSString *)serializedUserDetails if (self.birthYear) { if ([self.birthYear isKindOfClass:NSNumber.class] && ((NSNumber *)self.birthYear).integerValue < 0) { - // Negative byear means "clear on server" — match Android semantics. + // Negative byear means "clear on server" userDictionary[kCountlyUDKeyBirthyear] = NSNull.null; } else { userDictionary[kCountlyUDKeyBirthyear] = self.birthYear; @@ -486,7 +486,7 @@ - (void)serializeStringField:(id)field NSString *str = (NSString *)field; if (str.length == 0) { - // Empty string means "clear on server" — match Android semantics. + // Empty string means "clear on server" userDictionary[key] = NSNull.null; return; } @@ -518,7 +518,7 @@ - (void)assignNamedField:(NSString *)key value:(id)value { self.pictureLocalPath = nil; } else { NSString *path = [value description]; - // Match Android: drop the path with a warning if the file isn't readable. + // drop the path with a warning if the file isn't readable. if (path.length > 0 && ![[NSFileManager defaultManager] isReadableFileAtPath:path]) { CLY_LOG_W(@"%s provided picture path file [%@] can not be opened", __FUNCTION__, path); self.pictureLocalPath = nil; @@ -561,7 +561,7 @@ - (void)filterAndLimitUserProperties:(NSMutableDictionary *)properties } } -// Match Android's `onUserPropertiesChanged`: when user properties change, flush any +// when user properties change, flush any // pending events first so they reach the server before the next user-details request. - (void)userPropertiesChanged { From 75d27adf77088ecf679147813b17b75197993756 Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Mon, 4 May 2026 15:07:56 +0300 Subject: [PATCH 27/42] feat: deprecate user interface --- CHANGELOG.md | 1 + Countly.h | 2 +- Countly.m | 4 ++-- CountlyConnectionManager.m | 12 ++++++------ CountlyConsentManager.m | 2 +- CountlyPersistency.m | 4 ++-- CountlyUserDetails.h | 2 +- 7 files changed, 14 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ec945082..4444455e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ * Deprecated the direct property setters on `CountlyUserDetails`: `name`, `username`, `email`, `organization`, `phone`, `gender`, `pictureURL`, `pictureLocalPath`, `birthYear`, `custom`. Use `setProperty:value:` or `setProperties:` instead. * Deprecated `set:value:`, `set:numberValue:`, `set:boolValue:` on `CountlyUserDetails`. Use `setProperty:value:` instead. * Deprecated `unSet:` on `CountlyUserDetails`. Use `setProperty:value:` with an empty string `@""` to clear a property on the server. +* Deprecated the `+ user` class accessor on `Countly`. Use `Countly.sharedInstance.userProfile` instead. ## 26.1.1 * Added POST method support for contents. diff --git a/Countly.h b/Countly.h index 338de2de..f35fa19f 100644 --- a/Countly.h +++ b/Countly.h @@ -602,7 +602,7 @@ NS_ASSUME_NONNULL_BEGIN * Returns @c CountlyUserDetails singleton to be used throughout the app. * @return The shared @c CountlyUserDetails object */ -+ (CountlyUserDetails *)user; ++ (CountlyUserDetails *)user DEPRECATED_MSG_ATTRIBUTE("Use -userProfile via Countly.sharedInstance instead (e.g., Countly.sharedInstance.userProfile)."); diff --git a/Countly.m b/Countly.m index 317b3b50..4f131b1f 100644 --- a/Countly.m +++ b/Countly.m @@ -169,10 +169,10 @@ - (void)startWithConfig:(CountlyConfig *)config if (config.providedUserProperties.count > 0) { CLY_LOG_I(@"%s applying providedUserProperties at init [%lu]", __FUNCTION__, (unsigned long)config.providedUserProperties.count); - [Countly.user setProperties:config.providedUserProperties]; + [Countly.sharedInstance.userProfile setProperties:config.providedUserProperties]; } - [Countly.user save]; + [Countly.sharedInstance.userProfile save]; // If something added related to server config, make sure to check CountlyServerConfig.notifySdkConfigChange [CountlyServerConfig.sharedInstance fetchServerConfig:config]; diff --git a/CountlyConnectionManager.m b/CountlyConnectionManager.m index 928c8912..ef203b4d 100644 --- a/CountlyConnectionManager.m +++ b/CountlyConnectionManager.m @@ -606,9 +606,9 @@ - (void)beginSession } #endif - if([Countly.user hasUnsyncedChanges]) + if ([CountlyUserDetails.sharedInstance hasUnsyncedChanges]) { - [Countly.user save]; + [CountlyUserDetails.sharedInstance save]; } isSessionStarted = YES; @@ -649,9 +649,9 @@ - (void)updateSession return; } - if([Countly.user hasUnsyncedChanges]) + if ([CountlyUserDetails.sharedInstance hasUnsyncedChanges]) { - [Countly.user save]; + [CountlyUserDetails.sharedInstance save]; } NSString* queryString = [[self queryEssentials] stringByAppendingFormat:@"&%@=%d", @@ -691,9 +691,9 @@ - (void)endSession - (void)sendEventsWithSaveIfNeeded { - if([Countly.user hasUnsyncedChanges]) + if ([CountlyUserDetails.sharedInstance hasUnsyncedChanges]) { - [Countly.user save]; + [CountlyUserDetails.sharedInstance save]; } else { diff --git a/CountlyConsentManager.m b/CountlyConsentManager.m index 6cbf8c38..ef9ca76c 100644 --- a/CountlyConsentManager.m +++ b/CountlyConsentManager.m @@ -309,7 +309,7 @@ - (void)setConsentForUserDetails:(BOOL)consentForUserDetails { CLY_LOG_D(@"Consent for UserDetails is given."); [CountlyCommon.sharedInstance recordOrientation]; - [Countly.user save]; + [CountlyUserDetails.sharedInstance save]; } else { diff --git a/CountlyPersistency.m b/CountlyPersistency.m index c50d7aa4..b14575d0 100644 --- a/CountlyPersistency.m +++ b/CountlyPersistency.m @@ -271,9 +271,9 @@ - (void)recordEvent:(CountlyEvent *)event callback:(CLYRequestCallback)callback { @synchronized (self.recordedEvents) { - if([Countly.user hasUnsyncedChanges]) + if ([CountlyUserDetails.sharedInstance hasUnsyncedChanges]) { - [Countly.user save]; + [CountlyUserDetails.sharedInstance save]; } [self.recordedEvents addObject:event]; diff --git a/CountlyUserDetails.h b/CountlyUserDetails.h index 3018cb29..5b2d0536 100644 --- a/CountlyUserDetails.h +++ b/CountlyUserDetails.h @@ -122,7 +122,7 @@ extern NSString* const kCountlyLocalPicturePath; /** * Returns @c CountlyUserDetails singleton to be used throughout the app. * @return The shared @c CountlyUserDetails object - * @discussion @c Countly.user convenience accessor can also be used. + * @discussion @c Countly.sharedInstance.userProfile convenience accessor can also be used and is the recommended way. */ + (instancetype)sharedInstance; From 03ad04fbedd64572be774fa5ef72483736faf04a Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray <57103426+arifBurakDemiray@users.noreply.github.com> Date: Mon, 11 May 2026 11:13:58 +0300 Subject: [PATCH 28/42] Update CHANGELOG.md --- CHANGELOG.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 71c984ab..91e2bcd9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,13 +1,10 @@ ## XX.XX.XX -<<<<<<< session_immediates * Mitigated an issue where non-queued requests were affected from request timeout settings. -======= * Mitigated a race condition in the request queue that could drop or duplicate requests. * Mitigated an issue where server config defaults overrode user-provided SDK limits. * Mitigated an issue where invalid or unknown `sdkBehaviorSettings` keys were persisted. * Mitigated an issue where listing-filter conflicts cleared keys across unrelated categories. * Mitigated an issue where consent could be sent twice during initialization. ->>>>>>> staging ## 26.1.1 * Added POST method support for contents. From dd278531d476b6ecdb4200f4a38019cb547943d8 Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Mon, 11 May 2026 15:40:44 +0300 Subject: [PATCH 29/42] feat: review changes --- Countly.xcodeproj/project.pbxproj | 4 + CountlyCommon.m | 17 +- CountlyConnectionManager.m | 8 +- CountlyContentBuilderInternal.m | 2 +- .../CountlyServerConfigValidationTests.swift | 468 ++++++++++++++++++ 5 files changed, 491 insertions(+), 8 deletions(-) create mode 100644 CountlyTests/CountlyServerConfigValidationTests.swift diff --git a/Countly.xcodeproj/project.pbxproj b/Countly.xcodeproj/project.pbxproj index b7bf6339..bf32b896 100644 --- a/Countly.xcodeproj/project.pbxproj +++ b/Countly.xcodeproj/project.pbxproj @@ -86,6 +86,7 @@ 96095A5F2F20105600FDE933 /* TouchDelegatingView.h in Headers */ = {isa = PBXBuildFile; fileRef = 96095A5E2F20105600FDE933 /* TouchDelegatingView.h */; }; 962485BA2D9E971800FA3C20 /* TestUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 962485B92D9E971400FA3C20 /* TestUtils.swift */; }; 96329DE02D9426F300BFD641 /* CountlyServerConfigTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96329DDF2D9426F300BFD641 /* CountlyServerConfigTests.swift */; }; + ABCDE001000000000000ABCD /* CountlyServerConfigValidationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABCDE002000000000000ABCD /* CountlyServerConfigValidationTests.swift */; }; 96329DE22D94299D00BFD641 /* ServerConfigBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96329DE12D94299D00BFD641 /* ServerConfigBuilder.swift */; }; 96329DE42D952F1500BFD641 /* MockURLProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96329DE32D952F1500BFD641 /* MockURLProtocol.swift */; }; 965A2E9C2DDDCDAC00F28F6A /* CountlyHealthTracker.m in Sources */ = {isa = PBXBuildFile; fileRef = 965A2E9B2DDDCDAC00F28F6A /* CountlyHealthTracker.m */; }; @@ -210,6 +211,7 @@ 96095A5E2F20105600FDE933 /* TouchDelegatingView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TouchDelegatingView.h; sourceTree = ""; }; 962485B92D9E971400FA3C20 /* TestUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestUtils.swift; sourceTree = ""; }; 96329DDF2D9426F300BFD641 /* CountlyServerConfigTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountlyServerConfigTests.swift; sourceTree = ""; }; + ABCDE002000000000000ABCD /* CountlyServerConfigValidationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountlyServerConfigValidationTests.swift; sourceTree = ""; }; 96329DE12D94299D00BFD641 /* ServerConfigBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerConfigBuilder.swift; sourceTree = ""; }; 96329DE32D952F1500BFD641 /* MockURLProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockURLProtocol.swift; sourceTree = ""; }; 965A2E9A2DDDCDAC00F28F6A /* CountlyHealthTracker.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CountlyHealthTracker.h; sourceTree = ""; }; @@ -268,6 +270,7 @@ 96329DE32D952F1500BFD641 /* MockURLProtocol.swift */, 96329DE12D94299D00BFD641 /* ServerConfigBuilder.swift */, 96329DDF2D9426F300BFD641 /* CountlyServerConfigTests.swift */, + ABCDE002000000000000ABCD /* CountlyServerConfigValidationTests.swift */, 1A5C4C962B35B0850032EE1F /* CountlyTests.swift */, 1A50D7042B3C5AA3009C6938 /* CountlyBaseTestCase.swift */, 1AFD79012B3EF82C00772FBD /* CountlyTests-Bridging-Header.h */, @@ -557,6 +560,7 @@ 96CB10A12F04B3A100D1E2F0 /* CountlyContentBuilderTests.swift in Sources */, 96329DE42D952F1500BFD641 /* MockURLProtocol.swift in Sources */, 96329DE02D9426F300BFD641 /* CountlyServerConfigTests.swift in Sources */, + ABCDE001000000000000ABCD /* CountlyServerConfigValidationTests.swift in Sources */, 3966DBCF2C11EE270002ED97 /* CountlyDeviceIDTests.swift in Sources */, 3964A3E72C2AF8E90091E677 /* CountlySegmentationTests.swift in Sources */, 96DA74BB2D9FB687006FA6FF /* MockFeedbackWidget.swift in Sources */, diff --git a/CountlyCommon.m b/CountlyCommon.m index 0252e88a..5a9a2855 100644 --- a/CountlyCommon.m +++ b/CountlyCommon.m @@ -344,12 +344,17 @@ - (NSURLSession *)URLSession - (NSURLSession *)ImmediateURLSession { - NSURLSessionConfiguration *immediateConfig = [NSURLSessionConfiguration defaultSessionConfiguration]; - if (CountlyConnectionManager.sharedInstance.URLSessionConfiguration) - { - immediateConfig.HTTPAdditionalHeaders = CountlyConnectionManager.sharedInstance.URLSessionConfiguration.HTTPAdditionalHeaders; - } - + // Base on the user-provided URLSessionConfiguration so that things like + // protocolClasses (test mocks), cookie policy, etc. are preserved. + // If none was provided, fall back to default session configuration. + NSURLSessionConfiguration *userConfig = CountlyConnectionManager.sharedInstance.URLSessionConfiguration; + NSURLSessionConfiguration *immediateConfig = userConfig ? [userConfig copy] : [NSURLSessionConfiguration defaultSessionConfiguration]; + + // Immediate requests must not be constrained by the SDK's configured + // request timeout — reset to the system defaults. + immediateConfig.timeoutIntervalForRequest = 60; + immediateConfig.timeoutIntervalForResource = 7 * 24 * 60 * 60; + return [NSURLSession sessionWithConfiguration:immediateConfig]; } diff --git a/CountlyConnectionManager.m b/CountlyConnectionManager.m index 3b18749f..facb9d39 100644 --- a/CountlyConnectionManager.m +++ b/CountlyConnectionManager.m @@ -370,7 +370,6 @@ - (void)proceedOnQueue self.connection = [self.URLSession dataTaskWithRequest:request completionHandler:^(NSData * data, NSURLResponse * response, NSError * error) { self.connection = nil; - atomic_store(&self->_isProcessingQueue, NO); NSDate *endTimeRequest = [NSDate date]; long duration = (long)[endTimeRequest timeIntervalSinceDate:startTimeRequest]; @@ -402,6 +401,11 @@ - (void)proceedOnQueue [CountlyPersistency.sharedInstance saveToFile]; + // Clear the processing flag only after the head has been removed + // from the queue. Clearing it earlier would let a concurrent + // proceedOnQueue caller re-send the same head request. + atomic_store(&self->_isProcessingQueue, NO); + if(CountlyServerConfig.sharedInstance.backoffMechanism && [self backoff:duration queryString:queryString]){ CLY_LOG_D(@"%s, backed off dropping proceeding the queue", __FUNCTION__); self.startTime = nil; @@ -431,6 +435,7 @@ - (void)proceedOnQueue [CountlyHealthTracker.sharedInstance logFailedNetworkRequestWithStatusCode:((NSHTTPURLResponse*)response).statusCode errorResponse: [data cly_stringUTF8]]; [CountlyHealthTracker.sharedInstance saveState]; self.startTime = nil; + atomic_store(&self->_isProcessingQueue, NO); } } else @@ -452,6 +457,7 @@ - (void)proceedOnQueue [CountlyPersistency.sharedInstance saveToFile]; #endif self.startTime = nil; + atomic_store(&self->_isProcessingQueue, NO); } }]; diff --git a/CountlyContentBuilderInternal.m b/CountlyContentBuilderInternal.m index 8a111abd..ef913820 100644 --- a/CountlyContentBuilderInternal.m +++ b/CountlyContentBuilderInternal.m @@ -209,7 +209,7 @@ - (void)fetchContents:(void (^)(void))failureCallback contentId:(NSString *)cont [self setRequestQueueLockedThreadSafe:YES]; - NSURLSessionTask *dataTask = [[CountlyCommon.sharedInstance ImmediateURLSession] dataTaskWithRequest:[self fetchContentsRequest] completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { + NSURLSessionTask *dataTask = [[CountlyCommon.sharedInstance ImmediateURLSession] dataTaskWithRequest:[self fetchContentsRequest:contentId] completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { // IMMEDIATE REQUEST to find them better in search if (error) { CLY_LOG_I(@"%s fetch content details failed: [%@]", __FUNCTION__, error); diff --git a/CountlyTests/CountlyServerConfigValidationTests.swift b/CountlyTests/CountlyServerConfigValidationTests.swift new file mode 100644 index 00000000..64bd0219 --- /dev/null +++ b/CountlyTests/CountlyServerConfigValidationTests.swift @@ -0,0 +1,468 @@ +import XCTest +@testable import Countly + +/// Tests for the server-config validation, persistence cleanup, +/// per-category filter conflict resolution, and consent-during-init +/// behaviors introduced alongside the session_immediates changes. +class CountlyServerConfigValidationTests: CountlyBaseTestCase { + + private let persistencyKey = "kCountlyServerConfigPersistencyKey" + private var localCountlyInstances: [Countly] = [] + + override func setUp() { + super.setUp() + TestUtils.cleanup() + localCountlyInstances = [] + UserDefaults.standard.removeObject(forKey: persistencyKey) + UserDefaults.standard.synchronize() + } + + override func tearDown() { + for instance in localCountlyInstances { + instance.halt(true) + } + localCountlyInstances = [] + if Countly.sharedInstance() != nil { + Countly.sharedInstance().halt(true) + } + UserDefaults.standard.removeObject(forKey: persistencyKey) + UserDefaults.standard.synchronize() + super.tearDown() + } + + private func createLocalCountly() -> Countly { + let instance = Countly() + localCountlyInstances.append(instance) + return instance + } + + private func setStoredServerConfig(_ payload: [String: Any]) { + UserDefaults.standard.set(payload, forKey: persistencyKey) + UserDefaults.standard.synchronize() + } + + private func storedServerConfig() -> [String: Any] { + return UserDefaults.standard.object(forKey: persistencyKey) as? [String: Any] ?? [:] + } + + private func storedConfigDictionary() -> [String: Any] { + return storedServerConfig()["c"] as? [String: Any] ?? [:] + } + + private func makeServerConfig(_ config: [String: Any]) -> [String: Any] { + return [ + "v": 1, + "t": Int(Date().timeIntervalSince1970), + "c": config + ] + } + + // MARK: - Type validation + + /// Invalid types for boolean keys must be stripped from the persisted config + /// and must not change the corresponding SDK property. + func test_validation_boolKeyWithInvalidType_isStrippedAndDefaultsKept() { + setStoredServerConfig(makeServerConfig([ + "tracking": "not a bool", // wrong type + "networking": 42, // wrong type (integer, not bool) + "crt": true // valid bool + ])) + + let cfg = TestUtils.createBaseConfig() + cfg.enableServerConfiguration = true + cfg.manualSessionHandling = true + let countly = createLocalCountly() + countly.start(with: cfg) + + // Valid bool was applied; invalid ones fell back to default (YES) + XCTAssertTrue(CountlyServerConfig.sharedInstance().trackingEnabled()) + XCTAssertTrue(CountlyServerConfig.sharedInstance().networkingEnabled()) + XCTAssertTrue(CountlyServerConfig.sharedInstance().crashReportingEnabled()) + + let persisted = storedConfigDictionary() + XCTAssertNil(persisted["tracking"], "Invalid bool type should be stripped from persistence") + XCTAssertNil(persisted["networking"], "Invalid bool type should be stripped from persistence") + XCTAssertEqual(persisted["crt"] as? Bool, true) + } + + /// Invalid types for integer keys must be stripped from the persisted config. + func test_validation_integerKeyWithInvalidType_isStripped() { + setStoredServerConfig(makeServerConfig([ + "eqs": "not a number", // wrong type + "rqs": 500 // valid + ])) + + let cfg = TestUtils.createBaseConfig() + cfg.enableServerConfiguration = true + cfg.manualSessionHandling = true + let countly = createLocalCountly() + countly.start(with: cfg) + + XCTAssertEqual(500, CountlyServerConfig.sharedInstance().requestQueueSize()) + + let persisted = storedConfigDictionary() + XCTAssertNil(persisted["eqs"], "Invalid integer type should be stripped") + XCTAssertEqual(persisted["rqs"] as? Int, 500) + } + + /// Integer values below the configured minimum must be stripped. + /// `czi` (contentZoneInterval) requires minValue=16. + func test_validation_integerBelowMin_isStripped_contentZoneInterval() { + setStoredServerConfig(makeServerConfig([ + "czi": 5 // below minValue=16 + ])) + + let cfg = TestUtils.createBaseConfig() + cfg.enableServerConfiguration = true + cfg.manualSessionHandling = true + let countly = createLocalCountly() + countly.start(with: cfg) + + let persisted = storedConfigDictionary() + XCTAssertNil(persisted["czi"], "czi below min should be stripped from persistence") + } + + /// Integer values at/above the minimum should be accepted and persisted. + func test_validation_integerAtMin_isAccepted_contentZoneInterval() { + setStoredServerConfig(makeServerConfig([ + "czi": 16 + ])) + + let cfg = TestUtils.createBaseConfig() + cfg.enableServerConfiguration = true + cfg.manualSessionHandling = true + let countly = createLocalCountly() + countly.start(with: cfg) + + let persisted = storedConfigDictionary() + XCTAssertEqual(persisted["czi"] as? Int, 16) + } + + /// `dort` (dropOldRequestTime) has minValue=0 — zero is valid and must remain. + func test_validation_integerZero_isAccepted_dropOldRequestTime() { + setStoredServerConfig(makeServerConfig([ + "dort": 0 + ])) + + let cfg = TestUtils.createBaseConfig() + cfg.enableServerConfiguration = true + cfg.manualSessionHandling = true + let countly = createLocalCountly() + countly.start(with: cfg) + + let persisted = storedConfigDictionary() + XCTAssertEqual(persisted["dort"] as? Int, 0) + } + + /// Unknown keys in the config object must be stripped from persistence. + func test_validation_unknownKeys_areStripped() { + setStoredServerConfig(makeServerConfig([ + "tracking": true, // known + "totally_unknown_key": "abc", // unknown + "another_unknown": 123 // unknown + ])) + + let cfg = TestUtils.createBaseConfig() + cfg.enableServerConfiguration = true + cfg.manualSessionHandling = true + let countly = createLocalCountly() + countly.start(with: cfg) + + let persisted = storedConfigDictionary() + XCTAssertEqual(persisted["tracking"] as? Bool, true) + XCTAssertNil(persisted["totally_unknown_key"]) + XCTAssertNil(persisted["another_unknown"]) + } + + // MARK: - User-provided SDK limits preservation + + /// When server config returns valid keys but does not include a particular + /// limit, the user-provided SDK limit must not be overridden by a stale default. + func test_userLimits_preserved_whenServerConfigOmitsThem() { + // Server config provides a valid SC payload that excludes eventQueueSize + // and contentZoneInterval, but includes another valid key so that + // notifySdkConfigChange is invoked. + setStoredServerConfig(makeServerConfig([ + "tracking": true + ])) + + let cfg = TestUtils.createBaseConfig() + cfg.enableServerConfiguration = true + cfg.manualSessionHandling = true + cfg.eventSendThreshold = 7 // user-provided + cfg.storedRequestsLimit = 555 // user-provided + let countly = createLocalCountly() + countly.start(with: cfg) + + XCTAssertEqual(7, CountlyPersistency.sharedInstance().eventSendThreshold, + "User-provided eventSendThreshold must not be overridden when server config omits eqs") + XCTAssertEqual(555, CountlyPersistency.sharedInstance().storedRequestsLimit, + "User-provided storedRequestsLimit must not be overridden when server config omits rqs") + } + + // MARK: - Per-category filter conflict resolution + + /// Drives the SDK's server-config fetch through a mocked URL session so + /// that `mergeBehaviorSettings` runs with a controlled response payload. + private func mockURLSessionConfig(returning configDict: [String: Any]) -> URLSessionConfiguration { + let response = makeServerConfig(configDict) + let data = try! JSONSerialization.data(withJSONObject: response) + MockURLProtocol.requestHandler = { request in + let url = request.url?.absoluteString ?? "" + if url.contains("method=sc") { + return (data, HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: nil, headerFields: nil), nil) + } + return ("{\"result\":\"fail\"}".data(using: .utf8), + HTTPURLResponse(url: request.url!, statusCode: 400, httpVersion: nil, headerFields: nil), nil) + } + let config = URLSessionConfiguration.ephemeral + config.protocolClasses = [MockURLProtocol.self] + return config + } + + /// When a new merged config provides a whitelist for one category, the + /// stored blacklist for that same category must be removed — but the + /// filter for an unrelated category must be preserved. + func test_perCategoryFilterConflict_doesNotClearUnrelatedCategories() { + // Seed persisted config with both an event blacklist and a user + // property blacklist already present. + setStoredServerConfig(makeServerConfig([ + "eb": ["blocked_event"], + "upb": ["blocked_prop"] + ])) + + // Mock a server response that only introduces an event whitelist (`ew`). + let cfg = TestUtils.createBaseConfig() + cfg.manualSessionHandling = true + cfg.urlSessionConfiguration = mockURLSessionConfig(returning: [ + "ew": ["allowed_event"] + ]) + + let countly = createLocalCountly() + countly.start(with: cfg) + + TestUtils.sleep(2) { + let persisted = self.storedConfigDictionary() + XCTAssertNil(persisted["eb"], "eb must be removed because new config provided ew") + XCTAssertEqual(persisted["ew"] as? [String], ["allowed_event"]) + XCTAssertEqual(persisted["upb"] as? [String], ["blocked_prop"], + "upb must be preserved — the new config did not touch the user-property category") + } + } + + /// Conflict resolution must work independently per category — providing + /// both a segmentation blacklist and a user-property whitelist must + /// remove only their same-category counterparts. + func test_perCategoryFilterConflict_independentAcrossCategories() { + setStoredServerConfig(makeServerConfig([ + "sw": ["s_keep"], + "upb": ["u_blocked"] + ])) + + let cfg = TestUtils.createBaseConfig() + cfg.manualSessionHandling = true + cfg.urlSessionConfiguration = mockURLSessionConfig(returning: [ + "sb": ["s_blocked"], + "upw": ["u_allowed"] + ]) + + let countly = createLocalCountly() + countly.start(with: cfg) + + TestUtils.sleep(2) { + let persisted = self.storedConfigDictionary() + XCTAssertNil(persisted["sw"], "sw must be removed because new config provided sb") + XCTAssertEqual(persisted["sb"] as? [String], ["s_blocked"]) + XCTAssertNil(persisted["upb"], "upb must be removed because new config provided upw") + XCTAssertEqual(persisted["upw"] as? [String], ["u_allowed"]) + } + } + + // MARK: - Consent during init + + /// When server config requires consent and the user config does not, + /// consent must be sent exactly once during init (not twice). + func test_consent_sentOnceDuringInit_whenServerConfigEnablesConsent() { + setStoredServerConfig(makeServerConfig([ + "cr": true + ])) + + let cfg = TestUtils.createBaseConfig() + cfg.enableServerConfiguration = true + cfg.manualSessionHandling = true + // Do NOT set requiresConsent on the user config — only the server config requires it. + let countly = createLocalCountly() + countly.start(with: cfg) + + TestUtils.sleep(1) { + let rq = TestUtils.getCurrentRQ() ?? [] + let consentRequests = rq.filter { $0.contains("consent=") } + XCTAssertEqual(1, consentRequests.count, + "Consent should be queued exactly once during init, got requests: \(rq)") + } + } + + // MARK: - ImmediateURLSession + + /// The new ImmediateURLSession must propagate any HTTP additional headers + /// configured on the URLSessionConfiguration. + func test_immediateURLSession_propagatesAdditionalHeaders() { + let cfg = TestUtils.createBaseConfig() + let sessionConfig = URLSessionConfiguration.default + sessionConfig.httpAdditionalHeaders = ["X-Immediate-Test": "value1"] + cfg.urlSessionConfiguration = sessionConfig + cfg.manualSessionHandling = true + + Countly.sharedInstance().start(with: cfg) + + let immediate = CountlyCommon.sharedInstance().immediateURLSession() + let headers = immediate.configuration.httpAdditionalHeaders as? [String: String] + XCTAssertEqual(headers?["X-Immediate-Test"], "value1", + "ImmediateURLSession must inherit configured HTTPAdditionalHeaders") + } + + /// ImmediateURLSession must also propagate `protocolClasses` so that + /// callers (e.g. tests using a mock protocol) and pinned certs continue + /// to work for non-queued requests. + func test_immediateURLSession_propagatesProtocolClasses() { + // Provide a noop handler so background traffic during SDK init does + // not trigger MockURLProtocol's "handler not set" XCTFail. + MockURLProtocol.requestHandler = { request in + return ( + "{\"result\":\"ok\"}".data(using: .utf8), + HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: nil, headerFields: nil), + nil + ) + } + + let cfg = TestUtils.createBaseConfig() + let sessionConfig = URLSessionConfiguration.ephemeral + sessionConfig.protocolClasses = [MockURLProtocol.self] + cfg.urlSessionConfiguration = sessionConfig + cfg.manualSessionHandling = true + + Countly.sharedInstance().start(with: cfg) + + let immediate = CountlyCommon.sharedInstance().immediateURLSession() + let names = (immediate.configuration.protocolClasses ?? []).map { String(describing: $0) } + XCTAssertTrue(names.contains("MockURLProtocol"), + "ImmediateURLSession must propagate user-provided protocolClasses, got: \(names)") + } + + /// ImmediateURLSession must not be constrained by the SDK's configured + /// request timeout. The timeout on the returned session should match the + /// system default (60s), regardless of whatever the queued URLSession was + /// set to. + func test_immediateURLSession_doesNotInheritSdkRequestTimeout() { + let cfg = TestUtils.createBaseConfig() + cfg.manualSessionHandling = true + cfg.requestTimeoutDuration = 5 // SDK timeout small enough to detect + + Countly.sharedInstance().start(with: cfg) + + let immediate = CountlyCommon.sharedInstance().immediateURLSession() + XCTAssertEqual(60.0, immediate.configuration.timeoutIntervalForRequest, accuracy: 0.001, + "ImmediateURLSession should not adopt the SDK's request timeout — that's the bug being mitigated") + } + + // MARK: - Server config persistence is the cleaned dictionary + + /// After populate, the persisted config must reflect the cleaned version + /// of the dictionary (no invalid types, no unknown keys, no out-of-range + /// values). Stale entries from a previous launch must not leak through. + func test_serverConfig_persistedDictionaryReflectsCleaning() { + setStoredServerConfig(makeServerConfig([ + "tracking": true, // kept + "networking": "not a bool", // stripped (type) + "czi": 5, // stripped (below min 16) + "unknown_x": [1, 2, 3] // stripped (unknown) + ])) + + let cfg = TestUtils.createBaseConfig() + cfg.enableServerConfiguration = true + cfg.manualSessionHandling = true + let countly = createLocalCountly() + countly.start(with: cfg) + + let persisted = storedConfigDictionary() + XCTAssertEqual(persisted["tracking"] as? Bool, true) + XCTAssertNil(persisted["networking"]) + XCTAssertNil(persisted["czi"]) + XCTAssertNil(persisted["unknown_x"]) + } + + // MARK: - Consent gated by hasFinishedInit + + /// hasFinishedInit must be NO before start completes and YES afterwards. + /// This is what gates the early consent-send path in populateServerConfig. + func test_hasFinishedInit_flipsToTrueAfterStartCompletes() { + XCTAssertFalse(CountlyCommon.sharedInstance().hasFinishedInit, + "hasFinishedInit must start NO before init") + + let cfg = TestUtils.createBaseConfig() + cfg.manualSessionHandling = true + Countly.sharedInstance().start(with: cfg) + + XCTAssertTrue(CountlyCommon.sharedInstance().hasFinishedInit, + "hasFinishedInit must be YES after start completes") + + Countly.sharedInstance().halt(true) + XCTAssertFalse(CountlyCommon.sharedInstance().hasFinishedInit, + "hasFinishedInit must reset to NO on halt") + } + + // MARK: - Connection manager queue-concurrency guard + + /// Calling `proceedOnQueue` rapidly when a request is in flight must not + /// double-send the same request. The atomic `isProcessingQueue` flag + /// guards against re-entrancy. + /// + /// This test counts outbound hits for a uniquely-marked request and + /// asserts at most one is sent even when proceedOnQueue is invoked + /// concurrently from many threads. + func test_proceedOnQueue_doesNotDoubleSendHeadRequest() { + let lock = NSLock() + var markerHits = 0 + MockURLProtocol.requestHandler = { request in + let url = request.url?.absoluteString ?? "" + if url.contains("unique_marker=x") { + lock.lock() + markerHits += 1 + lock.unlock() + } + return ( + "{\"result\":\"ok\"}".data(using: .utf8), + HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: nil, headerFields: nil), + nil + ) + } + + let cfg = TestUtils.createBaseConfig() + cfg.manualSessionHandling = true + let sessionConfig = URLSessionConfiguration.ephemeral + sessionConfig.protocolClasses = [MockURLProtocol.self] + cfg.urlSessionConfiguration = sessionConfig + + Countly.sharedInstance().start(with: cfg) + + // Queue a single uniquely-marked request and pound proceedOnQueue from many threads. + Countly.sharedInstance().addDirectRequest(["unique_marker": "x"]) + let group = DispatchGroup() + for _ in 0..<32 { + group.enter() + DispatchQueue.global().async { + CountlyConnectionManager.sharedInstance().proceedOnQueue() + group.leave() + } + } + group.wait() + + TestUtils.sleep(2) { + lock.lock() + let hits = markerHits + lock.unlock() + XCTAssertEqual(1, hits, + "The marker request must be sent exactly once even with 32 concurrent proceedOnQueue calls; got \(hits)") + } + } +} From 9eaaf4160d5b55fdeb07ea64cedee304afccf3ae Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Mon, 11 May 2026 18:50:17 +0300 Subject: [PATCH 30/42] fix: after review --- Countly.xcodeproj/project.pbxproj | 4 + CountlyCommon.m | 8 +- CountlyTests/CountlyScreenMetricsTests.swift | 174 +++++++++++++++++++ 3 files changed, 181 insertions(+), 5 deletions(-) create mode 100644 CountlyTests/CountlyScreenMetricsTests.swift diff --git a/Countly.xcodeproj/project.pbxproj b/Countly.xcodeproj/project.pbxproj index b7bf6339..831704e0 100644 --- a/Countly.xcodeproj/project.pbxproj +++ b/Countly.xcodeproj/project.pbxproj @@ -93,6 +93,7 @@ 9673567F2EC60CD400C742D8 /* TestURLProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9673567E2EC60CD400C742D8 /* TestURLProtocol.swift */; }; 968426812BF2302C007B303E /* CountlyConnectionManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 968426802BF2302C007B303E /* CountlyConnectionManagerTests.swift */; }; 96CB10A12F04B3A100D1E2F0 /* CountlyContentBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96CB10A02F04B3A100D1E2F0 /* CountlyContentBuilderTests.swift */; }; + AB10000011112222333344A1 /* CountlyScreenMetricsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB10000011112222333344A0 /* CountlyScreenMetricsTests.swift */; }; 9691B7652F1FA35500A6ADCB /* CountlyOverlayWindow.h in Headers */ = {isa = PBXBuildFile; fileRef = 9691B7642F1FA35500A6ADCB /* CountlyOverlayWindow.h */; }; 9691B7672F1FA35C00A6ADCB /* CountlyOverlayWindow.m in Sources */ = {isa = PBXBuildFile; fileRef = 9691B7662F1FA35C00A6ADCB /* CountlyOverlayWindow.m */; }; 969E5BCE2ECC4D3200AB406A /* CountlyConsentManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 969E5BCD2ECC4D2C00AB406A /* CountlyConsentManagerTests.swift */; }; @@ -218,6 +219,7 @@ 9673567E2EC60CD400C742D8 /* TestURLProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestURLProtocol.swift; sourceTree = ""; }; 968426802BF2302C007B303E /* CountlyConnectionManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountlyConnectionManagerTests.swift; sourceTree = ""; }; 96CB10A02F04B3A100D1E2F0 /* CountlyContentBuilderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountlyContentBuilderTests.swift; sourceTree = ""; }; + AB10000011112222333344A0 /* CountlyScreenMetricsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountlyScreenMetricsTests.swift; sourceTree = ""; }; 9691B7642F1FA35500A6ADCB /* CountlyOverlayWindow.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CountlyOverlayWindow.h; sourceTree = ""; }; 9691B7662F1FA35C00A6ADCB /* CountlyOverlayWindow.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CountlyOverlayWindow.m; sourceTree = ""; }; 969E5BCD2ECC4D2C00AB406A /* CountlyConsentManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountlyConsentManagerTests.swift; sourceTree = ""; }; @@ -273,6 +275,7 @@ 1AFD79012B3EF82C00772FBD /* CountlyTests-Bridging-Header.h */, 968426802BF2302C007B303E /* CountlyConnectionManagerTests.swift */, 96CB10A02F04B3A100D1E2F0 /* CountlyContentBuilderTests.swift */, + AB10000011112222333344A0 /* CountlyScreenMetricsTests.swift */, 96E680412BFF89AC0091E105 /* CountlyCrashReporterTests.swift */, EBD1642D826B471A80175BE3 /* CountlyWebViewManagerTests.swift */, 265156CB5F59417EA14BAC2F /* CountlyWebViewManager+Tests.h */, @@ -555,6 +558,7 @@ 3979E47D2C0760E900FA1CA4 /* CountlyUserProfileTests.swift in Sources */, 968426812BF2302C007B303E /* CountlyConnectionManagerTests.swift in Sources */, 96CB10A12F04B3A100D1E2F0 /* CountlyContentBuilderTests.swift in Sources */, + AB10000011112222333344A1 /* CountlyScreenMetricsTests.swift in Sources */, 96329DE42D952F1500BFD641 /* MockURLProtocol.swift in Sources */, 96329DE02D9426F300BFD641 /* CountlyServerConfigTests.swift in Sources */, 3966DBCF2C11EE270002ED97 /* CountlyDeviceIDTests.swift in Sources */, diff --git a/CountlyCommon.m b/CountlyCommon.m index 568cdc94..715511b2 100644 --- a/CountlyCommon.m +++ b/CountlyCommon.m @@ -352,7 +352,6 @@ - (bool) hasTopNotch:(UIEdgeInsets)safeArea - (CGSize)getWindowSize{ #if (TARGET_OS_IOS) UIWindow *window = nil; - CGFloat screenScale; if (@available(iOS 13.0, *)) { for (UIScene *scene in UIApplication.sharedApplication.connectedScenes) { @@ -361,15 +360,14 @@ - (CGSize)getWindowSize{ break; } } - screenScale = window.traitCollection.displayScale; } else { window = [[UIApplication sharedApplication].delegate window]; - screenScale = [UIScreen mainScreen].scale; } if (!window) return CGSizeZero; - - UIEdgeInsets safeArea = UIEdgeInsetsZero; + + CGSize size = window.bounds.size; + if (@available(iOS 11.0, *)) { UIEdgeInsets safeArea = window.safeAreaInsets; if([self hasTopNotch:safeArea] || CountlyContentBuilderInternal.sharedInstance.webViewDisplayOption == SAFE_AREA){ diff --git a/CountlyTests/CountlyScreenMetricsTests.swift b/CountlyTests/CountlyScreenMetricsTests.swift new file mode 100644 index 00000000..62f7e5a4 --- /dev/null +++ b/CountlyTests/CountlyScreenMetricsTests.swift @@ -0,0 +1,174 @@ +// +// CountlyScreenMetricsTests.swift +// CountlyTests +// +// Covers screen-metric helpers refactored on the `mainscreen_dpc` branch: +// - CountlyCommon.getWindowSize() — used for content/feedback view sizing. +// - CountlyDeviceInfo.resolution() — used for the `_resolution` metric. +// +// These tests are written to work both in a hosted XCTest bundle (where +// UIApplication has connected window scenes) and in a logic-test bundle +// (where no scene is attached). They assert *property* invariants rather +// than concrete sizes so they hold across hosts and devices. +// + +import XCTest +@testable import Countly + +#if os(iOS) +class CountlyScreenMetricsTests: CountlyBaseTestCase { + + override func setUp() { + super.setUp() + Countly.sharedInstance().halt(true) + } + + override func tearDown() { + Countly.sharedInstance().halt(true) + super.tearDown() + } + + // MARK: - getWindowSize + + /** + * Regression guard: getWindowSize must not crash and must return a + * finite, non-negative CGSize. The `mainscreen_dpc` refactor previously + * broke the build by referencing `size` after removing its declaration; + * this test exists primarily so the build/runtime path stays exercised. + */ + func test_getWindowSize_returnsFiniteNonNegativeSize() { + let size = CountlyCommon.sharedInstance().getWindowSize() + + XCTAssertFalse(size.width.isNaN, "width should not be NaN") + XCTAssertFalse(size.height.isNaN, "height should not be NaN") + XCTAssertGreaterThanOrEqual(size.width, 0, "width should be >= 0") + XCTAssertGreaterThanOrEqual(size.height, 0, "height should be >= 0") + } + + /** + * getWindowSize must be deterministic — repeated calls without any + * window-scene churn should return the same value. + */ + func test_getWindowSize_isStableAcrossCalls() { + let first = CountlyCommon.sharedInstance().getWindowSize() + let second = CountlyCommon.sharedInstance().getWindowSize() + let third = CountlyCommon.sharedInstance().getWindowSize() + + XCTAssertEqual(first.width, second.width, accuracy: 0.0001) + XCTAssertEqual(first.height, second.height, accuracy: 0.0001) + XCTAssertEqual(second.width, third.width, accuracy: 0.0001) + XCTAssertEqual(second.height, third.height, accuracy: 0.0001) + } + + /** + * If a UIWindowScene is present (hosted test), getWindowSize should not + * exceed the underlying window's bounds — safe-area adjustments may + * shrink the size but never grow it. If no scene is present, the + * function returns CGSizeZero, which satisfies the same invariant. + */ + func test_getWindowSize_doesNotExceedWindowBounds() { + let size = CountlyCommon.sharedInstance().getWindowSize() + + if let window = firstWindow() { + XCTAssertLessThanOrEqual(size.width, window.bounds.width, + "Reported width should not exceed window width") + XCTAssertLessThanOrEqual(size.height, window.bounds.height, + "Reported height should not exceed window height") + } else { + XCTAssertEqual(size.width, 0, accuracy: 0.0001, + "Expected CGSizeZero when no window scene is attached") + XCTAssertEqual(size.height, 0, accuracy: 0.0001, + "Expected CGSizeZero when no window scene is attached") + } + } + + // MARK: - resolution + + /** + * resolution() must return a "WIDTHxHEIGHT" formatted string with two + * non-negative numeric components. This is the contract consumed by the + * `_resolution` metric and by the content-builder query parameters. + */ + func test_resolution_isWellFormedString() { + guard let resolution = CountlyDeviceInfo.resolution() else { + XCTFail("resolution() returned nil on iOS") + return + } + + let parts = resolution.components(separatedBy: "x") + XCTAssertEqual(parts.count, 2, + "Expected WIDTHxHEIGHT format, got: \(resolution)") + guard parts.count == 2 else { return } + + guard let width = Double(parts[0]), let height = Double(parts[1]) else { + XCTFail("resolution() components should be numeric, got: \(resolution)") + return + } + XCTAssertGreaterThanOrEqual(width, 0, "resolution width should be >= 0 (got: \(resolution))") + XCTAssertGreaterThanOrEqual(height, 0, "resolution height should be >= 0 (got: \(resolution))") + } + + /** + * On iOS 13+ the refactor reads bounds/scale from the first + * UIWindowScene's window. When a scene is connected, the returned + * string must match `bounds.size * displayScale` for that window. + * When no scene is connected, the implementation falls through with + * zero values and emits "0x0" — locks in the iOS 13+ no-fallback + * behavior so regressions are visible. + */ + func test_resolution_matchesFirstSceneOrIsZero() { + guard let resolution = CountlyDeviceInfo.resolution() else { + XCTFail("resolution() returned nil on iOS") + return + } + + if let window = firstWindow() { + let scale = window.traitCollection.displayScale + let expected = "\(percentG(window.bounds.width * scale))x\(percentG(window.bounds.height * scale))" + XCTAssertEqual(resolution, expected, + "resolution should reflect first window scene's pixel size") + } else { + XCTAssertEqual(resolution, "0x0", + "iOS 13+ without a window scene should report 0x0 (no UIScreen fallback)") + } + } + + /** + * The metrics dictionary returned by CountlyDeviceInfo.metrics() + * must include the `_resolution` key — this is the SDK-facing + * contract that ends up in `/i` requests. + */ + func test_resolution_appearsInMetricsString() { + guard let metricsString = CountlyDeviceInfo.metrics() else { + XCTFail("metrics() returned nil") + return + } + XCTAssertTrue(metricsString.contains("_resolution"), + "metrics() should include the _resolution key, got: \(metricsString)") + } + + // MARK: - Helpers + + private func firstWindow() -> UIWindow? { + if #available(iOS 13.0, *) { + for scene in UIApplication.shared.connectedScenes { + if let windowScene = scene as? UIWindowScene, + let window = windowScene.windows.first { + return window + } + } + return nil + } else { + return UIApplication.shared.delegate?.window ?? nil + } + } + + /// Mirrors Objective-C `%g` formatting used by `CountlyDeviceInfo.resolution`. + /// `%g` uses the shorter of `%e` or `%f`, trims trailing zeros, and drops + /// the decimal point when not needed. `String(format:)` with `%g` matches + /// this behavior on Apple platforms. + private func percentG(_ value: CGFloat) -> String { + return String(format: "%g", Double(value)) + } +} +#endif From a3c8eb81339f235bc139dc339983414442beab72 Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Thu, 11 Jun 2026 15:02:43 +0300 Subject: [PATCH 31/42] feat: more detailed test --- CHANGELOG.md | 1 - CountlyTests/CountlyUserProfileTests.swift | 145 ++++++++++++++++++--- 2 files changed, 129 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 43f32083..f0926816 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,6 @@ * Added a new user properties functions on `CountlyUserDetails`: * `setProperty:value:` for setting a single predefined or custom user property. * `setProperties:` for setting multiple predefined and custom properties in one call. - * `clear` to drop queued user property changes without sending them to the server. * Empty-string-as-clear semantics for predefined string fields (`name`, `username`, `email`, `organization`, `phone`, `gender`, `picture`, `picturePath`) — passing `@""` sends `null` to the server to clear the field. * Negative-value-as-clear semantics for `byear` — passing a negative `NSNumber` sends `null` to clear the field. * Pending events are flushed before the next user details request when a user property changes via this new functions, so they reach the server in the right order. diff --git a/CountlyTests/CountlyUserProfileTests.swift b/CountlyTests/CountlyUserProfileTests.swift index 93696a26..9b92e312 100644 --- a/CountlyTests/CountlyUserProfileTests.swift +++ b/CountlyTests/CountlyUserProfileTests.swift @@ -864,30 +864,79 @@ class CountlyUserProfileTests: CountlyBaseTestCase { } /// Gap E parity: changing a user property triggers an event-queue flush - /// (Android `onUserPropertiesChanged`). When events are pending and a - /// property change occurs, the events get drained into the request queue. + /// (Android `onUserPropertiesChanged`). Walks through an interleaved + /// set-property / record-event sequence and validates EQ and RQ sizes + /// after every single call, then the exact request order and contents. func test_propertyChange_flushesPendingEvents() { + purgePersistedState() + let config = createBaseConfig() config.requiresConsent = false config.manualSessionHandling = true Countly.sharedInstance().start(with: config) + // Clean start: nothing queued anywhere. + XCTAssertEqual(0, TestUtils.getCurrentEQ()?.count ?? -1) + XCTAssertEqual(0, TestUtils.getCurrentRQ()?.count ?? -1) + + // Events recorded with a clean property cache stay in the EQ. Countly.sharedInstance().recordEvent("eventA") - Countly.sharedInstance().recordEvent("eventB") + XCTAssertEqual(1, TestUtils.getCurrentEQ()?.count ?? -1) + XCTAssertEqual(0, TestUtils.getCurrentRQ()?.count ?? -1) - // Before the property change, events sit in the event queue. - XCTAssertEqual(2, TestUtils.getCurrentEQ()?.count ?? 0) + Countly.sharedInstance().recordEvent("eventB") + XCTAssertEqual(2, TestUtils.getCurrentEQ()?.count ?? -1) + XCTAssertEqual(0, TestUtils.getCurrentRQ()?.count ?? -1) + // Property change drains the EQ into a single events request; the + // property itself only goes into the cache (no user_details yet). Countly.user().setProperty("level", value: 42) - - // After the property change, the event queue should be drained. - XCTAssertEqual(0, TestUtils.getCurrentEQ()?.count ?? 0, + XCTAssertEqual(0, TestUtils.getCurrentEQ()?.count ?? -1, "Auto-flush should drain the event queue when a user property changes") + XCTAssertEqual(1, TestUtils.getCurrentRQ()?.count ?? -1, + "Drained events should land in the RQ as one events request") + + // Recording an event with a dirty property cache saves the cache + // first (user_details request), then queues the event in the EQ. + Countly.sharedInstance().recordEvent("eventC") + XCTAssertEqual(1, TestUtils.getCurrentEQ()?.count ?? -1) + XCTAssertEqual(2, TestUtils.getCurrentRQ()?.count ?? -1, + "Recording an event with pending property changes should enqueue a user_details request first") + + // Second property change flushes eventC the same way. + Countly.user().setProperty("level", value: 43) + XCTAssertEqual(0, TestUtils.getCurrentEQ()?.count ?? -1) + XCTAssertEqual(3, TestUtils.getCurrentRQ()?.count ?? -1) + + // Explicit save drains the cache into a final user_details request. + Countly.user().save() + XCTAssertEqual(0, TestUtils.getCurrentEQ()?.count ?? -1) + XCTAssertEqual(4, TestUtils.getCurrentRQ()?.count ?? -1) + + // Saving again with a clean cache must not enqueue anything. + Countly.user().save() + XCTAssertEqual(0, TestUtils.getCurrentEQ()?.count ?? -1) + XCTAssertEqual(4, TestUtils.getCurrentRQ()?.count ?? -1, + "save() with no unsynced changes should be a no-op") + + // Strict order: events always precede the user_details request that + // carries the property state they were recorded under. + guard let rq = TestUtils.getCurrentRQ(), rq.count == 4 else { + XCTFail("Expected exactly 4 requests, got: \(TestUtils.getCurrentRQ() ?? [])"); return + } + validateEvents(request: rq[0], keysToCheck: ["eventA", "eventB"]) + assertOnlyCustomProperty(rq[1], key: "level", expected: 42) + validateEvents(request: rq[2], keysToCheck: ["eventC"]) + assertOnlyCustomProperty(rq[3], key: "level", expected: 43) } /// Gap D parity: `providedUserProperties` from CountlyConfig is applied - /// at SDK start and saved automatically. + /// at SDK start and saved automatically, leaving a clean property cache. + /// Then interleaves event recording and a property overwrite, validating + /// EQ and RQ sizes after every call plus exact request order and contents. func test_providedUserPropertiesAtInit_appliedAndSaved() { + purgePersistedState() + let config = createBaseConfig() config.requiresConsent = false config.manualSessionHandling = true @@ -897,18 +946,82 @@ class CountlyUserProfileTests: CountlyBaseTestCase { ] Countly.sharedInstance().start(with: config) - guard let rq = TestUtils.getCurrentRQ(), - let request = rq.first(where: { $0.contains("user_details=") }) else { - XCTFail("Expected a user_details request from providedUserProperties"); return + // Init properties are saved automatically at start: exactly one + // user_details request, empty EQ, no leftover cache. + XCTAssertEqual(0, TestUtils.getCurrentEQ()?.count ?? -1) + XCTAssertEqual(1, TestUtils.getCurrentRQ()?.count ?? -1, + "providedUserProperties should produce exactly one user_details request at init") + + // Cache was cleared by the init save: this event must stay in the + // EQ without triggering another user_details request. + Countly.sharedInstance().recordEvent("postInitEvent") + XCTAssertEqual(1, TestUtils.getCurrentEQ()?.count ?? -1) + XCTAssertEqual(1, TestUtils.getCurrentRQ()?.count ?? -1, + "Recording an event after the init save should not enqueue a new user_details request") + + // Overwriting an init property flushes the pending event into the + // RQ; the new value only goes into the cache. + Countly.user().setProperty("favorite_color", value: "red") + XCTAssertEqual(0, TestUtils.getCurrentEQ()?.count ?? -1, + "Property change should drain the pending event") + XCTAssertEqual(2, TestUtils.getCurrentRQ()?.count ?? -1) + + // Recording another event saves the dirty cache first, then queues + // the event. + Countly.sharedInstance().recordEvent("secondEvent") + XCTAssertEqual(1, TestUtils.getCurrentEQ()?.count ?? -1) + XCTAssertEqual(3, TestUtils.getCurrentRQ()?.count ?? -1) + + guard let rq = TestUtils.getCurrentRQ(), rq.count == 3 else { + XCTFail("Expected exactly 3 requests, got: \(TestUtils.getCurrentRQ() ?? [])"); return } - let parsed = TestUtils.parseQueryString(request) + // Request 0: init save — named field at top level, custom key nested. + let parsed = TestUtils.parseQueryString(rq[0]) guard let userDetails = parsed["user_details"] as? [String: Any] else { - XCTFail("user_details missing"); return + XCTFail("user_details missing in init request: \(rq[0])"); return } XCTAssertEqual("init@example.com", userDetails["email"] as? String) - let custom = userDetails["custom"] as? [String: Any] - XCTAssertEqual("blue", custom?["favorite_color"] as? String) + guard let custom = userDetails["custom"] as? [String: Any] else { + XCTFail("custom missing in init user_details: \(userDetails)"); return + } + XCTAssertEqual(1, custom.count, "Init custom payload should hold only favorite_color") + XCTAssertEqual("blue", custom["favorite_color"] as? String) + + // Request 1: the event recorded under favorite_color=blue. + validateEvents(request: rq[1], keysToCheck: ["postInitEvent"]) + + // Request 2: the overwritten property value. + assertOnlyCustomProperty(rq[2], key: "favorite_color", expected: "red") + + // The second event is still pending in the EQ. + XCTAssertEqual("secondEvent", TestUtils.getCurrentEQ()?.first?.key) + } + + /// A previous (possibly crashed) run can leave persisted requests and a + /// generated device ID on disk; setUp's halt(true) is a no-op before the + /// first start in a process. Start + halt(true) wipes storage so the + /// exact queue-size assertions begin from a truly clean slate. + private func purgePersistedState() { + let purgeConfig = createBaseConfig() + purgeConfig.requiresConsent = false + purgeConfig.manualSessionHandling = true + Countly.sharedInstance().start(with: purgeConfig) + Countly.sharedInstance().halt(true) + } + + /// Asserts the request carries a user_details payload whose custom + /// dictionary contains exactly one entry: `key` with `expected` value. + private func assertOnlyCustomProperty(_ request: String, key: String, expected: Any) { + let parsed = TestUtils.parseQueryString(request) + guard let userDetails = parsed["user_details"] as? [String: Any] else { + XCTFail("user_details missing in request: \(request)"); return + } + guard let custom = userDetails["custom"] as? [String: Any] else { + XCTFail("custom missing in user_details: \(userDetails)"); return + } + XCTAssertEqual(1, custom.count, "Expected exactly one custom property, got: \(custom)") + XCTAssertEqual("\(expected)", "\(custom[key] ?? "nil")", "Custom property '\(key)' mismatch") } } From 6c72f3acacfd35cb7da0b65ec723072b4c2aa9d4 Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Thu, 11 Jun 2026 16:00:46 +0300 Subject: [PATCH 32/42] feat: update with latest findings --- CHANGELOG.md | 3 +- CountlyTests/CountlyUserProfileTests.swift | 39 ++++++++++++------ CountlyUserDetails.m | 46 +++++++++++++--------- 3 files changed, 56 insertions(+), 32 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f0926816..f7c2a276 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,8 +2,7 @@ * Added a new user properties functions on `CountlyUserDetails`: * `setProperty:value:` for setting a single predefined or custom user property. * `setProperties:` for setting multiple predefined and custom properties in one call. - * Empty-string-as-clear semantics for predefined string fields (`name`, `username`, `email`, `organization`, `phone`, `gender`, `picture`, `picturePath`) — passing `@""` sends `null` to the server to clear the field. - * Negative-value-as-clear semantics for `byear` — passing a negative `NSNumber` sends `null` to clear the field. + * Empty string clears predefined string fields (`name`, `username`, `email`, `organization`, `phone`, `gender`, `picture`, `picturePath`). * Pending events are flushed before the next user details request when a user property changes via this new functions, so they reach the server in the right order. * Added `providedUserProperties` to `CountlyConfig` to set initial user properties that are applied and saved automatically right after `start`. * Added `setMaxValueSizePicture:` to `CountlySDKLimitsConfig` to control the maximum size of picture URLs and picture paths independently of other value limits (default 4096). diff --git a/CountlyTests/CountlyUserProfileTests.swift b/CountlyTests/CountlyUserProfileTests.swift index 9b92e312..2ee08eaa 100644 --- a/CountlyTests/CountlyUserProfileTests.swift +++ b/CountlyTests/CountlyUserProfileTests.swift @@ -789,9 +789,10 @@ class CountlyUserProfileTests: CountlyBaseTestCase { // MARK: - iOS-specific Android-parity tests (gaps B, C, A, F, E) - /// Gap B parity: empty string for a named string field serializes as null - /// (clear-on-server semantics). - func test_emptyStringForNamedField_serializesAsNull() { + /// Gap B: empty string for a named string field is sent to the server + /// as-is — the server clears a field on "" but ignores null, so the + /// Android-style "" → null conversion would defeat the clear. + func test_emptyStringForNamedField_isSentAsIs() { let config = createBaseConfig() config.requiresConsent = false config.manualSessionHandling = true @@ -814,29 +815,43 @@ class CountlyUserProfileTests: CountlyBaseTestCase { XCTFail("user_details missing"); return } - // After serialization, name was empty string — should be NSNull (clear). - XCTAssertTrue(userDetails["name"] is NSNull, - "Empty string should serialize as null. Got: \(String(describing: userDetails["name"]))") + // The empty string must survive serialization untouched (clear-on-server). + XCTAssertEqual("", userDetails["name"] as? String, + "Empty string should be sent as-is. Got: \(String(describing: userDetails["name"]))") } - /// Gap C parity: negative byear serializes as null (clear-on-server). + /// Gap C parity: a negative byear is accepted and serialized as null on + /// the wire (Android behavior). It counts as a property change, so it + /// flushes pending events and dirties the cache like any other value. func test_negativeBirthYear_serializesAsNull() { + purgePersistedState() + let config = createBaseConfig() config.requiresConsent = false config.manualSessionHandling = true Countly.sharedInstance().start(with: config) + // A negative byear is a real property change: pending events flush. + Countly.sharedInstance().recordEvent("pendingEvent") + XCTAssertEqual(1, TestUtils.getCurrentEQ()?.count ?? -1) + XCTAssertEqual(0, TestUtils.getCurrentRQ()?.count ?? -1) + Countly.user().setProperty("byear", value: -1) + XCTAssertEqual(0, TestUtils.getCurrentEQ()?.count ?? -1, + "byear change should flush pending events") + XCTAssertEqual(1, TestUtils.getCurrentRQ()?.count ?? -1) + Countly.user().save() + XCTAssertEqual(2, TestUtils.getCurrentRQ()?.count ?? -1) - guard let rq = TestUtils.getCurrentRQ(), - let request = rq.first(where: { $0.contains("user_details=") }) else { - XCTFail("No user_details request"); return + guard let rq = TestUtils.getCurrentRQ(), rq.count == 2 else { + XCTFail("Expected exactly 2 requests, got: \(TestUtils.getCurrentRQ() ?? [])"); return } + validateEvents(request: rq[0], keysToCheck: ["pendingEvent"]) - let parsed = TestUtils.parseQueryString(request) + let parsed = TestUtils.parseQueryString(rq[1]) guard let userDetails = parsed["user_details"] as? [String: Any] else { - XCTFail("user_details missing"); return + XCTFail("user_details missing in request: \(rq[1])"); return } XCTAssertTrue(userDetails["byear"] is NSNull, "Negative byear should serialize as null. Got: \(String(describing: userDetails["byear"]))") diff --git a/CountlyUserDetails.m b/CountlyUserDetails.m index d6032d88..6f89f1da 100644 --- a/CountlyUserDetails.m +++ b/CountlyUserDetails.m @@ -83,7 +83,7 @@ - (NSString *)serializedUserDetails if (self.birthYear) { if ([self.birthYear isKindOfClass:NSNumber.class] && ((NSNumber *)self.birthYear).integerValue < 0) { - // Negative byear means "clear on server" + // Negative byear is sent as null on the wire (Android behavior). userDictionary[kCountlyUDKeyBirthyear] = NSNull.null; } else { userDictionary[kCountlyUDKeyBirthyear] = self.birthYear; @@ -445,8 +445,8 @@ - (void)setPropertiesInternal:(NSDictionary *)data { for (NSUInteger i = 0; i < kCountlyUDNamedFieldsCount; i++) { if ([kCountlyUDNamedFields[i] isEqualToString:key]) { isNamed = YES; - [self assignNamedField:key value:value]; - anyChange = YES; + if ([self assignNamedField:key value:value]) + anyChange = YES; break; } } @@ -485,17 +485,17 @@ - (void)serializeStringField:(id)field } NSString *str = (NSString *)field; - if (str.length == 0) { - // Empty string means "clear on server" - userDictionary[key] = NSNull.null; - return; - } - + // Empty strings are sent to the server as-is: the server clears a field + // on "" but ignores null, so converting "" to null (Android behavior) + // would defeat the clear. userDictionary[key] = isPicture ? [str cly_truncatedPictureValue:explanation] : [str cly_truncatedValue:explanation]; } -- (void)assignNamedField:(NSString *)key value:(id)value { +// Returns YES when the field was actually assigned, NO when the value was +// rejected — so the caller only marks a change (and flushes events) for +// values that will end up in the next user-details request. +- (BOOL)assignNamedField:(NSString *)key value:(id)value { BOOL isNull = (value == [NSNull null]); id stringOrNull = isNull ? NSNull.null : [value description]; @@ -529,18 +529,28 @@ - (void)assignNamedField:(NSString *)key value:(id)value { } else if ([key isEqualToString:kCountlyUDKeyBirthyear]) { if (isNull) { self.birthYear = NSNull.null; - } else if ([value isKindOfClass:[NSNumber class]]) { - self.birthYear = (NSNumber *)value; - } else if ([value isKindOfClass:[NSString class]]) { - NSNumberFormatter *formatter = [NSNumberFormatter new]; - NSNumber *parsed = [formatter numberFromString:(NSString *)value]; - if (parsed) { - self.birthYear = parsed; + } else { + NSNumber *parsed = nil; + if ([value isKindOfClass:[NSNumber class]]) { + parsed = (NSNumber *)value; + } else if ([value isKindOfClass:[NSString class]]) { + NSNumberFormatter *formatter = [NSNumberFormatter new]; + parsed = [formatter numberFromString:(NSString *)value]; + if (!parsed) { + CLY_LOG_W(@"%s incorrect byear number format: %@", __FUNCTION__, value); + return NO; + } } else { - CLY_LOG_W(@"%s incorrect byear number format: %@", __FUNCTION__, value); + return NO; } + + self.birthYear = parsed; } + } else { + return NO; } + + return YES; } - (void)filterAndLimitUserProperties:(NSMutableDictionary *)properties From 224446fc595896a414b1e231c14a5792af60bf60 Mon Sep 17 00:00:00 2001 From: turtledreams <62231246+turtledreams@users.noreply.github.com> Date: Fri, 12 Jun 2026 19:16:35 +0900 Subject: [PATCH 33/42] Update CHANGELOG.md --- CHANGELOG.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f7c2a276..b0efab3b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,8 +2,6 @@ * Added a new user properties functions on `CountlyUserDetails`: * `setProperty:value:` for setting a single predefined or custom user property. * `setProperties:` for setting multiple predefined and custom properties in one call. - * Empty string clears predefined string fields (`name`, `username`, `email`, `organization`, `phone`, `gender`, `picture`, `picturePath`). - * Pending events are flushed before the next user details request when a user property changes via this new functions, so they reach the server in the right order. * Added `providedUserProperties` to `CountlyConfig` to set initial user properties that are applied and saved automatically right after `start`. * Added `setMaxValueSizePicture:` to `CountlySDKLimitsConfig` to control the maximum size of picture URLs and picture paths independently of other value limits (default 4096). * Improved `$push` / `$pull` / `$addToSet` wire format: values are now always sent as arrays so multiple consecutive calls on the same key accumulate correctly. From e1f6f7b46c0bfafd9009e59d0d8e21459c381af8 Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Tue, 23 Jun 2026 14:32:59 +0300 Subject: [PATCH 34/42] fix: temp id leak on contents --- CHANGELOG.md | 1 + CountlyContentBuilderInternal.m | 8 ++- CountlyRemoteConfigInternal.m | 29 ++++++++-- CountlyTests/CountlyContentBuilderTests.swift | 55 +++++++++++++++++++ 4 files changed, 88 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b0efab3b..b5062f39 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ * Mitigated an issue where invalid or unknown `sdkBehaviorSettings` keys were persisted. * Mitigated an issue where listing-filter conflicts cleared keys across unrelated categories. * Mitigated an issue where consent could be sent twice during initialization. +* Mitigated an issue where content fetches and remote config requests could be sent while in temporary device ID mode, creating a `CLYTemporaryDeviceID` user on the server. * Deprecated the direct property setters on `CountlyUserDetails`: `name`, `username`, `email`, `organization`, `phone`, `gender`, `pictureURL`, `pictureLocalPath`, `birthYear`, `custom`. Use `setProperty:value:` or `setProperties:` instead. * Deprecated `set:value:`, `set:numberValue:`, `set:boolValue:` on `CountlyUserDetails`. Use `setProperty:value:` instead. diff --git a/CountlyContentBuilderInternal.m b/CountlyContentBuilderInternal.m index ef913820..57d5be48 100644 --- a/CountlyContentBuilderInternal.m +++ b/CountlyContentBuilderInternal.m @@ -197,7 +197,13 @@ - (void)fetchContents:(void (^)(void))failureCallback contentId:(NSString *)cont if (!CountlyServerConfig.sharedInstance.networkingEnabled) return; - + + if (CountlyDeviceInfo.sharedInstance.isDeviceIDTemporary) + { + CLY_LOG_W(@"%s content can not be fetched while in temporary device ID mode", __FUNCTION__); + return; + } + if(_isCurrentlyContentShown){ CLY_LOG_I(@"%s a content is already shown, skipping" ,__FUNCTION__); return; diff --git a/CountlyRemoteConfigInternal.m b/CountlyRemoteConfigInternal.m index 9bb12663..b9c31f79 100644 --- a/CountlyRemoteConfigInternal.m +++ b/CountlyRemoteConfigInternal.m @@ -193,9 +193,14 @@ - (void)fetchRemoteConfigForKeys:(NSArray *)keys omitKeys:(NSArray *)omitKeys i CLY_LOG_D(@"'fetchRemoteConfigForKeys' is aborted: SDK Networking is disabled from server config!"); return; } + if (CountlyDeviceInfo.sharedInstance.isDeviceIDTemporary) + { + CLY_LOG_W(@"'fetchRemoteConfigForKeys' is aborted: device ID is in temporary mode!"); + return; + } if (!completionHandler) return; - + NSURLRequest* request = [self remoteConfigRequestForKeys:keys omitKeys:omitKeys isLegacy:isLegacy]; NSURLSessionTask* task = [CountlyCommon.sharedInstance.ImmediateURLSession dataTaskWithRequest:request completionHandler:^(NSData* data, NSURLResponse* response, NSError* error) { @@ -493,9 +498,14 @@ - (void)testingDownloadAllVariantsInternal:(void (^)(CLYRequestResult response, CLY_LOG_D(@"'fetchVariantForKeys' is aborted: SDK Networking is disabled from server config!"); return; } + if (CountlyDeviceInfo.sharedInstance.isDeviceIDTemporary) + { + CLY_LOG_W(@"'fetchVariantForKeys' is aborted: device ID is in temporary mode!"); + return; + } if (!completionHandler) return; - + NSURLRequest* request = [self downloadVariantsRequest]; NSURLSessionTask* task = [CountlyCommon.sharedInstance.ImmediateURLSession dataTaskWithRequest:request completionHandler:^(NSData* data, NSURLResponse* response, NSError* error) { @@ -607,7 +617,13 @@ - (void)testingEnrollIntoVariantInternal:(NSString *)key variantName:(NSString * CLY_LOG_D(@"'enrollInRCVariant' is aborted: 'variantName' is not valid"); return; } - + + if (CountlyDeviceInfo.sharedInstance.isDeviceIDTemporary) + { + CLY_LOG_W(@"'enrollInRCVariant' is aborted: device ID is in temporary mode!"); + return; + } + NSURLRequest* request = [self enrollInVarianRequestForKey:key variantName:variantName]; NSURLSessionTask* task = [CountlyCommon.sharedInstance.ImmediateURLSession dataTaskWithRequest:request completionHandler:^(NSData* data, NSURLResponse* response, NSError* error) { @@ -720,9 +736,14 @@ - (void)testingDownloaExperimentInfoInternal:(void (^)(CLYRequestResult response CLY_LOG_D(@"'testingDownloaExperimentInfoInternal' is aborted: SDK Networking is disabled from server config!"); return; } + if (CountlyDeviceInfo.sharedInstance.isDeviceIDTemporary) + { + CLY_LOG_W(@"'testingDownloaExperimentInfoInternal' is aborted: device ID is in temporary mode!"); + return; + } if (!completionHandler) return; - + NSURLRequest* request = [self downloadExperimentInfoRequest]; NSURLSessionTask* task = [CountlyCommon.sharedInstance.ImmediateURLSession dataTaskWithRequest:request completionHandler:^(NSData* data, NSURLResponse* response, NSError* error) { diff --git a/CountlyTests/CountlyContentBuilderTests.swift b/CountlyTests/CountlyContentBuilderTests.swift index 564f7039..683a2255 100644 --- a/CountlyTests/CountlyContentBuilderTests.swift +++ b/CountlyTests/CountlyContentBuilderTests.swift @@ -232,5 +232,60 @@ class CountlyContentBuilderTests: CountlyBaseTestCase { XCTAssertEqual(fetchCount, countAfterFirstFetch, "No additional content fetches should occur after exitContentZone") } + + /** + *
+     * Reproduction + regression test for the temporary-device-ID leak.
+     *
+     * The content fetch is an immediate request (it bypasses the persisted request queue and its
+     * temporary-ID guard) and it carries device_id. While in temporary device ID mode it must not be
+     * sent, otherwise a "CLYTemporaryDeviceID" user is created on the server.
+     *
+     * 1- Init SDK with MockURLProtocol, then enter temporary device ID mode
+     * 2- Enter content zone with zero initial delay
+     * 3- Verify NO request to /o/sdk/content is made while in temporary mode (inverted expectation)
+     * 4- Exit content zone, switch to a real device ID (leaving temporary mode)
+     * 5- Re-enter content zone and verify the content fetch now fires normally
+     * 
+ */ + func test_fetchContent_isBlocked_inTemporaryDeviceIDMode() { + let noFetchInTempMode = self.expectation(description: "No content fetch while in temporary device ID mode") + noFetchInTempMode.isInverted = true + let fetchAfterExit = self.expectation(description: "Content fetch resumes after leaving temporary mode") + fetchAfterExit.assertForOverFulfill = false + + var inTemporaryMode = true + MockURLProtocol.requestHandler = { request in + if let url = request.url?.absoluteString, url.contains("/o/sdk/content") { + if inTemporaryMode { + noFetchInTempMode.fulfill() + } else { + fetchAfterExit.fulfill() + } + } + let response = HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: "HTTP/1.1", headerFields: nil)! + return ("{}".data(using: .utf8)!, response, nil) + } + + let config = createContentTestConfig() + Countly.sharedInstance().start(with: config) + Countly.sharedInstance().enableTemporaryDeviceIDMode() + + CountlyContentBuilderInternal.sharedInstance().contentInitialDelay = 0 + CountlyContentBuilderInternal.sharedInstance().enterContentZone([]) + + // Phase 1: nothing should hit the content endpoint while in temporary mode + wait(for: [noFetchInTempMode], timeout: 5) + + // Phase 2: leave temporary mode by assigning a real device ID, then re-enter the content zone + CountlyContentBuilderInternal.sharedInstance().exitContentZone() + inTemporaryMode = false + Countly.sharedInstance().changeDeviceIDWithoutMerge("real_user_after_temp") + + CountlyContentBuilderInternal.sharedInstance().contentInitialDelay = 0 + CountlyContentBuilderInternal.sharedInstance().enterContentZone([]) + + wait(for: [fetchAfterExit], timeout: 15) + } } #endif From a42c8dcbec45dc97158b5072441f04bfc7a49504 Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Tue, 23 Jun 2026 15:13:39 +0300 Subject: [PATCH 35/42] feat: add time fields to sbs --- CountlyServerConfig.m | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/CountlyServerConfig.m b/CountlyServerConfig.m index 829ccf3e..ad527d4c 100644 --- a/CountlyServerConfig.m +++ b/CountlyServerConfig.m @@ -620,8 +620,9 @@ - (void)fetchServerConfig:(CountlyConfig *)config - (NSURLRequest *)serverConfigRequest { - NSString *queryString = [NSString stringWithFormat:@"%@=%@&%@=%@&%@=%@&%@=%@&%@=%@", kCountlyQSKeyMethod, kCountlySCKeySC, kCountlyQSKeyAppKey, CountlyConnectionManager.sharedInstance.appKey.cly_URLEscaped, kCountlyQSKeyDeviceID, CountlyDeviceInfo.sharedInstance.deviceID.cly_URLEscaped, - kCountlyQSKeySDKName, CountlyCommon.sharedInstance.SDKName, kCountlyQSKeySDKVersion, CountlyCommon.sharedInstance.SDKVersion]; + NSString *queryString = [CountlyConnectionManager.sharedInstance queryEssentials]; + + queryString = [queryString stringByAppendingFormat:@"&%@=%@", kCountlyQSKeyMethod, kCountlySCKeySC]; queryString = [queryString stringByAppendingFormat:@"&%@=%@", kCountlyAppVersionKey, CountlyDeviceInfo.appVersion]; From 5b2e83e1d510a9c518994eab0a82e2af24baf57d Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Tue, 23 Jun 2026 15:20:48 +0300 Subject: [PATCH 36/42] feat: re-fetch sbs after exiting temp id --- Countly.m | 8 +- CountlyServerConfig.m | 3 + CountlyTests/CountlyServerConfigTests.swift | 83 ++++++++++++++++++++- 3 files changed, 90 insertions(+), 4 deletions(-) diff --git a/Countly.m b/Countly.m index 1259d534..c4355f18 100644 --- a/Countly.m +++ b/Countly.m @@ -10,6 +10,7 @@ @interface Countly () { NSTimer* timer; BOOL isSuspended; + CountlyConfig* _startConfig; } @end @@ -103,7 +104,8 @@ - (void)startWithConfig:(CountlyConfig *)config CountlyCommon.sharedInstance.internalLogLevel = config.internalLogLevel; config = [self checkAndFixInternalLimitsConfig:config]; - + _startConfig = config; + if (config.disableSDKBehaviorSettingsUpdates) { [CountlyServerConfig.sharedInstance disableSDKBehaviourSettings]; } @@ -739,8 +741,10 @@ - (void)setIDInternal:(NSString *)deviceID onServer:(BOOL)onServer [CountlyConnectionManager.sharedInstance proceedOnQueue]; + [CountlyServerConfig.sharedInstance fetchServerConfig:_startConfig]; + [CountlyRemoteConfigInternal.sharedInstance downloadRemoteConfigAutomatically]; - + [CountlyHealthTracker.sharedInstance sendHealthCheck]; return; diff --git a/CountlyServerConfig.m b/CountlyServerConfig.m index 829ccf3e..73e5f876 100644 --- a/CountlyServerConfig.m +++ b/CountlyServerConfig.m @@ -571,7 +571,10 @@ - (void)fetchServerConfig:(CountlyConfig *)config } if (CountlyDeviceInfo.sharedInstance.isDeviceIDTemporary) + { + CLY_LOG_W(@"%s, fetch is skipped while in temporary device ID mode", __FUNCTION__); return; + } _lastFetchTimestamp = NSDate.date.timeIntervalSince1970 * 1000; diff --git a/CountlyTests/CountlyServerConfigTests.swift b/CountlyTests/CountlyServerConfigTests.swift index c717c0f7..7945bf37 100644 --- a/CountlyTests/CountlyServerConfigTests.swift +++ b/CountlyTests/CountlyServerConfigTests.swift @@ -152,12 +152,91 @@ class CountlyServerConfigTests: CountlyBaseTestCase { * Verifies that the configuration is properly handled when received through the request generator. */ func test_serverConfig_withImmediateRequestGenerator() throws { - + try initServerConfigWithValues { config, serverConfig in config.urlSessionConfiguration = createUrlSessionConfigForResponse(serverConfig) } } - + + /** + * Regression test for SDK Behavior Settings (server config) deferral in temporary device ID mode. + * + * The server config fetch is an immediate request carrying device_id. When the SDK starts in + * temporary device ID mode it must not be sent (it would create a CLYTemporaryDeviceID user), but + * it must not be lost either: it should be deferred and run once a real device ID is assigned. + * + * 1- Start the SDK already in temporary device ID mode, with a mock session that counts method=sc requests + * 2- Verify NO server config request is sent while in temporary mode (inverted expectation) + * 3- Assign a real device ID (leaving temporary mode) + * 4- Verify the deferred server config request now runs + */ + func test_serverConfigFetch_isDeferred_untilLeavingTemporaryDeviceIDMode() { + let realDeviceID = "real_user_after_temp" + + // A distinctive server config so we can prove the re-fetched response is actually applied + // through the cached config (i.e. the cached config is valid and the re-fetch works end-to-end). + let builder = ServerConfigBuilder() + .networking(true) + .sessionUpdateInterval(120) + .limitKeyLength(89) + .limitValueSize(43) + let serverConfigJson = builder.build() + + let noFetchInTempMode = self.expectation(description: "No server config fetch while in temporary device ID mode") + noFetchInTempMode.isInverted = true + let fetchAfterExit = self.expectation(description: "Deferred server config fetch runs after leaving temporary mode") + fetchAfterExit.assertForOverFulfill = false + + var inTemporaryMode = true + var capturedSCRequestURL: String? + MockURLProtocol.requestHandler = { request in + let urlString = request.url?.absoluteString ?? "" + if urlString.contains("method=sc") { + if inTemporaryMode { + noFetchInTempMode.fulfill() + } else { + capturedSCRequestURL = urlString + fetchAfterExit.fulfill() + } + let response = HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: nil, headerFields: nil)! + return (serverConfigJson.data(using: .utf8), response, nil) + } + let response = HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: nil, headerFields: nil)! + return ("{}".data(using: .utf8), response, nil) + } + + let config = TestUtils.createBaseConfig() + let sessionConfig = URLSessionConfiguration.ephemeral + sessionConfig.protocolClasses = [MockURLProtocol.self] + config.urlSessionConfiguration = sessionConfig + config.enableTemporaryDeviceIDMode() + + let countly = createLocalCountly() + countly.start(with: config) + + // Phase 1: nothing should hit the server config endpoint while in temporary mode + wait(for: [noFetchInTempMode], timeout: 4) + + // Phase 2: leaving temporary mode should run the deferred fetch using the cached config + inTemporaryMode = false + countly.changeDeviceIDWithoutMerge(realDeviceID) + + wait(for: [fetchAfterExit], timeout: 10) + + // The re-fetch must carry the real device ID (the cached config / ordering must not leak the + // temporary device ID, which is what created the CLYTemporaryDeviceID user in the first place). + XCTAssertNotNil(capturedSCRequestURL, "A server config request should have been made after leaving temporary mode") + XCTAssertTrue(capturedSCRequestURL?.contains("device_id=\(realDeviceID)") ?? false, "SBS re-fetch must use the real device ID") + XCTAssertFalse(capturedSCRequestURL?.contains("CLYTemporaryDeviceID") ?? true, "SBS re-fetch must not leak the temporary device ID") + // The re-fetch must use the cached config's app key. + XCTAssertTrue(capturedSCRequestURL?.contains("app_key=\(TestUtils.commonAppKey)") ?? false, "SBS re-fetch must use the cached config app key") + + // The re-fetched response must actually be applied to the SDK. This proves the cached config is + // valid and the re-fetch path works end-to-end (not just that a request was sent). + TestUtils.sleep(2, { builder.validateAgainst() }) + } + + /** * Tests that all features work correctly with default server configuration. * Verifies that all SDK features (sessions, events, views, crashes, etc.) function as expected From 587e674b45446e829001deaffa4ef10dc2a79b8d Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Tue, 23 Jun 2026 16:04:52 +0300 Subject: [PATCH 37/42] feat: xros support --- Countly.podspec | 2 ++ Countly.xcodeproj/project.pbxproj | 4 ++-- CountlyCommon.m | 2 +- Package.swift | 11 ++++++----- 4 files changed, 11 insertions(+), 8 deletions(-) diff --git a/Countly.podspec b/Countly.podspec index d162fc6b..c745521d 100644 --- a/Countly.podspec +++ b/Countly.podspec @@ -14,12 +14,14 @@ Pod::Spec.new do |s| s.osx.deployment_target = '10.14' s.watchos.deployment_target = '4.0' s.tvos.deployment_target = '10.0' + s.visionos.deployment_target = '1.0' s.subspec 'Core' do |core| core.source_files = '*.{h,m}' core.public_header_files = 'Countly.h', 'CountlyUserDetails.h', 'CountlyConfig.h', 'CountlyFeedbackWidget.h', 'CountlyRCData.h', 'CountlyRemoteConfig.h', 'CountlyViewTracking.h', 'CountlyExperimentInformation.h', 'CountlyAPMConfig.h', 'CountlySDKLimitsConfig.h', 'Resettable.h', "CountlyCrashesConfig.h", "CountlyCrashData.h", "CountlyContentBuilder.h", "CountlyExperimentalConfig.h", "CountlyContentConfig.h", "CountlyFeedbacks.h" core.preserve_path = 'countly_dsym_uploader.sh' core.ios.frameworks = ['Foundation', 'UIKit', 'UserNotifications', 'CoreLocation', 'WebKit', 'CoreTelephony', 'WatchConnectivity'] + core.visionos.frameworks = ['Foundation', 'UIKit', 'UserNotifications', 'CoreLocation', 'WebKit'] end s.subspec 'NotificationService' do |ns| diff --git a/Countly.xcodeproj/project.pbxproj b/Countly.xcodeproj/project.pbxproj index bc684561..0a967487 100644 --- a/Countly.xcodeproj/project.pbxproj +++ b/Countly.xcodeproj/project.pbxproj @@ -750,7 +750,7 @@ MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = ""; - SUPPORTED_PLATFORMS = "watchsimulator watchos macosx iphonesimulator iphoneos appletvsimulator appletvos"; + SUPPORTED_PLATFORMS = "watchsimulator watchos macosx iphonesimulator iphoneos appletvsimulator appletvos xros xrsimulator"; SUPPORTS_MACCATALYST = YES; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; @@ -810,7 +810,7 @@ MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = ""; - SUPPORTED_PLATFORMS = "watchsimulator watchos macosx iphonesimulator iphoneos appletvsimulator appletvos"; + SUPPORTED_PLATFORMS = "watchsimulator watchos macosx iphonesimulator iphoneos appletvsimulator appletvos xros xrsimulator"; SUPPORTS_MACCATALYST = YES; VALIDATE_PRODUCT = YES; VERSIONING_SYSTEM = "apple-generic"; diff --git a/CountlyCommon.m b/CountlyCommon.m index e4864d88..b4a94144 100644 --- a/CountlyCommon.m +++ b/CountlyCommon.m @@ -66,7 +66,7 @@ - (instancetype)init - (void)resetInstance { CLY_LOG_I(@"%s", __FUNCTION__); -#if (TARGET_OS_IOS || TARGET_OS_VISION ) +#if (TARGET_OS_IOS) [NSNotificationCenter.defaultCenter removeObserver:self name:UIDeviceOrientationDidChangeNotification object:nil]; #endif _hasStarted = false; diff --git a/Package.swift b/Package.swift index 10bf0da8..45e97296 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.3 +// swift-tools-version:5.9 import PackageDescription @@ -11,7 +11,8 @@ let package = Package( .iOS(.v10), .macOS(.v10_14), .tvOS(.v10), - .watchOS(.v4) + .watchOS(.v4), + .visionOS(.v1) ], products: @@ -45,14 +46,14 @@ let package = Package( linkerSettings: [ .linkedFramework("Foundation"), - .linkedFramework("UIKit", .when(platforms: [.iOS, .tvOS])), + .linkedFramework("UIKit", .when(platforms: [.iOS, .tvOS, .visionOS])), .linkedFramework("WatchKit", .when(platforms: [.watchOS])), .linkedFramework("WatchConnectivity", .when(platforms: [.iOS, .watchOS])), .linkedFramework("AppKit", .when(platforms: [.macOS])), .linkedFramework("IOKit", .when(platforms: [.macOS])), - .linkedFramework("UserNotifications", .when(platforms: [.iOS, .macOS])), + .linkedFramework("UserNotifications", .when(platforms: [.iOS, .macOS, .visionOS])), .linkedFramework("CoreLocation"), - .linkedFramework("WebKit", .when(platforms: [.iOS])), + .linkedFramework("WebKit", .when(platforms: [.iOS, .visionOS])), .linkedFramework("CoreTelephony", .when(platforms: [.iOS])), ]), .testTarget( From 8f128c3a0d1181a843ed945204a52967703a001c Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Tue, 23 Jun 2026 16:34:39 +0300 Subject: [PATCH 38/42] feat: ios 26 support --- CHANGELOG.md | 3 +++ Countly.podspec | 4 ++-- Countly.xcodeproj/project.pbxproj | 8 ++++---- Package.swift | 4 ++-- 4 files changed, 11 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b5062f39..5fbd230d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,7 @@ ## XX.XX.XX +* !! Major breaking change !! Raised the minimum supported deployment target to iOS 12 and tvOS 12 (previously iOS 10 and tvOS 10) for compatibility with Xcode 26 and the iOS 26 SDK. Apps targeting iOS 10/11 or tvOS 10/11 are no longer supported. + +* Added support for building against the iOS 26 SDK with Xcode 26. * Added a new user properties functions on `CountlyUserDetails`: * `setProperty:value:` for setting a single predefined or custom user property. * `setProperties:` for setting multiple predefined and custom properties in one call. diff --git a/Countly.podspec b/Countly.podspec index c745521d..67410059 100644 --- a/Countly.podspec +++ b/Countly.podspec @@ -10,10 +10,10 @@ Pod::Spec.new do |s| s.requires_arc = true s.default_subspecs = 'Core' - s.ios.deployment_target = '10.0' + s.ios.deployment_target = '12.0' s.osx.deployment_target = '10.14' s.watchos.deployment_target = '4.0' - s.tvos.deployment_target = '10.0' + s.tvos.deployment_target = '12.0' s.visionos.deployment_target = '1.0' s.subspec 'Core' do |core| diff --git a/Countly.xcodeproj/project.pbxproj b/Countly.xcodeproj/project.pbxproj index 0a967487..fb79a263 100644 --- a/Countly.xcodeproj/project.pbxproj +++ b/Countly.xcodeproj/project.pbxproj @@ -744,7 +744,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 10.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MACOSX_DEPLOYMENT_TARGET = 10.14; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; @@ -805,7 +805,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 10.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MACOSX_DEPLOYMENT_TARGET = 10.14; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; @@ -831,7 +831,7 @@ DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 10.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -863,7 +863,7 @@ DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 10.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/Package.swift b/Package.swift index 45e97296..b6649983 100644 --- a/Package.swift +++ b/Package.swift @@ -8,9 +8,9 @@ let package = Package( platforms: [ - .iOS(.v10), + .iOS(.v12), .macOS(.v10_14), - .tvOS(.v10), + .tvOS(.v12), .watchOS(.v4), .visionOS(.v1) ], From ce14ee22a4c3e764b87ca0239d5a85faab3a6973 Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray <57103426+arifBurakDemiray@users.noreply.github.com> Date: Wed, 24 Jun 2026 08:24:37 +0300 Subject: [PATCH 39/42] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5fbd230d..159e2dc9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,5 @@ ## XX.XX.XX -* !! Major breaking change !! Raised the minimum supported deployment target to iOS 12 and tvOS 12 (previously iOS 10 and tvOS 10) for compatibility with Xcode 26 and the iOS 26 SDK. Apps targeting iOS 10/11 or tvOS 10/11 are no longer supported. +* ! Minor breaking change ! Raised the minimum supported deployment target to iOS 12 and tvOS 12 (previously iOS 10 and tvOS 10) for compatibility with Xcode 26 and the iOS 26 SDK. Apps targeting iOS 10/11 or tvOS 10/11 are no longer supported. * Added support for building against the iOS 26 SDK with Xcode 26. * Added a new user properties functions on `CountlyUserDetails`: From 154921c911cc904086a9990ff182f8d0978f81e2 Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Wed, 24 Jun 2026 08:31:19 +0300 Subject: [PATCH 40/42] feat: remove webkit from visionos for now --- Countly.podspec | 4 ++-- Package.swift | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Countly.podspec b/Countly.podspec index 67410059..f63e5c47 100644 --- a/Countly.podspec +++ b/Countly.podspec @@ -21,12 +21,12 @@ Pod::Spec.new do |s| core.public_header_files = 'Countly.h', 'CountlyUserDetails.h', 'CountlyConfig.h', 'CountlyFeedbackWidget.h', 'CountlyRCData.h', 'CountlyRemoteConfig.h', 'CountlyViewTracking.h', 'CountlyExperimentInformation.h', 'CountlyAPMConfig.h', 'CountlySDKLimitsConfig.h', 'Resettable.h', "CountlyCrashesConfig.h", "CountlyCrashData.h", "CountlyContentBuilder.h", "CountlyExperimentalConfig.h", "CountlyContentConfig.h", "CountlyFeedbacks.h" core.preserve_path = 'countly_dsym_uploader.sh' core.ios.frameworks = ['Foundation', 'UIKit', 'UserNotifications', 'CoreLocation', 'WebKit', 'CoreTelephony', 'WatchConnectivity'] - core.visionos.frameworks = ['Foundation', 'UIKit', 'UserNotifications', 'CoreLocation', 'WebKit'] + core.visionos.frameworks = ['Foundation', 'UIKit', 'UserNotifications', 'CoreLocation'] end s.subspec 'NotificationService' do |ns| ns.source_files = 'CountlyNotificationService.{m,h}' - ns.ios.deployment_target = '10.0' + ns.ios.deployment_target = '12.0' ns.ios.frameworks = ['Foundation', 'UserNotifications'] end diff --git a/Package.swift b/Package.swift index b6649983..e2d1c11f 100644 --- a/Package.swift +++ b/Package.swift @@ -53,7 +53,7 @@ let package = Package( .linkedFramework("IOKit", .when(platforms: [.macOS])), .linkedFramework("UserNotifications", .when(platforms: [.iOS, .macOS, .visionOS])), .linkedFramework("CoreLocation"), - .linkedFramework("WebKit", .when(platforms: [.iOS, .visionOS])), + .linkedFramework("WebKit", .when(platforms: [.iOS])), .linkedFramework("CoreTelephony", .when(platforms: [.iOS])), ]), .testTarget( From 52c534c650552ad50c1924df9fe8a9b6986a9a75 Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Wed, 24 Jun 2026 12:56:17 +0300 Subject: [PATCH 41/42] fix: tests for isolation --- Countly.m | 3 + CountlyContentBuilderInternal.h | 1 + CountlyContentBuilderInternal.m | 8 +- CountlyTests/CountlyBaseTestCase.swift | 11 +- CountlyTests/CountlyServerConfigTests.swift | 232 ++++++++++++-------- CountlyTests/ServerConfigBuilder.swift | 6 +- CountlyTests/TestUtils.swift | 14 +- 7 files changed, 179 insertions(+), 96 deletions(-) diff --git a/Countly.m b/Countly.m index c4355f18..06873cd1 100644 --- a/Countly.m +++ b/Countly.m @@ -1556,6 +1556,9 @@ - (void)halt:(BOOL) clearStorage [CountlyDeviceInfo.sharedInstance resetInstance]; [CountlyConnectionManager.sharedInstance resetInstance]; [CountlyServerConfig.sharedInstance resetInstance]; +#if (TARGET_OS_IOS) + [CountlyContentBuilderInternal.sharedInstance resetInstance]; +#endif [CountlyUserDetails.sharedInstance clearUserDetails]; [self resetInstance]; [CountlyCommon.sharedInstance resetInstance]; diff --git a/CountlyContentBuilderInternal.h b/CountlyContentBuilderInternal.h index a9859281..7983a27d 100644 --- a/CountlyContentBuilderInternal.h +++ b/CountlyContentBuilderInternal.h @@ -22,6 +22,7 @@ NS_ASSUME_NONNULL_BEGIN - (void)enterContentZone:(NSArray *)tags; - (void)exitContentZone; +- (void)resetInstance; - (void)changeContent:(NSArray *)tags; - (void)refreshContentZone; - (void)refreshContentZoneJTE; diff --git a/CountlyContentBuilderInternal.m b/CountlyContentBuilderInternal.m index 57d5be48..9f8c07d3 100644 --- a/CountlyContentBuilderInternal.m +++ b/CountlyContentBuilderInternal.m @@ -180,13 +180,19 @@ - (void)fetchContentsForJourneyWithMaxAttempts:(NSInteger)maxAttempts currentAtt - (void)clearContentState { [_requestTimer invalidate]; _requestTimer = nil; - + [_minuteTimer invalidate]; _minuteTimer = nil; self.currentTags = nil; [self setRequestQueueLockedThreadSafe:NO]; } +- (void)resetInstance { + CLY_LOG_I(@"%s", __FUNCTION__); + [self clearContentState]; + _isCurrentlyContentShown = NO; +} + - (void)fetchContents { [self fetchContents:nil contentId:nil]; } diff --git a/CountlyTests/CountlyBaseTestCase.swift b/CountlyTests/CountlyBaseTestCase.swift index c4fe7204..d032ed5b 100644 --- a/CountlyTests/CountlyBaseTestCase.swift +++ b/CountlyTests/CountlyBaseTestCase.swift @@ -34,7 +34,16 @@ class CountlyBaseTestCase: XCTestCase { } func cleanupState() { - // All cleanup logic now handled inside Countly.halt(true) + // Fully purge any state persisted by a previous test *process*. halt(true) alone does + // not reliably clear stale persisted requests when the SDK was never started in this + // process (this is the edge case the historical `testDummy` was added to work around). + // Starting first wires up persistency/connection-manager and loads the stale state, then + // halt(true) forces a full reset + empty save — so every test, including ones run in + // isolation, begins from a clean slate. + let config = createBaseConfig() + config.requiresConsent = false + config.manualSessionHandling = true + Countly.sharedInstance().start(with: config) Countly.sharedInstance().halt(true) } } diff --git a/CountlyTests/CountlyServerConfigTests.swift b/CountlyTests/CountlyServerConfigTests.swift index 7945bf37..a528acd9 100644 --- a/CountlyTests/CountlyServerConfigTests.swift +++ b/CountlyTests/CountlyServerConfigTests.swift @@ -5,6 +5,12 @@ class CountlyServerConfigTests: CountlyBaseTestCase { class CountTracker { var counts: [Int] = [0, 0, 0, 0, 0] + // When true, the mock returns 200 for stored /i requests and records them (in send + // order) in `sentRequests`, so the request queue actually drains and the content + // refresh queue-flush runnable fires. Default false preserves the 400 behavior that + // every other test relies on (requests stay in the RQ for index-based inspection). + var drainQueue = false + var sentRequests: [String] = [] } /// Local Countly instances created during tests. @@ -338,24 +344,33 @@ class CountlyServerConfigTests: CountlyBaseTestCase { func test_sessionsDisabled_allFeatures() throws { let sc = ServerConfigBuilder() sc.sessionTracking(false) - let tracker = setupTestAllFeatures(sc.buildJson()) - + let tracker = setupTestAllFeatures(sc.buildJson(), drainQueue: true) + XCTAssertEqual(0, TestUtils.getCurrentRQ()?.count) XCTAssertEqual(0, TestUtils.getCurrentEQ()?.count) - + let stackTrace = flowAllFeatures() - + immediateFlowAllFeatures() + XCTAssertEqual(0, TestUtils.getCurrentEQ()?.count) + feedbackFlowAllFeatures() + waitForQueueToDrain(tracker, expected: 8) + XCTAssertEqual(0, TestUtils.getCurrentEQ()?.count) + + let sent = tracker.sentRequests + XCTAssertEqual(8, sent.count, "Sent request count mismatch") + + // Sessions disabled => no begin_session; the crash request is first. TestUtils.validateRequest([:], 0, { request in let crash = request["crash"] as? [String: Any] XCTAssertTrue(crash != nil) - }) - try TestUtils.validateEventInRQ("test_event", [:], 1, 7, 0, 2) - try TestUtils.validateEventInRQ("[CLY]_view", ["name": "test_view", "segment": "iOS", "visit": "1"], 1, 7, 1, 2) + }, from: sent) + try TestUtils.validateEventInRQ("test_event", [:], 1, 7, 0, 2, from: sent) + try TestUtils.validateEventInRQ("[CLY]_view", ["name": "test_view", "segment": "iOS", "visit": "1"], 1, 7, 1, 2, from: sent) TestUtils.validateRequest([:], 2, { request in let userDetails = request["user_details"] as! [String: Any] XCTAssertTrue(TestUtils.compareDictionaries(userDetails["custom"] as! [String: Any], ["test_property": "test_value"])) - }) - TestUtils.validateRequest(["location": "33.689500,139.691700"], 3) + }, from: sent) + TestUtils.validateRequest(["location": "33.689500,139.691700"], 3, from: sent) TestUtils.validateRequest([:], 4, { request in let apm = request["apm"] as! [String: Any] XCTAssertEqual("test_trace", apm["name"] as! String) @@ -365,16 +380,9 @@ class CountlyServerConfigTests: CountlyBaseTestCase { "response_code": 400, "request_payload_size": 2000, "response_payload_size": 2000])) - }) - TestUtils.validateRequest(["attribution_data": "test_data"], 5) - TestUtils.validateRequest(["key": "value"], 6) - - XCTAssertEqual(7, TestUtils.getCurrentRQ()?.count) - immediateFlowAllFeatures() - - XCTAssertEqual(0, TestUtils.getCurrentEQ()?.count) - feedbackFlowAllFeatures() - XCTAssertEqual(0, TestUtils.getCurrentEQ()?.count) + }, from: sent) + TestUtils.validateRequest(["attribution_data": "test_data"], 5, from: sent) + TestUtils.validateRequest(["key": "value"], 6, from: sent) try TestUtils.validateEventInRQ("[CLY]_star_rating", [ "platform": "iOS", @@ -384,16 +392,14 @@ class CountlyServerConfigTests: CountlyBaseTestCase { "contactMe": "1", "email": "test", "comment": "test" - ], 7, 8, 0, 2) - + ], 7, 8, 0, 2, from: sent) + try TestUtils.validateEventInRQ("[CLY]_nps", [ "app_version": CountlyDeviceInfo.appVersion()!, "widget_id": "test", "closed": "1", "platform": "iOS" - ], 7, 8, 1, 2) - - XCTAssertEqual(8, TestUtils.getCurrentRQ()?.count) + ], 7, 8, 1, 2, from: sent) validateCounts(tracker.counts, hc: 1, fc: 1, rc: 1, cc: 2, sc: 1) } @@ -579,28 +585,29 @@ class CountlyServerConfigTests: CountlyBaseTestCase { func test_eventsDisabled_allFeatures() throws { let sc = ServerConfigBuilder() .customEventTracking(false) - let tracker = setupTestAllFeatures(sc.buildJson()) - + let tracker = setupTestAllFeatures(sc.buildJson(), drainQueue: true) + XCTAssertEqual(0, TestUtils.getCurrentRQ()?.count) XCTAssertEqual(0, TestUtils.getCurrentEQ()?.count) - + let stackTrace = flowAllFeatures() immediateFlowAllFeatures() + waitForQueueToDrain(tracker, expected: 8) + let sent = tracker.sentRequests - - XCTAssertTrue(TestUtils.getCurrentRQ()![0].contains("begin_session")) + XCTAssertTrue(sent[0].contains("begin_session")) // Events should not be tracked - XCTAssertFalse(containsEventWithKey(TestUtils.getCurrentRQ()!, "test_event")) - + XCTAssertFalse(containsEventWithKey(sent, "test_event")) + // But other features should work - try TestUtils.validateEventInRQ("[CLY]_view", ["name": "test_view", "segment": "iOS", "visit": "1", "start": "1"], 2, 7, 0, 1) + try TestUtils.validateEventInRQ("[CLY]_view", ["name": "test_view", "segment": "iOS", "visit": "1", "start": "1"], 2, 7, 0, 1, from: sent) TestUtils.validateRequest([:], 3, { request in let userDetails = request["user_details"] as! [String: Any] XCTAssertTrue(TestUtils.compareDictionaries(userDetails["custom"] as! [String: Any], ["test_property": "test_value"])) - }) - - XCTAssertEqual(8, TestUtils.getCurrentRQ()?.count) - XCTAssertFalse(containsEventWithKey(TestUtils.getCurrentRQ()!, "test_event")) + }, from: sent) + + XCTAssertEqual(8, sent.count) + XCTAssertFalse(containsEventWithKey(sent, "test_event")) XCTAssertTrue(TestUtils.getCurrentEQ()!.isEmpty) validateCounts(tracker.counts, hc: 1, fc: 1, rc: 1, cc: 2, sc: 1) } @@ -614,21 +621,23 @@ class CountlyServerConfigTests: CountlyBaseTestCase { func test_crashesDisabled_allFeatures() throws { let sc = ServerConfigBuilder() .crashReporting(false) - let tracker = setupTestAllFeatures(sc.buildJson()) - + let tracker = setupTestAllFeatures(sc.buildJson(), drainQueue: true) + XCTAssertEqual(0, TestUtils.getCurrentRQ()?.count) - + let stackTrace = flowAllFeatures() immediateFlowAllFeatures() + waitForQueueToDrain(tracker, expected: 7) + let sent = tracker.sentRequests // Verify that crash is not recorded - for request in TestUtils.getCurrentRQ()! { + for request in sent { XCTAssertFalse(request.contains("crash=")) } - + // But other features should work - XCTAssertTrue(TestUtils.getCurrentRQ()![0].contains("begin_session")) - try TestUtils.validateEventInRQ("test_event", [:], 2, 7, 0, 2) - + XCTAssertTrue(sent[0].contains("begin_session")) + try TestUtils.validateEventInRQ("test_event", [:], 2, 7, 0, 2, from: sent) + validateCounts(tracker.counts, hc: 1, fc: 1, rc: 1, cc: 2, sc: 1) } @@ -641,33 +650,35 @@ class CountlyServerConfigTests: CountlyBaseTestCase { .sessionTracking(false) .customEventTracking(false) .viewTracking(false) - let tracker = setupTestAllFeatures(sc.buildJson()) - + let tracker = setupTestAllFeatures(sc.buildJson(), drainQueue: true) + XCTAssertEqual(0, TestUtils.getCurrentRQ()?.count) - + let stackTrace = flowAllFeatures() immediateFlowAllFeatures() + waitForQueueToDrain(tracker, expected: 6) + let sent = tracker.sentRequests // Verify no sessions, events, or views are recorded - for request in TestUtils.getCurrentRQ()! { + for request in sent { XCTAssertFalse(request.contains("begin_session")) - XCTAssertFalse(containsEventWithKey(TestUtils.getCurrentRQ()!, "test_event")) - XCTAssertFalse(containsEventWithKey(TestUtils.getCurrentRQ()!, "[CLY]_view")) + XCTAssertFalse(containsEventWithKey(sent, "test_event")) + XCTAssertFalse(containsEventWithKey(sent, "[CLY]_view")) } - - XCTAssertEqual(6, TestUtils.getCurrentRQ()?.count) - + + XCTAssertEqual(6, sent.count) + TestUtils.validateRequest([:], 0, { request in XCTAssertTrue(request.contains(where: { (key: String, value: Any) in return key.elementsEqual("crash") })) - }) + }, from: sent) TestUtils.validateRequest([:], 1, { request in let userDetails = request["user_details"] as! [String: Any] XCTAssertTrue(TestUtils.compareDictionaries(userDetails["custom"] as! [String: Any], ["test_property": "test_value"])) - }) - TestUtils.validateRequest(["location": "33.689500,139.691700"], 2) + }, from: sent) + TestUtils.validateRequest(["location": "33.689500,139.691700"], 2, from: sent) + - validateCounts(tracker.counts, hc: 1, fc: 1, rc: 1, cc: 2, sc: 1) } @@ -883,9 +894,44 @@ class CountlyServerConfigTests: CountlyBaseTestCase { } - private func setupTestAllFeatures(_ serverConfig: [String: Any]) -> CountTracker { + /// Returns the query string of a captured request: the POST body (read from the body + /// stream, since URLProtocol nils out `httpBody`) when present, otherwise the URL query. + /// `parseQueryString` expects a bare `app_key=...&...` string, which is exactly what the + /// stored /i request body contains. + static func capturedQueryString(_ request: URLRequest) -> String { + if let body = request.httpBody, let s = String(data: body, encoding: .utf8), !s.isEmpty { + return s + } + if let stream = request.httpBodyStream { + stream.open() + defer { stream.close() } + let bufferSize = 8192 + let buffer = UnsafeMutablePointer.allocate(capacity: bufferSize) + defer { buffer.deallocate() } + var data = Data() + while stream.hasBytesAvailable { + let read = stream.read(buffer, maxLength: bufferSize) + if read > 0 { data.append(buffer, count: read) } else { break } + } + if let s = String(data: data, encoding: .utf8), !s.isEmpty { return s } + } + return request.url?.query ?? "" + } + + /// Spins until the stored request queue has fully drained (so the captured `sentRequests` + /// are complete and the content-refresh queue-flush runnable has run). Used by the + /// drainQueue-mode tests. + private func waitForQueueToDrain(_ tracker: CountTracker, expected: Int) { + for _ in 0..<12 { + if tracker.sentRequests.count >= expected && (TestUtils.getCurrentRQ()?.isEmpty ?? true) { break } + TestUtils.sleep(0.5) {} + } + } + + private func setupTestAllFeatures(_ serverConfig: [String: Any], drainQueue: Bool = false) -> CountTracker { let tracker = CountTracker() - + tracker.drainQueue = drainQueue + // Define the mock behavior MockURLProtocol.requestHandler = { request in let requestString = request.url?.absoluteString ?? "" @@ -907,8 +953,14 @@ class CountlyServerConfigTests: CountlyBaseTestCase { } catch { //ignored } + } else if tracker.drainQueue { + // Stored /i request: record its query string (POST body / GET query) in send + // order and return success so the request queue drains, letting the + // content-refresh queue-flush runnable run. + tracker.sentRequests.append(CountlyServerConfigTests.capturedQueryString(request)) + responseString = "{\"result\":\"Success\"}".data(using: .utf8) } - + if(responseString != nil){ return (responseString, HTTPURLResponse(url: request.url!, statusCode: 200, @@ -1002,27 +1054,44 @@ class CountlyServerConfigTests: CountlyBaseTestCase { let sc = ServerConfigBuilder() consumer(sc) - let tracker = setupTestAllFeatures(sc.buildJson()) - + let tracker = setupTestAllFeatures(sc.buildJson(), drainQueue: true) + XCTAssertEqual(0, TestUtils.getCurrentRQ()?.count) XCTAssertEqual(0, TestUtils.getCurrentEQ()?.count) - + + // Record the whole flow first. Under drainQueue the stored /i requests are answered + // with 200 and removed as they go, so we validate against the ordered `sentRequests` + // the mock saw (same FIFO order as the live RQ) rather than the live RQ, which empties + // asynchronously. Draining the queue is also what lets refreshContentZone's queue-flush + // runnable fire, producing the second content request (cc == 2). let stackTrace = flowAllFeatures() - XCTAssertEqual(8, TestUtils.getCurrentRQ()?.count) + immediateFlowAllFeatures() + XCTAssertEqual(0, TestUtils.getCurrentEQ()?.count) + feedbackFlowAllFeatures() - XCTAssertTrue(TestUtils.getCurrentRQ()![0].contains("begin_session")) + // Wait until the queue has fully drained (all 9 stored requests sent). + for _ in 0..<10 { + if tracker.sentRequests.count >= 9 && (TestUtils.getCurrentRQ()?.isEmpty ?? true) { break } + TestUtils.sleep(0.5) {} + } + XCTAssertEqual(0, TestUtils.getCurrentEQ()?.count) + + let sent = tracker.sentRequests + XCTAssertEqual(9, sent.count, "Sent request count mismatch") + + XCTAssertTrue(sent[0].contains("begin_session")) TestUtils.validateRequest([:], 1, { request in let crash = request["crash"] as? [String: Any] XCTAssertTrue(crash != nil) - }) + }, from: sent) //try TestUtils.validateEventInRQ("[CLY]_orientation", ["mode": "portrait"], 2, 8, 0, 3) - try TestUtils.validateEventInRQ("test_event", [:], 2, 8, 0, 2) // 1, 3 - try TestUtils.validateEventInRQ("[CLY]_view", ["name": "test_view", "segment": "iOS", "visit": "1", "start": "1"], 2, 8, 1, 2) // 2, 3 + try TestUtils.validateEventInRQ("test_event", [:], 2, 8, 0, 2, from: sent) // 1, 3 + try TestUtils.validateEventInRQ("[CLY]_view", ["name": "test_view", "segment": "iOS", "visit": "1", "start": "1"], 2, 8, 1, 2, from: sent) // 2, 3 TestUtils.validateRequest([:], 3, { request in let userDetails = request["user_details"] as! [String: Any] XCTAssertTrue(TestUtils.compareDictionaries(userDetails["custom"] as! [String: Any], ["test_property": "test_value"])) - }) - TestUtils.validateRequest(["location": "33.689500,139.691700"], 4) + }, from: sent) + TestUtils.validateRequest(["location": "33.689500,139.691700"], 4, from: sent) TestUtils.validateRequest([:], 5, { request in let apm = request["apm"] as! [String: Any] XCTAssertEqual("test_trace", apm["name"] as! String) @@ -1032,17 +1101,10 @@ class CountlyServerConfigTests: CountlyBaseTestCase { "response_code": 400, "request_payload_size": 2000, "response_payload_size": 2000])) - }) - TestUtils.validateRequest(["attribution_data": "test_data"], 6) - TestUtils.validateRequest(["key": "value"], 7) - - immediateFlowAllFeatures() - XCTAssertEqual(0, TestUtils.getCurrentEQ()?.count) - feedbackFlowAllFeatures() + }, from: sent) + TestUtils.validateRequest(["attribution_data": "test_data"], 6, from: sent) + TestUtils.validateRequest(["key": "value"], 7, from: sent) - XCTAssertEqual(0, TestUtils.getCurrentEQ()?.count) - XCTAssertEqual(9, TestUtils.getCurrentRQ()?.count) - try TestUtils.validateEventInRQ("[CLY]_star_rating", [ "platform": "iOS", "app_version": CountlyDeviceInfo.appVersion()!, @@ -1051,17 +1113,15 @@ class CountlyServerConfigTests: CountlyBaseTestCase { "contactMe": 1, "email": "test", "comment": "test" - ], 8, 9, 0, 2) - + ], 8, 9, 0, 2, from: sent) + try TestUtils.validateEventInRQ("[CLY]_nps", [ "app_version": CountlyDeviceInfo.appVersion()!, "widget_id": "test", "closed": "1", "platform": "iOS" - ], 8, 9, 1, 2) - - XCTAssertEqual(9, TestUtils.getCurrentRQ()?.count) - + ], 8, 9, 1, 2, from: sent) + validateCounts(tracker.counts, hc: hc, fc: fc, rc: rc, cc: cc, sc: scc) } diff --git a/CountlyTests/ServerConfigBuilder.swift b/CountlyTests/ServerConfigBuilder.swift index ce8dcc51..dd8394a3 100644 --- a/CountlyTests/ServerConfigBuilder.swift +++ b/CountlyTests/ServerConfigBuilder.swift @@ -271,7 +271,11 @@ class ServerConfigBuilder { requestQueueSize(1000) eventQueueSize(100) logging(false) - sessionUpdateInterval(60) + // CountlyServerConfig.sessionInterval defaults to 0 when no SBS is applied; 0 is the + // sentinel meaning "fall back to config.updateSessionPeriod (60s)" (see CountlyServerConfig + // populateConfig: `config.updateSessionPeriod = _sessionInterval ?: ...`). So the default + // of the server-config field itself is 0, not 60. + sessionUpdateInterval(0) contentZoneInterval(30) consentRequired(false) dropOldRequestTime(0) diff --git a/CountlyTests/TestUtils.swift b/CountlyTests/TestUtils.swift index 83c21634..48bbebad 100644 --- a/CountlyTests/TestUtils.swift +++ b/CountlyTests/TestUtils.swift @@ -46,12 +46,12 @@ class TestUtils { } } - static func validateRequest(_ params: [String: Any], _ idx: Int) { - validateRequest(params, idx, { request in }) + static func validateRequest(_ params: [String: Any], _ idx: Int, from source: [String]? = nil) { + validateRequest(params, idx, { request in }, from: source) } - static func validateRequest(_ params: [String: Any], _ idx: Int, _ customValidator: ([String: Any]) -> Void) { - guard let rq = getCurrentRQ() else { + static func validateRequest(_ params: [String: Any], _ idx: Int, _ customValidator: ([String: Any]) -> Void, from source: [String]? = nil) { + guard let rq = source ?? getCurrentRQ() else { XCTFail("Request queue is nil.") return } @@ -60,7 +60,7 @@ class TestUtils { return } - let requestStr = getCurrentRQ()![idx] + let requestStr = rq[idx] let request = parseQueryString(requestStr) validateRequiredParams(request) @@ -91,9 +91,9 @@ class TestUtils { static func validateEventInRQ( _ eventName: String, _ segmentation: [String: Any], _ idx: Int, _ rqCount: Int, _ eventIdx: Int, - _ eventCount: Int + _ eventCount: Int, from source: [String]? = nil ) throws { - let requestStr = getCurrentRQ()![idx] + let requestStr = (source ?? getCurrentRQ())![idx] let request = parseQueryString(requestStr) validateRequiredParams(request) From e4944016bc4ad0505aaebdd1ff93166e88d3d046 Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Wed, 24 Jun 2026 13:08:50 +0300 Subject: [PATCH 42/42] feat: 26.1.2 --- CHANGELOG.md | 10 +++------- Countly-PL.podspec | 2 +- Countly.podspec | 2 +- Countly.xcodeproj/project.pbxproj | 4 ++-- CountlyCommon.m | 2 +- CountlyTests/TestUtils.swift | 2 +- 6 files changed, 9 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 159e2dc9..8ba01a52 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -## XX.XX.XX +## 26.1.2 * ! Minor breaking change ! Raised the minimum supported deployment target to iOS 12 and tvOS 12 (previously iOS 10 and tvOS 10) for compatibility with Xcode 26 and the iOS 26 SDK. Apps targeting iOS 10/11 or tvOS 10/11 are no longer supported. * Added support for building against the iOS 26 SDK with Xcode 26. @@ -7,16 +7,12 @@ * `setProperties:` for setting multiple predefined and custom properties in one call. * Added `providedUserProperties` to `CountlyConfig` to set initial user properties that are applied and saved automatically right after `start`. * Added `setMaxValueSizePicture:` to `CountlySDKLimitsConfig` to control the maximum size of picture URLs and picture paths independently of other value limits (default 4096). -* Improved `$push` / `$pull` / `$addToSet` wire format: values are now always sent as arrays so multiple consecutive calls on the same key accumulate correctly. * Updated resolution extraction to accommodate iOS 26 deprecations. * Mitigated a race condition in the request queue that could drop or duplicate requests. * Mitigated an issue where non-queued requests were affected from request timeout settings. -* Mitigated a race condition for tests in the request queue that could drop or duplicate requests. -* Mitigated an issue where server config defaults overrode user-provided SDK limits. -* Mitigated an issue where invalid or unknown `sdkBehaviorSettings` keys were persisted. -* Mitigated an issue where listing-filter conflicts cleared keys across unrelated categories. -* Mitigated an issue where consent could be sent twice during initialization. +* Mitigated an issue where default SDK behavior settings overrode user-provided SDK limits. +* Mitigated an issue where consent could be sent twice during initialization when enabled via SDK Behavior Settings. * Mitigated an issue where content fetches and remote config requests could be sent while in temporary device ID mode, creating a `CLYTemporaryDeviceID` user on the server. * Deprecated the direct property setters on `CountlyUserDetails`: `name`, `username`, `email`, `organization`, `phone`, `gender`, `pictureURL`, `pictureLocalPath`, `birthYear`, `custom`. Use `setProperty:value:` or `setProperties:` instead. diff --git a/Countly-PL.podspec b/Countly-PL.podspec index 33338d45..7e969545 100644 --- a/Countly-PL.podspec +++ b/Countly-PL.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'Countly-PL' - s.version = '26.1.1' + s.version = '26.1.2' s.license = { :type => 'MIT', :file => 'LICENSE' } s.summary = 'Countly is an innovative, real-time, open source mobile analytics platform.' s.homepage = 'https://github.com/Countly/countly-sdk-ios' diff --git a/Countly.podspec b/Countly.podspec index f63e5c47..827d97fa 100644 --- a/Countly.podspec +++ b/Countly.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'Countly' - s.version = '26.1.1' + s.version = '26.1.2' s.license = { :type => 'MIT', :file => 'LICENSE' } s.summary = 'Countly is an innovative, real-time, open source mobile analytics platform.' s.homepage = 'https://github.com/Countly/countly-sdk-ios' diff --git a/Countly.xcodeproj/project.pbxproj b/Countly.xcodeproj/project.pbxproj index fb79a263..7c73122b 100644 --- a/Countly.xcodeproj/project.pbxproj +++ b/Countly.xcodeproj/project.pbxproj @@ -838,7 +838,7 @@ "@loader_path/Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.14; - MARKETING_VERSION = 26.1.1; + MARKETING_VERSION = 26.1.2; PRODUCT_BUNDLE_IDENTIFIER = ly.count.CountlyiOSSDK; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -870,7 +870,7 @@ "@loader_path/Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.14; - MARKETING_VERSION = 26.1.1; + MARKETING_VERSION = 26.1.2; PRODUCT_BUNDLE_IDENTIFIER = ly.count.CountlyiOSSDK; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/CountlyCommon.m b/CountlyCommon.m index b4a94144..13adc0a2 100644 --- a/CountlyCommon.m +++ b/CountlyCommon.m @@ -29,7 +29,7 @@ @interface CountlyCommon () #endif @end -NSString* const kCountlySDKVersion = @"26.1.1"; +NSString* const kCountlySDKVersion = @"26.1.2"; NSString* const kCountlySDKName = @"objc-native-ios"; NSString* const kCountlyErrorDomain = @"ly.count.ErrorDomain"; diff --git a/CountlyTests/TestUtils.swift b/CountlyTests/TestUtils.swift index 48bbebad..0fba71b2 100644 --- a/CountlyTests/TestUtils.swift +++ b/CountlyTests/TestUtils.swift @@ -13,7 +13,7 @@ class TestUtils { static let commonDeviceId: String = "deviceId" static let commonAppKey: String = "appkey" static let host: String = "https://YOUR_SERVER" - static let SDK_VERSION = "26.1.1" + static let SDK_VERSION = "26.1.2" static let SDK_NAME = "objc-native-ios" static func cleanup() {