Skip to content

Commit 8d64c55

Browse files
[AI-FSSDK] [FSSDK-12337] Add Feature Rollout support (#442)
* [AI-FSSDK] [FSSDK-12337] Add Feature Rollout support * [FSSDK-12337] Add ExperimentType constants for type-safe experiment type checks * [FSSDK-12337] Remove unused rolloutMap parameter from getEveryoneElseVariation * [AI-FSSDK] [FSSDK-12337] Update experiment type values to short-form abbreviations * [AI-FSSDK] [FSSDK-12337] Add validation for experiment type field
1 parent 0e66a98 commit 8d64c55

5 files changed

Lines changed: 417 additions & 0 deletions

File tree

pkg/config/datafileprojectconfig/config.go

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,13 +325,32 @@ func NewDatafileProjectConfig(jsonDatafile []byte, logger logging.OptimizelyLogP
325325
groupMap, experimentGroupMap := mappers.MapGroups(datafile.Groups)
326326
experimentIDMap, experimentKeyMap := mappers.MapExperiments(allExperiments, experimentGroupMap)
327327

328+
validExperimentTypes := map[entities.ExperimentType]bool{
329+
entities.ExperimentTypeAB: true,
330+
entities.ExperimentTypeMAB: true,
331+
entities.ExperimentTypeCMAB: true,
332+
entities.ExperimentTypeTD: true,
333+
entities.ExperimentTypeFR: true,
334+
}
335+
for _, experiment := range experimentIDMap {
336+
if experiment.Type != "" && !validExperimentTypes[experiment.Type] {
337+
err = fmt.Errorf(`experiment "%s" has invalid type "%s"`, experiment.Key, experiment.Type)
338+
logger.Error(err.Error(), err)
339+
return nil, err
340+
}
341+
}
342+
328343
rollouts, rolloutMap := mappers.MapRollouts(datafile.Rollouts)
329344
integrations := []entities.Integration{}
330345
for _, integration := range datafile.Integrations {
331346
integrations = append(integrations, entities.Integration{Key: *integration.Key, Host: integration.Host, PublicKey: integration.PublicKey})
332347
}
333348
eventMap := mappers.MapEvents(datafile.Events)
334349
featureMap := mappers.MapFeatures(datafile.FeatureFlags, rolloutMap, experimentIDMap)
350+
351+
// Inject "everyone else" variation into feature_rollout experiments
352+
injectFeatureRolloutVariations(featureMap, experimentIDMap)
353+
335354
audienceMap, audienceSegmentList := mappers.MapAudiences(append(datafile.TypedAudiences, datafile.Audiences...))
336355
flagVariationsMap := mappers.MapFlagVariations(featureMap)
337356
holdouts, holdoutIDMap, flagHoldoutsMap := mappers.MapHoldouts(datafile.Holdouts, featureMap)
@@ -385,3 +404,57 @@ func NewDatafileProjectConfig(jsonDatafile []byte, logger logging.OptimizelyLogP
385404
logger.Info("Datafile is valid.")
386405
return config, nil
387406
}
407+
408+
// injectFeatureRolloutVariations injects the "everyone else" variation from a flag's rollout
409+
// into any experiment with type "feature_rollout". This enables Feature Rollout experiments
410+
// to fall back to the everyone else variation when users are outside the rollout percentage.
411+
func injectFeatureRolloutVariations(featureMap map[string]entities.Feature, experimentMap map[string]entities.Experiment) {
412+
for _, feature := range featureMap {
413+
everyoneElseVariation := getEveryoneElseVariation(feature)
414+
if everyoneElseVariation == nil {
415+
continue
416+
}
417+
418+
for _, experimentID := range feature.ExperimentIDs {
419+
experiment, ok := experimentMap[experimentID]
420+
if !ok {
421+
continue
422+
}
423+
if experiment.Type != entities.ExperimentTypeFR {
424+
continue
425+
}
426+
427+
// Inject the everyone else variation
428+
experiment.Variations[everyoneElseVariation.ID] = *everyoneElseVariation
429+
experiment.VariationKeyToIDMap[everyoneElseVariation.Key] = everyoneElseVariation.ID
430+
experiment.TrafficAllocation = append(experiment.TrafficAllocation, entities.Range{
431+
EntityID: everyoneElseVariation.ID,
432+
EndOfRange: 10000,
433+
})
434+
435+
// Update the experiment in the map
436+
experimentMap[experimentID] = experiment
437+
}
438+
}
439+
}
440+
441+
// getEveryoneElseVariation retrieves the first variation from the last experiment
442+
// in the flag's rollout (the "everyone else" rule).
443+
func getEveryoneElseVariation(feature entities.Feature) *entities.Variation {
444+
rollout := feature.Rollout
445+
if rollout.ID == "" {
446+
return nil
447+
}
448+
if len(rollout.Experiments) == 0 {
449+
return nil
450+
}
451+
everyoneElseRule := rollout.Experiments[len(rollout.Experiments)-1]
452+
if len(everyoneElseRule.Variations) == 0 {
453+
return nil
454+
}
455+
// Get the first variation from the everyone else rule
456+
for _, variation := range everyoneElseRule.Variations {
457+
return &variation
458+
}
459+
return nil
460+
}

pkg/config/datafileprojectconfig/entities/entities.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ type Experiment struct {
5151
AudienceIds []string `json:"audienceIds"`
5252
ForcedVariations map[string]string `json:"forcedVariations"`
5353
AudienceConditions interface{} `json:"audienceConditions"`
54+
Type string `json:"type,omitempty"`
5455
Cmab *Cmab `json:"cmab,omitempty"` // is optional
5556
}
5657

0 commit comments

Comments
 (0)