Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions pkg/client/factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,12 @@ func convertDecideOptions(options []decide.OptimizelyDecideOptions) *decide.Opti
finalOptions.IncludeReasons = true
case decide.ExcludeVariables:
finalOptions.ExcludeVariables = true
case decide.IgnoreCMABCache:
finalOptions.IgnoreCMABCache = true
case decide.ResetCMABCache:
finalOptions.ResetCMABCache = true
case decide.InvalidateUserCMABCache:
finalOptions.InvalidateUserCMABCache = true
}
}
return &finalOptions
Expand Down
49 changes: 49 additions & 0 deletions pkg/client/factory_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -385,3 +385,52 @@ func TestOptimizelyClientWithNoTracer(t *testing.T) {
tracer := optimizelyClient.tracer.(*tracing.NoopTracer)
assert.NotNil(t, tracer)
}

func TestConvertDecideOptionsWithCMABOptions(t *testing.T) {
// Test with IgnoreCMABCache option
options := []decide.OptimizelyDecideOptions{decide.IgnoreCMABCache}
convertedOptions := convertDecideOptions(options)
assert.True(t, convertedOptions.IgnoreCMABCache)
assert.False(t, convertedOptions.ResetCMABCache)
assert.False(t, convertedOptions.InvalidateUserCMABCache)

// Test with ResetCMABCache option
options = []decide.OptimizelyDecideOptions{decide.ResetCMABCache}
convertedOptions = convertDecideOptions(options)
assert.False(t, convertedOptions.IgnoreCMABCache)
assert.True(t, convertedOptions.ResetCMABCache)
assert.False(t, convertedOptions.InvalidateUserCMABCache)

// Test with InvalidateUserCMABCache option
options = []decide.OptimizelyDecideOptions{decide.InvalidateUserCMABCache}
convertedOptions = convertDecideOptions(options)
assert.False(t, convertedOptions.IgnoreCMABCache)
assert.False(t, convertedOptions.ResetCMABCache)
assert.True(t, convertedOptions.InvalidateUserCMABCache)

// Test with all CMAB options
options = []decide.OptimizelyDecideOptions{
decide.IgnoreCMABCache,
decide.ResetCMABCache,
decide.InvalidateUserCMABCache,
}
convertedOptions = convertDecideOptions(options)
assert.True(t, convertedOptions.IgnoreCMABCache)
assert.True(t, convertedOptions.ResetCMABCache)
assert.True(t, convertedOptions.InvalidateUserCMABCache)

// Test with CMAB options mixed with other options
options = []decide.OptimizelyDecideOptions{
decide.DisableDecisionEvent,
decide.IgnoreCMABCache,
decide.EnabledFlagsOnly,
decide.ResetCMABCache,
decide.InvalidateUserCMABCache,
}
convertedOptions = convertDecideOptions(options)
assert.True(t, convertedOptions.DisableDecisionEvent)
assert.True(t, convertedOptions.EnabledFlagsOnly)
assert.True(t, convertedOptions.IgnoreCMABCache)
assert.True(t, convertedOptions.ResetCMABCache)
assert.True(t, convertedOptions.InvalidateUserCMABCache)
}
9 changes: 9 additions & 0 deletions pkg/config/datafileprojectconfig/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,15 @@ func (c DatafileProjectConfig) GetExperimentByKey(experimentKey string) (entitie
return entities.Experiment{}, fmt.Errorf(`experiment with key "%s" not found`, experimentKey)
}

// GetExperimentByID returns the experiment with the given ID
func (c DatafileProjectConfig) GetExperimentByID(experimentID string) (entities.Experiment, error) {
if experiment, ok := c.experimentMap[experimentID]; ok {
return experiment, nil
}

return entities.Experiment{}, fmt.Errorf(`experiment with ID "%s" not found`, experimentID)
}

