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 8 commits into
base: staging
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
## XX.XX.XX
* Adding SDK health check requests after init

## 25.4.1
* Mitigated an issue that could occur while serializing events to improve stability, performance and memory usage.

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

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

[CountlyHealthTracker.sharedInstance sendHealthCheck];
}

- (CountlyConfig *) checkAndFixInternalLimitsConfig:(CountlyConfig *)config
Expand Down Expand Up @@ -453,11 +455,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 @@ -470,6 +474,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 @@ -692,12 +698,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 retrieveHealtCheckTrackerState];
[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 storeHealtCheckTrackerState:@{}];
}

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

[CountlyPersistency.sharedInstance storeHealtCheckTrackerState:healtCheckState];
}

- (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, healt check status, sent: %d, not_enabled: %d", __FUNCTION__, _healthCheckSent, _healthCheckEnabled);
}

NSURLSessionTask* task = [CountlyCommon.sharedInstance.URLSession dataTaskWithRequest:[self healtCheckRequest] 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 *)healtCheckRequest {
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 *)retrieveHealtCheckTrackerState;
- (void)storeHealtCheckTrackerState:(NSDictionary *)healthCheckTrackerState;

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

@property (nonatomic) NSUInteger eventSendThreshold;
Expand Down
Loading
Loading