diff --git a/MixpanelDemo/MixpanelDemoTests/MixpanelFeatureFlagTests.swift b/MixpanelDemo/MixpanelDemoTests/MixpanelFeatureFlagTests.swift index 466ad182..0e142e29 100644 --- a/MixpanelDemo/MixpanelDemoTests/MixpanelFeatureFlagTests.swift +++ b/MixpanelDemo/MixpanelDemoTests/MixpanelFeatureFlagTests.swift @@ -882,6 +882,56 @@ class FeatureFlagManagerTests: XCTestCase { let missingFieldResult = parseResponse(missingFieldJSON) XCTAssertNotNil(missingFieldResult, "Parser should handle missing flags field") XCTAssertNil(missingFieldResult?.flags, "Flags should be nil when field is missing") + + let optionalExperimentPropertiesJSON = """ + { + "flags": { + "active_experiment_flag": { + "variant_key": "A", + "variant_value": "A", + "experiment_id": "447db52b-ec4a-4186-8d89-f9ba7bc7d7dd", + "is_experiment_active": true, + "is_qa_tester": false + }, + "experiment_flag_for_qa_user": { + "variant_key": "B", + "variant_value": "B", + "experiment_id": "447db52b-ec4a-4186-8d89-f9ba7bc7d7dd", + "is_experiment_active": false, + "is_qa_tester": true + }, + "flag_with_no_optionals": { + "variant_key": "C", + "variant_value": "C" + } + } + } + """.data(using: .utf8)! + + let experimentResult = parseResponse(optionalExperimentPropertiesJSON) + XCTAssertNotNil(experimentResult) + XCTAssertEqual(experimentResult?.flags?.count, 3) + + let activeFlag = experimentResult?.flags?["active_experiment_flag"] + XCTAssertEqual(activeFlag?.key, "A") + XCTAssertEqual(activeFlag?.value as? String, "A") + XCTAssertEqual(activeFlag?.experimentID, "447db52b-ec4a-4186-8d89-f9ba7bc7d7dd") + XCTAssertEqual(activeFlag?.isExperimentActive, true) + XCTAssertEqual(activeFlag?.isQATester, false) + + let qaFlag = experimentResult?.flags?["experiment_flag_for_qa_user"] + XCTAssertEqual(qaFlag?.key, "B") + XCTAssertEqual(qaFlag?.value as? String, "B") + XCTAssertEqual(qaFlag?.experimentID, "447db52b-ec4a-4186-8d89-f9ba7bc7d7dd") + XCTAssertEqual(qaFlag?.isExperimentActive, false) + XCTAssertEqual(qaFlag?.isQATester, true) + + let minimalFlag = experimentResult?.flags?["flag_with_no_optionals"] + XCTAssertEqual(minimalFlag?.key, "C") + XCTAssertEqual(minimalFlag?.value as? String, "C") + XCTAssertNil(minimalFlag?.experimentID) + XCTAssertNil(minimalFlag?.isExperimentActive) + XCTAssertNil(minimalFlag?.isQATester) } // --- Delegate Error Handling Tests --- @@ -1258,6 +1308,23 @@ class FeatureFlagManagerTests: XCTestCase { } } + func testTrackingIncludesOptionalProperties() { + // Set up flags with experiment properties + let flagsWithExperiment: [String: MixpanelFlagVariant] = [ + "experiment_flag": MixpanelFlagVariant(key: "variant_a", value: true, isExperimentActive: true, isQATester: false, experimentID: "exp_123") + ] + simulateFetchSuccess(flags: flagsWithExperiment) + + mockDelegate.trackExpectation = XCTestExpectation(description: "Track with experiment properties") + _ = manager.getVariantSync("experiment_flag", fallback: defaultFallback) + wait(for: [mockDelegate.trackExpectation!], timeout: 1.0) + + let props = mockDelegate.trackedEvents[0].properties! + XCTAssertEqual(props["$experiment_id"] as? String, "exp_123") + XCTAssertEqual(props["$is_experiment_active"] as? Bool, true) + XCTAssertEqual(props["$is_qa_tester"] as? Bool, false) + } + // MARK: - Timing Properties Sanity Tests func testTimingPropertiesSanity() { diff --git a/Sources/FeatureFlags.swift b/Sources/FeatureFlags.swift index 23d11022..ec91df94 100644 --- a/Sources/FeatureFlags.swift +++ b/Sources/FeatureFlags.swift @@ -33,10 +33,16 @@ struct AnyCodable: Decodable { public struct MixpanelFlagVariant: Decodable { public let key: String // Corresponds to 'variant_key' from API public let value: Any? // Corresponds to 'variant_value' from API + public let experimentID: String? // Corresponds to 'experiment_id' from API + public let isExperimentActive: Bool? // Corresponds to 'is_experiment_active' from API + public let isQATester: Bool? // Corresponds to 'is_qa_tester' from API enum CodingKeys: String, CodingKey { case key = "variant_key" case value = "variant_value" + case experimentID = "experiment_id" + case isExperimentActive = "is_experiment_active" + case isQATester = "is_qa_tester" } public init(from decoder: Decoder) throws { @@ -49,16 +55,24 @@ public struct MixpanelFlagVariant: Decodable { // If the value is an unsupported type, AnyCodable throws. let anyCodableValue = try container.decode(AnyCodable.self, forKey: .value) value = anyCodableValue.value // Extract the underlying Any? value + + // Decode optional fields for tracking + experimentID = try container.decodeIfPresent(String.self, forKey: .experimentID) + isExperimentActive = try container.decodeIfPresent(Bool.self, forKey: .isExperimentActive) + isQATester = try container.decodeIfPresent(Bool.self, forKey: .isQATester) } // Helper initializer with fallbacks, value defaults to key if nil - public init(key: String = "", value: Any? = nil) { + public init(key: String = "", value: Any? = nil, isExperimentActive: Bool? = nil, isQATester: Bool? = nil, experimentID: String? = nil) { self.key = key if let value = value { self.value = value } else { self.value = key } + self.experimentID = experimentID + self.isExperimentActive = isExperimentActive + self.isQATester = isQATester } } @@ -570,6 +584,16 @@ class FeatureFlagManager: Network, MixpanelFlags { properties["fetchLatencyMs"] = fetchLatencyMs } + if let experimentID = variant.experimentID { + properties["$experiment_id"] = experimentID + } + if let isExperimentActive = variant.isExperimentActive { + properties["$is_experiment_active"] = isExperimentActive + } + if let isQATester = variant.isQATester { + properties["$is_qa_tester"] = isQATester + } + // Dispatch delegate call asynchronously to main thread for safety DispatchQueue.main.async { delegate.track(event: "$experiment_started", properties: properties)