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
6 changes: 6 additions & 0 deletions OptimizelySwiftSDK.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -2053,6 +2053,8 @@
98261A472EDDC35900F7230A /* OptimizelyClientTests_Cmab_Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98261A462EDDC35900F7230A /* OptimizelyClientTests_Cmab_Config.swift */; };
982C071F2D8C82AE0068B1FF /* HoldoutTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 982C071E2D8C82AE0068B1FF /* HoldoutTests.swift */; };
982C07202D8C82AE0068B1FF /* HoldoutTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 982C071E2D8C82AE0068B1FF /* HoldoutTests.swift */; };
983F81842F801E7500CDBC8D /* FeatureRolloutTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 983F81832F801E7500CDBC8D /* FeatureRolloutTests.swift */; };
983F81852F801E7500CDBC8D /* FeatureRolloutTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 983F81832F801E7500CDBC8D /* FeatureRolloutTests.swift */; };
9841590F2E13013E0042C01E /* OptimizelyUserContextTests_Decide_Async.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9841590E2E13013E0042C01E /* OptimizelyUserContextTests_Decide_Async.swift */; };
984159102E13013E0042C01E /* OptimizelyUserContextTests_Decide_Async.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9841590E2E13013E0042C01E /* OptimizelyUserContextTests_Decide_Async.swift */; };
984159122E141B640042C01E /* OptimizelyUserContextTests_Decide_CMAB.swift in Sources */ = {isa = PBXBuildFile; fileRef = 984159112E141B640042C01E /* OptimizelyUserContextTests_Decide_CMAB.swift */; };
Expand Down Expand Up @@ -2620,6 +2622,7 @@
98261A172ED89A8500F7230A /* CmabConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CmabConfig.swift; sourceTree = "<group>"; };
98261A462EDDC35900F7230A /* OptimizelyClientTests_Cmab_Config.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptimizelyClientTests_Cmab_Config.swift; sourceTree = "<group>"; };
982C071E2D8C82AE0068B1FF /* HoldoutTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HoldoutTests.swift; sourceTree = "<group>"; };
983F81832F801E7500CDBC8D /* FeatureRolloutTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeatureRolloutTests.swift; sourceTree = "<group>"; };
9841590E2E13013E0042C01E /* OptimizelyUserContextTests_Decide_Async.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptimizelyUserContextTests_Decide_Async.swift; sourceTree = "<group>"; };
984159112E141B640042C01E /* OptimizelyUserContextTests_Decide_CMAB.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptimizelyUserContextTests_Decide_CMAB.swift; sourceTree = "<group>"; };
984159362E16A7C50042C01E /* BucketTests_BucketToEntity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BucketTests_BucketToEntity.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -3218,6 +3221,7 @@
6E75199E22C5211100B2B157 /* FeatureVariableTests.swift */,
6E75199F22C5211100B2B157 /* AttributeTests.swift */,
6E7519A022C5211100B2B157 /* VariableTests.swift */,
983F81832F801E7500CDBC8D /* FeatureRolloutTests.swift */,
6E7519A122C5211100B2B157 /* FeatureFlagTests.swift */,
6E7519A222C5211100B2B157 /* AudienceTests.swift */,
84640880281320F000CCF97D /* IntegrationTests.swift */,
Expand Down Expand Up @@ -5340,6 +5344,7 @@
6E7518CE22C520D400B2B157 /* Audience.swift in Sources */,
6E75189222C520D400B2B157 /* Project.swift in Sources */,
6E7516F822C520D400B2B157 /* OptimizelyError.swift in Sources */,
983F81842F801E7500CDBC8D /* FeatureRolloutTests.swift in Sources */,
980A40742F112EFF00F25D38 /* RetryStrategy.swift in Sources */,
84B4D75E27E2A7550078CDA4 /* OptimizelySegmentOption.swift in Sources */,
848617E82863E21200B7F41B /* OdpSegmentApiManager.swift in Sources */,
Expand Down Expand Up @@ -5561,6 +5566,7 @@
6E75193522C520D500B2B157 /* OPTDataStore.swift in Sources */,
6EC6DD4824ABF89B0017D296 /* OptimizelyUserContext.swift in Sources */,
6E75182122C520D400B2B157 /* BatchEventBuilder.swift in Sources */,
983F81852F801E7500CDBC8D /* FeatureRolloutTests.swift in Sources */,
6E86CEA924FDC847005DAFED /* OptimizelyUserContext+ObjC.swift in Sources */,
6E9B118322C5488100C22D81 /* UserAttributeTests_Evaluate.swift in Sources */,
98F28A1D2E01940500A86546 /* Cmab.swift in Sources */,
Expand Down
39 changes: 35 additions & 4 deletions Sources/Data Model/Experiment.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,23 @@
import Foundation

