Skip to content
Merged
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
67 changes: 67 additions & 0 deletions MixpanelDemo/MixpanelDemoTests/MixpanelFeatureFlagTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 ---
Expand Down Expand Up @@ -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() {
Expand Down
26 changes: 25 additions & 1 deletion Sources/FeatureFlags.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
}
}

Expand Down Expand Up @@ -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)
Expand Down