Skip to content

Commit fa934c3

Browse files
authored
Merge pull request #427 from Countly/2548
fix: metrics consent
2 parents 5d87dfc + b92b4d1 commit fa934c3

File tree

8 files changed

+137
-34
lines changed

8 files changed

+137
-34
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
## 25.4.8
2+
* Mitigated an issue where "giveAllConsent" did not include metrics consent.
3+
14
## 25.4.7
25
* Added a new function "addCustomNetworkRequestHeaders: customHeaderValues" for providing or overriding custom headers after init.
36
* Updated user properties caching mechanism according to sessions.

Countly-PL.podspec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
Pod::Spec.new do |s|
22
s.name = 'Countly-PL'
3-
s.version = '25.4.7'
3+
s.version = '25.4.8'
44
s.license = { :type => 'MIT', :file => 'LICENSE' }
55
s.summary = 'Countly is an innovative, real-time, open source mobile analytics platform.'
66
s.homepage = 'https://github.com/Countly/countly-sdk-ios'

Countly.podspec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
Pod::Spec.new do |s|
22
s.name = 'Countly'
3-
s.version = '25.4.7'
3+
s.version = '25.4.8'
44
s.license = { :type => 'MIT', :file => 'LICENSE' }
55
s.summary = 'Countly is an innovative, real-time, open source mobile analytics platform.'
66
s.homepage = 'https://github.com/Countly/countly-sdk-ios'

