diff --git a/pkg/client/client.go b/pkg/client/client.go index 7b19f178..868e163b 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -1199,6 +1199,9 @@ func (o *OptimizelyClient) getAllOptions(options *decide.Options) decide.Options ExcludeVariables: o.defaultDecideOptions.ExcludeVariables || options.ExcludeVariables, IgnoreUserProfileService: o.defaultDecideOptions.IgnoreUserProfileService || options.IgnoreUserProfileService, IncludeReasons: o.defaultDecideOptions.IncludeReasons || options.IncludeReasons, + IgnoreCMABCache: o.defaultDecideOptions.IgnoreCMABCache || options.IgnoreCMABCache, + ResetCMABCache: o.defaultDecideOptions.ResetCMABCache || options.ResetCMABCache, + InvalidateUserCMABCache: o.defaultDecideOptions.InvalidateUserCMABCache || options.InvalidateUserCMABCache, } } @@ -1252,5 +1255,14 @@ func isNil(v interface{}) bool { func (o *OptimizelyClient) handleDecisionServiceError(err error, key string, userContext OptimizelyUserContext) OptimizelyDecision { o.logger.Warning(fmt.Sprintf(`Received error while making a decision for feature %q: %s`, key, err)) - return NewErrorDecision(key, userContext, err) + // Return the error decision with the correct format for decision fields + return OptimizelyDecision{ + FlagKey: key, + UserContext: userContext, + VariationKey: "", + RuleKey: "", + Enabled: false, + Variables: optimizelyjson.NewOptimizelyJSONfromMap(map[string]interface{}{}), + Reasons: []string{err.Error()}, + } } diff --git a/pkg/client/client_test.go b/pkg/client/client_test.go index 16f91bd0..474b32a0 100644 --- a/pkg/client/client_test.go +++ b/pkg/client/client_test.go @@ -3186,6 +3186,36 @@ func (s *ClientTestSuiteTrackNotification) TestRemoveOnTrackThrowsErrorWhenRemov mockNotificationCenter.AssertExpectations(s.T()) } +func TestOptimizelyClient_handleDecisionServiceError(t *testing.T) { + // Create the client + client := &OptimizelyClient{ + logger: logging.GetLogger("", ""), + } + + // Create a CMAB error + cmabErrorMessage := "Failed to fetch CMAB data for experiment exp_1." + cmabError := fmt.Errorf(cmabErrorMessage) + + // Create a user context - needs to match the signature expected by handleDecisionServiceError + testUserContext := OptimizelyUserContext{ + UserID: "test_user", + Attributes: map[string]interface{}{}, + } + + // Call the error handler directly + decision := client.handleDecisionServiceError(cmabError, "test_flag", testUserContext) + + // Verify the decision is correctly formatted + assert.False(t, decision.Enabled) + assert.Equal(t, "", decision.VariationKey) // Should be empty string, not nil + assert.Equal(t, "", decision.RuleKey) // Should be empty string, not nil + assert.Contains(t, decision.Reasons, cmabErrorMessage) + + // Check that reasons contains exactly the expected message + assert.Equal(t, 1, len(decision.Reasons), "Reasons array should have exactly one item") + assert.Equal(t, cmabErrorMessage, decision.Reasons[0], "Error message should be added verbatim") +} + func TestClientTestSuiteAB(t *testing.T) { suite.Run(t, new(ClientTestSuiteAB)) } diff --git a/pkg/cmab/errors.go b/pkg/cmab/errors.go new file mode 100644 index 00000000..3efa94fc --- /dev/null +++ b/pkg/cmab/errors.go @@ -0,0 +1,21 @@ +/**************************************************************************** + * Copyright 2025, 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 * + * * + * http://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 cmab to define cmab errors// +package cmab + +// CmabFetchFailed is the error message format for CMAB fetch failures +const CmabFetchFailed = "failed to fetch CMAB data for experiment %s" diff --git a/pkg/cmab/service.go b/pkg/cmab/service.go index 4fb1ad82..9cd04bfa 100644 --- a/pkg/cmab/service.go +++ b/pkg/cmab/service.go @@ -137,8 +137,7 @@ func (s *DefaultCmabService) GetDecision( // Fetch new decision decision, err := s.fetchDecision(ruleID, userContext.ID, filteredAttributes) if err != nil { - decision.Reasons = append(reasons, decision.Reasons...) - return decision, fmt.Errorf("CMAB API error: %w", err) + return Decision{Reasons: reasons}, fmt.Errorf("CMAB API error: %w", err) } // Cache the decision diff --git a/pkg/decision/experiment_cmab_service.go b/pkg/decision/experiment_cmab_service.go index 817836f5..2d66b02c 100644 --- a/pkg/decision/experiment_cmab_service.go +++ b/pkg/decision/experiment_cmab_service.go @@ -159,9 +159,8 @@ func (s *ExperimentCmabService) GetDecision(decisionContext ExperimentDecisionCo // Get CMAB decision cmabDecision, err := s.cmabService.GetDecision(projectConfig, userContext, experiment.ID, options) if err != nil { - message := fmt.Sprintf("Failed to get CMAB decision: %v", err) - decisionReasons.AddInfo(message) - return decision, decisionReasons, fmt.Errorf("failed to get CMAB decision: %w", err) + // Format the error correctly with the experiment key we already have + return decision, decisionReasons, fmt.Errorf(cmab.CmabFetchFailed, experiment.Key) } // Find variation by ID diff --git a/pkg/decision/experiment_cmab_service_test.go b/pkg/decision/experiment_cmab_service_test.go index a73ad252..1bd670a5 100644 --- a/pkg/decision/experiment_cmab_service_test.go +++ b/pkg/decision/experiment_cmab_service_test.go @@ -307,7 +307,7 @@ func (s *ExperimentCmabTestSuite) TestGetDecisionWithCmabServiceError() { // Mock CMAB service to return error s.mockCmabService.On("GetDecision", s.mockProjectConfig, s.testUserContext, "cmab_exp_1", s.options). - Return(cmab.Decision{}, errors.New("CMAB service error")) + Return(cmab.Decision{}, errors.New("failed to fetch CMAB data for experiment")) // Create CMAB service with mocked dependencies (same pattern as TestGetDecisionSuccess) cmabService := &ExperimentCmabService{ @@ -320,7 +320,7 @@ func (s *ExperimentCmabTestSuite) TestGetDecisionWithCmabServiceError() { // Should return the CMAB service error s.Error(err) - s.Contains(err.Error(), "CMAB service error") + s.Contains(err.Error(), "failed to fetch CMAB data for experiment") s.Nil(decision.Variation) // No variation when error occurs s.mockExperimentBucketer.AssertExpectations(s.T())