Skip to content

feat: health tracker #392

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 13 commits into
base: staging
Choose a base branch
from
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
## XX.XX.XX
* Adding SDK health check requests after init
* Added a config method to disable server config in the initialization "disableSDKBehaviorSettings()".

## 25.4.1
Expand Down
10 changes: 9 additions & 1 deletion Countly.m
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,8 @@ - (void)startWithConfig:(CountlyConfig *)config

if (config.indirectAttribution)
[self recordIndirectAttribution:config.indirectAttribution];

[CountlyHealthTracker.sharedInstance sendHealthCheck];
}

- (CountlyConfig *) checkAndFixInternalLimitsConfig:(CountlyConfig *)config
Expand Down Expand Up @@ -456,11 +458,13 @@ - (void)applicationDidBecomeActive:(NSNotification *)notification
- (void)applicationWillResignActive:(NSNotification *)notification
{
CLY_LOG_D(@"App enters background");
[CountlyHealthTracker.sharedInstance saveState];
}

- (void)applicationDidEnterBackground:(NSNotification *)notification
{
CLY_LOG_D(@"App did enter background.");
[CountlyHealthTracker.sharedInstance saveState];
[self suspend];
}

Expand All @@ -473,6 +477,8 @@ - (void)applicationWillTerminate:(NSNotification *)notification
{
CLY_LOG_D(@"App will terminate.");

[CountlyHealthTracker.sharedInstance saveState];

CountlyConnectionManager.sharedInstance.isTerminating = YES;

[CountlyViewTrackingInternal.sharedInstance applicationWillTerminate];
Expand Down Expand Up @@ -695,12 +701,14 @@ - (void)setIDInternal:(NSString *)deviceID onServer:(BOOL)onServer
CLY_LOG_I(@"Going out of CLYTemporaryDeviceID mode and switching back to normal mode.");

[CountlyDeviceInfo.sharedInstance initializeDeviceID:deviceID];

[CountlyPersistency.sharedInstance replaceAllTemporaryDeviceIDsInQueueWithDeviceID:deviceID];

[CountlyConnectionManager.sharedInstance proceedOnQueue];

[CountlyRemoteConfigInternal.sharedInstance downloadRemoteConfigAutomatically];

[CountlyHealthTracker.sharedInstance sendHealthCheck];

return;
}
Expand Down
8 changes: 8 additions & 0 deletions Countly.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@
96329DE02D9426F300BFD641 /* CountlyServerConfigTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96329DDF2D9426F300BFD641 /* CountlyServerConfigTests.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 */; };
965A2E9D2DDDCDAC00F28F6A /* CountlyHealthTracker.h in Headers */ = {isa = PBXBuildFile; fileRef = 965A2E9A2DDDCDAC00F28F6A /* CountlyHealthTracker.h */; };
968426812BF2302C007B303E /* CountlyConnectionManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 968426802BF2302C007B303E /* CountlyConnectionManagerTests.swift */; };
96DA74BB2D9FB687006FA6FF /* MockFeedbackWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96DA74BA2D9FB687006FA6FF /* MockFeedbackWidget.swift */; };
96E680422BFF89AC0091E105 /* CountlyCrashReporterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96E680412BFF89AC0091E105 /* CountlyCrashReporterTests.swift */; };
Expand Down Expand Up @@ -190,6 +192,8 @@
96329DDF2D9426F300BFD641 /* CountlyServerConfigTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountlyServerConfigTests.swift; sourceTree = "<group>"; };
96329DE12D94299D00BFD641 /* ServerConfigBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerConfigBuilder.swift; sourceTree = "<group>"; };
96329DE32D952F1500BFD641 /* MockURLProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockURLProtocol.swift; sourceTree = "<group>"; };
965A2E9A2DDDCDAC00F28F6A /* CountlyHealthTracker.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CountlyHealthTracker.h; sourceTree = "<group>"; };
965A2E9B2DDDCDAC00F28F6A /* CountlyHealthTracker.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CountlyHealthTracker.m; sourceTree = "<group>"; };
96681A9B2D97D9B300A4845A /* CountlyTests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = CountlyTests.xctestplan; sourceTree = "<group>"; };
968426802BF2302C007B303E /* CountlyConnectionManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountlyConnectionManagerTests.swift; sourceTree = "<group>"; };
96DA74BA2D9FB687006FA6FF /* MockFeedbackWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockFeedbackWidget.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -320,6 +324,8 @@
3B20A9A42245228500E3D7AE /* CountlyUserDetails.m */,
3B20A9A72245228500E3D7AE /* CountlyViewTrackingInternal.h */,
3B20A9A32245228500E3D7AE /* CountlyViewTrackingInternal.m */,
965A2E9A2DDDCDAC00F28F6A /* CountlyHealthTracker.h */,
965A2E9B2DDDCDAC00F28F6A /* CountlyHealthTracker.m */,
3B20A9862245225A00E3D7AE /* Info.plist */,
1A5C4C952B35B0850032EE1F /* CountlyTests */,
3B20A9832245225A00E3D7AE /* Products */,
Expand Down Expand Up @@ -355,6 +361,7 @@
39BDF7572CC7CA870066DE7C /* CountlyFeedbacks.h in Headers */,
3B20A9D32245228700E3D7AE /* CountlyPushNotifications.h in Headers */,
3B20A9C42245228700E3D7AE /* CountlyUserDetails.h in Headers */,
965A2E9D2DDDCDAC00F28F6A /* CountlyHealthTracker.h in Headers */,
3961C6B72C6633C000DD38BA /* PassThroughBackgroundView.h in Headers */,
3B20A9CA2245228700E3D7AE /* CountlyConfig.h in Headers */,
3B20A9872245225A00E3D7AE /* Countly.h in Headers */,
Expand Down Expand Up @@ -536,6 +543,7 @@
1A3110712A7141AF001CB507 /* CountlyViewTracking.m in Sources */,
3903429D2C8051C700238C96 /* CountlyExperimentalConfig.m in Sources */,
1A3A576329ED47A20041B7BE /* CountlyServerConfig.m in Sources */,
965A2E9C2DDDCDAC00F28F6A /* CountlyHealthTracker.m in Sources */,
D219374C248AC71C00E5798B /* CountlyPerformanceMonitoring.m in Sources */,
3B20A9B42245228700E3D7AE /* CountlyPushNotifications.m in Sources */,
3B20A9C92245228700E3D7AE /* CountlyUserDetails.m in Sources */,
Expand Down
1 change: 1 addition & 0 deletions CountlyCommon.h
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
#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__)
Expand Down
6 changes: 6 additions & 0 deletions CountlyCommon.m
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,12 @@ - (BOOL)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;