Countly.xcodeproj/project.pbxproj

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@
9191
965A2E9D2DDDCDAC00F28F6A /* CountlyHealthTracker.h in Headers */ = {isa = PBXBuildFile; fileRef = 965A2E9A2DDDCDAC00F28F6A /* CountlyHealthTracker.h */; };
9292
9673567F2EC60CD400C742D8 /* TestURLProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9673567E2EC60CD400C742D8 /* TestURLProtocol.swift */; };
9393
968426812BF2302C007B303E /* CountlyConnectionManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 968426802BF2302C007B303E /* CountlyConnectionManagerTests.swift */; };
94+
969E5BCE2ECC4D3200AB406A /* CountlyConsentManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 969E5BCD2ECC4D2C00AB406A /* CountlyConsentManagerTests.swift */; };
9495
96C05AB02E82936F0028A976 /* CountlyHealthTrackerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96C05AAF2E8293630028A976 /* CountlyHealthTrackerTests.swift */; };
9596
96DA74BB2D9FB687006FA6FF /* MockFeedbackWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96DA74BA2D9FB687006FA6FF /* MockFeedbackWidget.swift */; };
9697
96E680422BFF89AC0091E105 /* CountlyCrashReporterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96E680412BFF89AC0091E105 /* CountlyCrashReporterTests.swift */; };
@@ -201,6 +202,7 @@
201202
96681A9B2D97D9B300A4845A /* CountlyTests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = CountlyTests.xctestplan; sourceTree = "<group>"; };
202203
9673567E2EC60CD400C742D8 /* TestURLProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestURLProtocol.swift; sourceTree = "<group>"; };
203204
968426802BF2302C007B303E /* CountlyConnectionManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountlyConnectionManagerTests.swift; sourceTree = "<group>"; };
205+
969E5BCD2ECC4D2C00AB406A /* CountlyConsentManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountlyConsentManagerTests.swift; sourceTree = "<group>"; };
204206
96C05AAF2E8293630028A976 /* CountlyHealthTrackerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountlyHealthTrackerTests.swift; sourceTree = "<group>"; };
205207
96DA74BA2D9FB687006FA6FF /* MockFeedbackWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockFeedbackWidget.swift; sourceTree = "<group>"; };
206208
96E680412BFF89AC0091E105 /* CountlyCrashReporterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountlyCrashReporterTests.swift; sourceTree = "<group>"; };
@@ -234,6 +236,7 @@
234236
1A5C4C952B35B0850032EE1F /* CountlyTests */ = {
235237
isa = PBXGroup;
236238
children = (
239+
969E5BCD2ECC4D2C00AB406A /* CountlyConsentManagerTests.swift */,
237240
9673567E2EC60CD400C742D8 /* TestURLProtocol.swift */,
238241
4C3A4C9F2EB4C40000827FEA /* EventThreadTests.swift */,
239242
96C05AAF2E8293630028A976 /* CountlyHealthTrackerTests.swift */,
@@ -505,6 +508,7 @@
505508
isa = PBXSourcesBuildPhase;
506509
buildActionMask = 2147483647;
507510
files = (
511+
969E5BCE2ECC4D3200AB406A /* CountlyConsentManagerTests.swift in Sources */,
508512
96329DE22D94299D00BFD641 /* ServerConfigBuilder.swift in Sources */,
509513
1A5C4C972B35B0850032EE1F /* CountlyTests.swift in Sources */,
510514
3969D0232CB80848000F8A32 /* CountlyViewTests.swift in Sources */,
@@ -780,7 +784,7 @@
780784
"@loader_path/Frameworks",
781785
);
782786
MACOSX_DEPLOYMENT_TARGET = 10.14;
783-
MARKETING_VERSION = 25.4.7;
787+
MARKETING_VERSION = 25.4.8;
784788
PRODUCT_BUNDLE_IDENTIFIER = ly.count.CountlyiOSSDK;
785789
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
786790
PROVISIONING_PROFILE_SPECIFIER = "";
@@ -812,7 +816,7 @@
812816
"@loader_path/Frameworks",
813817
);
814818
MACOSX_DEPLOYMENT_TARGET = 10.14;
815-
MARKETING_VERSION = 25.4.7;
819+
MARKETING_VERSION = 25.4.8;
816820
PRODUCT_BUNDLE_IDENTIFIER = ly.count.CountlyiOSSDK;
817821
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
818822
PROVISIONING_PROFILE_SPECIFIER = "";

CountlyCommon.m

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ @interface CountlyCommon ()
2929
#endif
3030
@end
3131

32-
NSString* const kCountlySDKVersion = @"25.4.7";
32+
NSString* const kCountlySDKVersion = @"25.4.8";
3333
NSString* const kCountlySDKName = @"objc-native-ios";
3434

3535
NSString* const kCountlyErrorDomain = @"ly.count.ErrorDomain";
@@ -69,7 +69,7 @@ - (void)resetInstance {
6969
#if (TARGET_OS_IOS || TARGET_OS_VISION )
7070
[NSNotificationCenter.defaultCenter removeObserver:self name:UIDeviceOrientationDidChangeNotification object:nil];
7171
#endif
72-
onceToken = 0;
72+
//onceToken = 0;
7373
s_sharedInstance = nil;
7474
_hasStarted = false;
7575
}

CountlyConsentManager.m

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,8 @@ - (NSArray *)allFeatures
237237
CLYConsentPerformanceMonitoring,
238238
CLYConsentFeedback,
239239
CLYConsentRemoteConfig,
240-
CLYConsentContent
240+
CLYConsentContent,
241+
CLYConsentMetrics,
241242
];
242243
}
243244

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
//
2+
// CountlyConsentManagerTests.swift
3+
// Countly
4+
//
5+
// Created by Arif Burak Demiray on 18.11.2025.
6+
// Copyright © 2025 Countly. All rights reserved.
7+
//
8+
import Foundation
9+
10+
import XCTest
11+
@testable import Countly
12+
13+
class CountlyConsentManagerTests: CountlyBaseTestCase {
14+
15+
override func setUp() {
16+
super.setUp()
17+
// Initialize or reset necessary objects here
18+
Countly.sharedInstance().halt(true)
19+
}
20+
21+
override func tearDown() {
22+
// Ensure everything is cleaned up properly
23+
super.tearDown()
24+
Countly.sharedInstance().halt(true)
25+
}
26+
/**
27+
* Tests that consent requirement is properly handled when enabled.
28+
* Verifies that:
29+
* 1. Initial consent request is sent as none given
30+
* 2. No data is collected until consent is given
31+
* 3. Location is properly handled with empty value
32+
* 4. After giveAllConsent call, consent request sent as all given
33+
*/
34+
func test_giveAllConsents() {
35+
let config = TestUtils.createBaseConfig()
36+
config.requiresConsent = true
37+
38+
Countly.sharedInstance().start(with: config)
39+
XCTAssertEqual(2, TestUtils.getCurrentRQ()?.count)
40+
Countly.sharedInstance().giveAllConsents()
41+
XCTAssertEqual(4, TestUtils.getCurrentRQ()?.count)
42+
var consents: [String: Any?] = [
43+
"push": 0,
44+
"content": 0,
45+
"crashes": 0,
46+
"events": 0,
47+
"users": 0,
48+
"feedback": 0,
49+
"apm": 0,
50+
"location": 0,
51+
"remote-config": 0,
52+
"sessions": 0,
53+
"attribution": 0,
54+
"views": 0,
55+
"metrics": 0
56+
]
57+
58+
TestUtils.validateRequest(["consent": consents], 0)
59+
TestUtils.validateRequest(["location": ""], 1)
60+
TestUtils.validateRequest(["begin_session": "1"], 2)
61+
for key in consents.keys {
62+
consents[key] = 1
63+
}
64+
TestUtils.validateRequest(["consent": consents], 3)
65+
66+
}
67+
68+
}
69+
70+
71+

CountlyTests/TestUtils.swift

