diff --git a/CHANGELOG.md b/CHANGELOG.md index b0efab3b..a39c3d6f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ * 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. +* Health check now reports anonymous SDK method usage (`fu`) and triggered log codes (`lc`) to help diagnose integration issues. * 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. diff --git a/Countly.m b/Countly.m index 1259d534..77ba3e19 100644 --- a/Countly.m +++ b/Countly.m @@ -94,7 +94,11 @@ - (instancetype)init - (void)startWithConfig:(CountlyConfig *)config { if (CountlyCommon.sharedInstance.hasStarted_) + { + CLY_LOG_W(@"%s SDK is already started! Countly configuration will not be applied again.", __FUNCTION__); + [CountlyHealthTracker.sharedInstance recordLogCode:@"w110"]; return; + } CountlyCommon.sharedInstance.hasStarted = YES; CountlyCommon.sharedInstance.enableDebug = config.enableDebug; @@ -194,7 +198,10 @@ - (void)startWithConfig:(CountlyConfig *)config } if (!CountlyCommon.sharedInstance.manualSessionHandling) + { + [CountlyHealthTracker.sharedInstance recordUsage:@"sessions" method:@"begin:a"]; [CountlyConnectionManager.sharedInstance beginSession]; + } else [CountlyCommon.sharedInstance recordOrientation]; @@ -396,11 +403,13 @@ - (void)onTimer:(NSTimer *)timer if (!CountlyCommon.sharedInstance.manualSessionHandling) { + [CountlyHealthTracker.sharedInstance recordUsage:@"sessions" method:@"update:a"]; [CountlyConnectionManager.sharedInstance updateSession]; } // this condtion is called only when both manual session handling and hybrid mode is enabled. else if (CountlyCommon.sharedInstance.enableManualSessionControlHybridMode) { + [CountlyHealthTracker.sharedInstance recordUsage:@"sessions" method:@"update:a"]; [CountlyConnectionManager.sharedInstance updateSession]; } @@ -428,8 +437,11 @@ - (void)suspend [CountlyConnectionManager.sharedInstance sendEventsWithSaveIfNeeded]; if (!CountlyCommon.sharedInstance.manualSessionHandling) + { + [CountlyHealthTracker.sharedInstance recordUsage:@"sessions" method:@"end:a"]; [CountlyConnectionManager.sharedInstance endSession]; - + } + [CountlyPersistency.sharedInstance saveToFile]; } @@ -456,8 +468,11 @@ - (void)resume CLY_LOG_D(@"%s manualSessions: [%d]", __FUNCTION__, CountlyCommon.sharedInstance.manualSessionHandling); if (!CountlyCommon.sharedInstance.manualSessionHandling) + { + [CountlyHealthTracker.sharedInstance recordUsage:@"sessions" method:@"begin:a"]; [CountlyConnectionManager.sharedInstance beginSession]; - + } + [CountlyViewTrackingInternal.sharedInstance applicationWillEnterForeground]; isSuspended = NO; @@ -615,6 +630,7 @@ - (void)addCustomNetworkRequestHeaders:(NSDictionary *_N - (void)beginSession { CLY_LOG_I(@"%s", __FUNCTION__); + [CountlyHealthTracker.sharedInstance recordUsage:@"sessions" method:@"begin"]; if (CountlyCommon.sharedInstance.manualSessionHandling) [CountlyConnectionManager.sharedInstance beginSession]; @@ -623,6 +639,7 @@ - (void)beginSession - (void)updateSession { CLY_LOG_I(@"%s", __FUNCTION__); + [CountlyHealthTracker.sharedInstance recordUsage:@"sessions" method:@"update"]; if (CountlyCommon.sharedInstance.manualSessionHandling) [CountlyConnectionManager.sharedInstance updateSession]; @@ -631,6 +648,7 @@ - (void)updateSession - (void)endSession { CLY_LOG_I(@"%s", __FUNCTION__); + [CountlyHealthTracker.sharedInstance recordUsage:@"sessions" method:@"end"]; if (CountlyCommon.sharedInstance.manualSessionHandling) { @@ -688,19 +706,16 @@ - (void)setID:(NSString *)deviceID; } - (void)changeDeviceIDWithMerge:(NSString * _Nullable)deviceID { - CLY_LOG_I(@"%s", __FUNCTION__); - [self setIDInternal:deviceID onServer:YES]; + 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]; + 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 @@ -801,8 +816,7 @@ - (void)giveConsentForFeature:(NSString *)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 @@ -829,8 +843,7 @@ - (void)cancelConsentForFeature:(NSString *)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 @@ -885,6 +898,7 @@ - (void)recordEvent:(NSString *)key segmentation:(NSDictionary *)segmentation co - (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); + [CountlyHealthTracker.sharedInstance recordUsage:@"events" method:@"record"]; NSNumber* isReservedEvent = [self isReservedEvent:key]; @@ -894,11 +908,13 @@ - (void)recordEvent:(NSString *)key segmentation:(NSDictionary *)segmentation co if (!isReservedEvent.boolValue) { CLY_LOG_W(@"%s, No consent given for the reserved event! Event will not be recorded.", __FUNCTION__); + [CountlyHealthTracker.sharedInstance recordLogCode:@"w401"]; 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__); + [CountlyHealthTracker.sharedInstance recordLogCode:@"w401"]; return; } @@ -1085,6 +1101,7 @@ - (NSNumber *)isReservedEvent:(NSString *)key - (void)startEvent:(NSString *)key { CLY_LOG_I(@"%s key: [%@]", __FUNCTION__, key); + [CountlyHealthTracker.sharedInstance recordUsage:@"events" method:@"start"]; if (!CountlyConsentManager.sharedInstance.consentForEvents) return; @@ -1104,6 +1121,7 @@ - (void)endEvent:(NSString *)key - (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); + [CountlyHealthTracker.sharedInstance recordUsage:@"events" method:@"end"]; if (!CountlyConsentManager.sharedInstance.consentForEvents) return; @@ -1113,6 +1131,7 @@ - (void)endEvent:(NSString *)key segmentation:(NSDictionary *)segmentation count if (!event) { CLY_LOG_W(@"%s Event with key '%@' not started yet or cancelled/ended before!", __FUNCTION__, key); + [CountlyHealthTracker.sharedInstance recordLogCode:@"w720"]; return; } @@ -1123,6 +1142,7 @@ - (void)endEvent:(NSString *)key segmentation:(NSDictionary *)segmentation count - (void)cancelEvent:(NSString *)key { CLY_LOG_I(@"%s key: [%@]", __FUNCTION__, key); + [CountlyHealthTracker.sharedInstance recordUsage:@"events" method:@"cancel"]; if (!CountlyConsentManager.sharedInstance.consentForEvents) return; @@ -1132,6 +1152,7 @@ - (void)cancelEvent:(NSString *)key if (!event) { CLY_LOG_W(@"%s Event with key '%@' not started yet or cancelled/ended before!", __FUNCTION__, key); + [CountlyHealthTracker.sharedInstance recordLogCode:@"w720"]; return; } @@ -1157,14 +1178,12 @@ - (void)askForNotificationPermissionWithOptions:(UNAuthorizationOptions)options - (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 @@ -1181,14 +1200,12 @@ - (void)clearPushNotificationToken - (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]; } @@ -1210,6 +1227,7 @@ - (void)recordException:(NSException *)exception isFatal:(BOOL)isFatal - (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); + [CountlyHealthTracker.sharedInstance recordUsage:@"crashes" method:@"record"]; [CountlyCrashReporter.sharedInstance recordException:exception isFatal:isFatal stackTrace:stackTrace segmentation:segmentation]; } @@ -1222,30 +1240,35 @@ - (void)recordError:(NSString *)errorName stackTrace:(NSArray * _Nullable)stackT - (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); + [CountlyHealthTracker.sharedInstance recordUsage:@"crashes" method:@"record"]; [CountlyCrashReporter.sharedInstance recordError:errorName isFatal:isFatal stackTrace:stackTrace segmentation:segmentation]; } - (void)recordHandledException:(NSException *)exception { CLY_LOG_I(@"%s exception: [%@]", __FUNCTION__, exception); + [CountlyHealthTracker.sharedInstance recordUsage:@"crashes" method:@"record:d"]; [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); + [CountlyHealthTracker.sharedInstance recordUsage:@"crashes" method:@"record:d"]; [CountlyCrashReporter.sharedInstance recordException:exception isFatal:NO stackTrace:stackTrace segmentation:nil]; } - (void)recordUnhandledException:(NSException *)exception withStackTrace:(NSArray * _Nullable)stackTrace { CLY_LOG_I(@"%s exception: [%@], stackTrace: [%@]", __FUNCTION__, exception, stackTrace); + [CountlyHealthTracker.sharedInstance recordUsage:@"crashes" method:@"record:d"]; [CountlyCrashReporter.sharedInstance recordException:exception isFatal:YES stackTrace:stackTrace segmentation:nil]; } - (void)recordCrashLog:(NSString *)log { CLY_LOG_I(@"%s log: [%@]", __FUNCTION__, log); + [CountlyHealthTracker.sharedInstance recordUsage:@"crashes" method:@"log"]; [CountlyCrashReporter.sharedInstance log:log]; } @@ -1264,14 +1287,14 @@ - (void)crashLog:(NSString *)format, ... - (void)recordView:(NSString *)viewName; { CLY_LOG_I(@"%s viewName: [%@]", __FUNCTION__, viewName); - [CountlyViewTrackingInternal.sharedInstance startAutoStoppedView:viewName segmentation:nil]; + [CountlyViewTrackingInternal.sharedInstance recordViewDeprecated:viewName segmentation:nil]; } - (void)recordView:(NSString *)viewName segmentation:(NSDictionary *)segmentation { CLY_LOG_I(@"%s viewName: [%@], segmentation: [%@]", __FUNCTION__, viewName, segmentation); - [CountlyViewTrackingInternal.sharedInstance startAutoStoppedView:viewName segmentation:segmentation]; + [CountlyViewTrackingInternal.sharedInstance recordViewDeprecated:viewName segmentation:segmentation]; } #if (TARGET_OS_IOS || TARGET_OS_VISION || TARGET_OS_TV ) @@ -1307,8 +1330,7 @@ - (BOOL)isAutoViewTrackingActive - (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 @@ -1327,15 +1349,13 @@ - (void)presentRatingWidgetWithID:(NSString *)widgetID completionHandler:(void ( - (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); - + 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 { 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]; } @@ -1362,7 +1382,6 @@ - (void)recordAttributionID:(NSString *)attributionID - (void)recordDirectAttributionWithCampaignType:(NSString *)campaignType andCampaignData:(NSString *)campaignData { CLY_LOG_I(@"%s campaignType: [%@], campaignData: [%@]", __FUNCTION__, campaignType, campaignData); - if (!CountlyConsentManager.sharedInstance.consentForAttribution) return; @@ -1417,7 +1436,6 @@ - (void)recordDirectAttributionWithCampaignType:(NSString *)campaignType andCamp - (void)recordIndirectAttribution:(NSDictionary *)attribution { CLY_LOG_I(@"%s attribution: [%@]", __FUNCTION__, attribution); - if (!CountlyConsentManager.sharedInstance.consentForAttribution) return; @@ -1465,20 +1483,17 @@ - (void)updateRemoteConfigExceptForKeys:(NSArray *)omitKeys completionHandler:(v - (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); - [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 { - 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 @@ -1496,7 +1511,6 @@ - (void)clearAllCustomTraces - (void)appLoadingFinished { CLY_LOG_I(@"%s", __FUNCTION__); - long long appLoadEndTime = floor(NSDate.date.timeIntervalSince1970 * 1000); [CountlyPerformanceMonitoring.sharedInstance recordAppStartDurationTraceWithStartTime:appLoadStartTime endTime:appLoadEndTime]; diff --git a/CountlyCommon.m b/CountlyCommon.m index e4864d88..d300c637 100644 --- a/CountlyCommon.m +++ b/CountlyCommon.m @@ -643,6 +643,7 @@ - (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); + [CountlyHealthTracker.sharedInstance recordLogCode:@"w201"]; return [self substringToIndex:CountlyCommon.sharedInstance.maxKeyLength]; } @@ -665,6 +666,7 @@ - (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); + [CountlyHealthTracker.sharedInstance recordLogCode:@"w202"]; return [self substringToIndex:CountlyCommon.sharedInstance.maxValueLength]; } @@ -734,6 +736,7 @@ - (NSDictionary *)cly_limited:(NSString *)explanation [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]); + [CountlyHealthTracker.sharedInstance recordLogCode:@"w203"]; NSMutableDictionary* limitedDict = self.mutableCopy; [limitedDict removeObjectsForKeys:excessKeys]; diff --git a/CountlyConnectionManager.m b/CountlyConnectionManager.m index d1ef9ac8..01958df1 100644 --- a/CountlyConnectionManager.m +++ b/CountlyConnectionManager.m @@ -418,7 +418,7 @@ - (void)proceedOnQueue } else { - CLY_LOG_D(@"%s, request:[ <%p> ] failed! response:[ %@ ]", __FUNCTION__, request, [data cly_stringUTF8]); + CLY_LOG_E(@"%s, request:[ <%p> ] failed! response:[ %@ ]", __FUNCTION__, request, [data cly_stringUTF8]); self.hasAnyRequestFailed = YES; // Mark that a request has failed @@ -433,6 +433,7 @@ - (void)proceedOnQueue } [CountlyHealthTracker.sharedInstance logFailedNetworkRequestWithStatusCode:((NSHTTPURLResponse*)response).statusCode errorResponse: [data cly_stringUTF8]]; + [CountlyHealthTracker.sharedInstance recordLogCode:@"e301"]; [CountlyHealthTracker.sharedInstance saveState]; self.startTime = nil; atomic_store(&self->_isProcessingQueue, NO); diff --git a/CountlyCrashReporter.m b/CountlyCrashReporter.m index ef1d697d..e867d23f 100644 --- a/CountlyCrashReporter.m +++ b/CountlyCrashReporter.m @@ -246,7 +246,10 @@ void CountlyExceptionHandler(NSException *exception, bool isFatal, bool isAutoDe { if (!CountlyServerConfig.sharedInstance.crashReportingEnabled) return; - + + if (isAutoDetect) + [CountlyHealthTracker.sharedInstance recordUsage:@"crashes" method:@"record:a"]; + NSArray* stackTrace = exception.userInfo[kCountlyExceptionUserInfoBacktraceKey]; if (!stackTrace) stackTrace = exception.callStackSymbols; diff --git a/CountlyHealthTracker.h b/CountlyHealthTracker.h index 772d29d5..7fe081f1 100644 --- a/CountlyHealthTracker.h +++ b/CountlyHealthTracker.h @@ -28,6 +28,10 @@ - (void)sendHealthCheck; +- (void)recordUsage:(NSString *)area method:(NSString *)method; + +- (void)recordLogCode:(NSString *)code; + // Exposed for testing purposes only. - (void)resetInstance; diff --git a/CountlyHealthTracker.m b/CountlyHealthTracker.m index 668f40c3..633a9c4e 100644 --- a/CountlyHealthTracker.m +++ b/CountlyHealthTracker.m @@ -22,7 +22,12 @@ @interface CountlyHealthTracker () @property (nonatomic, assign) BOOL healthCheckEnabled; @property (nonatomic, assign) BOOL healthCheckSent; +@property (nonatomic, strong) NSMutableDictionary *> *methodUsage; +@property (nonatomic, strong) NSMutableDictionary *logCodes; + @property (nonatomic, strong) dispatch_queue_t hcQueue; + +- (NSDictionary *)deepCopyMethodUsage; @end @implementation CountlyHealthTracker @@ -41,6 +46,14 @@ @implementation CountlyHealthTracker NSString * const requestKeyBackoffRequest = @"bom"; NSString * const requestKeyConsecutiveBackoffRequest = @"cbom"; +NSString * const keyMethodUsage = @"MU"; +NSString * const keyLogCodes = @"LC"; + +NSString * const requestKeyMethodUsage = @"fu"; +NSString * const requestKeyLogCodes = @"lc"; + +static const long kCountlyHCCountCap = 65535; + + (instancetype)sharedInstance { static CountlyHealthTracker *instance = nil; static dispatch_once_t onceToken; @@ -58,6 +71,9 @@ - (instancetype)init{ _healthCheckSent = NO; _healthCheckEnabled = YES; + _methodUsage = NSMutableDictionary.new; + _logCodes = NSMutableDictionary.new; + // queue for health tracker state _hcQueue = dispatch_queue_create("ly.count.healthtracker.queue", DISPATCH_QUEUE_SERIAL); @@ -79,6 +95,19 @@ - (void)setupInitialCounters:(NSDictionary *)initialState { self.countBackoffRequest = [initialState[keyBackoffRequest] longValue]; self.consecutiveBackoffRequest = [initialState[keyConsecutiveBackoffRequest] longValue]; + NSDictionary *storedUsage = initialState[keyMethodUsage]; + if ([storedUsage isKindOfClass:NSDictionary.class]) { + [storedUsage enumerateKeysAndObjectsUsingBlock:^(NSString *area, NSDictionary *methods, BOOL *stop) { + if ([methods isKindOfClass:NSDictionary.class]) { + self.methodUsage[area] = [methods mutableCopy]; + } + }]; + } + NSDictionary *storedCodes = initialState[keyLogCodes]; + if ([storedCodes isKindOfClass:NSDictionary.class]) { + [self.logCodes addEntriesFromDictionary:storedCodes]; + } + CLY_LOG_D(@"%s loaded initial health check state: [%@]", __FUNCTION__, initialState); } @@ -94,6 +123,35 @@ - (void)logError { }); } +- (void)recordUsage:(NSString *)area method:(NSString *)method { + if (area.length == 0 || method.length == 0) + return; + + dispatch_async(self.hcQueue, ^{ + NSMutableDictionary *methods = self.methodUsage[area]; + if (!methods) { + methods = NSMutableDictionary.new; + self.methodUsage[area] = methods; + } + long current = [methods[method] longValue]; + if (current < kCountlyHCCountCap) { + methods[method] = @(current + 1); + } + }); +} + +- (void)recordLogCode:(NSString *)code { + if (code.length == 0) + return; + + dispatch_async(self.hcQueue, ^{ + long current = [self.logCodes[code] longValue]; + if (current < kCountlyHCCountCap) { + self.logCodes[code] = @(current + 1); + } + }); +} + - (void)logFailedNetworkRequestWithStatusCode:(NSInteger)statusCode errorResponse:(NSString *)errorResponse { if (statusCode <= 0 || statusCode >= 1000 || errorResponse == nil) { @@ -148,13 +206,23 @@ - (void)saveState { keyStatusCode: @(self.statusCode), keyErrorMessage: self.errorMessage ?: @"", keyBackoffRequest: @(self.countBackoffRequest), - keyConsecutiveBackoffRequest: @(self.consecutiveBackoffRequest) + keyConsecutiveBackoffRequest: @(self.consecutiveBackoffRequest), + keyMethodUsage: [self deepCopyMethodUsage], + keyLogCodes: [self.logCodes copy] }; [CountlyPersistency.sharedInstance storeHealthCheckTrackerState:healthCheckState]; }); } +- (NSDictionary *)deepCopyMethodUsage { + NSMutableDictionary *copy = NSMutableDictionary.new; + [self.methodUsage enumerateKeysAndObjectsUsingBlock:^(NSString *area, NSMutableDictionary *methods, BOOL *stop) { + copy[area] = [methods copy]; + }]; + return copy; +} + - (void)resetInstance { CLY_LOG_D(@"%s resetting health check state", __FUNCTION__); dispatch_async(self.hcQueue, ^{ @@ -175,6 +243,8 @@ - (void)clearValues { self.countBackoffRequest = 0; self.consecutiveBackoffRequest = 0; self.countConsecutiveBackoffRequest = 0; + [self.methodUsage removeAllObjects]; + [self.logCodes removeAllObjects]; } - (void)sendHealthCheck { @@ -245,6 +315,8 @@ - (NSURLRequest *)healthCheckRequest { __block NSString *snapshotErrorMessage; __block long snapshotBackoffRequest; __block long snapshotConsecutiveBackoffRequest; + __block NSDictionary *snapshotMethodUsage; + __block NSDictionary *snapshotLogCodes; dispatch_sync(self.hcQueue, ^{ snapshotLogError = self.countLogError; @@ -253,18 +325,28 @@ - (NSURLRequest *)healthCheckRequest { snapshotErrorMessage = [self.errorMessage copy] ?: @""; snapshotBackoffRequest = self.countBackoffRequest; snapshotConsecutiveBackoffRequest = self.consecutiveBackoffRequest; + snapshotMethodUsage = [self deepCopyMethodUsage]; + snapshotLogCodes = [self.logCodes copy]; }); NSString *queryString = [CountlyConnectionManager.sharedInstance queryEssentials]; - queryString = [queryString stringByAppendingFormat:@"&%@=%@", @"hc", [self dictionaryToJsonString:@{ + NSMutableDictionary *hcDict = [@{ requestKeyErrorCount: @(snapshotLogError), requestKeyWarningCount: @(snapshotLogWarning), requestKeyStatusCode: @(snapshotStatusCode), requestKeyRequestError: snapshotErrorMessage, requestKeyBackoffRequest: @(snapshotBackoffRequest), requestKeyConsecutiveBackoffRequest: @(snapshotConsecutiveBackoffRequest) - }]]; + } mutableCopy]; + if (snapshotMethodUsage.count > 0) { + hcDict[requestKeyMethodUsage] = snapshotMethodUsage; + } + if (snapshotLogCodes.count > 0) { + hcDict[requestKeyLogCodes] = snapshotLogCodes; + } + + queryString = [queryString stringByAppendingFormat:@"&%@=%@", @"hc", [self dictionaryToJsonString:hcDict]]; queryString = [queryString stringByAppendingFormat:@"&%@=%@", @"metrics", [self dictionaryToJsonString:@{ CLYMetricKeyAppVersion: CountlyDeviceInfo.appVersion diff --git a/CountlyPersistency.m b/CountlyPersistency.m index b14575d0..2b297a54 100644 --- a/CountlyPersistency.m +++ b/CountlyPersistency.m @@ -99,6 +99,7 @@ - (void)addToQueue:(NSString *)queryString // in other case if exceeded count is 36 and out limit is 100 we can only remove 36 items because we have that amount NSUInteger gonnaRemoveSize = MIN(exceededSize, kCountlyRequestRemovalLoopLimit) + 1; CLY_LOG_W(@"[CountlyPersistency] addToQueue, request queue size:[ %lu ] exceeded limit:[ %lu ], will remove first:[ %lu ] request(s)", self.queuedRequests.count, self.storedRequestsLimit, gonnaRemoveSize); + [CountlyHealthTracker.sharedInstance recordLogCode:@"w310"]; NSRange itemsToRemove = NSMakeRange(0, gonnaRemoveSize); [self.queuedRequests removeObjectsInRange:itemsToRemove]; } @@ -333,6 +334,7 @@ - (void)recordTimedEvent:(CountlyEvent *)event if (self.startedEvents[event.key]) { CLY_LOG_W(@"Event with key '%@' already started!", event.key); + [CountlyHealthTracker.sharedInstance recordLogCode:@"w722"]; return; } diff --git a/CountlyTests/CountlyHealthTracker+Tests.h b/CountlyTests/CountlyHealthTracker+Tests.h new file mode 100644 index 00000000..150e321f --- /dev/null +++ b/CountlyTests/CountlyHealthTracker+Tests.h @@ -0,0 +1,7 @@ +#import "CountlyHealthTracker.h" + +@interface CountlyHealthTracker (Tests) +@property (nonatomic, strong) NSMutableDictionary *methodUsage; +@property (nonatomic, strong) NSMutableDictionary *logCodes; +- (NSURLRequest *)healthCheckRequest; +@end diff --git a/CountlyTests/CountlyHealthTrackerTests.swift b/CountlyTests/CountlyHealthTrackerTests.swift index c0ee33d6..e18381a1 100644 --- a/CountlyTests/CountlyHealthTrackerTests.swift +++ b/CountlyTests/CountlyHealthTrackerTests.swift @@ -398,4 +398,284 @@ class CountlyHealthTrackerTests: CountlyBaseTestCase { XCTAssertEqual(result, .success, "Concurrent performance test did not complete in time") } } + + // MARK: - Method Usage & Log Code Tests + + /// Drains the serial hcQueue by enqueuing saveState (which runs on the same queue as + /// recordUsage/recordLogCode) and waiting briefly, so asynchronous counter writes are applied. + private func drainHealthCheckQueue(_ tracker: CountlyHealthTracker, timeout: TimeInterval = 5) { + let exp = expectation(description: "drain hc queue") + tracker.saveState() + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { exp.fulfill() } + wait(for: [exp], timeout: timeout) + } + + /// recordUsage/recordLogCode accumulate counts per area/method and per code. + func testRecordUsageAndLogCodeAccumulate() { + let tracker: CountlyHealthTracker = CountlyHealthTracker.sharedInstance() + tracker.methodUsage.removeAllObjects() + tracker.logCodes.removeAllObjects() + + tracker.recordUsage("events", method: "record") + tracker.recordUsage("events", method: "record") + tracker.recordUsage("views", method: "start") + tracker.recordLogCode("w204") + tracker.recordLogCode("e301") + + drainHealthCheckQueue(tracker) + + XCTAssertEqual((tracker.methodUsage["events"] as? [String: NSNumber])?["record"], 2) + XCTAssertEqual((tracker.methodUsage["views"] as? [String: NSNumber])?["start"], 1) + XCTAssertEqual(tracker.logCodes["w204"] as? NSNumber, 1) + XCTAssertEqual(tracker.logCodes["e301"] as? NSNumber, 1) + } + + /// Empty area/method/code inputs are ignored (no map entries created). + func testRecordUsageIgnoresEmptyInputs() { + let tracker: CountlyHealthTracker = CountlyHealthTracker.sharedInstance() + tracker.methodUsage.removeAllObjects() + tracker.logCodes.removeAllObjects() + + tracker.recordUsage("", method: "record") + tracker.recordUsage("events", method: "") + tracker.recordLogCode("") + + drainHealthCheckQueue(tracker) + + XCTAssertEqual(tracker.methodUsage.count, 0) + XCTAssertEqual(tracker.logCodes.count, 0) + } + + /// All mutation goes through the serial hcQueue, so concurrent recordUsage must not crash. + func testRecordUsageThreadSafety() { + let tracker: CountlyHealthTracker = CountlyHealthTracker.sharedInstance() + tracker.methodUsage.removeAllObjects() + runConcurrentStressTest( + iterations: 10_000, + writerBlock: { _ in tracker.recordUsage("events", method: "record") }, + readerBlock: { tracker.saveState() } + ) + // No crash / no exception under contention is the primary assertion; + // also confirm the count is positive and within the cap. + drainHealthCheckQueue(tracker, timeout: 10) + let count = (tracker.methodUsage["events"] as? [String: NSNumber])?["record"]?.intValue ?? 0 + XCTAssertTrue(count > 0 && count <= 65535, "expected 0 < count <= 65535, got \(count)") + } + + /// Maps persist into the health-tracker state dict and are cleared on clearAndSave. + func testUsageAndLogCodesPersistAndClear() { + let tracker: CountlyHealthTracker = CountlyHealthTracker.sharedInstance() + tracker.methodUsage.removeAllObjects() + tracker.logCodes.removeAllObjects() + tracker.recordUsage("events", method: "record") + tracker.recordLogCode("w204") + + drainHealthCheckQueue(tracker) + + let state = CountlyPersistency.sharedInstance().retrieveHealthCheckTrackerState() ?? [:] + let mu = state["MU"] as? [String: [String: NSNumber]] + let lc = state["LC"] as? [String: NSNumber] + XCTAssertEqual(mu?["events"]?["record"], 1) + XCTAssertEqual(lc?["w204"], 1) + + // clearAndSave empties the maps. + let exp = expectation(description: "clear") + tracker.clearAndSave() + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { exp.fulfill() } + wait(for: [exp], timeout: 5) + XCTAssertEqual(tracker.methodUsage.count, 0) + XCTAssertEqual(tracker.logCodes.count, 0) + } + + /// fu/lc keys appear in the hc request only when the maps are non-empty. + func testHealthCheckRequestIncludesFuLcWhenPresent() { + let tracker: CountlyHealthTracker = CountlyHealthTracker.sharedInstance() + let config = createBaseConfig() + Countly.sharedInstance().start(with: config) + drainHealthCheckQueue(tracker) + + tracker.methodUsage.removeAllObjects() + tracker.logCodes.removeAllObjects() + tracker.recordUsage("events", method: "record") + tracker.recordLogCode("w204") + drainHealthCheckQueue(tracker) + + let request: URLRequest = tracker.healthCheckRequest()! + let query = request.url?.query ?? String(data: request.httpBody ?? Data(), encoding: .utf8) ?? "" + let decoded = query.removingPercentEncoding ?? query + XCTAssertTrue(decoded.contains("\"fu\""), "hc should contain fu; got: \(decoded)") + XCTAssertTrue(decoded.contains("\"lc\""), "hc should contain lc; got: \(decoded)") + XCTAssertTrue(decoded.contains("\"record\"")) + XCTAssertTrue(decoded.contains("\"w204\"")) + } + + func testHealthCheckRequestOmitsFuLcWhenEmpty() { + let tracker: CountlyHealthTracker = CountlyHealthTracker.sharedInstance() + let config = createBaseConfig() + Countly.sharedInstance().start(with: config) + drainHealthCheckQueue(tracker) + + tracker.methodUsage.removeAllObjects() + tracker.logCodes.removeAllObjects() + drainHealthCheckQueue(tracker) + + let request: URLRequest = tracker.healthCheckRequest()! + let query = request.url?.query ?? String(data: request.httpBody ?? Data(), encoding: .utf8) ?? "" + let decoded = query.removingPercentEncoding ?? query + XCTAssertFalse(decoded.contains("\"fu\"")) + XCTAssertFalse(decoded.contains("\"lc\"")) + } + + /// Calling a public API records usage under the shared taxonomy. + func testPublicMethodsPopulateUsage() { + let tracker: CountlyHealthTracker = CountlyHealthTracker.sharedInstance() + tracker.methodUsage.removeAllObjects() + + let config = createBaseConfig() + Countly.sharedInstance().start(with: config) + Countly.sharedInstance().recordEvent("test_event") + Countly.sharedInstance().recordEvent("test_event_2") + + drainHealthCheckQueue(tracker) + + let count = (tracker.methodUsage["events"] as? [String: NSNumber])?["record"]?.intValue ?? 0 + XCTAssertTrue(count >= 2, "expected >=2 events.record, got \(count)") + } + + /// A consent-blocked action records the w401 log code. + func testConsentBlockedRecordsLogCode() { + let tracker: CountlyHealthTracker = CountlyHealthTracker.sharedInstance() + tracker.logCodes.removeAllObjects() + + let config = createBaseConfig() + config.requiresConsent = true // no consent given + Countly.sharedInstance().start(with: config) + // an event recorded without events consent should be blocked and record w401 + Countly.sharedInstance().recordEvent("blocked_event") + + drainHealthCheckQueue(tracker) + + XCTAssertNotNil(tracker.logCodes["w401"], "expected w401 consent-blocked code") + } + + // MARK: - Auto / Manual & Deprecated differentiation + + /// The current `startView` API records the bare `start` leaf, while the deprecated `recordView` + /// API records `start:d` — the two are distinct keys and the deprecated call must NOT leak into + /// the modern `startAuto` leaf. + func testManualAndDeprecatedViewAreDistinct() { + let tracker: CountlyHealthTracker = CountlyHealthTracker.sharedInstance() + tracker.methodUsage.removeAllObjects() + + let config = createBaseConfig() + Countly.sharedInstance().start(with: config) + let _ = Countly.sharedInstance().views().startView("CurrentScreen") + Countly.sharedInstance().recordView("LegacyScreen") + + drainHealthCheckQueue(tracker) + + let views = tracker.methodUsage["views"] as? [String: NSNumber] + XCTAssertEqual(views?["start"], 1, "current startView should record bare start") + XCTAssertEqual(views?["start:d"], 1, "deprecated recordView should record start:d") + XCTAssertNil(views?["startAuto"], "deprecated recordView must not record startAuto") + } + + /// The deprecated handled/unhandled exception APIs record `crashes` `record:d`, distinct from the + /// current `recordException` API which records bare `record`. + func testDeprecatedExceptionRecordsRecordD() { + let tracker: CountlyHealthTracker = CountlyHealthTracker.sharedInstance() + tracker.methodUsage.removeAllObjects() + + let config = createBaseConfig() + Countly.sharedInstance().start(with: config) + let exception = NSException(name: .genericException, reason: "test", userInfo: nil) + Countly.sharedInstance().recordHandledException(exception) + Countly.sharedInstance().recordUnhandledException(exception, withStackTrace: []) + + drainHealthCheckQueue(tracker) + + let crashes = tracker.methodUsage["crashes"] as? [String: NSNumber] + XCTAssertEqual(crashes?["record:d"], 2, "deprecated exception APIs should record record:d") + XCTAssertNil(crashes?["record"], "deprecated APIs must not record the bare record leaf") + } + + /// Modifier-suffixed leaves coexist with their bare counterparts as separate keys + /// (auto `:a` and deprecated `:d` are tracked independently of the manual/current call). + func testModifierLeavesAreIndependentKeys() { + let tracker: CountlyHealthTracker = CountlyHealthTracker.sharedInstance() + tracker.methodUsage.removeAllObjects() + + tracker.recordUsage("sessions", method: "begin") + tracker.recordUsage("sessions", method: "begin:a") + tracker.recordUsage("sessions", method: "begin:a") + tracker.recordUsage("views", method: "start:d") + + drainHealthCheckQueue(tracker) + + let sessions = tracker.methodUsage["sessions"] as? [String: NSNumber] + XCTAssertEqual(sessions?["begin"], 1, "manual begin tracked separately") + XCTAssertEqual(sessions?["begin:a"], 2, "automatic begin:a tracked separately") + XCTAssertEqual((tracker.methodUsage["views"] as? [String: NSNumber])?["start:d"], 1) + } + + // MARK: - Log codes from real call sites + + /// Starting the SDK a second time is ignored and records the w110 init-twice code. + func testInitTwiceRecordsW110() { + let tracker: CountlyHealthTracker = CountlyHealthTracker.sharedInstance() + tracker.logCodes.removeAllObjects() + + let config = createBaseConfig() + Countly.sharedInstance().start(with: config) + Countly.sharedInstance().start(with: config) // second start is ignored + + drainHealthCheckQueue(tracker) + + XCTAssertNotNil(tracker.logCodes["w110"], "second start should record w110") + } + + /// Ending or cancelling a timed event that was never started records w720 each time. + func testEndingNonStartedTimedEventRecordsW720() { + let tracker: CountlyHealthTracker = CountlyHealthTracker.sharedInstance() + tracker.logCodes.removeAllObjects() + + let config = createBaseConfig() + Countly.sharedInstance().start(with: config) + Countly.sharedInstance().endEvent("never_started") + Countly.sharedInstance().cancelEvent("also_never_started") + + drainHealthCheckQueue(tracker) + + XCTAssertEqual(tracker.logCodes["w720"] as? NSNumber, 2, "end + cancel of non-started events → 2x w720") + } + + /// Starting the same timed-event key twice records w722. + func testDuplicateStartEventRecordsW722() { + let tracker: CountlyHealthTracker = CountlyHealthTracker.sharedInstance() + tracker.logCodes.removeAllObjects() + + let config = createBaseConfig() + Countly.sharedInstance().start(with: config) + Countly.sharedInstance().startEvent("dup_key") + Countly.sharedInstance().startEvent("dup_key") // already started + + drainHealthCheckQueue(tracker) + + XCTAssertNotNil(tracker.logCodes["w722"], "second startEvent with same key → w722") + } + + /// A manual view call while automatic view tracking is active is ignored and records w740. + func testManualViewDuringAutoTrackingRecordsW740() { + let tracker: CountlyHealthTracker = CountlyHealthTracker.sharedInstance() + tracker.logCodes.removeAllObjects() + + let config = createBaseConfig() + config.enableAutomaticViewTracking = true + Countly.sharedInstance().start(with: config) + let _ = Countly.sharedInstance().views().startView("ManualWhileAuto") + + drainHealthCheckQueue(tracker) + + XCTAssertNotNil(tracker.logCodes["w740"], "manual startView during auto tracking → w740") + } } diff --git a/CountlyTests/CountlyTests-Bridging-Header.h b/CountlyTests/CountlyTests-Bridging-Header.h index 4c7fa203..79897b80 100644 --- a/CountlyTests/CountlyTests-Bridging-Header.h +++ b/CountlyTests/CountlyTests-Bridging-Header.h @@ -2,6 +2,7 @@ #import "CountlyConnectionManager.h" #import "CountlyViewTrackingInternal.h" #import "CountlyHealthTracker.h" +#import "CountlyHealthTracker+Tests.h" #import "CountlyContentBuilderInternal.h" #import "CountlyWebViewManager.h" #import "CountlyWebViewManager+Tests.h" diff --git a/CountlyViewTrackingInternal.h b/CountlyViewTrackingInternal.h index 4f342350..107e52d3 100644 --- a/CountlyViewTrackingInternal.h +++ b/CountlyViewTrackingInternal.h @@ -37,6 +37,7 @@ extern NSString* const kCountlyVTKeyVisit; - (void)setGlobalViewSegmentation:(NSDictionary *)segmentation; - (void)updateGlobalViewSegmentation:(NSDictionary *)segmentation; - (NSString *)startAutoStoppedView:(NSString *)viewName segmentation:(NSDictionary *)segmentation; +- (NSString *)recordViewDeprecated:(NSString *)viewName segmentation:(NSDictionary *)segmentation; - (NSString *)startView:(NSString *)viewName segmentation:(NSDictionary *)segmentation; - (void)stopViewWithName:(NSString *)viewName segmentation:(NSDictionary *)segmentation; - (void)stopViewWithID:(NSString *)viewID segmentation:(NSDictionary *)segmentation; diff --git a/CountlyViewTrackingInternal.m b/CountlyViewTrackingInternal.m index 8407db6d..ca380497 100644 --- a/CountlyViewTrackingInternal.m +++ b/CountlyViewTrackingInternal.m @@ -156,9 +156,11 @@ - (void)updateGlobalViewSegmentation:(NSDictionary *)segmentation - (NSString *)startView:(NSString *)viewName segmentation:(NSDictionary *)segmentation { CLY_LOG_I(@"%s %@ %@", __FUNCTION__, viewName, segmentation); + [CountlyHealthTracker.sharedInstance recordUsage:@"views" method:@"start"]; #if (TARGET_OS_IOS || TARGET_OS_TV) if (self.isAutoViewTrackingActive) { CLY_LOG_W(@"%s Manually start view tracking is not allowed when automatic tracking is enabled!", __FUNCTION__); + [CountlyHealthTracker.sharedInstance recordLogCode:@"w740"]; return nil; } #endif @@ -169,9 +171,11 @@ - (NSString *)startView:(NSString *)viewName segmentation:(NSDictionary *)segmen - (NSString *)startAutoStoppedView:(NSString *)viewName segmentation:(NSDictionary *)segmentation { CLY_LOG_I(@"%s %@ %@", __FUNCTION__, viewName, segmentation); + [CountlyHealthTracker.sharedInstance recordUsage:@"views" method:@"startAuto"]; #if (TARGET_OS_IOS || TARGET_OS_TV) if (self.isAutoViewTrackingActive) { CLY_LOG_W(@"%s Manually start view tracking is not allowed when automatic tracking is enabled!", __FUNCTION__); + [CountlyHealthTracker.sharedInstance recordLogCode:@"w740"]; return nil; } #endif @@ -179,12 +183,30 @@ - (NSString *)startAutoStoppedView:(NSString *)viewName segmentation:(NSDictiona return viewID; } +// Backing call for the deprecated `recordView:` API. Mirrors `startAutoStoppedView:` but records +// the usage under `start:d` so deprecated view calls are distinguishable from the current API. +- (NSString *)recordViewDeprecated:(NSString *)viewName segmentation:(NSDictionary *)segmentation +{ + CLY_LOG_I(@"%s %@ %@", __FUNCTION__, viewName, segmentation); + [CountlyHealthTracker.sharedInstance recordUsage:@"views" method:@"start:d"]; +#if (TARGET_OS_IOS || TARGET_OS_TV) + if (self.isAutoViewTrackingActive) { + CLY_LOG_W(@"%s Manually start view tracking is not allowed when automatic tracking is enabled!", __FUNCTION__); + [CountlyHealthTracker.sharedInstance recordLogCode:@"w740"]; + return nil; + } +#endif + return [self startViewInternal:viewName customSegmentation:segmentation isAutoStoppedView:YES]; +} + - (void)stopViewWithName:(NSString *)viewName segmentation:(NSDictionary *)segmentation { CLY_LOG_I(@"%s %@ %@", __FUNCTION__, viewName, segmentation); + [CountlyHealthTracker.sharedInstance recordUsage:@"views" method:@"stop"]; #if (TARGET_OS_IOS || TARGET_OS_TV) if (self.isAutoViewTrackingActive) { CLY_LOG_W(@"%s Manually stop view tracking is not allowed when automatic tracking is enabled!", __FUNCTION__); + [CountlyHealthTracker.sharedInstance recordLogCode:@"w740"]; return; } #endif @@ -195,9 +217,11 @@ - (void)stopViewWithName:(NSString *)viewName segmentation:(NSDictionary *)segme - (void)stopViewWithID:(NSString *)viewID segmentation:(NSDictionary *)segmentation { CLY_LOG_I(@"%s %@ %@", __FUNCTION__, viewID, segmentation); + [CountlyHealthTracker.sharedInstance recordUsage:@"views" method:@"stop"]; #if (TARGET_OS_IOS || TARGET_OS_TV) if (self.isAutoViewTrackingActive) { CLY_LOG_W(@"%s Manually stop view tracking is not allowed when automatic tracking is enabled!", __FUNCTION__); + [CountlyHealthTracker.sharedInstance recordLogCode:@"w740"]; return; } #endif @@ -210,6 +234,7 @@ - (void)pauseViewWithID:(NSString *)viewID #if (TARGET_OS_IOS || TARGET_OS_TV) if (self.isAutoViewTrackingActive) { CLY_LOG_W(@"%s Manually pause view tracking is not allowed when automatic tracking is enabled!", __FUNCTION__); + [CountlyHealthTracker.sharedInstance recordLogCode:@"w740"]; return; } #endif @@ -222,6 +247,7 @@ - (void)resumeViewWithID:(NSString *)viewID #if (TARGET_OS_IOS || TARGET_OS_TV) if (self.isAutoViewTrackingActive) { CLY_LOG_W(@"%s Manually resume view tracking is not allowed when automatic tracking is enabled!", __FUNCTION__); + [CountlyHealthTracker.sharedInstance recordLogCode:@"w740"]; return; } #endif @@ -234,6 +260,7 @@ - (void)stopAllViews:(NSDictionary *)segmentation #if (TARGET_OS_IOS || TARGET_OS_TV) if (self.isAutoViewTrackingActive) { CLY_LOG_W(@"%s Manually stop view tracking is not allowed when automatic tracking is enabled!", __FUNCTION__); + [CountlyHealthTracker.sharedInstance recordLogCode:@"w740"]; return; } #endif @@ -261,6 +288,7 @@ - (void)startAutoViewTracking UIViewController* topVC = CountlyCommon.sharedInstance.topViewController; NSString* viewTitle = [self titleForViewController:topVC]; + [CountlyHealthTracker.sharedInstance recordUsage:@"views" method:@"start:a"]; [self startViewInternal:viewTitle customSegmentation:nil]; } @@ -716,7 +744,10 @@ - (void)performAutoViewTrackingForViewController:(UIViewController *)viewControl } if (!isException) + { + [CountlyHealthTracker.sharedInstance recordUsage:@"views" method:@"start:a"]; [self startViewInternal:viewTitle customSegmentation:nil]; + } }