struct Experiment: Codable, ExperimentCore {
/// Valid experiment type values from the datafile.
enum ExperimentType: String, Codable {
case ab = "ab"
case mab = "mab"
case cmab = "cmab"
case targetedDelivery = "td"
case featureRollout = "fr"
}

enum Status: String, Codable {
case running = "Running"
case launched = "Launched"
case paused = "Paused"
case notStarted = "Not started"
case archived = "Archived"
}

var id: String
var key: String
var status: Status
Expand All @@ -36,9 +45,26 @@ struct Experiment: Codable, ExperimentCore {
// datafile spec defines this as [String: Any]. Supposed to be [ExperimentKey: VariationKey]
var forcedVariations: [String: String]
var cmab: Cmab?

var type: ExperimentType?

enum CodingKeys: String, CodingKey {
case id, key, status, layerId, variations, trafficAllocation, audienceIds, audienceConditions, forcedVariations, cmab
case id, key, status, layerId, variations, trafficAllocation, audienceIds, audienceConditions, forcedVariations, cmab, type
}

init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decode(String.self, forKey: .id)
key = try container.decode(String.self, forKey: .key)
status = try container.decode(Status.self, forKey: .status)
layerId = try container.decode(String.self, forKey: .layerId)
variations = try container.decode([Variation].self, forKey: .variations)
trafficAllocation = try container.decode([TrafficAllocation].self, forKey: .trafficAllocation)
audienceIds = try container.decode([String].self, forKey: .audienceIds)
audienceConditions = try container.decodeIfPresent(ConditionHolder.self, forKey: .audienceConditions)
forcedVariations = try container.decode([String: String].self, forKey: .forcedVariations)
cmab = try container.decodeIfPresent(Cmab.self, forKey: .cmab)
// Gracefully handle unknown experiment types by dropping to nil
type = try? container.decodeIfPresent(ExperimentType.self, forKey: .type)
}

// MARK: - OptimizelyConfig
Expand All @@ -59,7 +85,8 @@ extension Experiment: Equatable {
lhs.audienceIds == rhs.audienceIds &&
lhs.audienceConditions == rhs.audienceConditions &&
lhs.forcedVariations == rhs.forcedVariations &&
lhs.cmab == rhs.cmab
lhs.cmab == rhs.cmab &&
lhs.type == rhs.type
}
}

Expand All @@ -74,4 +101,8 @@ extension Experiment {
var isCmab: Bool {
return cmab != nil
Comment thread
muzahidul-opti marked this conversation as resolved.
}

var isFeatureRollout: Bool {
return type == .featureRollout
}
}
59 changes: 53 additions & 6 deletions Sources/Data Model/ProjectConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,16 @@ class ProjectConfig {

self.allExperiments = project.experiments + project.groups.map { $0.experiments }.flatMap { $0 }

self.rolloutIdMap = {
var map = [String: Rollout]()
project.rollouts.forEach { map[$0.id] = $0 }
return map
}()

// Feature Rollout injection: for each feature flag, inject the "everyone else"
// variation into any experiment with type == .featureRollout
injectFeatureRolloutVariations()

holdoutConfig.allHoldouts = project.holdouts

self.experimentKeyMap = {
Expand Down Expand Up @@ -130,12 +140,6 @@ class ProjectConfig {
return project.featureFlags.map { $0.key }
}()

self.rolloutIdMap = {
var map = [String: Rollout]()
project.rollouts.forEach { map[$0.id] = $0 }
return map
}()

// all variations for each flag
// - datafile does not contain a separate entity for this.
// - we collect variations used in each rule (experiment rules and delivery rules)
Expand Down Expand Up @@ -179,6 +183,49 @@ class ProjectConfig {

}

// MARK: - Feature Rollout Injection

extension ProjectConfig {
/// Injects the "everyone else" variation from a flag's rollout into any
/// experiment with type == .featureRollout. After injection the existing
/// decision logic evaluates feature rollouts without modification.
func injectFeatureRolloutVariations() {
for flag in project.featureFlags {
guard let everyoneElseVariation = getEveryoneElseVariation(for: flag) else {
continue
}

for experimentId in flag.experimentIds {
guard let index = allExperiments.firstIndex(where: { $0.id == experimentId }) else {
continue
}

guard allExperiments[index].isFeatureRollout else {
continue
}

allExperiments[index].variations.append(everyoneElseVariation)
allExperiments[index].trafficAllocation.append(
TrafficAllocation(entityId: everyoneElseVariation.id, endOfRange: 10000)
)
}
}
}

/// Returns the first variation of the last experiment (the "everyone else"
/// rule) in the rollout associated with the given feature flag. Returns nil
/// if the rollout cannot be resolved or has no variations.
func getEveryoneElseVariation(for flag: FeatureFlag) -> Variation? {
guard !flag.rolloutId.isEmpty,
let rollout = rolloutIdMap[flag.rolloutId],
let everyoneElseRule = rollout.experiments.last,
let variation = everyoneElseRule.variations.first else {
return nil
}
return variation
}
}

// MARK: - Persistent Data

extension ProjectConfig {
Expand Down
Loading
Loading