diff --git a/src/Optimizely/Config/DatafileProjectConfig.php b/src/Optimizely/Config/DatafileProjectConfig.php index 5f071b5b..9663a924 100644 --- a/src/Optimizely/Config/DatafileProjectConfig.php +++ b/src/Optimizely/Config/DatafileProjectConfig.php @@ -93,6 +93,16 @@ class DatafileProjectConfig implements ProjectConfigInterface */ private $datafile; + /** + * @var string environmentKey of the config. + */ + private $environmentKey; + + /** + * @var string sdkKey of the config. + */ + private $sdkKey; + /** * @var string Revision of the datafile. */ @@ -172,6 +182,34 @@ class DatafileProjectConfig implements ProjectConfigInterface */ private $_rollouts; + /** + * list of Attributes that will be parsed from the datafile + * + * @var [Attribute] + */ + private $attributes; + + /** + * list of Audiences that will be parsed from the datafile + * + * @var [Audience] + */ + private $audiences; + + /** + * list of Events that will be parsed from the datafile + * + * @var [Event] + */ + private $events; + + /** + * list of Typed Audiences that will be parsed from the datafile + * + * @var [Audience] + */ + private $typedAudiences; + /** * internal mapping of feature keys to feature flag models. * @@ -222,6 +260,8 @@ public function __construct($datafile, $logger, $errorHandler) $this->_logger = $logger; $this->_errorHandler = $errorHandler; $this->_version = $config['version']; + $this->environmentKey = isset($config['environmentKey']) ? $config['environmentKey'] : ''; + $this->sdkKey = isset($config['sdkKey']) ? $config['sdkKey'] : ''; if (!in_array($this->_version, $supportedVersions)) { throw new InvalidDatafileVersionException( "This version of the PHP SDK does not support the given datafile version: {$this->_version}." @@ -237,10 +277,10 @@ public function __construct($datafile, $logger, $errorHandler) $groups = $config['groups'] ?: []; $experiments = $config['experiments'] ?: []; - $events = $config['events'] ?: []; - $attributes = $config['attributes'] ?: []; - $audiences = $config['audiences'] ?: []; - $typedAudiences = isset($config['typedAudiences']) ? $config['typedAudiences']: []; + $this->attributes = isset($config['attributes']) ? $config['attributes'] : []; + $this->audiences = isset($config['audiences']) ? $config['audiences'] : []; + $this->events = isset($config['events']) ? $config['events'] : []; + $this->typedAudiences = isset($config['typedAudiences']) ? $config['typedAudiences'] : []; $rollouts = isset($config['rollouts']) ? $config['rollouts'] : []; $featureFlags = isset($config['featureFlags']) ? $config['featureFlags']: []; @@ -258,10 +298,10 @@ public function __construct($datafile, $logger, $errorHandler) $this->_groupIdMap = ConfigParser::generateMap($groups, 'id', Group::class); $this->_experimentIdMap = ConfigParser::generateMap($experiments, 'id', Experiment::class); - $this->_eventKeyMap = ConfigParser::generateMap($events, 'key', Event::class); - $this->_attributeKeyMap = ConfigParser::generateMap($attributes, 'key', Attribute::class); - $typedAudienceIdMap = ConfigParser::generateMap($typedAudiences, 'id', Audience::class); - $this->_audienceIdMap = ConfigParser::generateMap($audiences, 'id', Audience::class); + $this->_eventKeyMap = ConfigParser::generateMap($this->events, 'key', Event::class); + $this->_attributeKeyMap = ConfigParser::generateMap($this->attributes, 'key', Attribute::class); + $typedAudienceIdMap = ConfigParser::generateMap($this->typedAudiences, 'id', Audience::class); + $this->_audienceIdMap = ConfigParser::generateMap($this->audiences, 'id', Audience::class); $this->_rollouts = ConfigParser::generateMap($rollouts, null, Rollout::class); $this->_featureFlags = ConfigParser::generateMap($featureFlags, null, FeatureFlag::class); @@ -449,6 +489,22 @@ public function getRevision() return $this->_revision; } + /** + * @return string Config environmentKey. + */ + public function getEnvironmentKey() + { + return $this->environmentKey; + } + + /** + * @return string Config sdkKey. + */ + public function getSdkKey() + { + return $this->sdkKey; + } + /** * @return array List of feature flags parsed from the datafile */ @@ -457,6 +513,38 @@ public function getFeatureFlags() return $this->_featureFlags; } + /** + * @return array List of attributes parsed from the datafile + */ + public function getAttributes() + { + return $this->attributes; + } + + /** + * @return array List of audiences parsed from the datafile + */ + public function getAudiences() + { + return $this->audiences; + } + + /** + * @return array List of events parsed from the datafile + */ + public function getEvents() + { + return $this->events; + } + + /** + * @return array List of typed audiences parsed from the datafile + */ + public function getTypedAudiences() + { + return $this->typedAudiences; + } + /** * @return array List of all experiments (including group experiments) * parsed from the datafile @@ -470,7 +558,7 @@ public function getAllExperiments() $rolloutExperimentIds[] = $experiment->getId(); } } - return array_filter(array_values($this->_experimentKeyMap), function ($experiment) use ($rolloutExperimentIds) { + return array_filter(array_values($this->_experimentIdMap), function ($experiment) use ($rolloutExperimentIds) { return !in_array($experiment->getId(), $rolloutExperimentIds); }); } diff --git a/src/Optimizely/Config/ProjectConfigInterface.php b/src/Optimizely/Config/ProjectConfigInterface.php index 53a6d6c2..887e706c 100644 --- a/src/Optimizely/Config/ProjectConfigInterface.php +++ b/src/Optimizely/Config/ProjectConfigInterface.php @@ -51,6 +51,37 @@ public function getBotFiltering(); */ public function getRevision(); + /** + * @return string String representing environment key of the datafile. + */ + public function getEnvironmentKey(); + + /** + * @return string String representing sdkkey of the datafile. + */ + public function getSdkKey(); + + /** + * @return array List of attributes parsed from the datafile + */ + public function getAttributes(); + + /** + * @return array List of audiences parsed from the datafile + */ + public function getAudiences(); + + /** + * @return array List of events parsed from the datafile + */ + public function getEvents(); + + /** + * @return array List of typed audiences parsed from the datafile + */ + public function getTypedAudiences(); + + /** * @return array List of feature flags parsed from the datafile */ diff --git a/src/Optimizely/OptimizelyConfig/OptimizelyAttribute.php b/src/Optimizely/OptimizelyConfig/OptimizelyAttribute.php new file mode 100644 index 00000000..d3eee9b9 --- /dev/null +++ b/src/Optimizely/OptimizelyConfig/OptimizelyAttribute.php @@ -0,0 +1,60 @@ +id = $id; + $this->key = $key; + } + + /** + * @return string attribute id. + */ + public function getId() + { + return $this->id; + } + + /** + * @return string attribute key. + */ + public function getKey() + { + return $this->key; + } + + /** + * @return string JSON representation of the object. + */ + public function jsonSerialize() + { + return get_object_vars($this); + } +} diff --git a/src/Optimizely/OptimizelyConfig/OptimizelyAudience.php b/src/Optimizely/OptimizelyConfig/OptimizelyAudience.php new file mode 100644 index 00000000..e331871a --- /dev/null +++ b/src/Optimizely/OptimizelyConfig/OptimizelyAudience.php @@ -0,0 +1,75 @@ +id = $id; + $this->name = $name; + $this->conditions = $conditions; + } + + /** + * @return string audience id. + */ + public function getId() + { + return $this->id; + } + + /** + * @return string audience name. + */ + public function getName() + { + return $this->name; + } + + /** + * @return string audience conditions. + */ + public function getConditions() + { + return $this->conditions; + } + + /** + * @return string JSON representation of the object. + */ + public function jsonSerialize() + { + return get_object_vars($this); + } +} diff --git a/src/Optimizely/OptimizelyConfig/OptimizelyConfig.php b/src/Optimizely/OptimizelyConfig/OptimizelyConfig.php index fdd5736c..26093779 100644 --- a/src/Optimizely/OptimizelyConfig/OptimizelyConfig.php +++ b/src/Optimizely/OptimizelyConfig/OptimizelyConfig.php @@ -1,6 +1,6 @@ associative array */ @@ -37,20 +51,62 @@ class OptimizelyConfig implements \JsonSerializable */ private $featuresMap; + /** + * Array of attributes as OptimizelyAttribute. + * + * @var [OptimizelyAttribute] + */ + private $attributes; + + /** + * Array of audiences as OptimizelyAudience. + * + * @var [OptimizelyAudience] + */ + private $audiences; + + /** + * Array of events as OptimizelyEvent. + * + * @var [OptimizelyEvent] + */ + private $events; + /** * @var string Contents of datafile. */ private $datafile; - public function __construct($revision, array $experimentsMap, array $featuresMap, $datafile = null) + public function __construct($revision, array $experimentsMap, array $featuresMap, $datafile = null, $environmentKey = '', $sdkKey = '', $attributes = [], $audiences = [], $events = []) { + $this->environmentKey = $environmentKey; + $this->sdkKey = $sdkKey; $this->revision = $revision; $this->experimentsMap = $experimentsMap; $this->featuresMap = $featuresMap; + $this->attributes = $attributes; + $this->audiences = $audiences; + $this->events = $events; $this->datafile = $datafile; } + /** + * @return string Config environmentKey. + */ + public function getEnvironmentKey() + { + return $this->environmentKey; + } + + /** + * @return string Config sdkKey. + */ + public function getSdkKey() + { + return $this->sdkKey; + } + /** * @return string Config revision. */ @@ -83,6 +139,30 @@ public function getFeaturesMap() return $this->featuresMap; } + /** + * @return array Attributes as OptimizelyAttribute. + */ + public function getAttributes() + { + return $this->attributes; + } + + /** + * @return array Audiences as OptimizelyAudience. + */ + public function getAudiences() + { + return $this->audiences; + } + + /** + * @return array Events as OptimizelyEvent. + */ + public function getEvents() + { + return $this->events; + } + /** * @return string JSON representation of the object. */ diff --git a/src/Optimizely/OptimizelyConfig/OptimizelyConfigService.php b/src/Optimizely/OptimizelyConfig/OptimizelyConfigService.php index eef1829a..d2dadb7c 100644 --- a/src/Optimizely/OptimizelyConfig/OptimizelyConfigService.php +++ b/src/Optimizely/OptimizelyConfig/OptimizelyConfigService.php @@ -1,6 +1,6 @@ featureFlags = $projectConfig->getFeatureFlags(); $this->revision = $projectConfig->getRevision(); $this->datafile = $projectConfig->toDatafile(); + $this->environmentKey = $projectConfig->getEnvironmentKey(); + $this->sdkKey = $projectConfig->getSdkKey(); + $this->projectConfig = $projectConfig; $this->createLookupMaps(); } @@ -80,15 +93,107 @@ public function getConfig() { $experimentsMaps = $this->getExperimentsMaps(); $featuresMap = $this->getFeaturesMap($experimentsMaps[1]); + $attributes = $this->getConfigAttributes(); + $audiences = $this->getConfigAudiences(); + $events = $this->getConfigEvents(); return new OptimizelyConfig( $this->revision, + # This experimentsMap is for experiments of legacy projects only. + # For flag projects, experiment keys are not guaranteed to be unique + # across multiple flags, so this map may not include all experiments + # when keys conflict. Use experimentRules and deliveryRules instead. $experimentsMaps[0], $featuresMap, - $this->datafile + $this->datafile, + $this->environmentKey, + $this->sdkKey, + $attributes, + $audiences, + $events ); } - + + /** + * Generates array of attributes as OptimizelyAttribute. + * + * @return array of OptimizelyAttributes. + */ + protected function getConfigAttributes() + { + $attributeArray = []; + $attributes = $this->projectConfig->getAttributes(); + foreach ($attributes as $attr) { + $optlyAttr = new OptimizelyAttribute( + $attr['id'], + $attr['key'] + ); + array_push($attributeArray, $optlyAttr); + } + return $attributeArray; + } + + + /** + * Generates array of events as OptimizelyEvents. + * + * @return array of OptimizelyEvents. + */ + protected function getConfigEvents() + { + $eventsArray = []; + $events = $this->projectConfig->getEvents(); + foreach ($events as $event) { + $optlyEvent = new OptimizelyEvent( + $event['id'], + $event['key'], + $event['experimentIds'] + ); + $eventsArray[] = $optlyEvent; + } + return $eventsArray; + } + + /** + * Generates array of audiences giving typed audiences high priority as OptimizelyAudience. + * + * @return array of OptimizelyEvents. + */ + protected function getConfigAudiences() + { + $allAudiences = []; + $typedAudienceIds = []; + $audiences = $this->projectConfig->getAudiences(); + $typedAudiences = $this->projectConfig->getTypedAudiences(); + $audiencesArray = $typedAudiences; + foreach ($audiencesArray as $key => $typedAudience) { + $typedAudienceIds[] = $typedAudience['id']; + $id = $typedAudience['id']; + if ($id != '$opt_dummy_audience') { + $optlyAudience = new OptimizelyAudience( + $id, + $typedAudience['name'], + json_encode($typedAudience['conditions']) + ); + array_push($allAudiences, $optlyAudience); + } + } + foreach ($audiences as $naudience) { + if (!in_array($naudience['id'], $typedAudienceIds, true)) { + $id = $naudience['id']; + if ($id != '$opt_dummy_audience') { + $optlyAudience = new OptimizelyAudience( + $id, + $naudience['name'], + $naudience['conditions'] + ); + array_push($allAudiences, $optlyAudience); + } + } + } + return $allAudiences; + } + /** * Generates lookup maps to avoid redundant iteration while creating OptimizelyConfig. */ @@ -103,7 +208,11 @@ protected function createLookupMaps() foreach ($feature->getExperimentIds() as $expId) { $this->experimentIdFeatureMap[$expId] = $feature; } - + $rolloutID = $feature->getRolloutId(); + $rollout = $this->projectConfig->getRolloutFromId($rolloutID); + foreach ($rollout->getExperiments() as $exp) { + $this->experimentIdFeatureMap[$exp->getId()] = $feature; + } # Populate featKeyOptlyVariableKeyVariableMap and featKeyOptlyVariableIdVariableMap $variablesKeyMap = []; $variablesIdMap = []; @@ -207,6 +316,75 @@ protected function getVariationsMap(Experiment $experiment) return $variationsMap; } + /** + * Converts array of audience conditions to serialized audiences. + * + * for examples: + * 1. Input: ["or", "1", "2"] + * Output: "us" OR "female" + * 2. Input: ["not", "1"] + * Output: "NOT "us" + * 3. Input: ["or", "1"] + * Output: "us" + * 4. Input: ["and", ["or", "1", ["and", "2", "3"]], ["and", "11", ["or", "12", "13"]]] + * Output: "("us" OR ("female" AND "adult")) AND ("fr" AND ("male" OR "kid"))" + * + * @param array audience conditions . + * + * @return string of experiment audience conditions. + */ + protected function getSerializedAudiences(array $audienceConditions) + { + $operators = ['and', 'or', 'not']; + $finalAudiences = ''; + if ($audienceConditions == null) { + return $finalAudiences; + } + $cond = ''; + foreach ($audienceConditions as $var) { + $subAudience = ''; + // Checks if item is list of conditions means if it is sub audience + if (is_array($var)) { + $subAudience = '(' . $this->getSerializedAudiences($var) . ')'; + } elseif (in_array($var, $operators, true)) { + $cond = strtoupper(strval($var)); + } else { + // Checks if item is audience id + $itemStr = strval($var); + $audience = $this->projectConfig->getAudience($itemStr); + $name = $audience == null ? $itemStr : $audience->getName(); + // if audience condition is "NOT" then add "NOT" at start. Otherwise check if there is already audience id in finalAudiences then append condition between finalAudiences and item + if ($finalAudiences !== '' || $cond == "NOT") { + if ($finalAudiences !== '') { + $finalAudiences .= ' '; + } + if ($cond == '') { + $cond = 'OR'; + } + $finalAudiences .= $cond . ' ' . '"' . $name . '"'; + } else { + $finalAudiences = '"' . $name . '"'; + } + } + // Checks if sub audience is empty or not + if ($subAudience !== '') { + if ($finalAudiences !== '' || $cond == "NOT") { + if ($finalAudiences !== '') { + $finalAudiences .= ' '; + } + if ($cond == '') { + $cond = 'OR'; + } + $finalAudiences = $finalAudiences . $cond . ' ' . $subAudience; + } else { + $finalAudiences = $finalAudiences . $subAudience; + } + } + } + return $finalAudiences; + } + + /** * Generates OptimizelyExperiment Key and ID Maps. * Returns an array with @@ -223,11 +401,16 @@ protected function getExperimentsMaps() foreach ($this->experiments as $exp) { $expId = $exp->getId(); $expKey = $exp->getKey(); - + $audiences = ''; + if ($exp->getAudienceConditions() != null) { + $audienceConditions = $exp->getAudienceConditions(); + $audiences = $this->getSerializedAudiences($audienceConditions); + } $optExp = new OptimizelyExperiment( $expId, $expKey, - $this->getVariationsMap($exp) + $this->getVariationsMap($exp), + $audiences ); $experimentsKeyMap[$expKey] = $optExp; @@ -237,6 +420,39 @@ protected function getExperimentsMaps() return [$experimentsKeyMap, $experimentsIdMap]; } + /** + * Generates array of delivery rules for optimizelyFeature. + * + * @param string feature rollout id. + * + * @return array of optimizelyExperiments as delivery rules. + */ + protected function getDeliveryRules($rollout_id) + { + $deliveryRules = []; + $rollout = $this->projectConfig->getRolloutFromId($rollout_id); + $experiments = $rollout->getExperiments(); + foreach ($experiments as $exp) { + $expId = $exp->getId(); + $expKey = $exp->getKey(); + $audiences = ''; + if ($exp->getAudienceConditions() != null) { + $audienceConditions = $exp->getAudienceConditions(); + $audiences = $this->getSerializedAudiences($audienceConditions); + } + $optExp = new OptimizelyExperiment( + $expId, + $expKey, + $this->getVariationsMap($exp), + $audiences + ); + array_push($deliveryRules, $optExp); + } + + return $deliveryRules; + } + + /** * Generates Features map for the project config. * @@ -251,10 +467,15 @@ protected function getFeaturesMap(array $experimentsIdMap) foreach ($this->featureFlags as $feature) { $featureKey = $feature->getKey(); $experimentsMap = []; - + $experimentRules = []; + $deliveryRules = []; + if ($feature->getRolloutId() != null) { + $deliveryRules = $this->getDeliveryRules($feature->getRolloutId()); + } foreach ($feature->getExperimentIds() as $expId) { $optExp = $experimentsIdMap[$expId]; $experimentsMap[$optExp->getKey()] = $optExp; + array_push($experimentRules, $optExp); } $variablesMap = $this->featKeyOptlyVariableKeyVariableMap[$featureKey]; @@ -262,8 +483,11 @@ protected function getFeaturesMap(array $experimentsIdMap) $optFeature = new OptimizelyFeature( $feature->getId(), $featureKey, + # This experimentsMap is deprecated. Use experimentRules and deliveryRules instead. $experimentsMap, - $variablesMap + $variablesMap, + $experimentRules, + $deliveryRules ); $featuresMap[$featureKey] = $optFeature; diff --git a/src/Optimizely/OptimizelyConfig/OptimizelyEvent.php b/src/Optimizely/OptimizelyConfig/OptimizelyEvent.php new file mode 100644 index 00000000..8a3a90de --- /dev/null +++ b/src/Optimizely/OptimizelyConfig/OptimizelyEvent.php @@ -0,0 +1,75 @@ +id = $id; + $this->key = $key; + $this->experimentIds = $experimentIds; + } + + /** + * @return string event ID. + */ + public function getId() + { + return $this->id; + } + + /** + * @return string event Key. + */ + public function getKey() + { + return $this->key; + } + + /** + * @return array experimentIds representing event experiment ids. + */ + public function getExperimentIds() + { + return $this->experimentIds; + } + + /** + * @return string JSON representation of the object. + */ + public function jsonSerialize() + { + return get_object_vars($this); + } +} diff --git a/src/Optimizely/OptimizelyConfig/OptimizelyExperiment.php b/src/Optimizely/OptimizelyConfig/OptimizelyExperiment.php index ed7b32b5..71bd9959 100644 --- a/src/Optimizely/OptimizelyConfig/OptimizelyExperiment.php +++ b/src/Optimizely/OptimizelyConfig/OptimizelyExperiment.php @@ -1,6 +1,6 @@ id = $id; $this->key = $key; + $this->audiences = $audiences; $this->variationsMap = $variationsMap; } @@ -58,6 +64,14 @@ public function getKey() return $this->key; } + /** + * @return string Experiment audiences. + */ + public function getExperimentAudiences() + { + return $this->audiences; + } + /** * @return array Map of Variation Keys to OptimizelyVariations. */ diff --git a/src/Optimizely/OptimizelyConfig/OptimizelyFeature.php b/src/Optimizely/OptimizelyConfig/OptimizelyFeature.php index 63f1afec..7b605064 100644 --- a/src/Optimizely/OptimizelyConfig/OptimizelyFeature.php +++ b/src/Optimizely/OptimizelyConfig/OptimizelyFeature.php @@ -1,6 +1,6 @@ id = $id; $this->key = $key; + $this->experimentRules = $experimentRules; + $this->deliveryRules = $deliveryRules; $this->experimentsMap = $experimentsMap; $this->variablesMap = $variablesMap; } @@ -66,11 +82,28 @@ public function getKey() return $this->key; } + /** + * @return array array of feature Experiments as OptimizelyExperiments. + */ + public function getExperimentRules() + { + return $this->experimentRules; + } + + /** + * @return array array of Rollout Experiments of feature as OptimizelyExperiments. + */ + public function getDeliveryRules() + { + return $this->deliveryRules; + } + /** * @return array Map of Experiment Keys to OptimizelyExperiments. */ public function getExperimentsMap() { + # This experimentsMap is deprecated. Use experimentRules and deliveryRules instead. return $this->experimentsMap; } diff --git a/tests/OptimizelyConfigTests/OptimizelyConfigServiceTest.php b/tests/OptimizelyConfigTests/OptimizelyConfigServiceTest.php index 0a1d5ad8..5bbaaf23 100644 --- a/tests/OptimizelyConfigTests/OptimizelyConfigServiceTest.php +++ b/tests/OptimizelyConfigTests/OptimizelyConfigServiceTest.php @@ -1,6 +1,6 @@ featExpVariationMap); - - + new OptimizelyExperiment("17279300791", "feat_experiment", $this->featExpVariationMap, ''); + + // creating optimizely Experiment for delivery rules + $boolDeliveryFeatVariable = new OptimizelyVariable('17252790456', 'boolean_var', 'boolean', 'false'); + $intDeliveryFeatVariable = new OptimizelyVariable('17258820367', 'integer_var', 'integer', 1); + $doubleDeliveryFeatVariable = new OptimizelyVariable('17260550714', 'double_var', 'double', 0.5); + $strDeliveryFeatVariable = new OptimizelyVariable('17290540010', 'string_var', 'string', 'i am default value'); + $jsonDeliveryFeatVariable = new OptimizelyVariable('17260550458', 'json_var', 'json', "{\"text\": \"default value\"}"); + + $this->deliveryDefaultVariableKeyMap = []; + $this->deliveryDefaultVariableKeyMap['boolean_var'] = $boolDeliveryFeatVariable; + $this->deliveryDefaultVariableKeyMap['integer_var'] = $intDeliveryFeatVariable; + $this->deliveryDefaultVariableKeyMap['double_var'] = $doubleDeliveryFeatVariable; + $this->deliveryDefaultVariableKeyMap['string_var'] = $strDeliveryFeatVariable; + $this->deliveryDefaultVariableKeyMap['json_var'] = $jsonDeliveryFeatVariable; + $this->deliveryExpVariationMap = []; + $this->deliveryExpVariationMap['17285550838'] = + new OptimizelyVariation('17285550838', '17285550838', true, $this->deliveryDefaultVariableKeyMap); + + $del_Experiment = + new OptimizelyExperiment("17268110732", "17268110732", $this->deliveryExpVariationMap, ''); // create feature $experimentsMap = ['feat_experiment' => $featExperiment]; + $experiment_rules = [$featExperiment]; + $deliver_rules = [$del_Experiment]; $this->feature = new OptimizelyFeature( '17266500726', 'test_feature', $experimentsMap, - $this->expectedDefaultVariableKeyMap + $this->expectedDefaultVariableKeyMap, + $experiment_rules, + $deliver_rules ); // create ab experiment and variations @@ -100,7 +125,7 @@ public function setUp() $variationsMap['variation_a'] = $variationA; $variationsMap['variation_b'] = $variationB; - $abExperiment = new OptimizelyExperiment('17301270474', 'ab_experiment', $variationsMap); + $abExperiment = new OptimizelyExperiment('17301270474', 'ab_experiment', $variationsMap, ''); // create group_ab_experiment and variations $variationA = new OptimizelyVariation('17287500312', 'variation_a', null, []); @@ -110,7 +135,7 @@ public function setUp() $variationsMap['variation_b'] = $variationB; $groupExperiment = - new OptimizelyExperiment('17258450439', 'group_ab_experiment', $variationsMap); + new OptimizelyExperiment('17258450439', 'group_ab_experiment', $variationsMap, ''); // create experiment key map $this->expectedExpKeyMap = []; @@ -172,10 +197,58 @@ public function testGetVariationsMap() $getVariationsMap = self::getMethod("getVariationsMap"); $response = $getVariationsMap->invokeArgs($this->optConfigService, array($featExp)); - $this->assertEquals($this->featExpVariationMap, $response); } + public function testGetOptimizelyConfigWithDuplicateExperimentKeys() + { + $this->datafile = DATAFILE_FOR_DUPLICATE_EXP_KEYS; + $this->projectConfig = new DatafileProjectConfig( + $this->datafile, + new NoOpLogger(), + new NoOpErrorHandler() + ); + $this->optConfigService = new OptimizelyConfigService($this->projectConfig); + $optimizelyConfig = $this->optConfigService->getConfig(); + $this->assertEquals(Count($optimizelyConfig->getExperimentsMap()), 1); + $experimentRulesFlag1 = $optimizelyConfig->getFeaturesMap()['flag1']->getExperimentRules(); // 9300000007569 + $experimentRulesFlag2 = $optimizelyConfig->getFeaturesMap()['flag2']->getExperimentRules(); // 9300000007573 + foreach ($experimentRulesFlag1 as $experimentRule) { + if ($experimentRule->getKey() == 'targeted_delivery') { + $this->assertEquals($experimentRule->getId(), '9300000007569'); + } + } + + foreach ($experimentRulesFlag2 as $experimentRule) { + if ($experimentRule->getKey() == 'targeted_delivery') { + $this->assertEquals($experimentRule->getId(), '9300000007573'); + } + } + } + + public function testGetOptimizelyConfigWithDuplicateRuleKeys() + { + $this->datafile = DATAFILE_FOR_DUPLICATE_RUL_KEYS; + $this->projectConfig = new DatafileProjectConfig( + $this->datafile, + new NoOpLogger(), + new NoOpErrorHandler() + ); + $this->optConfigService = new OptimizelyConfigService($this->projectConfig); + + $optimizelyConfig = $this->optConfigService->getConfig(); + $this->assertEquals(Count($optimizelyConfig->getExperimentsMap()), 0); + $rolloutFlag1 = $optimizelyConfig->getFeaturesMap()["flag_1"]->getDeliveryRules()[0]; // 9300000004977 + $rolloutFlag2 = $optimizelyConfig->getFeaturesMap()["flag_2"]->getDeliveryRules()[0]; // 9300000004979 + $rolloutFlag3 = $optimizelyConfig->getFeaturesMap()["flag_3"]->getDeliveryRules()[0]; // 9300000004981 + $this->assertEquals($rolloutFlag1->getId(), "9300000004977"); + $this->assertEquals($rolloutFlag1->getKey(), "targeted_delivery"); + $this->assertEquals($rolloutFlag2->getId(), "9300000004979"); + $this->assertEquals($rolloutFlag2->getKey(), "targeted_delivery"); + $this->assertEquals($rolloutFlag3->getId(), "9300000004981"); + $this->assertEquals($rolloutFlag3->getKey(), "targeted_delivery"); + } + public function testGetExperimentsMaps() { $getExperimentsMap = self::getMethod("getExperimentsMaps"); @@ -197,6 +270,114 @@ public function testGetFeaturesMap() ); $this->assertEquals(['test_feature' => $this->feature], $response); + foreach ($response as $feature) { + $this->assertInstanceof(OptimizelyFeature::class, $feature); + $experiment_rules = $feature->getExperimentRules(); + $deliver_rules = $feature->getDeliveryRules(); + if (!empty($experiment_rules)) { + foreach ($experiment_rules as $exp) { + $this->assertInstanceof(OptimizelyExperiment::class, $exp); + } + } + if (!empty($deliver_rules)) { + foreach ($deliver_rules as $del) { + $this->assertInstanceof(OptimizelyExperiment::class, $del); + } + } + } + } + + public function testgetExperimentAudiences() + { + $audienceConditions = [ + array("or", "3468206642"), + array('or', '3468206642', '3988293899'), + array('or', '3468206642', '3988293899', '3988293898'), + array("not", "3468206642"), + array('and', '3468206642', '3988293899'), + array("and", "3468206642"), + array('3468206642', '3988293899'), + array("3468206642"), + array('and', array('or', '3468206642', '3988293899'), '3988293898'), + array('and', 'and'), + array(), + array('not', array('and', '3468206642', '3988293899')), + array( + "and", array("or", "3468206642", array( + 'and', '3468206642', '3988293899' + )), + array( + "and", "3468206642", + array('or', '3468206642', '3988293899') + ) + ), + array('or', '1', '100000') + ]; + + $expectedAudienceOutputs = [ + '"' . "exactString" . '"', + '"' . "exactString" . '"' . ' ' . 'OR' . ' ' . '"' . '$$dummyExists' . '"', + '"' . "exactString" . '"' . ' ' . 'OR' . ' ' . '"' . '$$dummyExists' . '"' . ' ' . 'OR' . ' ' . '"' . '$$dummySubstringString' . '"', + 'NOT' . ' ' . '"' . "exactString" . '"', + '"' . "exactString" . '"' . ' ' . 'AND' . ' ' . '"' . '$$dummyExists' . '"', + '"' . "exactString" . '"', + '"' . "exactString" . '"' . ' ' . 'OR' . ' ' . '"' . '$$dummyExists' . '"', + '"' . "exactString" . '"', + '(' . '"' . 'exactString' . '"' . ' ' . 'OR' . ' ' . '"' . '$$dummyExists' . '"' . ')' + . ' ' . 'AND' . ' ' . '"' . '$$dummySubstringString' . '"', + '', + '', + 'NOT' . ' ' . '(' . '"' . "exactString" . '"' . ' ' . 'AND' . ' ' . '"' . '$$dummyExists' . '"' . ')', + + '(' . '"' . "exactString" . '"' . ' ' . 'OR' . ' ' . + '(' . '"' . "exactString" . '"' . ' ' . 'AND' . ' ' . '"' . '$$dummyExists' . '"' . ')' . ')' + . ' ' . 'AND' . ' ' . '(' . '"' . "exactString" . '"' . ' ' . 'AND' . ' ' . '(' . + '"' . "exactString" . '"' . ' ' . 'OR' . ' ' . '"' . '$$dummyExists' . '"' . ')' . ')', + '"'. '1'. '"' . ' '. 'OR' . ' ' . '"' . '100000' . '"' + ]; + + for ($testNo = 0; $testNo < count($audienceConditions); $testNo++) { + $getExperimentAudiences = self::getMethod("getSerializedAudiences"); + $response = $getExperimentAudiences->invokeArgs( + $this->optConfigService, + array($audienceConditions[$testNo]) + ); + $this->assertEquals($expectedAudienceOutputs[$testNo], $response); + } + } + + public function testgetConfigAttributes() + { + $getConfigAttributes = self::getMethod("getConfigAttributes"); + $response = $getConfigAttributes->invokeArgs($this->optConfigService, array()); + if (!empty($response)) { + foreach ($response as $attr) { + $this->assertInstanceof(OptimizelyAttribute::class, $attr); + } + } + } + + public function testgetConfigAudiences() + { + $getConfigAudiences = self::getMethod("getConfigAudiences"); + $response = $getConfigAudiences->invokeArgs($this->optConfigService, array()); + if (!empty($response)) { + foreach ($response as $attr) { + $this->assertInstanceof(OptimizelyAudience::class, $attr); + } + } + } + + + public function testgetConfigEvents() + { + $getConfigEvents = self::getMethod("getConfigEvents"); + $response = $getConfigEvents->invokeArgs($this->optConfigService, array()); + if (!empty($response)) { + foreach ($response as $event) { + $this->assertInstanceof(OptimizelyEvent::class, $event); + } + } } public function testGetConfig() @@ -220,11 +401,14 @@ public function testJsonEncodeofOptimizelyConfig() $response = $this->optConfigService->getConfig(); $expectedJSON = '{ + "environmentKey":"", + "sdkKey":"", "revision": "16", "experimentsMap": { "ab_experiment": { "id": "17301270474", "key": "ab_experiment", + "audiences":"", "variationsMap": { "variation_a": { "id": "17277380360", @@ -245,6 +429,7 @@ public function testJsonEncodeofOptimizelyConfig() "feat_experiment": { "id": "17279300791", "key": "feat_experiment", + "audiences":"", "variationsMap": { "variation_a": { "id": "17289540366", @@ -325,6 +510,7 @@ public function testJsonEncodeofOptimizelyConfig() "group_ab_experiment": { "id": "17258450439", "key": "group_ab_experiment", + "audiences":"", "variationsMap": { "variation_a": { "id": "17287500312", @@ -347,10 +533,14 @@ public function testJsonEncodeofOptimizelyConfig() "test_feature": { "id": "17266500726", "key": "test_feature", + "experimentRules":[{"id":"17279300791","key":"feat_experiment","audiences":"","variationsMap":{"variation_a":{"id":"17289540366","key":"variation_a","featureEnabled":true,"variablesMap":{"boolean_var":{"id":"17252790456","key":"boolean_var","type":"boolean","value":"true"},"integer_var":{"id":"17258820367","key":"integer_var","type":"integer","value":"5"},"double_var":{"id":"17260550714","key":"double_var","type":"double","value":"5.5"},"string_var":{"id":"17290540010","key":"string_var","type":"string","value":"i am variable value"},"json_var":{"id":"17260550458","key":"json_var","type":"json","value":"{\"text\": \"variable value\"}"}}},"variation_b":{"id":"17304990114","key":"variation_b","featureEnabled":false,"variablesMap":{"boolean_var":{"id":"17252790456","key":"boolean_var","type":"boolean","value":"false"},"integer_var":{"id":"17258820367","key":"integer_var","type":"integer","value":"1"},"double_var":{"id":"17260550714","key":"double_var","type":"double","value":"0.5"},"string_var":{"id":"17290540010","key":"string_var","type":"string","value":"i am default value"},"json_var":{"id":"17260550458","key":"json_var","type":"json","value":"{\"text\": \"default value\"}"}}}}}], + + "deliveryRules":[{"id":"17268110732","key":"17268110732","audiences":"","variationsMap":{"17285550838":{"id":"17285550838","key":"17285550838","featureEnabled":true,"variablesMap":{"boolean_var":{"id":"17252790456","key":"boolean_var","type":"boolean","value":"false"},"integer_var":{"id":"17258820367","key":"integer_var","type":"integer","value":"1"},"double_var":{"id":"17260550714","key":"double_var","type":"double","value":"0.5"},"string_var":{"id":"17290540010","key":"string_var","type":"string","value":"i am default value"},"json_var":{"id":"17260550458","key":"json_var","type":"json","value":"{\"text\": \"default value\"}"}}}}}], "experimentsMap": { "feat_experiment": { "id": "17279300791", "key": "feat_experiment", + "audiences":"", "variationsMap": { "variation_a": { "id": "17289540366", @@ -466,8 +656,12 @@ public function testJsonEncodeofOptimizelyConfig() }'; $optimizelyConfig = json_decode($expectedJSON, true); - $optimizelyConfig['datafile'] = DATAFILE_FOR_OPTIMIZELY_CONFIG; - + $optimizelyConfig["attributes"] = [["id" => "111094", "key" => "test_attribute"]]; + $json_encoded = '[{"id":"3468206642","name":"exactString","conditions":"[\"and\", [\"or\", [\"or\", {\"name\": \"house\", \"type\": \"custom_attribute\", \"value\": \"Gryffindor\"}]]]"},{"id":"3988293898","name":"$$dummySubstringString","conditions":"{ \"type\": \"custom_attribute\", \"name\": \"$opt_dummy_attribute\", \"value\": \"impossible_value\" }"},{"id":"3988293899","name":"$$dummyExists","conditions":"{ \"type\": \"custom_attribute\", \"name\": \"$opt_dummy_attribute\", \"value\": \"impossible_value\" }"}]'; + $converted_audiences = json_decode($json_encoded, true); + $optimizelyConfig["audiences"] = $converted_audiences; + $optimizelyConfig["events"] = [["id" => "111095", "key" => "test_event", "experimentIds" => ["111127"]]]; + $optimizelyConfig['datafile'] = DATAFILE_FOR_OPTIMIZELY_CONFIG; $this->assertEquals(json_encode($optimizelyConfig), json_encode($response)); } diff --git a/tests/OptimizelyConfigTests/OptimizelyEntitiesTest.php b/tests/OptimizelyConfigTests/OptimizelyEntitiesTest.php index 3551af3f..995d2364 100644 --- a/tests/OptimizelyConfigTests/OptimizelyEntitiesTest.php +++ b/tests/OptimizelyConfigTests/OptimizelyEntitiesTest.php @@ -37,11 +37,18 @@ public function testOptimizelyConfigEntity() $this->assertEquals("20", $optConfig->getRevision()); $this->assertEquals(["a" => "apple"], $optConfig->getExperimentsMap()); $this->assertEquals(["o" => "orange"], $optConfig->getFeaturesMap()); + $this->assertEquals("", $optConfig->getEnvironmentKey()); + $this->assertEquals("", $optConfig->getSdkKey()); $expectedJson = '{ + "environmentKey":"", + "sdkKey":"", "revision": "20", "experimentsMap" : {"a": "apple"}, "featuresMap": {"o": "orange"}, + "attributes":[], + "audiences":[], + "events":[], "datafile": null }'; @@ -55,16 +62,19 @@ public function testOptimizelyExperimentEntity() $optExp = new OptimizelyExperiment( "id", "key", - ["a" => "apple"] + ["a" => "apple"], + '' ); $this->assertEquals("id", $optExp->getId()); $this->assertEquals("key", $optExp->getKey()); $this->assertEquals(["a" => "apple"], $optExp->getVariationsMap()); + $this->assertEquals('', $optExp->getExperimentAudiences()); $expectedJson = '{ "id": "id", "key" : "key", + "audiences":"", "variationsMap": {"a": "apple"} }'; @@ -79,17 +89,23 @@ public function testOptimizelyFeatureEntity() "id", "key", ["a" => "apple"], - ["o" => "orange"] + ["o" => "orange"], + [], + [] ); $this->assertEquals("id", $optFeature->getId()); $this->assertEquals("key", $optFeature->getKey()); $this->assertEquals(["a" => "apple"], $optFeature->getExperimentsMap()); $this->assertEquals(["o" => "orange"], $optFeature->getVariablesMap()); + $this->assertEquals([], $optFeature->getExperimentRules()); + $this->assertEquals([], $optFeature->getDeliveryRules()); $expectedJson = '{ "id": "id", "key" : "key", + "experimentRules":[], + "deliveryRules":[], "experimentsMap": {"a": "apple"}, "variablesMap": {"o": "orange"} }'; diff --git a/tests/TestData.php b/tests/TestData.php index 0d668702..10c9ed83 100644 --- a/tests/TestData.php +++ b/tests/TestData.php @@ -145,93 +145,93 @@ "id": "122230", "forcedVariations": { - }, - "trafficAllocation": [ - { - "entityId": "122231", - "endOfRange": 2500 - }, - { - "entityId": "122232", - "endOfRange": 5000 - }, - { - "entityId": "122233", - "endOfRange": 7500 }, - { - "entityId": "122234", - "endOfRange": 10000 - } - ], - "variations": [ - { - "id": "122231", - "key": "Fred", - "variables": [ + "trafficAllocation": [ { - "id": "155560", - "value": "F" + "entityId": "122231", + "endOfRange": 2500 }, { - "id": "155561", - "value": "red" - } - ], - "featureEnabled": true - }, - { - "id": "122232", - "key": "Feorge", - "variables": [ + "entityId": "122232", + "endOfRange": 5000 + }, { - "id": "155560", - "value": "F" + "entityId": "122233", + "endOfRange": 7500 }, { - "id": "155561", - "value": "eorge" + "entityId": "122234", + "endOfRange": 10000 } - ], - "featureEnabled": true - }, - { - "id": "122233", - "key": "Gred", - "variables": [ + ], + "variations": [ { - "id": "155560", - "value": "G" + "id": "122231", + "key": "Fred", + "variables": [ + { + "id": "155560", + "value": "F" + }, + { + "id": "155561", + "value": "red" + } + ], + "featureEnabled": true }, { - "id": "155561", - "value": "red" - } - ], - "featureEnabled": true - }, - { - "id": "122234", - "key": "George", - "variables": [ + "id": "122232", + "key": "Feorge", + "variables": [ + { + "id": "155560", + "value": "F" + }, + { + "id": "155561", + "value": "eorge" + } + ], + "featureEnabled": true + }, { - "id": "155560", - "value": "G" + "id": "122233", + "key": "Gred", + "variables": [ + { + "id": "155560", + "value": "G" + }, + { + "id": "155561", + "value": "red" + } + ], + "featureEnabled": true }, { - "id": "155561", - "value": "eorge" + "id": "122234", + "key": "George", + "variables": [ + { + "id": "155560", + "value": "G" + }, + { + "id": "155561", + "value": "eorge" + } + ], + "featureEnabled": true } - ], - "featureEnabled": true - } - ] - }, - { - "key": "test_experiment_with_feature_rollout", - "status": "Running", - "layerId": "5", - "audienceIds": [ + ] + }, + { + "key": "test_experiment_with_feature_rollout", + "status": "Running", + "layerId": "5", + "audienceIds": [ ], "id": "122235", @@ -1575,7 +1575,23 @@ "conditions": "[\"or\", {\"match\": \"exact\", \"name\": \"$opt_dummy_attribute\", \"type\": \"custom_attribute\", \"value\": \"$opt_dummy_value\"}]", "id": "$opt_dummy_audience", "name": "Optimizely-Generated Audience for Backwards Compatibility" + }, + { + "id": "3468206642", + "name": "exactString", + "conditions": "[\"and\", [\"or\", [\"or\", {\"name\": \"house\", \"type\": \"custom_attribute\", \"value\": \"Gryffindor\"}]]]" + }, + { + "id": "3988293898", + "name": "$$dummySubstringString", + "conditions": "{ \"type\": \"custom_attribute\", \"name\": \"$opt_dummy_attribute\", \"value\": \"impossible_value\" }" + }, + { + "id": "3988293899", + "name": "$$dummyExists", + "conditions": "{ \"type\": \"custom_attribute\", \"name\": \"$opt_dummy_attribute\", \"value\": \"impossible_value\" }" } + ], "groups": [ { @@ -1693,15 +1709,318 @@ } ], "attributes": [ - - ], - "botFiltering": false, - "accountId": "8272261422", - "events": [ - + {"key": "test_attribute", "id": "111094"} + ], + "botFiltering": false, + "accountId": "8272261422", + "events": [ + {"key": "test_event", "experimentIds": ["111127"], "id": "111095"} + ], + "revision": "16" + }' +); + +define( + 'DATAFILE_FOR_DUPLICATE_EXP_KEYS', + '{ + "version": "4", + "rollouts": [], + "typedAudiences": [ + { + "id": "20415611520", + "conditions": [ + "and", + [ + "or", + [ + "or", + { + "value": true, + "type": "custom_attribute", + "name": "hiddenLiveEnabled", + "match": "exact" + } + ] + ] + ], + "name": "test1" + }, + { + "id": "20406066925", + "conditions": [ + "and", + [ + "or", + [ + "or", + { + "value": false, + "type": "custom_attribute", + "name": "hiddenLiveEnabled", + "match": "exact" + } + ] + ] + ], + "name": "test2" + } + ], + "anonymizeIP": true, + "projectId": "20430981610", + "variables": [], + "featureFlags": [ + { + "experimentIds": ["9300000007569"], + "rolloutId": "", + "variables": [], + "id": "3045", + "key": "flag1" + }, + { + "experimentIds": ["9300000007573"], + "rolloutId": "", + "variables": [], + "id": "3046", + "key": "flag2" + } ], - "revision": "16" - }' + "experiments": [ + { + "status": "Running", + "audienceConditions": ["or", "20415611520"], + "audienceIds": ["20415611520"], + "variations": [ + { + "variables": [], + "id": "8045", + "key": "variation1", + "featureEnabled": true + } + ], + "forcedVariations": {}, + "key": "targeted_delivery", + "layerId": "9300000007569", + "trafficAllocation": [{ "entityId": "8045", "endOfRange": 10000 }], + "id": "9300000007569" + }, + { + "status": "Running", + "audienceConditions": ["or", "20406066925"], + "audienceIds": ["20406066925"], + "variations": [ + { + "variables": [], + "id": "8048", + "key": "variation2", + "featureEnabled": true + } + ], + "forcedVariations": {}, + "key": "targeted_delivery", + "layerId": "9300000007573", + "trafficAllocation": [{ "entityId": "8048", "endOfRange": 10000 }], + "id": "9300000007573" + } + ], + "audiences": [ + { + "id": "20415611520", + "conditions": "[\"or\", {\"match\": \"exact\", \"name\": \"$opt_dummy_attribute\", \"type\": \"custom_attribute\", \"value\": \"$opt_dummy_value\"}]", + "name": "test1" + }, + { + "id": "20406066925", + "conditions": "[\"or\", {\"match\": \"exact\", \"name\": \"$opt_dummy_attribute\", \"type\": \"custom_attribute\", \"value\": \"$opt_dummy_value\"}]", + "name": "test2" + }, + { + "conditions": "[\"or\", {\"match\": \"exact\", \"name\": \"$opt_dummy_attribute\", \"type\": \"custom_attribute\", \"value\": \"$opt_dummy_value\"}]", + "id": "$opt_dummy_audience", + "name": "Optimizely-Generated Audience for Backwards Compatibility" + } + ], + "groups": [], + "attributes": [{ "id": "20408641883", "key": "hiddenLiveEnabled" }], + "botFiltering": false, + "accountId": "17882702980", + "events": [], + "revision": "25", + "sendFlagDecisions": true + }' +); + +define( + 'DATAFILE_FOR_DUPLICATE_RUL_KEYS', + '{ + "version": "4", + "rollouts": [ + { + "experiments": [ + { + "status": "Running", + "audienceConditions": [], + "audienceIds": [], + "variations": [ + { + "variables": [], + "id": "5452", + "key": "on", + "featureEnabled": true + } + ], + "forcedVariations": {}, + "key": "targeted_delivery", + "layerId": "9300000004981", + "trafficAllocation": [{ "entityId": "5452", "endOfRange": 10000 }], + "id": "9300000004981" + }, + { + "status": "Running", + "audienceConditions": [], + "audienceIds": [], + "variations": [ + { + "variables": [], + "id": "5451", + "key": "off", + "featureEnabled": false + } + ], + "forcedVariations": {}, + "key": "default-rollout-2029-20301771717", + "layerId": "default-layer-rollout-2029-20301771717", + "trafficAllocation": [{ "entityId": "5451", "endOfRange": 10000 }], + "id": "default-rollout-2029-20301771717" + } + ], + "id": "rollout-2029-20301771717" + }, + { + "experiments": [ + { + "status": "Running", + "audienceConditions": [], + "audienceIds": [], + "variations": [ + { + "variables": [], + "id": "5450", + "key": "on", + "featureEnabled": true + } + ], + "forcedVariations": {}, + "key": "targeted_delivery", + "layerId": "9300000004979", + "trafficAllocation": [{ "entityId": "5450", "endOfRange": 10000 }], + "id": "9300000004979" + }, + { + "status": "Running", + "audienceConditions": [], + "audienceIds": [], + "variations": [ + { + "variables": [], + "id": "5449", + "key": "off", + "featureEnabled": false + } + ], + "forcedVariations": {}, + "key": "default-rollout-2028-20301771717", + "layerId": "default-layer-rollout-2028-20301771717", + "trafficAllocation": [{ "entityId": "5449", "endOfRange": 10000 }], + "id": "default-rollout-2028-20301771717" + } + ], + "id": "rollout-2028-20301771717" + }, + { + "experiments": [ + { + "status": "Running", + "audienceConditions": [], + "audienceIds": [], + "variations": [ + { + "variables": [], + "id": "5448", + "key": "on", + "featureEnabled": true + } + ], + "forcedVariations": {}, + "key": "targeted_delivery", + "layerId": "9300000004977", + "trafficAllocation": [{ "entityId": "5448", "endOfRange": 10000 }], + "id": "9300000004977" + }, + { + "status": "Running", + "audienceConditions": [], + "audienceIds": [], + "variations": [ + { + "variables": [], + "id": "5447", + "key": "off", + "featureEnabled": false + } + ], + "forcedVariations": {}, + "key": "default-rollout-2027-20301771717", + "layerId": "default-layer-rollout-2027-20301771717", + "trafficAllocation": [{ "entityId": "5447", "endOfRange": 10000 }], + "id": "default-rollout-2027-20301771717" + } + ], + "id": "rollout-2027-20301771717" + } + ], + "typedAudiences": [], + "anonymizeIP": true, + "projectId": "20286295225", + "variables": [], + "featureFlags": [ + { + "experimentIds": [], + "rolloutId": "rollout-2029-20301771717", + "variables": [], + "id": "2029", + "key": "flag_3" + }, + { + "experimentIds": [], + "rolloutId": "rollout-2028-20301771717", + "variables": [], + "id": "2028", + "key": "flag_2" + }, + { + "experimentIds": [], + "rolloutId": "rollout-2027-20301771717", + "variables": [], + "id": "2027", + "key": "flag_1" + } + ], + "experiments": [], + "audiences": [ + { + "conditions": "[\"or\", {\"match\": \"exact\", \"name\": \"$opt_dummy_attribute\", \"type\": \"custom_attribute\", \"value\": \"$opt_dummy_value\"}]", + "id": "$opt_dummy_audience", + "name": "Optimizely-Generated Audience for Backwards Compatibility" + } + ], + "groups": [], + "attributes": [], + "botFiltering": false, + "accountId": "19947277778", + "events": [], + "revision": "11", + "sendFlagDecisions": true + }' ); /**