From d3d4a1005250eed32f7e85a8c6722d766cce5e0b Mon Sep 17 00:00:00 2001 From: FarhanAnjum-opti Date: Fri, 13 Mar 2026 19:50:42 +0600 Subject: [PATCH 1/5] [AI-FSSDK] [FSSDK-12337] Add Feature Rollout support --- pkg/config/datafileprojectconfig/config.go | 58 +++ .../entities/entities.go | 1 + .../feature_rollout_test.go | 329 ++++++++++++++++++ .../mappers/experiment.go | 1 + pkg/entities/experiment.go | 1 + 5 files changed, 390 insertions(+) create mode 100644 pkg/config/datafileprojectconfig/feature_rollout_test.go diff --git a/pkg/config/datafileprojectconfig/config.go b/pkg/config/datafileprojectconfig/config.go index 13526875..56c8afb3 100644 --- a/pkg/config/datafileprojectconfig/config.go +++ b/pkg/config/datafileprojectconfig/config.go @@ -332,6 +332,10 @@ func NewDatafileProjectConfig(jsonDatafile []byte, logger logging.OptimizelyLogP } eventMap := mappers.MapEvents(datafile.Events) featureMap := mappers.MapFeatures(datafile.FeatureFlags, rolloutMap, experimentIDMap) + + // Inject "everyone else" variation into feature_rollout experiments + injectFeatureRolloutVariations(featureMap, rolloutMap, experimentIDMap) + audienceMap, audienceSegmentList := mappers.MapAudiences(append(datafile.TypedAudiences, datafile.Audiences...)) flagVariationsMap := mappers.MapFlagVariations(featureMap) holdouts, holdoutIDMap, flagHoldoutsMap := mappers.MapHoldouts(datafile.Holdouts, featureMap) @@ -385,3 +389,57 @@ func NewDatafileProjectConfig(jsonDatafile []byte, logger logging.OptimizelyLogP logger.Info("Datafile is valid.") return config, nil } + +// injectFeatureRolloutVariations injects the "everyone else" variation from a flag's rollout +// into any experiment with type "feature_rollout". This enables Feature Rollout experiments +// to fall back to the everyone else variation when users are outside the rollout percentage. +func injectFeatureRolloutVariations(featureMap map[string]entities.Feature, rolloutMap map[string]entities.Rollout, experimentMap map[string]entities.Experiment) { + for _, feature := range featureMap { + everyoneElseVariation := getEveryoneElseVariation(feature, rolloutMap) + if everyoneElseVariation == nil { + continue + } + + for _, experimentID := range feature.ExperimentIDs { + experiment, ok := experimentMap[experimentID] + if !ok { + continue + } + if experiment.Type != "feature_rollout" { + continue + } + + // Inject the everyone else variation + experiment.Variations[everyoneElseVariation.ID] = *everyoneElseVariation + experiment.VariationKeyToIDMap[everyoneElseVariation.Key] = everyoneElseVariation.ID + experiment.TrafficAllocation = append(experiment.TrafficAllocation, entities.Range{ + EntityID: everyoneElseVariation.ID, + EndOfRange: 10000, + }) + + // Update the experiment in the map + experimentMap[experimentID] = experiment + } + } +} + +// getEveryoneElseVariation retrieves the first variation from the last experiment +// in the flag's rollout (the "everyone else" rule). +func getEveryoneElseVariation(feature entities.Feature, rolloutMap map[string]entities.Rollout) *entities.Variation { + rollout := feature.Rollout + if rollout.ID == "" { + return nil + } + if len(rollout.Experiments) == 0 { + return nil + } + everyoneElseRule := rollout.Experiments[len(rollout.Experiments)-1] + if len(everyoneElseRule.Variations) == 0 { + return nil + } + // Get the first variation from the everyone else rule + for _, variation := range everyoneElseRule.Variations { + return &variation + } + return nil +} diff --git a/pkg/config/datafileprojectconfig/entities/entities.go b/pkg/config/datafileprojectconfig/entities/entities.go index 76edd48d..1abed5d8 100644 --- a/pkg/config/datafileprojectconfig/entities/entities.go +++ b/pkg/config/datafileprojectconfig/entities/entities.go @@ -51,6 +51,7 @@ type Experiment struct { AudienceIds []string `json:"audienceIds"` ForcedVariations map[string]string `json:"forcedVariations"` AudienceConditions interface{} `json:"audienceConditions"` + Type string `json:"type,omitempty"` Cmab *Cmab `json:"cmab,omitempty"` // is optional } diff --git a/pkg/config/datafileprojectconfig/feature_rollout_test.go b/pkg/config/datafileprojectconfig/feature_rollout_test.go new file mode 100644 index 00000000..37499c15 --- /dev/null +++ b/pkg/config/datafileprojectconfig/feature_rollout_test.go @@ -0,0 +1,329 @@ +/**************************************************************************** + * Copyright 2026, Optimizely, Inc. and contributors * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); * + * you may not use this file except in compliance with the License. * + * You may obtain a copy of the License at * + * * + * https://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + ***************************************************************************/ + +package datafileprojectconfig + +import ( + "testing" + + "github.com/optimizely/go-sdk/v2/pkg/logging" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const featureRolloutDatafile = `{ + "accountId": "12345", + "anonymizeIP": false, + "sendFlagDecisions": true, + "botFiltering": false, + "projectId": "67890", + "revision": "1", + "sdkKey": "FeatureRolloutTest", + "environmentKey": "production", + "version": "4", + "audiences": [], + "typedAudiences": [], + "attributes": [], + "events": [], + "groups": [], + "integrations": [], + "experiments": [ + { + "id": "exp_rollout_1", + "key": "feature_rollout_experiment", + "status": "Running", + "layerId": "layer_1", + "audienceIds": [], + "forcedVariations": {}, + "type": "feature_rollout", + "variations": [ + { + "id": "var_rollout_1", + "key": "rollout_variation", + "featureEnabled": true + } + ], + "trafficAllocation": [ + { + "entityId": "var_rollout_1", + "endOfRange": 5000 + } + ] + }, + { + "id": "exp_ab_1", + "key": "ab_test_experiment", + "status": "Running", + "layerId": "layer_2", + "audienceIds": [], + "forcedVariations": {}, + "type": "a/b", + "variations": [ + { + "id": "var_ab_1", + "key": "control", + "featureEnabled": false + }, + { + "id": "var_ab_2", + "key": "treatment", + "featureEnabled": true + } + ], + "trafficAllocation": [ + { + "entityId": "var_ab_1", + "endOfRange": 5000 + }, + { + "entityId": "var_ab_2", + "endOfRange": 10000 + } + ] + }, + { + "id": "exp_no_type", + "key": "no_type_experiment", + "status": "Running", + "layerId": "layer_3", + "audienceIds": [], + "forcedVariations": {}, + "variations": [ + { + "id": "var_notype_1", + "key": "variation_1", + "featureEnabled": true + } + ], + "trafficAllocation": [ + { + "entityId": "var_notype_1", + "endOfRange": 10000 + } + ] + }, + { + "id": "exp_rollout_no_rollout_id", + "key": "rollout_no_rollout_id_experiment", + "status": "Running", + "layerId": "layer_4", + "audienceIds": [], + "forcedVariations": {}, + "type": "feature_rollout", + "variations": [ + { + "id": "var_no_rollout_1", + "key": "rollout_no_rollout_variation", + "featureEnabled": true + } + ], + "trafficAllocation": [ + { + "entityId": "var_no_rollout_1", + "endOfRange": 5000 + } + ] + } + ], + "featureFlags": [ + { + "id": "flag_1", + "key": "feature_with_rollout", + "rolloutId": "rollout_1", + "experimentIds": ["exp_rollout_1"], + "variables": [] + }, + { + "id": "flag_2", + "key": "feature_with_ab", + "rolloutId": "rollout_2", + "experimentIds": ["exp_ab_1"], + "variables": [] + }, + { + "id": "flag_3", + "key": "feature_no_rollout_id", + "rolloutId": "", + "experimentIds": ["exp_rollout_no_rollout_id"], + "variables": [] + } + ], + "rollouts": [ + { + "id": "rollout_1", + "experiments": [ + { + "id": "rollout_exp_1", + "key": "rollout_rule_1", + "status": "Running", + "layerId": "rollout_layer_1", + "audienceIds": [], + "forcedVariations": {}, + "variations": [ + { + "id": "rollout_var_1", + "key": "rollout_enabled", + "featureEnabled": true + } + ], + "trafficAllocation": [ + { + "entityId": "rollout_var_1", + "endOfRange": 10000 + } + ] + }, + { + "id": "rollout_exp_everyone", + "key": "everyone_else_rule", + "status": "Running", + "layerId": "rollout_layer_everyone", + "audienceIds": [], + "forcedVariations": {}, + "variations": [ + { + "id": "everyone_else_var", + "key": "everyone_else_variation", + "featureEnabled": false + } + ], + "trafficAllocation": [ + { + "entityId": "everyone_else_var", + "endOfRange": 10000 + } + ] + } + ] + }, + { + "id": "rollout_2", + "experiments": [ + { + "id": "rollout_exp_2", + "key": "rollout_rule_2", + "status": "Running", + "layerId": "rollout_layer_2", + "audienceIds": [], + "forcedVariations": {}, + "variations": [ + { + "id": "rollout_var_2", + "key": "rollout_variation_2", + "featureEnabled": true + } + ], + "trafficAllocation": [ + { + "entityId": "rollout_var_2", + "endOfRange": 10000 + } + ] + } + ] + } + ] +}` + +func loadFeatureRolloutConfig(t *testing.T) *DatafileProjectConfig { + config, err := NewDatafileProjectConfig([]byte(featureRolloutDatafile), logging.GetLogger("", "FeatureRolloutTest")) + require.NoError(t, err) + require.NotNil(t, config) + return config +} + +// Test 1: Backward compatibility - experiments without type field have type="" (zero value) +func TestExperimentWithoutTypeFieldHasEmptyType(t *testing.T) { + config := loadFeatureRolloutConfig(t) + experiment, err := config.GetExperimentByKey("no_type_experiment") + assert.NoError(t, err) + assert.Empty(t, experiment.Type, "Type should be empty for experiments without type field") +} + +// Test 2: Core injection - feature_rollout experiments get everyone else variation + trafficAllocation injected +func TestFeatureRolloutExperimentGetsEveryoneElseVariationInjected(t *testing.T) { + config := loadFeatureRolloutConfig(t) + experiment, err := config.GetExperimentByKey("feature_rollout_experiment") + assert.NoError(t, err) + assert.Equal(t, "feature_rollout", experiment.Type) + + // Should have 2 variations: original + everyone else + assert.Equal(t, 2, len(experiment.Variations), "Should have 2 variations after injection") + + // Check the injected variation exists + injectedVariation, ok := experiment.Variations["everyone_else_var"] + assert.True(t, ok, "Should contain injected variation by ID") + assert.Equal(t, "everyone_else_variation", injectedVariation.Key) + + // Check the injected traffic allocation + assert.Equal(t, 2, len(experiment.TrafficAllocation), "Should have 2 traffic allocations after injection") + lastAllocation := experiment.TrafficAllocation[len(experiment.TrafficAllocation)-1] + assert.Equal(t, "everyone_else_var", lastAllocation.EntityID) + assert.Equal(t, 10000, lastAllocation.EndOfRange) +} + +// Test 3: Variation maps updated - VariationKeyToIDMap contains the injected variation +func TestVariationMapsContainInjectedVariation(t *testing.T) { + config := loadFeatureRolloutConfig(t) + experiment, err := config.GetExperimentByKey("feature_rollout_experiment") + assert.NoError(t, err) + + // Check VariationKeyToIDMap contains the injected variation + variationID, ok := experiment.VariationKeyToIDMap["everyone_else_variation"] + assert.True(t, ok, "VariationKeyToIDMap should contain injected variation key") + assert.Equal(t, "everyone_else_var", variationID) +} + +// Test 4: Non-rollout unchanged - A/B experiments are not modified +func TestABTestExperimentNotModified(t *testing.T) { + config := loadFeatureRolloutConfig(t) + experiment, err := config.GetExperimentByKey("ab_test_experiment") + assert.NoError(t, err) + assert.Equal(t, "a/b", experiment.Type) + + // Should still have exactly 2 original variations + assert.Equal(t, 2, len(experiment.Variations), "A/B test should keep original 2 variations") + assert.Equal(t, 2, len(experiment.TrafficAllocation), "A/B test should keep original 2 traffic allocations") +} + +// Test 5: No rollout edge case - feature_rollout with empty rolloutId does not crash +func TestFeatureRolloutWithEmptyRolloutIdDoesNotCrash(t *testing.T) { + config := loadFeatureRolloutConfig(t) + experiment, err := config.GetExperimentByKey("rollout_no_rollout_id_experiment") + assert.NoError(t, err) + assert.Equal(t, "feature_rollout", experiment.Type) + + // Should keep only original variation since rollout cannot be resolved + assert.Equal(t, 1, len(experiment.Variations), "Should keep only original variation") +} + +// Test 6: Type field parsed - experiments with type field have the value correctly preserved +func TestTypeFieldCorrectlyParsed(t *testing.T) { + config := loadFeatureRolloutConfig(t) + + rolloutExp, err := config.GetExperimentByKey("feature_rollout_experiment") + assert.NoError(t, err) + assert.Equal(t, "feature_rollout", rolloutExp.Type) + + abExp, err := config.GetExperimentByKey("ab_test_experiment") + assert.NoError(t, err) + assert.Equal(t, "a/b", abExp.Type) + + noTypeExp, err := config.GetExperimentByKey("no_type_experiment") + assert.NoError(t, err) + assert.Empty(t, noTypeExp.Type) +} diff --git a/pkg/config/datafileprojectconfig/mappers/experiment.go b/pkg/config/datafileprojectconfig/mappers/experiment.go index 5f6e3bf6..f7d4372f 100644 --- a/pkg/config/datafileprojectconfig/mappers/experiment.go +++ b/pkg/config/datafileprojectconfig/mappers/experiment.go @@ -91,6 +91,7 @@ func mapExperiment(rawExperiment datafileEntities.Experiment) entities.Experimen AudienceConditionTree: audienceConditionTree, Whitelist: rawExperiment.ForcedVariations, IsFeatureExperiment: false, + Type: rawExperiment.Type, Cmab: mapCmab(rawExperiment.Cmab), } diff --git a/pkg/entities/experiment.go b/pkg/entities/experiment.go index 6d04e581..70206f41 100644 --- a/pkg/entities/experiment.go +++ b/pkg/entities/experiment.go @@ -45,6 +45,7 @@ type Experiment struct { AudienceConditionTree *TreeNode Whitelist map[string]string IsFeatureExperiment bool + Type string Cmab *Cmab } From a911b96013bd0244629e3babd8e55b475e00ee69 Mon Sep 17 00:00:00 2001 From: FarhanAnjum-opti Date: Fri, 13 Mar 2026 20:38:57 +0600 Subject: [PATCH 2/5] [FSSDK-12337] Add ExperimentType constants for type-safe experiment type checks --- pkg/config/datafileprojectconfig/config.go | 2 +- .../datafileprojectconfig/feature_rollout_test.go | 11 ++++++----- .../datafileprojectconfig/mappers/experiment.go | 2 +- pkg/entities/experiment.go | 13 ++++++++++++- 4 files changed, 20 insertions(+), 8 deletions(-) diff --git a/pkg/config/datafileprojectconfig/config.go b/pkg/config/datafileprojectconfig/config.go index 56c8afb3..e986af43 100644 --- a/pkg/config/datafileprojectconfig/config.go +++ b/pkg/config/datafileprojectconfig/config.go @@ -405,7 +405,7 @@ func injectFeatureRolloutVariations(featureMap map[string]entities.Feature, roll if !ok { continue } - if experiment.Type != "feature_rollout" { + if experiment.Type != entities.ExperimentTypeFR { continue } diff --git a/pkg/config/datafileprojectconfig/feature_rollout_test.go b/pkg/config/datafileprojectconfig/feature_rollout_test.go index 37499c15..ef0c805f 100644 --- a/pkg/config/datafileprojectconfig/feature_rollout_test.go +++ b/pkg/config/datafileprojectconfig/feature_rollout_test.go @@ -19,6 +19,7 @@ package datafileprojectconfig import ( "testing" + "github.com/optimizely/go-sdk/v2/pkg/entities" "github.com/optimizely/go-sdk/v2/pkg/logging" "github.com/stretchr/testify/assert" @@ -259,7 +260,7 @@ func TestFeatureRolloutExperimentGetsEveryoneElseVariationInjected(t *testing.T) config := loadFeatureRolloutConfig(t) experiment, err := config.GetExperimentByKey("feature_rollout_experiment") assert.NoError(t, err) - assert.Equal(t, "feature_rollout", experiment.Type) + assert.Equal(t, entities.ExperimentTypeFR, experiment.Type) // Should have 2 variations: original + everyone else assert.Equal(t, 2, len(experiment.Variations), "Should have 2 variations after injection") @@ -293,7 +294,7 @@ func TestABTestExperimentNotModified(t *testing.T) { config := loadFeatureRolloutConfig(t) experiment, err := config.GetExperimentByKey("ab_test_experiment") assert.NoError(t, err) - assert.Equal(t, "a/b", experiment.Type) + assert.Equal(t, entities.ExperimentTypeAB, experiment.Type) // Should still have exactly 2 original variations assert.Equal(t, 2, len(experiment.Variations), "A/B test should keep original 2 variations") @@ -305,7 +306,7 @@ func TestFeatureRolloutWithEmptyRolloutIdDoesNotCrash(t *testing.T) { config := loadFeatureRolloutConfig(t) experiment, err := config.GetExperimentByKey("rollout_no_rollout_id_experiment") assert.NoError(t, err) - assert.Equal(t, "feature_rollout", experiment.Type) + assert.Equal(t, entities.ExperimentTypeFR, experiment.Type) // Should keep only original variation since rollout cannot be resolved assert.Equal(t, 1, len(experiment.Variations), "Should keep only original variation") @@ -317,11 +318,11 @@ func TestTypeFieldCorrectlyParsed(t *testing.T) { rolloutExp, err := config.GetExperimentByKey("feature_rollout_experiment") assert.NoError(t, err) - assert.Equal(t, "feature_rollout", rolloutExp.Type) + assert.Equal(t, entities.ExperimentTypeFR, rolloutExp.Type) abExp, err := config.GetExperimentByKey("ab_test_experiment") assert.NoError(t, err) - assert.Equal(t, "a/b", abExp.Type) + assert.Equal(t, entities.ExperimentTypeAB, abExp.Type) noTypeExp, err := config.GetExperimentByKey("no_type_experiment") assert.NoError(t, err) diff --git a/pkg/config/datafileprojectconfig/mappers/experiment.go b/pkg/config/datafileprojectconfig/mappers/experiment.go index f7d4372f..394f897a 100644 --- a/pkg/config/datafileprojectconfig/mappers/experiment.go +++ b/pkg/config/datafileprojectconfig/mappers/experiment.go @@ -91,7 +91,7 @@ func mapExperiment(rawExperiment datafileEntities.Experiment) entities.Experimen AudienceConditionTree: audienceConditionTree, Whitelist: rawExperiment.ForcedVariations, IsFeatureExperiment: false, - Type: rawExperiment.Type, + Type: entities.ExperimentType(rawExperiment.Type), Cmab: mapCmab(rawExperiment.Cmab), } diff --git a/pkg/entities/experiment.go b/pkg/entities/experiment.go index 70206f41..937ba184 100644 --- a/pkg/entities/experiment.go +++ b/pkg/entities/experiment.go @@ -45,7 +45,7 @@ type Experiment struct { AudienceConditionTree *TreeNode Whitelist map[string]string IsFeatureExperiment bool - Type string + Type ExperimentType Cmab *Cmab } @@ -61,6 +61,17 @@ type VariationVariable struct { Value string } +// ExperimentType represents the type of an experiment +type ExperimentType string + +const ( + ExperimentTypeAB ExperimentType = "a/b" + ExperimentTypeMAB ExperimentType = "multi_armed_bandit" + ExperimentTypeCMAB ExperimentType = "contextual_multi_armed_bandit" + ExperimentTypeTD ExperimentType = "targeted_delivery" + ExperimentTypeFR ExperimentType = "feature_rollout" +) + // HoldoutStatus represents the status of a holdout type HoldoutStatus string From beb178cebd5058d24804d73e2f755777b4edeacf Mon Sep 17 00:00:00 2001 From: FarhanAnjum-opti Date: Fri, 13 Mar 2026 21:00:19 +0600 Subject: [PATCH 3/5] [FSSDK-12337] Remove unused rolloutMap parameter from getEveryoneElseVariation --- pkg/config/datafileprojectconfig/config.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/config/datafileprojectconfig/config.go b/pkg/config/datafileprojectconfig/config.go index e986af43..4f8faf3e 100644 --- a/pkg/config/datafileprojectconfig/config.go +++ b/pkg/config/datafileprojectconfig/config.go @@ -334,7 +334,7 @@ func NewDatafileProjectConfig(jsonDatafile []byte, logger logging.OptimizelyLogP featureMap := mappers.MapFeatures(datafile.FeatureFlags, rolloutMap, experimentIDMap) // Inject "everyone else" variation into feature_rollout experiments - injectFeatureRolloutVariations(featureMap, rolloutMap, experimentIDMap) + injectFeatureRolloutVariations(featureMap, experimentIDMap) audienceMap, audienceSegmentList := mappers.MapAudiences(append(datafile.TypedAudiences, datafile.Audiences...)) flagVariationsMap := mappers.MapFlagVariations(featureMap) @@ -393,9 +393,9 @@ func NewDatafileProjectConfig(jsonDatafile []byte, logger logging.OptimizelyLogP // injectFeatureRolloutVariations injects the "everyone else" variation from a flag's rollout // into any experiment with type "feature_rollout". This enables Feature Rollout experiments // to fall back to the everyone else variation when users are outside the rollout percentage. -func injectFeatureRolloutVariations(featureMap map[string]entities.Feature, rolloutMap map[string]entities.Rollout, experimentMap map[string]entities.Experiment) { +func injectFeatureRolloutVariations(featureMap map[string]entities.Feature, experimentMap map[string]entities.Experiment) { for _, feature := range featureMap { - everyoneElseVariation := getEveryoneElseVariation(feature, rolloutMap) + everyoneElseVariation := getEveryoneElseVariation(feature) if everyoneElseVariation == nil { continue } @@ -425,7 +425,7 @@ func injectFeatureRolloutVariations(featureMap map[string]entities.Feature, roll // getEveryoneElseVariation retrieves the first variation from the last experiment // in the flag's rollout (the "everyone else" rule). -func getEveryoneElseVariation(feature entities.Feature, rolloutMap map[string]entities.Rollout) *entities.Variation { +func getEveryoneElseVariation(feature entities.Feature) *entities.Variation { rollout := feature.Rollout if rollout.ID == "" { return nil From 62d2e3909b49cae3458c8bdbad153b06ce38283f Mon Sep 17 00:00:00 2001 From: FarhanAnjum-opti Date: Mon, 16 Mar 2026 22:40:01 +0600 Subject: [PATCH 4/5] [AI-FSSDK] [FSSDK-12337] Update experiment type values to short-form abbreviations --- .../datafileprojectconfig/feature_rollout_test.go | 6 +++--- pkg/entities/experiment.go | 10 +++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/pkg/config/datafileprojectconfig/feature_rollout_test.go b/pkg/config/datafileprojectconfig/feature_rollout_test.go index ef0c805f..53187970 100644 --- a/pkg/config/datafileprojectconfig/feature_rollout_test.go +++ b/pkg/config/datafileprojectconfig/feature_rollout_test.go @@ -50,7 +50,7 @@ const featureRolloutDatafile = `{ "layerId": "layer_1", "audienceIds": [], "forcedVariations": {}, - "type": "feature_rollout", + "type": "fr", "variations": [ { "id": "var_rollout_1", @@ -72,7 +72,7 @@ const featureRolloutDatafile = `{ "layerId": "layer_2", "audienceIds": [], "forcedVariations": {}, - "type": "a/b", + "type": "ab", "variations": [ { "id": "var_ab_1", @@ -124,7 +124,7 @@ const featureRolloutDatafile = `{ "layerId": "layer_4", "audienceIds": [], "forcedVariations": {}, - "type": "feature_rollout", + "type": "fr", "variations": [ { "id": "var_no_rollout_1", diff --git a/pkg/entities/experiment.go b/pkg/entities/experiment.go index 937ba184..0de662b6 100644 --- a/pkg/entities/experiment.go +++ b/pkg/entities/experiment.go @@ -65,11 +65,11 @@ type VariationVariable struct { type ExperimentType string const ( - ExperimentTypeAB ExperimentType = "a/b" - ExperimentTypeMAB ExperimentType = "multi_armed_bandit" - ExperimentTypeCMAB ExperimentType = "contextual_multi_armed_bandit" - ExperimentTypeTD ExperimentType = "targeted_delivery" - ExperimentTypeFR ExperimentType = "feature_rollout" + ExperimentTypeAB ExperimentType = "ab" + ExperimentTypeMAB ExperimentType = "mab" + ExperimentTypeCMAB ExperimentType = "cmab" + ExperimentTypeTD ExperimentType = "td" + ExperimentTypeFR ExperimentType = "fr" ) // HoldoutStatus represents the status of a holdout From 2f9a05234dd7067bf80820c68abadcb67fc92fb3 Mon Sep 17 00:00:00 2001 From: FarhanAnjum-opti Date: Mon, 16 Mar 2026 22:52:29 +0600 Subject: [PATCH 5/5] [AI-FSSDK] [FSSDK-12337] Add validation for experiment type field --- pkg/config/datafileprojectconfig/config.go | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/pkg/config/datafileprojectconfig/config.go b/pkg/config/datafileprojectconfig/config.go index 4f8faf3e..cddda08e 100644 --- a/pkg/config/datafileprojectconfig/config.go +++ b/pkg/config/datafileprojectconfig/config.go @@ -325,6 +325,21 @@ func NewDatafileProjectConfig(jsonDatafile []byte, logger logging.OptimizelyLogP groupMap, experimentGroupMap := mappers.MapGroups(datafile.Groups) experimentIDMap, experimentKeyMap := mappers.MapExperiments(allExperiments, experimentGroupMap) + validExperimentTypes := map[entities.ExperimentType]bool{ + entities.ExperimentTypeAB: true, + entities.ExperimentTypeMAB: true, + entities.ExperimentTypeCMAB: true, + entities.ExperimentTypeTD: true, + entities.ExperimentTypeFR: true, + } + for _, experiment := range experimentIDMap { + if experiment.Type != "" && !validExperimentTypes[experiment.Type] { + err = fmt.Errorf(`experiment "%s" has invalid type "%s"`, experiment.Key, experiment.Type) + logger.Error(err.Error(), err) + return nil, err + } + } + rollouts, rolloutMap := mappers.MapRollouts(datafile.Rollouts) integrations := []entities.Integration{} for _, integration := range datafile.Integrations {