Skip to content

Commit 2b6e7ef

Browse files
authored
Merge pull request #392 from Countly/health_tracker
feat: health tracker
2 parents d4a168e + 5524d29 commit 2b6e7ef

10 files changed

+290
-1
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
## XX.XX.XX
2+
* Adding SDK health check requests after init
23
* The feedback widgets now have fullscreen and transparent backgrounds for a cleaner look.
34
* Added a config method to disable server config in the initialization "disableSDKBehaviorSettings()".
45

Countly.m

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,8 @@ - (void)startWithConfig:(CountlyConfig *)config
320320

321321
if (config.indirectAttribution)
322322
[self recordIndirectAttribution:config.indirectAttribution];
323+
324+
[CountlyHealthTracker.sharedInstance sendHealthCheck];
323325
}
324326

325327
- (CountlyConfig *) checkAndFixInternalLimitsConfig:(CountlyConfig *)config
@@ -456,11 +458,13 @@ - (void)applicationDidBecomeActive:(NSNotification *)notification
456458
- (void)applicationWillResignActive:(NSNotification *)notification
457459
{
458460
CLY_LOG_D(@"App enters background");
461+
[CountlyHealthTracker.sharedInstance saveState];
459462
}
460463

461464
- (void)applicationDidEnterBackground:(NSNotification *)notification
462465
{
463466
CLY_LOG_D(@"App did enter background.");
467+
[CountlyHealthTracker.sharedInstance saveState];
464468
[self suspend];
465469
}
466470

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

480+
[CountlyHealthTracker.sharedInstance saveState];
481+
476482
CountlyConnectionManager.sharedInstance.isTerminating = YES;
477483

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

697703
[CountlyDeviceInfo.sharedInstance initializeDeviceID:deviceID];
698-
704+
699705
[CountlyPersistency.sharedInstance replaceAllTemporaryDeviceIDsInQueueWithDeviceID:deviceID];
700706

701707
[CountlyConnectionManager.sharedInstance proceedOnQueue];
702708

703709
[CountlyRemoteConfigInternal.sharedInstance downloadRemoteConfigAutomatically];
710+
711+
[CountlyHealthTracker.sharedInstance sendHealthCheck];
704712

705713
return;
706714
}

