Skip to content

Commit 99223d3

Browse files
efahkKwame Efah
andauthored
Plumb additional flags properties through to exposure event (#678)
* Plumb additional flags properties through to exposure event * Add experiment ID decoding and tests * Update tracking prop keys --------- Co-authored-by: Kwame Efah <[email protected]>
1 parent 7bd2a56 commit 99223d3

File tree

2 files changed

+92
-1
lines changed

2 files changed

+92
-1
lines changed

MixpanelDemo/MixpanelDemoTests/MixpanelFeatureFlagTests.swift

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -882,6 +882,56 @@ class FeatureFlagManagerTests: XCTestCase {
882882
let missingFieldResult = parseResponse(missingFieldJSON)
883883
XCTAssertNotNil(missingFieldResult, "Parser should handle missing flags field")
884884
XCTAssertNil(missingFieldResult?.flags, "Flags should be nil when field is missing")
885+
886+
let optionalExperimentPropertiesJSON = """
887+
{
888+
"flags": {
889+
"active_experiment_flag": {
890+
"variant_key": "A",
891+
"variant_value": "A",
892+
"experiment_id": "447db52b-ec4a-4186-8d89-f9ba7bc7d7dd",
893+
"is_experiment_active": true,
894+
"is_qa_tester": false
895+
},
896+
"experiment_flag_for_qa_user": {
897+
"variant_key": "B",
898+
"variant_value": "B",
899+
"experiment_id": "447db52b-ec4a-4186-8d89-f9ba7bc7d7dd",
900+
"is_experiment_active": false,
901+
"is_qa_tester": true
902+
},
903+
"flag_with_no_optionals": {
904+
"variant_key": "C",
905+
"variant_value": "C"
906+
}
907+
}
908+
}
909+
""".data(using: .utf8)!
910+
911+
let experimentResult = parseResponse(optionalExperimentPropertiesJSON)
912+
XCTAssertNotNil(experimentResult)
913+
XCTAssertEqual(experimentResult?.flags?.count, 3)
914+
915+
let activeFlag = experimentResult?.flags?["active_experiment_flag"]
916+
XCTAssertEqual(activeFlag?.key, "A")
917+
XCTAssertEqual(activeFlag?.value as? String, "A")
918+
XCTAssertEqual(activeFlag?.experimentID, "447db52b-ec4a-4186-8d89-f9ba7bc7d7dd")
919+
XCTAssertEqual(activeFlag?.isExperimentActive, true)
920+
XCTAssertEqual(activeFlag?.isQATester, false)
921+
922+
let qaFlag = experimentResult?.flags?["experiment_flag_for_qa_user"]
923+
XCTAssertEqual(qaFlag?.key, "B")
924+
XCTAssertEqual(qaFlag?.value as? String, "B")
925+
XCTAssertEqual(qaFlag?.experimentID, "447db52b-ec4a-4186-8d89-f9ba7bc7d7dd")
926+
XCTAssertEqual(qaFlag?.isExperimentActive, false)
927+
XCTAssertEqual(qaFlag?.isQATester, true)
928+
929+
let minimalFlag = experimentResult?.flags?["flag_with_no_optionals"]
930+
XCTAssertEqual(minimalFlag?.key, "C")
931+
XCTAssertEqual(minimalFlag?.value as? String, "C")
932+
XCTAssertNil(minimalFlag?.experimentID)
933+
XCTAssertNil(minimalFlag?.isExperimentActive)
934+
XCTAssertNil(minimalFlag?.isQATester)
885935
}
886936

887937
// --- Delegate Error Handling Tests ---
@@ -1258,6 +1308,23 @@ class FeatureFlagManagerTests: XCTestCase {
12581308
}
12591309
}
12601310