Expand Down
4 changes: 4 additions & 0 deletions CountlyConnectionManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,8 @@ - (void)proceedOnQueue
else
{
CLY_LOG_D(@"%s, request:[ <%p> ] failed! response:[ %@ ]", __FUNCTION__, request, [data cly_stringUTF8]);
[CountlyHealthTracker.sharedInstance logFailedNetworkRequestWithStatusCode:((NSHTTPURLResponse*)response).statusCode errorResponse:[NSString stringWithUTF8String:[data bytes]]];
[CountlyHealthTracker.sharedInstance saveState];
self.startTime = nil;
}
}
Expand All @@ -309,6 +311,8 @@ - (void)proceedOnQueue
#if (TARGET_OS_WATCH)
[CountlyPersistency.sharedInstance saveToFile];
#endif
[CountlyHealthTracker.sharedInstance logFailedNetworkRequestWithStatusCode:((NSHTTPURLResponse*)response).statusCode errorResponse: [NSString stringWithFormat:@"%@", error]];
[CountlyHealthTracker.sharedInstance saveState];
self.startTime = nil;
}
}];
Expand Down
29 changes: 29 additions & 0 deletions CountlyHealthTracker.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
//
// CountlyHealthTracker.h
// CountlyTestApp-iOS
//
// Created by Arif Burak Demiray on 20.05.2025.
// Copyright © 2025 Countly. All rights reserved.
//
#import <Foundation/Foundation.h>

@interface CountlyHealthTracker : NSObject

+ (instancetype)sharedInstance;

