diff --git a/.gitignore b/.gitignore index f19ead5..30e6a9e 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,5 @@ xcuserdata/ .swiftpm/ Carthage/ .idea +Package.resolved +Experiment.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/ diff --git a/Sources/Experiment/ExperimentClient.swift b/Sources/Experiment/ExperimentClient.swift index 2ab8c92..fcdd752 100644 --- a/Sources/Experiment/ExperimentClient.swift +++ b/Sources/Experiment/ExperimentClient.swift @@ -19,6 +19,7 @@ import Foundation @objc func getUser() -> ExperimentUser? @objc func clear() @objc func stop() + @objc func setTrackAssignmentEvent(_ track: Bool) @available(*, deprecated, message: "User ExperimentConfig.userProvider instead") @objc func getUserProvider() -> ExperimentUserProvider? @@ -45,6 +46,9 @@ internal class DefaultExperimentClient : NSObject, ExperimentClient { internal let flags: LoadStoreCache private let flagsStorageQueue = DispatchQueue(label: "com.amplitude.experiment.VariantsStorageQueue", attributes: .concurrent) + + public let trackingOption: LoadStoreCache + private let trackingOptionStorageQueue = DispatchQueue(label: "com.amplitude.experiment.TrackingOptionStorageQueue", attributes: .concurrent) internal let config: ExperimentConfig private let engine = EvaluationEngine() @@ -97,6 +101,8 @@ internal class DefaultExperimentClient : NSObject, ExperimentClient { self.flags = getFlagStorage(apiKey: self.apiKey, instanceName: self.config.instanceName, storage: storage) self.flags.load() self.flags.mergeInitialFlagsWithStorage(config.initialFlags) + self.trackingOption = getTrackingOptionStorage(apiKey: self.apiKey, instanceName: self.config.instanceName, storage: storage) + self.trackingOption.load() } public func start(_ user: ExperimentUser? = nil, completion: ((Error?) -> Void)? = nil) -> Void { @@ -506,6 +512,12 @@ internal class DefaultExperimentClient : NSObject, ExperimentClient { let flagKeysB64EncodedUrl = base64EncodeData(jsonFlagKeys) request.setValue(flagKeysB64EncodedUrl, forHTTPHeaderField: "X-Amp-Exp-Flag-Keys") } + + // Add tracking option from stored setting + let trackingOptionValue = trackingOptionStorageQueue.sync { trackingOption.get(key: "default") } + if let trackingOptionValue = trackingOptionValue { + request.setValue(trackingOptionValue, forHTTPHeaderField: "X-Amp-Exp-Track") + } request.timeoutInterval = Double(timeoutMillis) / 1000.0 // Do fetch request @@ -690,6 +702,14 @@ internal class DefaultExperimentClient : NSObject, ExperimentClient { } return e.statusCode < 400 || e.statusCode >= 500 || e.statusCode == 429 } + + public func setTrackAssignmentEvent(_ track: Bool) { + let trackingOption = track ? "track" : "no-track" + trackingOptionStorageQueue.sync(flags: .barrier) { + self.trackingOption.put(key: "default", value: trackingOption) + self.trackingOption.store(async: false) + } + } } private struct VariantAndSource { diff --git a/Sources/Experiment/Storage.swift b/Sources/Experiment/Storage.swift index d89dbe5..2134ea7 100644 --- a/Sources/Experiment/Storage.swift +++ b/Sources/Experiment/Storage.swift @@ -17,6 +17,11 @@ internal func getFlagStorage(apiKey: String, instanceName: String, storage: Stor return LoadStoreCache(namespace: namespace, storage: storage) } +internal func getTrackingOptionStorage(apiKey: String, instanceName: String, storage: Storage) -> LoadStoreCache { + let namespace = "com.amplituide.experiment.trackingOption.\(instanceName).\(apiKey.suffix(6))" + return LoadStoreCache(namespace: namespace, storage: storage) +} + internal protocol Storage { func get(key: String) -> Data? func put(key: String, value: Data) diff --git a/Tests/ExperimentTests/ExperimentClientTests.swift b/Tests/ExperimentTests/ExperimentClientTests.swift index 7bbf783..efb09b7 100644 --- a/Tests/ExperimentTests/ExperimentClientTests.swift +++ b/Tests/ExperimentTests/ExperimentClientTests.swift @@ -1255,6 +1255,101 @@ class ExperimentClientTests: XCTestCase { let client = DefaultExperimentClient(apiKey: "", config: config, storage: InMemoryStorage()) XCTAssertEqual(900000, client.config.flagConfigPollingIntervalMillis) } + + func testSetTrackAssignmentEventSetsTrackingOptionAndFetchUsesCorrectOptions() { + let client = DefaultExperimentClient( + apiKey: API_KEY, + config: ExperimentConfigBuilder() + .debug(true) + .build(), + storage: InMemoryStorage() + ) + + // Set track assignment event to true + client.setTrackAssignmentEvent(true) + + // Verify the setting was stored + let storedOption = client.trackingOption.get(key: "default") + XCTAssertEqual(storedOption, "track") + + // Set track assignment event to false + client.setTrackAssignmentEvent(false) + + // Verify the setting was updated + let updatedOption = client.trackingOption.get(key: "default") + XCTAssertEqual(updatedOption, "no-track") + } + + func testSetTrackAssignmentEventPersistsSettingToStorage() { + let storage = InMemoryStorage() + + // Create first client and set track assignment event + let client1 = DefaultExperimentClient( + apiKey: API_KEY, + config: ExperimentConfigBuilder() + .debug(true) + .build(), + storage: storage + ) + client1.setTrackAssignmentEvent(true) + + // Create second client with same storage + let client2 = DefaultExperimentClient( + apiKey: API_KEY, + config: ExperimentConfigBuilder() + .debug(true) + .build(), + storage: storage + ) + + // Verify the setting was persisted and loaded by the second client + let storedOption = client2.trackingOption.get(key: "default") + XCTAssertEqual(storedOption, "track") + } + + func testMultipleCallsToSetTrackAssignmentEventUsesLatestSetting() { + let client = DefaultExperimentClient( + apiKey: API_KEY, + config: ExperimentConfigBuilder() + .debug(true) + .build(), + storage: InMemoryStorage() + ) + + // Set track assignment event to true, then false + client.setTrackAssignmentEvent(true) + client.setTrackAssignmentEvent(false) + + // Verify the latest setting is used + let storedOption = client.trackingOption.get(key: "default") + XCTAssertEqual(storedOption, "no-track") + } + + func testSetTrackAssignmentEventPreservesOtherExistingOptions() { + let client = DefaultExperimentClient( + apiKey: API_KEY, + config: ExperimentConfigBuilder() + .debug(true) + .build(), + storage: InMemoryStorage() + ) + + // Set track assignment event to true + client.setTrackAssignmentEvent(true) + + // Verify the tracking option is set + let storedOption = client.trackingOption.get(key: "default") + XCTAssertEqual(storedOption, "track") + + // Test that FetchOptions can still be created with flag keys + let fetchOptions = FetchOptions(["test-flag"]) + XCTAssertEqual(fetchOptions.flagKeys, ["test-flag"]) + + // Verify that both the tracking option and flag keys can coexist + // (The tracking option is stored separately from FetchOptions) + XCTAssertNotNil(storedOption) + XCTAssertNotNil(fetchOptions.flagKeys) + } } class TestAnalyticsProvider : ExperimentAnalyticsProvider {