Skip to content

Commit f88f90b

Browse files
raju-opticlaude
andcommitted
[FSSDK-12275] Add experiment type filtering to skip unsupported types during flag decision
- Add Type field to Experiment struct in entities/experiment.go - Define supportedExperimentTypes map with values: a/b, mab, cmab, feature_rollouts - Update FeatureExperimentService.GetDecision to check experiment types - Skip evaluation only if Type is non-empty but not in supported list - Evaluate normally if Type is empty or is supported - Add comprehensive unit tests covering all scenarios Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent db814dd commit f88f90b

File tree

3 files changed

+239
-0
lines changed

3 files changed

+239
-0
lines changed

pkg/decision/feature_experiment_service.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,14 @@ import (
2525
"github.com/optimizely/go-sdk/v2/pkg/logging"
2626
)
2727

28+
// supportedExperimentTypes defines the experiment types that the SDK can handle
29+
var supportedExperimentTypes = map[string]bool{
30+
"a/b": true,
31+
"mab": true,
32+
"cmab": true,
33+
"feature_rollouts": true,
34+
}
35+
2836
// FeatureExperimentService helps evaluate feature test associated with the feature
2937
type FeatureExperimentService struct {
3038
compositeExperimentService ExperimentService
@@ -46,6 +54,18 @@ func (f FeatureExperimentService) GetDecision(decisionContext FeatureDecisionCon
4654
// @TODO this can be improved by getting group ID first and determining experiment and then bucketing in experiment
4755
for _, featureExperiment := range feature.FeatureExperiments {
4856

57+
// Check if experiment type is supported
58+
if featureExperiment.Type != "" && !supportedExperimentTypes[featureExperiment.Type] {
59+
message := fmt.Sprintf(
60+
"Experiment %q has unsupported type %q. Skipping to next experiment.",
61+
featureExperiment.Key,
62+
featureExperiment.Type,
63+
)
64+
f.logger.Debug(message)
65+
reasons.AddInfo(message)
66+
continue
67+
}
68+
4969
// Checking for forced decision
5070
if decisionContext.ForcedDecisionService != nil {
5171
forcedDecision, _reasons, err := decisionContext.ForcedDecisionService.FindValidatedForcedDecision(decisionContext.ProjectConfig, OptimizelyDecisionContext{FlagKey: feature.Key, RuleKey: featureExperiment.Key}, options)

pkg/decision/feature_experiment_service_test.go

Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,224 @@ func (s *FeatureExperimentServiceTestSuite) TestGetDecisionWithCmabError() {
295295
s.mockExperimentService.AssertExpectations(s.T())
296296
}
297297

298+
func (s *FeatureExperimentServiceTestSuite) TestGetDecisionSkipsUnsupportedExperimentType() {
299+
testUserContext := entities.UserContext{
300+
ID: "test_user_1",
301+
}
302+
303+
// Create experiment with unsupported type
304+
unsupportedExp := testExp1113
305+
unsupportedExp.Type = "unsupported_type"
306+
307+
// Create feature with the unsupported experiment
308+
featureWithUnsupportedExp := testFeat3335
309+
featureWithUnsupportedExp.FeatureExperiments = []entities.Experiment{unsupportedExp}
310+
311+
testFeatureDecisionContext := FeatureDecisionContext{
312+
Feature: &featureWithUnsupportedExp,
313+
ProjectConfig: s.mockConfig,
314+
}
315+
316+
featureExperimentService := &FeatureExperimentService{
317+
compositeExperimentService: s.mockExperimentService,
318+
logger: logging.GetLogger("sdkKey", "FeatureExperimentService"),
319+
}
320+
321+
decision, _, err := featureExperimentService.GetDecision(testFeatureDecisionContext, testUserContext, s.options)
322+
323+
// Should return empty decision since experiment was skipped
324+
s.Equal(FeatureDecision{}, decision)
325+
s.NoError(err)
326+
}
327+
328+
func (s *FeatureExperimentServiceTestSuite) TestGetDecisionEvaluatesExperimentWithSupportedTypeAB() {
329+
testUserContext := entities.UserContext{
330+
ID: "test_user_1",
331+
}
332+
333+
// Create experiment with supported type "a/b"
334+
abExp := testExp1113
335+
abExp.Type = "a/b"
336+
337+
expectedVariation := abExp.Variations["2223"]
338+
returnExperimentDecision := ExperimentDecision{
339+
Variation: &expectedVariation,
340+
}
341+
testExperimentDecisionContext := ExperimentDecisionContext{
342+
Experiment: &abExp,
343+
ProjectConfig: s.mockConfig,
344+
}
345+
s.mockExperimentService.On("GetDecision", testExperimentDecisionContext, testUserContext, s.options).Return(returnExperimentDecision, s.reasons, nil)
346+
347+
// Create feature with the a/b experiment
348+
featureWithABExp := testFeat3335
349+
featureWithABExp.FeatureExperiments = []entities.Experiment{abExp}
350+
351+
testFeatureDecisionContext := FeatureDecisionContext{
352+
Feature: &featureWithABExp,
353+
ProjectConfig: s.mockConfig,
354+
}
355+
356+
featureExperimentService := &FeatureExperimentService{
357+
compositeExperimentService: s.mockExperimentService,
358+
logger: logging.GetLogger("sdkKey", "FeatureExperimentService"),
359+
}
360+
361+
expectedFeatureDecision := FeatureDecision{
362+
Experiment: *testExperimentDecisionContext.Experiment,
363+
Variation: &expectedVariation,
364+
Source: FeatureTest,
365+
}
366+
367+
decision, _, err := featureExperimentService.GetDecision(testFeatureDecisionContext, testUserContext, s.options)
368+
s.Equal(expectedFeatureDecision, decision)
369+
s.NoError(err)
370+
s.mockExperimentService.AssertExpectations(s.T())
371+
}
372+
373+
func (s *FeatureExperimentServiceTestSuite) TestGetDecisionEvaluatesExperimentWithSupportedTypeMAB() {
374+
testUserContext := entities.UserContext{
375+
ID: "test_user_1",
376+
}
377+
378+
// Create experiment with supported type "mab"
379+
mabExp := testExp1113
380+
mabExp.Type = "mab"
381+
382+
expectedVariation := mabExp.Variations["2223"]
383+
returnExperimentDecision := ExperimentDecision{
384+
Variation: &expectedVariation,
385+
}
386+
testExperimentDecisionContext := ExperimentDecisionContext{
387+
Experiment: &mabExp,
388+
ProjectConfig: s.mockConfig,
389+
}
390+
s.mockExperimentService.On("GetDecision", testExperimentDecisionContext, testUserContext, s.options).Return(returnExperimentDecision, s.reasons, nil)
391+
392+
// Create feature with the mab experiment
393+
featureWithMABExp := testFeat3335
394+
featureWithMABExp.FeatureExperiments = []entities.Experiment{mabExp}
395+
396+
testFeatureDecisionContext := FeatureDecisionContext{
397+
Feature: &featureWithMABExp,
398+
ProjectConfig: s.mockConfig,
399+
}
400+
401+
featureExperimentService := &FeatureExperimentService{
402+
compositeExperimentService: s.mockExperimentService,
403+
logger: logging.GetLogger("sdkKey", "FeatureExperimentService"),
404+
}
405+
406+
expectedFeatureDecision := FeatureDecision{
407+
Experiment: *testExperimentDecisionContext.Experiment,
408+
Variation: &expectedVariation,
409+
Source: FeatureTest,
410+
}
411+
412+
decision, _, err := featureExperimentService.GetDecision(testFeatureDecisionContext, testUserContext, s.options)
413+
s.Equal(expectedFeatureDecision, decision)
414+
s.NoError(err)
415+
s.mockExperimentService.AssertExpectations(s.T())
416+
}
417+
418+
func (s *FeatureExperimentServiceTestSuite) TestGetDecisionEvaluatesExperimentWithEmptyType() {
419+
testUserContext := entities.UserContext{
420+
ID: "test_user_1",
421+
}
422+
423+
// Create experiment with empty type (backward compatible)
424+
emptyTypeExp := testExp1113
425+
emptyTypeExp.Type = ""
426+
427+
expectedVariation := emptyTypeExp.Variations["2223"]
428+
returnExperimentDecision := ExperimentDecision{
429+
Variation: &expectedVariation,
430+
}
431+
testExperimentDecisionContext := ExperimentDecisionContext{
432+
Experiment: &emptyTypeExp,
433+
ProjectConfig: s.mockConfig,
434+
}
435+
s.mockExperimentService.On("GetDecision", testExperimentDecisionContext, testUserContext, s.options).Return(returnExperimentDecision, s.reasons, nil)
436+
437+
// Create feature with the empty type experiment
438+
featureWithEmptyTypeExp := testFeat3335
439+
featureWithEmptyTypeExp.FeatureExperiments = []entities.Experiment{emptyTypeExp}
440+
441+
testFeatureDecisionContext := FeatureDecisionContext{
442+
Feature: &featureWithEmptyTypeExp,
443+
ProjectConfig: s.mockConfig,
444+
}
445+
446+
featureExperimentService := &FeatureExperimentService{
447+
compositeExperimentService: s.mockExperimentService,
448+
logger: logging.GetLogger("sdkKey", "FeatureExperimentService"),
449+
}
450+
451+
expectedFeatureDecision := FeatureDecision{
452+
Experiment: *testExperimentDecisionContext.Experiment,
453+
Variation: &expectedVariation,
454+
Source: FeatureTest,
455+
}
456+
457+
decision, _, err := featureExperimentService.GetDecision(testFeatureDecisionContext, testUserContext, s.options)
458+
s.Equal(expectedFeatureDecision, decision)
459+
s.NoError(err)
460+
s.mockExperimentService.AssertExpectations(s.T())
461+
}
462+
463+
func (s *FeatureExperimentServiceTestSuite) TestGetDecisionSkipsUnsupportedAndEvaluatesSupported() {
464+
testUserContext := entities.UserContext{
465+
ID: "test_user_1",
466+
}
467+
468+
// Create unsupported experiment
469+
unsupportedExp := testExp1113
470+
unsupportedExp.Type = "unsupported_type"
471+
unsupportedExp.Key = "unsupported_exp"
472+
473+
// Create supported experiment
474+
supportedExp := testExp1113
475+
supportedExp.Type = "a/b"
476+
supportedExp.Key = "supported_exp"
477+
478+
expectedVariation := supportedExp.Variations["2223"]
479+
returnExperimentDecision := ExperimentDecision{
480+
Variation: &expectedVariation,
481+
}
482+
testExperimentDecisionContext := ExperimentDecisionContext{
483+
Experiment: &supportedExp,
484+
ProjectConfig: s.mockConfig,
485+
}
486+
s.mockExperimentService.On("GetDecision", testExperimentDecisionContext, testUserContext, s.options).Return(returnExperimentDecision, s.reasons, nil)
487+
488+
// Create feature with both experiments
489+
featureWithBothExp := testFeat3335
490+
featureWithBothExp.FeatureExperiments = []entities.Experiment{unsupportedExp, supportedExp}
491+
492+
testFeatureDecisionContext := FeatureDecisionContext{
493+
Feature: &featureWithBothExp,
494+
ProjectConfig: s.mockConfig,
495+
}
496+
497+
featureExperimentService := &FeatureExperimentService{
498+
compositeExperimentService: s.mockExperimentService,
499+
logger: logging.GetLogger("sdkKey", "FeatureExperimentService"),
500+
}
501+
502+
expectedFeatureDecision := FeatureDecision{
503+
Experiment: *testExperimentDecisionContext.Experiment,
504+
Variation: &expectedVariation,
505+
Source: FeatureTest,
506+
}
507+
508+
decision, _, err := featureExperimentService.GetDecision(testFeatureDecisionContext, testUserContext, s.options)
509+
510+
// Should skip unsupported and evaluate supported
511+
s.Equal(expectedFeatureDecision, decision)
512+
s.NoError(err)
513+
s.mockExperimentService.AssertExpectations(s.T())
514+
}
515+
298516
func TestFeatureExperimentServiceTestSuite(t *testing.T) {
299517
suite.Run(t, new(FeatureExperimentServiceTestSuite))
300518
}

pkg/entities/experiment.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ type Experiment struct {
4545
AudienceConditionTree *TreeNode
4646
Whitelist map[string]string
4747
IsFeatureExperiment bool
48+
Type string
4849
Cmab *Cmab
4950
}
5051

0 commit comments

Comments
 (0)