- (void)logWarning;

- (void)logError;

- (void)logFailedNetworkRequestWithStatusCode:(NSInteger)statusCode
errorResponse:(NSString *)errorResponse;

- (void)logBackoffRequest;

- (void)clearAndSave;

- (void)saveState;

- (void)sendHealthCheck;

@end
215 changes: 215 additions & 0 deletions CountlyHealthTracker.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
//
// CountlyHealthTracker.m
// CountlyTestApp-iOS
//
// Created by Arif Burak Demiray on 20.05.2025.
// Copyright © 2025 Countly. All rights reserved.
//

#import <Foundation/Foundation.h>
#import "CountlyHealthTracker.h"
#import "CountlyCommon.h"

@interface CountlyHealthTracker ()

@property (nonatomic, assign) long countLogWarning;
@property (nonatomic, assign) long countLogError;
@property (nonatomic, assign) long countBackoffRequest;
@property (nonatomic, assign) NSInteger statusCode;
@property (nonatomic, strong) NSString *errorMessage;
@property (nonatomic, assign) BOOL healthCheckEnabled;
@property (nonatomic, assign) BOOL healthCheckSent;

@end

@implementation CountlyHealthTracker

NSString * const keyLogError = @"LErr";
NSString * const keyLogWarning = @"LWar";
NSString * const keyStatusCode = @"RStatC";
NSString * const keyErrorMessage = @"REMsg";
NSString * const keyBackoffRequest = @"BReq";

NSString * const requestKeyErrorCount = @"el";
NSString * const requestKeyWarningCount = @"wl";
NSString * const requestKeyStatusCode = @"sc";
NSString * const requestKeyRequestError = @"em";
NSString * const requestKeyBackoffRequest = @"br";

+ (instancetype)sharedInstance {
static CountlyHealthTracker *instance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [[self alloc] init];
});
return instance;
}

- (instancetype)init{
self = [super init];
if (self) {
_errorMessage = @"";
_statusCode = -1;
_healthCheckSent = NO;
_healthCheckEnabled = YES;

NSDictionary *initialState = [CountlyPersistency.sharedInstance retrieveHealthCheckTrackerState];
[self setupInitialCounters:initialState];
}
return self;
}

- (void)setupInitialCounters:(NSDictionary *)initialState {
if (initialState == nil || [initialState count] == 0) {
return;
}

self.countLogWarning = [initialState[keyLogWarning] longValue];
self.countLogError = [initialState[keyLogError] longValue];
self.statusCode = [initialState[keyStatusCode] integerValue];
self.errorMessage = initialState[keyErrorMessage] ?: @"";
self.countBackoffRequest = [initialState[keyBackoffRequest] longValue];

CLY_LOG_D(@"%s, Loaded initial health check state: [%@]", __FUNCTION__, initialState);
}

- (void)logWarning {
self.countLogWarning++;
}

- (void)logError {
self.countLogError++;
}

- (void)logFailedNetworkRequestWithStatusCode:(NSInteger)statusCode
errorResponse:(NSString *)errorResponse {
if (statusCode <= 0 || statusCode >= 1000 || errorResponse == nil) {
return;
}

self.statusCode = statusCode;
if (errorResponse.length > 1000) {
self.errorMessage = [errorResponse substringToIndex:1000];
} else {
self.errorMessage = errorResponse;
}
}

- (void)logBackoffRequest {
self.countBackoffRequest++;
}

- (void)clearAndSave {
[self clearValues];
[CountlyPersistency.sharedInstance storeHealthCheckTrackerState:@{}];
}

- (void)saveState {
NSDictionary *healthCheckState = @{
keyLogWarning: @(self.countLogWarning),
keyLogError: @(self.countLogError),
keyStatusCode: @(self.statusCode),
keyErrorMessage: self.errorMessage ?: @"",
keyBackoffRequest: @(self.countBackoffRequest)
};

[CountlyPersistency.sharedInstance storeHealthCheckTrackerState:healthCheckState];
}

- (void)clearValues {
CLY_LOG_W(@"%s, Clearing counters", __FUNCTION__);

self.countLogWarning = 0;
self.countLogError = 0;
self.statusCode = -1;
self.errorMessage = @"";
self.countBackoffRequest = 0;
}