Countly.xcodeproj/project.pbxproj

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,8 @@
8686
96329DE02D9426F300BFD641 /* CountlyServerConfigTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96329DDF2D9426F300BFD641 /* CountlyServerConfigTests.swift */; };
8787
96329DE22D94299D00BFD641 /* ServerConfigBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96329DE12D94299D00BFD641 /* ServerConfigBuilder.swift */; };
8888
96329DE42D952F1500BFD641 /* MockURLProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96329DE32D952F1500BFD641 /* MockURLProtocol.swift */; };
89+
965A2E9C2DDDCDAC00F28F6A /* CountlyHealthTracker.m in Sources */ = {isa = PBXBuildFile; fileRef = 965A2E9B2DDDCDAC00F28F6A /* CountlyHealthTracker.m */; };
90+
965A2E9D2DDDCDAC00F28F6A /* CountlyHealthTracker.h in Headers */ = {isa = PBXBuildFile; fileRef = 965A2E9A2DDDCDAC00F28F6A /* CountlyHealthTracker.h */; };
8991
968426812BF2302C007B303E /* CountlyConnectionManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 968426802BF2302C007B303E /* CountlyConnectionManagerTests.swift */; };
9092
96DA74BB2D9FB687006FA6FF /* MockFeedbackWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96DA74BA2D9FB687006FA6FF /* MockFeedbackWidget.swift */; };
9193
96E680422BFF89AC0091E105 /* CountlyCrashReporterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96E680412BFF89AC0091E105 /* CountlyCrashReporterTests.swift */; };
@@ -190,6 +192,8 @@
190192
96329DDF2D9426F300BFD641 /* CountlyServerConfigTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountlyServerConfigTests.swift; sourceTree = "<group>"; };
191193
96329DE12D94299D00BFD641 /* ServerConfigBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerConfigBuilder.swift; sourceTree = "<group>"; };
192194
96329DE32D952F1500BFD641 /* MockURLProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockURLProtocol.swift; sourceTree = "<group>"; };
195+
965A2E9A2DDDCDAC00F28F6A /* CountlyHealthTracker.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CountlyHealthTracker.h; sourceTree = "<group>"; };
196+
965A2E9B2DDDCDAC00F28F6A /* CountlyHealthTracker.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CountlyHealthTracker.m; sourceTree = "<group>"; };
193197
96681A9B2D97D9B300A4845A /* CountlyTests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = CountlyTests.xctestplan; sourceTree = "<group>"; };
194198
968426802BF2302C007B303E /* CountlyConnectionManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountlyConnectionManagerTests.swift; sourceTree = "<group>"; };
195199
96DA74BA2D9FB687006FA6FF /* MockFeedbackWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockFeedbackWidget.swift; sourceTree = "<group>"; };
@@ -320,6 +324,8 @@
320324
3B20A9A42245228500E3D7AE /* CountlyUserDetails.m */,
321325
3B20A9A72245228500E3D7AE /* CountlyViewTrackingInternal.h */,
322326
3B20A9A32245228500E3D7AE /* CountlyViewTrackingInternal.m */,
327+
965A2E9A2DDDCDAC00F28F6A /* CountlyHealthTracker.h */,
328+
965A2E9B2DDDCDAC00F28F6A /* CountlyHealthTracker.m */,
323329
3B20A9862245225A00E3D7AE /* Info.plist */,
324330
1A5C4C952B35B0850032EE1F /* CountlyTests */,
325331
3B20A9832245225A00E3D7AE /* Products */,
@@ -355,6 +361,7 @@
355361
39BDF7572CC7CA870066DE7C /* CountlyFeedbacks.h in Headers */,
356362
3B20A9D32245228700E3D7AE /* CountlyPushNotifications.h in Headers */,
357363
3B20A9C42245228700E3D7AE /* CountlyUserDetails.h in Headers */,
364+
965A2E9D2DDDCDAC00F28F6A /* CountlyHealthTracker.h in Headers */,
358365
3961C6B72C6633C000DD38BA /* PassThroughBackgroundView.h in Headers */,
359366
3B20A9CA2245228700E3D7AE /* CountlyConfig.h in Headers */,
360367
3B20A9872245225A00E3D7AE /* Countly.h in Headers */,
@@ -536,6 +543,7 @@
536543
1A3110712A7141AF001CB507 /* CountlyViewTracking.m in Sources */,
537544
3903429D2C8051C700238C96 /* CountlyExperimentalConfig.m in Sources */,
538545
1A3A576329ED47A20041B7BE /* CountlyServerConfig.m in Sources */,
546+
965A2E9C2DDDCDAC00F28F6A /* CountlyHealthTracker.m in Sources */,
539547
D219374C248AC71C00E5798B /* CountlyPerformanceMonitoring.m in Sources */,
540548
3B20A9B42245228700E3D7AE /* CountlyPushNotifications.m in Sources */,
541549
3B20A9C92245228700E3D7AE /* CountlyUserDetails.m in Sources */,

CountlyCommon.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
#import "CountlyCrashData.h"
3232
#import "CountlyContentBuilderInternal.h"
3333
#import "CountlyExperimentalConfig.h"
34+
#import "CountlyHealthTracker.h"
3435

3536
#define CLY_LOG_E(fmt, ...) CountlyInternalLog(CLYInternalLogLevelError, fmt, ##__VA_ARGS__)
3637
#define CLY_LOG_W(fmt, ...) CountlyInternalLog(CLYInternalLogLevelWarning, fmt, ##__VA_ARGS__)

CountlyCommon.m

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,12 @@ - (BOOL)hasStarted_
8888

