Skip to content

Commit bccad3d

Browse files
committed
add decision service methods to support cmab
1 parent 462a6e8 commit bccad3d

File tree

5 files changed

+449
-4
lines changed

5 files changed

+449
-4
lines changed

pkg/decision/composite_experiment_service.go

+17-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/****************************************************************************
2-
* Copyright 2019-2020, Optimizely, Inc. and contributors *
2+
* Copyright 2019-2025, Optimizely, Inc. and contributors *
33
* *
44
* Licensed under the Apache License, Version 2.0 (the "License"); *
55
* you may not use this file except in compliance with the License. *
@@ -40,11 +40,19 @@ func WithOverrideStore(overrideStore ExperimentOverrideStore) CESOptionFunc {
4040
}
4141
}
4242

43+
// WithCmabService adds a CMAB service
44+
func WithCmabService(cmabService CmabService) CESOptionFunc {
45+
return func(f *CompositeExperimentService) {
46+
f.cmabService = cmabService
47+
}
48+
}
49+
4350
// CompositeExperimentService bridges together the various experiment decision services that ship by default with the SDK
4451
type CompositeExperimentService struct {
4552
experimentServices []ExperimentService
4653
overrideStore ExperimentOverrideStore
4754
userProfileService UserProfileService
55+
cmabService CmabService
4856
logger logging.OptimizelyLogProducer
4957
}
5058

@@ -53,7 +61,8 @@ func NewCompositeExperimentService(sdkKey string, options ...CESOptionFunc) *Com
5361
// These decision services are applied in order:
5462
// 1. Overrides (if supplied)
5563
// 2. Whitelist
56-
// 3. Bucketing (with User profile integration if supplied)
64+
// 3. CMAB (if experiment is a CMAB experiment)
65+
// 4. Bucketing (with User profile integration if supplied)
5766
compositeExperimentService := &CompositeExperimentService{logger: logging.GetLogger(sdkKey, "CompositeExperimentService")}
5867
for _, opt := range options {
5968
opt(compositeExperimentService)
@@ -68,6 +77,12 @@ func NewCompositeExperimentService(sdkKey string, options ...CESOptionFunc) *Com
6877
experimentServices = append([]ExperimentService{overrideService}, experimentServices...)
6978
}
7079

80+
// Add CMAB service if available
81+
if compositeExperimentService.cmabService != nil {
82+
cmabService := NewExperimentCmabService(compositeExperimentService.cmabService, logging.GetLogger(sdkKey, "ExperimentCmabService"))
83+
experimentServices = append(experimentServices, cmabService)
84+
}
85+
7186
experimentBucketerService := NewExperimentBucketerService(logging.GetLogger(sdkKey, "ExperimentBucketerService"))
7287
if compositeExperimentService.userProfileService != nil {
7388
persistingExperimentService := NewPersistingExperimentService(compositeExperimentService.userProfileService, experimentBucketerService, logging.GetLogger(sdkKey, "PersistingExperimentService"))

pkg/decision/entities.go

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/****************************************************************************
2-
* Copyright 2019-2021, Optimizely, Inc. and contributors *
2+
* Copyright 2019-2025, Optimizely, Inc. and contributors *
33
* *
44
* Licensed under the Apache License, Version 2.0 (the "License"); *
55
* you may not use this file except in compliance with the License. *
@@ -55,6 +55,8 @@ const (
5555
Rollout Source = "rollout"
5656
// FeatureTest - the decision came from a feature test
5757
FeatureTest Source = "feature-test"
58+
// Cmab - the decision came from a CMAB service
59+
Cmab Source = "cmab"
5860
)
5961

6062
// Decision contains base information about a decision
+97
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
/****************************************************************************
2+
* Copyright 2025, Optimizely, Inc. and contributors *
3+
* *
4+
* Licensed under the Apache License, Version 2.0 (the "License"); *
5+
* you may not use this file except in compliance with the License. *
6+
* You may obtain a copy of the License at *
7+
* *
8+
* http://www.apache.org/licenses/LICENSE-2.0 *
9+
* *
10+
* Unless required by applicable law or agreed to in writing, software *
11+
* distributed under the License is distributed on an "AS IS" BASIS, *
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
13+
* See the License for the specific language governing permissions and *
14+
* limitations under the License. *
15+
***************************************************************************/
16+
17+
// Package decision //
18+
package decision
19+
20+
import (
21+
"errors"
22+
"fmt"
23+
24+
"github.com/optimizely/go-sdk/v2/pkg/decide"
25+
"github.com/optimizely/go-sdk/v2/pkg/decision/reasons"
26+
"github.com/optimizely/go-sdk/v2/pkg/entities"
27+
"github.com/optimizely/go-sdk/v2/pkg/logging"
28+
)
29+
30+
// ExperimentCmabService makes decisions for CMAB experiments
31+
type ExperimentCmabService struct {
32+
cmabService CmabService
33+
logger logging.OptimizelyLogProducer
34+
}
35+
36+
// NewExperimentCmabService creates a new instance of ExperimentCmabService
37+
func NewExperimentCmabService(cmabService CmabService, logger logging.OptimizelyLogProducer) *ExperimentCmabService {
38+
return &ExperimentCmabService{
39+
cmabService: cmabService,
40+
logger: logger,
41+
}
42+
}
43+
44+
// GetDecision returns a decision for the given experiment and user context
45+
func (s *ExperimentCmabService) GetDecision(decisionContext ExperimentDecisionContext, userContext entities.UserContext, options *decide.Options) (decision ExperimentDecision, decisionReasons decide.DecisionReasons, err error) {
46+
decisionReasons = decide.NewDecisionReasons(options)
47+
experiment := decisionContext.Experiment
48+
projectConfig := decisionContext.ProjectConfig
49+
50+
// Check if experiment is nil or not a CMAB experiment
51+
if experiment == nil || !isCmab(*experiment) {
52+
message := "Not a CMAB experiment, skipping CMAB decision service"
53+
decisionReasons.AddInfo(message)
54+
return decision, decisionReasons, nil
55+
}
56+
57+
// Check if CMAB service is available
58+
if s.cmabService == nil {
59+
message := "CMAB service is not available"
60+
decisionReasons.AddInfo(message)
61+
return decision, decisionReasons, errors.New(message)
62+
}
63+
64+
// Get CMAB decision
65+
cmabDecision, err := s.cmabService.GetDecision(projectConfig, userContext, experiment.ID, options)
66+
if err != nil {
67+
message := fmt.Sprintf("Failed to get CMAB decision: %v", err)
68+
decisionReasons.AddInfo(message)
69+
return decision, decisionReasons, fmt.Errorf("failed to get CMAB decision: %w", err)
70+
}
71+
72+
// Find variation by ID
73+
for _, variation := range experiment.Variations {
74+
if variation.ID != cmabDecision.VariationID {
75+
continue
76+
}
77+
78+
// Create a copy of the variation to avoid memory aliasing
79+
variationCopy := variation
80+
decision.Variation = &variationCopy
81+
decision.Reason = reasons.CmabVariationAssigned
82+
message := fmt.Sprintf("User bucketed into variation %s by CMAB service", variation.Key)
83+
decisionReasons.AddInfo(message)
84+
return decision, decisionReasons, nil
85+
}
86+
87+
// If we get here, the variation ID returned by CMAB service was not found
88+
message := fmt.Sprintf("variation with ID %s not found in experiment %s", cmabDecision.VariationID, experiment.ID)
89+
decisionReasons.AddInfo(message)
90+
return decision, decisionReasons, fmt.Errorf("variation with ID %s not found in experiment %s", cmabDecision.VariationID, experiment.ID)
91+
92+
}
93+
94+
// isCmab is a helper method to check if an experiment is a CMAB experiment
95+
func isCmab(experiment entities.Experiment) bool {
96+
return experiment.Cmab != nil
97+
}

0 commit comments

Comments
 (0)