- (void)sendHealthCheck {
if (CountlyDeviceInfo.sharedInstance.isDeviceIDTemporary) {
CLY_LOG_W(@"%s, currently in temporary id mode, omitting", __FUNCTION__);
}

if (!_healthCheckEnabled || _healthCheckSent) {
CLY_LOG_D(@"%s, health check status, sent: %d, not_enabled: %d", __FUNCTION__, _healthCheckSent, _healthCheckEnabled);
}

NSURLSessionTask* task = [CountlyCommon.sharedInstance.URLSession dataTaskWithRequest:[self healthCheckRequest] completionHandler:^(NSData* data, NSURLResponse* response, NSError* error)
{
if (error)
{
CLY_LOG_W(@"%s, error while sending health checks error: %@", __FUNCTION__, error);
return;
}

NSError *jsonError;
NSDictionary *jsonResponse = [NSJSONSerialization JSONObjectWithData:data options:0 error:&jsonError];

if (jsonError || !jsonResponse) {
CLY_LOG_I(@"%s, error while sending health checks, Failed to parse JSON response: %@", __FUNCTION__, jsonError);
return;
}

if (!jsonResponse[@"result"]) {
CLY_LOG_D(@"%s, Retrieved request response does not match expected pattern %@", __FUNCTION__, jsonResponse);
return;
}

[self clearAndSave];
self->_healthCheckSent = YES;
}];

[task resume];
}

- (NSString *)dictionaryToJsonString:(NSDictionary *)json {
NSError *error;
NSData *data = [NSJSONSerialization dataWithJSONObject:json options:0 error:&error];
NSString *encodedData = @"";

if (!error && data) {
NSString *jsonString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
encodedData = [jsonString stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]];
} else {
CLY_LOG_W(@"%s, Failed to create json for hc request, %@", __FUNCTION__, error);
}

return encodedData;
}

- (NSURLRequest *)healthCheckRequest {
NSString *queryString = [CountlyConnectionManager.sharedInstance queryEssentials];

queryString = [queryString stringByAppendingFormat:@"&%@=%@", @"hc", [self dictionaryToJsonString:@{
requestKeyErrorCount: @(self.countLogError),
requestKeyWarningCount: @(self.countLogWarning),
requestKeyStatusCode: @(self.statusCode),
requestKeyRequestError: self.errorMessage ?: @"",
requestKeyBackoffRequest: @(self.countBackoffRequest)
}]];

queryString = [queryString stringByAppendingFormat:@"&%@=%@", @"metrics", [self dictionaryToJsonString:@{
kCountlyAppVersionKey: CountlyDeviceInfo.appVersion
}]];

queryString = [CountlyConnectionManager.sharedInstance appendChecksum:queryString];
NSString* hcSendURL = [CountlyConnectionManager.sharedInstance.host stringByAppendingFormat:@"%@",kCountlyEndpointI];

CLY_LOG_D(@"%s, generated health check request: %@", __FUNCTION__, queryString);

if (queryString.length > kCountlyGETRequestMaxLength || CountlyConnectionManager.sharedInstance.alwaysUsePOST)
{
NSMutableURLRequest* request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:hcSendURL]];
request.HTTPMethod = @"POST";
request.HTTPBody = [queryString cly_dataUTF8];
return request.copy;
}
else
{
NSString* withQueryString = [hcSendURL stringByAppendingFormat:@"?%@", queryString];
NSURLRequest* request = [NSURLRequest requestWithURL:[NSURL URLWithString:withQueryString]];
return request;
}
}
@end
3 changes: 3 additions & 0 deletions CountlyPersistency.h
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@
- (NSDictionary *)retrieveServerConfig;
- (void)storeServerConfig:(NSDictionary *)serverConfig;

- (NSDictionary *)retrieveHealthCheckTrackerState;
- (void)storeHealthCheckTrackerState:(NSDictionary *)healthCheckTrackerState;

-(BOOL)isOldRequest:(NSString*) queryString;

@property (nonatomic) NSUInteger eventSendThreshold;
Expand Down
Loading