Skip to content

Commit 1e92f00

Browse files
committed
Add more error handling to feature exper and composite feature service
1 parent 45d51cf commit 1e92f00

File tree

4 files changed

+115
-3
lines changed

4 files changed

+115
-3
lines changed

pkg/decision/composite_feature_service.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
package decision
1919

2020
import (
21+
"strings"
22+
2123
"github.com/optimizely/go-sdk/v2/pkg/decide"
2224
"github.com/optimizely/go-sdk/v2/pkg/entities"
2325
"github.com/optimizely/go-sdk/v2/pkg/logging"
@@ -51,6 +53,19 @@ func (f CompositeFeatureService) GetDecision(decisionContext FeatureDecisionCont
5153
reasons.Append(decisionReasons)
5254
if err != nil {
5355
f.logger.Debug(err.Error())
56+
// Check if this is a CMAB error - if so, stop the loop and return empty decision
57+
if strings.Contains(err.Error(), "Failed to fetch CMAB data") {
58+
return FeatureDecision{}, reasons, nil // Return empty decision for CMAB errors
59+
}
60+
}
61+
62+
// Also check for CMAB errors in decision reasons (when err is nil)
63+
if decisionReasons != nil {
64+
for _, reason := range decisionReasons.ToReport() {
65+
if strings.Contains(reason, "Failed to fetch CMAB data") {
66+
return FeatureDecision{}, reasons, nil
67+
}
68+
}
5469
}
5570

5671
if featureDecision.Variation != nil && err == nil {

pkg/decision/composite_feature_service_test.go

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,15 +109,16 @@ func (s *CompositeFeatureServiceTestSuite) TestGetDecisionFallthrough() {
109109
}
110110

111111
func (s *CompositeFeatureServiceTestSuite) TestGetDecisionReturnsError() {
112-
// test that we move onto the next decision service if an inner service returns an error
112+
// test that we move onto the next decision service if an inner service returns a non-CMAB error
113113
testUserContext := entities.UserContext{
114114
ID: "test_user_1",
115115
}
116116

117117
shouldBeIgnoredDecision := FeatureDecision{
118118
Variation: &testExp1113Var2223,
119119
}
120-
s.mockFeatureService.On("GetDecision", s.testFeatureDecisionContext, testUserContext, s.options).Return(shouldBeIgnoredDecision, s.reasons, errors.New("Error making decision"))
120+
// Use a non-CMAB error to ensure fallthrough still works for other errors
121+
s.mockFeatureService.On("GetDecision", s.testFeatureDecisionContext, testUserContext, s.options).Return(shouldBeIgnoredDecision, s.reasons, errors.New("Generic experiment error"))
121122

122123
expectedDecision := FeatureDecision{
123124
Variation: &testExp1113Var2224,
@@ -165,6 +166,38 @@ func (s *CompositeFeatureServiceTestSuite) TestGetDecisionReturnsLastDecisionWit
165166
s.mockFeatureService2.AssertExpectations(s.T())
166167
}
167168

169+
func (s *CompositeFeatureServiceTestSuite) TestGetDecisionWithCmabError() {
170+
// Test that CMAB errors are terminal and don't fall through to rollout service
171+
testUserContext := entities.UserContext{
172+
ID: "test_user_1",
173+
}
174+
175+
// Mock the first service (FeatureExperimentService) to return a CMAB error
176+
cmabError := errors.New("Failed to fetch CMAB data for experiment exp_1.")
177+
emptyDecision := FeatureDecision{}
178+
s.mockFeatureService.On("GetDecision", s.testFeatureDecisionContext, testUserContext, s.options).Return(emptyDecision, s.reasons, cmabError)
179+
180+
// The second service (RolloutService) should NOT be called for CMAB errors
181+
182+
compositeFeatureService := &CompositeFeatureService{
183+
featureServices: []FeatureService{
184+
s.mockFeatureService,
185+
s.mockFeatureService2,
186+
},
187+
logger: logging.GetLogger("sdkKey", "CompositeFeatureService"),
188+
}
189+
190+
decision, _, err := compositeFeatureService.GetDecision(s.testFeatureDecisionContext, testUserContext, s.options)
191+
192+
// CMAB errors should result in empty decision with no error
193+
s.Equal(FeatureDecision{}, decision)
194+
s.NoError(err, "CMAB errors should not propagate as Go errors")
195+
196+
s.mockFeatureService.AssertExpectations(s.T())
197+
// Verify that the rollout service was NOT called
198+
s.mockFeatureService2.AssertNotCalled(s.T(), "GetDecision")
199+
}
200+
168201
func (s *CompositeFeatureServiceTestSuite) TestNewCompositeFeatureService() {
169202
// Assert that the service is instantiated with the correct child services in the right order
170203
compositeExperimentService := NewCompositeExperimentService("")

pkg/decision/feature_experiment_service.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ func (f FeatureExperimentService) GetDecision(decisionContext FeatureDecisionCon
7979
// Handle CMAB experiment errors - they should terminate the decision process
8080
if err != nil && experiment.Cmab != nil {
8181
// For CMAB experiments, errors should prevent fallback to other experiments
82-
// Return empty FeatureDecision (enabled: false, variation_key: null, rule_key: null)
82+
// The error is already in reasons from decisionReasons, so return nil error
8383
return FeatureDecision{}, reasons, nil
8484
}
8585

pkg/decision/feature_experiment_service_test.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package decision
1818

1919
import (
20+
"errors"
2021
"testing"
2122

2223
"github.com/optimizely/go-sdk/v2/pkg/decide"
@@ -230,6 +231,69 @@ func (s *FeatureExperimentServiceTestSuite) TestGetDecisionWithCmabUUID() {
230231
s.mockExperimentService.AssertExpectations(s.T())
231232
}
232233

234+
func (s *FeatureExperimentServiceTestSuite) TestGetDecisionWithCmabError() {
235+
testUserContext := entities.UserContext{
236+
ID: "test_user_1",
237+
}
238+
239+
// Create a NEW CMAB experiment (don't modify existing testExp1113)
240+
cmabExperiment := entities.Experiment{
241+
ID: "cmab_experiment_id",
242+
Key: "cmab_experiment_key",
243+
Cmab: &entities.Cmab{
244+
AttributeIds: []string{"attr1", "attr2"},
245+
TrafficAllocation: 5000, // 50%
246+
},
247+
Variations: testExp1113.Variations, // Reuse variations for simplicity
248+
}
249+
250+
// Setup experiment decision context for CMAB experiment
251+
testExperimentDecisionContext := ExperimentDecisionContext{
252+
Experiment: &cmabExperiment,
253+
ProjectConfig: s.mockConfig,
254+
}
255+
256+
// Mock the experiment service to return a CMAB error
257+
cmabError := errors.New("Failed to fetch CMAB data for experiment cmab_experiment_key.")
258+
s.mockExperimentService.On("GetDecision", testExperimentDecisionContext, testUserContext, s.options).
259+
Return(ExperimentDecision{}, s.reasons, cmabError)
260+
261+
// Create a test feature that uses our CMAB experiment
262+
testFeatureWithCmab := entities.Feature{
263+
ID: "test_feature_cmab",
264+
Key: "test_feature_cmab_key",
265+
FeatureExperiments: []entities.Experiment{
266+
cmabExperiment, // Only our CMAB experiment
267+
},
268+
}
269+
270+
// Create feature decision context with our CMAB feature
271+
testFeatureDecisionContextWithCmab := FeatureDecisionContext{
272+
Feature: &testFeatureWithCmab,
273+
ProjectConfig: s.mockConfig,
274+
Variable: testVariable,
275+
ForcedDecisionService: NewForcedDecisionService("test_user"),
276+
}
277+
278+
// Create service under test
279+
featureExperimentService := &FeatureExperimentService{
280+
compositeExperimentService: s.mockExperimentService,
281+
logger: logging.GetLogger("sdkKey", "FeatureExperimentService"),
282+
}
283+
284+
// Call GetDecision
285+
actualFeatureDecision, actualReasons, err := featureExperimentService.GetDecision(testFeatureDecisionContextWithCmab, testUserContext, s.options)
286+
287+
// Verify that CMAB error results in empty feature decision (not error)
288+
s.NoError(err, "CMAB errors should not propagate as Go errors")
289+
s.Equal(FeatureDecision{}, actualFeatureDecision, "Should return empty FeatureDecision when CMAB fails")
290+
291+
// Verify that reasons include the CMAB error (should be in actualReasons from mock)
292+
s.NotNil(actualReasons, "Decision reasons should not be nil")
293+
294+
s.mockExperimentService.AssertExpectations(s.T())
295+
}
296+
233297
func TestFeatureExperimentServiceTestSuite(t *testing.T) {
234298
suite.Run(t, new(FeatureExperimentServiceTestSuite))
235299
}

0 commit comments

Comments
 (0)