Skip to content

Commit 4134932

Browse files
raju-opticlaude
andcommitted
[FSSDK-12275] Skip unsupported experiment type during flag decision
- Add ExperimentTypes class with supported types constant - Add type parameter to Experiment entity (defaults to None) - Update decision service to skip experiments with unsupported types - Experiments with None type (not set in datafile) are still evaluated - Add unit tests for type skipping, None type evaluation, all supported types, and parsing Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 494e18b commit 4134932

File tree

3 files changed

+126
-1
lines changed

3 files changed

+126
-1
lines changed

optimizely/decision_service.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -762,6 +762,16 @@ def get_decision_for_flag(
762762
experiment = project_config.get_experiment_from_id(experiment_id)
763763

764764
if experiment:
765+
# Skip experiments with unsupported types.
766+
# If the experiment type is None (not set in datafile), we still evaluate it.
767+
# If the experiment type is set but not in the supported list, we skip it.
768+
if experiment.type is not None and experiment.type not in entities.ExperimentTypes.SUPPORTED_TYPES:
769+
self.logger.debug(
770+
f'Skipping experiment "{experiment.key}" with unsupported type '
771+
f'"{experiment.type}" for feature "{feature_flag.key}".'
772+
)
773+
continue
774+
765775
# Check for forced decision
766776
optimizely_decision_context = OptimizelyUserContext.OptimizelyDecisionContext(
767777
feature_flag.key, experiment.key)

optimizely/entities.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
# See the License for the specific language governing permissions and
1212
# limitations under the License.
1313
from __future__ import annotations
14-
from typing import TYPE_CHECKING, Any, Optional, Sequence
14+
from typing import TYPE_CHECKING, Any, Final, Optional, Sequence
1515
from sys import version_info
1616

1717
if version_info < (3, 8):
@@ -72,6 +72,20 @@ def __init__(self, id: str, key: str, experimentIds: list[str], **kwargs: Any):
7272
self.experimentIds = experimentIds
7373

7474

75+
class ExperimentTypes:
76+
"""Supported experiment types recognized by the SDK.
77+
78+
Experiments with a type not in SUPPORTED_TYPES will be skipped during flag decisions.
79+
If an experiment has no type (None), it is still evaluated.
80+
"""
81+
AB = 'a/b'
82+
MAB = 'mab'
83+
CMAB = 'cmab'
84+
FEATURE_ROLLOUTS = 'feature_rollouts'
85+
86+
SUPPORTED_TYPES: Final = frozenset({AB, MAB, CMAB, FEATURE_ROLLOUTS})
87+
88+
7589
class Experiment(BaseEntity):
7690
def __init__(
7791
self,
@@ -87,6 +101,7 @@ def __init__(
87101
groupId: Optional[str] = None,
88102
groupPolicy: Optional[str] = None,
89103
cmab: Optional[CmabDict] = None,
104+
type: Optional[str] = None,
90105
**kwargs: Any
91106
):
92107
self.id = id
@@ -101,6 +116,7 @@ def __init__(
101116
self.groupId = groupId
102117
self.groupPolicy = groupPolicy
103118
self.cmab = cmab
119+
self.type = type
104120

105121
def get_audience_conditions_or_ids(self) -> Sequence[str | list[str]]:
106122
""" Returns audienceConditions if present, otherwise audienceIds. """

tests/test_decision_service.py

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2008,3 +2008,102 @@ def test_get_variation_for_feature_returns_rollout_in_experiment_bucket_range_25
20082008
mock_config_logging.debug.assert_called_with(
20092009
'Assigned bucket 4000 to user with bucketing ID "test_user".')
20102010
mock_generate_bucket_value.assert_called_with("test_user211147")
2011+
2012+
def test_get_decision_for_flag_skips_unsupported_experiment_type(self):
2013+
"""Test that experiments with unsupported types are skipped during flag decisions."""
2014+
2015+
user = optimizely_user_context.OptimizelyUserContext(
2016+
optimizely_client=None, logger=None, user_id="test_user", user_attributes={}
2017+
)
2018+
feature = self.project_config.get_feature_from_key("test_feature_in_experiment")
2019+
2020+
# Get the experiment and set an unsupported type
2021+
experiment = self.project_config.get_experiment_from_key("test_experiment")
2022+
original_type = experiment.type
2023+
experiment.type = "unsupported_type"
2024+
2025+
try:
2026+
with mock.patch(
2027+
"optimizely.decision_service.DecisionService.get_variation"
2028+
) as mock_get_variation:
2029+
result = self.decision_service.get_variation_for_feature(
2030+
self.project_config, feature, user, options=None
2031+
)
2032+
# get_variation should NOT have been called since the experiment type is unsupported
2033+
mock_get_variation.assert_not_called()
2034+
finally:
2035+
experiment.type = original_type
2036+
2037+
def test_get_decision_for_flag_evaluates_experiment_with_none_type(self):
2038+
"""Test that experiments with None type (not set in datafile) are still evaluated."""
2039+
2040+
user = optimizely_user_context.OptimizelyUserContext(
2041+
optimizely_client=None, logger=None, user_id="test_user", user_attributes={}
2042+
)
2043+
feature = self.project_config.get_feature_from_key("test_feature_in_experiment")
2044+
2045+
# Make sure the experiment type is None
2046+
experiment = self.project_config.get_experiment_from_key("test_experiment")
2047+
original_type = experiment.type
2048+
experiment.type = None
2049+
2050+
expected_variation = self.project_config.get_variation_from_id("test_experiment", "111129")
2051+
try:
2052+
with mock.patch(
2053+
"optimizely.decision_service.DecisionService.get_variation",
2054+
return_value={'variation': expected_variation, 'cmab_uuid': None, 'reasons': [], 'error': False},
2055+
) as mock_get_variation:
2056+
result = self.decision_service.get_variation_for_feature(
2057+
self.project_config, feature, user, options=None
2058+
)
2059+
# get_variation SHOULD be called since the experiment type is None
2060+
mock_get_variation.assert_called_once()
2061+
finally:
2062+
experiment.type = original_type
2063+
2064+
def test_get_decision_for_flag_evaluates_supported_experiment_types(self):
2065+
"""Test that experiments with supported types are evaluated."""
2066+
2067+
user = optimizely_user_context.OptimizelyUserContext(
2068+
optimizely_client=None, logger=None, user_id="test_user", user_attributes={}
2069+
)
2070+
feature = self.project_config.get_feature_from_key("test_feature_in_experiment")
2071+
experiment = self.project_config.get_experiment_from_key("test_experiment")
2072+
expected_variation = self.project_config.get_variation_from_id("test_experiment", "111129")
2073+
original_type = experiment.type
2074+
2075+
for exp_type in entities.ExperimentTypes.SUPPORTED_TYPES:
2076+
experiment.type = exp_type
2077+
with mock.patch(
2078+
"optimizely.decision_service.DecisionService.get_variation",
2079+
return_value={'variation': expected_variation, 'cmab_uuid': None, 'reasons': [], 'error': False},
2080+
) as mock_get_variation:
2081+
result = self.decision_service.get_variation_for_feature(
2082+
self.project_config, feature, user, options=None
2083+
)
2084+
mock_get_variation.assert_called_once()
2085+
2086+
experiment.type = original_type
2087+
2088+
def test_experiment_type_field_parsed_from_datafile(self):
2089+
"""Test that the type field is correctly parsed when constructing Experiment entities."""
2090+
# Test with type set
2091+
exp_with_type = entities.Experiment(
2092+
id="123", key="test", status="Running", audienceIds=[],
2093+
variations=[], forcedVariations={}, trafficAllocation=[],
2094+
layerId="1", type="a/b"
2095+
)
2096+
self.assertEqual("a/b", exp_with_type.type)
2097+
2098+
# Test with type not set (default is None)
2099+
exp_without_type = entities.Experiment(
2100+
id="456", key="test2", status="Running", audienceIds=[],
2101+
variations=[], forcedVariations={}, trafficAllocation=[],
2102+
layerId="1"
2103+
)
2104+
self.assertIsNone(exp_without_type.type)
2105+
2106+
def test_supported_experiment_types_values(self):
2107+
"""Test that the supported experiment types contain the expected values."""
2108+
expected_types = {'a/b', 'mab', 'cmab', 'feature_rollouts'}
2109+
self.assertEqual(expected_types, entities.ExperimentTypes.SUPPORTED_TYPES)

0 commit comments

Comments
 (0)