Skip to content
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
84 changes: 49 additions & 35 deletions Countly.m

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions CountlyCommon.m
Original file line number Diff line number Diff line change
Expand Up @@ -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];
}

Expand All @@ -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];
}

Expand Down Expand Up @@ -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];
Expand Down
3 changes: 2 additions & 1 deletion CountlyConnectionManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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);
Expand Down
5 changes: 4 additions & 1 deletion CountlyCrashReporter.m
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
4 changes: 4 additions & 0 deletions CountlyHealthTracker.h
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@

- (void)sendHealthCheck;

- (void)recordUsage:(NSString *)area method:(NSString *)method;

- (void)recordLogCode:(NSString *)code;

// Exposed for testing purposes only.
- (void)resetInstance;

Expand Down
88 changes: 85 additions & 3 deletions CountlyHealthTracker.m
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,12 @@ @interface CountlyHealthTracker ()
@property (nonatomic, assign) BOOL healthCheckEnabled;
@property (nonatomic, assign) BOOL healthCheckSent;

@property (nonatomic, strong) NSMutableDictionary<NSString *, NSMutableDictionary<NSString *, NSNumber *> *> *methodUsage;
@property (nonatomic, strong) NSMutableDictionary<NSString *, NSNumber *> *logCodes;

@property (nonatomic, strong) dispatch_queue_t hcQueue;

- (NSDictionary *)deepCopyMethodUsage;
@end

@implementation CountlyHealthTracker
Expand All @@ -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;
Expand All @@ -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);

Expand All @@ -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);
}

Expand All @@ -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<NSString *, NSNumber *> *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) {
Expand Down Expand Up @@ -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, ^{
Expand All @@ -175,6 +243,8 @@ - (void)clearValues {
self.countBackoffRequest = 0;
self.consecutiveBackoffRequest = 0;
self.countConsecutiveBackoffRequest = 0;
[self.methodUsage removeAllObjects];
[self.logCodes removeAllObjects];
}

- (void)sendHealthCheck {
Expand Down Expand Up @@ -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;
Expand All @@ -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
Expand Down
2 changes: 2 additions & 0 deletions CountlyPersistency.m
Original file line number Diff line number Diff line change
Expand Up @@ -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];
}
Expand Down Expand Up @@ -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;
}

Expand Down
7 changes: 7 additions & 0 deletions CountlyTests/CountlyHealthTracker+Tests.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#import "CountlyHealthTracker.h"

@interface CountlyHealthTracker (Tests)
@property (nonatomic, strong) NSMutableDictionary *methodUsage;
@property (nonatomic, strong) NSMutableDictionary *logCodes;
- (NSURLRequest *)healthCheckRequest;
@end
Loading
Loading