// GetGroupByID returns the group with the given ID
func (c DatafileProjectConfig) GetGroupByID(groupID string) (entities.Group, error) {
if group, ok := c.groupMap[groupID]; ok {
Expand Down
30 changes: 29 additions & 1 deletion pkg/config/datafileprojectconfig/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -590,7 +590,7 @@ func TestCmabExperiments(t *testing.T) {
experiments := datafileJSON["experiments"].([]interface{})
exp0 := experiments[0].(map[string]interface{})
exp0["cmab"] = map[string]interface{}{
"attributes": []string{"808797688", "808797689"},
"attributeIds": []string{"808797688", "808797689"},
"trafficAllocation": 5000, // Changed from array to integer
}

Expand Down Expand Up @@ -655,6 +655,34 @@ func TestCmabExperimentsNil(t *testing.T) {
}
}

func TestGetExperimentByID(t *testing.T) {
// Create a test config with some experiments
testConfig := DatafileProjectConfig{
experimentMap: map[string]entities.Experiment{
"exp1": {ID: "exp1", Key: "experiment_1"},
"exp2": {ID: "exp2", Key: "experiment_2"},
},
}

// Test getting an experiment that exists
experiment, err := testConfig.GetExperimentByID("exp1")
assert.NoError(t, err)
assert.Equal(t, "exp1", experiment.ID)
assert.Equal(t, "experiment_1", experiment.Key)

// Test getting another experiment that exists
experiment, err = testConfig.GetExperimentByID("exp2")
assert.NoError(t, err)
assert.Equal(t, "exp2", experiment.ID)
assert.Equal(t, "experiment_2", experiment.Key)

// Test getting an experiment that doesn't exist
experiment, err = testConfig.GetExperimentByID("non_existent")
assert.Error(t, err)
assert.Equal(t, `experiment with ID "non_existent" not found`, err.Error())
assert.Equal(t, entities.Experiment{}, experiment)
}

func TestGetAttributeKeyByID(t *testing.T) {
// Setup
id := "id"
Expand Down
2 changes: 1 addition & 1 deletion pkg/config/datafileprojectconfig/entities/entities.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ type Attribute struct {
// It contains a list of attribute IDs that are used for the CMAB algorithm and
// traffic allocation settings for the CMAB implementation.
type Cmab struct {
AttributeIds []string `json:"attributes"`
AttributeIds []string `json:"attributeIds"`
TrafficAllocation int `json:"trafficAllocation"`
}

Expand Down
4 changes: 3 additions & 1 deletion pkg/config/interface.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/****************************************************************************
* Copyright 2019-2022, Optimizely, Inc. and contributors *
* Copyright 2019-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. *
Expand Down Expand Up @@ -31,6 +31,8 @@ type ProjectConfig interface {
GetAnonymizeIP() bool
GetAttributeID(id string) string // returns "" if there is no id
GetAttributeByKey(key string) (entities.Attribute, error)
GetAttributeKeyByID(id string) (string, error) // method is intended for internal use only
GetExperimentByID(id string) (entities.Experiment, error) // method is intended for internal use only
GetAudienceList() (audienceList []entities.Audience)
GetAudienceByID(string) (entities.Audience, error)
GetAudienceMap() map[string]entities.Audience
Expand Down
17 changes: 16 additions & 1 deletion pkg/decide/decide_options.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/****************************************************************************
* Copyright 2020-2021, Optimizely, Inc. and contributors *
* Copyright 2020-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. *
Expand Down Expand Up @@ -33,6 +33,12 @@ const (
IncludeReasons OptimizelyDecideOptions = "INCLUDE_REASONS"
// ExcludeVariables when set, excludes variable values from the decision result.
ExcludeVariables OptimizelyDecideOptions = "EXCLUDE_VARIABLES"
// IgnoreCMABCache instructs the SDK to ignore the CMAB cache and make a fresh request
IgnoreCMABCache OptimizelyDecideOptions = "IGNORE_CMAB_CACHE"
// ResetCMABCache instructs the SDK to reset the entire CMAB cache
ResetCMABCache OptimizelyDecideOptions = "RESET_CMAB_CACHE"
// InvalidateUserCMABCache instructs the SDK to invalidate CMAB cache entries for the current user
InvalidateUserCMABCache OptimizelyDecideOptions = "INVALIDATE_USER_CMAB_CACHE"
)

// Options defines options for controlling flag decisions.
Expand All @@ -42,6 +48,9 @@ type Options struct {
IgnoreUserProfileService bool
IncludeReasons bool
ExcludeVariables bool
IgnoreCMABCache bool
ResetCMABCache bool
InvalidateUserCMABCache bool
}

// TranslateOptions converts string options array to array of OptimizelyDecideOptions
Expand All @@ -59,6 +68,12 @@ func TranslateOptions(options []string) ([]OptimizelyDecideOptions, error) {
decideOptions = append(decideOptions, ExcludeVariables)
case IncludeReasons:
decideOptions = append(decideOptions, IncludeReasons)
case IgnoreCMABCache:
decideOptions = append(decideOptions, IgnoreCMABCache)
case ResetCMABCache:
decideOptions = append(decideOptions, ResetCMABCache)
case InvalidateUserCMABCache:
decideOptions = append(decideOptions, InvalidateUserCMABCache)
default:
return []OptimizelyDecideOptions{}, errors.New("invalid option: " + val)
}
Expand Down
46 changes: 45 additions & 1 deletion pkg/decide/decide_options_test.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/****************************************************************************
* Copyright 2021, Optimizely, Inc. and contributors *
* Copyright 2021-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. *
Expand Down Expand Up @@ -68,3 +68,47 @@ func TestTranslateOptionsInvalidCases(t *testing.T) {
assert.Equal(t, fmt.Errorf("invalid option: %v", options[0]), err)
assert.Len(t, translatedOptions, 0)
}

// TestTranslateOptionsCMABOptions tests the new CMAB-related options
func TestTranslateOptionsCMABOptions(t *testing.T) {
// Test IGNORE_CMAB_CACHE option
options := []string{"IGNORE_CMAB_CACHE"}
translatedOptions, err := TranslateOptions(options)
assert.NoError(t, err)
assert.Len(t, translatedOptions, 1)
assert.Equal(t, IgnoreCMABCache, translatedOptions[0])

// Test RESET_CMAB_CACHE option
options = []string{"RESET_CMAB_CACHE"}
translatedOptions, err = TranslateOptions(options)
assert.NoError(t, err)
assert.Len(t, translatedOptions, 1)
assert.Equal(t, ResetCMABCache, translatedOptions[0])

// Test INVALIDATE_USER_CMAB_CACHE option
options = []string{"INVALIDATE_USER_CMAB_CACHE"}
translatedOptions, err = TranslateOptions(options)
assert.NoError(t, err)
assert.Len(t, translatedOptions, 1)
assert.Equal(t, InvalidateUserCMABCache, translatedOptions[0])

// Test all CMAB options together
options = []string{"IGNORE_CMAB_CACHE", "RESET_CMAB_CACHE", "INVALIDATE_USER_CMAB_CACHE"}
translatedOptions, err = TranslateOptions(options)
assert.NoError(t, err)
assert.Len(t, translatedOptions, 3)
assert.Equal(t, IgnoreCMABCache, translatedOptions[0])
assert.Equal(t, ResetCMABCache, translatedOptions[1])
assert.Equal(t, InvalidateUserCMABCache, translatedOptions[2])

// Test CMAB options with other options
options = []string{"DISABLE_DECISION_EVENT", "IGNORE_CMAB_CACHE", "ENABLED_FLAGS_ONLY", "RESET_CMAB_CACHE", "INVALIDATE_USER_CMAB_CACHE"}
translatedOptions, err = TranslateOptions(options)
assert.NoError(t, err)
assert.Len(t, translatedOptions, 5)
assert.Equal(t, DisableDecisionEvent, translatedOptions[0])
assert.Equal(t, IgnoreCMABCache, translatedOptions[1])
assert.Equal(t, EnabledFlagsOnly, translatedOptions[2])
assert.Equal(t, ResetCMABCache, translatedOptions[3])
assert.Equal(t, InvalidateUserCMABCache, translatedOptions[4])
}
60 changes: 60 additions & 0 deletions pkg/decision/cmab.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/****************************************************************************
* 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 decision provides CMAB decision service interfaces and types
package decision

import (
"github.com/optimizely/go-sdk/v2/pkg/config"
"github.com/optimizely/go-sdk/v2/pkg/decide"
"github.com/optimizely/go-sdk/v2/pkg/entities"
)

// CmabDecision represents a decision from the CMAB service
type CmabDecision struct {
VariationID string
CmabUUID string
Reasons []string
}

// CmabCacheValue represents a cached CMAB decision with attribute hash
type CmabCacheValue struct {
AttributesHash string
VariationID string
CmabUUID string
}

// CmabService defines the interface for CMAB decision services
type CmabService interface {
// GetDecision returns a CMAB decision for the given rule and user context
GetDecision(
projectConfig config.ProjectConfig,
userContext entities.UserContext,
ruleID string,
options *decide.Options,
) (CmabDecision, error)
}

// CmabClient defines the interface for CMAB API clients
type CmabClient interface {
// FetchDecision fetches a decision from the CMAB API
FetchDecision(
ruleID string,
userID string,
attributes map[string]interface{},
cmabUUID string,
) (string, error)
}
Loading