Lines changed: 51 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ class TestUtils {
1313
static let commonDeviceId: String = "deviceId"
1414
static let commonAppKey: String = "appkey"
1515
static let host: String = "https://testing.count.ly/"
16-
static let SDK_VERSION = "25.1.1"
16+
static let SDK_VERSION = "25.4.8"
1717
static let SDK_NAME = "objc-native-ios"
1818

1919
static func cleanup() -> Void {
@@ -50,21 +50,38 @@ class TestUtils {
5050
}
5151

5252
static func validateRequest(_ params: [String: Any], _ idx: Int, _ customValidator: ([String: Any]) -> Void){
53+
guard let rq = getCurrentRQ() else {
54+
XCTFail("Request queue is nil.")
55+
return
56+
}
57+
guard rq.indices.contains(idx) else {
58+
XCTFail("Request index \(idx) out of bounds. RQ count: \(rq.count).")
59+
return
60+
}
61+
5362
let requestStr = getCurrentRQ()![idx]
5463
let request = parseQueryString(requestStr)
5564
validateRequiredParams(request)
5665

5766
for (key, value) in params {
5867
let reqValue = request[key]
68+
guard let reqValue else {
69+
XCTFail("Missing key '\(key)' in request. Expected value: \(value), Request:\(requestStr)")
70+
continue
71+
}
5972

60-
if let nestedMap = value as? [String: Any] {
61-
let nestedReqValue = reqValue as! [String: Any]
73+
if let nestedMap = value as? [String: Any] {
74+
guard let nestedReqValue = reqValue as? [String: Any] else {
75+
XCTFail("Key '\(key)' expected to be nested dictionary but got: \(reqValue)")
76+
continue
77+
}
78+
6279
for (nestedKey, nestedValue) in nestedMap {
6380
XCTAssertEqual("\(String(describing: nestedReqValue[nestedKey]))", "\(nestedValue)")
6481
}
6582
XCTAssertEqual(nestedMap.count, nestedReqValue.count)
6683
} else {
67-
XCTAssertEqual("\(String(describing: reqValue!))", "\(value)")
84+
XCTAssertEqual("\(String(describing: reqValue))", "\(value)")
6885
}
6986
}
7087

@@ -216,38 +233,45 @@ class TestUtils {
216233

217234
static func parseQueryString(_ queryString: String) -> [String: Any] {
218235
var result: [String: Any] = [:]
219-
220-
// Split the query string by '&' to get individual key-value pairs
236+
237+
// Split query string into pairs
221238
let pairs = queryString.split(separator: "&")
222239

223240
for pair in pairs {
224-
// Split each pair by '=' to separate the key and value
225-
let components = pair.split(separator: "=", maxSplits: 1)
241+
// Always split into 2 parts; missing value becomes empty string
242+
let components = pair.split(separator: "=", maxSplits: 1, omittingEmptySubsequences: false)
226243

227-
if components.count == 2 {
228-
let key = String(components[0])
229-
let value = String(components[1])
230-
231-
// If the value is a JSON string (starts and ends with '%7B' and '%7D' respectively after URL decoding), decode it
232-
if let decodedValue = value.removingPercentEncoding, decodedValue.hasPrefix("{"), decodedValue.hasSuffix("}") {
233-
if let jsonData = decodedValue.data(using: .utf8) {
234-
do {
235-
let jsonObject = try JSONSerialization.jsonObject(with: jsonData, options: [])
236-
result[key] = jsonObject
237-
continue
238-
} catch {
239-
print("Error decoding JSON for key \(key): \(error)")
240-
}
241-
}
244+
guard components.count == 2 else {
245+
continue
246+
}
247+
248+
let key = String(components[0])
249+
let value = String(components[1]) // <-- empty string stays empty string
250+
251+
let decodedKey = key.removingPercentEncoding ?? key
252+
let decodedValue = value.removingPercentEncoding ?? value
253+
254+
// JSON detection (only if non-empty)
255+
if !decodedValue.isEmpty,
256+
decodedValue.hasPrefix("{"),
257+
decodedValue.hasSuffix("}"),
258+
let jsonData = decodedValue.data(using: .utf8)
259+
{
260+
do {
261+
let jsonObject = try JSONSerialization.jsonObject(with: jsonData, options: [])
262+
result[decodedKey] = jsonObject
263+
continue
264+
} catch {
242265
}
243-
244-
// Otherwise, simply assign the value to the key in the result dictionary
245-
result[key] = value.removingPercentEncoding ?? value
246266
}
267+
268+
// Assign empty string value properly
269+
result[decodedKey] = decodedValue
247270
}
248-
271+
249272
return result
250273
}
274+
251275

252276
static func compareDictionaries(_ dict1: [String: Any],_ dict2: [String: Any]) -> Bool {
253277
guard dict1.count == dict2.count else {

0 commit comments

Comments
 (0)