1311+
func testTrackingIncludesOptionalProperties() {
1312+
// Set up flags with experiment properties
1313+
let flagsWithExperiment: [String: MixpanelFlagVariant] = [
1314+
"experiment_flag": MixpanelFlagVariant(key: "variant_a", value: true, isExperimentActive: true, isQATester: false, experimentID: "exp_123")
1315+
]
1316+
simulateFetchSuccess(flags: flagsWithExperiment)
1317+
1318+
mockDelegate.trackExpectation = XCTestExpectation(description: "Track with experiment properties")
1319+
_ = manager.getVariantSync("experiment_flag", fallback: defaultFallback)
1320+
wait(for: [mockDelegate.trackExpectation!], timeout: 1.0)
1321+
1322+
let props = mockDelegate.trackedEvents[0].properties!
1323+
XCTAssertEqual(props["$experiment_id"] as? String, "exp_123")
1324+
XCTAssertEqual(props["$is_experiment_active"] as? Bool, true)
1325+
XCTAssertEqual(props["$is_qa_tester"] as? Bool, false)
1326+
}
1327+
12611328
// MARK: - Timing Properties Sanity Tests
12621329

12631330
func testTimingPropertiesSanity() {

Sources/FeatureFlags.swift

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,16 @@ struct AnyCodable: Decodable {
3333
public struct MixpanelFlagVariant: Decodable {
3434
public let key: String // Corresponds to 'variant_key' from API
3535
public let value: Any? // Corresponds to 'variant_value' from API
36+
public let experimentID: String? // Corresponds to 'experiment_id' from API
37+
public let isExperimentActive: Bool? // Corresponds to 'is_experiment_active' from API
38+
public let isQATester: Bool? // Corresponds to 'is_qa_tester' from API
3639

3740
enum CodingKeys: String, CodingKey {
3841
case key = "variant_key"
3942
case value = "variant_value"
43+
case experimentID = "experiment_id"
44+
case isExperimentActive = "is_experiment_active"
45+
case isQATester = "is_qa_tester"
4046
}
4147

4248
public init(from decoder: Decoder) throws {
@@ -49,16 +55,24 @@ public struct MixpanelFlagVariant: Decodable {
4955
// If the value is an unsupported type, AnyCodable throws.
5056
let anyCodableValue = try container.decode(AnyCodable.self, forKey: .value)
5157
value = anyCodableValue.value // Extract the underlying Any? value
58+
59+
// Decode optional fields for tracking
60+
experimentID = try container.decodeIfPresent(String.self, forKey: .experimentID)
61+
isExperimentActive = try container.decodeIfPresent(Bool.self, forKey: .isExperimentActive)
62+
isQATester = try container.decodeIfPresent(Bool.self, forKey: .isQATester)
5263
}
5364

5465
// Helper initializer with fallbacks, value defaults to key if nil
55-
public init(key: String = "", value: Any? = nil) {
66+
public init(key: String = "", value: Any? = nil, isExperimentActive: Bool? = nil, isQATester: Bool? = nil, experimentID: String? = nil) {
5667
self.key = key
5768
if let value = value {
5869
self.value = value
5970
} else {
6071
self.value = key
6172
}
73+
self.experimentID = experimentID
74+
self.isExperimentActive = isExperimentActive
75+
self.isQATester = isQATester
6276
}
6377
}
6478

@@ -570,6 +584,16 @@ class FeatureFlagManager: Network, MixpanelFlags {
570584
properties["fetchLatencyMs"] = fetchLatencyMs
571585
}
572586

587+
if let experimentID = variant.experimentID {
588+
properties["$experiment_id"] = experimentID
589+
}
590+
if let isExperimentActive = variant.isExperimentActive {
591+
properties["$is_experiment_active"] = isExperimentActive
592+
}
593+
if let isQATester = variant.isQATester {
594+
properties["$is_qa_tester"] = isQATester
595+
}
596+
573597
// Dispatch delegate call asynchronously to main thread for safety
574598
DispatchQueue.main.async {
575599
delegate.track(event: "$experiment_started", properties: properties)

0 commit comments

Comments
 (0)