8989
void CountlyInternalLog(CLYInternalLogLevel level, NSString *format, ...)
9090
{
91+
if (level == CLYInternalLogLevelError) {
92+
[CountlyHealthTracker.sharedInstance logError];
93+
} else if(level == CLYInternalLogLevelWarning) {
94+
[CountlyHealthTracker.sharedInstance logWarning];
95+
}
96+
9197
if (!CountlyCommon.sharedInstance.enableDebug && !CountlyCommon.sharedInstance.loggerDelegate)
9298
return;
9399

CountlyConnectionManager.m

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,8 @@ - (void)proceedOnQueue
300300
else
301301
{
302302
CLY_LOG_D(@"%s, request:[ <%p> ] failed! response:[ %@ ]", __FUNCTION__, request, [data cly_stringUTF8]);
303+
[CountlyHealthTracker.sharedInstance logFailedNetworkRequestWithStatusCode:((NSHTTPURLResponse*)response).statusCode errorResponse: [data cly_stringUTF8]];
304+
[CountlyHealthTracker.sharedInstance saveState];
303305
self.startTime = nil;
304306
}
305307
}

CountlyHealthTracker.h

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
//
2+
// CountlyHealthTracker.h
3+
// CountlyTestApp-iOS
4+
//
5+
// Created by Arif Burak Demiray on 20.05.2025.
6+
// Copyright © 2025 Countly. All rights reserved.
7+
//
8+
#import <Foundation/Foundation.h>
9+
10+
@interface CountlyHealthTracker : NSObject
11+
12+
+ (instancetype)sharedInstance;
13+
14+
- (void)logWarning;
15+
16+
- (void)logError;
17+
18+
- (void)logFailedNetworkRequestWithStatusCode:(NSInteger)statusCode
19+
errorResponse:(NSString *)errorResponse;
20+
21+
- (void)logBackoffRequest;
22+
23+
- (void)clearAndSave;
24+
25+
- (void)saveState;
26+
27+
- (void)sendHealthCheck;
28+
29+
@end

