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
23 changes: 19 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,10 @@ 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
}

// MARK: - OptimizelyConfig
Expand All @@ -59,7 +69,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 +85,8 @@ extension Experiment {
var isCmab: Bool {
return cmab != nil
}

var isFeatureRollout: Bool {
return type == .featureRollout
}
}
62 changes: 61 additions & 1 deletion Sources/Data Model/ProjectConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,24 @@ class ProjectConfig {
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()

// Rebuild experiment maps after injection so lookup maps contain injected variations
self.experimentKeyMap = {
var map = [String: Experiment]()
allExperiments.forEach { map[$0.key] = $0 }
return map
}()

self.experimentIdMap = {
var map = [String: Experiment]()
allExperiments.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 +196,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