Skip to content
Open
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@ xcuserdata/
.swiftpm/
Carthage/
.idea
Package.resolved
Experiment.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/
20 changes: 20 additions & 0 deletions Sources/Experiment/ExperimentClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand All @@ -45,6 +46,9 @@ internal class DefaultExperimentClient : NSObject, ExperimentClient {

internal let flags: LoadStoreCache<EvaluationFlag>
private let flagsStorageQueue = DispatchQueue(label: "com.amplitude.experiment.VariantsStorageQueue", attributes: .concurrent)

public let trackingOption: LoadStoreCache<String>
private let trackingOptionStorageQueue = DispatchQueue(label: "com.amplitude.experiment.TrackingOptionStorageQueue", attributes: .concurrent)

internal let config: ExperimentConfig
private let engine = EvaluationEngine()
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down
5 changes: 5 additions & 0 deletions Sources/Experiment/Storage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> {
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)
Expand Down
95 changes: 95 additions & 0 deletions Tests/ExperimentTests/ExperimentClientTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down