CountlyHealthTracker.m

Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
//
2+
// CountlyHealthTracker.m
3+
// CountlyTestApp-iOS
4+
//
5+
// Created by Arif Burak Demiray on 20.05.2025.
6+
// Copyright © 2025 Countly. All rights reserved.
7+
//
8+
9+
#import <Foundation/Foundation.h>
10+
#import "CountlyHealthTracker.h"
11+
#import "CountlyCommon.h"
12+
13+
@interface CountlyHealthTracker ()
14+
15+
@property (nonatomic, assign) long countLogWarning;
16+
@property (nonatomic, assign) long countLogError;
17+
@property (nonatomic, assign) long countBackoffRequest;
18+
@property (nonatomic, assign) NSInteger statusCode;
19+
@property (nonatomic, strong) NSString *errorMessage;
20+
@property (nonatomic, assign) BOOL healthCheckEnabled;
21+
@property (nonatomic, assign) BOOL healthCheckSent;
22+
23+
@end
24+
25+
@implementation CountlyHealthTracker
26+
27+
NSString * const keyLogError = @"LErr";
28+
NSString * const keyLogWarning = @"LWar";
29+
NSString * const keyStatusCode = @"RStatC";
30+
NSString * const keyErrorMessage = @"REMsg";
31+
NSString * const keyBackoffRequest = @"BReq";
32+
33+
NSString * const requestKeyErrorCount = @"el";
34+
NSString * const requestKeyWarningCount = @"wl";
35+
NSString * const requestKeyStatusCode = @"sc";
36+
NSString * const requestKeyRequestError = @"em";
37+
NSString * const requestKeyBackoffRequest = @"br";
38+
39+
+ (instancetype)sharedInstance {
40+
static CountlyHealthTracker *instance = nil;
41+
static dispatch_once_t onceToken;
42+
dispatch_once(&onceToken, ^{
43+
instance = [[self alloc] init];
44+
});
45+
return instance;
46+
}
47+
48+
- (instancetype)init{
49+
self = [super init];
50+
if (self) {
51+
_errorMessage = @"";
52+
_statusCode = -1;
53+
_healthCheckSent = NO;
54+
_healthCheckEnabled = YES;
55+
56+
NSDictionary *initialState = [CountlyPersistency.sharedInstance retrieveHealthCheckTrackerState];
57+
[self setupInitialCounters:initialState];
58+
}
59+
return self;
60+
}
61+
62+
- (void)setupInitialCounters:(NSDictionary *)initialState {
63+
if (initialState == nil || [initialState count] == 0) {
64+
return;
65+
}
66+
67+
self.countLogWarning = [initialState[keyLogWarning] longValue];
68+
self.countLogError = [initialState[keyLogError] longValue];
69+
self.statusCode = [initialState[keyStatusCode] integerValue];
70+
self.errorMessage = initialState[keyErrorMessage] ?: @"";
71+
self.countBackoffRequest = [initialState[keyBackoffRequest] longValue];
72+
73+
CLY_LOG_D(@"%s, Loaded initial health check state: [%@]", __FUNCTION__, initialState);
74+
}
75+
76+
- (void)logWarning {
77+
self.countLogWarning++;
78+
}
79+
80+
- (void)logError {
81+
self.countLogError++;
82+
}
83+
84+
- (void)logFailedNetworkRequestWithStatusCode:(NSInteger)statusCode
85+
errorResponse:(NSString *)errorResponse {
86+
if (statusCode <= 0 || statusCode >= 1000 || errorResponse == nil) {
87+
return;
88+
}
89+
90+
self.statusCode = statusCode;
91+
if (errorResponse.length > 1000) {
92+
self.errorMessage = [errorResponse substringToIndex:1000];
93+
} else {
94+
self.errorMessage = errorResponse;
95+
}
96+
}
97+
98+
- (void)logBackoffRequest {
99+
self.countBackoffRequest++;
100+
}
101+
102+
- (void)clearAndSave {
103+
[self clearValues];
104+
[CountlyPersistency.sharedInstance storeHealthCheckTrackerState:@{}];
105+
}
106+
107+
- (void)saveState {
108+
NSDictionary *healthCheckState = @{
109+
keyLogWarning: @(self.countLogWarning),
110+
keyLogError: @(self.countLogError),
111+
keyStatusCode: @(self.statusCode),
112+
keyErrorMessage: self.errorMessage ?: @"",
113+
keyBackoffRequest: @(self.countBackoffRequest)
114+
};
115+
116+
[CountlyPersistency.sharedInstance storeHealthCheckTrackerState:healthCheckState];
117+
}
118+
119+
- (void)clearValues {
120+
CLY_LOG_W(@"%s, Clearing counters", __FUNCTION__);
121+
122+
self.countLogWarning = 0;
123+
self.countLogError = 0;
124+
self.statusCode = -1;
125+
self.errorMessage = @"";
126+
self.countBackoffRequest = 0;
127+
}
128+
129+
- (void)sendHealthCheck {
130+
if (CountlyDeviceInfo.sharedInstance.isDeviceIDTemporary) {
131+
CLY_LOG_W(@"%s, currently in temporary id mode, omitting", __FUNCTION__);
132+
}
133+
134+
if (!_healthCheckEnabled || _healthCheckSent) {
135+
CLY_LOG_D(@"%s, health check status, sent: %d, not_enabled: %d", __FUNCTION__, _healthCheckSent, _healthCheckEnabled);
136+
}
137+
138+
NSURLSessionTask* task = [CountlyCommon.sharedInstance.URLSession dataTaskWithRequest:[self healthCheckRequest] completionHandler:^(NSData* data, NSURLResponse* response, NSError* error)
139+
{
140+
if (error)
141+
{
142+
CLY_LOG_W(@"%s, error while sending health checks error: %@", __FUNCTION__, error);
143+
return;
144+
}
145+
146+
NSError *jsonError;
147+
NSDictionary *jsonResponse = [NSJSONSerialization JSONObjectWithData:data options:0 error:&jsonError];
148+
149+
if (jsonError || !jsonResponse) {
150+
CLY_LOG_I(@"%s, error while sending health checks, Failed to parse JSON response: %@", __FUNCTION__, jsonError);
151+
return;
152+
}
153+
154+
if (!jsonResponse[@"result"]) {
155+
CLY_LOG_D(@"%s, Retrieved request response does not match expected pattern %@", __FUNCTION__, jsonResponse);
156+
return;
157+
}
158+
159+
[self clearAndSave];
160+
self->_healthCheckSent = YES;
161+
}];
162+
163+
[task resume];
164+
}
165+
166+
- (NSString *)dictionaryToJsonString:(NSDictionary *)json {
167+
NSError *error;
168+
NSData *data = [NSJSONSerialization dataWithJSONObject:json options:0 error:&error];
169+
NSString *encodedData = @"";
170+
171+
if (!error && data) {
172+
NSString *jsonString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
173+
encodedData = [jsonString stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]];
174+
} else {
175+
CLY_LOG_W(@"%s, Failed to create json for hc request, %@", __FUNCTION__, error);
176+
}
177+
178+
return encodedData;
179+
}
180+
181+
- (NSURLRequest *)healthCheckRequest {
182+
NSString *queryString = [CountlyConnectionManager.sharedInstance queryEssentials];
183+
184+
queryString = [queryString stringByAppendingFormat:@"&%@=%@", @"hc", [self dictionaryToJsonString:@{
185+
requestKeyErrorCount: @(self.countLogError),
186+
requestKeyWarningCount: @(self.countLogWarning),
187+
requestKeyStatusCode: @(self.statusCode),
188+
requestKeyRequestError: self.errorMessage ?: @"",
189+
requestKeyBackoffRequest: @(self.countBackoffRequest)
190+
}]];
191+
192+
queryString = [queryString stringByAppendingFormat:@"&%@=%@", @"metrics", [self dictionaryToJsonString:@{
193+
kCountlyAppVersionKey: CountlyDeviceInfo.appVersion
194+
}]];
195+
196+
queryString = [CountlyConnectionManager.sharedInstance appendChecksum:queryString];
197+
NSString* hcSendURL = [CountlyConnectionManager.sharedInstance.host stringByAppendingFormat:@"%@",kCountlyEndpointI];
198+
199+
CLY_LOG_D(@"%s, generated health check request: %@", __FUNCTION__, queryString);
200+
201+
if (queryString.length > kCountlyGETRequestMaxLength || CountlyConnectionManager.sharedInstance.alwaysUsePOST)
202+
{
203+
NSMutableURLRequest* request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:hcSendURL]];
204+
request.HTTPMethod = @"POST";
205+
request.HTTPBody = [queryString cly_dataUTF8];
206+
return request.copy;
207+
}
208+
else
209+
{
210+
NSString* withQueryString = [hcSendURL stringByAppendingFormat:@"?%@", queryString];
211+
NSURLRequest* request = [NSURLRequest requestWithURL:[NSURL URLWithString:withQueryString]];
212+
return request;
213+
}
214+
}
215+
@end

CountlyPersistency.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,9 @@
6161
- (NSDictionary *)retrieveServerConfig;
6262
- (void)storeServerConfig:(NSDictionary *)serverConfig;
6363

64+
- (NSDictionary *)retrieveHealthCheckTrackerState;
65+
- (void)storeHealthCheckTrackerState:(NSDictionary *)healthCheckTrackerState;
66+
6467
-(BOOL)isOldRequest:(NSString*) queryString;
6568

6669
@property (nonatomic) NSUInteger eventSendThreshold;

0 commit comments

Comments
 (0)